From c57580e6eb7a67abd8ee7043e79b1b156e329586 Mon Sep 17 00:00:00 2001 From: Paulo Date: Sun, 23 Aug 2026 09:36:33 +0200 Subject: [PATCH] Docs say app, not extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow the code rename through the prose. The concept is an app, the author guide moves to docs/writing-an-app.md (with its inbound links and the "app boundary" anchor updated), and every symbol matches the shipped surface: druks.apps, the App base class, /api/apps, druks create app. "application" folds into "app" — an app owns its workflows, subjects, and UI, so the two words no longer split. GitHub App stays fully qualified, and the CHANGELOG keeps its history verbatim. --- .druks/review/checklist.md | 4 +- AGENTS.md | 52 ++++---- CONTRIBUTING.md | 6 +- README.md | 25 ++-- deploy/README.md | 8 +- docs/concepts.md | 68 +++++----- docs/configuration.md | 54 ++++---- docs/connect-your-agent.md | 6 +- docs/development.md | 46 +++---- docs/full-local.md | 18 +-- docs/index.md | 16 +-- docs/releasing.md | 2 +- docs/troubleshooting.md | 22 ++-- ...ting-an-extension.md => writing-an-app.md} | 122 +++++++++--------- frontend/README.md | 26 ++-- 15 files changed, 237 insertions(+), 238 deletions(-) rename docs/{writing-an-extension.md => writing-an-app.md} (91%) diff --git a/.druks/review/checklist.md b/.druks/review/checklist.md index 9100afdb..a324be6d 100644 --- a/.druks/review/checklist.md +++ b/.druks/review/checklist.md @@ -70,7 +70,7 @@ English (ASD-STE100 register): - Active voice with a named actor: "Druks checks this at load", not "this is checked at load". - Plain words: use, not utilize; missing, not unimplemented. -- Error messages state the fact, then the fix, imperative: "extension 'x' +- Error messages state the fact, then the fix, imperative: "app 'x' declares subject Ledger without list_summaries(); the board calls it. Implement list_summaries() on Ledger." @@ -98,7 +98,7 @@ the code is wrong: fix the code, delete the explanation. and why it falls short — a second copy of one the repo already runs is the finding. -The author-facing surface (extensions/SDK) is the product, not the plumbing. +The author-facing surface (apps/SDK) is the product, not the plumbing. Design it by writing the example first: the obvious call is the correct one, the correct one is short, and a newcomer gets it right without reading the source or the docs. No exposed internals, no required boilerplate, no ceremony, no knowledge diff --git a/AGENTS.md b/AGENTS.md index 57191dea..bb703ab7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,17 +1,17 @@ # AGENTS.md -Druks runs durable agent applications on DBOS and Postgres. It owns +Druks runs durable agent apps on DBOS and Postgres. It owns workflow execution, persisted state and events, gates, webhooks, sandbox access, -and the shared dashboard. Apps are **extensions**: standalone Python packages -that self-register through the `druks.extensions` entry point. `ship` is the -bundled reference extension for coordinating coding agents through GitHub PRs. +and the shared dashboard. Apps are standalone Python packages +that self-register through the `druks.apps` entry point. `ship` is the +bundled reference app for coordinating coding agents through GitHub PRs. ## Read map Start with `README.md`, then read only the material relevant to the task: - Workflow lifecycle, state, replay, or recovery: `docs/concepts.md`. -- Extension contracts or the public author surface: `docs/writing-an-extension.md`. +- App contracts or the public author surface: `docs/writing-an-app.md`. - Configuration or environment variables: `docs/configuration.md`. - Local install and operations: `docs/full-local.md`. - Remote deployment: `deploy/README.md`. @@ -22,34 +22,34 @@ Start with `README.md`, then read only the material relevant to the task: - Documentation navigation and audience ownership: `docs/index.md`. - The checklist and craft gate every change is held to: `.druks/review/checklist.md`. -For extension-surface changes, inspect the proof extension at +For app-surface changes, inspect the proof app at `backend/tests/druks-field_notes/` and its tests as well as the author guide. ## Architectural boundaries -- Keep platform and extension ownership explicit. GitHub issue, branch, PR, and +- Keep platform and app ownership explicit. GitHub issue, branch, PR, and coding-agent policy belongs to `ship`, not to Druks core. - Describe durability precisely: completed durable checkpoints are reused when orchestration replays, but an interrupted operation may run again. Do not imply arbitrary-line resume or exactly-once external side effects. - `Run.state` is derived from DBOS workflow status. Do not add a second writable state mirror. -- Extension authors import the public concern namespaces documented in - `docs/writing-an-extension.md`, not Druks internals. -- Backend extension discovery is runtime packaging. Shared-dashboard extension UI - registration is a compile-time frontend concern. Standalone extensions may ship +- App authors import the public concern namespaces documented in + `docs/writing-an-app.md`, not Druks internals. +- Backend app discovery is runtime packaging. Shared-dashboard app UI + registration is a compile-time frontend concern. Standalone apps may ship their own `dist/`; do not conflate the two delivery paths. - Druks owns generic agent, harness, workspace, sandbox, event, gate, webhook, and - settings plumbing. Domain-specific policy stays in the extension. -- The author surface grows by parameter, not by namespace. When an extension needs + settings plumbing. Domain-specific policy stays in the app. +- The author surface grows by parameter, not by namespace. When an app needs something the SDK lacks, widen the primitive that already owns the concern — a keyword argument, a method on the class holding the data. Do not add a namespace, a facade, a context object, or a helper module whose only justification is that the call site would read shorter. -- No author-surface module imports an extension. `druks.workflows`, `druks.agents`, +- No author-surface module imports an app. `druks.workflows`, `druks.agents`, `druks.events`, `druks.signals`, `druks.db`, `druks.schemas`, `druks.prompts`, - `druks.durable`, `druks.extensions`, and `druks.webhooks` are what an author imports; - a reference to `druks.build` or any other extension inside them inverts the platform. + `druks.durable`, `druks.apps`, and `druks.webhooks` are what an author imports; + a reference to `druks.build` or any other app inside them inverts the platform. - Liveness — is this subject still being worked — derives from run state; never mirror it in a column. An outcome somebody else owns, such as whether a pull request was merged, is stored when its owner announces it — never inferred from run lifecycle, @@ -63,20 +63,20 @@ For extension-surface changes, inspect the proof extension at through its schedule overrides rather than a settings column. - A contract is one canonical name and shape that fails loudly on anything else. Do not accept two spellings of the same thing. -- Extension code does not type-switch over a typed stream. When a projection needs +- App code does not type-switch over a typed stream. When a projection needs ordering or anchoring, grow the SDK primitive instead of an `isinstance` chain. - A read-side field carries identity and facts — gate name, kind, reason code. UI - wording lives in the extension's own pages, never on the wire. + wording lives in the app's own pages, never on the wire. - A shared resource gets one global registry delivered everywhere. Add a scoping axis when a second consumer needs a different answer, not in anticipation. ## Layout - `backend/druks/` — FastAPI, DBOS, SQLAlchemy 2.0, Pydantic v2, and bundled - extensions. + apps. - `backend/migrations/` — platform Alembic migrations. - `backend/tests/` — pytest suite backed by real Postgres. -- `backend/tests/druks-field_notes/` — independently packaged proof extension. +- `backend/tests/druks-field_notes/` — independently packaged proof app. - `frontend/` — React 19 and Vite shared SPA; production output is repository-root `dist/` and is copied into the backend image. - `deploy/` — Compose files, the bind-mounted Caddy configuration, and sandbox @@ -89,7 +89,7 @@ For extension-surface changes, inspect the proof extension at Backend tests need Postgres on `localhost:5432` with user and password `druks`, and the `druks_test` database. `DRUKS_TEST_DATABASE_URL` overrides it — -`DRUKS_DATABASE_URL` is the application's and the suite never reads it. DBOS +`DRUKS_DATABASE_URL` is the runtime's and the suite never reads it. DBOS integration tests also read `DRUKS_TEST_PG`. Start the development database with: @@ -106,11 +106,11 @@ uv pip install -e backend/tests/druks-field_notes uv run pytest backend/ ``` -The suite collects the proof extension, so `pytest backend/` fails at collection +The suite collects the proof app, so `pytest backend/` fails at collection until that editable install has run. -If the public extension surface changed, also install and exercise the proof -extension as described in `docs/development.md`. +If the public app surface changed, also install and exercise the proof +app as described in `docs/development.md`. Run the frontend gates: @@ -121,11 +121,11 @@ npm --prefix frontend run build ``` The PR workflows in `.github/workflows/on-pull-request-*.yml` are the source of -truth for CI, including the proof-extension install phase. +truth for CI, including the proof-app install phase. ## Documentation discipline -- Put product behavior, setup, operations, troubleshooting, and extension author +- Put product behavior, setup, operations, troubleshooting, and app author contracts in the appropriate public guide. Keep this file limited to task routing, architectural boundaries, and contributor rules. - Link to one canonical explanation instead of copying it into multiple pages. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 698dcf2e..881d8853 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,14 +1,14 @@ # Contributing to Druks Druks is alpha software. Discussion before a large change is useful because the -public extension surface and deployment model are still moving. +public app surface and deployment model are still moving. ## Before opening a pull request 1. Search existing issues and open one for behavior changes or substantial work. 2. Read [the development guide](docs/development.md) and the relevant concept, - operator, or extension-author guide. -3. Keep platform behavior separate from application-specific extension policy. + operator, or app-author guide. +3. Keep platform behavior separate from app-specific policy. 4. Add focused tests for behavior changes and update the canonical public guide when a contract changes. diff --git a/README.md b/README.md index 0ba31c24..b618dd8a 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ described in [the release process](https://github.com/czpython/druks/blob/main/d Re-running is also the upgrade path. Then follow [full local setup](https://github.com/czpython/druks/blob/main/docs/full-local.md) to finish in the dashboard: connect the agent harnesses and the GitHub App the bundled -`ship` extension acts through; a standalone extension may have different +`ship` app acts through; a standalone app may have different integration requirements. Or hand the install to a coding agent — paste this into Claude Code, Codex, @@ -72,15 +72,15 @@ set them in `druks.toml` and re-run the same command. See the for prerequisites, access control, verification, and rollback. ```text -trigger ──> extension workflow ──> durable step ──> agent ──> sandbox +trigger ──> app workflow ──> durable step ──> agent ──> sandbox │ │ │ │ │ └─ Claude or Codex harness │ └─ result checkpointed in Postgres - ├─ event ──> feed / extension reaction + ├─ event ──> feed / app reaction └─ gate ──> wait for human or external system ──> resume ``` -**Platform and applications stay separate** +**Platform and apps stay separate** Druks owns the execution and operating substrate: @@ -89,24 +89,23 @@ Druks owns the execution and operating substrate: - Claude and Codex harness dispatch through isolated Drukbox sandboxes - append-only events, live feeds, webhooks, notifications, MCP servers, and skills - validated operator settings, encrypted MCP/OAuth secrets, and the dashboard shell -- extension discovery, API namespaces, and independent migration histories +- app discovery, API namespaces, and independent migration histories -An **extension** owns the application: its workflows, agents, domain models, -routes, events, provider reactions, and optional dashboard pages. It is a normal -Python distribution registered through the `druks.extensions` entry-point -group. Installing the distribution registers it; Druks does not need an -extension-specific plugin list. +An **app** owns its workflows, agents, domain models, routes, events, provider +reactions, and optional dashboard pages. It is a normal Python distribution +registered through the `druks.apps` entry-point group. Installing the +distribution registers it; Druks does not need an app-specific plugin list. Scaffold one with the published CLI, no checkout required: ```bash -uvx --from druks druks create extension night_watch +uvx --from druks druks create app night_watch ``` The generated project root carries an `AGENTS.md` with the contracts and a link to the authoring guide. -The bundled `ship` extension is a concrete example. It coordinates coding +The bundled `ship` app is a concrete example. It coordinates coding agents through tickets and GitHub pull requests, but GitHub PR orchestration is `ship` behavior—not the definition of Druks. @@ -116,7 +115,7 @@ agents through tickets and GitHub pull requests, but GitHub PR orchestration is - **Installing locally:** [Full local setup](https://github.com/czpython/druks/blob/main/docs/full-local.md) - **Operating a remote stack:** [Deployment runbook](https://github.com/czpython/druks/blob/main/deploy/README.md) - **Configuring integrations and secrets:** [Configuration](https://github.com/czpython/druks/blob/main/docs/configuration.md) -- **Building an application:** [Writing an extension](https://github.com/czpython/druks/blob/main/docs/writing-an-extension.md) +- **Building an app:** [Writing an app](https://github.com/czpython/druks/blob/main/docs/writing-an-app.md) - **Diagnosing a run or service:** [Troubleshooting](https://github.com/czpython/druks/blob/main/docs/troubleshooting.md) - **Contributing to Druks:** [Contribution guide](https://github.com/czpython/druks/blob/main/CONTRIBUTING.md) - **Reporting a vulnerability:** [Security policy](https://github.com/czpython/druks/blob/main/SECURITY.md) diff --git a/deploy/README.md b/deploy/README.md index 5af59788..6d026042 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -46,7 +46,7 @@ peers). Other remote providers have their own network and credential requirements; the local Docker shape is covered in [Full local](../docs/full-local.md). -The Druks application and sandbox images are published for both `linux/amd64` +The Druks service and sandbox images are published for both `linux/amd64` and `linux/arm64`. Everything else — `compose.yaml`, the Caddyfile, `druks.toml`, the rendered @@ -225,12 +225,12 @@ Caddyfile fetched by the installer) enforces path-level access: - `POST /_external/*` — public, authenticated by the matching webhook class in Druks. Per-provider paths land under - `/_external///`; extension role-module discovery + `/_external///`; app role-module discovery registers them at import time. - `/mcp` — public, authenticated per request by personal access token inside - the app; proxied unbuffered so its SSE frames stream. + Druks; proxied unbuffered so its SSE frames stream. - Everything else — a nonempty trusted identity header (exe.dev login provides one) required, then proxied to `web` (`127.0.0.1:8001`), which - serves the API, the SPA, and extension frontends alike; the app maps that + serves the API, the SPA, and app frontends alike; Druks maps that asserted email to your account per request ([access control](../docs/configuration.md#public-urls-and-access-control)). diff --git a/docs/concepts.md b/docs/concepts.md index b9ddd275..b356b5f9 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -2,35 +2,35 @@ ## The problem Druks solves -Agent applications routinely cross boundaries a request handler should not: +Agent apps routinely cross boundaries a request handler should not: they call slow models, provision machines, wait for people, react to webhooks, and run for longer than a process or deploy. Retrying the whole script is expensive and can repeat side effects; keeping one process alive indefinitely is not a recovery strategy. -Druks separates the durable control flow from the application. DBOS records -workflow progress in Postgres. Druks layers application-facing workflows, -agents, gates, subjects, events, settings, and extension loading on top, then -exposes their state through an API and dashboard. +Druks separates durable control flow from app code. DBOS records workflow +progress in Postgres. Druks layers workflows, agents, gates, subjects, events, +settings, and app loading on top, then exposes their state through an API and +dashboard. -## The extension boundary +## The app boundary -An extension is an independently packaged application installed into the same -Python environment as Druks. Its package registers an `Extension` subclass: +An app is an independently packaged Python distribution installed into the same +environment as Druks. Its package registers an `App` subclass: ```toml -[project.entry-points."druks.extensions"] -night_watch = "druks_night_watch.extension:NightWatch" +[project.entry-points."druks.apps"] +night_watch = "druks_night_watch.app:NightWatch" ``` -At boot Druks resolves installed entry points, imports each extension's models +At boot Druks resolves installed entry points, imports each app's models and role modules, and mounts its routes under `/api/`. The entry-point -name must match `Extension.name`. The same name scopes: +name must match `App.name`. The same name scopes: - the API namespace - the default `_` table prefix -- the extension's Alembic version table -- extension setting keys +- the app's Alembic version table +- app setting keys ### Druks owns @@ -38,18 +38,18 @@ name must match `Extension.name`. The same name scopes: - agent descriptors, Claude/Codex harness dispatch, and sandbox access - subject timelines, the event feed, signals, webhook dispatch, and notifications - MCP and skill delivery, settings, MCP secret encryption, and diagnostics -- the FastAPI application, shared dashboard shell, and extension loading +- the FastAPI server, shared dashboard shell, and app loading -### An extension owns +### An app owns - domain workflows, what each one is about, and the policy for starting them - agents, prompts, and structured output contracts - domain models, migrations, HTTP routes, and subject summaries - normalized reactions to events and provider-specific webhook behavior -- any provider credentials or prerequisites specific to that application -- optional static frontend assets shipped in the extension package +- any provider credentials or prerequisites specific to its domain +- optional static frontend assets shipped in the app package -The bundled `ship` extension owns projects, work items, ticket intake, GitHub +The bundled `ship` app owns projects, work items, ticket intake, GitHub branches and pull requests, coding-agent policy, and its dashboard pages. Those are useful examples, not platform guarantees. @@ -80,23 +80,23 @@ sandbox; recovery follows the operation boundary above. ## When Druks fits -Druks is for applications whose work crosses process lifetimes: several +Druks is for apps whose work crosses process lifetimes: several durable operations, agent calls in isolated hosts, external triggers, or waits for people and systems. It is especially useful when several independently -packaged applications should share one execution and operating substrate. +packaged apps should share one execution and operating substrate. It is not an agent model SDK, a sandbox provider, or a reason to wrap a single short model call in a workflow. Drukbox owns host provisioning, and an -extension still owns domain policy and side-effect idempotency. +app still owns domain policy and side-effect idempotency. ### State has one lifecycle owner The `durable_runs` row stores the Druks-owned facts DBOS has no slot for: the current gate ask, the failure text, and timestamps. The run's subject lives on the DBOS workflow itself as custom attributes, so "runs for this subject" is -answered by `workflow_status` alone. The run's extension is not stored at all — +answered by `workflow_status` alone. The run's app is not stored at all — it is workflow-class metadata, derivable from the run's `kind` through the -extension registry. The row's lifecycle state is read-only and derived from +app registry. The row's lifecycle state is read-only and derived from DBOS's workflow status: ```text @@ -138,9 +138,9 @@ These terms describe different ownership layers: | Layer | Responsibility | | --- | --- | -| Agent | Extension-owned prompt, output contract, and default model/settings | +| Agent | App-owned prompt, output contract, and default model/settings | | Harness | Platform adapter that invokes the Claude or Codex CLI for a model | -| Workspace | Extension customization of what a call receives, such as a cloned repository | +| Workspace | App customization of what a call receives, such as a cloned repository | | Sandbox | Drukbox-provisioned isolated host where the harness process runs | | Provider | Any Drukbox backend name that supplies the host; `docker` and `exe` select install shapes | @@ -153,27 +153,27 @@ provider-specific execution code. By default, each agent call uses an ephemeral sandbox. A workflow can retain one warm sandbox across a segment, but Druks releases it before a gate and at workflow exit and rotates it before its lease is too short for another call. -Application state should live in a durable external system such as Git rather +Durable state should live in an external system such as Git rather than only on the VM. ## Events, signals, webhooks, and subjects -A subject is what a run is about, and it is always a class — a row the extension +A subject is what a run is about, and it is always a class — a row the app keeps, or identity alone. The workflow declares which, so druks knows the kind of thing before any run about it exists; only the subject's type and id travel further, because a run outlives what it points at. Everything that happens to a -run lands in an append-only event log. Extensions add their own events and give +run lands in an append-only event log. Apps add their own events and give their subject class a summary; druks supplies pagination, activity composition, and a live feed of facts — a subject's words are the client's. -Signals connect producers to extension reactions. They are awaited and +Signals connect producers to app reactions. They are awaited and delivered at least once: webhook failures return an error so the provider can redeliver, while durable lifecycle publishers retry. Subscribers must therefore be idempotent. Webhook classes authenticate and normalize provider deliveries before publishing signals. The framework supplies routing and deduplication; an -extension or integration owns the provider payload and domain reaction. +app or integration owns the provider payload and domain reaction. ## Settings and capabilities delivered to agents @@ -181,7 +181,7 @@ Configuration has two planes: - `druks.toml` configures the deployment and renders the process environment - Postgres-backed settings configure operator profile, harness defaults, - extension/workflow knobs, per-agent overrides, notifications, MCP servers, + app/workflow knobs, per-agent overrides, notifications, MCP servers, and skills Stored MCP tokens and OAuth grants are encrypted at rest. They are decrypted @@ -195,14 +195,14 @@ treated as credential access. Enabled MCP servers are injected through both harnesses. A call receives the enabled skills it requests, or every enabled skill when it requests none. A -workspace may also require and credential an MCP server for its own application. +workspace may also require and credential an MCP server for the app. Each agent call records what was declared and delivered so later evaluation can distinguish capability sets without storing the tokens. ## Process and access topology The shipped `web` process serves FastAPI, the SPA, DBOS workflows, and schedules. -Postgres stores application and DBOS state. Redis stores short-lived +Postgres stores app and DBOS state. Redis stores short-lived coordination such as webhook deduplication, OAuth state/token caches, and the sandbox provisioning gate. Drukbox provisions sandbox hosts; Druks then reaches them over SSH. diff --git a/docs/configuration.md b/docs/configuration.md index 4d0922f8..b037065e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -59,8 +59,8 @@ Secrets are generated only when the TOML is first created. Preserve | Variable | Default | Purpose | | --- | --- | --- | -| `DRUKS_DATABASE_URL` | local `druks` Postgres | Application and DBOS database | -| `DRUKS_TEST_DATABASE_URL` | local `druks_test` Postgres | What the shipped pytest fixtures use — never the application's | +| `DRUKS_DATABASE_URL` | local `druks` Postgres | Runtime and DBOS database | +| `DRUKS_TEST_DATABASE_URL` | local `druks_test` Postgres | What the shipped pytest fixtures use — never the runtime's | | `DRUKS_TEST_REDIS_URL` | `redis://127.0.0.1:6379/15` | What the shipped pytest fixtures flush | | `DRUKS_REDIS_URL` | `redis://127.0.0.1:6379/0` | Short-lived coordination and caches | | `DRUKS_DATA_DIR` | `/var/lib/druks` | Logs, artifacts, installed skills | @@ -158,28 +158,28 @@ second. Agents consume the API through the MCP endpoint; see ## GitHub -Druks acts at GitHub as one **operator App** — its service identity. The App -receives webhooks and performs application-owned writes such as branches, +Druks acts at GitHub as one **operator GitHub App** — its service identity. The +GitHub App receives webhooks and performs domain writes such as branches, pull requests, comments, labels, and merges. Its credentials live encrypted in Postgres; there is no TOML, environment, or PEM-file source — until GitHub is connected, agent runs refuse with a pointed message and `druks doctor` reports the identity as not connected. Connect it from **Settings → Services**. **Create GitHub App** registers the -App through GitHub's manifest flow: name a GitHub org (or leave it empty for -a personal account), confirm on GitHub, and druks stores the created App's -credentials and sends you on to install it on your repositories. Creating the -App needs `urls.endpoint` set to the base URL the operator's browser reaches -druks at, and the webhook lands on `urls.webhook_host` when configured, the -endpoint host otherwise. - -Alternatively paste an existing App's credentials into the same card: the App -ID, the PEM private key exactly as GitHub issued it, and the webhook secret. +GitHub App through GitHub's manifest flow: name a GitHub org (or leave it empty +for a personal account), confirm on GitHub, and druks stores the created +GitHub App's credentials and sends you on to install it on your repositories. +Creating the GitHub App needs `urls.endpoint` set to the base URL the operator's browser +reaches druks at, and the webhook lands on `urls.webhook_host` when configured, +the endpoint host otherwise. + +Alternatively paste an existing GitHub App's credentials into the same card: the +GitHub App ID, the PEM private key exactly as GitHub issued it, and the webhook secret. Connecting validates the pasted credentials against GitHub and stores the -App's slug; from then on every operator client resolves from that row and +GitHub App's slug; from then on every operator client resolves from that row and webhook deliveries verify against its stored secret. -Registering the App by hand instead: +Registering the GitHub App by hand instead: Webhook URL: `https:///_external/github/events/` @@ -195,25 +195,25 @@ Subscribe to issue comment, pull request, pull request review, and push events. | Checks | Read | | Commit statuses | Read | -Install the App on the repositories Druks should work in; that installation +Install the GitHub App on the repositories Druks should work in; that installation set is where `ship` may act. Personal access tokens are not a supported substitute. **Upgrading an existing installation** is a one-time paste on each live box after rollout: open Settings → Services and connect GitHub with the existing -operator App's ID, private key, and webhook secret. Do not create a -replacement App — the current App's webhook and installations keep working -under the pasted credentials. +operator GitHub App's ID, private key, and webhook secret. Do not create a +replacement GitHub App — the current GitHub App's webhook and installations +keep working under the pasted credentials. ### Review identity (optional) -The bundled `review` extension can post its verdict reviews as a second +The bundled `review` app can post its verdict reviews as a second GitHub App, so GitHub accepts approvals on Druks-authored pull requests. -Configure it in **Settings → Review**: the review App ID and its PEM private -key, both stored encrypted and empty-as-unset. Leave the pair empty and +Configure it in **Settings → Review**: the review GitHub App ID and its PEM +private key, both stored encrypted and empty-as-unset. Leave the pair empty and reviews publish as operator comments; setting both flips reviews to distinct -approving reviews. The review App needs read access to metadata and contents, -read/write access to pull requests, and no webhook. +approving reviews. The review GitHub App needs read access to metadata and +contents, read/write access to pull requests, and no webhook. `GITHUB_API_URL` defaults to `https://api.github.com` and can point every client at another compatible GitHub API endpoint. @@ -225,7 +225,7 @@ secret) or Jira Cloud (base URL, email, API token, webhook secret) from **Settings → Services**, on the same cards as the GitHub App. Connecting verifies the credentials against the tracker before anything is stored. Which tracker drives `ship` work — and the statuses that trigger or move it — stays a -ship extension setting in **Settings → Ship**. +ship app setting in **Settings → Ship**. Webhook URLs remain `/_external/linear/events/` and `/_external/jira/events/`. The Jira webhook is a Jira Automation "Send web @@ -353,7 +353,7 @@ is one of: - token read from a named process environment variable - OAuth connection, which requires `urls.endpoint` -Enabled servers are delivered to both harnesses unless an extension workspace +Enabled servers are delivered to both harnesses unless an app workspace owns a required server with the same name. Tokens enter the agent environment under a derived variable and are never returned by the API. @@ -395,5 +395,5 @@ The encryption envelope does **not** currently cover harness subscription payloads or notification webhook URLs. They are stored as ordinary Postgres fields, although APIs withhold or mask their values. Treat access to Postgres and its backups as access to those credentials. GitHub App -private keys — the operator identity's and the review extension's — are +private keys — the operator identity's and the review app's — are database values under the envelope, no longer files mounted into the process. diff --git a/docs/connect-your-agent.md b/docs/connect-your-agent.md index fa6fe0e8..16971f74 100644 --- a/docs/connect-your-agent.md +++ b/docs/connect-your-agent.md @@ -3,7 +3,7 @@ Druks serves an MCP endpoint at `/mcp` (streamable HTTP, stateless). Its tools are derived from the agent-tagged API routes. The platform contributes seven — `list_open_subjects`, `get_gate`, `answer_gate`, `get_agent_call`, -`cancel_run`, `retry_run`, `get_usage` — and each installed extension +`cancel_run`, `retry_run`, `get_usage` — and each installed app contributes its own verbs beside them; `tools/list` is the live catalog. Every request authenticates with a personal access token sent as `Authorization: Bearer `. @@ -42,7 +42,7 @@ bearer_token_env_var = "DRUKS_PAT" - **Discovery first.** There is no push channel. Call `list_open_subjects` first and poll it about every 30 seconds while waiting. Each workflow's `run` feeds the gate and run tools, and `latestAgentCall` feeds `get_agent_call`; - An extension's verbs open work whose run enters the same flow. + an app's verbs open work whose run enters the same flow. Call `get_gate` before `answer_gate` and echo its `parkedAt` value unchanged; it names the exact question being answered, and a repeat answer to the same `parkedAt` reports `already_answered` instead of @@ -53,6 +53,6 @@ bearer_token_env_var = "DRUKS_PAT" - **Stable error shapes.** Gateway and run tool failures embed the agent routes' `{"code", "message", "retryable"}` body in their error text — the codes (`GATE_ROUND_STALE`, `RUN_NOT_ACTIVE`, …) are stable and safe to match - on. Extension verbs use the API's `{"error", "detail"}` shape for their + on. App verbs use the API's `{"error", "detail"}` shape for their refusals. Requests that fail shape validation carry `VALIDATION_ERROR` detail instead. diff --git a/docs/development.md b/docs/development.md index 54a30176..9936f799 100644 --- a/docs/development.md +++ b/docs/development.md @@ -27,23 +27,23 @@ runs with `[identity].mode = "none"`: the loopback dashboard has no authentication and exactly one operator account, created by your first harness connection. To exercise `header` mode against the dev server, set `identity.mode = "header"` and `identity.header` in `druks.toml`, then send the -header yourself (for example with a browser header extension or +header yourself (for example with a browser header add-on or `curl -H 'X-Edge-Email: you@example.com'`). The dev Compose project creates two databases: -- `druks_dev` for the host-run application +- `druks_dev` for the host-run server - `druks_test` for pytest, which rebuilds its schema during the suite -`.env.example` points the application at `druks_dev`. The suite reaches +`.env.example` points the server at `druks_dev`. The suite reaches `druks_test` and Redis index 15 through `DRUKS_TEST_DATABASE_URL` and -`DRUKS_TEST_REDIS_URL`, never through the application's own settings, so the two +`DRUKS_TEST_REDIS_URL`, never through the server's own settings, so the two cannot be confused. Start the backend: ```bash -uv run uvicorn druks.api.app:app --host 127.0.0.1 --port 8001 +uv run uvicorn druks.api.server:app --host 127.0.0.1 --port 8001 ``` In another terminal: @@ -63,34 +63,34 @@ contains the built SPA and serves it from FastAPI. | `backend/druks/workflows.py` | Public workflow, step, gate, scheduling, and start API | | `backend/druks/agents.py` | Public agent descriptor and output contract | | `backend/druks/durable/` | DBOS integration, run projection, lifecycle internals | -| `backend/druks/extensions/` | Entry-point loading, discovery, author settings | +| `backend/druks/apps/` | Entry-point loading, discovery, author settings | | `backend/druks/events/`, `signals.py` | Event log, feed, and reactions | | `backend/druks/webhooks/` | Authenticated delivery framework and deduplication | | `backend/druks/harnesses/` | Claude/Codex invocation, auth, usage, capability manifests | | `backend/druks/sandbox/` | Drukbox lifecycle, SSH execution, workspace delivery | | `backend/druks/api/` | FastAPI composition and platform routes | | `backend/druks/{mcp,skills,notifications,user_settings}/` | Shared operator services | -| `backend/druks/contrib/ship/` | Bundled reference extension, not framework core | -| `frontend/src/` | Shared dashboard shell and bundled extension UI | +| `backend/druks/contrib/ship/` | Bundled reference app, not framework core | +| `frontend/src/` | Shared dashboard shell and bundled app UI | | `backend/migrations/` | Core/bundled schema history | | `deploy/`, `scripts/` | Images, Compose, Caddy, setup, and deployment | -The API process embeds DBOS and executes workflows. Extension modules register +The API process embeds DBOS and executes workflows. App modules register capabilities during boot, after DBOS initialization and before launch. -## Extension test surface +## App test surface -The main package registers bundled extensions through `pyproject.toml`. CI also +The main package registers bundled apps through `pyproject.toml`. CI also installs `backend/tests/druks-field_notes` as a real editable distribution and -runs the proof-extension tests. Those tests are the executable contract for: +runs the proof-app tests. Those tests are the executable contract for: -- app-less and boot-time entry-point loading +- headless and boot-time entry-point loading - role-module discovery - route and subject read-side mounting - independent migrations and table-prefix enforcement - workflow start, settings, and feed formatting -When changing the author API, update the scaffold, proof extension, author +When changing the author API, update the scaffold, proof app, author guide, and tests together. ## Database changes @@ -102,14 +102,14 @@ uv run alembic -c backend/alembic.ini revision --autogenerate -m "describe chang uv run druks init-db ``` -For an independently packaged extension: +For an independently packaged app: ```bash -uv run druks makemigrations -m "describe change" +uv run druks makemigrations -m "describe change" uv run druks init-db ``` -The extension owns its migration directory and version table. Review every +The app owns its migration directory and version table. Review every autogenerated revision before applying it. ## Verification @@ -123,9 +123,9 @@ uv run ruff format --check backend uv run pytest backend/ ``` -The suite builds its subjects out of `field_notes`, the proof extension. It is a +The suite builds its subjects out of `field_notes`, the proof app. It is a standalone distribution that depends on druks, so it installs like any author's -extension rather than being a dependency of druks — install it once and the whole +app rather than being a dependency of druks — install it once and the whole suite runs. The pull-request backend workflow does the same. Pyright is configured for local/editor use but is not currently a CI gate. @@ -167,12 +167,12 @@ not part of the normal test suite. ## Frontend ownership -Backend extension entry points and shared-shell React routes have different -delivery mechanisms. Python discovery can load an installed extension at -runtime. An extension can ship a standalone static app in its package's +Backend app entry points and shared-shell React routes have different +delivery mechanisms. Python discovery can load an installed app at +runtime. An app can ship a standalone static frontend in its package's `dist/`, served at `/app/`. React code that joins the bundled dashboard shell must already be in the SPA and register through -`frontend/src/extensions/index.ts`; a wheel cannot inject routes into that +`frontend/src/apps/index.ts`; a wheel cannot inject routes into that existing JavaScript bundle. See the [frontend guide](../frontend/README.md) before adding dashboard pages. diff --git a/docs/full-local.md b/docs/full-local.md index 1cf7e34f..c8a49edc 100644 --- a/docs/full-local.md +++ b/docs/full-local.md @@ -20,7 +20,7 @@ runs in those isolated containers rather than in the Druks process. short-lived sandbox containers No Tailscale account or remote VM provider is needed. -The Druks application and sandbox images are published for both `linux/amd64` +The Druks service and sandbox images are published for both `linux/amd64` and `linux/arm64`. ## 1. Install the local Druks profile @@ -47,9 +47,9 @@ user may use it. Drukbox keeps its schema in a `drukbox` database in the same Postgres — no separate datastore. On macOS, if sandbox SSH turns out unreachable, enable host networking in Docker Desktop's settings. -For the bundled `ship` extension, connect the GitHub App druks acts as from +For the bundled `ship` app, connect the GitHub App druks acts as from the dashboard after boot (**Settings → Services**) — create it there or -paste an existing App's credentials, see +paste an existing GitHub App's credentials, see [the GitHub connection](configuration.md#github). Existing installs that still run Drukbox on the host via `make dev`: finish @@ -116,22 +116,22 @@ save the replacement profile. Cancel destroys the disposable sandbox without changing the saved state. A web-process restart also destroys open login windows; reopen the window after Druks returns. -To verify the complete path, run an application workflow that borrows the +To verify the complete path, run an app workflow that borrows the session after saving and confirm its browser opens the authenticated site. Saving a login window always stores `profile_dir`, including when the session was originally imported as Playwright `storage_state`. -## 5. Exercise an application +## 5. Exercise an app -Druks does not invent a generic domain job: an installed extension supplies the +Druks does not invent a generic domain job: an installed app supplies the workflow and its trigger. In the bundled distribution, `ship` is the reference -application. Register a project in its dashboard and use its configured ticket +app. Register a project in its dashboard and use its configured ticket or GitHub trigger. Watch the run appear in the subject page and Events feed; agent-call pages stream transcript and artifact data. -If you are developing a different extension, install that distribution into a +If you are developing a different app, install that distribution into a development Druks environment and invoke its documented trigger or -`Workflow.start()` path. See [writing an extension](writing-an-extension.md). +`Workflow.start()` path. See [writing an app](writing-an-app.md). ## Sandbox image diff --git a/docs/index.md b/docs/index.md index ac8ea169..7a43fdf6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,12 +1,12 @@ # Druks documentation -Druks has three distinct audiences: operators run the platform, extension -authors build applications on it, and contributors change Druks itself. Start +Druks has three distinct audiences: operators run the platform, app +authors build apps on it, and contributors change Druks itself. Start with the route that matches what you are doing. ## Understand the platform -- [Concepts and guarantees](concepts.md) — platform versus extension ownership, +- [Concepts and guarantees](concepts.md) — platform versus app ownership, durable execution, recovery, gates, events, harnesses, sandboxes, and the access boundary. - [README](../README.md) — short project overview and installation entry point. @@ -25,12 +25,12 @@ with the route that matches what you are doing. - [Troubleshooting](troubleshooting.md) — symptom-driven diagnosis for boot, webhooks, harnesses, sandboxes, gates, and recovery. -## Build an extension +## Build an app -- [Writing an extension](writing-an-extension.md) — scaffold a separately - packaged application and use workflows, agents, gates, events, webhooks, +- [Writing an app](writing-an-app.md) — scaffold a separately + packaged app and use workflows, agents, gates, events, webhooks, settings, routes, and migrations. -- [Concepts and guarantees](concepts.md#the-extension-boundary) — the ownership +- [Concepts and guarantees](concepts.md#the-app-boundary) — the ownership contract behind the author API. ## Contribute @@ -45,7 +45,7 @@ with the route that matches what you are doing. - [Open-source cut](open-source-cut.md) — one-time clean-history publication and public repository settings. - [Frontend guide](../frontend/README.md) — dashboard shell, compile-time - extension UI registry, and frontend commands. + app UI registry, and frontend commands. The repository intentionally uses plain Markdown rather than a documentation framework. The pages above are the navigation; internal research and temporary diff --git a/docs/releasing.md b/docs/releasing.md index d760105e..ba4500fc 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -9,7 +9,7 @@ publishes the matching version tag and the immutable SHA tag; it does not move ## Prepare a release 1. Start from a clean checkout of the commit to release. -2. Run the complete backend, proof-extension, frontend, package, secret, and +2. Run the complete backend, proof-app, frontend, package, secret, and workflow checks from [Development](development.md#verification). 3. Review migrations and workflow replay compatibility. A container rollback does not downgrade Postgres or DBOS state. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 7dcd29fe..85cd3174 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -11,8 +11,8 @@ docker compose logs --tail=200 web `druks doctor` checks settings, secrets, GitHub App credentials and installations, optional ticketing integrations, data-directory writes, -Postgres, Redis, Drukbox, harness connections, extension imports, capability -module names, and extension-owned checks. A failed check exits nonzero. +Postgres, Redis, Drukbox, harness connections, app imports, capability +module names, and app-owned checks. A failed check exits nonzero. Use the opt-in sandbox check only when the normal Drukbox check passes but real execution fails: @@ -119,7 +119,7 @@ expects HTTP 401 from Druks. A different response means the request did not reach the webhook verifier. Webhook delivery is deduplicated in Redis. A handler failure releases the claim -and returns an error so the provider can redeliver. Extension subscribers must +and returns an error so the provider can redeliver. App subscribers must be idempotent. ## An agent cannot reach `/mcp` @@ -191,7 +191,7 @@ code. Cancelling a parked run clears the ask and frees its subject slot. - `failed`: the workflow raised; inspect its failure text, last agent call, and transcript/stderr. -- `cancelled`: an operator or application reaction asked DBOS to stop it. +- `cancelled`: an operator or app reaction asked DBOS to stop it. - `orphaned`: the Druks run row exists, but its DBOS workflow row has been missing for more than five minutes. It cannot resume. @@ -213,20 +213,20 @@ If two starts for the same subject return the same run id, deduplication is working: only one active run per workflow kind and subject is allowed. Cancel or finish the active run before expecting a new id. -## An extension does not load +## An app does not load Run `druks doctor` and inspect the boot error. Typical causes: -- entry-point key does not equal `Extension.name` +- entry-point key does not equal `App.name` - duplicate installed distributions register the same name -- the entry point does not resolve to an `Extension` subclass -- an extension table lacks its `_` prefix +- the entry point does not resolve to an `App` subclass +- an app table lacks its `_` prefix - a capability lives in `workflow.py` or `webhook.py` instead of a discoverable `workflows.py` or `webhooks.py` leaf -- the extension's import raised +- the app's import raised -The loader fails loudly because the extension name owns API, settings, and -migration namespaces. See [writing an extension](writing-an-extension.md). +The loader fails loudly because the app name owns API, settings, and +migration namespaces. See [writing an app](writing-an-app.md). ## Collecting a useful incident report diff --git a/docs/writing-an-extension.md b/docs/writing-an-app.md similarity index 91% rename from docs/writing-an-extension.md rename to docs/writing-an-app.md index e8bbdbc2..e55d6619 100644 --- a/docs/writing-an-extension.md +++ b/docs/writing-an-app.md @@ -1,28 +1,28 @@ -# Writing an extension +# Writing an app -An extension is an application installed into Druks as its own Python -distribution. It owns domain behavior; Druks supplies durable execution and -shared operating services. Read [the extension boundary](concepts.md#the-extension-boundary) +An app is a Python distribution installed into Druks. It owns domain behavior; +Druks supplies durable execution and shared operating services. Read +[the app boundary](concepts.md#the-app-boundary) before choosing which side should own a capability. ## Scaffold and prove the package ```bash -uvx --from druks druks create extension night_watch +uvx --from druks druks create app night_watch cd druks-night_watch uv sync uv run pytest ``` -From a Druks checkout, `uv run druks create extension night_watch` scaffolds with +From a Druks checkout, `uv run druks create app night_watch` scaffolds with that checkout's CLI instead. The command writes a standalone `druks-night_watch` project in the current directory. Its `pyproject.toml` contains: ```toml -[project.entry-points."druks.extensions"] -night_watch = "druks_night_watch.extension:NightWatch" +[project.entry-points."druks.apps"] +night_watch = "druks_night_watch.app:NightWatch" ``` The name must match `[a-z][a-z0-9_]*`. It becomes the API namespace, table @@ -40,7 +40,7 @@ unprefixed table. The project root also carries an `AGENTS.md` holding the contracts a coding agent cannot infer from the stubs, and a link back to this guide. -The scaffold depends on the published `druks`. To develop an extension against a +The scaffold depends on the published `druks`. To develop an app against a local checkout instead, pin it: ```toml @@ -51,11 +51,11 @@ druks = { path = "../druks", editable = true } ## Package layout The scaffold separates self-registering capability modules from ordinary -application modules: +package modules: | Path | Contract | | --- | --- | -| `extension.py` | `Extension` subclass, agents, extension settings | +| `app.py` | `App` subclass, agents, app settings | | `workflows.py` | durable `Workflow` and `Gate` subclasses | | `models.py` | SQLAlchemy models with `_` table names, `StoredSubject` among them | | `contracts.py` | `AgentOutput` contracts | @@ -72,13 +72,13 @@ Druks recursively discovers leaf modules named `workflows`, `routes`, discovered. Ordinary names such as `policy.py` and `workspace.py` have no import side effect unless a discovered module imports them. -## Declare the extension +## Declare the app ```python -from druks.extensions import Extension +from druks.apps import App -class NightWatch(Extension): +class NightWatch(App): name = "night_watch" icon = "telescope" description = "Checks repositories after hours." @@ -146,7 +146,7 @@ run_id = await Sweep.start( A workflow that declares none passes `subject=None`. A subject has at most one active run of a workflow kind; a duplicate start returns the active run id — attribution never changes that (two accounts starting the same subject share -the one run). Wrap `start()` in a domain `dispatch()` method when the extension +the one run). Wrap `start()` in a domain `dispatch()` method when the app needs lookup, snapshot, or routing policy before launch. A browser-origin start attributes itself: the request identity gate stamps @@ -260,7 +260,7 @@ from replayed orchestration allows later edits to change an in-flight run. ## Add an agent -An agent belongs to the extension class. Its family default (`claude` or +An agent belongs to the app class. Its family default (`claude` or `codex`) resolves through the corresponding operator harness setting; a full model name pins the default. @@ -273,7 +273,7 @@ class ReportOutput(AgentOutput): body: str -class NightWatch(Extension): +class NightWatch(App): report = Agent( model="claude", prompt="night_watch/report.md", @@ -294,37 +294,37 @@ CLI, validates the structured output, and records the call. Override `AgentOutput.to_result()` to map the strict agent contract to a domain value; override `get_artifact()` to publish a reviewable artifact. -Do not ask the framework to infer application side effects from agent prose. +Do not ask the framework to infer domain side effects from agent prose. The prompt or a subsequent explicit step owns those actions. ## Customize the workspace Every agent runs through a `Workspace` around a Drukbox sandbox. Override -`Workflow.workspace_class` and `get_workspace_kwargs()` when the application +`Workflow.workspace_class` and `get_workspace_kwargs()` when the app needs to clone a repository, mint a short-lived token, or require an MCP server. -Keep durable application state outside the VM. A workflow may opt into +Keep durable state outside the VM. A workflow may opt into `steps_reuse_sandbox = True` to retain one host across a segment, but Druks releases it at a gate and at workflow exit and rotates it near lease expiry. ### Borrow a browser session -Declare the logins your extension needs on the Extension class; the attribute -name and the extension's name become the session's identity, and the sessions +Declare the logins your app needs on the App class; the attribute +name and the app's name become the session's identity, and the sessions pane asks the operator to sign in: ```python from druks.browser import BrowserSession -from druks.extensions import Extension +from druks.apps import App -class NightWatch(Extension): +class NightWatch(App): name = "night_watch" acme = BrowserSession(site="acme.example", persist=True) ``` A workflow borrows the logged-in browser as a playwright handle — the -extension declares playwright as its own dependency, and druks owns +app declares playwright as its own dependency, and druks owns everything else (the browser boots in its own container on the druks box and dies with the block; a ``persist`` session is exported and stored back first): @@ -349,7 +349,7 @@ shows it and refuses further borrows until the operator signs in again — and the run fails under that reason. There is nothing to catch; the next scheduled run proceeds once the login is back. -Provider selection is an operator concern. Extension workspace code targets the +Provider selection is an operator concern. App workspace code targets the Druks sandbox contract, not `exe`, AWS, or Docker directly. ## Wait for input @@ -502,14 +502,14 @@ reads it and passes the caller. `account_id` is the signed-in account, or None outside a request. Use it to scope the rows when each operator has their own board. Ignore it when every operator shares one board. A model method never reads request context. Druks checks the method at load. If it is missing, the -extension does not load. The error names the extension, the subject, and the +app does not load. The error names the app, the subject, and the method. Druks serves the same `/api/night_watch/repository` surface either way: a board, a page for one, and a live stream of either, mounted for every subject your workflows declare. Each response pairs your summary with the run's status, timeline, agent calls, artifacts, and the question it is waiting on. Override -`get_subject_activity()` on the extension only to add a passing detail of your +`get_subject_activity()` on the app only to add a passing detail of your own, like "Building sandbox VM…". Hand the subject itself to anything that asks for one — starting a run, @@ -539,7 +539,7 @@ returns the step it is on. ## Record events and react to signals -Record an extension event through the extension so ownership is stamped: +Record an event through the app so ownership is stamped: ```python NightWatch.record_event( @@ -588,7 +588,7 @@ providers or DBOS retry the publication; make reactions idempotent. ## Receive webhooks A webhook authenticates and normalizes provider input. It should publish a -domain-neutral signal rather than contain application workflow policy: +domain-neutral signal rather than contain workflow policy: ```python from fastapi.responses import JSONResponse @@ -623,7 +623,7 @@ claim so the provider can retry. ## Models and migrations -Models subclass `druks.db.Base` and every normal extension table starts with +Models subclass `druks.db.Base` and every normal app table starts with `_`: ```python @@ -638,7 +638,7 @@ class Report(Base): id: Mapped[int] = mapped_column(primary_key=True) ``` -Generate the extension's revision after the model is importable: +Generate the app's revision after the model is importable: ```bash uv run druks makemigrations night_watch -m "add reports" @@ -652,7 +652,7 @@ HTTP request, durable step, or other platform-bound session. HTTP response models subclass `druks.schemas.BaseResponse`, whose snake_case fields serialize as camelCase. Request models are ordinary Pydantic models. Every router declared in a discovered `routes.py` is mounted below the -extension namespace, tagged with the extension's name — a router declares only +app namespace, tagged with the app's name — a router declares only the prefix its own resource is called: ```python @@ -673,7 +673,7 @@ def list_reviews() -> list[ReviewResponse]: ``` Tagging a route `agent` also derives it into an MCP tool: you give it an explicit -`operation_id`, and Druks derives the tool name by prefixing it with your extension +`operation_id`, and Druks derives the tool name by prefixing it with your app name — write `operation_id="add_peer"` in `peer_tracker` and the tool is `peer_tracker_add_peer`. The docstring is the description. `GET` derives read-only; a write declares `x-destructive: false` or `x-idempotent: true` in `openapi_extra` @@ -685,7 +685,7 @@ Two spellings run through druks, and which one a segment wears says who owns it: | | | | --- | --- | -| `snake_case` | an identity the platform serves — your extension name, a subject type | +| `snake_case` | an identity the platform serves — your app name, a subject type | | `kebab-case` | a resource you named — your route prefixes, your frontend paths | So `/api/review/pull_request` is the board of review runs, keyed by subject, and @@ -700,8 +700,8 @@ A service identity is the appliance's own registered app at an external provider — one per deployment, keyed by a service string; the platform's GitHub App is the first one. OAuth grants are not service identities — the platform stores those when the operator connects (see "Connect provider -accounts") — and a credential only your extension posts with belongs in your -extension settings instead. +accounts") — and a credential only your app posts with belongs in your +app settings instead. Declare one class in `services.py` and the platform does the rest: it renders the connect card in Settings, verifies and stores the paste (`SecretStr` @@ -751,7 +751,7 @@ async def verify(cls, settings: Settings) -> dict: Set `required = False` on the class when the appliance is healthy without the service connected; doctor then notes it instead of reporting pending setup. -Key the service for the integration your extension consumes (`Gmail`), not +Key the service for the integration your app consumes (`Gmail`), not the provider (`Google`). A second integration on the same provider declares its own service, and the operator decides per card whether the underlying registration is shared or a narrower one — that choice is their scope and @@ -835,11 +835,11 @@ class GoogleCalendar(GoogleOauth): pass ``` -Declare your extension's use of the service, with the scopes your calls +Declare your app's use of the service, with the scopes your calls need: ```python -class NightWatch(Extension): +class NightWatch(App): name = "night_watch" acme = Acme.with_scopes("profile.read", "posts.write") ``` @@ -865,7 +865,7 @@ live connections only. A revoked connection drops out of `get` and identity. Your rows never need tombstone copies of either. Your UI starts a sign-in by opening `/api/oauth/acme/connect` — the -platform runs the consent with the union of every installed extension's +platform runs the consent with the union of every installed app's declared scopes and stores the connection for the signed-in user. A fresh sign-in creates a new connection, unless the service's `identity_key` matches it to an existing connection for the same owner. To widen an @@ -923,9 +923,9 @@ narrower than the grant — pass it when the token goes to untrusted compute, with a subset of the connection's scopes. `cached=False` refreshes past the cache for a full-lifetime token. -## Extension settings and checks +## App settings and checks -An inner `ExtensionSettings` class defines dashboard-editable knobs and owns their +An inner `AppSettings` class defines dashboard-editable knobs and owns their cross-field coherence: ```python @@ -933,13 +933,13 @@ from typing import Literal from pydantic import Field -from druks.extensions import Extension, ExtensionSettings, Secret +from druks.apps import App, AppSettings, Secret -class NightWatch(Extension): +class NightWatch(App): name = "night_watch" - class Settings(ExtensionSettings): + class Settings(AppSettings): provider: Literal["none", "acme"] = "none" service_token: Secret = Field( json_schema_extra={"section": "Acme", "visible_when": {"provider": "acme"}}, @@ -974,19 +974,19 @@ resolved settings after the proposed edits and rejects an incoherent save. `druk runs the same method over stored settings so rows from older releases or manual database edits remain visible. Workflow settings stay plain Pydantic `BaseModel` declarations. -An extension may contribute precondition checks beyond settings coherence through +An app may contribute precondition checks beyond settings coherence through `checks`. Return `druks.doctor.CheckResult`; Druks namespaces the result and converts a raising or malformed check into a failure without hiding later checks. -## Test an extension +## Test an app -Installing Druks registers its pytest plugin. An extension can request the +Installing Druks registers its pytest plugin. An app can request the fixtures directly without a `conftest.py` or `pytest_plugins` declaration: | Fixture | Contract | | --- | --- | | `druks_db` | A SQLAlchemy `Session` bound to a per-test transaction. Commits become savepoints, and teardown rolls the outer transaction back. | -| `druks_client` | An authenticated `TestClient` with installed extensions mounted, sharing `druks_db`'s connection. | +| `druks_client` | An authenticated `TestClient` with installed apps mounted, sharing `druks_db`'s connection. | | `druks_redis` | The test Redis database, flushed before the test. | | `druks_without_dispatch` | Workflow starts and run-phase writes become no-ops, for tests that stand up no durable engine. | | `druks_without_remote_config` | Every `.druks` namespace lookup misses, so prompts resolve to bundled templates and config to its declared defaults. | @@ -1028,19 +1028,19 @@ call = seed_call( `Run.state` is derived. Its `kind` is required. Pass `input_gate` when seeding `state="pending_input"`. `seed_call` accepts an `Agent` or its string id. -`make_settings(tmp_path, **overrides)` builds isolated application settings. +`make_settings(tmp_path, **overrides)` builds isolated Druks settings. `configure_app_for_test(settings=..., authenticated=False)` returns the mounted app when a test needs its own client or an unauthenticated request path; `druks_client` covers the normal authenticated case. -The fixtures never read the application's settings. They read +The fixtures never read the runtime's settings. They read `DRUKS_TEST_DATABASE_URL` and `DRUKS_TEST_REDIS_URL`, defaulting to a local `druks_test` database and Redis index 15, and they point the code under test at the same pair — so a run cannot reach whatever `DRUKS_DATABASE_URL` and `DRUKS_REDIS_URL` name. Create the database once (`createdb druks_test`); the dev Compose project already does. -On that database the plugin creates `citext`, imports installed extension models, +On that database the plugin creates `citext`, imports installed app models, runs SQLAlchemy `create_all`, seeds platform reference rows, and builds the DBOS system tables through DBOS's database migrations. It never resets or drops a schema; per-test writes made through `druks_db` are rolled back. `druks_redis` @@ -1048,20 +1048,20 @@ runs `FLUSHDB` on the test index. ## Frontends -An installed extension is visible in the dashboard without shipping any UI. The -shell reads the installed roster from `/api/extensions` and gives every -extension an entry in the app switcher plus generic pages: a board per subject +An installed app is visible in the dashboard without shipping any UI. The +shell reads the installed roster from `/api/apps` and gives every +app an entry in the app switcher plus generic pages: a board per subject type, and a subject page with the run timeline, transcripts, and gate controls. There is nothing to declare — the subject summary's fields are the board row. The switcher label is derived from `name` (underscores become spaces). -Chrome contributions are declared data. `navigation` on the extension class +Chrome contributions are declared data. `navigation` on the app class adds appbar subnav tabs as `(url, name)` pairs, rendered by the shell for generic pages and shipped frontends alike; the active tab is the one whose url is the longest prefix of the current location: ```python -class NightWatch(Extension): +class NightWatch(App): name = "night_watch" navigation = [("/night_watch", "reports")] ``` @@ -1086,8 +1086,8 @@ inside its own document, below the chrome. The scaffold ships a placeholder resolves them to its own copy, so one React instance serves the whole document. Other dependencies are bundled as usual. -The bundled Druks SPA also has a shared React extension registry. Joining that -shell requires compiling the extension's UI module into the dashboard image; +The bundled Druks SPA also has a shared React app registry. Joining that +shell requires compiling the app's UI module into the dashboard image; installing a Python wheel cannot mutate an existing JavaScript bundle. See the [frontend guide](../frontend/README.md) for that in-repository path. @@ -1098,7 +1098,7 @@ Import from concern namespaces, not from `druks.durable` or internal modules: | Namespace | Public names | | --- | --- | | `druks.accounts` | `current_account_id` | -| `druks.extensions` | `Extension`, `ExtensionSettings`, `Secret` | +| `druks.apps` | `App`, `AppSettings`, `Secret` | | `druks.services` | `Service`, `ServiceConnectError`, `ServiceNotConnectedError`, `OauthClient`, `OauthExchangeError`, `OauthRefreshError` | | `druks.secrets.fields` | `EncryptedJsonField`, `SecretsMapping` | | `druks.agents` | `Agent`, `AgentOutput` | diff --git a/frontend/README.md b/frontend/README.md index 2d3519fb..b0ff807d 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -23,32 +23,32 @@ runs lint, tests, and build. `src/App.tsx` is the platform shell. It owns: -- the app bar and extension picker +- the app bar and app picker - Settings - Events and Usage pages - the optional system-health strip - shared routing and fallback behavior -Bundled extension UI lives under `src/extensions//`. Its module calls -`registerExtensionUI()` with routes and an optional home path; subnav tabs are -declared on the extension's backend class and rendered from the roster. -Import that module once from `src/extensions/index.ts`; the shell discovers the -registration and does not hardcode the extension name. +Bundled app UI lives under `src/apps//`. Its module calls +`registerAppUI()` with routes and an optional home path; subnav tabs are +declared on the app's backend class and rendered from the roster. +Import that module once from `src/apps/index.ts`; the shell discovers the +registration and does not hardcode the app name. -Backend and frontend extension discovery are intentionally separate: +Backend and frontend app discovery are intentionally separate: -- Python entry points load an installed backend extension at runtime. -- React extension modules are compiled into the SPA at build time. +- Python entry points load an installed backend app at runtime. +- React app modules are compiled into the SPA at build time. Installing a Python distribution cannot inject JavaScript into an already-built -dashboard. A backend-only extension can still use the platform API, settings, +dashboard. A backend-only app can still use the platform API, settings, events, and generic subject read-side; custom pages require a dashboard build that includes its UI module. -An independently packaged extension has another option: ship a built ES module +An independently packaged app has another option: ship a built ES module in `/dist/` exposing `mount(el, ctx)`. Druks serves it under `/app/` and the shell imports and mounts it below the chrome, sharing -one React via an import map (`src/runtime/`). See the extension-author guide. +one React via an import map (`src/runtime/`). See the app-author guide. ## API and live data @@ -67,4 +67,4 @@ OpenAPI types are not currently checked into the repository. Start Postgres, Redis, the backend, and Vite using [the development guide](../docs/development.md). For a production-like static asset check, run `npm --prefix frontend run build` and then start the backend; -the application serves the repository-root `dist/` when it exists. +the server serves the repository-root `dist/` when it exists.