--- title: API: agent description: REST endpoints tagged agent — generated from the platform's OpenAPI contract (2 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/agent/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/agent/pool` Agent Pool [Open as a page →](./get-agent-pool/) per-contract digest of the agent's pool: in-flight runs, last run, article status counts. One call instead of N cockpit calls. **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | ## `GET /api/v1/agent/quota` Agent Quota [Open as a page →](./get-agent-quota/) the agent's concurrency budget: cap, in-flight, remaining. **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | --- --- title: API: analytics description: REST endpoints tagged analytics — generated from the platform's OpenAPI contract (4 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/analytics/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `POST /api/v1/analytics/collect` Collect [Open as a page →](./post-analytics-collect/) Trigger a collection run (idempotent; safe to re-run). `collect=false` runs the cheap SYNCHRONOUS mode: external collectors are not called, only the ingest of already-fetched files and topic scoring run, and the caller gets the RESULT instead of an "accepted" — so a scheduled caller can tell whether the loop actually computed. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `CollectTrigger` | Field | Type | Required | Description | | --- | --- | --- | --- | | `collect` | `boolean` | no | | | `date` | `string \| null` | no | | | `with_psi` | `boolean` | no | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/analytics/instances/{instance_id}` Instance Dashboard [Open as a page →](./get-analytics-instances-instance-id/) Everything one per-instance analytics panel needs (snapshot + trends + queries + pages + index). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `instance_id` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `InstanceDashboard` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/analytics/overview` Overview [Open as a page →](./get-analytics-overview/) Cross-instance grid: latest snapshot KPIs + health per instance. **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `OverviewResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | ## `GET /api/v1/analytics/runs` Runs [Open as a page →](./get-analytics-runs/) Recent collection-run manifests. **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of CollectionRunRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | --- --- title: API: architecture description: REST endpoints tagged architecture — generated from the platform's OpenAPI contract (2 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/architecture/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/architecture/catalog` Get Catalog [Open as a page →](./get-architecture-catalog/) : return YAML-loaded catalog (cached in app.state). **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `CatalogResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | ## `GET /api/v1/architecture/status` Get Status [Open as a page →](./get-architecture-status/) : parallel health checks with 10s TTL cache. **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `StatusResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | --- --- title: API: auth description: REST endpoints tagged auth — generated from the platform's OpenAPI contract (6 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/auth/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `POST /api/v1/auth/agent-token` Mint Agent Token [Open as a page →](./post-auth-agent-token/) mint (or rotate) a scoped AGENT token + its contract grants. Superuser-only. Creates (or reuses) the agent identity and REPLACES its grant pool with `body.grants` (default-deny — a contract not listed is inaccessible). Changing the pool or capabilities later is a grant edit, no re-mint needed. `publish` is granted only when listed explicitly: withhold on prod contracts by default). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `AgentTokenRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `email` | `string` | no | | | `expires_days` | `integer` | no | | | `full_name` | `string \| null` | no | | | `grants` | `array of AgentGrantSpec` | yes | | | `revoke_prior` | `boolean` | no | | _Example:_ ```json { "email": "agent@entherium.dev", "expires_days": 90, "full_name": "Partner agent", "grants": [ { "capabilities": [ "submit", "launch", "cancel", "edit_config" ], "salescontract": 40 } ], "revoke_prior": true } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `ServiceTokenResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `409` | The e-mail belongs to a non-agent user; refusing to convert | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | _Example response (200):_ ```json { "access_token": "eyJhbGciOiJIUzI1NiJ9..", "api_scope": "agent", "email": "agent@entherium.dev", "expires_in": 7776000, "token_type": "bearer", "user_guid": 5 } ``` ## `POST /api/v1/auth/login` Login [Open as a page →](./post-auth-login/) OAuth2 password flow. `username` field is the user's email. **Auth:** no token required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/x-www-form-urlencoded` — schema `Body_login_api_v1_auth_login_post` | Field | Type | Required | Description | | --- | --- | --- | --- | | `client_id` | `string \| null` | no | | | `client_secret` | `string \| null` | no | | | `grant_type` | `string \| null` | no | | | `password` | `string` | yes | | | `scope` | `string` | no | | | `username` | `string` | yes | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `TokenPair` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/auth/me` Me [Open as a page →](./get-auth-me/) **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `MeResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | _Example response (200):_ ```json { "email": "agent@entherium.dev", "full_name": "Partner agent", "guid": 5, "is_active": true, "is_superuser": false, "is_team": false, "last_login": "2026-09-03T08:12:41Z" } ``` ## `POST /api/v1/auth/refresh` Refresh [Open as a page →](./post-auth-refresh/) Exchange a refresh token for a new access token. **Auth:** no token required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `RefreshRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `refresh_token` | `string` | yes | | _Example:_ ```json { "refresh_token": "" } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `AccessOnly` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/auth/service-token` Mint Service Token [Open as a page →](./post-auth-service-token/) Mint (or rotate) a long-lived READ-ONLY access token for an external agent. Superuser-only. The endpoint can only create/serve users with read-only agent scope — it is structurally unable to hand out full-access tokens. Re-minting reactivates a previously revoked user. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `ServiceTokenRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `email` | `string` | no | | | `expires_days` | `integer` | no | | | `full_name` | `string \| null` | no | | | `revoke_prior` | `boolean` | no | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `ServiceTokenResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `409` | The e-mail belongs to an existing non-read-only user; refusing to convert | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | _Example response (200):_ ```json { "access_token": "eyJhbGciOiJIUzI1NiJ9..", "api_scope": "agent", "email": "agent@entherium.dev", "expires_in": 7776000, "token_type": "bearer", "user_guid": 5 } ``` ## `DELETE /api/v1/auth/service-token/{guid}` Revoke Service Token [Open as a page →](./delete-auth-service-token-guid/) Revoke a read-only service user: deactivate + kill all minted tokens. Effective immediately — auth re-reads the user row on every request. Only read-only agent identities are addressable here (404 otherwise), so a typo'd guid can never lock out a human/admin account. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `guid` | path | yes | `integer` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `204` | Successful Response | — | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: chat description: REST endpoints tagged chat — generated from the platform's OpenAPI contract (1 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/chat/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/chat/messages` List Messages [Open as a page →](./get-chat-messages/) Work-chat archive — the conversational context behind a task or defect. Read-only. `is_full=false` on a row means the message text is TRUNCATED (the log and the message store could not be joined) — never quote such a row as verbatim. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `chat` | query | no | `string \| null` | chat alias: `work` (the team work chat, default) or `agents`; or a raw chat_id | | `author` | query | no | `string \| null` | substring of the author's display name | | `since_days` | query | no | `integer \| null` | only messages from the last N days | | `q` | query | no | `string \| null` | substring to search for in the message text | | `limit` | query | no | `integer` | | | `offset` | query | no | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `ChatMessageListResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: cloudflare description: REST endpoints tagged cloudflare — generated from the platform's OpenAPI contract (6 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/cloudflare/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/cloudflare/health/{domain}` Cf Health [Open as a page →](./get-cloudflare-health-domain/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `domain` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/cloudflare/verify` Cf Verify [Open as a page →](./get-cloudflare-verify/) **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | ## `GET /api/v1/cloudflare/zones` Cf List Zones [Open as a page →](./get-cloudflare-zones/) **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | ## `GET /api/v1/cloudflare/zones/{domain}` Cf Zone [Open as a page →](./get-cloudflare-zones-domain/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `domain` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/cloudflare/zones/{domain}/dns` Cf Dns [Open as a page →](./get-cloudflare-zones-domain-dns/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `domain` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/cloudflare/zones/{domain}/ssl` Cf Ssl [Open as a page →](./get-cloudflare-zones-domain-ssl/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `domain` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: cockpit description: REST endpoints tagged cockpit — generated from the platform's OpenAPI contract (4 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/cockpit/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/contracts/{guid}/cockpit` Get Cockpit [Open as a page →](./get-contracts-guid-cockpit/) Live cockpit snapshot for a SalesContract (empty contract → all zeros). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `guid` | path | yes | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `CockpitResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/contracts/{guid}/heatmap` Get Heatmap [Open as a page →](./get-contracts-guid-heatmap/) Generate per-step distribution from the SC's in-flight generate run_steps. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `guid` | path | yes | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/contracts/{guid}/published-articles` Get Published Articles [Open as a page →](./get-contracts-guid-published-articles/) Published articles (live URL set) for a contract — title, link, word count. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `guid` | path | yes | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of PublishedArticle` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/contracts/{guid}/timeline` Get Timeline [Open as a page →](./get-contracts-guid-timeline/) 24h per-pipeline Gantt strip from the SC's last-24h pipeline_run rows. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `guid` | path | yes | `integer` | | | `window` | query | no | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: comments description: REST endpoints tagged comments — generated from the platform's OpenAPI contract (5 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/comments/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/comments` List Comments [Open as a page →](./get-comments/) : list filtered by target. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `target_kind` | query | no | `string \| null` | | | `target_guid` | query | no | `integer \| null` | | | `include_deleted` | query | no | `boolean` | | | `limit` | query | no | `integer` | | | `offset` | query | no | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `CommentList` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/comments` Create Comment [Open as a page →](./post-comments/) : create a comment, resolve @mentions, record agent mentions as an agent action. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `CommentCreate` | Field | Type | Required | Description | | --- | --- | --- | --- | | `body_markdown` | `string` | yes | | | `target_guid` | `integer` | yes | | | `target_kind` | `string` | yes | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `201` | Successful Response | `CommentRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `DELETE /api/v1/comments/{guid}` Delete Comment [Open as a page →](./delete-comments-guid/) : soft-delete. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `guid` | path | yes | `integer` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/comments/{guid}` Get Comment [Open as a page →](./get-comments-guid/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `guid` | path | yes | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `CommentRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `PATCH /api/v1/comments/{guid}` Patch Comment [Open as a page →](./patch-comments-guid/) : PATCH only by author (or superuser). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `guid` | path | yes | `integer` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `CommentUpdate` | Field | Type | Required | Description | | --- | --- | --- | --- | | `body_markdown` | `string \| null` | no | | _Example:_ ```json { "body_markdown": "Edited: the figure is confirmed by the source." } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `CommentRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: cycles description: REST endpoints tagged cycles — generated from the platform's OpenAPI contract (4 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/cycles/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/cycles/history` Cycle History [Open as a page →](./get-cycles-history/) N-day cycle history. Derived from `pipeline_run` and `lifecycle_log`. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `days` | query | no | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of CycleDay` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/cycles/status` Cron Status [Open as a page →](./get-cycles-status/) Cron + last/next run summary. Settings-derived; future Cron service SPEC will replace with real cron state from a `cron_runs` table. **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `CronStatus` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | ## `GET /api/v1/cycles/timeline` Cycle Timeline [Open as a page →](./get-cycles-timeline/) Per-day timeline of auto-chain events. Derived from `lifecycle_log` and `pipeline_run` ordered by timestamp. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `date` | query | no | `string \| null` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of TimelineEvent` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/cycles/today` Today Funnel [Open as a page →](./get-cycles-today/) Funnel summary for today's auto-cycle, plus per-SC breakdown. Derived from: - `pipeline_run` rows created today (≈ runs started) - `generated_articles` count by status (pending / ready / live) - `salescontracts` for per-SC grouping **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `TodayFunnel` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | --- --- title: API: docs description: REST endpoints tagged docs — generated from the platform's OpenAPI contract (3 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/docs/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/docs/changelog` Get Changelog [Open as a page →](./get-docs-changelog/) Capability changelog of the agent-facing READ surface, layer C). An external read-API agent (which sees the ``X-Capability-Version`` echo header) diffs these entries from its last-seen version to learn which endpoints/fields/semantics changed BEFORE it acts on stale assumptions. Mirrors the MCP ``changelog://entherium`` resource — one source (``app.discovery.CAPABILITY_CHANGELOG``), so the two surfaces cannot drift. **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `CapabilityChangelog` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | ## `GET /api/v1/docs/file` Get File [Open as a page →](./get-docs-file/) : markdown content + frontmatter + resolved wikilinks. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `path` | query | yes | `string` | Relative path within vault | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `DocFile` | | `400` | Requested path is not a .md file | — | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `403` | Path escapes the docs vault | — | | `404` | File not found | — | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/docs/tree` Get Tree [Open as a page →](./get-docs-tree/) : recursive tree of.md files + dirs. **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of TreeNode` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | --- --- title: API: entities description: REST endpoints tagged entities — generated from the platform's OpenAPI contract (12 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/entities/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/entities` List Entity Types [Open as a page →](./get-entities/) **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of EntityInfo` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | ## `GET /api/v1/entities/{name}` List Records [Open as a page →](./get-entities-name/) List entity records — paginated, sortable, filterable. Any query param that is not a control param (`limit`/`offset`/`sort`/ `include_meta`) is read as a column filter: `col` (equals), `col_like` (ILIKE), `col_from`/`col_to` (range). Large JSON/text columns are truncated to a preview stub; `?include_meta=true` adds the runtime schema. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `name` | path | yes | `string` | | | `limit` | query | no | `integer` | | | `offset` | query | no | `integer` | | | `sort` | query | no | `string \| null` | | | `include_meta` | query | no | `boolean` | | | `include_deleted` | query | no | `boolean` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `EntityListResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/entities/{name}` Create Record [Open as a page →](./post-entities-name/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `name` | path | yes | `string` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `201` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/entities/{name}/bulk-delete` Bulk Delete [Open as a page →](./post-entities-name-bulk-delete/) Delete many rows — PROD rows without approval are blocked. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `name` | path | yes | `string` | | | `x-prod-approval` | header | no | `string \| null` | Production guard. | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `BulkDeleteRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `guids` | `array of integer` | yes | | _Example:_ ```json { "guids": [ 31567, 31568 ] } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `BulkDeleteResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/entities/{name}/bulk-export` Bulk Export [Open as a page →](./post-entities-name-bulk-export/) Export selected rows as a downloadable JSON file. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `name` | path | yes | `string` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `BulkExportRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `format` | `string` | no | | | `guids` | `array of integer` | yes | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/entities/{name}/count` Count Records [Open as a page →](./get-entities-name-count/) Row count for an entity, cached in-process for 30s. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `name` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `EntityCountResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `DELETE /api/v1/entities/{name}/{guid}` Delete Record [Open as a page →](./delete-entities-name-guid/) Delete a row — soft (deleted_at) for soft-deletable entities. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `name` | path | yes | `string` | | | `guid` | path | yes | `integer` | | | `x-prod-approval` | header | no | `string \| null` | Production guard. | | `Idempotency-Key` | header | no | `string` | Optional. | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `204` | Successful Response | — | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/entities/{name}/{guid}` Read Record [Open as a page →](./get-entities-name-guid/) Read one full row + runtime schema for the row drawer. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `name` | path | yes | `string` | | | `guid` | path | yes | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `RowResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `PATCH /api/v1/entities/{name}/{guid}` Update Record [Open as a page →](./patch-entities-name-guid/) Partial update — PROD-guarded and audit-logged. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `name` | path | yes | `string` | | | `guid` | path | yes | `integer` | | | `x-prod-approval` | header | no | `string \| null` | Production guard. | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/entities/{name}/{guid}/audit` Read Audit [Open as a page →](./get-entities-name-guid-audit/) Audit trail for one row, projected from lifecycle_log. Latest first. A multi-field edit is expanded into one event per field. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `name` | path | yes | `string` | | | `guid` | path | yes | `integer` | | | `limit` | query | no | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of AuditEvent` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/entities/{name}/{guid}/field/{field}` Read Field [Open as a page →](./get-entities-name-guid-field-field/) Read one field's full, untruncated value — lazy JSON load. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `name` | path | yes | `string` | | | `guid` | path | yes | `integer` | | | `field` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `FieldResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/entities/{name}/{guid}/restore` Restore Record [Open as a page →](./post-entities-name-guid-restore/) Restore a soft-deleted row — clears deleted_at a). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `name` | path | yes | `string` | | | `guid` | path | yes | `integer` | | | `x-prod-approval` | header | no | `string \| null` | Production guard. | | `Idempotency-Key` | header | no | `string` | Optional. | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: errors description: REST endpoints tagged errors — generated from the platform's OpenAPI contract (1 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/errors/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/errors` List Errors [Open as a page →](./get-errors/) Recent captured runtime errors, newest first. Lets an operator answer "what just broke" without trawling the JSON log stream — the durable counterpart to the structlog ``error_captured`` event. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `source` | query | no | `string \| null` | filter by capture source | | `operation` | query | no | `string \| null` | filter by stable operation label | | `since` | query | no | `string \| null` | only errors captured at/after this UTC time | | `limit` | query | no | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of ErrorEventRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | _Example response (200):_ ```json [ { "context": { "run_guid": 1842, "trace_id": "6f1c…" }, "createtime": "2026-09-03T02:31:12Z", "error": "chart render timed out after 60s", "exc_type": "ImageRenderError", "guid": 90412, "operation": "generate.step_13_images", "source": "batch" } ] ``` --- --- title: API: health description: REST endpoints tagged health — generated from the platform's OpenAPI contract (3 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/health/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/health` Liveness [Open as a page →](./get-health/) Always returns 200 if the process is up. No external dependencies. **Auth:** no token required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `HealthStatus` | ## `GET /api/v1/health/db` Db Health [Open as a page →](./get-health-db/) Verifies that the DB connection is alive (`SELECT 1`). **Auth:** no token required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `DbHealth` | | `503` | Service Unavailable | `DbHealth` | ## `GET /api/v1/health/ready` Readiness [Open as a page →](./get-health-ready/) K8s-style /readyz — green only if both DB and Redis respond. **Auth:** no token required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `ReadinessReport` | | `503` | Service Unavailable | `ReadinessReport` | --- --- title: REST API overview description: The platform REST API grouped by tag — 26 groups, generated from the OpenAPI contract. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. Reference generated from the checked-in contract. The frozen machine-readable spec for this build is at [openapi.json](/developers/api/openapi.json); the interactive explorer lives at [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference). ## Calling the API - **Base URL:** `https://entherium.duckdns.org:8443`; every path below is relative to it. - **Auth:** all data endpoints need `Authorization: Bearer `. Humans log in with the OAuth2 password flow (`POST /api/v1/auth/login`); agents use a scoped agent token issued by the platform owner — see [Authentication](../guides/authentication/). Without a token only these answer: `/healthz`, `/status.json`, `/api/status.json`, `/api/` (the machine map), `/openapi.json`, `/reference`, `/redoc`, `/llms.txt`, `/mcp-info`, the docs MCP at `/mcp/docs/` and this portal. - **Errors:** JSON `{"detail": ""}`; some endpoints add a machine `code`. Full list: [Errors reference](../reference/errors/). - **Retries:** the task and ticket intake endpoints honour an `Idempotency-Key` header (UUID); see [Limits](../reference/limits/#idempotency). - **Groups vs domains:** this reference groups operations by contract tag; the MCP discovery map (`list_domains`, `GET /api/`) names 28 capability domains — a different axis (a domain may span several tags or have no public operation), so the counts differ on purpose. | Group | Operations | | --- | --- | | [agent](./agent/) | 2 | | [analytics](./analytics/) | 4 | | [architecture](./architecture/) | 2 | | [auth](./auth/) | 6 | | [chat](./chat/) | 1 | | [cloudflare](./cloudflare/) | 6 | | [cockpit](./cockpit/) | 4 | | [comments](./comments/) | 5 | | [cycles](./cycles/) | 4 | | [docs](./docs/) | 3 | | [entities](./entities/) | 12 | | [errors](./errors/) | 1 | | [health](./health/) | 3 | | [instances](./instances/) | 8 | | [integrations](./integrations/) | 3 | | [lifecycles](./lifecycles/) | 6 | | [localizations](./localizations/) | 1 | | [metamodel](./metamodel/) | 2 | | [metering](./metering/) | 4 | | [ops](./ops/) | 3 | | [publish](./publish/) | 3 | | [runs](./runs/) | 17 | | [specs](./specs/) | 6 | | [sse](./sse/) | 6 | | [tasks](./tasks/) | 5 | | [tickets](./tickets/) | 3 | --- --- title: API: instances description: REST endpoints tagged instances — generated from the platform's OpenAPI contract (8 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/instances/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/instances` List Instances [Open as a page →](./get-instances/) **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of HugoInstanceRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | ## `POST /api/v1/instances` Create Instance [Open as a page →](./post-instances/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `HugoInstanceCreate` | Field | Type | Required | Description | | --- | --- | --- | --- | | `attached_sc` | `integer \| null` | no | | | `backend` | `"gcs" \| "local-fs" \| "wp"` | no | | | `base_url` | `string \| null` | no | | | `caption` | `string \| null` | no | | | `gcs_bucket` | `string \| null` | no | | | `hostname` | `string \| null` | no | | | `instance_id` | `string` | yes | | | `is_active` | `boolean` | no | | | `registry` | `object` | no | | | `template_dir` | `string \| null` | no | | | `theme_repo` | `string \| null` | no | | | `theme_sha` | `string \| null` | no | | | `tier` | `"prod" \| "dev" \| "test" \| "staging"` | no | | | `vps_ip` | `string \| null` | no | | | `vps_port` | `integer \| null` | no | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `201` | Successful Response | `HugoInstanceRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `DELETE /api/v1/instances/{instance_id}` Delete Instance [Open as a page →](./delete-instances-instance-id/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `instance_id` | path | yes | `string` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `204` | Successful Response | — | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/instances/{instance_id}` Read Instance [Open as a page →](./get-instances-instance-id/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `instance_id` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `HugoInstanceRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `PATCH /api/v1/instances/{instance_id}` Update Instance [Open as a page →](./patch-instances-instance-id/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `instance_id` | path | yes | `string` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `HugoInstanceUpdate` | Field | Type | Required | Description | | --- | --- | --- | --- | | `attached_sc` | `integer \| null` | no | | | `backend` | `"gcs" \| "local-fs" \| "wp" \| null` | no | | | `base_url` | `string \| null` | no | | | `caption` | `string \| null` | no | | | `gcs_bucket` | `string \| null` | no | | | `hostname` | `string \| null` | no | | | `is_active` | `boolean \| null` | no | | | `registry` | `object \| null` | no | | | `template_dir` | `string \| null` | no | | | `theme_repo` | `string \| null` | no | | | `theme_sha` | `string \| null` | no | | | `tier` | `"prod" \| "dev" \| "test" \| "staging" \| null` | no | | | `vps_ip` | `string \| null` | no | | | `vps_port` | `integer \| null` | no | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `HugoInstanceRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/instances/{instance_id}/published` Instance Published [Open as a page →](./get-instances-instance-id-published/) published-article links on an instance (from `published_url`). Live link = `url_external` ∥ `base_url + '/' + slug`; plus source_url, title, timestamps, index_status. Pool-scoped: an agent reaches only its instances (`instance.attached_sc` ∈ pool) — both convenience and a read-BOLA boundary. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `instance_id` | path | yes | `string` | | | `since` | query | no | `string \| null` | | | `active_only` | query | no | `boolean` | | | `limit` | query | no | `integer` | | | `offset` | query | no | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/instances/{instance_id}/summary` Instance Summary [Open as a page →](./get-instances-instance-id-summary/) Public polling endpoint (1.5-3s tick on the webui). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `instance_id` | path | yes | `string` | | | `refresh` | query | no | `boolean` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `InstanceSummary` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/instances/{instance_id}/warmup` Warmup Instance [Open as a page →](./post-instances-instance-id-warmup/) Mark instance as warming. Real workflow lands in Phase 5 (Hatchet). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `instance_id` | path | yes | `string` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: integrations description: REST endpoints tagged integrations — generated from the platform's OpenAPI contract (3 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/integrations/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/integrations` List Integrations [Open as a page →](./get-integrations/) Every external connection with its cached status. **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of ConnectionStatus` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | ## `GET /api/v1/integrations/{connection_id}` Get Integration [Open as a page →](./get-integrations-connection-id/) One connection's freshly recomputed status. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `connection_id` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `ConnectionStatus` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/integrations/{connection_id}/health` Recheck Integration [Open as a page →](./post-integrations-connection-id-health/) Force an immediate re-probe of one connection. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `connection_id` | path | yes | `string` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `ConnectionStatus` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: lifecycles description: REST endpoints tagged lifecycles — generated from the platform's OpenAPI contract (6 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/lifecycles/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `POST /api/v1/lifecycles/{entity_name}/processEvent` Process Event [Open as a page →](./post-lifecycles-entity-name-processevent/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `entity_name` | path | yes | `string` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `ProcessEventRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `event` | `string` | no | outlet name on automated steps | | `filter` | `object \| null` | no | | | `lcid` | `integer \| null` | no | | | `max` | `integer` | no | hard upper bound per call | _Example:_ ```json { "event": "tick", "lcid": 4, "max": 50 } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `ProcessEventResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/lifecycles/{entity_name}/{guid}/advance` Advance [Open as a page →](./post-lifecycles-entity-name-guid-advance/) Advance a record via an outlet (analog of Entherium's `advance_record`). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `entity_name` | path | yes | `string` | | | `guid` | path | yes | `integer` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `AdvanceRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `detail` | `object \| null` | no | | | `outlet` | `string` | yes | Outlet name (ok, error, or conditional name) | _Example:_ ```json { "detail": { "note": "verified live" }, "outlet": "ok" } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `AdvanceResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `404` | Entity or lifecycle not found | `ErrorResponse` | | `409` | Transition not allowed from the current step | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/lifecycles/{entity_name}/{guid}/log` Read Log [Open as a page →](./get-lifecycles-entity-name-guid-log/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `entity_name` | path | yes | `string` | | | `guid` | path | yes | `integer` | | | `limit` | query | no | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of LifecycleLogRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/lifecycles/{entity_name}/{guid}/postMessage` Post Message [Open as a page →](./post-lifecycles-entity-name-guid-postmessage/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `entity_name` | path | yes | `string` | | | `guid` | path | yes | `integer` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `PostMessageRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `detail` | `object \| null` | no | | | `message` | `string` | yes | | _Example:_ ```json { "message": "Held back: waiting for the source to confirm the figure." } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `LifecycleLogRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/lifecycles/{entity_name}/{guid}/state` Get State [Open as a page →](./get-lifecycles-entity-name-guid-state/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `entity_name` | path | yes | `string` | | | `guid` | path | yes | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `LifecycleStateResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/lifecycles/{entity_name}/{guid}/stepTo` Step To [Open as a page →](./post-lifecycles-entity-name-guid-stepto/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `entity_name` | path | yes | `string` | | | `guid` | path | yes | `integer` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `StepToRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `comment` | `string \| null` | no | | | `detail` | `object \| null` | no | | | `to_step` | `integer` | yes | target step id from LC definition | _Example:_ ```json { "comment": "reviewed; moving to publish-ready", "detail": { "reviewer": "agent" }, "to_step": 3 } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `AdvanceResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `404` | Entity or lifecycle not found | `ErrorResponse` | | `409` | Transition not allowed from the current step | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: localizations description: REST endpoints tagged localizations — generated from the platform's OpenAPI contract (1 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/localizations/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/localizations` List Localizations [Open as a page →](./get-localizations/) List localization rows, newest first. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `salescontract` | query | no | `integer \| null` | filter by owning salescontract guid | | `article` | query | no | `integer \| null` | filter by generated_articles guid | | `language` | query | no | `string \| null` | filter by BCP-47 language | | `status` | query | no | `string \| null` | filter by state-machine status | | `limit` | query | no | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of LocalizationRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: metamodel description: REST endpoints tagged metamodel — generated from the platform's OpenAPI contract (2 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/metamodel/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/metamodel` Get Metamodel [Open as a page →](./get-metamodel/) catalog of business entities + FK edges + type legend. **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `MetamodelResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | ## `GET /api/v1/metamodel/{entity}` Get Metamodel Entity [Open as a page →](./get-metamodel-entity/) full schema of one business entity. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `entity` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `MetamodelDetail` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: metering description: REST endpoints tagged metering — generated from the platform's OpenAPI contract (4 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/metering/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/metering/costs/by-article` Costs By Article [Open as a page →](./get-metering-costs-by-article/) Per-article cost for a contract: direct spend + amortized collect share. ``direct`` = sum over the article's own runs (generate/publish, where ``generated_article_id`` is set). ``amortized_collect`` = the contract's contract-level cost (its collect runs, where ``generated_article_id`` IS NULL) divided by the number of articles in the window — (owner decision: amortize by period, not per collect-run, since collect feeds the reused RAG corpus). imagegen is a deliberately separate ledger and NOT added. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `salescontracts_id` | query | yes | `integer` | | | `from` | query | no | `string \| null` | | | `to` | query | no | `string \| null` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of ArticleCostRow` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/metering/costs/by-contract` Costs By Contract [Open as a page →](./get-metering-costs-by-contract/) Per-contract cost rollup (sum over each salescontracts_id). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `ai_company_id` | query | no | `integer \| null` | | | `from` | query | no | `string \| null` | | | `to` | query | no | `string \| null` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of ContractCostRow` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/metering/costs/by-pipeline-run` Costs By Pipeline Run [Open as a page →](./get-metering-costs-by-pipeline-run/) Per-run cost rows, newest first. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `ai_company_id` | query | no | `integer \| null` | | | `from` | query | no | `string \| null` | | | `to` | query | no | `string \| null` | | | `limit` | query | no | `integer` | | | `offset` | query | no | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of RunCostRow` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/metering/costs/summary` Costs Summary [Open as a page →](./get-metering-costs-summary/) Cost + tokens + brave aggregated by day / contract / model / provider. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `group_by` | query | no | `"day" \| "contract" \| "model" \| "provider"` | | | `ai_company_id` | query | no | `integer \| null` | | | `from` | query | no | `string \| null` | | | `to` | query | no | `string \| null` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of CostSummaryRow` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: ops description: REST endpoints tagged ops — generated from the platform's OpenAPI contract (3 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/ops/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/ops/image-references` Get Image References [Open as a page →](./get-ops-image-references/) Image URLs referenced by articles + plan-vs-generated gaps. Feed for the image-generation usage reconciler. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `sc_id` | query | no | `integer \| null` | | | `page` | query | no | `integer` | | | `page_size` | query | no | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `ImageReferencesPage` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/ops/orchestrator/heartbeat` Post Orchestrator Heartbeat [Open as a page →](./post-ops-orchestrator-heartbeat/) Record the gen-orchestrator's per-tick heartbeat (targets/active/note). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `OrchestratorHeartbeatIn` | Field | Type | Required | Description | | --- | --- | --- | --- | | `active` | `object \| null` | no | | | `note` | `string \| null` | no | | | `targets` | `object \| null` | no | | | `tick_s` | `integer \| null` | no | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `204` | Successful Response | — | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/ops/overview` Get Overview [Open as a page →](./get-ops-overview/) Live platform ops snapshot — poll this (3-5s) to watch the whole platform. **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `OpsOverview` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | --- --- title: API: publish description: REST endpoints tagged publish — generated from the platform's OpenAPI contract (3 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/publish/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `POST /api/v1/publish/article` Publish Article [Open as a page →](./post-publish-article/) Publish one article to its Hugo instance (real the publishing hub flow, async). 404s if the article/instance is unknown; 400 if the instance has no the publishing hub config (so a publish that can never succeed isn't accepted). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `PublishArticleRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `article_guid` | `integer` | yes | | | `instance_id` | `string` | yes | | _Example:_ ```json { "article_guid": 31567, "instance_id": "example-prod-hugo-vps" } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `202` | Successful Response | `TaskAccepted` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/publish/batch` Publish Batch [Open as a page →](./post-publish-batch/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `PublishBatchRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `article_guids` | `array of integer` | yes | | | `instance_id` | `string` | yes | | _Example:_ ```json { "article_guids": [ 31567, 31568 ], "instance_id": "example-prod-hugo-vps" } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `202` | Successful Response | `TaskAccepted` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/publish/deploy` Deploy [Open as a page →](./post-publish-deploy/) **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `DeployRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `full_rebuild` | `boolean` | no | | | `instance_id` | `string` | yes | | _Example:_ ```json { "full_rebuild": false, "instance_id": "example-prod-hugo-vps" } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `202` | Successful Response | `TaskAccepted` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: runs description: REST endpoints tagged runs — generated from the platform's OpenAPI contract (17 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/runs/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/runs` List Runs [Open as a page →](./get-runs/) filtered, paginated run list (default sort createtime:desc). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `status` | query | no | `string \| null` | | | `pipeline_id` | query | no | `string \| null` | | | `entity` | query | no | `string \| null` | | | `contract` | query | no | `integer \| null` | | | `limit` | query | no | `integer` | | | `offset` | query | no | `integer` | | | `sort` | query | no | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `RunListResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/runs` Launch Run [Open as a page →](./post-runs/) validate, create a queued run, stage the fixture, enqueue the job. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `RunLaunchRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `allow_regenerate` | `boolean` | no | | | `entity` | `string` | yes | | | `guid` | `integer` | yes | | | `mode` | `"live" \| "mock" \| "replay"` | no | | | `params` | `object` | no | | | `pipeline_id` | `string` | yes | | | `pipeline_variant` | `string \| null` | no | | | `pipeline_version` | `string \| null` | no | | | `steps` | `array of string \| null` | no | | _Example:_ ```json { "allow_regenerate": false, "entity": "generated_articles", "guid": 31567, "mode": "live", "params": {}, "pipeline_id": "generate" } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `201` | Successful Response | `PipelineRunRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `409` | The record already generated successfully and allow_regenerate is false, or the entity is locked | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | | `429` | Agent token: the concurrent-run cap for this token is reached | `ErrorResponse` | _Example response (201):_ ```json { "createtime": "2026-09-03T02:14:04Z", "current_step": "make_article_structure", "entity_guid": 31567, "entity_name": "generated_articles", "guid": 1842, "metrics": { "llm_calls": 11 }, "mode": "live", "pipeline_id": "generate", "started_at": "2026-09-03T02:14:05Z", "started_by": "agent@entherium.dev", "status": "running", "steps_done": 7, "steps_total": 18, "trigger": "api", "updatetime": "2026-09-03T02:19:40Z" } ``` ## `GET /api/v1/runs/pipelines` List Pipelines [Open as a page →](./get-runs-pipelines/) every registered pipeline definition. **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of PipelineDefRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | ## `GET /api/v1/runs/pipelines/{pipeline_id}` Get Pipeline Def [Open as a page →](./get-runs-pipelines-pipeline-id/) a single pipeline definition. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `pipeline_id` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `PipelineDefRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/runs/preflight` Run Preflight [Open as a page →](./get-runs-preflight/) Can this run launch? Returns can_launch + blocking reasons WITHOUT creating a run, so a caller can check quota, pool and pipeline state before spending a launch. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `pipeline_id` | query | yes | `string` | | | `guid` | query | yes | `integer` | | | `entity` | query | no | `string \| null` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/runs/schedules` List Schedules [Open as a page →](./get-runs-schedules/) every schedule, newest first. **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of RunScheduleRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | ## `POST /api/v1/runs/schedules` Create Schedule [Open as a page →](./post-runs-schedules/) create a schedule (validates pipeline + cron; computes next_fire_at). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `RunScheduleCreate` | Field | Type | Required | Description | | --- | --- | --- | --- | | `cron` | `string` | yes | | | `enabled` | `boolean` | no | | | `entity` | `string` | yes | | | `max_targets_per_fire` | `integer` | no | | | `mode` | `string` | no | | | `name` | `string` | yes | | | `params` | `object` | no | | | `pipeline_id` | `string` | yes | | | `selector` | `object` | yes | | | `steps` | `array of string \| null` | no | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `201` | Successful Response | `RunScheduleRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `DELETE /api/v1/runs/schedules/{schedule_id}` Delete Schedule [Open as a page →](./delete-runs-schedules-schedule-id/) delete a schedule (idempotent). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `schedule_id` | path | yes | `integer` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `204` | Successful Response | — | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/runs/schedules/{schedule_id}` Get Schedule [Open as a page →](./get-runs-schedules-schedule-id/) a single schedule. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `schedule_id` | path | yes | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `RunScheduleRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `PATCH /api/v1/runs/schedules/{schedule_id}` Update Schedule [Open as a page →](./patch-runs-schedules-schedule-id/) partial update; a changed cron recomputes next_fire_at. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `schedule_id` | path | yes | `integer` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `RunScheduleUpdate` | Field | Type | Required | Description | | --- | --- | --- | --- | | `cron` | `string \| null` | no | | | `enabled` | `boolean \| null` | no | | | `max_targets_per_fire` | `integer \| null` | no | | | `mode` | `string \| null` | no | | | `name` | `string \| null` | no | | | `params` | `object \| null` | no | | | `selector` | `object \| null` | no | | | `steps` | `array of string \| null` | no | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `RunScheduleRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/runs/schedules/{schedule_id}/fire` Fire Now [Open as a page →](./post-runs-schedules-schedule-id-fire/) fire now (same seam as the tick-loop / an external Cloud Scheduler). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `schedule_id` | path | yes | `integer` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `ScheduleFireResult` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/runs/schedules/{schedule_id}/runs` Schedule Runs [Open as a page →](./get-runs-schedules-schedule-id-runs/) guids of the runs this schedule produced (params.schedule_id). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `schedule_id` | path | yes | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of integer` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/runs/{run_guid}` Get Run [Open as a page →](./get-runs-run-guid/) run record + its steps + target card + pipeline def. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `run_guid` | path | yes | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `RunDetailResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/runs/{run_guid}/cancel` Cancel Run [Open as a page →](./post-runs-run-guid-cancel/) Cancel a run. A queued run is cancelled immediately; a run that is already executing is marked cancelled but finishes its current execution (mid-flight interruption is not implemented). Idempotent: a terminal run answers 409. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `run_guid` | path | yes | `integer` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `PipelineRunRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `409` | The run is already terminal | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | _Example response (200):_ ```json { "createtime": "2026-09-03T02:14:04Z", "current_step": "make_article_structure", "entity_guid": 31567, "entity_name": "generated_articles", "guid": 1842, "metrics": { "llm_calls": 11 }, "mode": "live", "pipeline_id": "generate", "started_at": "2026-09-03T02:14:05Z", "started_by": "agent@entherium.dev", "status": "running", "steps_done": 7, "steps_total": 18, "trigger": "api", "updatetime": "2026-09-03T02:19:40Z" } ``` ## `GET /api/v1/runs/{run_guid}/log` Get Run Log [Open as a page →](./get-runs-run-guid-log/) assembled log; `step` filters to one step, `tail` keeps last N. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `run_guid` | path | yes | `integer` | | | `step` | query | no | `string \| null` | | | `tail` | query | no | `integer \| null` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `RunLogResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/runs/{run_guid}/retry` Retry Run [Open as a page →](./post-runs-run-guid-retry/) create a new run; `from_step` resumes from a step snapshot. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `run_guid` | path | yes | `integer` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `RunRetryRequest` | Field | Type | Required | Description | | --- | --- | --- | --- | | `from_step` | `string \| null` | no | | _Example:_ ```json { "from_step": "step_15_make_layout" } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `201` | Successful Response | `PipelineRunRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `409` | The run cannot be retried from its current state | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | | `429` | Agent token: the concurrent-run cap for this token is reached | `ErrorResponse` | _Example response (201):_ ```json { "createtime": "2026-09-03T02:14:04Z", "current_step": "make_article_structure", "entity_guid": 31567, "entity_name": "generated_articles", "guid": 1842, "metrics": { "llm_calls": 11 }, "mode": "live", "pipeline_id": "generate", "started_at": "2026-09-03T02:14:05Z", "started_by": "agent@entherium.dev", "status": "running", "steps_done": 7, "steps_total": 18, "trigger": "api", "updatetime": "2026-09-03T02:19:40Z" } ``` ## `GET /api/v1/runs/{run_guid}/steps` Get Run Steps [Open as a page →](./get-runs-run-guid-steps/) the step rows of a run, ordered by seq/attempt. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `run_guid` | path | yes | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of RunStepRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: specs description: REST endpoints tagged specs — generated from the platform's OpenAPI contract (6 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/specs/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/specs` List Specs [Open as a page →](./get-specs/) : list all specs sorted by id descending. **Auth:** `Authorization: Bearer ` required **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `SpecListResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | ## `GET /api/v1/specs/{spec_id}` Get Spec [Open as a page →](./get-specs-spec-id/) : detail with frontmatter, body, sections, tests_status. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `spec_id` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `SpecDetailResponse` | | `400` | Malformed spec id (expected SPEC-NNN) | — | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `404` | Spec not found | — | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/specs/{spec_id}/history` Get Spec History [Open as a page →](./get-specs-spec-id-history/) : git log per file. Empty if.git not mounted. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `spec_id` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of object` | | `400` | Malformed spec id (expected SPEC-NNN) | — | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `404` | Spec not found | — | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/specs/{spec_id}/progress` Get Spec Progress [Open as a page →](./get-specs-spec-id-progress/) Implementation progress grouped by spec section — each leaf §-anchor with its label and whether a test covers it (`SPEC-NNN §X.Y` citation). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `spec_id` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of ProgressSection` | | `400` | Malformed spec id (expected SPEC-NNN) | — | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `404` | Spec not found | — | | `422` | Validation Error | `HTTPValidationError` | ## `PATCH /api/v1/specs/{spec_id}/status` Patch Spec Status [Open as a page →](./patch-specs-spec-id-status/) : status transition with validation. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `spec_id` | path | yes | `string` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `StatusPatch` | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `string` | yes | One of draft/review/accepted/implemented/obsolete | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/specs/{spec_id}/tests` Get Spec Tests [Open as a page →](./get-specs-spec-id-tests/) Return individual pytest test-cases for this spec from junit.xml. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `spec_id` | path | yes | `string` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of SpecTestCase` | | `400` | Malformed spec id (expected SPEC-NNN) | — | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `404` | Spec not found | — | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: sse description: REST endpoints tagged sse — generated from the platform's OpenAPI contract (6 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/sse/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/sse/all` Sse All [Open as a page →](./get-sse-all/) Subscribe to every event channel. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `access_token` | query | no | `string \| null` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/sse/integrations` Sse Integrations [Open as a page →](./get-sse-integrations/) connection status-change events. The integrations health-checker emits `integration.status` whenever a connection's credential or health status changes. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `access_token` | query | no | `string \| null` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/sse/lifecycle` Sse Lifecycle [Open as a page →](./get-sse-lifecycle/) lifecycle SSE channel. Emits: lifecycle.transitioned, lifecycle.autorun.completed, lifecycle.message, lifecycle.batch.completed. Source of truth — LifecycleEngine, which publishes only after session.commit. Multi-worker note: in-memory bus is MVP; Redis-backed migration is (same pattern as runs:sse. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `access_token` | query | no | `string \| null` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/sse/publish` Sse Publish [Open as a page →](./get-sse-publish/) Subscribe to publish/deploy progress events. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `access_token` | query | no | `string \| null` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/sse/runs` Sse Runs [Open as a page →](./get-sse-runs/) Subscribe to pipeline-run events — Redis-backed, multi-worker. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `access_token` | query | no | `string \| null` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `GET /api/v1/sse/workspace` Sse Workspace [Open as a page →](./get-sse-workspace/) Workspace channel. Emits: spec.*, issue.*, defect.*, comment.*, agent_action.* events. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `access_token` | query | no | `string \| null` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: API: tasks description: REST endpoints tagged tasks — generated from the platform's OpenAPI contract (5 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/tasks/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/tasks` List Tasks Endpoint [Open as a page →](./get-tasks/) The read-only task board — paginated, filterable by assignee / status / area. Team-only (owner + team members); an agent or non-team token gets 403. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `assignee_id` | query | no | `integer \| null` | | | `status` | query | no | `string \| null` | | | `area` | query | no | `string \| null` | platform\|pipelines\|instances\|other | | `limit` | query | no | `integer` | | | `offset` | query | no | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `TaskListResponse` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/tasks` Create [Open as a page →](./post-tasks/) Create a task (agent needs ``manage_task``; owner/humans no-op the gate). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `TaskCreate` | Field | Type | Required | Description | | --- | --- | --- | --- | | `area` | `string \| null` | no | | | `assignee_id` | `integer \| null` | no | | | `description` | `string \| null` | no | | | `spec_ref` | `string \| null` | no | | | `title` | `string` | yes | | _Example:_ ```json { "area": "platform", "assignee_id": 2, "description": "Ten key schemas, one example each", "spec_ref": "SPEC-NNN", "title": "Add request examples to the public contract" } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `201` | Successful Response | `TaskRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | | `429` | Task intake rate exceeded; retry after the Retry-After seconds | `ErrorResponse` | _Example response (201):_ ```json { "area": "platform", "assignee_id": 2, "assignee_name": "Owner", "createtime": "2026-09-03T08:00:00Z", "description": "Ten key schemas, one example each", "guid": 118, "owner": 5, "spec_ref": "SPEC-NNN", "status": "open", "title": "Add request examples to the public contract", "updatetime": "2026-09-03T08:00:00Z" } ``` ## `GET /api/v1/tasks/{guid}` Get Task Endpoint [Open as a page →](./get-tasks-guid/) One task + its merged change-log (agent actions and notes, time-ordered). Team-only; 404 if absent. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `guid` | path | yes | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `TaskDetailRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | _Example response (200):_ ```json { "area": "platform", "assignee_id": 2, "assignee_name": "Owner", "createtime": "2026-09-03T08:00:00Z", "description": "Ten key schemas, one example each", "guid": 118, "owner": 5, "spec_ref": "SPEC-NNN", "status": "open", "title": "Add request examples to the public contract", "updatetime": "2026-09-03T08:00:00Z" } ``` ## `PATCH /api/v1/tasks/{guid}` Patch [Open as a page →](./patch-tasks-guid/) Change status (open↔closed) / reassign / edit; logs each field change. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `guid` | path | yes | `integer` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `TaskPatch` | Field | Type | Required | Description | | --- | --- | --- | --- | | `area` | `string \| null` | no | | | `assignee_id` | `integer \| null` | no | | | `description` | `string \| null` | no | | | `spec_ref` | `string \| null` | no | | | `status` | `"open" \| "closed" \| null` | no | | | `title` | `string \| null` | no | | _Example:_ ```json { "description": "Done: examples added to 17 request schemas", "status": "closed" } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `TaskRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `409` | Illegal status transition (e.g. closing an already-closed task) | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | _Example response (200):_ ```json { "area": "platform", "assignee_id": 2, "assignee_name": "Owner", "createtime": "2026-09-03T08:00:00Z", "description": "Ten key schemas, one example each", "guid": 118, "owner": 5, "spec_ref": "SPEC-NNN", "status": "open", "title": "Add request examples to the public contract", "updatetime": "2026-09-03T08:00:00Z" } ``` ## `POST /api/v1/tasks/{guid}/notes` Add Task Note [Open as a page →](./post-tasks-guid-notes/) Append a free-text note to the task's log (stored as a comment). **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `guid` | path | yes | `integer` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `TaskNoteIn` | Field | Type | Required | Description | | --- | --- | --- | --- | | `body_markdown` | `string` | yes | | _Example:_ ```json { "body_markdown": "Verified live: `/api/` answers with the machine map." } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `201` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | | `429` | Task intake rate exceeded; retry after the Retry-After seconds | `ErrorResponse` | --- --- title: API: tickets description: REST endpoints tagged tickets — generated from the platform's OpenAPI contract (3 operations). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/api/tickets/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the OpenAPI contract — do not edit by hand. Interactive explorer with full schemas: [https://entherium.duckdns.org:8443/reference](https://entherium.duckdns.org:8443/reference); frozen spec for this build: [openapi.json](/developers/api/openapi.json). ::: ## `GET /api/v1/tickets` List Board [Open as a page →](./get-tickets/) The owner board — defects/incidents by default, or a vote-ranked suggestion backlog with ``?kind=suggestion``. Team-readable (owner + team, the defect section of the tasks board; agents still get 403. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `kind` | query | no | `string \| null` | defect\|incident\|suggestion (default: the defect/incident board) | | `status` | query | no | `string \| null` | open\|closed | | `component` | query | no | `string \| null` | | | `area` | query | no | `string \| null` | platform\|pipelines\|instances\|other | | `severity` | query | no | `string \| null` | | | `since` | query | no | `string \| null` | tickets created at/after this UTC time | | `limit` | query | no | `integer` | | **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `array of TicketRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `POST /api/v1/tickets` File Ticket [Open as a page →](./post-tickets/) Intake one report; return a bare 202. Write-only fire-and-forget: no ticket id, no dedup verdict — a hostile or looping reporter gets nothing back to react to. Over the flood cap → 429; an oversized body → 413. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `TicketReportIn` | Field | Type | Required | Description | | --- | --- | --- | --- | | `actual` | `string \| null` | no | | | `alternatives` | `array of string \| null` | no | | | `area` | `string \| null` | no | | | `behavior_cases` | `array of string \| null` | no | | | `body` | `string \| null` | no | | | `component` | `string \| null` | no | | | `contract_guid` | `integer \| null` | no | | | `environment` | `object \| null` | no | | | `evidence` | `array of EvidenceItem \| null` | no | | | `expected` | `string \| null` | no | | | `expected_benefit` | `string \| null` | no | | | `fingerprint` | `string \| null` | no | | | `kind` | `string \| null` | no | | | `payload` | `object \| null` | no | | | `problem` | `string \| null` | no | | | `proposed_change` | `string \| null` | no | | | `run_guid` | `integer \| null` | no | | | `severity` | `string \| null` | no | | | `spec_ref` | `string \| null` | no | | | `steps_to_reproduce` | `array of string \| null` | no | | | `title` | `string` | yes | | _Example:_ ```json { "actual": "The Description column is empty", "component": "portal", "environment": { "commit": "dcaf59d1" }, "evidence": [ { "type": "url", "url": "https://enterium-developer-docs.pages.dev/developers/mcp/tools/entherium_create_task/" } ], "expected": "Every parameter has a description", "kind": "defect", "severity": "sev3", "steps_to_reproduce": [ "Open /developers/mcp/tools/entherium_create_task/", "Look at the Parameters table" ], "title": "Tool page renders an empty Description column" } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `202` | Successful Response | `object` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | ## `PATCH /api/v1/tickets/{guid}` Transition Ticket [Open as a page →](./patch-tickets-guid/) Resolve / dismiss / promote a ticket, state-machine-guarded. Owner-only. An illegal transition (e.g. closing an already-closed ticket) → 409. **Auth:** `Authorization: Bearer ` required | Parameter | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `guid` | path | yes | `integer` | | | `Idempotency-Key` | header | no | `string` | Optional. | **Request body:** `application/json` — schema `TicketPatch` | Field | Type | Required | Description | | --- | --- | --- | --- | | `action` | `"resolve" \| "dismiss" \| "promote" \| "target" \| "reopen" \| "edit"` | yes | | | `description` | `string \| null` | no | | | `promoted_to` | `string \| null` | no | | | `resolution` | `string \| null` | no | | | `severity` | `"sev1" \| "sev2" \| "sev3" \| "sev4" \| null` | no | | | `spec_ref` | `string \| null` | no | | | `title` | `string \| null` | no | | _Example:_ ```json { "action": "resolve", "resolution": "fixed in build 0.3.1; verified on the live URL" } ``` **Responses:** | Code | Meaning | Body | | --- | --- | --- | | `200` | Successful Response | `TicketRead` | | `401` | Missing or invalid authentication credentials | `ErrorResponse` | | `422` | Validation Error | `HTTPValidationError` | --- --- title: For LLM agents description: How to consume this portal programmatically — llms.txt, Markdown twins of every page, the frozen OpenAPI contract, and a build manifest with per-page content hashes. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/for-llm-agents/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. This page tells an agent how to read this portal cheaply and reliably. ## Machine surface (contract) | Artifact | Purpose | | --- | --- | | `/developers/llms.txt` | Curated entry points with one-line descriptions, plus an Instructions section | | `/developers/llms-full.txt` | Entire docs as one Markdown document | | `/developers/.md` | Markdown twin of every HTML page — fetch this, not the HTML | | `/developers/manifest.json` | Nav tree + route → content-hash map; compare hashes to detect changed pages | | `/developers/versions.json` | Published doc versions and the current build timestamp | | `/developers/api/openapi.json` | The frozen OpenAPI contract this build's REST reference was generated from | | `/developers/sitemap.md` | Every page as a flat list of `.md` twins — the cheapest full index | | `/developers/agents.md` | What this portal offers an agent and what it does not — read before assuming anything | | `/developers/search-index.json` | Route, title, headings and a text excerpt per page — the index the docs MCP `search` tool ranks | | `/developers/changelog.json` | Release notes as JSON Feed 1.1: portal and API entries plus every agent capability version | | `/.well-known/agent-skills/index.json` | Skills catalog (Agent Skills discovery 0.2.0): `enterium-integration/SKILL.md` with a sha256 digest — `npx skills add` installs it | | `https://entherium.duckdns.org:8443/mcp/docs/` | This documentation as MCP tools (`search` / `fetch` / `submit_feedback`), no token — [details](../mcp/connecting/#the-docs-mcp-no-token) | | `https://entherium.duckdns.org:8443/api/status.json` | Platform status in the Statuspage v2 shape, no token | | `/.well-known/llms.txt`, `/.well-known/api-catalog` | Root-level aliases: the llms.txt convention and the RFC 9727 API catalog (`application/linkset+json`) pointing at the frozen contract, the reference, the health probe and the MCP onboarding page | ## Discovery from any page Every HTML page carries two link relations in `` (llms.txt v2): `` — this page's Markdown twin — and `` — the index that covers it. The mirror also sends them as a `Link:` HTTP header (`rel="llms-txt"`, `rel="llms-full-txt"`, `rel="api-catalog"`) and `X-Llms-Txt`, so a tool that only reads headers finds the index too. Every `.md` twin opens with a one-line directive naming the index, so an agent that arrives by a direct link is never lost. `.md` twins and `manifest.json` are served with an `ETag`: send `If-None-Match` on re-reads and a `304` tells you the page has not changed without transferring it. ## Rules of thumb 1. Prefer `.md` twins over HTML — same content, a fraction of the tokens. 2. To find what changed since your last read, keep the `pages` hash map from `manifest.json` and compare it to the current one — the per-route hash changes only when that page's content does. 3. English pages are canonical; Russian mirrors exist for humans. 4. Mermaid diagrams keep their source in the `.md` twins — read the code block, do not fetch the rendered SVG. --- --- title: Authentication and grants description: How agent tokens are minted, what the grant pool is, the token-replaces-pool gotcha and the two safe ways around it, and every 401/403 cause with its fix. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/guides/authentication/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. The platform has three access tiers: full access (owner/admin), read-only scoped tokens, and **agent tokens** — the tier this page is about. An agent token is a JWT giving broad read access (secrets always redacted) and write access **only** to an explicitly granted pool of contracts. ## How an agent token is issued Only a superuser (the owner) can mint agent tokens — `POST /api/v1/auth/agent-token` rejects everyone else with `403`. An agent can never mint or extend its own token. The owner's request looks like: ```json { "email": "my-agent@example.dev", "expires_days": 90, "revoke_prior": true, "grants": [ { "salescontract": 12345, "capabilities": ["submit", "launch", "cancel", "edit_config"] } ] } ``` - `grants` is **required and non-empty** — a token with no pool is refused (`400`). - `expires_days` (1–365, default 90) sets the JWT's own lifetime. - `revoke_prior: true` (the default) immediately invalidates **all previously issued tokens** for that agent identity. The response contains the `access_token` (send it as `Authorization: Bearer `), its lifetime in seconds, and the agent's user id. ## The grant pool Write permissions live in one table: one row per (agent, contract) with a capability list — a subset of `submit`, `launch`, `cancel`, `edit_config`, `publish`. **Default deny**: no row means no access, and `publish` is withheld by default on production contracts — it must be granted explicitly. Task/defect management is a separate **global** capability (`manage_task`), not tied to any contract. ## ⚠️ The token-replaces-pool gotcha Minting a token **deletes the agent's entire grant pool and inserts exactly what the request listed**. Re-issuing a token "to add one contract" while listing only that contract silently drops every other grant the agent had. Two safe ways to extend a pool: 1. **List the full pool** in the mint request: fetch the current grants first, then pass old + new together. 2. **Additive grant without re-minting**: the sanctioned path is inserting the single new grant row directly (owner operation) — the existing token picks it up immediately, nothing else changes. After any re-mint, an agent should re-check `my_capabilities` before acting. ## If it fails | Response | Cause | Action | | --- | --- | --- | | `401` with a token-format detail | Signature invalid, token expired, or wrong token type | Request a fresh token from the owner | | `401 token revoked` | A newer token was minted for your identity with `revoke_prior: true` — yours is dead even if its expiry date is in the future | Use the newest token; there can be only one | | `401 user not found or inactive` | The agent identity was deactivated | Escalate to the owner | | `403 superuser required` | You called an owner-only route (e.g. token minting) | Don't — ask the owner | | `403` naming a contract or capability | Your token is valid, but the write targets a contract outside your pool, or needs a capability you don't hold | Check `my_capabilities`; request the specific grant | | `403` on a read route | Some read surfaces (e.g. the human task board REST reads) are team-only by design | Use the MCP read tools instead — that is the supported agent path | ## Where authentication is enforced Two layers, both active: a coarse route allowlist for agent writes (only a small set of write routes accept agent tokens at all), and a per-object check inside each handler that verifies the target contract is in your pool with the right capability. MCP tools re-run the same checks in their own bodies — the MCP mount additionally requires a valid token for **every** call. --- --- title: Searching the chat archive description: Querying the archived working chat — parameters of the search tools, and the is_full rule that decides whether a message may be quoted. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/guides/chat-search/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. The platform archives its team working chat and exposes it to search — the history of decisions, requests and incidents often lives there. Agents search it via MCP; team members also have a REST route. ## Via MCP ```json {"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "entherium_search_chat", "arguments": {"query": "publish gate", "chat": "work", "since_days": 30, "limit": 20} }} ``` Parameters: `query` (substring), `chat` (`work` — the main working chat; `agents` — the bot-team chat), optional `author` substring, `since_days` (default 30), `limit` (default 20). The response: ```json {"found": 3, "messages": [ {"at": "…", "author": "…", "text": "…", "is_full": true, "chat": "work"} ]} ``` ## Via REST (team tokens) ```sh curl -fsS "https://entherium.duckdns.org:8443/api/v1/chat/messages?chat=work&q=publish+gate&since_days=30&limit=20" \ -H "Authorization: Bearer " ``` Same filters, plus `offset` for paging. Limits: `since_days` 1–365, `limit` 1–500. ## The `is_full` rule (hard) `is_full: false` means the archived text is **truncated** — the message was recovered through a lossy path and its tail is missing. - `is_full: true` → you may quote the text verbatim. - `is_full: false` → **never quote it as a literal citation.** Paraphrase with an explicit "truncated in archive" caveat, or find the full message by narrowing the search. Misquoting truncated messages as verbatim evidence has burned agents before — evidence built on a truncated quote is treated as fabricated. ## What the archive is good for - **Provenance of decisions** — "who asked for X and in what words" before filing tasks that reference an owner request. - **Incident context** — what was reported live while something was broken. - **Avoiding duplicate work** — many requests found in chat are already done; check the platform's current state before turning a chat message into a task. --- --- title: Agent quickstart description: From zero to your first authenticated Enterium API call — every step is a complete, copy-pasteable command with its expected result. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/guides/quickstart/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. This guide takes an agent from nothing to a successful authenticated API call. Every step is a complete command; no step assumes context from outside this page. ## Preconditions - Base URL: `https://entherium.duckdns.org:8443` — the port is part of the address. Without `:8443` the hostname reaches a different service on the same machine (another certificate, `404`), not this platform. - You need an **agent token**. Agents cannot mint their own tokens — the platform owner issues one for you (see [Authentication](../authentication/) for what the owner does and what your token can do). If you have no token, stop here: access is by invitation only — there is no self-service sign-up or public request form; partners are onboarded by the platform owner. ## Step 1 — verify the platform is reachable ```sh curl -fsS https://entherium.duckdns.org:8443/healthz ``` Expected — HTTP 200 with: ```json {"status": "ok", "service": "", "version": ""} ``` If this fails, the platform is down; do not debug your token. Retry later. ## Step 2 — make your first authenticated read ```sh curl -fsS https://entherium.duckdns.org:8443/api/v1/errors?limit=5 \ -H "Authorization: Bearer " ``` Expected — HTTP 200 with a JSON array (possibly empty) of recent platform error events. If you get `401`, your token is invalid or expired; if `403`, your token type is not allowed on this route — both cases are explained in [Authentication](../authentication/#if-it-fails). ## Step 3 — discover what you may do Your write permissions are scoped to a per-contract grant pool. Check them before attempting any write — via MCP this is one call: - Connect to MCP ([walkthrough](../../mcp/connecting/)) and call `my_capabilities`. The response lists your readable domains and the exact contracts + capabilities your token can write to. ## Step 4 — pick your path | You want to | Go to | | --- | --- | | Understand the platform first | [What is Enterium](../../platform/what-is-enterium/) → [Architecture](../../platform/architecture/) | | Work with tasks and defects | [Tasks and defects](../tasks-and-defects/) | | Investigate a failed run | [How to read errors](../reading-errors/) | | Use the MCP tools | [Connecting to MCP](../../mcp/connecting/) → [Tools reference](../../mcp/tools/) | | Call REST endpoints | [REST API overview](../../api/) | ## Rules that keep you out of trouble 1. **Never assume a write succeeded** — platform write responses tell you `{"accepted": true|false}`; read the value. 2. **Idle is not broken.** Before reporting an outage, read [Interpreting platform status](../../mcp/interpreting-status/). 3. **A new token replaces your grants.** If someone re-issues your token, re-check `my_capabilities` — your pool may have changed. --- --- title: How to read errors description: The platform's single error sink — the shape of an error event, the filters on GET /api/v1/errors, and the standard flow for investigating a failed run. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/guides/reading-errors/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. Every captured failure across the platform lands in one durable **error sink**, readable via one endpoint. If something went wrong, this is where you look first. ## The endpoint ```sh curl -fsS "https://entherium.duckdns.org:8443/api/v1/errors?limit=20" \ -H "Authorization: Bearer " ``` Filters (all optional, combined with AND): | Parameter | Meaning | | --- | --- | | `source` | Where it happened: `http`, `background-loop`, `batch`, `sse`, `startup` | | `operation` | The stable operation label, e.g. `POST /api/v1/runs` or `projector` | | `since` | UTC datetime — only events after this moment | | `limit` | 1–500, default 100; newest first | ## The shape of one event ```json { "guid": 123, "source": "batch", "operation": "generate step_07", "exc_type": "StepError", "error": "…message, truncated at 2000 chars…", "context": { "run_guid": 456, "contract": 789 }, "createtime": "2026-07-23T02:14:05Z" } ``` `context` is a dictionary of relevant identifiers with secrets stripped; it may be null. Use it to cross-reference the run and contract. ## Standard flow: "a run failed — what happened?" 1. Note the run's start time (from the run record or the report). 2. Fetch errors since then, scoped to batch work: ```sh curl -fsS "https://entherium.duckdns.org:8443/api/v1/errors?since=&source=batch&limit=50" \ -H "Authorization: Bearer " ``` 3. Read `operation` (which step), `exc_type` and `error` (what broke), and `context` (which run/contract). 4. If nothing is there, widen: drop `source` — the failure may have been in a background loop (`source=background-loop`) rather than the batch itself. 5. Correlate with the run's `run_step` records: the failed step and the error event should agree. The run record is ground truth for *where* it stopped; the error event for *why*. ## Caveats - **The sink is best-effort by design.** The write happens asynchronously; a crash during process startup (before the event loop runs) may leave only a log line and no event row. Absence of an error event is therefore weaker evidence than presence of one. - **Fail-loud philosophy:** platform steps do not swallow errors into defaults, so a genuinely failed run will be visible either here or in the run's step records — silence plus a completed run means success, not a hidden failure. --- --- title: Tasks and defects description: The working cycle of the platform's board — task states, filing a defect that actually lands, quarantine rules, ticket actions, and what agents can and cannot do. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/guides/tasks-and-defects/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. The platform tracks its own work on a board with two kinds of records: **tasks** (assignable work items) and **tickets** (defects, incidents and suggestions). Agents participate through MCP tools; humans through the web board and REST. ## Task lifecycle A task has exactly two states — `open` and `closed` — plus an assignee and an append-only note log. Agent operations (all require the global `manage_task` grant): | Do | MCP tool | | --- | --- | | Create a task | `entherium_create_task` | | Change status | `entherium_update_task_status` | | Reassign | `entherium_reassign_task` | | Append a note | `entherium_log_task_note` | | Link to a SPEC | `entherium_target_task_spec` | | List / read | `entherium_list_tasks`, `entherium_get_task` | Two things weak agents get wrong: 1. **REST reads of the board are team-only by design.** An agent token gets `403` on `GET /api/v1/tasks` even though it has read access elsewhere — this is intentional; use the MCP read tools. 2. **`note_count` does not contain the notes.** A task list row tells you how many notes exist, not what they say — call `entherium_get_task` before concluding anything about a task's history. ## Filing a defect that actually lands File via `entherium_report_defect` (or REST `POST /api/v1/tickets`). The REST route deliberately answers a bare `202` with no id and no verdict; the MCP tool answers `{"accepted": true|false}`. **Quarantine rule — the one that matters:** a defect report with **neither** reproduction steps **nor** a URL/value evidence item is quarantined: it is recorded in the raw intake only, creates no board ticket, and expires. If you want your defect to exist, include at least one of: - `steps_to_reproduce` — ordered, literal steps; - `evidence` — items with a `url` or a concrete `value` (a number, a quote). **Deduplication is automatic.** Reports are fingerprinted (kind + component + title): a duplicate of an open ticket merges into it; a duplicate of a ticket closed as *fixed* **reopens it as a regression**; a duplicate of a ticket closed as *dismissed* is silently dropped. ## Ticket actions and their traps Tickets are `open`/`closed` with a `resolution`. The PATCH actions (owner-level for state changes): | Action | Effect | Trap | | --- | --- | --- | | `resolve` | Closes with `resolution: "fixed"` | It **always** writes `fixed` and ignores any resolution you pass — to close for another reason, use `dismiss` | | `dismiss` | Closes with `duplicate`, `not_real` or `wont_fix` | Wrong pairing of action and resolution → `422` | | `reopen` | Reopens a closed ticket, clears the resolution | — | | `edit` | Updates severity / title / description in any state | — | | `target` | Links the ticket to a SPEC | The SPEC id is validated — a nonexistent one is rejected | `resolve`/`dismiss` on an already-closed ticket returns `409 illegal transition` — check state before transitioning. ## The quality bar for defects A ticket is only useful if it is **verified**: reproduced, or backed by a file:line from current code, a query result with a real number, or a live URL. The platform's culture treats an unverified ticket as worse than none — it sends someone to fix nothing. State the root cause you checked, not the symptom you assumed, and quote evidence literally. --- --- title: How the agent trust model works description: The platform treats LLM agents as first-class but untrusted users — a ladder of token scopes, default-deny grants, redaction, and anti-enumeration by design. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/agent-trust-model/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** this platform is *operated by* LLM agents — they read state, file defects, launch runs. An agent is a legitimate user and an untrusted one at the same time: it hallucinates, it retries, it can be prompt-injected by content it reads. Classic RBAC assumes a user who mostly behaves; this platform assumes one that sometimes won't. ## Layer 1 — the token ladder Three access tiers, in increasing power: | Tier | Can read | Can write | | --- | --- | --- | | Read-only scoped | An allowlist of GET surfaces | Nothing | | **Agent** | The platform, with secrets always redacted | Only its granted contract pool | | Owner | Everything | Everything | The agent tier is the interesting one: reads are broad (an agent that cannot see cannot reason), but every response passes recursive **secret redaction** — credentials never reach an agent even when it reads the config that contains them. ## Layer 2 — default-deny grants with two enforcement layers Write access lives in one table: one row per (agent, contract) with an explicit capability list. **No row = no access**; the sensitive `publish` capability is withheld by default even inside a granted pool. Enforcement is two-layered — a coarse route allowlist (only a small set of write routes accept agent tokens at all), then a per-object check inside each handler that the *specific target contract* is in the caller's pool with the *specific capability*. The second layer is what stops the classic BOLA failure: a valid token quietly writing to object it was never granted. ```mermaid flowchart LR T[Agent token] --> L1[Layer 1
route allowlist] L1 --> L2[Layer 2
object-level grant check] L2 --> W[Write executes] L1 -- route not agent-writable --> R1[403] L2 -- contract not in pool --> R2[403 naming the gap] ``` MCP calls bypass none of this: the MCP mount requires a valid token for every call, and each tool re-runs the same capability checks in its body. ## Layer 3 — discovery for the weakest agent The platform assumes the *least* capable client, not the most: an unauthenticated onboarding page, a curated `llms.txt`, an MCP `start_here` tool returning an ordered plan, and — the hard-won lesson — **meaning lives in payload values, not schemas**, because many agents never introspect tool descriptions. Everything an agent must know to behave correctly is in the data it actually reads. ## Layer 4 — untrusted input surfaces Where agents *submit* content, the platform treats them as a hostile stream: defect reports pass a gate chain (rate limit → dedup fingerprint → actionability check), and the public intake answers a **bare 202** — no ticket id, no verdict — so the endpoint cannot be used to enumerate the board. A report without reproduction steps or evidence is quarantined rather than trusted. ## Trade-offs Redaction-by-default occasionally hides something an agent legitimately needs — the resolution is an explicit grant, never a bypass. The bare-202 intake means a well-behaved agent can't confirm its report landed; the answer is the separate authenticated read surface (the board tools), not a leakier intake. And re-minting a token **replaces** the whole grant pool — a sharp edge documented prominently in [Authentication](../../guides/authentication/#-the-token-replaces-pool-gotcha). ## See it in two minutes Call `my_capabilities` over MCP — the response is your exact trust envelope: readable domains, writable contracts, capabilities per contract. --- --- title: How copywriting evaluation works description: Two co-equal gates — TRUE∧ACCURATE and EASY∧PLEASANT to read — enforced by adversarial screening, evidence-anchored judging and an editor-grade reading rubric. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/copy-evaluation/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** "is this article good?" hides two independent questions — *is it true* and *is it worth reading* — and optimizing either alone produces failure: accurate-but-unreadable, or delightful fabrication. The platform's doctrine makes them **two co-equal gates**: an article ships only in the top-right quadrant. ```mermaid quadrantChart title The two co-equal gates x-axis Hard to read --> Easy and pleasant y-axis Fabricated --> True and accurate quadrant-1 Ships quadrant-2 Rewrite style quadrant-3 Reject quadrant-4 Fix the facts ``` ## Layer 1 — the truth gate Fabrication hunting is adversarial: strip every claim, then try to ground each one. Four grounding classes are accepted — a real source URL from the article's own research bundle, persona facts from the author's bio, brand specifics from the company's declared profile, and reviewer-confirmable textbook knowledge of the niche. Everything else is a fabrication finding. The class list exists because each one was once a false positive that burned review time — codifying them made the hunt repeatable. ## Layer 2 — the readability gate Readability is judged by a structured **editor-grade reading rubric** — an actual read-through scored on defined axes, not a proxy metric. It is deliberately *not* an LLM score: cheap LLM judges measurably anti-correlated with human quality ratings on this workload (see [Quality gates](../quality-gates/)), so the reading gate stayed human- calibrated. An anti-template battery backs it mechanically: banned AI-vocabulary replacements, banned heading shapes ("Core concepts behind X…"), and a corpus-level twin-similarity pass that catches articles converging on the same skeleton. ## Layer 3 — evidence-anchored judging, where LLM opinion is used at all Where an LLM verdict participates (as advisory signal, never as the gate), it is structurally constrained: a locked 0/1/2 rubric per category, a **verbatim quote required for any non-zero score** — checked mechanically as a substring — multiple samples with a median, and a judge from a different model family than the writer. A judge that cannot cite cannot praise. The full formal judge (with human-set calibration) remains an analytics instrument: its calibration study is not finished, so it has not earned gate status — a deliberate application of the platform's own "deterministic decides, LLM advises" doctrine to itself. ## Layer 4 — after publication Every published article carries a quality stamp computed from the deterministic automatic categories (structure, AI-marker absence, technical SEO) minus defect penalties — brand-safety hits flip the *verdict*, not the number, so a beautiful article that promotes the wrong brand fails loudly rather than averaging out. The designed next phase — a "rising tide" that re-enriches old articles as the research corpus matures — is on the roadmap, not yet built. ## Trade-offs Human-calibrated reading gates do not scale like a metric — that is the accepted cost of the measured anti-correlation result. And the two-gate doctrine means an article can loop: fixed facts can degrade flow, style rewrites can drift facts; the loop converges because each gate re-checks after the other's edit, but convergence costs iterations. ## See it in two minutes Any article's quality stamp and signal battery are readable through the API (`quality` domain); the anti-template ban lists live in the generation pipeline's version directory — versioned like everything else. --- --- title: How cost accounting works description: Token-accurate, multi-dimensional metering — how the platform knows the true cost of one published article, and what that number changes. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/cost-accounting/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** an LLM platform's unit economics live or die on one number — *what does one published article actually cost?* — and that number is surprisingly hard to get. Flat per-call estimates drift from reality (here, a flat constant once under-counted the platform's dominant cost driver by ~6×); provider dashboards aggregate by account, not by article. ## Layer 1 — the immutable usage event Every run emits one immutable **usage event** — broken down by model, with per-step token deltas kept alongside the run — carrying the full attribution vector: tenant, contract, record, pipeline, step, model, token counts, timestamp. Not a log line — a queryable ledger row. Nothing about cost is derived at call time; events store *facts* (tokens, model), and money is computed later. ```mermaid flowchart LR CALL[LLM call] --> UE[(usage_event
tokens · model · contract ·
pipeline · step · time)] PB[Versioned pricebook] --> COST[Cost queries] UE --> COST COST --> A[Cost per article] COST --> B[Cost per contract / day] COST --> C[Reconciliation vs provider invoice] ``` ## Layer 2 — the versioned pricebook Prices change; history must not. Cost is computed by joining events against a **versioned pricebook** — the price valid *at event time* — so last month's numbers stay true after a provider reprices. The same join reconciles the computed total against the provider's actual invoice: if the ledger and the bill diverge, something is unmetered, and that is a defect, not a rounding note. ## Layer 3 — the cost of one article, amortized "Cost per article" includes more than its Generate run: Collect research that fed it is amortized across the articles it enabled. This is the number the platform actually steers by — it exposed, for example, that a period of post-gate rejects multiplied the *effective* cost per published article several-fold even though per-call prices never moved. Without per-article attribution that regression is invisible inside a monthly total. ## Layer 4 — budgets (designed, not yet armed) Metering is meant to feed enforcement: per-run budgets with a watcher that would stop a breaching tenant's runs through the run control channel. The enforcement stage is not enabled yet: no budgets are configured, and the control channel the watcher would use is the one the [run engine](../run-engine/#trade-offs) documents as not interrupting a run mid-flight. There is deliberately **no automatic provider-level kill switch** either — an automatic billing cut-off takes production down with the runaway it was meant to stop — so the working control today is a daily spend-anomaly signal in the nightly report: it notifies the owner, and a human decides. This page will change when budgets are configured and the watcher is measured. ## Trade-offs Metering every call costs a write per call and a discipline: any new LLM call path must emit the event, and the reconciliation check exists precisely to catch paths that forget. Amortization rules are a modeling choice — the platform records the raw vectors so the model can be changed retroactively without losing data. Absolute figures — per article, per night, per month — are deliberately not published on this portal: the mechanism is public, the ledger is not. ## See it in two minutes The `metering` domain in `list_domains` exposes the read surface; per-run costs appear in run reports. --- --- title: How immutable pipeline versions work description: Why a pipeline change is always a new carve, never an edit — and how that makes every past run reproducible and every rollback a pointer move. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/immutable-pipeline-versions/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** an LLM content pipeline changes constantly — prompts, gates, step order. If the code that produced yesterday's thousand articles can be edited in place, you lose the ability to answer the only question that matters when quality regresses: *what exactly produced this article?* The naive answer — "check git history" — fails operationally: a running fleet doesn't execute git history, it executes whatever is on disk, and a "small fix" deployed at noon silently changes what the nightly run does. ## Layer 1 — a version is a directory, not a ref Every pipeline version is a **complete, immutable code directory**: `pipelines/{id}/{variant}/{version}/` with its own `manifest.yaml`. Once registered, the directory is never edited. A change — however small — becomes a **carve**: byte-copy the parent version, apply the change, register the new version in the catalog. ```mermaid flowchart LR A[v3.1.16
frozen] -- carve: copy + patch --> B[v3.1.17
frozen] B -- carve --> C[v3.1.18
latest] A -.rollback target.-> CAT[catalog.yaml
latest pointer] C -.-> CAT ``` The manifest is the contract: the dispatcher reads it — steps, required environment, entrypoints — and never introspects the code. Old versions stay listed and stay runnable. ## Layer 2 — resolution and pins Which version runs is resolved at dispatch time: **explicit request → per-contract pin → catalog `latest`**. The nightly production run additionally pins its Collect/Generate versions explicitly, which turns promotion into a deliberate two-step: land the version, then move the pin. A regression discovered between those steps never touches the fleet. ## Layer 3 — what this buys - **Forensics.** Every run records its version; the version can never change under it. "Which code wrote this sentence" has an exact answer, months later. - **Instant rollback.** Move the pointer back. No revert commits, no redeploys of changed files, no "mostly the same" ambiguity. - **Safe parallelism.** Multiple agents carve future versions while production stays pinned; nothing they do can destabilize the running code. - **Honest A/B.** Two versions coexisting in the catalog can run against live contracts and be compared on output, because neither is a moving target. ## Trade-offs Immutability accumulates directories. The retention policy keeps the HEAD tree small — live-pinned versions, `latest` and its predecessor, and named rollback targets survive; everything else is deleted from the tree by an explicit, owner-acknowledged cleanup (history stays in git, and every removed version leaves a tag it can be restored from). Version numbering carries meaning: a new **major** marks an architectural generation (for Collect, v3.x is the vector-ranking generation; v2.x — the keyword one — remains listed as the emergency fallback). ## See it in two minutes Fetch the catalog surface and the current pins through the API, or simply read the machine-readable manifest of this documentation build — the same philosophy applies to these docs: `versions.json` + `manifest.json` with content hashes, at the root of this portal. --- --- title: How localization works description: English-pivot transcreation — one canonical content pipeline, a separate localize pipeline with its own deterministic QA gate, and one shared research corpus across all languages. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/localization/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** running the full Collect → Generate → verify machinery per language multiplies cost and splits the research corpus into per-language silos that each mature slowly. The naive alternative — "just translate" — ships literal translations that fail native reading and lets translation errors bypass every quality gate built for English. ## Layer 1 — English is the pivot The main pipeline stays **English-only**: collection, generation, grounding and all quality gates operate on one canonical English article. Localization is a **separate, downstream pipeline** with four steps: select → transcreate → QA gate → publish. Nothing about the main machinery changes per language. ```mermaid flowchart LR CORPUS[(One research corpus
per contract)] --> GEN[Generate EN
all gates apply] GEN --> MASTER[Canonical EN article] MASTER --> LOC[Localize pipeline
select → transcreate → QA → publish] LOC --> L1[es] LOC --> L2[de] LOC --> L3[zh …] ``` The key economic consequence: **the research corpus stays unified.** Every language version grounds in the same facts, so the [RAG flywheel](../rag-flywheel/) spins at full speed regardless of how many languages ship — a per-language corpus would divide its momentum. ## Layer 2 — transcreation guarded by a deterministic gate The localize QA gate mirrors the platform's [quality doctrine](../quality-gates/): deterministic checks first, LLM opinion last and only if everything else passed (an LLM verdict costs money — spending it on a draft that already failed a mechanical check is waste). The mechanical battery: - **Structure parity** against the English master — sections, lists and FAQ must survive transcreation; - **Per-script length bands** — expected length ratios differ by writing system (CJK compresses; the gate knows); - **Language-leak checks** — English fragments left in the body, or worse, a third language substituted wholesale; - **Foreign-script detector** — a run of characters from an unexpected script is an automatic flag. ## Layer 3 — publishing without breaking identity Language versions publish as **filename-paired variants** of the master page on the same site, giving the site generator what it needs for language switchers and `hreflang` — while the canonical published-URL registry keeps tracking the master article (locale pages record their own live URLs alongside, without competing for the master's identity key). ## Trade-offs Transcreation through an LLM is a quality risk by definition — that is why the gate is deterministic-first and why a failed draft is held, not auto-retried. And operationally: the localize pipeline is currently registered for the **dev** contour only — the code has ridden to the production branch, but no production catalog variant exists yet, so no production dispatch can reach it. It is documented here as architecture that exists, not as a switched-on production feature. ## See it in two minutes The pipeline catalog lists `localize` with its variants — the honest switched-on/not surface — and a localized article's variants are visible right on the destination site's language switcher once a contract uses it. --- --- title: How media generation works description: Per-section infographic-or-illustration decisions, a zero-cost deterministic chart renderer beside a generative model, and an image registry that makes storage cleanup safe. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/media-generation/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** article images are where cost, quality and storage rot collide — generative images cost money per picture, orphaned files accumulate forever, and a naive cleanup that guesses wrong deletes images off live pages. ## Layer 1 — infographic or illustration, decided per section The media step doesn't decorate the whole article uniformly. Each content section is classified by a ~10-signal heuristic (density of numbers, comparisons, list shapes…): data-heavy sections get an **infographic** — a chart spec rendered deterministically in a headless browser, costing effectively zero per image — narrative sections get a **generative illustration** from an image model, costing fractions of a cent. The platform's own bias shows here: where the content is numeric, deterministic beats generative on both truth and price. ```mermaid flowchart TB SEC[Article section] --> C{Classifier
~10 signals} C -- data-heavy --> IG[Infographic
chart spec → headless render
deterministic] C -- narrative --> IL[Illustration
image model] IG --> ST[(Object storage)] IL --> ST ST --> ART[Rendered into the article] ``` The media service itself is a small vendored cloud function — stateless, storage-backed, with usage stats in memory; the platform calls it through a typed adapter like any other integration. ## Layer 2 — quota safety Generative quota exhaustion once looked like a mysterious 500-storm. Now the adapter surfaces the provider's retry-after hint, applies a cooldown floor, and a **quota breaker** trips after consecutive refusals — the run fails fast with an explicit quota verdict instead of burning minutes in retries. ## Layer 3 — the image registry: cleanup without fear Every stored image is tracked in a registry with an explicit lifecycle: ```mermaid stateDiagram-v2 [*] --> generated: created generated --> in_use: referenced by an article generated --> orphan_candidate: unreferenced past grace orphan_candidate --> in_use: reference appears orphan_candidate --> trashed: cleanup pass trashed --> [*]: storage lifecycle deletes after retention in_use --> orphan_candidate: references removed ``` The load-bearing decisions: - **References are reconciled from the real render.** The platform reports which images its articles actually reference — including a scan of the final HTML, added after the field-level extractor was measured to miss the vast majority of real references. The registry believes the rendered truth, not the metadata. - **There is no hard delete.** Cleanup can only *trash* — a retention window in object storage does the physical deletion later. A wrong orphan verdict is recoverable for weeks; a live cleanup pass once reclaimed most of the bucket with zero lost live images. ## Trade-offs Two storages exist by design — object storage for pipeline-generated media, the site hosts' filesystem for site chrome (avatars, hero images filled by a separate path) — and the registry governs only the former; unifying hero-image handling is a known open edge. The per-section classifier is heuristic: a borderline section can get a chart where an illustration would read better, a price accepted for determinism. ## See it in two minutes Any article's image plan and generated assets are visible in its telemetry; the registry's reference snapshot endpoint shows exactly what the platform currently claims to use. --- --- title: How observability and alerting work description: Three error primitives that make silence impossible, a durable error sink, and an alerting philosophy built on one rule — "zero useful output" is a failure even when nothing crashed. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/observability-alerting/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** an autonomous nightly platform fails in two very different ways — loudly (an exception) and *quietly* (everything exits 0 and produces nothing). Classic monitoring catches the first kind and is blind to the second — a fleet can produce zeros all night with every dashboard green. This design starts from that second kind. ## Layer 1 — three primitives that make silence structural­ly hard All error handling funnels through three small primitives: | Primitive | For | Guarantee | | --- | --- | --- | | `capture_exception` | Any caught error | Never raises; logs + metric + durable `error_event` row (secrets stripped) in one call | | `guarded(...)` | One-shot batch operations | Catches everything, captures, returns None — a batch step cannot take the process down | | `supervise(...)` | Long-running background loops | On crash: capture + restart with backoff — a loop cannot die silently | The third one is the important lesson: the projector, watchdogs, health monitor and autoscaler all run under supervision, because a background loop that dies quietly turns into "the platform looks fine and does nothing" — the worst failure class of an autonomous system. ```mermaid flowchart TB ERR[Any caught error] --> CAP[capture_exception] CAP --> LOG[structured log] CAP --> MET[error counter] CAP --> DB[(error_event
durable sink)] subgraph loops [Supervised background loops] P[projector] --- W[watchdog] --- H[health monitor] --- A[autoscaler] end loops -- crash --> CAP loops -- restart with backoff --> loops ``` The sink is queryable (`GET /api/v1/errors` — see [How to read errors](../../guides/reading-errors/)); the write is best-effort by design, so the structured log line remains the last-resort record for a crash before the event loop exists. ## Layer 2 — the "zero is a failure" alert doctrine The nightly run ends with a self-assessment that distinguishes three bad endings — and the third is the doctrine: ```mermaid flowchart LR RUN[Nightly run ends] --> Q{Assess} Q -- half of collects failed --> C[collapse:
exit 1 → alert] Q -- generated > 0, published 0 --> PF[publish_fail:
exit 1 → alert] Q -- all green but zero qualified --> Z[all_zero:
exit 0, still alarmed
via dead-man's switch] Q -- normal --> OK[report + success ping] ``` **`all_zero` is the interesting one:** nothing crashed, every exit code is clean — but a platform whose whole night produced zero useful output is failing at its job. Infrastructure monitoring cannot see this; only an outcome check can. ## Layer 3 — the dead-man's switch Alert delivery itself can die (the host, the network, the messaging credentials — or the same cloud account that runs production). So the final tier is inverted: the nightly job **pings an external service on success and on failure**, and *the absence of any ping* is what raises the alarm — from infrastructure deliberately independent of the platform's own account. A provider-level kill switch that takes down production cannot take down the thing watching production. ## Trade-offs The full typed-alert pipeline (severity levels, dedup keys, a rules-driven dispatcher, distributed tracing) is designed and approved but parked — what runs today is the Phase-0 belt above: three outcome modes, the error sink, and the dead-man's switch. The page will change when the rest ships; until then, this is the honest inventory. There is also no metrics-scrape endpoint yet — counters exist, dashboards do not. ## See it in two minutes `GET /api/v1/errors?limit=5` shows the sink live; the nightly report (the `cycles` domain) carries the outcome assessment of the latest run. --- --- title: How ontologies and filtering work description: Every contract declares what it is about — and that declaration drives candidate ranking, draft alignment and brand defense, with filters that each own exactly one concern. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/ontologies-and-filtering/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** "relevant to the niche" is not a vibe — it has to be a computable property, or discovery drifts and brand safety becomes luck. The platform's answer: make the niche an explicit, derived **ontology**, and make every filter a separate gate with one owner. ## Layer 1 — the ontology is derived, not hand-typed Each contract's niche ontology is **derived from its brand description** (the same source that feeds brand voice) plus the contract's own structural pages — About and Services, not articles. Empty inputs fail loud: a contract that cannot say what it is about cannot collect. The result is a tiered term structure: core concepts, actions, ecosystem entities, user intents. ```mermaid flowchart LR BD[Brand description +
structural pages] --> O[Niche ontology
tiers · core · actions ·
ecosystem · intents] O --> Q[Curated search queries] O --> CMP[Per-candidate comparison] O --> GEN[Draft alignment in Generate] ``` ## Layer 2 — per-candidate taxonomy, then comparison During discovery every surviving candidate gets its **own** small LLM-built taxonomy in the same shape — and scoring compares taxonomies, not keywords: term overlap across tiers plus a domain affinity bonus, blended into the candidate's overall rank. "Is this about our niche?" becomes a structured comparison instead of substring luck. ## Layer 3 — filters with one owner each Filtering is deliberately not one big function. Each gate owns a concern (the same charter philosophy as the [writing framework](../writing-framework/)): | Gate | Owns | Verdict | | --- | --- | --- | | URL dedup | "have we seen this exact source" | delete | | Per-domain cap | funnel diversity | cap | | LLM screener | cheap topicality pass/fail | drop | | Ontology comparison | niche fit, structured | score | | Topic dedup | semantic novelty vs committed topics | **defer** (reversible) | | Export policy | rank floor + capped bands | never seeded | ## Layer 4 — brand defense as declarations plus signals The ontology says what the contract *is* about; brand-defense declarations say what it must *not* promote. Competitor lists live in the brand description as explicit sections — maintained by an automated candidate miner (signals: donor gravity, corpus frequency) with an owner-approved lifecycle, so the lists cannot silently rot. Downstream, a battery of deterministic signals watches the *rendered result*: donor on a competitor domain, competitor name in the title, recommendation-verbs near a competitor, third-party promotion even for brands never declared (a list-independent rule: *promote only your own*), buyers-guide shapes, outbound links. A brand-safety hit doesn't lower the quality number — it flips the **verdict** (reject/rework), a deliberate separation of "how good is the text" from "may this ship". ## Trade-offs Filtering by declared ontology means the ontology's quality is the ceiling — a vague brand description yields vague collection, which is why derivation fails loud instead of falling back to defaults. Competitor filtering is intentionally *absent* at the discovery stage (a competitor URL can be a legitimate research source); defense is enforced where intent is visible — in what gets written and published. ## See it in two minutes Any candidate row carries its ontology comparison and rank; any published article carries its brand-safety signal battery — both readable through the API. --- --- title: How nightly orchestration works description: What actually drives the fleet every night — a deliberately boring batch script with layered safety nets — and the reconciler-first design it is converging toward. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/orchestration/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** "orchestration" invites over-engineering. This platform's honest answer today is a deliberately boring one — a timer-driven batch script with explicit pins and layered safety nets — plus a documented target design (durable intent + reconciliation) it may grow into. This page describes both and says plainly which is which. ## Layer 1 — the real thing: the nightly batch ```mermaid gantt dateFormat HH:mm axisFormat %H:%M title The nightly window (conceptual) section Per contract Collect (pinned version) :a, 00:00, 45m Generate (queue backpressure) :b, after a, 90m Publish + hub deploy :c, after b, 30m Live-URL verify + stamp :d, after c, 10m section Safety nets Publish sweep (same run) :e, after d, 15m Reconcile timers (every 4h) :f, 00:00, 24h Outcome watchdog (late morning) :g, 11:00, 10m ``` A scheduled timer starts a plain batch script that walks the active contracts: Collect (explicitly pinned version) → generation under a queue-depth backpressure gate → publish → hub deploy → live-URL verification → stamp. No daemon, no workflow engine — an auditable script whose every phase leaves records. ## Layer 2 — the safety nets are the architecture The interesting design is not the happy path but the nets under it: - **Publish sweep** — at the end of the same run, anything generated but not yet published gets a second chance immediately. - **Reconcile timers** — two independent recurring jobs (publish-side and collect-side) re-drive stranded work every few hours; a crashed run's leftovers don't wait for tomorrow. - **Outcome watchdog** — late morning, a separate timer checks that the night *produced an outcome at all* — the ["zero is a failure" doctrine](../observability-alerting/) applied to the whole night. - **Run-level self-healing** — the [run engine's](../run-engine/) watchdog and reaper handle stuck runs and abandoned jobs below this layer. Redundancy here is deliberate: each net catches a failure class the others cannot see. ## Layer 3 — the target design (status: designed, not built) The documented next generation is **reconciler-first**: each contract's day becomes a durable *cycle record* (intent), and a single loop continuously compares intent against observed facts, taking one idempotent step at a time — the pattern that would replace scattered safety nets with one convergence engine, add a proper admission gate and provider-aware submission breaker. **Status: designed, not built.** No stored cycle *record* (intent) exists yet — the `cycles` API domain that shows each night's funnel derives it after the fact from run and publication records; the current admission control is the batch script's own backpressure; a general cron-scheduler for individual runs is implemented but not enabled in production. This page will change when that changes. ## Trade-offs A batch script is single-shape: contracts run in a fixed pattern, and mid-run steering is limited (a running pipeline completes rather than stopping mid-step; see [the run engine](../run-engine/#trade-offs)). The layered nets mean recovery logic lives in several places — exactly the dispersion the reconciler design wants to collapse. The platform chose to ship the boring version first and measure where it actually hurts; the design doc exists because those measurements now do. ## See it in two minutes The `cycles` API domain shows each night's funnel — dispatched, completed, failed, recovered — which is this whole page in numbers. --- --- title: How publishing and the site fleet work description: Three independently-deployed layers — the platform, the publishing hub, and a fleet of static Hugo sites — and the identity discipline that keeps published URLs stable. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/publishing-and-the-fleet/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** one platform publishes to many sites, each with its own brand, domain and theme. Coupling the content engine to site rendering makes every theme change a platform deploy and every platform bug a fleet outage. The design answer is three layers that deploy independently. ## Layer 1 — the three-layer topology ```mermaid flowchart TB P[Platform
generates + gates articles] -- publish payload --> HUB[Publishing hub
separate microservice:
instance registry · renderers · builders] HUB -- render to Hugo content --> T[Per-brand site templates
shared theme + brand overrides] T -- build + deploy --> FLEET[Fleet of static sites
served by nginx] FLEET -- verified live URL --> P ``` The **hub** is a vendored microservice with its own instance registry: per-site records carrying template, status, deploy target and public URL. The registry — not the platform database — is the hub's source of truth for *where* each brand lives. The platform hands the hub a finished article; everything from "markdown file in the right folder" to "site rebuilt and rsynced" is the hub's business. ## Layer 2 — the publish flow, end to end ```mermaid sequenceDiagram participant P as Platform participant H as Hub API participant A as Hugo adapter participant B as Builder participant S as Site host P->>H: publish-live (article payload) H->>A: upsert by source_url
(article registry — republish ≠ duplicate) A->>B: build site (hugo --minify) B->>S: deploy (rsync to web root) P->>S: fetch the live URL S-->>P: 200 → publication counts ``` Two details carry the reliability: - **Upsert by source URL.** The hub keeps its own article registry keyed by the source URL, so republishing an improved version replaces the page instead of minting a duplicate. - **Slug identity lives on the platform side.** The published-URL registry keys identity as *(instance, normalized source URL)* — deliberately not the article's internal id, which changes on regeneration. A regenerated article **reuses its frozen slug**; a brand-new source mints one. This is what protects accumulated SEO equity from being reshuffled by routine regeneration. ## Layer 3 — brand configuration is code, not server state Branding (title, logo, colors, taxonomies, the author roster) lives in **git templates** per brand over a shared theme. A hand-edit on the web host survives only until the next build — by design: the template is the single source of truth, and every deploy reasserts it. New sites start as **staged port-instances** (reachable, no domain) and convert to production domains in a short, reversible procedure (DNS → vhost → registry URL → platform record). ## Layer 4 — deploying the layers themselves Each layer ships on its own cadence, with its own invariants: - **Platform** — a four-phase deploy script: preflight (refuses while runs are active; snapshots the secret surface), code swap with a timestamped backup, container rebuild, postflight (health-wait + re-hash of the secret surface — an unexpected diff aborts). Database migrations apply automatically at container boot. - **Hub** — rebuilt from its repo; a template change redeploys per instance without touching the platform. - **This documentation** — built statically, shipped as an immutable release directory with a symlink flip; rollback is flipping the symlink back. ## Trade-offs Three layers mean three registries of truth (platform DB, hub instance registry, git templates) — and the operational cost is keeping their pointers aligned; misaligned registry state has produced "instance not found" incidents. The verified-live-URL rule also means publishing is only as done as the slowest site build — a queue at the hub delays proof, not just delivery. Both costs are accepted in exchange for fleet-wide brand changes that never require touching the content engine. ## See it in two minutes Any published article row carries its live URL; fetch it — that URL responding is the platform's own definition of "published". The `instances` API domain lists the fleet the hub serves. --- --- title: How quality gates work description: Why the publish path is guarded by deterministic gates instead of LLM judges — with the measurements that forced that design. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/quality-gates/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** an autonomous pipeline that publishes without human review needs something on the publish path that says *no*. The obvious 2024-era answer — "have an LLM judge the article" — was tried here and measured. It failed. ## The measurements that set the design Two results shaped everything: 1. **Cheap LLM judges anti-correlate with human quality ratings** on this workload — articles the judge scored highest, humans scored lower. A gate built on that signal would have *inverted* quality. 2. **Naive corpus-grounding produced ~73% false positives** when used to detect fabricated claims — checking a sentence against the corpus flags mostly innocent phrasing, not invention. The conclusion is the platform's quality doctrine: **deterministic gates decide; LLMs only advise.** Where an LLM verdict is unavoidable, it must be *evidence-anchored*: a verbatim quote from the text is required for any non-zero score, and the quote is mechanically checked to actually occur as a substring — a judge cannot praise what it cannot cite. ## The gates on the publish path ```mermaid flowchart LR ART[Candidate article] --> D[Topic dedup
embedding similarity] D --> G[Claim verification
grounding gate] G --> Q[Quality scoring
brand + language signals] Q --> PUB[Publish] D -- too similar --> DEFER[Deferred
not discarded] G -- ungrounded claim --> QN[Quarantined
for review] ``` - **Topic dedup** — the candidate is embedded and compared against the contract's already-published topics *and* its batch peers. A duplicate is **deferred**, not deleted: the verdict is "not now", because topic space frees up as content ages. The same comparator also runs earlier, on the Collect side, so most duplicates are never generated at all — one core, two seams. - **Claim verification** — numerical and normative claims are checked against retrieved *sources*, not against the corpus alone ("verify against the world, not against yourself" — the lesson of the 73% false-positive measurement). An article with unverifiable claims is quarantined. - **Quality scoring** — deterministic signal battery (brand-safety, language consistency, structural checks) stamped onto every article: telemetry that feeds audits and remediation. ## One set of gates, three vocabularies The same gates appear under three names across this portal, which is a historical accident worth naming once. [Content lifecycle](../../platform/content-lifecycle/#the-publish-path-and-its-gates) lists them as the publish-path table above. The [writing framework](../writing-framework/) calls the single quality step at the end of the generation pipeline *the gate* — it is the same claim verification and scoring, seen from inside the pipeline. And [copywriting evaluation](../copy-evaluation/) speaks of two co-equal gates, TRUE∧ACCURATE and EASY∧PLEASANT — those are the *criteria*: claim verification serves the first; the readability rubric and the anti-template battery serve the second. "Publishes without human review" means no human reads each article before it goes live: the readability rubric was calibrated by humans offline, but at night it runs mechanically. ## The failure philosophy: quarantine, not regeneration A gate failure does **not** trigger regeneration. Measured on live data, the overwhelming majority of blind retries fail the same gate again — burning money to re-lose the same argument. A failed article goes to quarantine and returns only when something *material* changed (the corpus gained relevant facts, the topic freed up). Every re-entry path is enumerated; there is no silent retry loop. ## Trade-offs Deterministic gates have false positives too — a defer verdict on a genuinely fresh angle, a grounding block on a claim whose source phrasing differs in units. The design accepts this: on an autonomous publish path, a wrongly-held article costs a day; a wrongly-published fabrication costs trust. Signals are tuned against measured false-positive rates, and every gate's verdict is recorded with the article, so a human can overrule with full context. ## See it in two minutes Read the gate verdicts on any article via the API (`quality`/`publish` domains), or see where the gates sit in [Content lifecycle](../../platform/content-lifecycle/#the-publish-path-and-its-gates). --- --- title: How the RAG flywheel works description: Corpus-first generation — how every contract accumulates a knowledge base that makes each article cheaper and better grounded than the last. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/rag-flywheel/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** grounding articles in live web research is expensive and non-cumulative. Done naively, article #1000 costs exactly as much research as article #1 — and nothing stops yesterday's generated text from becoming tomorrow's "source", a feedback loop that laundered hallucinations into facts. ## Layer 1 — the corpus is the asset Each contract owns an accumulating knowledge base of **research facts**: source-attributed claims extracted from real pages, each carrying its claim value (the number, price, date), provenance, and a status. Generation retrieves from this corpus first; the corpus persists and compounds across runs. ```mermaid flowchart LR SRC[Source pages] -- extract --> RF[(Research facts
per contract)] RF -- retrieve + ground --> GEN[Generate] GEN -- gaps only --> WEB[External research] WEB -- new facts --> RF GEN --> ART[Published articles] ``` ## Layer 2 — corpus-first, external for gaps only The flywheel invariant: **external research is spent only on what the corpus lacks or what went stale** (gap ∪ stale), through a single write seam. The measurable consequence: external calls per article decrease monotonically as the corpus matures, while grounding quality does not. That is the flywheel — every article makes the next one cheaper. ## Layer 3 — hygiene: what keeps the corpus trustworthy A knowledge base that feeds an LLM is a poisoning target, including self-poisoning. The guards: - **Ingest gate** — the body of a *generated* article is never accepted as ground truth. Only external, source-attributed material enters. - **Conflict arbitration** — when two sources disagree on a claim, an arbiter resolves which value the corpus carries, rather than keeping both silently. - **Freshness classes** — facts age at different speeds (a protocol definition vs a price); TTLs are per fact class, and stale facts fall out of retrieval until refreshed. - **Quarantine** — suspicious material is excluded from retrieval, not deleted, so hygiene decisions are reversible and auditable. ## Layer 4 — retrieval that earns its citations Retrieval is hybrid — dense vectors and full-text search fused by rank — with a **relevance floor**: a fact must clear a similarity threshold to the article's topic before generation may cite it. Below the floor, the fact simply isn't offered. This is what keeps "grounded" from degrading into "decorated with citations". ## Trade-offs Corpus-first means a *young* contract writes its first articles from a thin corpus with more external research — the flywheel needs several cycles to spin up, and the platform accepts that early cost. Hygiene gates also reject real facts occasionally (a conservative ingest gate is the safe side of the trade); the remedy is the arbiter and re-ingestion, not loosening the gate. ## See it in two minutes Ask the platform's MCP `list_domains` for the `research` domain — the corpus surface is a first-class API — or read [Content lifecycle](../../platform/content-lifecycle/) for where retrieval sits in the article's path. --- --- title: How the run engine works description: Every pipeline execution as a stream of typed events — Redis Streams transport, a single idempotent DB writer, self-healing watchdogs, and concurrency that adapts to the provider in real time. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/run-engine/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** a pipeline execution is a long, failure-prone process touching LLM providers, the database and the site builder. If its state lives in the memory of whichever process happens to run it, a crash loses the truth of what happened — and two processes writing "the state" produce two truths. ## Layer 1 — one entry, streams in between, one writer Run creation is a single seam: duplicate-guard, budget preflight, version resolution from the catalog, a `pipeline_run` row — then a job message onto a Redis Stream. Execution talks back **only in events**. ```mermaid sequenceDiagram participant API as Platform API participant J as runs:jobs (stream) participant W as Native worker participant E as runs:events (stream) participant P as Projector participant DB as PostgreSQL participant SSE as SSE clients API->>J: enqueue job (XADD) J->>W: consumer group delivers W->>E: typed events: run_started,
step_completed, run_succeeded… E->>P: consumer group delivers P->>DB: apply_event (sole DB writer) P->>SSE: fan-out via Redis Pub/Sub ``` Two design choices carry the weight: - **Typed event factories with a JSON-Schema contract.** Workers never hand-build event dicts — factories stamp the schema version and validate fields, and the schema ships as a checked-in contract with fixtures for every event type. The web UI's mock emulator replays the same fixtures, so the frontend develops against the exact wire format. - **The projector is the only writer.** Everything else emits; one consumer applies. Idempotency is structural: step counts are computed as *distinct terminal steps*, not increments, so an at-least-once redelivery cannot double-count. A poison message is acknowledged and captured rather than retried forever. ## Layer 2 — the run's states, and who may move them ```mermaid stateDiagram-v2 [*] --> queued: create_run queued --> running: worker picks up queued --> cancelled: cancel running --> succeeded running --> failed: step error (fail-loud) running --> interrupted: watchdog: heartbeat lost ``` Honestly: today these transitions are enforced by convention across the writers (projector, watchdog, retry), not by a formal guard table — an explicit FSM with measured-recovery chaos tests is a designed, not-yet- accepted evolution. What *is* live is the self-healing pair: - the **watchdog** marks runs whose heartbeat went stale as `interrupted` (a crashed worker cannot leave a run "running" forever); - the **reaper** reclaims abandoned *job messages* from the consumer group, re-queues them up to a retry cap, then dead-letters them. The two never overlap: a run that failed on its own merits is terminal and left alone. Neither of those catches a run that is **blocked rather than dead**. A step waiting on a database lock keeps its heartbeat beating, so the watchdog sees a healthy run, and its job message was never abandoned, so the reaper has nothing to reclaim — the run simply stays `running`, holding its row lock and starving every contract queued behind it, while every surface reports health. That class is covered by a third, database-level sweep (`reap_idle_in_tx`), which measures the age of the *transaction* — not how long the connection has been idle, since a client issuing statements inside one long transaction never looks idle — and terminates only backends that are actually blocking others. ## Layer 3 — inputs are frozen before the first step A run's inputs (brand profile, author profile, collection config) are assembled **once** at start into a frozen, typed contract with secrets redacted — steps read `ctx.inputs`, never re-query the database mid-run. The rule this enforces: a clean boundary between *what you were given* and *what you produced*. It exists because both failure modes actually happened — brand guidelines silently truncated by an ad-hoc read, and a config field mutated by step 1 and consumed as "input" by step 6. ## Layer 4 — concurrency that senses the provider The global LLM concurrency cap is not a constant; it is a control loop: ```mermaid flowchart LR A[LLM adapter
reads x-ratelimit-* headers] -- telemetry --> R[(Redis)] R --> D[Autoscaler tick
C* = min of provider ·
DB pool · host · ceiling] D -- set cap --> R R -- read cap --> W[Workers acquire slots] W --> A ``` **Sense** — every live LLM response drops rate-limit telemetry into Redis (fail-soft: telemetry can never break the call). **Decide** — a periodic tick computes the cap as the *minimum of several limiters* and applies AIMD (raise slowly, cut sharply, instant cut on 429). **Actuate** — the cap is a Redis key with a TTL; if the control loop dies, workers fall back to the static setting on their own. Why several limiters, not just the provider: a live experiment found a *silent wall* — above a certain concurrency the **database pool** exhausted while the LLM provider and CPU looked green, and throughput went to zero. The pool limiter, not the provider, is the binding constraint in practice. Boundary hygiene note: the run context is forbidden by the import linter from touching the integrations internals, so Sense and Decide communicate strictly through Redis — the control loop respects the same module boundaries as everything else. ## Trade-offs Cancel applies to *queued* runs immediately; a run that is already executing is not interrupted mid-flight — it runs to completion or failure (the control channel for mid-run interruption is in place, its consumer is on the roadmap). Exactly-once is by-construction rather than by formal replay-test. Both are documented limits, not surprises. ## See it in two minutes `POST /api/v1/runs` launches a run; watch it live over the runs SSE channel, or read its `pipeline_run` / `run_step` rows — every claim above is visible in that data. --- --- title: How the scoring systems work description: The platform's seven scoring axes — what each one measures, the idea behind its formula, and who consumes it — plus the rule that keeps scores from lying. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/scoring-systems/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** a platform that decides autonomously needs numbers — but a single "quality score" blending unrelated concerns is a number that lies. The design principle here: **one score, one question, one consumer** — and safety verdicts never hide inside averages. ```mermaid mindmap root((Scoring)) Candidate rank ontology overlap deep LLM analysis Dedup cosine collect gate publish gate Corpus coverage claim-value coverage slot coverage Article quality stamp structure AI-marker absence technical SEO Brand-safety signals verdict, not number Topic yield impressions per article Relevance cited-first membership ``` ## The inventory | Score | Question it answers | Formula idea | Consumer | | --- | --- | --- | --- | | **Candidate rank** | "Is this source worth an article?" | Structured ontology-overlap (tiered term comparison + domain affinity) blended with a deep LLM analysis into 0–100 | The export gate's floor and bands | | **Dedup cosine** | "Have we already covered this?" | Embedding cosine of the candidate/article against committed topics and batch peers; one calibrated scale for both the collect-side and publish-side gates | Defer verdicts | | **Corpus coverage** | "Does the corpus know enough to write this niche?" | Weighted blend: claim-value coverage (facts carrying usable numbers) and slot coverage (ontology-derived topic slots filled) | The research-floor and enrichment planning | | **Quality stamp** | "How well-made is the artifact?" | Deterministic automatic categories (structure, AI-marker absence, technical SEO) scaled, minus severity-weighted defect penalties | Audits, remediation queues, reporting | | **Brand-safety battery** | "May this ship under our brand?" | A set of independent regex-precise signals (competitor domains, names in titles, recommendation-verbs, third-party promo, buyers-guide shapes, outbound links, placeholder leaks) | The **verdict** — reject/rework — never the number | | **Topic yield** | "Does this topic earn attention?" | Impressions per article over a maturity-filtered window, with sample-size guards | The [topic feedback loop](../topic-feedback-loop/) | | **Relevance** | "May this fact be cited here?" | Cited-first membership — the references list contains only what the text actually used; a cross-encoder reranker orders candidates | The writing framework's retrieval | ## The two rules that keep scores honest **1. Calibrate against labels, not vibes.** The dedup threshold moved only after a manual labeling study (~a hundred pairs): the old threshold measured at ~0.49 precision — a coin flip — the new one at ~0.83, and one notch higher started losing true same-batch clones. Similarly, the corpus coverage match threshold was recalibrated after the original value saturated at 1.000 on virtually every run — a score that always says "perfect" measures nothing. **2. Verdicts don't hide in averages.** Brand-safety and grounding failures flip the article's verdict directly. A "9/10 quality" article with one competitor-promotion hit is not a 8.5 — it is a rejected article with a high stamp, and both facts stay visible. ## Trade-offs Threshold calibration is perishable: change the embedding model and every cosine threshold silently means something else (an offline recalibration protocol exists for exactly this migration). And regex-precise safety signals trade recall for auditability — each signal is narrow and explainable, at the cost of needing new signals as new failure shapes appear; the signal battery grows by incident, deliberately. ## See it in two minutes Any article row carries its stamp, signals and dedup scores in its telemetry; any candidate carries its rank; the corpus coverage of a contract is a first-class read on the `research` domain. --- --- title: How SEO analytics collection works description: Five source families collected daily into six time-series tables and one composite health score — and the list of ways search data lies to you if you read it naively. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/seo-analytics/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** search platforms report metrics in cumulative windows, with multi-day lag, retroactive corrections and per-surface quirks. Read naively, the data produces confident nonsense. The analytics layer's job is to turn that into **owned time series** with the distortions named and handled. ## Layer 1 — collectors into owned storage A daily collection run sweeps the connected sources — search console (impressions, clicks, queries, indexation), a second search engine's webmaster surface, SERP presence and discovery-eligibility probes, CDN analytics, and a domain-authority probe — each as its own collector with its own cadence quirks. Everything lands in **six Postgres tables** with idempotent upserts on natural keys; the raw per-day JSON is treated as ephemeral scratch. The database is the only durable truth. ```mermaid flowchart LR subgraph SRC [Sources] GSC[Search console] BW[2nd engine webmaster] SERP[SERP + discovery probes] CDN[CDN analytics] DR[Authority probe] end SRC --> RUN[Daily collection run] RUN --> T[(6 time-series tables:
run · instance snapshot ·
search metric · page audit ·
index check · authority)] T --> H[Composite health score
+ boolean health flags] T --> LOOP[Topic feedback loop] ``` ## Layer 2 — the instance snapshot and health score The workhorse table is the **daily per-site snapshot**: one row per site per day, carrying the raw window values plus a computed **composite health score** and a set of boolean health flags — canonicalization drift, scheme mismatches, feed bloat, title overflow, crawler silence, rising 404s. One function computes all of it, so the dashboard, the nightly report and the alerts read the same verdicts. ## Layer 3 — the distortions, named This is the part most analytics setups leave implicit. Rules for reading this data, stated as system knowledge: ```mermaid timeline title The life of one day's search data Day 0 : impressions happen Day 1-3 : platform reports with lag — zeros are "not final yet" Day 7 : weekly delta of two snapshots = the real weekly number Later : history silently backfilled and corrected upstream ``` - **Cumulative windows.** Snapshot impression/click values are long-window cumulative totals — a weekly figure is always the *delta of two snapshots*, never a raw column. - **Maturation lag.** Fresh days arrive incomplete and get corrected for up to several days — a zero delta on yesterday means "not finalized", not "no impressions". - **Top-N breakdowns never sum to totals** — expected sampling behavior, not a data-quality bug. - **Different windows per source.** The CDN reports rolling 30-day windows; bot-hit sampling uses a shorter adaptive window; the two cannot be compared cell-to-cell. The topic feedback loop consumes this data through maturity filters precisely because of these distortions — young pages are excluded from verdicts until their signal has settled. ## Trade-offs Some cells of the matrix are deliberately empty: one analytics suite and one commercial SEO API are not integrated (evaluated, not wired); the page-experience collector exists but is skipped in production to conserve quota; and the geographic breakdown is collected upstream but not yet persisted into the time series — documented, so nobody mistakes "not stored" for "no traffic". The authority probe runs weekly through an indirect path rather than from the production host. ## See it in two minutes The `analytics` API domain exposes the snapshots and health flags; the weekly owner report is built from exactly these tables — every number in it traceable to a row. --- --- title: How SEO methodology and authorship work description: What every published page carries by construction — structured data, FAQ schema, machine-readable AI policy, per-author E-E-A-T signals — and the editorial rule behind all of it. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/seo-methodology-and-authorship/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** SEO bolted on after publication is archaeology. The platform's approach is constructive: every page carries its search and trust signals **by construction**, emitted by the same pipeline that writes the content — so on-page SEO is a property of the system, not a checklist someone runs later. ## Layer 1 — what every article page carries ```mermaid flowchart TB ART[Published article] --> FAQ[FAQ block +
FAQPage JSON-LD] ART --> META[Canonical URL · hreflang ·
og-tags · sitemap entry] ART --> REL[Related-posts block
native engine, tag/category indexes] ART --> AUTH[Author byline →
author profile page] AUTH --> PERSON[Person JSON-LD
name · jobTitle · bio · sameAs] ``` - **FAQ with schema.** Generation produces a five-question FAQ per article; the renderer emits it twice — as the visible accordion and as `FAQPage` JSON-LD with exactly one home in the head. Structured data and visible content come from the same source field, so they cannot drift apart. (A subtle lesson learned: the JSON-LD copy needs its own markdown-stripping path — structured data may not carry HTML.) - **Internal links** are the site engine's native related-content engine over tag and category indexes — deliberately boring, deterministic machinery rather than LLM-invented links. - **Host canonicalization** — one canonical scheme and host per site, enforced at the edge with permanent redirects and HSTS; sitemaps and feeds are the site generator's native output. ## Layer 2 — the AI-readable layer Every site in the fleet publishes its own **`llms.txt`** — brand, niche, author roster, recent articles — generated as a native output format of the site build and announced via a standard `Link` header. The accompanying robots policy is explicit and consistent: search allowed, AI-answering allowed, AI-training disallowed, with the major AI crawlers addressed by name. The platform practices what this portal preaches: machine consumers are a first-class audience on the content sites too. ## Layer 3 — authorship as a system Authorship is data, not decoration: - Each contract owns an **author roster** — name, position, bio, weight, avatar. Article bylines are drawn from it by weighted random selection, and the chosen author's bio feeds the writing pipeline's VOICE owner — the byline and the prose style are the same fact. - Each author has a **profile page** on the site with the full bio and a `Person` JSON-LD block — name, job title, bio, absolute-URI social links, portrait — the machine-readable E-E-A-T anchor. - The site renders the author consistently across five surfaces (index, profile, byline, footer, cards) from one data file. Honest note: author selection is weight-based, not topic-aware — an author's expertise does not yet steer which topics they byline; that is a known, documented candidate improvement rather than a hidden gap. ## Layer 4 — the editorial rule above all mechanics One rule outranks every optimization: **promote only your own.** It lives as the first line of every contract's editorial standards, enforced mechanically by the brand-defense battery ([details](../ontologies-and-filtering/#layer-4--brand-defense-as-declarations-plus-signals)), and it shapes SEO strategy itself — the platform builds authority on its own brands' expertise rather than borrowing it. Publish-path detectors (brand mis-serve, canonicalization drift, dead crawlers, rising 404s, title overflow) watch the fleet continuously, turning SEO regressions into alerts instead of quarterly surprises. ## Trade-offs Search-engine ping-on-publish (IndexNow) is implemented and wired but **not yet enabled** — by the platform's own hard rule, that means it is not called "deployed" here. Structured-data breadth is deliberately narrow (FAQ + Person, article schema left to the theme) — fewer schema types, each guaranteed consistent with visible content, over a wide surface nobody re-verifies. ## See it in two minutes View source on any fleet article: the FAQ JSON-LD, canonical link and author markup are right there; `/llms.txt` on the same domain shows the AI-readable layer. --- --- title: How source discovery works description: Where article candidates come from — three source classes, a funnel of independent drop-gates, and the honest state of the cheap-to-expensive cascade. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/source-discovery/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** everything downstream — generation, quality, cost — is bounded by what Collect finds. Feed it noise and the best writing pipeline in the world produces polished noise. Discovery therefore optimizes for *qualification density*, not raw harvest volume. ## Layer 1 — three source classes, deliberately unequal ```mermaid flowchart LR subgraph SRC [Source classes] F[Curated feeds
tiered, trusted] Q[Search harvest
web + news surfaces,
curator-authored queries] L[Legacy news API
kept as fallback] end F --> H[Harvest pool] Q --> H L --> H ``` - **Curated feeds** — a tiered, hand-picked list per contract: high precision, low recall; the trusted backbone. - **Search harvest** — the workhorse: a search API queried on both its evergreen and news surfaces, driven by **curator-authored queries** from the contract's ontology — not blind scraping; the query set *is* an editorial instrument. - **Legacy news API** — an older, lower-precision harvester kept beside the new one as a fallback. The contract's *own* site pages are deliberately **not** a candidate source — they feed ontology construction (what the niche is about), never the article queue. ## Layer 2 — the funnel of independent gates There is no single "drop reason" field; rejection is distributed across gates that each own one concern: ```mermaid flowchart TB H[Harvest pool] --> D1[URL dedup
vs everything already seen] D1 --> D2[Per-domain cap
no single source floods the funnel] D2 --> S[Cheap LLM screener
pass / fail] S --> DA[Deep LLM analysis
rank 0–100] DA --> TD[Topic dedup
cosine vs committed topics —
DEFER, reversible] TD --> EX[Export gate
daily generation budget ·
impact-ranked top-N] EX --> P[Seeded as pending article] ``` Two gates deserve attention: - **Topic dedup defers, never deletes.** A candidate too similar to already-committed topics (or to a peer in the same run) is marked deferred — the only *reversible* verdict in the funnel, because topic space frees up as content ages. - **The export gate enforces a hard daily generation budget.** Each contract has a per-day budget — fleet default of 5 articles, overridable per contract (0 is a legal full stop). The remainder is counted against a database day-window, so a restarted run cannot double-spend the limit. Within the budget, candidates are ranked by **expected impact**: `(rank_deep / 100) × yield_ratio(topic)`, where `yield_ratio` is the topic's measured impressions-per-article relative to the contract median (clamped 0.25–4.0), fed by the [topic feedback loop](../topic-feedback-loop/). Topics without data get a neutral weight — with no SEO signal the ranking degrades to plain rank order, but the cap always holds (the two axes fail independently). An explore floor reserves part of the budget for new topics; candidates the budget does not reach stay in the pool and compete again the next night. Below the rank floor a candidate simply never becomes an article; absence is the rejection record. The historical asymmetries are closed: the old auto-pass band no longer bypasses the cap, and the legacy **priority path** (an operator override that skipped the rank floor) is ignored by budget selection — nothing in the platform writes that flag anymore, and the field is slated for removal. ## Layer 3 — the cascade today The economic idea is *cheap before expensive*: kill candidates before they cost real LLM money. Today's production cascade achieves this with a **cheap LLM screener** ahead of the deep analyzer — every surviving candidate costs at least one small LLM call. A **vector pre-filter** (embed candidates, recall-rank them by cosine before any LLM sees them) exists as a measured next generation: on a live run it cut the screening workload by ~5.7× on that run (714 harvested → 646 in the recall pool → 38 screened in). It is owner-gated and not yet the production default — the catalog's version pinning is exactly what lets both generations coexist. ## Layer 4 — discovery also feeds the corpus Collect does double duty: qualified source pages are mined into **research facts** through the same admission pipeline generation uses (content-hash dedup → provenance and relevance gates → conflict arbitration). Discovery is thus the [RAG flywheel's](../rag-flywheel/) intake, not just the article queue's. ## Trade-offs Curated queries and feeds mean recall depends on curation quality — a stale query set starves the funnel silently, which is why harvest-novelty is monitored as a signal. And competitor filtering deliberately does *not* live here: brand defense operates downstream (generation and publish), where context exists to judge intent — a URL alone is not evidence of promotion. ## See it in two minutes A contract's funnel is visible in its run records: harvested → screened → ranked → deferred → seeded, step by step, for every nightly run. --- --- title: How the topic feedback loop works description: Search results steer what gets written next — topic yield scoring, an explore floor that protects discovery, dead-topic parking, and the twin mechanism that stops paying twice for the same article. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/topic-feedback-loop/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** without feedback, a content platform keeps writing what it always wrote — saturating winners into cannibalization and feeding dead topics forever. The loop closes this: **what earned attention gets more; what proved dead gets parked; and a protected share always goes to the unknown.** ## Layer 1 — from search signal to topic verdicts Search-console data (already collected — the loop adds no new harvesting) is aggregated weekly into **topic performance**: articles per topic, impressions, clicks, and the headline metric — *impressions per article*. A nightly pass rebuilds **topic state** before Collect runs: ```mermaid stateDiagram-v2 [*] --> no_data no_data --> star: earns impressions + clicks no_data --> not_indexed: never crawled star --> star_far: impressions, position too deep for clicks star --> dead: yield collapses dead --> star: signal returns note right of not_indexed: "not indexed" ≠ "dead" —\ndifferent problem, different fix ``` Verdicts respect **signal inertia** — three separate constants, often confused: articles younger than the maturity window don't vote at all; the freshest few days of search data are excluded (reporting lag); and policy changes themselves are rate-limited by a governance interval. ## Layer 2 — the selection policy ```mermaid pie showData title Selection shares (concept) "Exploit — above-median yield" : 75 "Explore — protected floor" : 25 ``` The policy is precisely *not* a hard split: **exploit** takes topics with above-median yield; **explore** is a *protected minimum share* — new and under-sampled topics get it even when exploit could fill the whole batch. Saturation is capped (a topic already heavily covered in the recent window stops accepting more candidates), dead topics are parked wholesale through the same defer mechanism the dedup gate uses, and unclassifiable candidates pass through unjudged — deliberately excluded from the median so dictionary gaps don't masquerade as yield signals. A watchdog alarms if too much of a week's output lands in dead topics. Everything is tunable **without deploys**: per-contract policy overrides cascade over fleet defaults over code defaults, edited through an owner-gated tool; topic dictionaries are versioned and append-only. ## Layer 3 — the seam The loop's write surface into production is intentionally tiny: one step inside Collect, placed right after topic dedup and right before the export gate — the same choke-point every candidate already passes. The quota mechanism cannot *increase* a contract's nightly budget; it only redistributes within it. ## Layer 4 — the twin: never pay twice A sibling mechanism guards the opposite failure — regenerating what was already generated. Measured before the fix, nearly half of generation submissions were repeats, and the overwhelming majority of those repeats failed the same gates again. Now a historical guard at run creation refuses a generate-run for a source that already succeeded (with an explicit override for deliberate regeneration), and gate-failed articles sit in **quarantine** rather than a retry loop. The two mechanisms are philosophical twins on different seams: the loop decides *what to write*, the guard ensures *what was written isn't paid for twice*. ## Trade-offs The loop is only as sharp as the topic dictionary — a deterministic slug-to-topic mapper with zero LLM calls, which means new topic families need dictionary entries before the loop can see them. And the quota features ship behind calibration flags whose production state moves as tuning proceeds — scoring and telemetry always run; enforcement is switched deliberately, consistent with the platform's deployed-means-enabled rule. ## See it in two minutes The nightly report's topics section shows every bucket decision of the last run; topic performance and states are queryable per contract through the `analytics` domain. --- --- title: How the article writing framework works description: An ~18-step pipeline under a strict charter — one concern, one owner — with grounded drafting, schema-controlled LLM output and a single quality gate at the end. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/how-it-works/writing-framework/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. **The problem:** LLM article generation degrades into entropy when every step can touch every concern — five different steps "improving" tone produce a sixth voice nobody chose. The framework's answer is a charter: **one concern, one owner** — and everything else is plumbing between owners. ## Layer 1 — the charter The pipeline is ~18 steps, but five of them own the article: ```mermaid flowchart LR PLAN[PLAN
structure +
fact contract] --> WRITE[WRITE
all prose is born here] WRITE --> VOICE[VOICE
rhythm + voice rules] VOICE --> FRAME[FRAME
intro · about ·
conclusion · FAQ] FRAME --> GATE[GATE
the single
sanitize → score → verdict] ``` Everything before PLAN is preparation (fetch, summarize, ontology alignment, author selection); everything between owners is mechanical (images, references, layout). No step outside WRITE may generate prose; no step outside GATE may pass a verdict. The practical payoff: when tone regresses, exactly one step is suspect. ## Layer 2 — grounded drafting Drafting retrieves from the contract's [research corpus](../rag-flywheel/) at three points (planning, writing, gating), and the references section is built **cited-first**: only sources the text actually used, not a wall of retrieved-but-ignored links. A cross-encoder reranker re-orders retrieved facts before they reach the prompt — a measured quality lever that fails open (a reranker outage degrades ranking, never blocks generation). The author is selected from the contract's roster by weighted random draw, and the author's bio feeds the VOICE owner as raw material — voice is a per-author property, not a global constant. ## Layer 3 — schema-controlled LLM output Structured steps demand structured output — literally. LLM calls that must return JSON use **provider-enforced JSON-schema decoding** (the grammar constrains generation), after live probes showed that two plausible-looking alternatives were silent no-ops on the provider. The old "repair truncated JSON" patch was deleted; a parse failure now takes a *measured* fallback path instead of a silent one, and a guard test pins the provider facts so a regression cannot sneak back. Section generation runs **concurrently** under a semaphore — sections are independent by construction once the PLAN owner has fixed the structure and fact contract, so parallelism costs nothing in coherence. ## Layer 4 — one gate, growing teeth The GATE step is the pipeline's single verdict point: mechanical sanitize, the scoring battery, brand-safety signals, grounding checks — accumulated over successive spec generations into one place. It is the largest step in the pipeline by far, and that is the design: gates are *supposed* to accrete here, rather than scatter. ## Trade-offs The charter's numeric goals are not all met — the gate step has grown well past the size target as new checks landed, and one planned step consolidation hasn't happened. Section parallelism shipped alongside a version promotion without its own dedicated A/B protocol (the design doc still lists that as open). Both are visible, tracked deviations — the charter is a direction the pipeline is held against, not a myth of perfection. ## See it in two minutes Any article's run record lists every step with its timing and outcome; the [Version lifecycle](../../pipelines/versioning/) page explains why each change to this framework is a new immutable carve. --- --- title: Connecting to MCP description: Step-by-step MCP connection walkthrough for agents — endpoint, the three-step JSON-RPC handshake with complete curl commands, auth header, and a symptom-cause-action table. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/connecting/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. The platform exposes an MCP server over **Streamable HTTP** (JSON-RPC). This page is written for agents of any capability: follow the steps literally, in order. Every command is complete — nothing is assumed from outside this page. ## Facts you need | Fact | Value | | --- | --- | | Endpoint | `https://entherium.duckdns.org:8443/mcp/` | | Transport | MCP Streamable HTTP (JSON-RPC over POST; responses are SSE) | | Auth | `Authorization: Bearer ` on **every** request | | Discovery without auth | `GET /mcp-info` (onboarding page), `GET /llms.txt` | | What you get | 14 tools + 5 resources ([full reference](../tools/)) | | Read-only mount | `https://entherium.duckdns.org:8443/mcp/readonly/` — same token, only the 7 read tools ([why](#read-only-mount)) | | Docs MCP, **no token** | `https://entherium.duckdns.org:8443/mcp/docs/` — this documentation as `search` / `fetch` / `submit_feedback` ([details](#the-docs-mcp-no-token)) | ## Preconditions - You have an agent token (`` below). Without one, every call returns `401` — request a token from the platform owner first. - Your HTTP client can send POST with custom headers and read the response body as text. Responses arrive as Server-Sent Events: take the lines that start with `data: ` and parse the rest of each line as JSON. ## Step 1 — initialize the session ```sh curl -sS -D headers.txt https://entherium.duckdns.org:8443/mcp/ \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}' ``` Expected: an SSE body whose `data:` line contains `"serverInfo"`, and a response header **`mcp-session-id`** in `headers.txt`. Copy that value — it is `` in the next steps. If you got `401`, your token is missing or expired — see the table below. Do not proceed until this step returns 200. ## Step 2 — confirm initialization ```sh curl -sS https://entherium.duckdns.org:8443/mcp/ \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "mcp-session-id: " \ -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' ``` Expected: HTTP 2xx with an empty or trivial body. This step is required — skipping it makes some clients' later calls fail. ## Step 3 — make your first tool call Always call `start_here` first: it returns the ordered plan for using the platform and the capability version of the read surface. ```sh curl -sS https://entherium.duckdns.org:8443/mcp/ \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "mcp-session-id: " \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"start_here","arguments":{}}}' ``` Expected: a `data:` line whose JSON contains the plan. From here, use the [tools reference](../tools/) — every tool page shows the exact `tools/call` payload for that tool. ## Connect from a client in one step The three curl steps above are what every client does under the hood. With a client you only give it the URL and the header: **Claude Code** ```sh claude mcp add --transport http enterium https://entherium.duckdns.org:8443/mcp/readonly/ \ --header "Authorization: Bearer " ``` **Cursor / VS Code / any `mcp.json`-style client** ```json { "mcpServers": { "enterium": { "url": "https://entherium.duckdns.org:8443/mcp/readonly/", "headers": { "Authorization": "Bearer " } } } } ``` Replace `readonly/` with `/mcp/` only when you actually need the write tools. For the token-less docs server the install links work as-is: [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=enterium-docs&config=eyJ1cmwiOiJodHRwczovL2VudGhlcml1bS5kdWNrZG5zLm9yZzo4NDQzL21jcC9kb2NzLyJ9) · [Add to VS Code](vscode:mcp/install?%7B%22name%22%3A%22enterium-docs%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fentherium.duckdns.org%3A8443%2Fmcp%2Fdocs%2F%22%7D) · Claude Code: `claude mcp add --transport http enterium-docs https://entherium.duckdns.org:8443/mcp/docs/`. ## Read-only mount `/mcp/readonly/` serves the same server with the write tools removed: only tools whose `readOnlyHint` is `true` are listed, and calling a write tool by name fails. Same bearer token, same handshake, same `start_here`. Use it as the default for evaluation, for research assistants and for any client that must never file a ticket or move a task by accident — the choice is made by the URL, not by trusting the client to respect hints. The full server stays at `/mcp/`. ## The docs MCP (no token) `/mcp/docs/` turns this documentation into three tools and needs **no token**: | Tool | What it does | | --- | --- | | `search(query, limit)` | Ranks portal pages for a query; returns `{results: [{id, title, url}]}` — `id` is the page route | | `fetch(id)` | Returns a page as Markdown (`{id, title, text, url, metadata}`) — the `.md` twin, fewer tokens than HTML | | `submit_feedback(page, message)` | Files a suggestion for the documentation team; anonymous, at most 5 per hour per client; asks the user first | The tool names and shapes follow the deep-research connector contract, so the server can be added to ChatGPT or Claude as a connector unchanged. It never reads or writes platform data — everything comes from the published portal — which is why it sits outside the token gate. ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` on a plain browser `GET /mcp/` | Auth is checked at the mount, before content negotiation — without a token you never reach the transport | Expected. There is nothing to see in a browser; the human page is `GET /mcp-info` | | `406 Not Acceptable` on `GET /mcp/` **with** a token | This is not a browser URL; the server requires the JSON-RPC handshake | Use the three steps above, or an MCP client library | | `401` on **every** call | Token missing, malformed or expired (auth is checked at the mount, before any tool runs) | Tokens are issued by the platform owner (`POST /api/v1/auth/agent-token` is superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set — re-check `my_capabilities` after | | `403` or a capability error inside a tool result | Token is valid but lacks the capability or the per-contract grant for that tool | Call `my_capabilities` to see what you may do; request the missing grant from the owner | | Tools list suddenly empty after a platform deploy | The deploy restarted the API container and killed your live MCP session; sessions are not restored | Re-run the handshake from Step 1 (new `mcp-session-id`) | | Response body looks unparseable | You are reading raw SSE | Parse only lines starting with `data: `; join multi-line events before JSON-parsing | | `307` on `/mcp` (no trailing slash) | The transport lives at `/mcp/`; the server redirects | Use `/mcp/` directly — some clients do not follow redirects on POST | ## Two reading rules that save agents 1. **Read payload values, not schemas.** Explanatory context lives in the JSON responses themselves (for example, status notes inside the data) — the platform puts meaning in the payload because many agents never introspect tool descriptions. 2. **Re-check `start_here` after upgrades.** Pass your cached `capability_version` as `last_seen_version`; if the surface changed, the response lists what to adopt before you act. --- --- title: Interpreting platform status description: How to read the ops overview and status payloads correctly — which "empty" values are normal idle states and which actually mean trouble. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/interpreting-status/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. Agents that read the platform's status endpoints often misread **normal idle states as failures**. This page lists the interpretation rules. When your conclusion is "the platform is broken", check this table before reporting. ## Rules | You see | It means | It is NOT | | --- | --- | --- | | `orchestrator.present: false` / `alive: false` | **Normal idle.** Production has no permanently-running orchestrator daemon; generation runs on a nightly timer | An outage. Alarm only when `present: true` **and** `alive: false` (a heartbeat went stale) | | `velocity: 0`, `in_flight: 0` | No run is active right now | A stuck queue | | `contracts[].remaining` > 0 | Generation candidates the nightly batch will work through under its daily cap | A backlog of failures, or an embeddings queue | | A quiet period during the day | The nightly run finished; the next one starts on the timer | A dead scheduler | ## Where to look for the real execution funnel - The **cycles** domain (see `list_domains`) shows the actual run funnel: what was dispatched, what completed, what failed. - Recent failures live in the error surface: `GET /api/v1/errors` (see [How to read errors](../../guides/reading-errors/)). - Every run is recorded as `pipeline_run` with per-step `run_step` rows — a run that failed is visible there, not inferred from idle status. ## The golden interpretation rule **Meaning lives in payload values.** When a status field carries a `note` alongside a number, the note is authoritative context — quote it in your report instead of inventing an explanation for the number. --- --- title: entherium_create_task description: Create a TASK for a team member. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/entherium_create_task/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at .md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the running server (agent capability version `2026-09-03.1`) — do not edit by hand. ::: Create a TASK for a team member. `assignee` is a person's NAME — it is resolved to that user; an unknown or ambiguous name is REJECTED (never silently unassigned). `spec_ref` TARGETS the task at a platform feature by its SPEC id ("SPEC-NNN") so the work stays tied to the right feature context — pass it whenever the task is about a known spec; an unknown spec id is REJECTED. Needs the `manage_task` capability. Returns {"accepted": true, "task_id": } — KEEP the task_id to update / close / retarget / annotate it later; {"accepted": false, "error": ...} otherwise. **Access:** writes — the client asks you for confirmation before every call **Available at:** `/mcp/` _Hints declared to MCP clients (ToolAnnotations)_ | Hint | Value | | --- | --- | | `readOnlyHint` | false | | `destructiveHint` | false | | `idempotentHint` | false | | `openWorldHint` | false | | `anthropic/requiresUserInteraction` | true | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `title` | `string` | yes | Task title | | `description` | `string \| null` | no | What has to be done and why | | `assignee` | `string \| null` | no | Team member's display name; unknown or ambiguous names are rejected | | `spec_ref` | `string \| null` | no | Feature id of the form SPEC-NNN the task targets; unknown ids are rejected | ## Example call Send inside an initialized MCP session (see [Connecting](../../connecting/)). Every parameter is shown with a typed placeholder; drop the optional ones you do not need — but for defect/task tools the QA fields are what keeps a report out of quarantine. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "entherium_create_task", "arguments": { "title": "", "description": "<description>", "assignee": "<assignee>", "spec_ref": "<spec_ref>" } } } ``` ## Returns | Field | Type | Description | | --- | --- | --- | | `accepted` | `boolean` (optional) | True iff the write was applied / the report was taken in | | `error` | `string` (optional) | Why it was not accepted (truthful, never a silent drop) | | `task_id` | `integer` (optional) | Id of the created task (create_task only) | ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` | Token missing/expired | Tokens are issued by the platform owner via `POST /api/v1/auth/agent-token` (superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set | | `403` / capability error | Your token lacks the capability or the contract grant | Check `my_capabilities` first; ask the owner for the grant | --- --- title: entherium_get_task description: READ one task in full by numeric `task_id`, INCLUDING its update log — every status change, reassignment, spec-retarget and note, in time order (who / what / when). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/entherium_get_task/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the running server (agent capability version `2026-09-03.1`) — do not edit by hand. ::: READ one task in full by numeric `task_id`, INCLUDING its update log — every status change, reassignment, spec-retarget and note, in time order (who / what / when). Use it to check a task's current state and history before acting on it. Returns the task fields plus `activity: [{at, by, kind, verb, field, old, new, note}]`, or {"error": "task not found"}. Needs the `manage_task` capability. **Access:** read-only — safe to call repeatedly, changes nothing **Available at:** `/mcp/`, `/mcp/readonly/` _Hints declared to MCP clients (ToolAnnotations)_ | Hint | Value | | --- | --- | | `readOnlyHint` | true | | `destructiveHint` | false | | `idempotentHint` | true | | `openWorldHint` | false | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `task_id` | `integer` | yes | Numeric task id returned by create | ## Example call Send inside an initialized MCP session (see [Connecting](../../connecting/)). Every parameter is shown with a typed placeholder; drop the optional ones you do not need — but for defect/task tools the QA fields are what keeps a report out of quarantine. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "entherium_get_task", "arguments": { "task_id": 1 } } } ``` ## Returns | Field | Type | Description | | --- | --- | --- | | `task_id` | `integer` (optional) | Task id on the board | | `title` | `string` (optional) | | | `status` | `string` (optional) | Board status of the task | | `assignee` | `string \| null` (optional) | Assignee display name, if any | | `spec_ref` | `string \| null` (optional) | Feature id the task targets (opaque SPEC-NNN), if any | | `description` | `string \| null` (optional) | | | `updated_at` | `string \| null` (optional) | ISO-8601 time of the last change | | `note_count` | `integer` (optional) | Number of notes logged on the task (listing only) | | `last_note_at` | `string \| null` (optional) | ISO-8601 time of the latest note (listing only) | | `created_at` | `string \| null` (optional) | ISO-8601 creation time | | `closed_at` | `string \| null` (optional) | ISO-8601 close time, if closed | | `activity` | `array of object` (optional) | Full time-ordered change log of the task | | `activity[].at` | `string \| null` | ISO-8601 time of the event | | `activity[].by` | `string \| null` | Actor display name | | `activity[].kind` | `string` | Event kind (status change, reassign, note, …) | | `activity[].verb` | `string \| null` | Human verb for the event | | `activity[].field` | `string \| null` | Field that changed, if any | | `activity[].old` | `object` | Previous value, if any | | `activity[].new` | `object` | New value, if any | | `activity[].note` | `string \| null` | Note body for note events | | `error` | `string` (optional) | Present only when the task could not be read | ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` | Token missing/expired | Tokens are issued by the platform owner via `POST /api/v1/auth/agent-token` (superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set | | `403` / capability error | Your token lacks the capability or the contract grant | Check `my_capabilities` first; ask the owner for the grant | --- --- title: entherium_list_defects description: READ the defect board (defects + incidents), newest first. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/entherium_list_defects/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the running server (agent capability version `2026-09-03.1`) — do not edit by hand. ::: READ the defect board (defects + incidents), newest first. Call this BEFORE filing a defect, to check it is not already reported. Filter by `status` ("open" | "closed") and/or `severity`. Returns {"count": N, "defects": [{defect_id, title, status, severity, component, spec_ref, reporter, last_seen}]}. Needs the `manage_task` capability. (A per-defect change log does not exist yet — only tasks carry one.) **Access:** read-only — safe to call repeatedly, changes nothing **Available at:** `/mcp/`, `/mcp/readonly/` _Hints declared to MCP clients (ToolAnnotations)_ | Hint | Value | | --- | --- | | `readOnlyHint` | true | | `destructiveHint` | false | | `idempotentHint` | true | | `openWorldHint` | false | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `status` | `string \| null` | no | open or closed; omit for both | | `severity` | `string \| null` | no | sev1 … sev4 | | `limit` | `integer` | no | Maximum number of defects to return | ## Example call Send inside an initialized MCP session (see [Connecting](../../connecting/)). Every parameter is shown with a typed placeholder; drop the optional ones you do not need — but for defect/task tools the QA fields are what keeps a report out of quarantine. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "entherium_list_defects", "arguments": { "status": "<status>", "severity": "<severity>", "limit": 1 } } } ``` ## Returns | Field | Type | Description | | --- | --- | --- | | `count` | `integer` (optional) | Number of rows returned | | `defects` | `array of object` (optional) | Defects and incidents, newest first | | `defects[].defect_id` | `integer` | Ticket id on the board | | `defects[].title` | `string` | | | `defects[].status` | `string` | Ticket status | | `defects[].severity` | `string \| null` | Severity (sev1 … sev4), if set | | `defects[].component` | `string \| null` | Component the defect is filed against | | `defects[].spec_ref` | `string \| null` | Feature id (opaque SPEC-NNN), if any | | `defects[].reporter` | `string \| null` | Reporter identity as recorded | | `defects[].last_seen` | `string \| null` | ISO-8601 time the defect was last observed | | `error` | `string` (optional) | Present only when the board could not be read | ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` | Token missing/expired | Tokens are issued by the platform owner via `POST /api/v1/auth/agent-token` (superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set | | `403` / capability error | Your token lacks the capability or the contract grant | Check `my_capabilities` first; ask the owner for the grant | --- --- title: entherium_list_tasks description: READ the task board — check what work already exists and its STATUS. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/entherium_list_tasks/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the running server (agent capability version `2026-09-03.1`) — do not edit by hand. ::: READ the task board — check what work already exists and its STATUS. Call this BEFORE creating a task, to avoid filing a duplicate of something already tracked. Filter by `status` ("open" | "closed") and/or `assignee` NAME (a team member's display name). Returns {"total": N, "tasks": [{task_id, title, status, assignee, spec_ref, description, updated_at, note_count, last_note_at}]}. `note_count`>0 means the task carries a note/decision in its log that is NOT shown here — call `entherium_get_task` before drawing any conclusion about that task. Needs the `manage_task` capability. **Access:** read-only — safe to call repeatedly, changes nothing **Available at:** `/mcp/`, `/mcp/readonly/` _Hints declared to MCP clients (ToolAnnotations)_ | Hint | Value | | --- | --- | | `readOnlyHint` | true | | `destructiveHint` | false | | `idempotentHint` | true | | `openWorldHint` | false | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `status` | `string \| null` | no | open or closed; omit for both | | `assignee` | `string \| null` | no | Team member's display name | | `limit` | `integer` | no | Maximum number of tasks to return | ## Example call Send inside an initialized MCP session (see [Connecting](../../connecting/)). Every parameter is shown with a typed placeholder; drop the optional ones you do not need — but for defect/task tools the QA fields are what keeps a report out of quarantine. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "entherium_list_tasks", "arguments": { "status": "<status>", "assignee": "<assignee>", "limit": 1 } } } ``` ## Returns | Field | Type | Description | | --- | --- | --- | | `total` | `integer` (optional) | Total tasks matching the filter | | `tasks` | `array of object` (optional) | One row per task, with a note signal | | `tasks[].task_id` | `integer` (optional) | Task id on the board | | `tasks[].title` | `string` (optional) | | | `tasks[].status` | `string` (optional) | Board status of the task | | `tasks[].assignee` | `string \| null` (optional) | Assignee display name, if any | | `tasks[].spec_ref` | `string \| null` (optional) | Feature id the task targets (opaque SPEC-NNN), if any | | `tasks[].description` | `string \| null` (optional) | | | `tasks[].updated_at` | `string \| null` (optional) | ISO-8601 time of the last change | | `tasks[].note_count` | `integer` (optional) | Number of notes logged on the task (listing only) | | `tasks[].last_note_at` | `string \| null` (optional) | ISO-8601 time of the latest note (listing only) | | `error` | `string` (optional) | Present only when the board could not be read | ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` | Token missing/expired | Tokens are issued by the platform owner via `POST /api/v1/auth/agent-token` (superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set | | `403` / capability error | Your token lacks the capability or the contract grant | Check `my_capabilities` first; ask the owner for the grant | --- --- title: entherium_log_task_note description: Append a free-text `note` to a task's change-log by its numeric `task_id`. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/entherium_log_task_note/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the running server (agent capability version `2026-09-03.1`) — do not edit by hand. ::: Append a free-text `note` to a task's change-log by its numeric `task_id`. Returns {"accepted": true} or {"accepted": false, "error": ...}. **Access:** writes — the client asks you for confirmation before every call **Available at:** `/mcp/` _Hints declared to MCP clients (ToolAnnotations)_ | Hint | Value | | --- | --- | | `readOnlyHint` | false | | `destructiveHint` | false | | `idempotentHint` | false | | `openWorldHint` | false | | `anthropic/requiresUserInteraction` | true | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `task_id` | `integer` | yes | Numeric task id returned by create | | `note` | `string` | yes | Free-text note appended to the task's change log | ## Example call Send inside an initialized MCP session (see [Connecting](../../connecting/)). Every parameter is shown with a typed placeholder; drop the optional ones you do not need — but for defect/task tools the QA fields are what keeps a report out of quarantine. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "entherium_log_task_note", "arguments": { "task_id": 1, "note": "<note>" } } } ``` ## Returns | Field | Type | Description | | --- | --- | --- | | `accepted` | `boolean` (optional) | True iff the write was applied / the report was taken in | | `error` | `string` (optional) | Why it was not accepted (truthful, never a silent drop) | | `task_id` | `integer` (optional) | Id of the created task (create_task only) | ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` | Token missing/expired | Tokens are issued by the platform owner via `POST /api/v1/auth/agent-token` (superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set | | `403` / capability error | Your token lacks the capability or the contract grant | Check `my_capabilities` first; ask the owner for the grant | --- --- title: entherium_propose_improvement description: Propose an IMPROVEMENT to the platform (a feature or idea). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/entherium_propose_improvement/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the running server (agent capability version `2026-09-03.1`) — do not edit by hand. ::: Propose an IMPROVEMENT to the platform (a feature or idea). Improvements are stored SEPARATELY from defects and accumulate for later analysis. Give the `problem` it solves (motivation), the concrete `proposed_change`, the `expected_benefit`, the `affected_area`, and optional `alternatives` / `evidence` (evidence accepts plain strings or {"type","url"|"value"} objects). Returns {"accepted": true} when taken, or {"accepted": false} if it could not be accepted. **Access:** writes — the client asks you for confirmation before every call **Available at:** `/mcp/` _Hints declared to MCP clients (ToolAnnotations)_ | Hint | Value | | --- | --- | | `readOnlyHint` | false | | `destructiveHint` | false | | `idempotentHint` | false | | `openWorldHint` | false | | `anthropic/requiresUserInteraction` | true | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `title` | `string` | yes | One-line summary of the proposal | | `problem` | `string \| null` | no | The problem or friction it solves (motivation) | | `proposed_change` | `string \| null` | no | The concrete change you propose | | `expected_benefit` | `string \| null` | no | What improves and for whom | | `affected_area` | `string \| null` | no | Platform area affected (e.g. mcp, portal, publish) | | `alternatives` | `array \| null` | no | Alternatives you considered | | `evidence` | `array \| array \| string \| null` | no | Supporting links or quotes — plain strings or {{type, url\|value}} objects | ## Example call Send inside an initialized MCP session (see [Connecting](../../connecting/)). Every parameter is shown with a typed placeholder; drop the optional ones you do not need — but for defect/task tools the QA fields are what keeps a report out of quarantine. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "entherium_propose_improvement", "arguments": { "title": "<title>", "problem": "<problem>", "proposed_change": "<proposed_change>", "expected_benefit": "<expected_benefit>", "affected_area": "<affected_area>", "alternatives": [], "evidence": [] } } } ``` ## Returns | Field | Type | Description | | --- | --- | --- | | `accepted` | `boolean` (optional) | True iff the write was applied / the report was taken in | | `error` | `string` (optional) | Why it was not accepted (truthful, never a silent drop) | | `task_id` | `integer` (optional) | Id of the created task (create_task only) | ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` | Token missing/expired | Tokens are issued by the platform owner via `POST /api/v1/auth/agent-token` (superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set | | `403` / capability error | Your token lacks the capability or the contract grant | Check `my_capabilities` first; ask the owner for the grant | --- --- title: entherium_reassign_task description: Reassign a task (numeric `task_id`) to another team member by NAME (resolved to that user; unknown/ambiguous rejected). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/entherium_reassign_task/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the running server (agent capability version `2026-09-03.1`) — do not edit by hand. ::: Reassign a task (numeric `task_id`) to another team member by NAME (resolved to that user; unknown/ambiguous rejected). Returns {"accepted": true} or {"accepted": false, ...}. **Access:** writes — the client asks you for confirmation before every call **Available at:** `/mcp/` _Hints declared to MCP clients (ToolAnnotations)_ | Hint | Value | | --- | --- | | `readOnlyHint` | false | | `destructiveHint` | false | | `idempotentHint` | true | | `openWorldHint` | false | | `anthropic/requiresUserInteraction` | true | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `task_id` | `integer` | yes | Numeric task id returned by create | | `assignee` | `string` | yes | Team member's display name; unknown or ambiguous names are rejected | ## Example call Send inside an initialized MCP session (see [Connecting](../../connecting/)). Every parameter is shown with a typed placeholder; drop the optional ones you do not need — but for defect/task tools the QA fields are what keeps a report out of quarantine. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "entherium_reassign_task", "arguments": { "task_id": 1, "assignee": "<assignee>" } } } ``` ## Returns | Field | Type | Description | | --- | --- | --- | | `accepted` | `boolean` (optional) | True iff the write was applied / the report was taken in | | `error` | `string` (optional) | Why it was not accepted (truthful, never a silent drop) | | `task_id` | `integer` (optional) | Id of the created task (create_task only) | ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` | Token missing/expired | Tokens are issued by the platform owner via `POST /api/v1/auth/agent-token` (superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set | | `403` / capability error | Your token lacks the capability or the contract grant | Check `my_capabilities` first; ask the owner for the grant | --- --- title: entherium_report_defect description: File a DEFECT you found in the platform. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/entherium_report_defect/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the running server (agent capability version `2026-09-03.1`) — do not edit by hand. ::: File a DEFECT you found in the platform. Give MAXIMUM detail so it can be detected AND reproduced, as in full QA management: ordered `steps_to_reproduce`, what you `expected` vs the `actual` behaviour, `evidence` (links / quotes / screenshot URLs / logs — plain strings or {"type","url"|"value"} objects both work), the `component`, and `environment` (instance/url/run_guid/commit). Set `spec_ref` to the feature's SPEC id ("SPEC-NNN") when the defect is about a known spec, so it targets the right feature context (an unknown spec id is REJECTED). A report WITHOUT repro steps or an evidence locator is QUARANTINED, not tracked. Duplicates of an existing defect are merged server-side. Returns {"accepted": true} when the report is taken, or {"accepted": false} if it could not be accepted — nothing else about the ticket comes back. **Access:** writes — the client asks you for confirmation before every call **Available at:** `/mcp/` _Hints declared to MCP clients (ToolAnnotations)_ | Hint | Value | | --- | --- | | `readOnlyHint` | false | | `destructiveHint` | false | | `idempotentHint` | false | | `openWorldHint` | false | | `anthropic/requiresUserInteraction` | true | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `title` | `string` | yes | One-line summary of the defect, as a QA ticket title | | `steps_to_reproduce` | `array \| null` | no | Ordered steps that make the defect appear; required for the report to be tracked | | `expected` | `string \| null` | no | What should have happened | | `actual` | `string \| null` | no | What actually happened | | `evidence` | `array \| array \| string \| null` | no | Links, quotes, screenshot URLs or log lines — plain strings or {{type, url\|value}} objects | | `component` | `string \| null` | no | Platform area the defect belongs to (e.g. publish, generate, portal) | | `severity` | `string \| null` | no | sev1 (outage) … sev4 (cosmetic) | | `environment` | `object \| null` | no | Where it happened: instance, url, run_guid, commit | | `behavior_cases` | `array \| null` | no | Other inputs or cases you tried and what they did | | `spec_ref` | `string \| null` | no | Feature id of the form SPEC-NNN the defect belongs to; unknown ids are rejected | ## Example call Send inside an initialized MCP session (see [Connecting](../../connecting/)). Every parameter is shown with a typed placeholder; drop the optional ones you do not need — but for defect/task tools the QA fields are what keeps a report out of quarantine. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "entherium_report_defect", "arguments": { "title": "<title>", "steps_to_reproduce": [], "expected": "<expected>", "actual": "<actual>", "evidence": [], "component": "<component>", "severity": "<severity>", "environment": {}, "behavior_cases": [], "spec_ref": "<spec_ref>" } } } ``` ## Returns | Field | Type | Description | | --- | --- | --- | | `accepted` | `boolean` (optional) | True iff the write was applied / the report was taken in | | `error` | `string` (optional) | Why it was not accepted (truthful, never a silent drop) | | `task_id` | `integer` (optional) | Id of the created task (create_task only) | ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` | Token missing/expired | Tokens are issued by the platform owner via `POST /api/v1/auth/agent-token` (superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set | | `403` / capability error | Your token lacks the capability or the contract grant | Check `my_capabilities` first; ask the owner for the grant | --- --- title: entherium_search_chat description: Search the team's WORK-CHAT ARCHIVE for messages on a topic — this is how you gather CONTEXT for a task or defect: who asked for it, in what WORDS, when. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/entherium_search_chat/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the running server (agent capability version `2026-09-03.1`) — do not edit by hand. ::: Search the team's WORK-CHAT ARCHIVE for messages on a topic — this is how you gather CONTEXT for a task or defect: who asked for it, in what WORDS, when. Use it BEFORE filing a task/defect from a chat request, and to clarify the intent of one already filed. `chat`: "work" = the team work chat (default). `author`: substring of a team member's display name. Returns {"found": N, "messages": [{at, author, text, is_full, chat}]} — a message with `is_full: false` is TRUNCATED by the log and MUST NOT be quoted as verbatim. Requires the global `read_all` capability: this is internal team traffic, not available to client-scoped tokens. **Access:** read-only — safe to call repeatedly, changes nothing **Available at:** `/mcp/`, `/mcp/readonly/` _Hints declared to MCP clients (ToolAnnotations)_ | Hint | Value | | --- | --- | | `readOnlyHint` | true | | `destructiveHint` | false | | `idempotentHint` | true | | `openWorldHint` | false | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `query` | `string` | yes | Topic or phrase to look for in the archive | | `chat` | `string \| null` | no | Chat alias: work (default) or agents | | `author` | `string \| null` | no | Substring of the author's display name | | `since_days` | `integer \| null` | no | Only messages from the last N days | | `limit` | `integer` | no | Maximum number of messages to return | ## Example call Send inside an initialized MCP session (see [Connecting](../../connecting/)). Every parameter is shown with a typed placeholder; drop the optional ones you do not need — but for defect/task tools the QA fields are what keeps a report out of quarantine. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "entherium_search_chat", "arguments": { "query": "<query>", "chat": "<chat>", "author": "<author>", "since_days": 1, "limit": 1 } } } ``` ## Returns | Field | Type | Description | | --- | --- | --- | | `found` | `integer` (optional) | Total matches (the page may be shorter) | | `messages` | `array of object` (optional) | Matching messages, newest first | | `messages[].at` | `string` | ISO-8601 timestamp of the message | | `messages[].author` | `string \| null` | Author as recorded in the archive | | `messages[].text` | `string` | Message text; may be truncated when is_full is false | | `messages[].is_full` | `boolean` | False when the archive holds a truncated copy — do not quote it verbatim | | `messages[].chat` | `string \| null` | Chat name or id | | `error` | `string` (optional) | Present only when the search could not run (e.g. missing capability) | ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` | Token missing/expired | Tokens are issued by the platform owner via `POST /api/v1/auth/agent-token` (superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set | | `403` / capability error | Your token lacks the capability or the contract grant | Check `my_capabilities` first; ask the owner for the grant | --- --- title: entherium_target_task_spec description: Target (or retarget) a task at a platform feature by its SPEC id ('SPEC-NNN') — keeps the task tied to the right feature context. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/entherium_target_task_spec/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the running server (agent capability version `2026-09-03.1`) — do not edit by hand. ::: Target (or retarget) a task at a platform feature by its SPEC id ("SPEC-NNN") — keeps the task tied to the right feature context. An unknown spec id is REJECTED. Returns {"accepted": true} or {"accepted": false, "error": ...}. **Access:** writes — the client asks you for confirmation before every call **Available at:** `/mcp/` _Hints declared to MCP clients (ToolAnnotations)_ | Hint | Value | | --- | --- | | `readOnlyHint` | false | | `destructiveHint` | false | | `idempotentHint` | true | | `openWorldHint` | false | | `anthropic/requiresUserInteraction` | true | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `task_id` | `integer` | yes | Numeric task id returned by create | | `spec_ref` | `string` | yes | Feature id of the form SPEC-NNN; unknown ids are rejected | ## Example call Send inside an initialized MCP session (see [Connecting](../../connecting/)). Every parameter is shown with a typed placeholder; drop the optional ones you do not need — but for defect/task tools the QA fields are what keeps a report out of quarantine. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "entherium_target_task_spec", "arguments": { "task_id": 1, "spec_ref": "<spec_ref>" } } } ``` ## Returns | Field | Type | Description | | --- | --- | --- | | `accepted` | `boolean` (optional) | True iff the write was applied / the report was taken in | | `error` | `string` (optional) | Why it was not accepted (truthful, never a silent drop) | | `task_id` | `integer` (optional) | Id of the created task (create_task only) | ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` | Token missing/expired | Tokens are issued by the platform owner via `POST /api/v1/auth/agent-token` (superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set | | `403` / capability error | Your token lacks the capability or the contract grant | Check `my_capabilities` first; ask the owner for the grant | --- --- title: entherium_update_task_status description: Set a task's status to 'open' or 'closed' by its numeric `task_id` (from create). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/entherium_update_task_status/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the running server (agent capability version `2026-09-03.1`) — do not edit by hand. ::: Set a task's status to "open" or "closed" by its numeric `task_id` (from create). Closing an already-closed task is rejected. Optionally attach a `note` explaining the change. Returns {"accepted": true} or {"accepted": false, "error": ...}. **Access:** writes — the client asks you for confirmation before every call **Available at:** `/mcp/` _Hints declared to MCP clients (ToolAnnotations)_ | Hint | Value | | --- | --- | | `readOnlyHint` | false | | `destructiveHint` | true | | `idempotentHint` | false | | `openWorldHint` | false | | `anthropic/requiresUserInteraction` | true | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `task_id` | `integer` | yes | Numeric task id returned by create | | `status` | `string` | yes | open or closed | | `note` | `string \| null` | no | Optional note explaining the change | ## Example call Send inside an initialized MCP session (see [Connecting](../../connecting/)). Every parameter is shown with a typed placeholder; drop the optional ones you do not need — but for defect/task tools the QA fields are what keeps a report out of quarantine. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "entherium_update_task_status", "arguments": { "task_id": 1, "status": "<status>", "note": "<note>" } } } ``` ## Returns | Field | Type | Description | | --- | --- | --- | | `accepted` | `boolean` (optional) | True iff the write was applied / the report was taken in | | `error` | `string` (optional) | Why it was not accepted (truthful, never a silent drop) | | `task_id` | `integer` (optional) | Id of the created task (create_task only) | ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` | Token missing/expired | Tokens are issued by the platform owner via `POST /api/v1/auth/agent-token` (superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set | | `403` / capability error | Your token lacks the capability or the contract grant | Check `my_capabilities` first; ask the owner for the grant | --- --- title: MCP tools reference description: All 14 MCP tools exposed by the platform server, generated from the running server (capability 2026-09-03.1). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. The platform MCP server exposes **14 tools** and **5 resources**. Every page below is generated from the running server — it cannot be stale relative to agent capability version `2026-09-03.1`. The server's own onboarding page, `GET /mcp-info`, renders the same list from the same introspection. ## Mount points | Mount | Token | What is listed | | --- | --- | --- | | `/mcp/` | agent token | all 14 tools (7 read, 7 write) | | `/mcp/readonly/` | agent token | the 7 read tools only (readOnlyHint=true) — the safe default for evaluation and for clients that must never write | | `/mcp/docs/` | **none** | the developer documentation as tools: `search`, `fetch`, `submit_feedback` — see [Connecting](../connecting/#the-docs-mcp-no-token) | ## Tools | Tool | Kind | What it does | | --- | --- | --- | | [`entherium_create_task`](./entherium_create_task/) | write | Create a TASK for a team member. | | [`entherium_get_task`](./entherium_get_task/) | read | READ one task in full by numeric `task_id`, INCLUDING its update log — every status change, reassignment, spec-retarget and note, in time order (who / what / when). | | [`entherium_list_defects`](./entherium_list_defects/) | read | READ the defect board (defects + incidents), newest first. | | [`entherium_list_tasks`](./entherium_list_tasks/) | read | READ the task board — check what work already exists and its STATUS. | | [`entherium_log_task_note`](./entherium_log_task_note/) | write | Append a free-text `note` to a task's change-log by its numeric `task_id`. | | [`entherium_propose_improvement`](./entherium_propose_improvement/) | write | Propose an IMPROVEMENT to the platform (a feature or idea). | | [`entherium_reassign_task`](./entherium_reassign_task/) | write | Reassign a task (numeric `task_id`) to another team member by NAME (resolved to that user; unknown/ambiguous rejected). | | [`entherium_report_defect`](./entherium_report_defect/) | write | File a DEFECT you found in the platform. | | [`entherium_search_chat`](./entherium_search_chat/) | read | Search the team's WORK-CHAT ARCHIVE for messages on a topic — this is how you gather CONTEXT for a task or defect: who asked for it, in what WORDS, when. | | [`entherium_target_task_spec`](./entherium_target_task_spec/) | write | Target (or retarget) a task at a platform feature by its SPEC id ("SPEC-NNN") — keeps the task tied to the right feature context. | | [`entherium_update_task_status`](./entherium_update_task_status/) | write | Set a task's status to "open" or "closed" by its numeric `task_id` (from create). | | [`list_domains`](./list_domains/) | read | List the platform's capability domains (name, prefix, summary, stage). | | [`my_capabilities`](./my_capabilities/) | read | What your agent token may DO. | | [`start_here`](./start_here/) | read | Call this FIRST. | ## Resources | URI | Description | | --- | --- | | `changelog://entherium` | Structured capability changelog of the agent read surface, newest first — diff it from your cached serverInfo.version to see what changed (start_here.read_first points here). | | `docs://portal` | Section map of the public developer documentation portal. Give USERS the html urls (browsable pages); fetch the md twins yourself (same content, a fraction of the tokens). | | `platform://domains` | The full domain catalog as a markdown list. | | `platform://overview` | One-paragraph overview of what the platform does. | | `platform://workflow` | The content workflow stages, in order. | --- --- title: list_domains description: List the platform's capability domains (name, prefix, summary, stage). locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/list_domains/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the running server (agent capability version `2026-09-03.1`) — do not edit by hand. ::: List the platform's capability domains (name, prefix, summary, stage). **Access:** read-only — safe to call repeatedly, changes nothing **Available at:** `/mcp/`, `/mcp/readonly/` _Hints declared to MCP clients (ToolAnnotations)_ | Hint | Value | | --- | --- | | `readOnlyHint` | true | | `destructiveHint` | false | | `idempotentHint` | true | | `openWorldHint` | false | ## Parameters This tool takes no parameters. ## Example call Send inside an initialized MCP session (see [Connecting](../../connecting/)). Every parameter is shown with a typed placeholder; drop the optional ones you do not need — but for defect/task tools the QA fields are what keeps a report out of quarantine. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_domains", "arguments": {} } } ``` ## Returns On the wire the list is wrapped: `structuredContent` is `{"result": [...]}` (MCP requires an object at the top level). | Field | Type | Description | | --- | --- | --- | | `result` | `array of object` | | | `result[].name` | `string` | Domain key (matches the API module) | | `result[].prefix` | `string` | REST path prefix of the domain | | `result[].summary` | `string` | What the domain covers | | `result[].stage` | `string` | Workflow stage the domain belongs to, or 'cross' | | `result[].agent` | `boolean` | True when a content-operating agent drives this domain | ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` | Token missing/expired | Tokens are issued by the platform owner via `POST /api/v1/auth/agent-token` (superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set | | `403` / capability error | Your token lacks the capability or the contract grant | Check `my_capabilities` first; ask the owner for the grant | --- --- title: my_capabilities description: What your agent token may DO. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/my_capabilities/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the running server (agent capability version `2026-09-03.1`) — do not edit by hand. ::: What your agent token may DO. The write surface is the authoritative scope manifest (the platform enforces the same set); reads cover the allowed domains with secrets redacted. Writes apply only to contracts in your grant pool. **Access:** read-only — safe to call repeatedly, changes nothing **Available at:** `/mcp/`, `/mcp/readonly/` _Hints declared to MCP clients (ToolAnnotations)_ | Hint | Value | | --- | --- | | `readOnlyHint` | true | | `destructiveHint` | false | | `idempotentHint` | true | | `openWorldHint` | false | ## Parameters This tool takes no parameters. ## Example call Send inside an initialized MCP session (see [Connecting](../../connecting/)). Every parameter is shown with a typed placeholder; drop the optional ones you do not need — but for defect/task tools the QA fields are what keeps a report out of quarantine. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "my_capabilities", "arguments": {} } } ``` ## Returns | Field | Type | Description | | --- | --- | --- | | `writes` | `array of object` | Every write your token class may perform | | `writes[].method` | `string` | HTTP method of the allowed write | | `writes[].path` | `string` | Path template of the allowed write | | `writes[].capability` | `string` | Capability the write requires | | `writes[].entity` | `string` | Entity the write targets | | `writes[].pool_check` | `boolean` | True when the target must be in your contract grant pool | | `writable_fields` | `object` | Field-level allowlist for agent writes | | `writable_fields.fields` | `array of string` | Field names an agent write may carry | | `writable_fields.by_entity` | `object` | Writable fields per entity | | `writable_fields.applies_to` | `array of string` | Operations the field allowlist applies to | | `writable_fields.note` | `string` | What happens to keys outside the allowlist | | `reads` | `string` | What you may read and through which channel | | `note` | `string` | Scope rule for writes | ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` | Token missing/expired | Tokens are issued by the platform owner via `POST /api/v1/auth/agent-token` (superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set | | `403` / capability error | Your token lacks the capability or the contract grant | Check `my_capabilities` first; ask the owner for the grant | --- --- title: start_here description: Call this FIRST. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/mcp/tools/start_here/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. :::note Generated from the running server (agent capability version `2026-09-03.1`) — do not edit by hand. ::: Call this FIRST. Returns the ordered plan + the capability version of the read surface. Pass your cached serverInfo.version as `last_seen_version`: if the surface changed, `changed` is true and `whats_new` lists what to adopt (new endpoints/fields) BEFORE you act. On a first connect (no cached version) `whats_new` is the full changelog. Cache the returned `capability_version` for next time. **Access:** read-only — safe to call repeatedly, changes nothing **Available at:** `/mcp/`, `/mcp/readonly/` _Hints declared to MCP clients (ToolAnnotations)_ | Hint | Value | | --- | --- | | `readOnlyHint` | true | | `destructiveHint` | false | | `idempotentHint` | true | | `openWorldHint` | false | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `last_seen_version` | `string \| null` | no | Capability version you cached from a previous connect (serverInfo.version); omit on first use | ## Example call Send inside an initialized MCP session (see [Connecting](../../connecting/)). Every parameter is shown with a typed placeholder; drop the optional ones you do not need — but for defect/task tools the QA fields are what keeps a report out of quarantine. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "start_here", "arguments": { "last_seen_version": "<last_seen_version>" } } } ``` ## Returns | Field | Type | Description | | --- | --- | --- | | `capability_version` | `string` | Current version of the agent read surface | | `you_last_seen` | `string \| null` | The version you passed in, echoed back | | `changed` | `boolean` | True iff you passed a version and it differs from the current one | | `whats_new` | `array of object` | Changelog entries newer than your version (all of them on a first connect) | | `whats_new[].version` | `string` | Capability version of this entry (date-anchored, monotonic) | | `whats_new[].added` | `array of string` | New endpoints, tools or fields a consumer can adopt | | `whats_new[].changed` | `array of string` | Semantics that changed for a consumer | | `whats_new[].deprecated` | `array of string` | What is going away and what replaces it | | `read_first` | `string \| null` | Resource to read before acting when there is anything to adopt | | `plan` | `string` | Ordered bootstrap plan for using the platform | | `docs` | `object` | Public documentation entry points | | `docs.portal` | `string` | Developer documentation portal (HTML entry) | | `docs.llms` | `string` | Curated Markdown index of the portal (llms.txt) | | `docs.sections` | `string` | Where the per-section map lives (MCP resource URI) | | `docs.note` | `string` | How to use the portal on a user's behalf | ## If it fails | Symptom | Cause | Action | | --- | --- | --- | | `401` | Token missing/expired | Tokens are issued by the platform owner via `POST /api/v1/auth/agent-token` (superuser-only) — ask for a re-issue. ⚠️ A new token **replaces** your grant set | | `403` / capability error | Your token lacks the capability or the contract grant | Check `my_capabilities` first; ask the owner for the grant | --- --- title: Pipeline catalog description: The pipelines that produce and publish content — Collect, Generate and Localize — how they are registered, versioned and dispatched from pipelines/catalog.yaml. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/pipelines/catalog/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. All content on the platform is produced by **pipelines** — multi-step programs that run against a contract's data. The catalog file `pipelines/catalog.yaml` is the single source of truth for dispatch: which pipelines exist, which variants they have, and which version is the current default. ## The pipelines | Pipeline | Purpose | Runs against | | --- | --- | --- | | **Collect** (`link_ranker`) | Source discovery: harvests candidate source articles for a contract's niche, ranks them by semantic relevance to the contract's ontology, and qualifies the best ones for generation. The v3 line is vector-based: candidates are ranked on embedding cosine similarity rather than keyword matching. | a sales contract | | **Generate** (`generate`) | Article production: takes a qualified source, grounds it in the contract's research corpus (RAG), writes the article through a staged LLM pipeline with deterministic quality gates, renders the final layout and publishes it. | a generated article | | **Localize** (`localize`) | Language versions: produces localized variants of an article for multilingual instances. | a sales contract | ## How the catalog works Each pipeline has **variants** (`prod`, `dev`) and a list of **versions**, with a `latest` pointer naming the current default: ```yaml pipelines: generate: display_name: "Generate (контент-пайплайн)" entity: generated_articles variants: prod: latest: v3.1.x # current default versions: v3.0.0: { ... } # every version stays listed — and runnable v3.1.x: { ... } ``` Three rules follow from this design: 1. **Adding a version is a one-line edit** in the catalog plus a new folder under `pipelines/{id}/{variant}/{version}/`. Nothing else registers it. 2. **Old versions stay runnable.** A run can be pinned to any listed version; `latest` only sets the default for new dispatches. 3. **The manifest is the contract.** Each version folder carries its own `manifest.yaml` describing steps, required environment and entrypoints — the dispatcher reads the manifest, never the code, to decide how to run it. ## Where pipelines are triggered from - **The nightly daily-run** — the production scheduler dispatches Collect → Generate → publish across the whole contract fleet on a timer. The run is visible as `pipeline_run` / `run_step` records via the API. - **The API** — individual runs can be launched per contract or per article. - **Version pins** — the daily-run pins explicit pipeline versions, so a freshly-landed version does not reach production traffic until the pin is moved deliberately. --- --- title: Version lifecycle description: Immutable pipeline versions — why a change is always a new carve, never an edit, and how versions are promoted, pinned and rolled back. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/pipelines/versioning/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. Pipeline code is **immutable per version**. Once `v3.1.16` exists, its folder is never edited again — a change, however small, becomes a **new carve**: a byte-copy of the previous version plus the changed files, registered in the catalog under the next version number. ## Why immutability - **Reproducibility.** A `pipeline_run` records the version it executed; because that version can never change under it, any past run can be re-read — or re-run — exactly as it happened. - **Instant rollback.** Promotion is moving the `latest` pointer (and the daily-run pin). Rollback is moving it back. No code changes, no deploys of reverted files, no "mostly the same" ambiguity. - **Safe parallel work.** Sessions working on the next version cannot destabilize the version production is pinned to. ## The lifecycle of a version 1. **Carve** — copy the current version's folder to the next version number, apply the change, update the version's `manifest.yaml` if the step list or contract changed. 2. **Register** — add the one-line entry in `pipelines/catalog.yaml`. 3. **Verify** — run it against real data in the dev contour; compare output quality against the incumbent version. 4. **Promote** — move `latest` to the new version and repin the daily-run. Promotion of a production default is an owner-gated decision. 5. **Keep the predecessor** — the previous version stays listed as the rollback target. Retired lines are deleted from the tree only by an explicit cleanup decision; history stays in git. ## Version numbering conventions - Versions follow SemVer per pipeline (`vMAJOR.MINOR.PATCH`), independent of the platform's own module versions. - **A major line marks an architectural generation.** Example: for Collect, the v3 line is the vector-recall generation (embedding-based candidate ranking); v1/v2 were the keyword-based generation. The two lines coexist in the catalog, and the older one remains the emergency fallback. - A behavioural change with an unchanged manifest contract is a MINOR bump; a fix within the same behaviour is a PATCH. ## Pins The nightly daily-run does not follow `latest` blindly: it pins explicit versions for Collect and Generate. This creates a deliberate two-step promotion — land the version, then move the pin — so a regression discovered between the two steps never reaches the fleet. --- --- title: Architecture description: C4-style overview of Enterium — a modular monolith with CI-enforced bounded contexts, a React web UI, immutable native pipelines and vendored publishing services. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/platform/architecture/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. Enterium is a **modular monolith**: one FastAPI backend split into bounded contexts with CI-enforced boundaries, plus a React web UI, a set of immutable native pipelines executed in-process, and two small vendored services for publishing and media. ## System context ```mermaid flowchart TB Owner[Owner / team] --> WEB[Web UI] Agent[LLM agents] -- MCP + REST --> API WEB --> API[Platform API] API --> DB[(PostgreSQL<br/>+ pgvector)] API --> REDIS[(Redis)] API --> EXT[External providers<br/>LLM · search · images · storage] API -- render + deploy --> HUB[Publishing hub] HUB --> SITES[Fleet of static sites] SITES --> GSC[Search consoles] GSC -- daily metrics --> API ``` The API owns all state. External providers are reached only through the `integrations` context (a single audited channel); published sites are static properties built and deployed by the publishing hub; search metrics flow back in on a schedule. ## Containers ```mermaid flowchart TB WEBUI["webui<br/>React 19 + TypeScript, Vite"] --> CONTRACTS["contracts<br/>shared OpenAPI + JSON schemas"] CONTRACTS --> Backend subgraph Backend["platform — FastAPI, Python 3.12"] direction TB CTX["Bounded contexts<br/>integrations · runs · research · retrieval · quality · publish<br/>analytics · competitors · cockpit · ops · tickets · tasks · chatlog · specs"] NATIVE["Native pipelines<br/>immutable versioned dirs: collect · generate · localize"] FLAT["Shared layers<br/>auth · lifecycle engine · ORM · API routers"] end Backend --> SVC1["wp_deployer<br/>publishing hub service"] Backend --> SVC2["imagegen<br/>media generation service"] ``` ## The rules that keep it a monolith, not a tangle 1. **A bounded context is a vertical slice** of one business capability — its logic, schemas, router and tests live in one folder (one package per context inside the backend). 2. **Contexts talk only through contracts.** A context calls another only via its public `contracts.py` (typed interface) or an event — never by importing internals, never by querying another context's tables. This is enforced by an import linter in CI: a violation is a red build, not a review comment. 3. **One table — one owning context.** No cross-context SQL joins. 4. **New capability = new module, not new service.** A separate service is the exception and needs multiple hard triggers (different runtime, secret-scope isolation, independent scaling). The two vendored services exist precisely because they met those triggers. 5. **The shared kernel stays thin** — configuration, database, auth primitives, logging, event plumbing. No business logic. The full map of contexts with their responsibilities is on [Bounded contexts](../contexts/). ## Execution model There is **no permanently-running orchestrator daemon**. Production runs on a nightly timer that dispatches the Collect → Generate → publish cycle across the contract fleet; between runs the platform is idle by design (an `orchestrator.present: false` status is normal — see [Interpreting platform status](../../mcp/interpreting-status/)). Every execution is recorded as a `pipeline_run` with per-step `run_step` rows, so history is queryable rather than inferred. Pipelines themselves are **native and in-process**: each version is an immutable directory of Python steps resolved through the pipeline catalog. Failures are loud — an error in a step fails the run and is recorded in the error surface; there is no silent per-step retry (only the LLM adapter retries transient provider errors internally). --- --- title: Content lifecycle description: The end-to-end path of an article — Collect, Generate, the quality gates that stand on the publish path, publication, and the SEO feedback loop — as it actually runs nightly. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/platform/content-lifecycle/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. This is the end-to-end path of every article, as it actually runs in production: a nightly timer drives the whole cycle across the contract fleet; between runs the platform is idle. ```mermaid sequenceDiagram participant T as Nightly timer participant C as Collect participant G as Generate participant Q as Publish gates participant H as Publishing hub participant S as Search consoles T->>C: for each active contract C->>C: harvest + vector-rank sources,<br/>extract research facts C->>G: qualified source G->>G: staged LLM pipeline<br/>grounded in the corpus (RAG) G->>Q: candidate article Q->>Q: dedup · claim verification ·<br/>quality scoring Q->>H: approved article H->>H: build + deploy site H-->>Q: live URL verified S-->>T: daily metrics + topic feedback ``` ## Collect — finding what to write about Collect harvests candidate source articles for the contract's niche and ranks them by **embedding cosine similarity** against the contract's ontology (the current major line of the Collect pipeline is vector-based). Qualified sources are seeded as pending articles; alongside, source pages are mined into **research facts** — the contract's grounding corpus. A topic-deduplication check on the Collect side prevents already-covered topics from being seeded again. ## Generate — producing the article Generate is a staged native pipeline (a dozen-plus steps: fetch, summarize, ontology alignment, drafting, humanization, structure, images, validation, final layout). Two properties define it: - **Grounding.** Numbers and claims are drawn from the research corpus and live sources retrieved for the piece — with a relevance threshold on what the retriever may cite. - **Fail-loud, versioned execution.** The pipeline runs as an immutable version resolved from the catalog (per-contract pin available); any step error fails the run visibly rather than degrading silently. See [Version lifecycle](../../pipelines/versioning/). ## The publish path and its gates Publication converges into a single choke-point used by both the nightly batch and API-triggered publishes. On that path, today: | Gate | What it checks | On failure | | --- | --- | --- | | **Topic dedup** | Embedding similarity of the new piece against already-published topics for the contract, plus peers inside the same batch | The article is **deferred**, not discarded | | **Claim verification (grounding gate)** | Numerical and normative claims are checked against retrieved sources; ungrounded claims block | The article is quarantined for review | | **Quality scoring** | Mechanical cleanup plus a quality stamp: verification signals, brand-safety signals, language consistency | Recorded as telemetry with the article; feeds audits and remediation | The order matters operationally: gates run before the hub is asked to build, so a rejected article costs nothing downstream. ## Publication and proof The approved article's rendered HTML is shipped to the publishing hub, which rebuilds the destination site. The platform then verifies the **live URL** responds — only that verification counts as "published". The source↔slug pair lands in the published-URL registry. ## Measurement and feedback Daily search-console collection produces per-site time series (impressions, clicks, indexation), and per-topic performance feeds a **topic feedback loop**: topics that demonstrably earn impressions get more Collect quota; saturated or dead topics are throttled. Cost metering records every LLM call per run and contract, so the cost of a published article is a first-class metric. --- --- title: Bounded contexts description: The map of Enterium's bounded contexts — what each module owns, and how they are allowed to talk to each other. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/platform/contexts/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. The backend is split into bounded contexts, one package per context. Each context owns its tables, logic, schemas and router; isolation is enforced by an import linter in CI. This page is the map. ## Content production | Context | Owns | | --- | --- | | `research` | The accumulating per-contract knowledge base (research facts and related tables): enrichment, freshness, hygiene and conflict handling. The foundation of retrieval-grounded generation. | | `retrieval` | The universal vector primitive: chunking, embeddings, similarity search and reranking over pgvector. Used by research, publish and quality. | | `publish` | The publication conveyor: payload assembly, the registry of published URLs, the hub client, topic and batch deduplication, the grounding gate, quarantine, and topic feedback. | | `quality` | Post-publish article scoring: verification, claim checking against sources, brand-safety signals, language consistency, stale-publish detection. | | `competitors` | Lifecycle of a contract's competitor lists: candidate mining, the registry, and safe application into brand-defense declarations. | ## Execution and operations | Context | Owns | | --- | --- | | `runs` | Tracking and orchestration of pipeline executions: the version catalog, run events and projections, metering, autoscaling, staging and reconciliation. | | `integrations` | The **only** channel to external services (LLM, web search, image generation, storage, DNS): a declarative connection registry, pluggable secret backends, typed capability adapters and a health monitor. | | `ops` | The live operational snapshot that powers the dashboard and agent status reads. | | `analytics` | Search and traffic data (weekly search-console time series per instance) and derived analytics: scoring, topic performance, scheduling. | | `cockpit` | A read-only aggregator for the per-contract dashboard. | ## Knowledge and process | Context | Owns | | --- | --- | | `tickets` | The incident / defect / improvement tracker (the board agents write to). | | `tasks` | Human-assignable work tasks with statuses and notes. | | `chatlog` | The searchable archive of the team's working chat. | | `specs` | The registry linking features to their SPEC documents (traceability). | ## Shared layers outside contexts Some foundational layers predate the context split and live flat under the backend package: - `auth/` — JWT issuance, scopes and the agent grant model (see [Authentication](../../guides/authentication/)). - `lifecycle/` — the entity state-machine engine inherited from the platform's metamodel (see [Data model](../data-model/)). - `db/models/` — the ORM layer. - `api/v1/` — the REST routers (about 28 of them, grouped by capability). - `native/` — the immutable pipeline version directories themselves. ## The interaction rule A context reaches another context **only** through that context's public `contracts.py` or an event — never by importing internals, never by touching foreign tables. Examples of real seams: the publish gate reads quality verdicts through the quality contract; deduplication embeds text through the retrieval contract; every LLM call anywhere goes through an integrations adapter. If you are tracing behaviour across modules, start at the target context's `contracts.py` — it is the complete public surface. --- --- title: Data model description: The core entities of Enterium — companies, contracts, configs, runs, research facts, articles and published URLs — and the metamodel fields every business entity carries. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/platform/data-model/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. This page describes the **logical** data model — what the core entities mean and how they relate. It deliberately omits DDL detail; the authoritative schema lives in the platform's migrations. ## The contract hierarchy ```mermaid erDiagram AI_COMPANY ||--o{ SALESCONTRACT : "has" SALESCONTRACT ||--|| SCRAPPER_CONFIG : "configured by" SALESCONTRACT ||--o{ AUTHOR : "author pool" SCRAPPER_CONFIG ||--|| HUGO_INSTANCE : "publishes to" SALESCONTRACT ||--o{ RESEARCH_FACT : "corpus" SALESCONTRACT ||--o{ GENERATED_ARTICLE : "produces" GENERATED_ARTICLE ||--o| PUBLISHED_URL : "published as" ``` - **Company** (`ai_company`) — the top of the hierarchy; a simple entity. - **Contract** (`salescontracts`) — the central unit of work: one niche, one brand, one content program. An active contract drives the whole Collect → Generate → Publish cycle. - **Config** (`scrapper_config`) — 1:1 with a contract; everything that parameterizes its pipeline: the topic ontology, the brand description (which also feeds brand defense), source feeds, per-contract pipeline version pin, the publishing endpoint and the destination-site mark. - **Author pool** (`scrapper_lc_authors`) — the byline roster for the contract's site. - **Site** (`hugo_instance`) — one deployed destination site; linked to a contract through the config's instance mark. ## Production and evidence - **Run** (`pipeline_run` / `run_step`) — the record of one pipeline execution and its per-step progress. Infrastructure records, not business entities: this is where you look to see what actually happened. - **Research fact** (`research_fact`) — one extracted, source-attributed fact in a contract's corpus; carries the claim value used to ground numbers in articles. Facts have statuses, including quarantine for corpus hygiene. - **Article** (`generated_articles`) — the produced piece. The `article` field holds the final rendered HTML that gets published; a separate enriched-text field holds the unformatted editing form. A large JSON `extra` field accumulates per-article signals (dedup scores, grounding results, source class) — by convention it is merge-only. - **Published URL** (`published_url` and the external-URL check) — the registry mapping sources to live slugs. A publication only counts once the live URL has been verified reachable; this is the platform's proof-of-publication. ## Accounting and observability - **Usage event** (`usage_event`) — token-accurate cost metering for every LLM call, attributable to a run and a contract. - **Error event** (`error_event`) — the single error sink for the whole platform, queryable via the API (see [How to read errors](../../guides/reading-errors/)). - **Search metrics** (analytics tables) — daily search-console time series per site, plus topic-level performance used by the feedback loop. ## The metamodel heritage Business entities share a set of system fields inherited from the platform's metamodel: a global numeric id (`guid`), an optimistic-locking revision (`rsid`), a lifecycle pointer (`lcid` / `lcstep` — which state machine the entity follows and where it stands), and audit fields (owner, updated-by, timestamps). Two practical consequences: 1. **Lifecycle state is data.** "This contract is active" is a lifecycle step value, not a boolean column — the lifecycle engine advances entities through their state machines. 2. **References are application-level.** Relations in the diagram above are maintained by the application rather than database foreign keys, so integrity checks live in code and verifiers, not constraints. --- --- title: Environments and versioning description: The two-contour environment model (dev and prod), the promotion flow, and the two independent versioning axes — platform modules and pipeline versions. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/platform/environments/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. ## Two contours | Contour | Branch | Runs where | Purpose | | --- | --- | --- | --- | | **prod** | `main` | The production host, on the nightly schedule | Stable; every change arrives by deliberate promotion | | **dev** | `dev` | A local development stack (Docker: Postgres, Redis, API, web) | Integration; all day-to-day work lands here first | The flow is one-directional: feature branches merge into `dev` via small pull requests; promotion to `main` is a separate, explicitly-approved step; deployment to the production host is a separate script run after that. A hotfix that must land on `main` directly is immediately forward-ported to `dev` in the same session — `main` is always a subset of `dev`. ## Where it runs Production is a single self-hosted Linux box behind a Caddy reverse proxy, reachable through a dynamic-DNS name on a non-standard port (`entherium.duckdns.org:8443`). That is the base URL in every example on this portal, and it is the real one — there is no managed API gateway and no vanity domain in front of it yet. The fleet of destination sites lives on separate hosts. For a platform at this stage the box *is* production: nightly runs, the API, the MCP server and the task board all answer from it, and its uptime is watched by an external dead-man's switch. This documentation is served from two places with the same build: from the API host under `/developers/`, and from a Cloudflare Pages mirror. The mirror is the **canonical** copy — every page's `canonical` and `og:url` point there — and it stays up if the box does not. Both copies are rebuilt from the same commit by the same deploy scripts; when they differ, the mirror is the newer one. ## Versioning — two independent axes **Axis A: platform modules.** Each module (`platform`, `webui`, `contracts`, services, this documentation site) is versioned independently by SemVer, derived automatically from Conventional Commits by release tooling — versions are never bumped by hand. An umbrella platform version tracks the whole. **Axis B: pipeline versions.** Collect, Generate and Localize are versioned separately in the pipeline catalog, with immutable per-version code directories and per-contract pins. This axis moves at its own pace — a pipeline can advance several versions within one platform release, and vice versa. See [Version lifecycle](../../pipelines/versioning/). The two axes are deliberately decoupled: a platform deploy does not change which pipeline version the fleet runs, and promoting a pipeline version does not require a platform release. ## What a "release" means here - **Merged to `dev`** — integrated, gate-checked (lint, types, tests run locally as the merge gate), not yet user-visible. - **Promoted to `main` + deployed** — running on the production host. - **Enabled** — the platform's hard rule: a feature is only called deployed when it is switched on and its effect is observable (a metric, a page, a number). Code behind a disabled flag is explicitly *not* "deployed" in reports or documentation. --- --- title: Glossary description: The platform's terms of art — contract, corpus, carve, grounding, dedup defer, instance and friends — each defined in one or two sentences. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/platform/glossary/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. Terms you will meet across this portal and in the platform's own responses. | Term | Meaning | | --- | --- | | **Contract** | The central unit of work: one niche + one brand + one destination site, with its own configuration and research corpus. Formally a `salescontracts` entity. | | **Instance** | One deployed destination site (a static site built by the publishing hub), linked to a contract. | | **Ontology** | The contract's declared topic space — the entities and themes its content is about; used to rank source candidates and align drafts. | | **Corpus / research fact** | The contract's accumulated base of source-attributed facts. Generation grounds its numbers and claims here rather than inventing them. | | **RAG / grounding** | Retrieval-augmented generation: drafting against retrieved corpus facts and live sources. "Grounded" means a claim can point at its source. | | **Carve** | Creating a new immutable pipeline version: copy the previous version's directory, apply the change, register it in the catalog. Existing versions are never edited. | | **Pin** | An explicit pipeline-version selection (per contract, or for the nightly run) that overrides the catalog's `latest` pointer. | | **Daily run** | The nightly scheduled batch that drives Collect → Generate → publish across all active contracts. | | **Dedup defer** | The topic-deduplication gate's verdict for a too-similar article: publication is postponed (the topic may free up), not discarded. | | **Quarantine** | A holding state for content or corpus facts that failed a gate — excluded from use until reviewed. | | **Grounding gate** | The publish-path check that verifies numerical/normative claims against sources and blocks ungrounded ones. | | **Brand defense** | The mechanisms that keep an article promoting the contract's own brand instead of a competitor: declared competitor lists, demotion, and detector signals. | | **Lifecycle (`lcid`/`lcstep`)** | The metamodel's state machine for a business entity; "active contract" is a lifecycle step, not a flag. | | **Run / run step** | The recorded execution of a pipeline and its per-step progress — the platform's ground truth for "what happened". | | **Proof-of-publication** | A publication is only counted after the live URL has been fetched and verified reachable. | | **Agent token** | A scoped JWT for LLM agents: read access to the platform with secrets redacted, write access only to an explicitly granted pool of contracts. | | **SPEC / `spec_ref`** | An opaque identifier of an internal specification record (`SPEC-…`) that a task or ticket may point at; the public contract shows the shape only (`SPEC-NNN`) — the records themselves are not published. | | **Grant pool** | The per-agent set of contracts (with capabilities) that its token may write to. Re-issuing a token replaces the pool. | | **Topic yield** | The topic feedback loop's headline metric: impressions earned per article of a topic, over a maturity-filtered window. | | **Explore floor** | The protected minimum share of nightly selection reserved for new and under-sampled topics — exploitation can never consume the whole batch. | | **Health score** | The daily composite per-site SEO score with boolean flags (canonicalization, crawler silence, 404 spikes…), computed by one shared function. | | **Fail-loud** | The platform's error philosophy: missing required input or a broken invariant stops the run and is recorded, instead of a silent default. | | **Projector** | The single component that reads the run event stream and writes run/step state to the database — the platform's only DB writer for run state, so ordering and idempotency live in one place. | | **Reaper** | The maintenance loop that kills stuck database transactions and stale runs so a hung step cannot hold locks or slots forever. | | **Watchdog / dead-man's switch** | External liveness checks that expect a heartbeat; silence itself is the alarm ("zero is a failure"). | | **Hub** | The publishing service that builds and deploys the static destination sites and verifies live URLs. | | **Native worker** | The in-process pipeline executor (as opposed to the earlier subprocess runner) that runs pipeline steps under the run engine's concurrency control. | | **Seam** | A point where one shared mechanism is invoked from two paths (e.g. the topic-dedup comparator on both the Collect and the publish side) — one core, two seams. | | **Charter** | The per-step contract of the writing framework: one concern, one owner, declared inputs and outputs. | | **Contour** | An environment: *dev* (local integration stack) or *prod* (the production host). | | **Donor** | An existing site or version whose configuration/content seeds a new one (a new instance is cloned from a donor). | | **Site chrome** | Everything on a published page that is not the article: navigation, footer, author box, legal pages. | | **Stamp** | A deterministic signal or score written onto an article record as telemetry (not a gate verdict). | | **Signal battery** | A set of deterministic detectors run together on an article (brand-safety, language consistency, structural checks). | | **BOLA** | Broken Object Level Authorization — the class of bug where a caller reaches an object it does not own; the agent grant pool is the layer that prevents it. | | **AIMD** | Additive-increase / multiplicative-decrease — the adaptive concurrency rule the run engine uses against provider rate limits. | | **E-E-A-T** | Experience, Expertise, Authoritativeness, Trust — the search-quality frame the authorship model is built around. | | **IndexNow** | A push protocol for telling search engines about new or changed URLs (implemented; not yet enabled). | ## One thing, several names The REST API, the MCP tools and the database grew at different times and kept their own words. The mapping: | Concept | REST API | MCP tools | Database / other | | --- | --- | --- | --- | | Contract | `contracts`, `contract_guid` | `contract`, `sc_id` in payloads | `salescontracts` table, `SalesContract` entity | | Ticket | `tickets` (`kind`: `defect` or `improvement`) | `entherium_report_defect`, `entherium_propose_improvement`, `entherium_list_defects` | `ticket` | | Task | `tasks` | `entherium_create_task`, `entherium_list_tasks`, … | `task` | | Identifier | `guid` | `task_id`, `defect_id` | primary key `guid` | | Assignee | `assignee_id` (integer user id) | `assignee` (a person's display name, resolved server-side) | `users.guid` | --- --- title: Knowledge Graph description: The team's internal knowledge vault — 1400+ notes behind this portal — assembling themselves into one system, driven by the same few forces that shape the real graph. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/platform/knowledge-graph/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. <p class="kg-lead">Every dot is a note, every thread is a link — the platform's knowledge assembling itself out of 1400+ notes.</p> <p class="kg-lead">What you see is the team's <strong>internal knowledge vault</strong> (design notes, runbooks, audits, hand-offs) that this portal is distilled from — not the portal's own pages. The vault itself is not published; the portal's pages are the curated, public subset. The canvas is a live animation and needs a browser with JavaScript and canvas enabled.</p> <div class="kg-top"> <span>~40s growth · <strong>Replay</strong> restarts · scroll to zoom · drag to pan · hover a node</span> <a href="/developers/graph/">Open full screen →</a> </div> <div class="kg-stage"> <iframe class="kg-frame" src="/developers/graph/" title="Living knowledge graph"></iframe> </div> --- --- title: What is Enterium description: Enterium is a content-operations platform that plans, generates, verifies, publishes and measures expert content across a fleet of owned sites — end to end, mostly autonomously. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/platform/what-is-enterium/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. Enterium is a **content-operations platform**. It runs the complete lifecycle of expert web content for a fleet of owned sites: discovering source material, generating articles grounded in a per-niche research corpus, verifying them with deterministic quality gates, publishing them to live sites, and measuring the search-traffic outcome — feeding what it learns back into the next cycle. **One product, two spellings.** *Enterium* is the product name used throughout this documentation. *Entherium* is the same project's technical identifier — you will meet it in the API host name, in the `service` field of `/healthz`, in the OpenAPI title and in the `entherium_*` prefix of the MCP tools. Not two products and not a typo: the identifiers predate the name and are kept stable so that clients never break. The platform is operated by a small human team together with **LLM agents** as first-class users: agents read platform state, file defects, manage tasks and launch runs through the same API and an MCP server. This documentation is written for them as much as for people. ## The operating model Work is organized around a **contract**: one niche, one brand voice, one destination site. For each contract the platform maintains configuration (topic ontology, source feeds, brand description) and an accumulating research corpus. A nightly scheduled run drives every active contract through the full cycle without human intervention; humans supervise through dashboards, reports and the task board. ## The platform in one pass 1. **Collect** — harvest candidate sources for the contract's niche and rank them by semantic (vector) relevance to its ontology; extract research facts into the corpus. 2. **Generate** — produce an article from a qualified source through a staged, versioned LLM pipeline, grounded in the research corpus (RAG). 3. **Verify** — deterministic gates on the publish path: topic deduplication, claim verification against sources (anti-fabrication), relevance filtering and brand-safety scoring. 4. **Publish** — render the final layout and ship it to the destination site; a URL only counts as published once it is verified live. 5. **Measure** — search-console metrics, cost accounting and error tracking close the loop; topic-level feedback steers what Collect looks for next. The full walkthrough with the exact gates is in [Content lifecycle](../content-lifecycle/). ## What makes it interesting to build on - **Modular monolith with enforced boundaries** — one backend split into bounded contexts whose isolation is checked in CI, not by convention. See [Architecture](../architecture/). - **Immutable pipeline versions** — generation code is versioned like data: any past run is reproducible, and rollback is a pointer move. See [Pipelines](../../pipelines/catalog/). - **Agent-native surface** — a documented MCP server, scoped agent tokens and machine-readable state everywhere. See [MCP](../../mcp/connecting/). - **Fail-loud philosophy** — steps do not fall back to defaults silently; a broken invariant stops the run and lands in the error surface where it is visible. See [How to read errors](../../guides/reading-errors/). ## Who runs it, and how to get access Enterium is an independent platform built and operated by a small team — one engineer together with LLM agents as first-class users. It is neither an open-source project nor a hosted product with self-service sign-up. **Access is by invitation**: user accounts and agent tokens are issued by the platform owner, and every data endpoint answers `401` without one — by design, not by accident. What you can use without a token is everything an outsider needs to evaluate the platform: this portal (with its `.md` twins and `llms.txt`), the API contract (`/openapi.json`, the `/reference` and `/redoc` explorers), the MCP onboarding page (`/mcp-info`) and the health probes (`/healthz`). The authenticated surface — data endpoints, the MCP tools, the task board — is for the team and its agents; access to it is arranged outside this portal, not requested through it. Everything an outside reader needs to understand and evaluate the platform is in the open surface above. ## What this documentation does and does not disclose The portal is public and written to be read by strangers, so its boundaries are explicit: - **Named:** the open-source stack — FastAPI on Python 3.12 with async SQLAlchemy, PostgreSQL with `pgvector`, Redis Streams, a React/TypeScript web UI, Hugo for the published sites, Astro for this portal. - **Deliberately not named:** the commercial LLM and embedding vendors and the exact models behind the writer, judges and screeners (they change as pricing and quality shift and are an operational detail, not an architectural one), absolute costs and budgets, and the identities and traffic of the sites in the fleet. - **Not published at all:** the internal design specifications behind these mechanisms, the task board and the team's working notes. Where a tool or field takes a *feature id* (the `SPEC-NNN` shape), that is an opaque identifier for the team's own workflow, not a link to a document. Where a number appears in these pages it is a measurement from a real run, rounded — not a target and not a projection. ## How to read this portal The sidebar is the map. Suggested order: this page → [Architecture](../architecture/) → [Content lifecycle](../content-lifecycle/); acting as an agent, start at [Agent quickstart](../../guides/quickstart/) and [Connecting to MCP](../../mcp/connecting/). Deep dives live under **How it works**; operational facts (limits, errors, status, changelogs) under **Reference**. Consuming this portal as an LLM? [For LLM agents](../../for-llm-agents/) is your contract — every page has a Markdown twin at `<route>.md`. --- --- title: Changelog surfaces description: Where change is recorded — the agent capability changelog, module release notes, pipeline versions, and this portal's own per-page content hashes — and how to consume each. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/reference/changelog/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. The platform records change on four independent surfaces. Which one you need depends on what you consume. ## 1. Agent capability changelog (the one agents must watch) The agent-facing read surface (API + MCP) carries its own version — exposed as `serverInfo.version` on MCP connect and echoed by the `start_here` tool. When it differs from the version you cached, the surface changed. - **How to consume:** pass your cached value as `last_seen_version` to `start_here`; the response's `whats_new` lists exactly the entries you have not adopted. Full history: MCP resource `changelog://entherium`. - **Contract:** entries are structured (`added` / `changed` / `deprecated`), newest first, monotonic date-anchored versions. ## 2. Module release notes Platform modules (backend, web UI, contracts, services, this portal) version independently by SemVer, derived automatically from commit history. Each module carries its own `CHANGELOG.md` in the repository; the umbrella platform version tracks the whole. ## 3. Pipeline versions Collect / Generate / Localize version separately in the pipeline catalog — an intentionally different axis from module releases (see [Version lifecycle](../../pipelines/versioning/)). A new pipeline version's rationale is recorded in the catalog entry itself. ## 4. This portal's own content hashes Every documentation build emits `manifest.json` — a route → content-hash map covering every page. An agent that cached the docs compares its stored map against the current one and re-reads only the routes whose hash moved; nothing else has to be fetched. See [For LLM agents](../../for-llm-agents/). ## The rule that keeps changelogs honest Entries are written for **consumers** ("you can now read the board", "this field's meaning changed"), never as commit digests. If a change does not alter what a consumer can do or observe, it does not belong on surfaces 1 or 4. --- --- title: Errors reference description: The error envelope every 4xx/5xx returns (detail, code, request_id, doc_url) and, per HTTP status, what happened and what to do — plus the rule for telling platform trouble from your own request. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/reference/errors/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. One page for "I got an error — what now". Every error answer has the same shape, and every status below has a stable anchor (`#http-<code>`): the `doc_url` inside the body points here. For investigating *platform* failures (a run that broke), see [How to read errors](../../guides/reading-errors/). ## The error envelope ```json { "detail": "run 1842 is already terminal (succeeded)", "request_id": "6f1c2b8e0d4a4e1b9c3f7a2d5e8b1c04", "doc_url": "https://enterium-developer-docs.pages.dev/developers/reference/errors/#http-409" } ``` | Field | Meaning | | --- | --- | | `detail` | Human-readable cause. On `422` it is a list of `{loc, msg, type}` naming the offending field | | `code` | Machine-readable code — present only when the endpoint defines one | | `request_id` | Correlation id of the request; identical to the `x-trace-id` response header. Quote it when reporting a problem | | `doc_url` | This page, anchored at the status code | MCP tool results are not HTTP errors: a tool that could not do its job answers a truthful payload — `{"accepted": false, "error": "…"}` for write tools, `{"error": "…"}` for reads — with the reason in `error`. The transport itself answers `401` like the REST API. ## By status code <a id="http-400"></a> ### 400 — Bad request | What happened | What to do | | --- | --- | | Token mint with an empty `grants` | A token must carry a non-empty grant pool — owner action | | A request the endpoint rejects before validation (malformed action, unknown resource kind) | Read `detail`; fix the request. Do not retry as-is | <a id="http-401"></a> ### 401 — Unauthorized | `detail` says | Cause | What to do | | --- | --- | --- | | `missing agent bearer token` / `Not authenticated` | No `Authorization: Bearer <token>` header | Add the header to **every** request, including MCP | | signature / expired / wrong token type | The token is not valid for this surface | Ask the owner for a fresh token | | `token revoked` | A newer token was minted for your identity | Use the newest token — there can be only one | | `user not found or inactive` | The agent identity was deactivated | Escalate to the owner | <a id="http-403"></a> ### 403 — Forbidden | `detail` says | Cause | What to do | | --- | --- | --- | | `superuser required` | You called an owner-only route (token minting, admin) | Don't — ask the owner | | names a contract or capability | Target outside your grant pool, or a missing capability | Call `my_capabilities`; request the grant | | on board REST reads (`GET /api/v1/tasks` …) | Team-only by design | Use the MCP read tools instead | <a id="http-404"></a> ### 404 — Not found | What happened | What to do | | --- | --- | | Unknown route | Check the path against the [REST overview](../../api/) — every path is relative to the base URL and starts with `/api/v1/` | | A known route, unknown id | The object does not exist or is outside your visibility; do not retry | <a id="http-405"></a> ### 405 — Method not allowed | What happened | What to do | | --- | --- | | Right path, wrong verb (e.g. `GET` on a `POST`-only intake) | Use the method the contract declares | <a id="http-406"></a> ### 406 — Not acceptable | What happened | What to do | | --- | --- | | `GET /mcp/` with a token: the MCP transport speaks JSON-RPC, not HTML | Run the [handshake](../../mcp/connecting/); browsers get `401` first | <a id="http-409"></a> ### 409 — Conflict | `detail` says | Cause | What to do | | --- | --- | --- | | `illegal transition` | `resolve` / `dismiss` on an already-closed ticket, or an impossible run/task transition | Read the current state first; use `reopen` if you need it open | | `Idempotency-Key conflict` | The same key was reused with a different body | Use a fresh key for a new request; see [Idempotency](../idempotency/) | | already terminal / already running | The object is past the state your call assumes | Re-read the object; the operation is already done or no longer possible | <a id="http-422"></a> ### 422 — Unprocessable entity | What happened | What to do | | --- | --- | | A parameter is out of range, a field has the wrong type, an action/resolution pairing is invalid | `detail[].loc` names the field, `detail[].msg` the rule. Fix the request — never retry as-is | <a id="http-429"></a> ### 429 — Too many requests | What happened | What to do | | --- | --- | | Per-reporter intake cap on tickets / tasks, or a read-surface rate limit ([limits](../limits/)) | Wait for `Retry-After` seconds, then retry the *same* request. Batch your reports | <a id="http-500"></a> ### 500 — Internal error | What happened | What to do | | --- | --- | | The platform hit an unhandled error; it is recorded on the platform side with your `request_id` | Retry once after a short pause. If it repeats, file a defect quoting `request_id` | <a id="http-502"></a><a id="http-503"></a><a id="http-504"></a> ### 502 / 503 / 504 — The platform is not serving | What happened | What to do | | --- | --- | | Connection refused / `502` on everything, including `/healthz` | The instance is down **or intentionally paused** (maintenance windows exist). Check [Platform status](../status/); do not file a defect for a paused instance | | `503` from `/readyz` or `/api/status.json` | The process is up but a dependency (database, cache, catalog) is not | Wait and retry; the JSON names the failing check | ## Transport quirks | Code | Surface | Cause | Fix | | --- | --- | --- | --- | | `307` | `/mcp` (no slash) | Redirect to the transport path | Use `/mcp/` directly — some clients do not follow redirects on POST | | Unparseable body | MCP responses | You are reading raw SSE | Parse `data: ` lines only | ## Platform-side vs your-side: the triage rule Before concluding "the platform is broken": 1. `2xx`/`4xx` with a detail → **your side**. The detail names the fix. 2. `502`/timeout on everything, including `/healthz` → **platform side** (down or intentionally paused). See [Status](../status/). Do not file a defect for a paused instance. 3. Empty results with `200` → usually **normal idle**, not loss. See [Interpreting platform status](../../mcp/interpreting-status/). 4. A genuine defect (reproducible wrong behaviour with evidence) → file it with repro steps ([how](../../guides/tasks-and-defects/#filing-a-defect-that-actually-lands)) — reports without evidence are quarantined. ## Error responses never contain secrets Details name causes and objects, never credentials. If a response ever appears to contain a secret, that itself is a defect worth filing. --- --- title: Idempotency and retries description: What is actually idempotent on this platform — the Idempotency-Key header on the two intake endpoints, its 24-hour window and conflict rule, the de-duplicated report intake, which MCP tools are safe to repeat — and how to retry each error class. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/reference/idempotency/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. Retry safety is narrower than most clients assume. This page lists exactly what the platform guarantees today; anything not listed is **not** idempotent. ## The `Idempotency-Key` header | Fact | Value | | --- | --- | | Where it is honoured | Every write under `/api/v1/` (`POST`/`PATCH`/`PUT`/`DELETE`) — a replay cache at the HTTP layer; `POST /api/v1/tasks` and `POST /api/v1/tickets` also store the key on the created record | | Format | Any UUID, one per logical request | | Window | 24 hours: a retry with the same key returns the **original** response instead of creating a second object | | Same key, different body | `422` with `code: "idempotency_conflict"` and `detail: "Idempotency-Key conflict: same key used with different request body"` — the key is bound to the first body | | What is replayed | `2xx` and `4xx` answers; a `5xx` is never cached, so a failed call can be retried with the same key. The replayed answer carries `idempotent-replayed: true` | | In the contract | Declared as an optional header parameter on every write operation in `openapi.json` | ```sh curl -sS -X POST https://entherium.duckdns.org:8443/api/v1/tasks \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 3f1a6c62-0b5e-4d0f-9d1e-1f6a3b2c4d5e" \ -d '{"title": "Re-check the source feeds of contract 42"}' ``` ## Naturally idempotent operations | Operation | Why repeating is safe | | --- | --- | | Every `GET` | Reads have no side effects | | Defect / suggestion reports (`POST /api/v1/tickets`, MCP `entherium_report_defect`, `entherium_propose_improvement`) | Intake de-duplicates by content fingerprint: a repeated report is folded into the existing ticket, not filed twice — on top of the header | | MCP `entherium_target_task_spec`, `entherium_reassign_task` | Setting the same target twice leaves the same state (`idempotentHint: true`) | | `PATCH` that sets a field to the value it already has | A no-op by construction; status *transitions* are the exception below | ## Not idempotent — read the state first | Operation | What repeating does | | --- | --- | | `entherium_update_task_status` / ticket `resolve` · `dismiss` · `reopen` | The second call hits a state that no longer allows the transition → `409 illegal transition`. Re-read before deciding | | `entherium_log_task_note` | Appends a second note | | Run launches (`POST /api/v1/runs/…`) | Starts another run; check the run list before retrying a launch whose response you lost | | Agent writes to entities | Applied each time; a lost response does not mean a lost write — read back before repeating | ## How to retry, by error class | You got | Retry? | How | | --- | --- | --- | | Network timeout / connection reset | Yes, for reads and for the two intake endpoints **with the same `Idempotency-Key`**; for other writes read back first | Exponential backoff from 1 s, at most 5 attempts | | `429` | Yes | Wait the `Retry-After` seconds, then repeat the same request | | `500` | Once | Short pause, then file a defect quoting `request_id` if it repeats | | `502` / `503` / `504` | Yes, slowly | The platform is down, paused or not ready — poll [status](../status/) rather than hammering | | Any other `4xx` | No | The request is wrong; `detail` and `doc_url` say what to fix ([Errors](../errors/)) | ## MCP: what the hints promise Every tool declares `idempotentHint` and `readOnlyHint` ([tools reference](../../mcp/tools/)). `readOnlyHint: true` tools can be repeated freely. A write tool with `idempotentHint: true` may be repeated with the same arguments; one with `idempotentHint: false` must not be repeated without reading the object first. The hints are derived from the same code that runs the tools, so they cannot be out of date relative to the server you are connected to. --- --- title: Limits and quotas description: The bounds an agent operates inside — token lifetimes, grant scopes, request limits, publish timeouts and the nightly cap — so retries and surprises don't happen. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/reference/limits/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. Knowing the bounds prevents two failure classes: pointless retries against hard limits, and "outage" reports about behaviour that is a quota working as designed. ## Tokens and grants | Limit | Value | Notes | | --- | --- | --- | | Agent token lifetime | 1–365 days (default 90) | Set at mint; the token cannot extend itself | | Tokens per agent identity | Effectively one | Minting with `revoke_prior` (default) kills all prior tokens immediately | | Write scope | Granted contract pool only | Default-deny; `publish` capability withheld unless explicitly granted | | Task/defect tools | Global `manage_task` grant | Separate axis from contract grants | ## Request limits (read surfaces) These are **parameter ranges**, not rate limits. Health probes, the documentation surfaces and the public status are not rate-limited; the one rate limit on the platform today is the per-reporter cap on report intake described above. | Surface | Limit | | --- | --- | | `GET /api/v1/errors` | `limit` 1–500 (default 100), newest first | | Chat archive search | `limit` 1–500, `since_days` 1–365 | | Board list tools (MCP) | `limit` default 50 | Requests beyond a range are rejected with a validation error — lower the parameter instead of retrying. ## Pagination List endpoints page with `limit` and `offset` (most) or `page` and `page_size` (a few); the exact parameters, defaults and maxima are on each operation's page in the [REST reference](../../api/). Results are newest first unless the operation says otherwise; there are no cursors. ## Publish and pipeline execution - **Synchronous publish calls have a hard timeout** (on the order of minutes, not seconds — site builds are slow). Treat a timeout as "unknown outcome": check the published-URL registry before retrying, or you may double-publish. - **The nightly run works under a daily cap** per contract: a `remaining` backlog on a contract is the queue the next run will drain, not an error state. - **Deduplication defers**: a topic-dedup verdict postpones an article; it is not a failure to retry around — the gate will simply defer it again. ## MCP session limits - Sessions do not survive a platform deploy; reconnect with a fresh handshake ([details](../../mcp/connecting/#if-it-fails)). - Plain `GET /mcp/` **with a valid token** returns `406` by design — the endpoint speaks JSON-RPC, not HTML. Without a token, auth answers first: `401`. ## Idempotency Every write under `/api/v1/` (`POST`/`PATCH`/`PUT`/`DELETE`) honours an `Idempotency-Key` header (UUID) at the HTTP layer: a retry with the same key and body within 24 hours returns the original response instead of acting again; the same key with a different body answers `422 idempotency_conflict`. The two intake endpoints (`POST /api/v1/tasks`, `POST /api/v1/tickets`) additionally store the key on the created record, so their de-duplication survives the cache window. The header is declared on every write operation in the OpenAPI contract; the rules live on [Idempotency and retries](../idempotency/). --- --- title: Release notes description: Dated, consumer-facing changes of the developer portal, the REST API and the agent capability surface — generated from the changelog data and the MCP surface export. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/reference/release-notes/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. Two logs, one page. **Portal and API** entries describe what changed for a reader or an API client (Keep a Changelog categories). **Agent capability** entries are the exact records the MCP server returns in `start_here.whats_new` and the `changelog://entherium` resource — the surface agents must watch. Machine-readable twin: [changelog.json](/developers/changelog.json) (JSON Feed 1.1). How each surface is versioned: [Changelog surfaces](../changelog/) · [Versions](../versions/). ## Portal and API ### 2026-09-07 — Machine map under /api/, HEAD on probes, cleaner contract and API-host index **Added** - The machine-readable map of the API is reachable at `/api/` (and `/api`) on every host; `GET /` keeps answering it where the edge routes the root to the API. - Public probes and documentation URLs answer `HEAD` (`/healthz`, `/status.json`, `/api/status.json`, `/llms.txt`, `/mcp-info`, `/reference`, `/redoc`). - `robots.txt` on the API host and honest `404` for stray root-level documents; `/developers/` on the API host answers a real `301` like the mirror. **Changed** - The API host's own `/llms.txt` now describes the current surface (three MCP mounts, the error envelope, the read-only mount) and points at the portal index; the docs MCP reports the platform build as its version. - Contract descriptions no longer name source files, fleet sites or internal process; the owner-only Telegram Mini App endpoints left the public contract. - Docs MCP `search` ranks guides above generated reference pages and ignores stop-words, so natural questions find the right page first. - The MCP onboarding page fits a phone screen; release notes carry consumer-facing titles. - The machine map's links are public ones (`/mcp-info`, the portal); the contract lists only the production server and only tags that carry operations. - Docs MCP: `fetch` and `search` report a miss as an MCP error (`isError`), and the search stems inflected words (`authenticate` finds *Authentication*). - Markdown twins of per-operation pages resolve on case-sensitive hosts (slugs are lower-case); the landing tagline states what the MCP covers. ### 2026-09-03 — Per-operation reference, typed MCP answers, read-only and docs MCP, status and release notes **Added** - One page per REST operation (request curl → response → returns → parameters with defaults and constraints), generated from the frozen contract; the tag pages link to them. - Field-level `Returns` tables on every MCP tool page, from the tools' declared `outputSchema`; the tools index shows the three mount points. - Read-only MCP mount `/mcp/readonly/` (same token, read tools only) and the token-less docs MCP `/mcp/docs/` (`search` / `fetch` / `submit_feedback`), with one-step connect commands for Claude Code, Cursor and VS Code. - Error envelope: every 4xx/5xx body now carries `request_id` and `doc_url`; the Errors reference is anchored per status code and lists what to do. - Idempotency, Versions and Release notes pages; `changelog.json` (JSON Feed 1.1) as the machine-readable changelog. - Public `/api/status.json` in the Statuspage v2 shape and a live status block on the Platform status page. - Skills catalog `/.well-known/agent-skills/index.json` with an installable `SKILL.md` on integrating over REST and MCP; `search-index.json` for tool-based search. **Changed** - The landing page opens with a hero and three entry chips (Quickstart · Connecting to MCP · API reference). ### 2026-09-02 — Contract examples, one error schema, discovery layer and page badges **Added** - Request and response examples on the key contract schemas; a single `ErrorResponse` shape and declared 409/429 codes where they are raised. - MCP tool annotations (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`) and parameter descriptions on every tool. - llms.txt v2 link relations in every page head and in the mirror's `Link` headers; `/.well-known/llms.txt` and the RFC 9727 `/.well-known/api-catalog`. - `agents.md`; status / access / contract-version badges and View-as-Markdown / Open-in-assistant actions on every page; Russian twins for the generated reference pages. **Changed** - Public surface hygiene: internal identifiers, vendor names and fictional example instances removed from the contract, the docs and `llms.txt`; the pipeline catalog no longer lists inference vendors. - Security headers on the documentation mirror. **Fixed** - Token-mint path and the `openapi.json` link resolve correctly from every locale; favicon on all pages. ### 2026-08-28 — Public mirror **Added** - The portal is published on a public mirror with canonical URLs and social previews; the same build is served from the API host under `/developers/`. ### 2026-07-17 — Developer portal launched **Added** - Platform concepts, guides, how-it-works deep dives, REST API reference generated from the contract, MCP tools reference generated from the server, `llms.txt` / `llms-full.txt`, Markdown twins of every page, `manifest.json` change detection. ## Agent capability changelog Current capability version: `2026-09-03.1`. Pass your cached value to `start_here` as `last_seen_version` to receive only the entries you have not adopted. ### 2026-09-03.1 **Added** - Read-only MCP mount at /mcp/readonly/ — same bearer token, but only the tools with readOnlyHint=true are listed and callable (start_here, list_domains, my_capabilities, entherium_list_tasks, entherium_get_task, entherium_list_defects, entherium_search_chat). Use it as the default for evaluation and for clients that must never write; the full server stays at /mcp/. - Every MCP tool now declares an outputSchema: results arrive as structuredContent with typed fields (e.g. list_tasks → {total, tasks[{task_id, title, status, assignee, spec_ref, description, updated_at, note_count, last_note_at}]}); the write tools answer a typed ack {accepted, error?, task_id?}. Field-level tables are on each tool's page in the developer docs. **Changed** - Tools carry tags (read / write) alongside the annotations; a client can group them without inspecting hints. ### 2026-07-24.2 **Changed** - `entherium_search_chat` now requires the `read_all` GLOBAL capability (agent_global_grant) — the work-chat archive is internal cross-contract team traffic. Team agents keep access via a granted `read_all`; client-scoped tokens get a truthful 'not authorized' payload. - Platform-wide read-all is PER-TOKEN now: the AGENT_READ_ALL env flag remains a global kill-switch, but each agent token also needs the `read_all` capability in its agent_global_grant row. Tokens without it read only their contract pool (client-scoped read-only tokens,. ### 2026-07-24.1 **Added** - The docs portal's How-it-works series grew from 5 to 19 deep-dive pages (engineering showcase): source discovery, ontologies & filtering, the writing framework, copywriting evaluation, scoring systems, media generation, publishing & the site fleet, SEO methodology & authorship, SEO analytics, the topic feedback loop, nightly orchestration, the run engine, observability & alerting, localization — in English and Russian, with structural/timing diagrams. The `docs://portal` resource now maps every deep dive individually (keys `hiw_*`): when a user asks HOW a mechanism works, hand them that page's html link and fetch the md twin yourself. ### 2026-07-23.1 **Added** - Developer documentation portal: https://enterium-developer-docs.pages.dev/developers/ — platform concepts, guides, REST API and MCP reference, for humans AND agents. Machine entry points: https://enterium-developer-docs.pages.dev/developers/llms.txt (curated map), https://enterium-developer-docs.pages.dev/developers/llms-full.txt (all docs as one Markdown), and a Markdown twin of EVERY page at <route>.md (fetch twins, not HTML). New MCP resource `docs://portal` returns the section map. When a USER asks how the platform works or where to see something, give them the portal URL for that topic — they can open it in a browser and see it with their own eyes. ### 2026-07-14.2 **Added** - You can now READ the task/defect board — you are no longer writing blind. `entherium_list_tasks(status?, assignee?)` lists tasks with their STATUS; `entherium_get_task(task_id)` returns one task IN FULL incl. its update log (who / what / when — every status change, reassign, spec-retarget and note, in time order); `entherium_list_defects(status?, severity?)` lists defects + incidents. Needs the `manage_task` capability. CALL `entherium_list_tasks` / `entherium_list_defects` BEFORE creating a task or filing a defect, to check it does not already exist (no duplicates). **Changed** - The web board GET stays human-only; the agent's read channel is these three MCP tools. ### 2026-07-14.1 **Added** - The `chat` domain — the team's WORK-CHAT is now archived in the platform and searchable: `entherium_search_chat(query, chat='work', author?, since_days?)` returns real replies (author, verbatim text, date). USE IT to gather CONTEXT before filing a task/defect from a chat request, and to clarify an existing one — who asked, in WHICH WORDS. A message with `is_full: false` is TRUNCATED and must NOT be quoted verbatim. Secrets are redacted. ### 2026-07-13.2 **Added** - Traceability: tasks AND defects can now TARGET a platform feature by its SPEC id (`spec_ref` = "SPEC-NNN"). Pass `spec_ref` to `entherium_create_task` / `entherium_report_defect`, or retarget a task via the new `entherium_target_task_spec` tool. An unknown spec id is REJECTED (never silently unset). Tag work to the right spec so the platform context stays coherent. ### 2026-07-13.1 **Added** - The `tasks` domain — create / assign / close TASKS for team members via the MCP tools `entherium_create_task` (returns the task_id), `entherium_update_task_status`, `entherium_reassign_task`, `entherium_log_task_note`. Requires the `manage_task` capability; give `assignee` as a person's NAME. The read-only board is human-only. ### 2026-07-09.1 **Added** - The `tickets` domain — file a defect via the MCP tool `entherium_report_defect`, or an improvement via `entherium_propose_improvement` (or POST /api/v1/tickets). Give FULL QA detail: steps_to_reproduce/expected/actual/evidence for a defect; problem/proposed_change/benefit for a suggestion. Low-detail reports are quarantined, not tracked. Write-only: a bare 202, nothing comes back. ### 2026-07-08.1 **Added** - GET /api/v1/errors — the durable error sink is now readable with a read-only token: source / operation / exc_type of recent failures. Read this for WHY a run failed instead of inferring failure rates from run counts. - The `errors` domain now appears in list_domains / llms.txt / GET / (discovery map). --- --- title: Platform status and health description: How to check whether the platform is up, what each health surface means, and how to distinguish down, idle and intentionally paused. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/reference/status/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. ## The health endpoints ```sh curl -fsS https://entherium.duckdns.org:8443/healthz ``` Returns `{"status": "ok", "service": …, "version": …}` — the `version` field doubles as "which build is live". `/health` is an alias. | Signal | Meaning | | --- | --- | | `200` on `/healthz` | Platform up. Everything else on this page is about interpretation, not availability | | `502` / connection refused | The platform instance is not serving — down **or intentionally paused** (maintenance windows exist; a paused instance is not an incident) | | `200` but idle metrics | Normal between nightly runs — see below | ## Live status <div id="live-status-panel" data-src="https://entherium.duckdns.org:8443/api/status.json">Loading live status from <code>/api/status.json</code>…</div> <script> (function () { var el = document.getElementById('live-status-panel'); if (!el || !window.fetch) return; fetch(el.getAttribute('data-src'), { cache: 'no-store' }) .then(function (r) { return r.json(); }) .then(function (s) { var rows = (s.components || []).map(function (c) { return '<li><b>' + c.name + '</b>: ' + String(c.status).replace(/_/g, ' ') + (c.description ? ' <span class="muted">— ' + c.description + '</span>' : '') + '</li>'; }).join(''); el.innerHTML = '<p><b>' + s.status.description + '</b> · checked ' + (s.checked_at || s.page.updated_at) + ' · build ' + (s.version || '') + '</p><ul>' + rows + '</ul>'; }) .catch(function () { el.textContent = 'Live status could not be loaded from this page (the API host did not answer, or the browser blocked the request). Open the JSON directly: https://entherium.duckdns.org:8443/api/status.json'; }); })(); </script> `GET /api/status.json` (no token) renders the same data in the **Statuspage v2 `summary.json` shape** — `status.indicator` (`none` / `minor` / `major` / `critical`), `components[]` for the three public surfaces (API, MCP, Developer docs) and `page.updated_at` — so any status widget or agent that already reads that format works unchanged. It answers `503` when the database is down and sends `Access-Control-Allow-Origin: *`. ```sh curl -sS https://entherium.duckdns.org:8443/api/status.json | jq '.status, [.components[] | {name, status}]' ``` ## Freshness of this documentation Every page shows the API contract version and the agent capability version it was built against (the badges under the title). The build itself is described by [`versions.json`](/developers/versions.json) (`version`, `builtAt`) and [`manifest.json`](/developers/manifest.json) (a content hash per page); the `Developer docs` component above carries the portal version the API host currently sees. ## Idle is the default state Production runs on a nightly batch; between runs there is no orchestrator process by design. The definitive interpretation table lives at [Interpreting platform status](../../mcp/interpreting-status/) — read it before reporting an outage based on `orchestrator.present: false`, `velocity: 0`, or a quiet afternoon. ## Deeper surfaces (authenticated) | Surface | What it shows | | --- | --- | | `GET /api/v1/ops/overview` | The live operational snapshot: orchestrator state, per-contract backlogs, alerts | | `GET /api/v1/errors` | The durable error sink ([how to read it](../../guides/reading-errors/)) | | The `cycles` domain | The actual execution funnel of the nightly runs | | Run records (`pipeline_run` / `run_step`) | Ground truth for any specific execution | ## If you operate through MCP A platform deploy kills live MCP sessions without restoring them — an empty tools list right after a version bump is reconnect-time, not an outage ([details](../../mcp/connecting/#if-it-fails)). After reconnecting, `start_here` tells you if the capability surface changed while you were away. --- --- title: Versions description: The five version axes a client meets — platform build, agent capability version, API contract, portal build and pipeline versions — where each is exposed, how to detect a change, and the compatibility rules. locale: en version: 0.2.0 canonical: https://enterium-developer-docs.pages.dev/developers/reference/versions/ --- > Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json. Five things carry a version, on purpose independently: bumping one does not imply the others changed. This page says where to read each and what a change means for you. ## The axes | Axis | Where you read it | Scheme | Changes when | | --- | --- | --- | --- | | **Platform build** | `version` in `GET /healthz` and `/api/status.json` | SemVer, derived from the release history | Any module ships — most builds do not touch what an agent can do | | **Agent capability version** | `x-capability-version` header on every API response · `serverInfo.version` on MCP connect · `capability_version` in `start_here` | Date-anchored, monotonic (`2026-09-03.1`) — a plain string compare works | The **agent-facing read surface** changes: new/changed tools, fields, semantics. Every bump has an entry in the [capability changelog](../release-notes/#agent-capability-changelog) | | **API contract** | `info.version` in [`openapi.json`](/developers/api/openapi.json) (frozen with this portal build) | SemVer | Paths, schemas or declared responses change | | **Portal build** | [`versions.json`](/developers/versions.json) (`version`, `builtAt`) · [`manifest.json`](/developers/manifest.json) (route → content hash) · the badge under every page title | SemVer | The documentation is republished | | **Pipeline versions** | The [pipeline catalog](../../pipelines/catalog/) | Per pipeline, immutable directories | A pipeline is carved — see [Version lifecycle](../../pipelines/versioning/) | ## Detecting change - **Agents:** cache `capability_version`; on every reconnect pass it to `start_here` as `last_seen_version` — `changed: true` plus `whats_new` is your diff. Between connects, the `x-capability-version` header on any response tells you the same without a call. - **Docs consumers:** compare the stored `manifest.json` against the current one and re-read only the routes whose hash moved; `.md` twins carry an `ETag`. - **Everyone:** [`changelog.json`](/developers/changelog.json) (JSON Feed 1.1) is the machine twin of the [release notes](../release-notes/) — subscribe to it. ## Compatibility rules 1. **Additive is silent.** New endpoints, new tools and new optional fields appear without a contract major bump; ignore fields you do not know. 2. **Renames and removals are announced first.** They enter the capability changelog under `deprecated` with the replacement, live side by side for at least one capability version, then move to `removed` in the release notes. 3. **Breaking contract changes bump the contract's major** and the capability version on the same day; the frozen `openapi.json` of the previous portal build stays available in the release history. 4. **Pipeline versions never mutate.** A change is a new version directory; the catalog pin decides which one production runs.