diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 466df71..2be9c43 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0" + ".": "0.2.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0d1c98e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,50 @@ +# Changelog + +## [0.2.0](https://github.com/BlocksOrg/blue/compare/v0.1.0...v0.2.0) (2026-09-17) + + +### Features + +* add Blue Desktop teaser page ([#88](https://github.com/BlocksOrg/blue/issues/88)) ([e596c5d](https://github.com/BlocksOrg/blue/commit/e596c5d86254b43ee13f6cbaca19e43a2572a0b1)) +* **ci:** cut dev images on demand from any ref ([#113](https://github.com/BlocksOrg/blue/issues/113)) ([80a50b1](https://github.com/BlocksOrg/blue/commit/80a50b147d6473a0c5281ae2b321cc5be703dd01)) +* **ci:** manage versions with release-please ([#134](https://github.com/BlocksOrg/blue/issues/134)) ([8bece80](https://github.com/BlocksOrg/blue/commit/8bece801d104a91f4baf074a63a535084c240993)) +* **ci:** publish release candidates from any ref ([#153](https://github.com/BlocksOrg/blue/issues/153)) ([5a28e5e](https://github.com/BlocksOrg/blue/commit/5a28e5e29142269c8e9db198756d1411715ce994)) +* **control-api:** derive the gateway JWKS from the signing key ([#107](https://github.com/BlocksOrg/blue/issues/107)) ([1ea082d](https://github.com/BlocksOrg/blue/commit/1ea082d3b9c889c24d95a01584b9b49dd05e76e2)) +* **control-api:** publish recommended client version ([#130](https://github.com/BlocksOrg/blue/issues/130)) ([aa86d33](https://github.com/BlocksOrg/blue/commit/aa86d3351c0d2e3bfd564546e5e26fe26225aca2)) +* **dashboard:** add ephemeral invitation links ([#159](https://github.com/BlocksOrg/blue/issues/159)) ([d713d90](https://github.com/BlocksOrg/blue/commit/d713d904f37510fa40b0f2f053298de23fa6bc3c)) +* **deploy:** create the EKS cluster and make AWS datastores optional ([#72](https://github.com/BlocksOrg/blue/issues/72)) ([f0c3ba4](https://github.com/BlocksOrg/blue/commit/f0c3ba4b3143c8d62f752d50cb2934b531a48b36)) +* **deploy:** front Blue with an ALB on a Route 53 zone ([#95](https://github.com/BlocksOrg/blue/issues/95)) ([34abb7f](https://github.com/BlocksOrg/blue/commit/34abb7fea9fac56f8fa5e6b70529f08e0ea8924e)) +* **deploy:** let values configure Service annotations ([#86](https://github.com/BlocksOrg/blue/issues/86)) ([eba0642](https://github.com/BlocksOrg/blue/commit/eba064242d066b2523578a8c19218b80f9542450)) +* **deploy:** reserve an inference proxy hostname in the AWS module ([#116](https://github.com/BlocksOrg/blue/issues/116)) ([5fbb814](https://github.com/BlocksOrg/blue/commit/5fbb814f6ba668a4d7a1012a7319eb6c718aed37)) +* **proxy,deploy:** split client cert/key, and let the chart issue both ([#111](https://github.com/BlocksOrg/blue/issues/111)) ([c8a77bd](https://github.com/BlocksOrg/blue/commit/c8a77bda2ccdae7514dcc4ef5b0cfd91c89819a5)) +* **website:** add blog ([#87](https://github.com/BlocksOrg/blue/issues/87)) ([8820dbf](https://github.com/BlocksOrg/blue/commit/8820dbf0eedc2caef1674efc3fb6ffac5dc7b96b)) + + +### Bug Fixes + +* **cli:** launch npm-installed agents on Windows ([#96](https://github.com/BlocksOrg/blue/issues/96)) ([28901aa](https://github.com/BlocksOrg/blue/commit/28901aa0fe90e3974f0328594b8e96a28db45b54)) +* **cli:** offer installation for allowed absent agents ([#131](https://github.com/BlocksOrg/blue/issues/131)) ([b0e07d2](https://github.com/BlocksOrg/blue/commit/b0e07d2eb6ace827993fee6c5035ffa2fc58dd63)) +* **cli:** offer the version repair from the agent picker ([#99](https://github.com/BlocksOrg/blue/issues/99)) ([d445642](https://github.com/BlocksOrg/blue/commit/d445642ce16ebdf7c400240359d8303c94d81909)) +* **cli:** repair the detected harness installation ([#124](https://github.com/BlocksOrg/blue/issues/124)) ([ab61512](https://github.com/BlocksOrg/blue/commit/ab6151286955162439f16fb9b055679494acabbc)) +* **config:** fall back to a governed default model for OpenCode gateway mode ([#123](https://github.com/BlocksOrg/blue/issues/123)) ([81a47c1](https://github.com/BlocksOrg/blue/commit/81a47c11470a7aa78704e026b95371663d07fcb4)) +* **dashboard:** read sslmode with libpq semantics ([#98](https://github.com/BlocksOrg/blue/issues/98)) ([d37e02e](https://github.com/BlocksOrg/blue/commit/d37e02e2384785117e66d54e2519b008caf370e9)) +* **deploy:** bump rustls to 0.23.45 for RUSTSEC-2026-0285 ([#109](https://github.com/BlocksOrg/blue/issues/109)) ([851b68d](https://github.com/BlocksOrg/blue/commit/851b68da4dfeb5449c9998a3380d4bfdbaffe50c)) +* **deploy:** mount the gateway signing key into the worker ([#127](https://github.com/BlocksOrg/blue/issues/127)) ([1fbeb04](https://github.com/BlocksOrg/blue/commit/1fbeb045f7e3e89269844943d0d994cb0713c5fc)), closes [#126](https://github.com/BlocksOrg/blue/issues/126) +* **deploy:** restore the docs snapshot gate on release ([#80](https://github.com/BlocksOrg/blue/issues/80)) ([9f71cd8](https://github.com/BlocksOrg/blue/commit/9f71cd8d793174d9fdd624303c4e0de5c1e55cf9)), closes [#79](https://github.com/BlocksOrg/blue/issues/79) +* **docs:** automate release version references ([#163](https://github.com/BlocksOrg/blue/issues/163)) ([4262361](https://github.com/BlocksOrg/blue/commit/4262361f19b404b6ec3c359b3c10e2fccd606051)) +* **website:** remove stale blog card top spacing ([#103](https://github.com/BlocksOrg/blue/issues/103)) ([147e2da](https://github.com/BlocksOrg/blue/commit/147e2dadc8bf8beb18af2bf502aa01e1df610f4a)) + + +### Documentation + +* correct stale README facts about the Compose stack and repo layout ([#76](https://github.com/BlocksOrg/blue/issues/76)) ([1f8bd3f](https://github.com/BlocksOrg/blue/commit/1f8bd3fa1ee83e279a8aa8fd321642fb94604a3b)), closes [#75](https://github.com/BlocksOrg/blue/issues/75) +* **docs:** document managed configuration ownership and inheritance ([#157](https://github.com/BlocksOrg/blue/issues/157)) ([941c839](https://github.com/BlocksOrg/blue/commit/941c8394fa1fe7e1498ace0d50687c142ec0d4c5)), closes [#156](https://github.com/BlocksOrg/blue/issues/156) +* **docs:** quickstart as a TL;DR and Helm guide as the verified EKS walkthrough ([#125](https://github.com/BlocksOrg/blue/issues/125)) ([0478f82](https://github.com/BlocksOrg/blue/commit/0478f829a5a73dac095c7df8e9eb56fc3d110d22)) +* freeze the 0.1.0 documentation snapshot ([#82](https://github.com/BlocksOrg/blue/issues/82)) ([b460c1d](https://github.com/BlocksOrg/blue/commit/b460c1d5addc5bc47d8bde627e53c2a4811543cf)), closes [#81](https://github.com/BlocksOrg/blue/issues/81) +* **website:** remove desktop capabilities heading ([#105](https://github.com/BlocksOrg/blue/issues/105)) ([ff2f428](https://github.com/BlocksOrg/blue/commit/ff2f428a358e30b978d6b21c922f6ed4ff73d7c4)) + + +### Code Refactoring + +* **dashboard:** hardcode the documentation link ([#129](https://github.com/BlocksOrg/blue/issues/129)) ([837244e](https://github.com/BlocksOrg/blue/commit/837244e071385e293eb83703bd7ef2a89e716d28)) +* **website:** drop date and author from blog cards and post headers ([#102](https://github.com/BlocksOrg/blue/issues/102)) ([ac879ca](https://github.com/BlocksOrg/blue/commit/ac879cab8f51d1e58ebd1d265a7ad96d4908e424)) diff --git a/Cargo.lock b/Cargo.lock index 14c5213..766a86d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -950,7 +950,7 @@ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "control-api" -version = "0.1.0" +version = "0.2.0" dependencies = [ "aes-gcm", "anyhow", @@ -1523,7 +1523,7 @@ dependencies = [ [[package]] name = "gh-agent" -version = "0.1.0" +version = "0.2.0" dependencies = [ "gh-common", "gh-config", @@ -1535,7 +1535,7 @@ dependencies = [ [[package]] name = "gh-cli" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "clap", @@ -1572,7 +1572,7 @@ dependencies = [ [[package]] name = "gh-common" -version = "0.1.0" +version = "0.2.0" dependencies = [ "rand 0.8.8", "semver", @@ -1585,7 +1585,7 @@ dependencies = [ [[package]] name = "gh-config" -version = "0.1.0" +version = "0.2.0" dependencies = [ "flate2", "gh-common", @@ -1608,7 +1608,7 @@ dependencies = [ [[package]] name = "gh-gateway" -version = "0.1.0" +version = "0.2.0" dependencies = [ "gh-common", "gh-service", @@ -1619,7 +1619,7 @@ dependencies = [ [[package]] name = "gh-gateway-provisioner" -version = "0.1.0" +version = "0.2.0" dependencies = [ "async-trait", "hex", @@ -1634,7 +1634,7 @@ dependencies = [ [[package]] name = "gh-harness" -version = "0.1.0" +version = "0.2.0" dependencies = [ "gh-common", "gh-config", @@ -1649,7 +1649,7 @@ dependencies = [ [[package]] name = "gh-proxy" -version = "0.1.0" +version = "0.2.0" dependencies = [ "gh-common", "tracing", @@ -1657,7 +1657,7 @@ dependencies = [ [[package]] name = "gh-service" -version = "0.1.0" +version = "0.2.0" dependencies = [ "gh-common", "reqwest", @@ -1674,7 +1674,7 @@ dependencies = [ [[package]] name = "gh-telemetry" -version = "0.1.0" +version = "0.2.0" dependencies = [ "gh-common", "reqwest", @@ -2073,7 +2073,7 @@ dependencies = [ [[package]] name = "inference-proxy" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index b4d771d..48eeeea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ members = [ # recipe. See tests/e2e-slim/Cargo.toml for the rationale. [workspace.package] -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT" repository = "https://github.com/BlocksOrg/blue" diff --git a/apps/docs/0.2.0/admin/configuration.mdx b/apps/docs/0.2.0/admin/configuration.mdx new file mode 100644 index 0000000..4e9de26 --- /dev/null +++ b/apps/docs/0.2.0/admin/configuration.mdx @@ -0,0 +1,61 @@ +--- +title: "Manage organization policy" +description: "Edit, validate, version, and deliver governance policy from the dashboard." +icon: "file-pen" +--- + +The user menu's **Current deployment configuration** dialog presents a read-only export of the effective deployment configuration. Managed coding-agent settings are edited on **Harnesses**, while extensions and MCP servers use **Extensions**. Other organization policy is edited in the mounted `blue.yaml`. + +The mounted `blue.yaml` is also reconciled at Control API startup. A deployment change creates a revision when it changes the effective policy. Dashboard changes win when both sources changed the same field; the service logs the skipped paths without logging their values. + +## Source-of-truth boundary + +Store organization decisions in the deployment `governance` section: + +- allowed harnesses +- managed harness settings, which can also be changed from **Harnesses** +- global gateway type; client-to-proxy authentication is fixed to bearer +- session-upload opt-in + +Packages, skills, subagents, hooks, plugins, helpers, and MCP servers can be managed from **Extensions** and are included in the exported deployment YAML. + +Do not store the deployed inference proxy URL or an inference JWT. The Control API injects both when serving `GET /governance-config` to an authenticated user with an active OAuth session. + +## Publishing a change + + + Change the smallest policy block needed in the mounted `blue.yaml`. + Restart the Control API. It validates and reconciles the new baseline into an immutable revision. + Developers can run `blue apply` immediately. Daemons pick up the revision on their next TTL poll. + Use the Clients page to compare desired and applied revisions and inspect managed-file health. + + +## Harness settings + +Open **Harnesses** in the administrator sidebar. Its metadata-driven table lists the available harnesses; each row's **Edit** dialog provides an optional semantic-version range and a YAML editor scoped to that agent's `managed_config`. The server validates both before publishing them and records the minimum compatible Blue client when version pinning is enabled. + +Blue suppresses native harness updates when the policy has an explicit maximum +or relies on Blue's certified release ceiling. To leave native updates enabled, +select **Allow unverified versions** and provide an explicit range without an +upper bound. This accepts releases beyond Blue's certification and may require +a newer compatibility profile after a vendor change. + +Saving replaces only the selected agent's `managed_config` and preserves packages, MCP servers, and every other policy block. Each save uses the current base revision, so a concurrent policy change must be reloaded instead of being overwritten. + +## Extensions + +Open **Extensions** in the administrator sidebar. Package cards show immutable versions, digests, harness compatibility, and included skills, subagents, hooks, plugins, or helper executables. The MCP servers tab manages local-command and remote-URL servers and the agents that receive each definition. See [Managed packages](/0.2.0/admin/managed-packages) for the complete package lifecycle. + +Custom packages use the same normalized schema as catalog entries. The source inspector accepts `github:owner/repository@ref` for public GitHub repositories, public HTTPS archives, or an allowed deployment-managed GitHub/Bitbucket repository connection. Public GitHub refs are resolved to immutable codeload URLs and downloaded directly by clients. Managed repository refs are resolved to immutable commits and mirrored into organization-scoped storage. Every inspected source displays its exact SHA-256, and archives are capped at 100 MiB. Review the complete adapter inventory and digest before saving. + +Per-harness overrides can disable a package or provide adapter settings. They cannot change the organization-approved archive or digest. All Extension tabs share one pending-change state and publish through the same revision checks. + +## Reconcile with deployment YAML + +The user menu's **Current deployment configuration** dialog combines the mounted server and catalog sections with the complete current governance revision and provides **Copy** and **Download** actions. Use it when dashboard changes should become the new deployment baseline. Existing environment and file references are preserved. Literal database credentials, SCIM tokens, gateway secrets, and repository credentials are replaced by usable `os.environ/NAME` references and listed above the export. + +Managed-repository extensions include their connection, repository, requested ref, resolved commit, and digest in the export. On startup Blue reuses a matching artifact or mirrors the exact pinned commit again; a missing connection or digest mismatch is rejected. + +The Clients page reports each package as applied or drifted. Package failures are isolated, so unrelated packages can converge, but a governed launch does not proceed while one of its required packages failed for that harness. + +See [Harness version compatibility](/0.2.0/concepts/harness-version-compatibility) for version routing and [Governance configuration](/0.2.0/reference/governance-config) for field definitions and examples. diff --git a/apps/docs/0.2.0/admin/custom-gateway-provisioners.mdx b/apps/docs/0.2.0/admin/custom-gateway-provisioners.mdx new file mode 100644 index 0000000..a6f8964 --- /dev/null +++ b/apps/docs/0.2.0/admin/custom-gateway-provisioners.mdx @@ -0,0 +1,278 @@ +--- +title: "Custom gateway provisioners" +description: "Deploy a digest-pinned executable gateway provisioner." +icon: "gears" +--- + +Custom provisioners are deployment-trusted executables. Blue invokes the configured absolute path directly, with no arguments and the Control API environment. The file must be a regular executable, and its lowercase SHA-256 pin is verified at startup. A shebang selects the interpreter; that interpreter and every imported dependency must exist in the Control API image. + +## Configuration + +```yaml +gateway: + provisioner: + type: organization-gateway + executable_path: /var/run/blue/provisioner/provisioner + executable_sha256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + policy_revision: organization-gateway-v1 + reconcile_ttl_seconds: 86400 + timeout_seconds: 15 + max_concurrency: 8 +``` + +Helm can copy the executable from a separately pinned artifact image: + +```yaml +blue: + provisionerExecutable: + enabled: true + image: + repository: ghcr.io/acme/blue-provisioner + digest: sha256:REPLACE_WITH_IMAGE_DIGEST + pullPolicy: IfNotPresent + executableSha256: REPLACE_WITH_FILE_SHA256 +``` + +The artifact image must contain `/executable/provisioner`. The init container verifies it, installs it at `/var/run/blue/provisioner/provisioner` with mode `0555`, and mounts it read-only. + +## Implement the executable + +The executable reads one request from stdin and writes one response to stdout. Blue passes no command-line arguments. + +```python provisioner.py +#!/usr/bin/env python3 +import hashlib +import json +import os +import sys +import urllib.error +import urllib.parse +import urllib.request + +def respond(payload, exit_code=0): + json.dump(payload, sys.stdout, separators=(",", ":")) + sys.stdout.write("\n") + raise SystemExit(exit_code) + +class ProvisionerError(Exception): + def __init__(self, code, message): + self.code = code + self.message = message + +def required_env(name): + value = os.environ.get(name, "").strip() + if not value: + raise ProvisionerError("invalid_config", f"{name} is required") + return value + +BASE_URL = required_env("HARNESS_GATEWAY_URL").rstrip("/") +ADMIN_KEY = required_env("HARNESS_LITELLM_ADMIN_KEY") +MODELS = [value for value in os.environ.get("LITELLM_MODELS", "").split(",") if value] + +def litellm(method, path, body=None, query=None): + url = f"{BASE_URL}/{path.lstrip('/')}" + if query: + url += "?" + urllib.parse.urlencode(query) + encoded = None if body is None else json.dumps(body).encode() + request = urllib.request.Request(url, data=encoded, method=method, headers={ + "Authorization": f"Bearer {ADMIN_KEY}", + "Accept": "application/json", + "Content-Type": "application/json", + }) + try: + with urllib.request.urlopen(request, timeout=20) as response: + return json.load(response) + except urllib.error.HTTPError as error: + # Do not include the upstream body: it can contain credentials. + if path == "/key/info" and error.code == 404: + raise ProvisionerError("credential_invalid", "LiteLLM key was deleted") + raise ProvisionerError("rejected", f"LiteLLM returned HTTP {error.code}") + except (urllib.error.URLError, TimeoutError): + raise ProvisionerError("unavailable", "LiteLLM request failed") + except (UnicodeDecodeError, json.JSONDecodeError): + raise ProvisionerError("rejected", "LiteLLM returned invalid JSON") + +def find_user(email): + payload = litellm("GET", "/user/list", query={ + "user_email": email, + "page_size": 100, + }) + users = [user for user in payload.get("users", []) + if user.get("user_email", "").casefold() == email.casefold()] + if not users: + raise ProvisionerError("account_missing", email) + if len(users) != 1: + raise ProvisionerError("conflict", f"multiple LiteLLM users match {email}") + user = users[0] + if not user.get("user_id"): + raise ProvisionerError("rejected", "LiteLLM user has no user_id") + teams = [team.get("team_id", team.get("id")) + for team in user.get("teams", [])] + return user["user_id"], [team for team in teams if team] + +def ensure(request): + identity = request["identity"] + user_id, teams = find_user(identity["email"]) + alias = f"blue:{identity['email'].strip().lower()}" + previous = request.get("previous") + metadata = { + "managed_by": "blue", + "owner_email": identity["email"].strip().lower(), + "provisioner": "organization-litellm", + } + key_policy = { + "user_id": user_id, + "key_alias": alias, + "models": MODELS, + "metadata": metadata, + } + + # Reconcile an existing virtual key without returning plaintext again. + if previous: + current = litellm("GET", "/key/info", query={"key": previous["external_id"]}) + if current.get("info", {}).get("blocked") is True: + raise ProvisionerError("credential_invalid", "LiteLLM key was blocked") + litellm("POST", "/key/update", { + **key_policy, + "key": previous["external_id"], + }) + return { + "credential": None, + "external_id": previous["external_id"], + "alias": alias, + "metadata": {"team_ids": teams, "models": MODELS}, + "expires_at": None, + } + + generated = litellm("POST", "/key/generate", { + **key_policy, + "key_type": "llm_api", + }) + credential = generated.get("key") + if not credential: + raise ProvisionerError("rejected", "LiteLLM generated no key") + # LiteLLM commonly returns the stored token/hash separately. Fall back to + # a SHA-256 identifier so plaintext is never persisted as external_id. + external_id = generated.get("token") or hashlib.sha256( + credential.encode() + ).hexdigest() + return { + "credential": credential, + "external_id": external_id, + "alias": alias, + "metadata": {"team_ids": teams, "models": MODELS}, + "expires_at": generated.get("expires"), + } + +def revoke(request): + litellm("POST", "/key/delete", {"keys": [request["external_id"]]}) + return {"revoked": True} + +try: + envelope = json.load(sys.stdin) + if envelope.get("protocol_version") != 1: + raise ProvisionerError("invalid_config", "unsupported protocol version") + operation = envelope.get("operation") + if operation == "ensure": + result = ensure(envelope["request"]) + elif operation == "revoke": + result = revoke(envelope["request"]) + else: + raise ProvisionerError("invalid_config", "unsupported operation") + respond({"protocol_version": 1, "status": "success", "result": result}) +except ProvisionerError as error: + respond({"protocol_version": 1, "status": "error", "error": { + "code": error.code, "message": error.message + }}, 1) +except (KeyError, TypeError, json.JSONDecodeError): + respond({"protocol_version": 1, "status": "error", "error": { + "code": "invalid_config", "message": "invalid provisioner request" + }}, 1) +``` + +This policy requires an existing LiteLLM user with the same email as the Blue identity. Set `LITELLM_MODELS` to a comma-separated model allowlist, or leave it empty to let LiteLLM apply its defaults. Extend `key_policy` with `team_id`, budgets, rate limits, or duration when your organization requires them. + + +Never print credentials, request stdin, or successful response JSON to logs. Stdout belongs exclusively to the protocol. Keep stderr diagnostics free of identities, credentials, and upstream response bodies. + + +Exercise the LiteLLM HTTP flow against a non-production gateway before building the artifact. The ensure request below calls `/user/list` and `/key/generate`; use the returned `external_id` in a revoke request to call `/key/delete`. + +```bash +chmod 0555 provisioner.py +export HARNESS_GATEWAY_URL=https://litellm.staging.example.com +export HARNESS_LITELLM_ADMIN_KEY=REDACTED +export LITELLM_MODELS=gpt-5.4,gpt-5.6-sol +printf '%s' '{"protocol_version":1,"operation":"ensure","request":{"identity":{"id":"user-id","email":"user@example.com","organization_id":"org-id","groups":[]},"reason":"missing","previous":null}}' | ./provisioner.py +``` + +## JSON protocol + +For ensure, stdin contains exactly one request: + +```json +{"protocol_version":1,"operation":"ensure","request":{"identity":{"id":"user-id","email":"user@example.com","organization_id":"org-id","groups":[]},"reason":"missing","previous":null}} +``` + +`reason` is `missing`, `configuration_changed`, `reconciliation_due`, or `credential_invalidated`. When present, `previous` contains `external_id`, `alias`, and `metadata`. Return `credential_invalid` when a previous key is confirmed deleted or blocked; Blue clears the stale encrypted credential and performs at most one bounded replacement attempt. Revoke uses operation `revoke` and a request containing `identity` and `external_id`. + +A successful ensure returns: + +```json +{"protocol_version":1,"status":"success","result":{"credential":"sk-new","external_id":"key-id","alias":"user@example.com","metadata":{},"expires_at":"2026-12-01T00:00:00Z"}} +``` + +`credential` may be null only when retaining the existing encrypted credential. `expires_at` is optional and, when supplied, must be RFC 3339. A successful revoke result is `{"revoked":true}`. + +Errors use a non-zero exit and this envelope: + +```json +{"protocol_version":1,"status":"error","error":{"code":"unavailable","message":"gateway request timed out"}} +``` + +Codes are `invalid_config`, `account_missing`, `conflict`, `credential_invalid`, `unavailable`, and `rejected`. Exit zero is valid only with a success envelope; every non-zero status (including shell `exit -1`, observed as 255 on Unix) is valid only with an error envelope. Empty, malformed, multiple, oversized, or protocol-mismatched stdout is treated as an unavailable host error. + +Stdout is reserved for the protocol. Blue never logs request stdin or successful stdout because they can contain credentials. Bounded stderr is available only as sanitized server diagnostics and is never returned to API or CLI clients. Timed-out children are terminated and reaped. + +Blue serializes lifecycle work per user. Effective provisioner concurrency is `min(max_concurrency, max(database_max_connections / 2, 1))`, with `max_concurrency` defaulting to `8`; gateway provisioning refuses to start when the control database pool has fewer than two connections. Invalid-key replacement is attempted once per trigger, uses persisted exponential cooldown, and stops automatic retries after five consecutive failures. + +Failed ensure messages are persisted in `provisioning_error` and returned through existing API and CLI error responses. No partial result is stored. Reconciliation remains retryable, so the next governed launch or personalized configuration fetch runs ensure again before inference. + +## Build and verify the artifact + +The artifact image supplies the pinned file to Helm; the executable runs inside the Control API container. For this Python example, install Python 3 in your deployed Control API image. Any imported third-party packages must be installed there as well. This example uses only Python's standard library. + +```dockerfile +FROM busybox:1.37.0-musl +COPY --chown=1000:1000 provisioner.py /executable/provisioner +RUN chmod 0555 /executable/provisioner +USER 1000:1000 +``` + +For example, extend the Control API runtime image separately: + +```dockerfile +FROM ghcr.io/blocksorg/blue@sha256:REPLACE_WITH_BLUE_IMAGE_DIGEST +USER root +RUN apt-get update && apt-get install -y --no-install-recommends python3 \ + && rm -rf /var/lib/apt/lists/* +USER node +``` + +```bash +docker build -t ghcr.io/acme/blue-provisioner:v1 . +docker run --rm --entrypoint sha256sum ghcr.io/acme/blue-provisioner:v1 /executable/provisioner +docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/acme/blue-provisioner:v1 +``` + +Pin the file digest in both `gateway.provisioner.executable_sha256` and `blue.provisionerExecutable.executableSha256`. Pin the OCI digest under `blue.provisionerExecutable.image.digest`. + +## Retry and troubleshooting behavior + +| Symptom | Result and next action | +| --- | --- | +| Executable fails startup validation | Control API startup fails. Check the absolute path, regular-file type, execute bit, lowercase digest, and policy revision. | +| Ensure returns a typed error | Blue records the public message without storing partial output. Correct the cause and retry the governed launch or configuration fetch. | +| Process exceeds `timeout_seconds` | Blue terminates and reaps it, reports `unavailable`, and leaves reconciliation eligible for retry. | +| Exit status and envelope disagree | Blue reports an `unavailable` host error. Exit zero only for success; use non-zero only for error. | +| Credential is `null` for a new user | Blue rejects the result. Null is valid only when the previous encrypted credential is retained. | diff --git a/apps/docs/0.2.0/admin/dashboard.mdx b/apps/docs/0.2.0/admin/dashboard.mdx new file mode 100644 index 0000000..0b80ac5 --- /dev/null +++ b/apps/docs/0.2.0/admin/dashboard.mdx @@ -0,0 +1,37 @@ +--- +title: "Dashboard administration" +description: "Manage users, policy revisions, clients, and captured sessions in the dashboard." +icon: "gauge-high" +--- + +The dashboard is the browser interface for organization administrators. In the reference Compose stack it runs at `http://127.0.0.1:3000`. + +## Areas + +| Area | Purpose | +| --- | --- | +| Gateway | When gateway policy is active, see routing readiness and the signed-in user's single managed gateway key. | +| Harnesses | Edit the managed model and execution settings for Codex, Claude, Kimi, and OpenCode. | +| [Extensions](/0.2.0/admin/managed-packages) | Select skills, hooks, subagents, plugins, helper binaries, and MCP servers for managed distribution. | +| Current deployment configuration | Open the user-menu dialog to copy or download effective deployment YAML. | +| [Members](/0.2.0/admin/user-management) | Invite users, manage roles and access state, revoke credentials, and retain account history. | +| Clients | See machine, harness, package, revision, drift, and managed-file health. | +| Sessions | Search captured sessions by native title or preview, download artifacts, and manage private, workspace, or selected-member sharing. | + +## Roles and visibility + +Administrative routes require the user's governance role to be `admin`. All dashboard data is scoped to the user's organization. Members can see sessions they own or that were shared with them and managed Gateway key status, but cannot edit organization policy. Only a session owner can change its sharing grants; recipients cannot edit, delete, or re-share it. Revocation does not remove copies that a recipient already restored locally. + +Portable session bundles can contain raw prompts, tool results, filesystem paths, and other sensitive conversation data. The Sessions detail page displays this disclosure before sharing controls. Legacy single-file captures remain downloadable audit records and are never advertised as resumable. + +The Extensions page displays curated and custom digest-pinned packages plus organization-managed MCP servers. Package selections, per-harness overrides, and MCP targets create the same immutable governance revisions as policy edits. Packages containing hooks, plugins, or helper binaries are marked as executable before administrators authorize them. + +## Bootstrap account + +Compose creates one local administrator from `HARNESS_BOOTSTRAP_ADMIN_EMAIL` and `HARNESS_BOOTSTRAP_ADMIN_PASSWORD`. Defaults are for loopback development only. Production deployment must supply a unique password, a strong `BETTER_AUTH_SECRET`, and public issuer/audience URLs. + +The bootstrap administrator is protected from demotion, suspension, and deletion. See [Manage users and invitations](/0.2.0/admin/user-management) for the complete lifecycle and API behavior. + + + Do not expose a deployment that still uses the documented development password or Better Auth secret. + diff --git a/apps/docs/0.2.0/admin/gateway-access.mdx b/apps/docs/0.2.0/admin/gateway-access.mdx new file mode 100644 index 0000000..d04cd04 --- /dev/null +++ b/apps/docs/0.2.0/admin/gateway-access.mdx @@ -0,0 +1,202 @@ +--- +title: "Gateway access" +description: "Operate gateway identities, managed credentials, inference routing, and request history." +icon: "route" +--- + +Gateway access connects the Blue inference proxy to the inference gateway your +organization operates. Blue does not bundle the upstream gateway; LiteLLM is +the first and currently only supported gateway type. It is independent of +[session capture](/0.2.0/admin/session-capture): enabling either feature does +not enable the other. + +## Responsibilities and data flow + +```mermaid +sequenceDiagram + participant User + participant Agent + participant Proxy as Blue inference proxy + participant API as Control API + participant Gateway as LiteLLM gateway + User->>API: Sign in and ensure gateway access + API->>Gateway: Create or reconcile managed credential + API-->>User: Session-bound inference JWT + Agent->>Proxy: Inference request + JWT + Proxy->>Proxy: Validate JWT + Proxy->>API: Resolve OAuth session + Blue user + API-->>Proxy: Server-side gateway credential + Proxy->>Gateway: Forward inference request + Proxy--)API: Send request metadata asynchronously +``` + +Blue stores the gateway credential envelope-encrypted and gives the client a +session-bound, inference-only JWT. Provider credentials, LiteLLM administrator +credentials, the signing key, and the managed gateway key remain server-side. + +## Prerequisites + +- A global `gateway` block in `blue.yaml`. +- Your reachable LiteLLM gateway and the Blue inference proxy. +- A provisioner selected by `gateway.provisioner.type`. +- Encryption configured for stored gateway credentials. +- Matching user identities in Blue and the gateway. + +Choose one provisioner model. The built-in LiteLLM provisioner matches users +by exact, case-insensitive email. A +[custom executable provisioner](/0.2.0/admin/custom-gateway-provisioners) can +apply another identity, account, team, budget, or credential policy. + +## Configure gateway access + + + +```yaml blue/blue.yaml +gateway: + type: litellm + url: os.environ/HARNESS_GATEWAY_URL + inference_proxy_url: os.environ/HARNESS_INFERENCE_PROXY_URL + internal_allowed_client_id: blue-inference-proxy + secret_encryption: + provider: aws-kms + key_id: os.environ/HARNESS_GATEWAY_KMS_KEY_ID + provisioner: + type: builtin-litellm + reconcile_ttl_seconds: 86400 +``` + +The built-in implementation uses `HARNESS_LITELLM_ADMIN_KEY` and the configured +gateway URL. It does not use executable fields. + + + +```yaml blue/blue.yaml +gateway: + type: litellm + url: os.environ/HARNESS_GATEWAY_URL + inference_proxy_url: os.environ/HARNESS_INFERENCE_PROXY_URL + internal_allowed_client_id: blue-inference-proxy + secret_encryption: + provider: aws-kms + key_id: os.environ/HARNESS_GATEWAY_KMS_KEY_ID + provisioner: + type: organization-gateway + executable_path: /var/run/blue/provisioner/provisioner + executable_sha256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + policy_revision: organization-gateway-v1 + reconcile_ttl_seconds: 86400 + timeout_seconds: 15 +``` + +The path must be absolute and identify a regular executable file. Blue verifies +the lowercase SHA-256 pin and execute bit at Control API startup. The policy +revision is required and should change whenever executable policy changes. + + + +Keep the gateway administrator key, the inference proxy's OAuth client secret, +provider credentials, and encryption credentials in the runtime Secret, not in +`blue.yaml`. + +| Runtime setting | Purpose | +| --- | --- | +| `HARNESS_GATEWAY_TYPE` | Required inference-proxy adapter key; must match top-level `gateway.type`. | +| `HARNESS_GATEWAY_URL` | Internal upstream gateway URL used by the Control API and provisioner. | +| `HARNESS_INFERENCE_PROXY_URL` | Client-reachable inference proxy URL injected into personalized policy. | +| `HARNESS_LITELLM_ADMIN_KEY` | Administrator credential used only to provision and reconcile LiteLLM access. | +| Provisioner-specific variables | Administrator credentials and settings inherited by a custom executable. Define their names in your deployment Secret. | +| `HARNESS_PROXY_OAUTH_CLIENT_SECRET` | Client-credentials secret the inference proxy uses to obtain a short-lived Control API token (shared with the dashboard client seed). | +| `HARNESS_GATEWAY_KMS_KEY_ID` | Production KMS key used for envelope encryption. | +| `HARNESS_GATEWAY_REQUEST_LOG_RETENTION_DAYS` | Request-metadata retention; defaults to `30`. | + +See [Bring your own gateway](/0.2.0/concepts/gateway-mode) for deployment topology and +[Blue YAML](/0.2.0/deployment/blue-yaml) for the complete configuration schema. + +## Provision and reconcile a user + +Users can provision from the dashboard's **Gateway** page or the CLI: + +```bash +blue gateway +``` + +Harness launch performs the same ensure operation lazily. If provisioning +fails, Blue blocks personalized gateway policy delivery instead of launching +with incomplete routing credentials. + +When status is ready, the dashboard's **Key** tab also calls `POST /gateway/key/validate`. Validation rechecks the upstream key at most once every 60 seconds. A confirmed invalid credential is cleared and reported as `invalid`; replacement requires the ensure operation, such as running `blue gateway`. A missing credential is reported without contacting the provisioner. + +The provisioner receives the authenticated Blue identity, ensure reason, and +the previous external ID, alias, and metadata when a credential already +exists. It creates a credential when none exists, updates it when configuration +or `policy_revision` changes, and periodically reconciles it after the TTL. +Harness launch and personalized configuration fetch both ensure access before +governed inference begins. + +An executable can return `credential: null` only when retaining the previous +encrypted credential. A rotated credential causes Blue to replace the stored +encrypted value and invalidate cached credential mappings. If ensure fails, +Blue records the public error, stores no partial +result, and retries on the next governed launch or configuration fetch. + + + Relaunch gateway-routed harnesses after credential rotation or access-profile + changes. A process that is already running retains the configuration it + received at launch. + + +## Dashboard and permissions + +The **Gateway** page appears only when gateway policy is active. + +- **Key** is visible to members and administrators and shows managed access status without exposing plaintext credentials. +- **Overview** is administrator-only and shows upstream and client-facing endpoints, supported harnesses, proxy health, and a non-secret runtime checklist. +- **Logs** is administrator-only and shows organization-wide request metadata and filters. + +Members can see their own credential status but cannot see request records. API role checks enforce the same boundaries as the dashboard. + +## Request history + +Blue records metadata for authenticated requests forwarded through the proxy, +including upstream errors and transport failures. Filters include user, key, +model, harness, result, and date. Records can include allowlisted repository, +branch, commit, dirty-state, and run attribution. + +Blue does not store prompts, request or response bodies, credentials, query +strings, inference JWTs, virtual keys, or arbitrary headers in request history. +Delivery is asynchronous and best effort so a Control API logging failure does +not delay inference traffic. Expired database rows are removed automatically +after `control_api.gateway_request_logs.retention_days`. + +## Change or disable access + +- Change provisioner policy and its policy revision to reconcile existing + credentials. +- Remove a user's gateway access through the source gateway or identity policy; + Blue retries idempotent revocation where applicable. +- Remove the top-level `gateway` block to disable gateway personalization for + the deployment. Native agents then use their normal provider configuration. +- Stop the inference proxy after the policy no longer advertises gateway mode. + +Starting an agent binary directly bypasses Blue's launch-time gateway wiring. +Launch through `blue`, for example `blue codex`. + +## Troubleshooting + +| Symptom | Check | +| --- | --- | +| Gateway page is absent | Confirm the active global policy contains `gateway`. | +| Runtime is incomplete | Verify gateway URL, proxy URL, OAuth client-credentials settings, encryption, and provisioner settings; then restart the services. | +| Account is missing | Ensure the gateway identity email matches the authenticated Blue email. | +| Provisioning is rejected | Check provisioner and gateway logs with secret redaction; verify the administrator key and team/model policy. | +| Executable fails startup validation | Verify its absolute path, regular-file type, execute bit, lowercase SHA-256 pin, and non-empty policy revision. | +| Executable times out or returns malformed JSON | Check the configured timeout and version 1 JSON contract. Keep stdout reserved for one response envelope. | +| Proxy health fails | Verify the browser/client can reach the public proxy and the proxy can reach the Control API and gateway. | +| Agent still calls a provider directly | Reconcile policy, confirm the harness is supported, and launch it through `blue`. | +| Logs are absent | Verify the log ingestion URL and authentication. Logging is best effort and can be disabled independently of routing. | + + + Never deliver a provider key, LiteLLM administrator key, or server-side + virtual key or signing key to a developer machine. Only the session-bound + inference JWT and client-reachable proxy URL belong in personalized harness configuration. + diff --git a/apps/docs/0.2.0/admin/identity-provisioning.mdx b/apps/docs/0.2.0/admin/identity-provisioning.mdx new file mode 100644 index 0000000..42a4109 --- /dev/null +++ b/apps/docs/0.2.0/admin/identity-provisioning.mdx @@ -0,0 +1,345 @@ +--- +title: "Set up Okta SSO and SCIM" +description: "Configure OIDC single sign-on and SCIM 2.0 user and group provisioning with Okta, then validate the complete identity lifecycle." +icon: "id-card" +--- + +Blue supports either local password authentication or one deployment-managed OIDC provider per workspace. This guide configures Okta, but the integration uses standard OIDC discovery and SCIM 2.0 so you can substitute another standards-compatible identity provider. + +In OIDC mode, users enter their email in Blue and continue to Okta to authenticate. Okta must provision a user through SCIM before that user can sign in. The protected bootstrap administrator remains available with its local password for recovery. + + + Blue currently uses strict directory provisioning rather than just-in-time (JIT) provisioning. Assigning a user only to an OIDC application permits authentication in Okta, but OIDC does not notify Blue about assignment, profile changes, group changes, or deactivation. SCIM creates the account before first login and remains the lifecycle source of truth. + + +Okta represents authentication and private SCIM provisioning as two application instances during development: + +| Okta application | Purpose | +| --- | --- | +| OIDC Web Application | Authenticates the user and returns the browser to Blue. | +| SCIM 2.0 Test App (Header Auth) | Creates, updates, groups, and deactivates the Blue account. | + +Assign each managed user to both applications. The word “Test” is Okta's name for its generic private SCIM connector; it is not a second user authentication mechanism. + +## Before you begin + +You need: + +- An Okta administrator who can create an OIDC web application and a private SCIM integration +- Public HTTPS URLs for the dashboard and Control API in production +- Access to the Blue deployment configuration and secrets +- The bootstrap administrator credentials + +Record these values before you start: + +| Value | Example | +| --- | --- | +| Dashboard URL (`BETTER_AUTH_URL`) | `https://governance.example.com` | +| Control API URL (`CONTROL_API_PUBLIC_URL`) | `https://control.example.com` | +| Provider ID | `okta` | +| OIDC callback URL | `https://governance.example.com/api/auth/callback/okta` | +| SCIM base URL | `https://control.example.com/scim/v2` | +| Okta issuer | `https://example.okta.com` | + + + The callback URL is `{BETTER_AUTH_URL}/api/auth/callback/{HARNESS_OIDC_PROVIDER_ID}`. The provider ID is part of the URL, so changing it later also changes the callback URL registered in Okta. + + +### Test with local services + +For local development, the browser can return directly to the loopback dashboard: + +```text +http://127.0.0.1:3000/api/auth/callback/okta +``` + +Okta's SCIM service cannot reach your loopback Control API. Expose port `8080` with an HTTPS tunnel such as ngrok, then use the tunnel URL only as Okta's SCIM connector base URL: + +```bash +ngrok http 8080 +``` + +```text +https://your-tunnel.ngrok-free.app/scim/v2 +``` + +Keep the tunnel running while Okta provisions users. A temporary tunnel URL may change when the tunnel restarts. + +## Configure Okta and Blue + + + + Generate a dedicated, high-entropy SCIM bearer token. For a new deployment, also generate and persist a Better Auth secret. For example: + + ```bash + openssl rand -base64 48 + ``` + + Store the generated value in your deployment secret manager as `HARNESS_SCIM_BEARER_TOKEN`. Do not reuse a user password, OIDC client secret, or API token. + + + If the deployment already has authentication data, keep its existing `BETTER_AUTH_SECRET`. Better Auth encrypts OAuth signing keys with this value. Replacing it without a planned key rotation prevents token issuance with “Failed to decrypt private key” and invalidates existing browser sessions. + + + + + In the Okta Admin Console, create an **OIDC - OpenID Connect** app integration with **Web Application** as the application type. + + Configure the application with: + + - **Sign-in redirect URI:** the callback URL from the table above + - **Grant type:** Authorization Code + - **Assignments:** the people or groups that are allowed to use Blue + + Save the integration, then copy its client ID and client secret. See Okta's [web application redirect guide](https://developer.okta.com/docs/guides/sign-into-web-app-redirect/) if your Admin Console labels differ. + + + Assigning a user to the Okta application does not create the user in Blue. Configure SCIM and provision the user before testing SSO. + + + + + For basic workforce SSO, use Okta's organization authorization server. Its issuer is the Okta domain without `/oauth2/default`: + + ```text + https://example.okta.com + ``` + + Blue uses OIDC discovery from this issuer. Confirm that `{issuer}/.well-known/openid-configuration` is reachable from the dashboard service. + + If your organization requires the custom `default` authorization server, use `https://example.okta.com/oauth2/default`. You must also permit the OIDC client: + + 1. Go to **Security → API → Authorization Servers → default → Access Policies**. + 2. Add a policy assigned to the Blue OIDC client. + 3. Add a rule allowing **Authorization Code**, assigned users, and the `openid`, `profile`, and `email` scopes. + + A custom authorization-server request that matches no policy and rule fails with “Policy evaluation failed for this request.” + + + + Set these environment variables for the deployment: + + ```bash + HARNESS_AUTH_MODE=oidc + HARNESS_OIDC_PROVIDER_ID=okta + HARNESS_OIDC_PROVIDER_NAME=Okta + HARNESS_OIDC_ISSUER=https://example.okta.com + HARNESS_OIDC_CLIENT_ID=your-okta-client-id + HARNESS_OIDC_CLIENT_SECRET=your-okta-client-secret + HARNESS_SCIM_BEARER_TOKEN=your-generated-scim-token + HARNESS_SCIM_GROUP_ROLE_MAPPINGS={"Blue Admins":"admin"} + ``` + + Keep the existing `BETTER_AUTH_URL`, `CONTROL_API_PUBLIC_URL`, bootstrap credentials, database, and internal service settings. For Docker Compose, place the values in the environment or `.env` file read by Compose. + + `HARNESS_SCIM_GROUP_ROLE_MAPPINGS` must be a JSON object. Group names are exact and case-sensitive, and roles must be `admin` or `member`. A provisioned user defaults to `member`; an `admin` mapping takes precedence if multiple pushed groups match. + + + + Rebuild or restart the Control API and dashboard with the new settings. Verify the Control API and authenticated SCIM discovery endpoint: + + ```bash + curl --fail-with-body https://control.example.com/health + curl --fail-with-body \ + --header "Authorization: Bearer $HARNESS_SCIM_BEARER_TOKEN" \ + https://control.example.com/scim/v2/ServiceProviderConfig + ``` + + The second request should return a SCIM `ServiceProviderConfig` document. A `401 Unauthorized` response normally means the bearer token does not match the deployment secret. + + + Use the bootstrap administrator while you finish provisioning. Ordinary local password users cannot sign in after the deployment switches to OIDC mode. + + + + + Create Okta's generic private SCIM connector: + + 1. Go to **Applications → Applications → Browse App Catalog**. + 2. Search for **SCIM 2.0 Test App (Header Auth)** and select **Add Integration**. + 3. Give the application a recognizable label. + 4. Leave the initial sign-on options at their defaults and finish creating the application. + 5. Open **Provisioning → Configure API Integration** and enable the API integration. + + Configure: + + - **SCIM connector base URL:** the SCIM base URL from the table above + - **Unique identifier field for users:** `userName` + - **Authentication mode:** HTTP Header + - **API Token:** `Bearer ` followed by the `HARNESS_SCIM_BEARER_TOKEN` value + + Test the API credentials in Okta before continuing. Follow Okta's [private SCIM integration guide](https://developer.okta.com/docs/guides/scim-provisioning-integration-connect/) for the current Admin Console workflow. + + + The **SCIM 2.0 Test App (Header Auth)** sends the API Token field as the complete `Authorization` header. Include the literal `Bearer ` prefix, but do not include the environment-variable name, quotes, or extra whitespace. + + + + + Enable these To App actions in Okta: + + - Create Users + - Update User Attributes + - Deactivate Users + + Leave password synchronization disabled. SCIM-managed users authenticate through Okta and do not receive a local password. + + Ensure profile mappings send `userName`, `active`, `name.givenName`, and `name.familyName`. Blue treats `userName` as the user's email address and also accepts the standard SCIM email attribute. + + + + Assign one non-administrator test user to the SCIM application and confirm that Okta reports a successful push. Then assign the same user to the separate OIDC Web Application. + + Both assignments are required: + + - The SCIM assignment creates the Blue account. + - The OIDC assignment permits the user to authenticate. Without it, Okta reports that the user is not assigned to the client application. + + Sign in as the bootstrap administrator and open **Members**. The test user should appear with a SCIM-managed indicator and the `member` role. In managed mode, the **Identity & provisioning** tab shows the status reported by `GET /admin/identity/status`. + + If an existing local user has the same normalized email, SCIM converts that account to an identity-provider-managed account. Governance history remains attached to the user, while the local password and issued credentials are revoked. The protected bootstrap administrator cannot be converted. + + + + Create or select the Okta group whose members should administer Blue. Its name must match a key in `HARNESS_SCIM_GROUP_ROLE_MAPPINGS`; for example, `Blue Admins`. + + To grant the configured example administrator role: + + 1. Go to **Directory → Groups** and create `Blue Admins`. + 2. Add the managed user to the group. + 3. Open the SCIM application and select **Push Groups**. + 4. Select **Push Groups → Find groups by name**. + 5. Select `Blue Admins` and push its memberships immediately. + + Verify that the pushed test administrator has the `admin` role on **Members**. + + Users with no matching group remain `member`. Removing the final matching administrator group demotes the user to `member`; it does not deactivate the account. + + + + Sign out, enter the provisioned user's email on the Blue login page, and confirm that the browser redirects to Okta. Complete authentication and verify that you return to the dashboard. + + Test the complete lifecycle with your test user: + + 1. Remove and restore an administrator group membership, confirming the role changes. + 2. Deactivate the user in Okta, confirming access and active sessions are revoked. + 3. Reactivate the user, confirming a new SSO login works but an old session does not. + 4. If you use the CLI, run its device login and confirm browser approval uses Okta SSO. + + + +## Prepare gateway access for CLI users + +SSO authenticates the dashboard and CLI device-authorization browser flow. Gateway access is a separate downstream dependency. If `blue.yaml` contains the top-level `gateway` section, finish gateway setup before asking users to run `blue login`. + +Confirm: + +- LiteLLM is running and reachable from the Control API. +- The inference proxy is running and its public URL is reachable from the user's machine. +- `LITELLM_MASTER_KEY` is available to the deployment as `HARNESS_LITELLM_ADMIN_KEY`. +- `HARNESS_GATEWAY_URL`, `HARNESS_INFERENCE_PROXY_URL`, and the inference proxy's OAuth client-credentials settings (`HARNESS_PROXY_OAUTH_TOKEN_URL`, `HARNESS_PROXY_OAUTH_CLIENT_ID`, `HARNESS_PROXY_OAUTH_CLIENT_SECRET`, `HARNESS_PROXY_OAUTH_RESOURCE`) are configured. +- A LiteLLM user exists with the exact SCIM email, ignoring case. +- The configured provisioner's account, team, and policy requirements are satisfied. The provided local Compose provisioner requires the LiteLLM user to belong to at least one team. +- Blue can create or update one managed key for that user. The shipped LiteLLM provisioners use `blue:` as the base alias and add a numeric suffix when it is occupied. + +For the provided local deployment, start the gateway profile with the same LiteLLM environment: + +```bash +docker compose --env-file /path/to/ai-gateway/.env \ + --profile gateway up -d --build control-api inference-proxy +``` + +After SSO and SCIM provisioning succeed, provision or reconcile the user's managed gateway key: + +```bash +blue login +blue gateway +blue apply +blue status +``` + +`blue status` should report matching desired and applied revisions with managed files present. + + + SCIM provisions the Blue identity but does not create the LiteLLM user or satisfy provisioner-specific account policy. Once those prerequisites are met, Blue provisions the managed key automatically. If gateway mode is active and provisioning prerequisites are missing, CLI configuration cannot complete. Governance-only deployments can omit the top-level `gateway` section from `blue.yaml` and skip this section. + + +## Identity lifecycle behavior + +| Identity provider action | Blue result | +| --- | --- | +| Create user | Creates a passwordless, SCIM-managed `member`, or converts the matching local account. | +| Update user | Updates the managed email and display name. | +| Push group membership | Recalculates the role from exact group mappings. | +| Set `active: false` | Suspends access and revokes browser, CLI, device, and gateway credentials. | +| Set `active: true` | Allows a new login; old sessions remain revoked. | +| Delete user | Removes authentication access while retaining governance and captured-session history. | + +Dashboard administrators can revoke sessions for managed users, but role and lifecycle changes must come from the identity provider. Invitations are disabled in OIDC mode. + +## Roll out from password authentication + +SCIM endpoints are enabled whenever `scim_bearer_token` is configured, independently of authentication mode. OIDC mode requires SCIM configuration, and ordinary local password login is disabled as soon as you switch modes. Plan a short controlled migration: + +1. Create the Okta OIDC and SCIM applications and prepare all deployment secrets. +2. Back up the database and confirm the bootstrap administrator credentials work. +3. Deploy `HARNESS_AUTH_MODE=oidc` and verify SCIM discovery. +4. Immediately provision a small test group, validate login, then provision the remaining users. +5. Push administrator groups only after confirming their names and membership. + + + Switching back to password mode does not restore passwords or credentials revoked when a local account was converted to SCIM. The bootstrap administrator can still sign in, but converted users need newly issued local access before they can use password mode again. + + +## Rotate secrets + +The deployment accepts one SCIM bearer token at a time. Coordinate rotation so Okta and Blue change together: + +1. Generate a new high-entropy token. +2. Update Okta and the deployment secret in the same maintenance window. +3. Restart the Control API. +4. Test API credentials in Okta and trigger a test user update. + +To rotate the OIDC client secret, follow Okta's application-secret procedure, update `HARNESS_OIDC_CLIENT_SECRET`, restart the dashboard, and complete a new SSO login. + +Do not rotate `BETTER_AUTH_SECRET` as though it were an OIDC client secret. It protects Better Auth data at rest, including the private signing key stored in the database. A planned rotation must replace or re-encrypt that stored key and intentionally invalidate sessions; changing only the environment variable breaks OAuth token issuance. + +## Troubleshooting + +| Symptom | What to check | +| --- | --- | +| Okta's API credentials test returns `401` | For the Header Auth test app, enter `Bearer ` plus the exact current SCIM token in the API Token field. | +| Okta cannot reach SCIM | Confirm the base URL ends in `/scim/v2`, uses publicly trusted TLS, and reaches the Control API. | +| OIDC reports a redirect URI error | Confirm the URI exactly matches `{BETTER_AUTH_URL}/api/auth/callback/{provider-id}`. | +| Okta says the user is not assigned to the client | Assign the user to the OIDC Web Application as well as the SCIM application. | +| Okta reports “Policy evaluation failed” | If the issuer uses `/oauth2/default`, add an access policy and Authorization Code rule for the OIDC client. | +| Login says the account is not provisioned | Push the active user through SCIM first. OIDC sign-up is intentionally disabled. | +| The user stays a `member` | Confirm the group and membership were pushed and the mapping name matches exactly, including case. | +| Password login no longer appears | This is expected for non-bootstrap users in OIDC mode. Enter the email to continue to Okta. | +| OIDC discovery fails | Confirm the issuer and dashboard access to `.well-known/openid-configuration`. | +| A user signs in with the wrong account | Ensure Okta's verified email claim matches the SCIM `userName` or email after normalization. | +| CLI token exchange returns an empty `500` | Check dashboard logs for a Better Auth private-key decryption error and restore the stable `BETTER_AUTH_SECRET` used to create the stored JWKS. | +| CLI falls back to cached config with a gateway-runtime error | Configure LiteLLM and the inference proxy, then restart the Control API with the gateway admin key. | +| `blue gateway` reports no active keys | Provision an active LiteLLM API key for the matching email and include every model required by the agents' managed configuration. | + +## Configuration reference + +| Setting | Purpose | +| --- | --- | +| `HARNESS_AUTH_MODE=oidc` | Enables email-first OIDC login and disables invitations; it also requires SCIM configuration. | +| `HARNESS_OIDC_PROVIDER_ID` | Stable provider key; defaults to `okta`. | +| `HARNESS_OIDC_PROVIDER_NAME` | Login UI label; defaults to `Okta`. | +| `HARNESS_OIDC_ISSUER` | OIDC discovery issuer. | +| `HARNESS_OIDC_CLIENT_ID` / `HARNESS_OIDC_CLIENT_SECRET` | Confidential OIDC web application credentials. | +| `HARNESS_SCIM_BEARER_TOKEN` | Dedicated administrative token sent by the SCIM client. | +| `HARNESS_SCIM_GROUP_ROLE_MAPPINGS` | JSON object mapping exact group names to `admin` or `member`. | + +The SCIM bearer token and group-role mappings may instead be supplied in `blue.yaml` as `control_api.identity.scim_bearer_token` and `control_api.identity.group_role_mappings`. + +## Use another identity provider + +The integration does not call Okta-specific APIs. Create a confidential OIDC web application and a SCIM 2.0 client in the other provider, then change the provider ID, display name, issuer, and credentials. Keep the same callback and SCIM URL patterns and validate the provider's claims and SCIM payloads with a test user before rollout. + + + Treat the SCIM bearer token as an administrative credential. Expose SCIM only over TLS, store the token in deployment secrets, and never reuse it for user or CLI authentication. + diff --git a/apps/docs/0.2.0/admin/managed-packages.mdx b/apps/docs/0.2.0/admin/managed-packages.mdx new file mode 100644 index 0000000..e5bd243 --- /dev/null +++ b/apps/docs/0.2.0/admin/managed-packages.mdx @@ -0,0 +1,336 @@ +--- +title: "Managed packages" +description: "Distribute and remove governed skills, hooks, subagents, plugins, and helper binaries from the dashboard." +icon: "box-open" +--- + +Managed packages let an administrator choose extensions in the dashboard and have each developer's CLI install the compatible contents locally. Removing a package from governance removes its activation on the next reconciliation. + +## Manage packages in the dashboard + +You must have the `admin` role to see **Extensions** in the dashboard sidebar. + + + Open `http://127.0.0.1:3000/extensions` in the Compose deployment, or select **Extensions** in the administrator sidebar. + Select catalog packages containing the skills, hooks, subagents, plugins, or helper binaries your organization approves. + Cards identify packages that contain hooks, plugins, or helpers. Selecting one authorizes that executable content for noninteractive client reconciliation. + Keep **Everyone** to deploy across the organization, or select **Specific members** and choose one or more active administrators or members. + Optionally disable a package for an individual harness or merge harness-specific settings into its organization defaults. + Publishing validates the complete selection and creates a new immutable governance revision. + + +If the page reports that no curated catalog is configured, add `package_catalog.packages` to the deployment's `blue.yaml`. You can still select **Add extension** to add a custom digest-pinned package. + +## Deploy to specific members + +Open an extension's details and choose **Specific members** under **Deployment audience**. Search by email and select every person who should receive the complete package bundle. Audience choices are independent per extension, so one revision can deploy different packages to different groups of people. MCP servers remain organization-wide. + +Targeting uses the workspace user's stable identity. Suspended and removed users cannot authenticate, but their existing assignments are retained and resume if access is restored. Inactive assignments remain visible to administrators and can be removed; inactive users cannot be newly selected. + +The Control API removes untargeted packages and their harness overrides before returning governance configuration. Changing an audience creates a new revision, so the normal client reconciliation deactivates a package when a user is removed from its audience. Mirrored private artifacts are also available only when the current package audience includes the requesting user. Public source URLs remain subject to their host's access controls. + +## Add a custom package + +Use the **Add extension** form when an extension is not in the curated catalog: + +1. Enter a stable package ID, display name, and version. +2. Choose a managed GitHub/Bitbucket connection and enter `namespace/repository` plus a ref, or inspect a public GitHub repository or HTTPS archive. Managed repositories may be private; credentials remain in the control plane. The form records the source and computed SHA-256, and mirrors managed repository artifacts. +3. For each supported harness, choose a component type and enter its path relative to the archive root. Add separate mappings for skills, subagents, hooks, plugin directories, plugin modules, and platform-specific helper binaries. + Set **Harness available from** and **Harness available before** when the package supports only part of that harness's release history. Use layout intervals only when paths or component types change inside that availability range. +4. Review the capability table, then select **Add to pending changes**. +5. Review the extension inventory. It shows the package, harness, enabled state, capability type, archive path, and expected managed local location. +6. Select **Publish extension changes** to create a governance revision. + +The form adds the package to the pending selection first; it does not publish anything until you publish the complete selection. + +To change a custom package later, open its **Edit** dialog. The **Harness version availability** section lets you update the inclusive **available from** and exclusive **available before** bounds for every selected harness. Curated catalog package ranges remain controlled by the deployment's `blue.yaml`. + +## Add an extension from a public repository or archive + +Public GitHub repositories do not require a managed repository connection. In the **Public repository or archive** tab, enter one of these source forms: + +- `github:owner/repository@ref` for a public GitHub repository. The ref may be a branch, tag, or full commit SHA. Blue resolves it to an immutable commit and records the corresponding GitHub codeload URL. +- An immutable public HTTPS `.tar.gz` URL. Blue uses the URL as supplied and does not resolve a branch or tag encoded in it. The URL must return the archive itself: Blue does not follow redirects. + +Do not enter a repository webpage such as `https://github.com/owner/repository`; it returns HTML rather than a package archive. + +Do not enter a GitHub release-asset URL (`https://github.com/owner/repository/releases/download/...`) either. It answers with a 302 to a short-lived signed `objects.githubusercontent.com` URL, so pointing at the final URL is not a workaround — that URL expires. Use the `github:owner/repository@ref` form, whose codeload URL serves the archive directly, or a managed repository connection. Prefer a release tag or full commit SHA when using the GitHub form. Although Blue resolves a branch to the current commit during inspection, an explicit immutable ref makes the administrator's intent easier to audit. + + + + Enter a source such as `github:BlocksOrg/agent-extensions@v1.2.0`, then select **Inspect source**. Blue resolves GitHub refs, downloads at most 100 MiB, and computes the SHA-256 digest shown for review. + + + Paths are relative to the literal extracted archive root. GitHub codeload archives include a generated top-level directory named from the repository and commit. For example, a repository skill at `skills/secure-code-review/SKILL.md` is mapped as `agent-extensions-/skills/secure-code-review`. + + + Add the capability mappings, select **Add to pending changes**, and publish the complete extension selection. Public sources have no `artifact_id`; each client downloads the recorded public URL directly and accepts it only when its SHA-256 matches the governance revision. + + + +```yaml Public GitHub skill after inspection +packages: + - id: secure-code-review + version: "1.2.0" + source_ref: https://codeload.github.com/BlocksOrg/agent-extensions/tar.gz/0123456789abcdef0123456789abcdef01234567 + sha256: + adapters: + codex: + skills_dir: agent-extensions-0123456789abcdef0123456789abcdef01234567/skills/secure-code-review + claude: + skills_dir: agent-extensions-0123456789abcdef0123456789abcdef01234567/skills/secure-code-review +``` + +Public clients must be able to reach the recorded host directly. Blue does not use environment proxy settings for public package downloads: it resolves every address, rejects the entire request if any answer is non-public, and connects only to the validated addresses without following redirects. Public URLs cannot contain embedded credentials or fragments, and diagnostics omit query strings. Blue does not mirror public sources into organization artifact storage. If an archive URL later serves different bytes, reconciliation fails digest verification instead of activating changed content. + +The host must also resolve to public addresses **from each client's resolver**, not just from the control plane. Blue rejects the whole DNS answer set when any address in it is non-public, so a split-horizon deployment whose internal resolver returns a private address for a genuinely public hostname fails with `package URL must resolve only to public addresses`. Do not work around this by making the client trust private answers: use a [managed package source connection](/0.2.0/deployment/managed-repositories), which keeps its own explicit host allowlist, for anything an internal resolver rewrites. + +### Scope a package to harness versions + +Package availability and layout are separate contracts: + +- **Harness available from** maps to inclusive adapter `introduced`. +- **Harness available before** maps to exclusive adapter `before`. +- **Layout introduced** and **Layout before** define a nested variant selected + only within the adapter availability range. +- Top-level component fields form the fallback layout when no variant matches. + +Publishing fails when a variant escapes adapter availability, variants overlap, +a variant-only adapter leaves a gap, or the organization's harness policy allows +versions outside the package's availability. Narrow the harness policy or disable +the package for that harness rather than claiming unsupported compatibility. + +```yaml +packages: + - id: review-kit + version: "2.0.0" + source_ref: https://example.com/review-kit-2.0.0.tar.gz + sha256: <64-lowercase-hex-characters> + adapters: + claude: + introduced: 2.0.12 + before: 3.0.0 + variants: + - introduced: 2.0.12 + before: 2.5.0 + plugin_dir: review-kit/claude-v2 + - introduced: 2.5.0 + before: 3.0.0 + plugin_dir: review-kit/claude-v2_5 +``` + +See [Harness version compatibility](/0.2.0/concepts/harness-version-compatibility#versioned-package-layouts) for the interval semantics. + +## Add an extension from a managed repository + +A deployment operator must first configure a GitHub or Bitbucket connection and allow it for your organization. Administrators select configured connections in the dashboard; they never enter repository credentials there. See the [managed repository connection guide](/0.2.0/deployment/managed-repositories) for the operator setup. + + + + On **Extensions**, select **Add extension**, then select the **Managed repository** tab. + + + Select a connection from the list. Only connections allowed for your organization appear. If the list is empty, ask a deployment operator to configure and authorize one, or use the **Public archive** tab for a repository archive that does not require authentication. + + + Enter the repository using the format expected by its provider: + + | Provider | Repository format | Example | + | --- | --- | --- | + | GitHub or GitHub Enterprise Server | `owner/repository` | `platform/team-toolkit` | + | Bitbucket Cloud | `workspace/repository` | `platform/team-toolkit` | + | Bitbucket Data Center | `PROJECT/repository` | `PLAT/team-toolkit` | + + Enter a branch, tag, or full commit SHA in **Ref**, such as `main`, `v1.2.0`, or a 40-character commit SHA. The repository namespace must match one of the allowed namespaces shown below the field. + + + Select **Inspect source**. The Control API authenticates to the provider, resolves the requested ref to an immutable commit, downloads an archive of at most 100 MiB, computes its SHA-256 digest, and mirrors it into the organization's artifact storage. The result displays the resolved commit and archive size. + + Inspection does not publish the extension. Repository credentials remain in the control plane and are never included in a governance revision or sent to a client. + + + Enter an ID containing lowercase letters, numbers, and hyphens, a version, and an optional display name. The ID must be unique among configured extensions. + + + Add every capability the extension contributes. Each path is relative to the root of the downloaded archive and must exist after extraction. + + | Capability | Example repository path | + | --- | --- | + | Plugin | `toolkit` | + | Skill | `shared-skills/secure-code-review` | + | Subagent | `toolkit/agents` | + | Hook | `toolkit/hooks/hooks.json` | + | OpenCode plugin module | `toolkit/plugins/index.js` | + | Helper | `toolkit/bin/review` | + + A skill path can point directly to a skill folder containing `SKILL.md`, regardless of where it lives in the repository. A folder containing several skill folders remains supported for compatibility. Codex and Claude subagents and hooks still require a native plugin mapping; OpenCode hooks are plugin modules. For a helper, also enter its command name and platform key. + + + Select **Add to pending changes**, review the inventory on the Extensions page, and then select **Publish extension changes**. Publishing validates the full configuration and creates a new immutable governance revision for clients to reconcile. + + + +### Example: deploy one private skill to every developer + +Suppose the platform team maintains a `secure-code-review` skill in the private GitHub repository `BlocksOrg/agent-extensions`. They want every developer governed by the `dev` organization policy to receive it in Codex and Claude. + + + **Add extension** does not upload files to GitHub or Bitbucket. A developer pushes the skill with Git. An administrator selects the **Skill** capability and enters the exact repository path to its folder. + + + + + The repository does not need to be a plugin or follow a prescribed top-level layout. The selected folder only needs to contain a valid `SKILL.md`: + + ```text Repository layout + agent-extensions/ + ├── shared-skills/ + │ └── secure-code-review/ + │ └── SKILL.md + └── unrelated-project/ + └── ... + ``` + + Define the skill in `shared-skills/secure-code-review/SKILL.md`: + + ```markdown SKILL.md + --- + name: secure-code-review + description: Review application changes for authentication, authorization, and secret-handling risks. + --- + + # Secure code review + + Inspect the changed files and identify trust boundaries before reporting findings. + Prioritize exploitable issues and include a concrete remediation for each finding. + ``` + + + Commit the files and push a version tag. The tag gives the administrator a recognizable ref; Blue resolves it to a commit during inspection. + + ```bash + git add shared-skills/secure-code-review + git commit -m "Add secure code review skill" + git tag secure-code-review-v1.0.0 + git push origin main secure-code-review-v1.0.0 + ``` + + + A deployment operator installs the GitHub App with read access to `BlocksOrg/agent-extensions`, configures the connection, and allows the `BlocksOrg` namespace for the Blue organization named `dev`: + + ```yaml blue.yaml + control_api: + package_sources: + connections: + - id: company-github + name: Company GitHub + provider: github + app_id: os.environ/HARNESS_GITHUB_APP_ID + private_key: file:///run/secrets/github-app-private-key.pem + organizations: + dev: + - BlocksOrg + ``` + + Restart the Control API after adding the connection. For Bitbucket, configure the equivalent workspace or project allowlist as described in [Managed repository connections](/0.2.0/deployment/managed-repositories#configure-a-provider). + + + In **Extensions**, select **Add extension** and enter: + + | Field | Value | + | --- | --- | + | Source type | **Managed repository** | + | Connection | **Company GitHub** | + | Repository | `BlocksOrg/agent-extensions` | + | Ref | `secure-code-review-v1.0.0` | + + Select **Inspect source** and confirm that the form reports a resolved commit, archive size, and verified managed artifact. + + + Describe the extension as ID `secure-code-review`, version `1.0.0`, and display name `Secure code review`. Then add one mapping for each target harness: + + | Harness | Capability | Path inside repository | + | --- | --- | --- | + | Codex | Skill | `shared-skills/secure-code-review` | + | Claude | Skill | `shared-skills/secure-code-review` | + + Blue wraps the selected skill in harness-owned runtime metadata. The repository does not need `.codex-plugin` or `.claude-plugin` manifests, and `unrelated-project` is not activated. + + + Select **Add to pending changes**, review the two capability mappings, and select **Publish extension changes**. The new governance revision now requires the extension for compatible Codex and Claude clients in the `dev` organization. + + + Developers can run `blue apply` to reconcile immediately. Otherwise, their background agent installs the digest-pinned artifact on its next policy poll. On **Clients**, verify that active clients have applied the new revision and that the package is not reported as failed or drifted. + + “Everyone” means every authenticated developer receiving this organization policy whose machine has an allowed, compatible harness. It does not grant repository access to developers and does not affect users in another Blue organization. + + + +### Source metadata + +| Value | Meaning | +| --- | --- | +| Requested ref | The branch, tag, or commit entered during inspection. | +| Resolved commit | The immutable provider commit returned for that ref. | +| `source_ref` | Human-readable repository provenance stored with the package. | +| `artifact_id` | Organization-scoped identifier for the mirrored archive. | +| `sha256` | Digest clients verify before installing the archive. | + +### Troubleshoot repository inspection + +| Message or symptom | What to check | +| --- | --- | +| No repository connections | The connection is configured and allowed for this organization. | +| Namespace is not allowed | The repository owner, workspace, or project matches the connection's namespace allowlist. | +| Provider returns `401` or `403` | The configured credential is valid and can read the private repository. | +| Ref or repository is not found | The repository format is correct and the branch, tag, or commit exists. Some providers return `404` when credentials lack access. | +| Archive exceeds 100 MiB | Reduce the repository archive or publish a smaller extension repository. | +| Redirect target is not trusted | The operator must allow the provider's archive host in the connection configuration. | +| Capability validation fails | Every mapped path exists inside the archive and the selected harness supports that capability type. | + +## Client lifecycle + +```mermaid +sequenceDiagram + participant Admin as Dashboard administrator + participant API as Control API + participant Blob as Artifact storage + participant CLI as Developer CLI + participant Store as Local managed store + Admin->>API: Save package selection + API-->>Admin: Create governance revision + CLI->>API: Fetch revision after login or poll + CLI->>API: Request artifact download with harness session + API-->>CLI: Fresh short-lived download request + CLI->>Blob: Download mirrored archive + CLI->>CLI: Verify SHA-256 + CLI->>Store: Install immutable package content + CLI->>CLI: Activate compatible harness adapters + Admin->>API: Remove package from selection + CLI->>Store: Deactivate and remove unchanged content +``` + +Login stores authentication without writing agent configuration. The background agent keeps the configured default agent current on every policy poll. Other agents receive their managed packages just in time when launched through Blue. Run `blue apply` to reconcile the default agent immediately. + +Package failures are isolated: other packages can still converge. A governed harness launch is blocked if one of its required packages failed to activate. + +## Installation and teardown + +Archives are downloaded with a 100 MiB compressed limit and verified against their configured SHA-256 digest. Extraction allows at most 10,000 entries, 512 MiB of expanded regular-file data, 128 MiB per file, 32 path components, and 4,096 encoded path bytes. Blue rejects duplicate or non-portable paths, absolute paths, traversal, links, and special files before activation. Random owner-only staging directories are removed after failures, installed files are owner-only, and only manifest-declared helpers become executable. Installed versions live under the harness-managed XDG configuration directory rather than user-owned native extension directories. + +When a package is removed from the managed configuration, the CLI removes its harness activation. Unreferenced content is deleted only when it still matches the recorded tree hash. Locally modified content is quarantined and reported on the **Clients** page instead of being silently deleted. + +## Package contents + +Each package declares an immutable `source_ref`, a SHA-256 digest, and adapters for one or more harnesses. A managed repository package also declares an organization-scoped `artifact_id`; `source_ref` remains human-readable provenance. Packages such as native CLI helpers may also declare digest-pinned `platform_sources`; when that map is present, the client requires an exact `-` match. An adapter can expose: + +| Field | Purpose | +| --- | --- | +| `plugin_dir` | A Claude or Codex plugin root. | +| `skills_dir` | Skills loaded for the target harness. | +| `agents_dir` | Managed subagent definitions. | +| `hooks_file` | Hook configuration supported by that harness. | +| `plugins` | OpenCode plugin modules. | +| `helpers` | Platform-specific executables added to the governed launch `PATH`. | + +See [Governance configuration](/0.2.0/reference/governance-config#managed-packages) for the complete YAML schema and adapter example. Maintainers can use [Local development with Docker Compose](/0.2.0/development/local-compose) to test package-catalog mounting. diff --git a/apps/docs/0.2.0/admin/session-capture.mdx b/apps/docs/0.2.0/admin/session-capture.mdx new file mode 100644 index 0000000..5b8b90a --- /dev/null +++ b/apps/docs/0.2.0/admin/session-capture.mdx @@ -0,0 +1,290 @@ +--- +title: "Session capture" +description: "Configure, operate, retain, share, and restore portable coding-agent sessions." +icon: "file-shield" +--- + +Session capture packages a supported coding agent's safe session-owned artifacts into a versioned, integrity-checked bundle and uploads it to +organization-controlled object storage after its lifecycle hook runs. It is +independent of [gateway access](/0.2.0/admin/gateway-access) and does not inspect +inference-proxy traffic. + + + Native transcripts can contain prompts, model responses, source fragments, + tool inputs and outputs, file paths, and secrets exposed during a session. + Define disclosure, access, encryption, retention, incident response, and + deletion policy before enabling capture. + + +## Upload architecture + +```mermaid +sequenceDiagram + participant Hook as Agent lifecycle hook + participant Spool as Local protected spool + participant API as Control API + participant Blob as Object storage + Hook->>Spool: Build bundle manifest and record SHA-256 + Hook->>Hook: Start detached upload worker + Spool->>API: Request short-lived upload authorization + API-->>Spool: Signed URL, method, and headers + Spool->>Blob: Upload compressed session bundle + Spool->>API: Complete with SHA-256 + API->>Blob: Verify object size and digest metadata + API-->>Spool: Register current artifact + Spool->>Spool: Remove successful local record +``` + +The hook first spools data atomically, then starts a detached worker so a +short or non-awaited agent shutdown hook cannot truncate the upload. The worker +claims records to avoid duplicate concurrent processing and retries transient +failures. Failed records remain in the local spool for a later worker run. + +Object storage is opaque to Blue's service layer. The bundle manifest carries +the information needed to validate and restore an object, but both the Control +API and the restoring client repeat the checks appropriate to their trust +boundary. A successful transport checksum alone does not make an archive safe +to extract. + +## Prerequisites + +- A separate S3-compatible bucket for session bundles and legacy raw captures. +- Workload identity or ambient credentials with scoped object permissions. +- A client version and installed harness version with a verified session-hook + compatibility profile. +- A public presign endpoint reachable from developer workstations. +- An object lifecycle rule aligned with your retention policy. + +Do not reuse the managed-package bucket. Package artifacts can be referenced +indefinitely, while session objects are expected to expire. + +## Configure storage + +Configure the Control API's S3-compatible backend in `blue.yaml`: + +```yaml blue/blue.yaml +control_api: + blob_storage: + bucket: os.environ/HARNESS_BLOB_BUCKET + region: us-west-2 + retention_days: 30 +``` + +For non-AWS S3-compatible storage, also configure its endpoint, public +endpoint, and path-style behavior as described in +[Blue YAML](/0.2.0/deployment/blue-yaml). The internal endpoint is used by the +service; any signed upload URL must be reachable from client machines. + +`retention_days` records each artifact's deadline. The current release does +not include a background object-deletion worker, so configure a bucket +lifecycle rule or an external cleanup job for actual deletion. + +## Enable capture in governance policy + +Add one top-level policy: + +```yaml +session_upload: + presign_url: https://control.example.com/session-uploads/presign +``` + +`session_upload` is global. Do not place it under an individual harness. On +reconciliation, each selected compatibility interval registers a native managed +hook when that interval supports the required lifecycle event. Other intervals +still reconcile policy and report that capture is inactive. Removing the +top-level block removes Blue-managed capture hooks without removing unrelated +user hooks. + +The installed Blue version chooses a tested compatibility profile such as +`claude-v2_0_12`. That profile is stored with captured metadata so the hook payload +and transcript lookup remain tied to the harness generation that produced it. +Unsupported versions still reconcile other policy, but warn that session +capture is inactive. + +## What is uploaded + +New clients upload `blue-session-bundle-v1`, a gzip-compressed tar containing `manifest.json` and adapter-approved regular files. The manifest records the schema, harness profile, native ID, capture time, working directory, available Git identity, native title or bounded prompt preview, and a SHA-256 and size for every file. Capture and restore reject symlinks, unsafe paths, duplicate entries, unsupported file types, oversized content, and digest mismatches. Older clients may still upload a single raw artifact; those records remain downloadable but are marked non-resumable. + +Bundles contain the native data needed by each harness: + +- Codex: rollout JSONL and native index-derived display metadata. +- Claude: primary JSONL, session-owned subagent transcripts, metadata, and externalized tool results. +- Kimi: sanitized `state.json`, agent wire histories, plans, and attachments. Credentials, approval grants, processes, cron jobs, queued goals, and logs are excluded. +- OpenCode: its native export-shaped session and messages JSON, restored through `opencode import`. + +## How titles and previews are generated + +Blue does not ask a model to name or summarize a session. It uses only metadata +and conversation data already written by the native harness, in this order: + +1. Find the record for the exact native session ID in the harness's native + session index or export metadata. ID fields such as `id`, `session_id`, and + `sessionId` are recognized so a supported profile can tolerate native naming + differences. +2. Prefer a native title or summary field. Recognized title fields include + `title`, `session_title`, `thread_name`, `name`, and `summary`. Recognized + preview fields include `summary`, `firstPrompt`, `prompt`, and `preview`. +3. If no native preview exists, scan the primary transcript for the first user + turn. The extractor accepts JSON documents and JSONL records, nested + `message` or `info` objects, and text represented as a string, content array, + parts array, or prompt field. +4. Skip Blue-injected environment and `AGENTS.md` context when choosing the + first-user fallback. Normalize whitespace, bound the stored preview to 1,024 + characters, and derive an 80-character title from it when no native title is + available. + +The manifest enforces a 256-character title bound even when a native index +contains a longer value. The dashboard applies a four-line visual clamp, while +the CLI renders a shorter row preview to keep the picker aligned. The complete +native transcript remains inside the bundle; title truncation does not modify +session content. + +## Compatibility profiles and native parsing + +Blue treats native session storage as a versioned adapter contract rather than +one format shared by every release. + +```mermaid actions={false} +flowchart LR + Detect[Detect installed harness version] --> Select[Select half-open version interval] + Select --> Profile[Record canonical compatibility profile] + Profile --> Capture[Resolve profile-approved native artifacts] + Capture --> Manifest[Store profile, roles, paths, and digests] + Manifest --> Restore[Select the stored profile during restore] + Restore --> Validate{Known and resumable?} + Validate -->|Yes| Native[Restore with harness-specific strategy] + Validate -->|No| Reject[Keep downloadable; do not advertise as resumable] +``` + +Version detection accepts each harness's registered version-output forms and +normalizes them to semantic versions. Registrations use half-open ranges: +`introduced` is inclusive and `before` is exclusive. Every range also has an +exclusive `verified_before` certification ceiling. A new native layout gets a +new profile and operation set; shipped profiles are retained so existing +bundles do not silently switch parsers when Blue is upgraded. + +The profile stored in the manifest controls validation of the source bundle. +An unknown profile, a profile without `session_resume`, an unexpected artifact +role, or a native path outside that harness's session root fails before native +files are written. Launch still goes through Blue's normal installed-version +and governance eligibility checks for the destination machine. + +| Harness | Version-aware handling | +| --- | --- | +| Codex | Portable capture begins with the profile whose native lifecycle supports `SessionEnd`. The bundle requires exactly one rollout under `.codex/sessions`; earlier captures stay downloadable but non-resumable. Display metadata is matched by native session ID before the rollout fallback is scanned. Restore places the rollout at its native path and invokes the explicit `codex resume ` path; it does not overwrite Codex database state. | +| Claude | Only profiles with a uniform portable capture contract are resumable. Blue captures the primary project JSONL and session-owned companion subtree, accepts the supported native index container shapes, and merges a new entry without replacing unrelated sessions. Older pre-hook and hooks-only profiles remain non-resumable. | +| Kimi | The adapter accepts the hook path or searches only the documented managed/native session roots for the exact session ID. It captures all agent histories, plans, and attachments, sanitizes `state.json`, and recognizes existing index IDs written as `session_id`, `sessionId`, or `id`. | +| OpenCode | Blue writes the native export shape `{info, messages}` instead of parsing OpenCode's private database. Restore delegates format interpretation to `opencode import`, reads the imported ID reported by the CLI, and resumes that ID. | + + + Compatibility profiles protect known storage layouts; they do not make an + old bundle compatible with every future vendor release. Certification tests + advance each profile's `verified_before` ceiling only after the locked native + CLI version has been exercised. + + +Blue calculates the completed archive's SHA-256 and byte size before requesting an upload. +The presign request includes: + +| Field | Purpose | +| --- | --- | +| `harness` | Canonical harness name. | +| `compatibility_profile` | Adapter used to interpret the lifecycle payload and locate the transcript. | +| `session_id` | Native harness session identifier. | +| `sha256` and `size_bytes` | End-to-end integrity checks. | +| `content_type` | Media type sent with the object. | +| `cwd` | Working directory attribution, when available. | +| `artifact_format` and `resumable` | Portable format and whether this profile supports native restoration. | +| `title` and `summary` | Bounded native title/summary or prompt preview; Blue does not invoke a model to create these. | +| Repository and capture time | Available Git root/remote identity and source timestamp. | + +The client uploads directly to object storage with the returned short-lived +method, URL, and signed headers. Completion succeeds only after the Control API +verifies object size and SHA-256 metadata. + +## Browse and download sessions + +PostgreSQL stores searchable session metadata and artifact history; object +storage contains raw bytes. Administrators see all sessions in their +organization for audit. Members see sessions they own or that an owner shared with them. The CLI resume picker is narrower for every role, including administrators: only owned or explicitly shared resumable sessions appear. + +The Sessions dashboard supports search by native title, summary/preview, session ID, or working directory and filters for harness, user, status, and update date. Owners may keep a session private, share it with all active workspace members, or select active recipients. Recipients can only view, download, and resume; they cannot edit, delete, or re-share. Revocation cannot remove a copy already restored locally. Status values +include `pending`, `complete`, `superseded`, and `failed`. Opening a session +shows its metadata and artifact history. Downloads use a new short-lived +presigned URL and are rejected after the retention deadline. + +## Resume a remote session + +Open Blue's control menu and run `/resume`. Blue opens a dedicated resume screen showing the native title, harness, owner/shared state, source path, and update time. If the recorded directory differs from the current directory, choose the current directory, the recorded directory when it still exists locally, or another existing destination. Blue downloads and verifies the bundle and checks compatibility, repository identity, and local-ID collisions while the current agent remains running. A repository mismatch has its own confirmation before the final active-session prompt. That final prompt defaults to **Keep current session**; resuming requires explicitly selecting **End current session and resume**. Blue then restores native files atomically, rebuilds supported native indexes, and launches the selected harness with its native resume arguments. Blue does not clone repositories or transfer working-tree changes, credentials, approvals, active processes, or scheduled work. + +```mermaid +sequenceDiagram + actor User + participant Blue as Blue supervisor + participant API as Control API + participant Blob as Object storage + participant Adapter as Stored compatibility profile + participant Agent as Native agent CLI + User->>Blue: Open /resume + Blue->>API: List owned/shared resumable sessions + API-->>Blue: Bounded metadata rows + User->>Blue: Select session and destination + Blue->>API: Request authorized download + API-->>Blue: Short-lived signed URL + Blue->>Blob: Download opaque bundle + Blob-->>Blue: Compressed bytes + Blue->>Adapter: Verify schema, roles, paths, sizes, and digests + Adapter->>Adapter: Preflight compatibility, indexes, and collisions + Blue-->>User: Prompt to keep current session or end it and resume + Blue->>Agent: Gracefully stop current agent + Blue->>Adapter: Restore files or call native importer + Adapter->>Agent: Launch native resume arguments +``` + +Cancellation and every failure through preflight leave the active agent +running. Blue never silently changes the working directory and never overwrites +a different local session with the same native ID. Codex and Claude resume by +native ID, Kimi launches with `--session`, and OpenCode launches the ID returned +by its native importer. + +## Rotation, retries, and duplicate uploads + +Uploading the same native session again can create a new artifact revision. +The newest verified artifact becomes current and older artifacts remain in +history until retention removes them. The worker verifies its own spooled bytes +before every upload and deletes local copies only after successful completion. + +The hook path is intentionally non-blocking. A successful agent shutdown does +not prove that remote registration completed; use the dashboard and service +logs for confirmation. + +## Disable capture + + + + Remove the top-level `session_upload` block and publish a new policy + revision. Clients remove Blue-managed hooks when they reconcile. + + + Decide whether clients should finish uploading already-spooled sessions or + remove them according to your incident and retention policy. A worker that + fetches policy after capture is disabled does not upload the record. + + + Removing policy does not delete PostgreSQL metadata or object-storage + artifacts. Apply your documented deletion workflow and bucket lifecycle. + + + +## Troubleshooting + +| Symptom | Check | +| --- | --- | +| No capture hook appears | Confirm `session_upload` is top-level, the harness is allowed, and its installed version has a compatible profile. | +| Hook reports no session ID | Inspect the native lifecycle payload and verify it matches the selected compatibility profile. | +| Presign fails | Verify login/session token, endpoint reachability, organization scope, and Control API storage configuration. | +| Object upload fails | Verify the signed URL is workstation-reachable and required signed headers are returned unchanged. | +| Completion fails | Compare object size and SHA-256 metadata with the presign request. | +| Record remains pending | Inspect the detached worker, Control API, and object-store logs; failed local spool records are retained. | +| Download is unavailable | Confirm the artifact is complete, visible to the caller, and inside its retention deadline. | +| Expired objects remain | Configure the required object-store lifecycle rule or external deletion job. | diff --git a/apps/docs/0.2.0/admin/user-management.mdx b/apps/docs/0.2.0/admin/user-management.mdx new file mode 100644 index 0000000..6aa4c35 --- /dev/null +++ b/apps/docs/0.2.0/admin/user-management.mdx @@ -0,0 +1,92 @@ +--- +title: "Manage users and invitations" +description: "Invite accounts, assign roles, suspend access, revoke sessions, and remove login access without deleting governance history." +icon: "users" +--- + +In password mode, Blue uses invitation-only onboarding. Administrators choose an email and role; the recipient follows the invitation link and sets a password. In OIDC mode, invitations are disabled and the identity provider provisions users and groups through SCIM. See [Identity provider provisioning](/0.2.0/admin/identity-provisioning). + +The reference implementation does not send email. Invitation links are built from `HARNESS_AUTH_PUBLIC_URL` and written to the Control API log; copy them from there, or connect that delivery boundary to SMTP or a transactional mail provider before relying on invitations in production. + +## Account lifecycle + +```mermaid +stateDiagram-v2 + [*] --> Pending: create invitation + Pending --> Active: accept and set password + Pending --> Pending: resend for 24 hours + Pending --> Canceled: cancel invitation + Active --> Suspended: suspend + Suspended --> Active: reactivate + Active --> Removed: delete access + Suspended --> Removed: delete access + Removed --> Pending: invite the same email + Pending --> Active: accept with a new auth identity +``` + +A removed user keeps the same governance user ID when the email is invited again. This preserves ownership of configuration revisions, client reports, and captured sessions across removal and reactivation. + +## User APIs + +All routes require an `admin` role and are restricted to the caller's organization. They accept either the HTTP-only dashboard session cookie or an audience-bound bearer token. + +| Method and path | Purpose | +| --- | --- | +| `GET /admin/users` | List users. Paginate with `page` and `per_page`; filter with `q` (email or subject), `role`, `status`, or `provisioning_source`. Returns `{items,page,per_page,total,total_pages}`. | +| `GET /admin/users/{id}` | Read one organization user. | +| `PATCH /admin/users/{id}` | Change `role` and/or set `status` to `active` or `suspended`. | +| `POST /admin/users/{id}/sessions/revoke` | Revoke all browser, OAuth, and device sessions. | +| `DELETE /admin/users/{id}` | Delete authentication access while retaining identifiable history. | + +```bash +curl -X PATCH "$CONTROL_API/admin/users/$USER_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"role":"admin"}' +``` + +Role changes and suspension invalidate existing browser and CLI credentials. Reactivation permits the existing password to be used again but does not restore old sessions. The deployment-managed bootstrap administrator is protected, and an organization must always retain at least one active administrator. Administrators cannot demote, suspend, or delete themselves. + +SCIM-managed users expose `provisioning_source: "scim"` and `managed: true`. Their role and lifecycle endpoints reject local changes; administrators retain session revocation as an emergency response. + +## Invitation APIs + +| Method and path | Purpose | +| --- | --- | +| `GET /admin/invitations` | List invitations, including accepted, canceled, and computed expired states. Paginate with `page` and `per_page`; filter with `status=outstanding`, `q` (email), or `role`. Returns `{items,page,per_page,total,total_pages}`. | +| `GET /admin/invitations/{id}` | Read one organization invitation. | +| `POST /admin/invitations` | Invite an email with an `admin` or `member` role. | +| `POST /admin/invitations/{id}/resend` | Renew a pending or expired invitation for 24 hours. | +| `DELETE /admin/invitations/{id}` | Cancel a pending invitation. | + +```bash +curl -X POST "$CONTROL_API/admin/invitations" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"email":"developer@example.com","role":"member"}' +``` + +The reference deployment writes the acceptance URL to the Control API log. Configure `HARNESS_AUTH_PUBLIC_URL` to the public dashboard origin so the emitted link is usable outside the container network. A production deployment can replace the log delivery boundary with its mail adapter. + +## Suspension and deletion + +Suspension retains the Better Auth account and organization membership but prevents authentication. It revokes credentials, removes the local gateway selection, and attempts to revoke the server-held LiteLLM virtual key. + +Deletion removes the Better Auth account, password, memberships, sessions, OAuth grants, pending device codes, credentials, and gateway selection. It intentionally retains: + +- the governance user ID, subject, email, role, and `removed` state; +- configuration revision authorship; +- client status and captured-session metadata; +- captured-session artifacts and their existing retention deadlines. + +Remote gateway-key cleanup is best effort. Gateway auth sessions are revoked transactionally, so governed clients lose gateway access even if the upstream gateway is unavailable. + + + `DELETE /admin/users/{id}` is access removal, not a privacy-erasure API. Organizations that require anonymization or historical-data deletion need a separate retention workflow. + + +## Dashboard workflow + +Open **Members** in the administrator sidebar. The page provides the same API operations for role changes, suspension and reactivation, session revocation, access deletion, and invitation resend or cancellation. Destructive actions require confirmation, and prohibited bootstrap, self, and last-administrator operations are disabled or rejected by the API. + +In managed OIDC mode, **Members** also shows an **Identity & provisioning** tab backed by `GET /admin/identity/status`. It summarizes OIDC and SCIM readiness without exposing secrets. diff --git a/apps/docs/0.2.0/cli/commands.mdx b/apps/docs/0.2.0/cli/commands.mdx new file mode 100644 index 0000000..f0cd325 --- /dev/null +++ b/apps/docs/0.2.0/cli/commands.mdx @@ -0,0 +1,142 @@ +--- +title: "CLI commands" +description: "Reference the public harness commands and their expected behavior." +icon: "terminal" +--- + +The binary is named `blue`. Run `blue --help` for the arguments supported by your installed version. + +## Install + +On a workstation with Codex, Claude, Kimi, or OpenCode already on `PATH`, install Blue: + + +```bash macOS and Linux +curl --proto '=https' --tlsv1.2 -LsSf https://github.com/BlocksOrg/blue/releases/latest/download/install.sh | sh +``` + +```powershell Windows PowerShell +irm https://github.com/BlocksOrg/blue/releases/latest/download/install.ps1 | iex +``` + + +The installer detects your platform, downloads the matching release asset, verifies it against the release's `SHA256SUMS`, and installs `blue` in your user path. You can [inspect the release assets and installers](https://github.com/BlocksOrg/blue/releases) before running them. + +| Variable | Effect | +| --- | --- | +| `BLUE_VERSION=0.2.0` | Install a specific release instead of the latest. | +| `BLUE_INSTALL_DIR=/path` | Install somewhere other than `~/.local/bin`. | +| `BLUE_UPDATE_PATH=0` | Do not add the default install directory to your shell profile. | + +Then connect to your organization's deployment: + +```bash +blue setup # enter the Control API URL from your administrator, then sign in +blue doctor # check installed agents and organization policy +blue codex # launch Codex through Blue; or claude, kimi, opencode +``` + +## Commands + +| Command | Behavior | +| --- | --- | +| `blue` | Set up or renew login when needed, reconcile policy, and launch the preferred agent. Hold Ctrl and press `]` for controls. On first run it asks which agent to use; an installed agent whose version the policy blocks is offered with a repair prompt rather than hidden. | +| `blue version` / `blue --version` | Print the installed Blue metaharness version. | +| `blue setup` | Clear the stored session, reconfigure the Control API connection through deployment discovery, and complete login. | +| `blue reset [--yes]` | Disconnect the active deployment, remove Blue-managed agent overlays, and retain non-secret tenant state for a later reconnect. | +| `blue login [--force]` | Verify the stored session with the service and, when it is missing or rejected, complete OAuth device authorization. `--force` safely retires an accessible session before re-authorizing. An inaccessible refresh token is replaced without signing out the user's other Blue sessions. | +| `blue logout` | Revoke the refresh family when reachable and remove local tokens. | +| `blue doctor` | Show detected harness paths, versions, and policy allow or deny state. | +| `blue agent [NAME]` | Choose the preferred agent used by bare `blue`. In an interactive terminal this also offers installed agents whose version needs repairing, and installs a policy-supported release before saving the choice. Without a terminal only eligible agents are accepted. | +| `blue status` | Compare desired/applied revisions and report managed-file and package health. | +| `blue verify` | Exit non-zero when policy or managed files are stale. | +| `blue config` | Print the resolved, personalized configuration and its source. | +| `blue apply [--yes]` | Reconcile the configured default agent. | +| `blue daemon [--interval N]` | Keep the default agent current and repair its package drift using TTL polling. | +| `blue gateway` | Provision or reconcile the current user's single managed gateway key. | +| `blue run NAME -- ARGS` | Reconcile and launch one governed native agent, notifying when a newer revision requires a restart. | +| `blue NAME ARGS` | Shorthand for `blue run NAME -- ARGS`. | +| `blue shim install [--dir PATH]` | Install command shims into `~/.local/bin` on Unix or `%LOCALAPPDATA%\Blue\bin` on Windows by default. | +| `blue shim uninstall [--dir PATH]` | Remove shims previously installed by Blue from the default or specified directory. | + +## Typical workflow + +```bash +blue login +blue doctor +blue agent codex +blue apply +blue verify +blue codex +``` + +## In-session controls + +Hold Ctrl and press ] while an agent is running. The persistent footer reports Control API connectivity as `checking`, `connected`, or `offline`. Run `/agent` to choose a new default from the interactive list. The current agent keeps running unless you then select **Quit and reload now**; otherwise the new default applies the next time Blue starts an agent. Commands that require confirmation show an interactive choice instead of accepting typed arguments. Run `/health` to check `database`, `blob_storage`, `package_storage`, and `inference_proxy`; the proxy check appears when the server supplies `gateway_inference_proxy_health_url`. Run `/version` to show the installed metaharness version. + +| Command | Behavior | +| --- | --- | +| `/status` | Show policy and managed-file status. | +| `/health` | Check Control API dependencies. | +| `/version` | Show the Blue metaharness version. | +| `/gateway` | Show gateway account status. | +| `/doctor` | Show detected agents and their policy state. | +| `/agent` | Choose the default agent and optionally quit and reload immediately. Agents needing a version repair are named but not selectable here; run `blue agent ` outside Blue to install one. | +| `/apply` | Review and confirm immediate reconciliation. | +| `/login` | Show identity or interactively confirm reauthentication when the agent must stop. | +| `/logout` | Interactively confirm sign-out. | +| `/reset` | Interactively confirm tenant reset, ending the active agent. | +| `/resume` | Pick an owned or shared portable session, verify it, then gracefully replace the active agent with the native resumed session. Escape or Ctrl-] returns to the active agent. | +| `/help` | Show the available slash commands. | +| `/quit` | Interactively confirm ending the agent and exiting Blue. | + + +## Reset a deployment + +Use reset when you want to disconnect this workstation from its current Blue deployment and connect to another tenant later. Use `blue logout` instead when you only need to change the signed-in account without changing deployments. + + + Reset ends the running agent and removes the active deployment's Blue-managed overlays and hooks. Your native agent configuration and its backups are not removed. + + +From a shell, run: + +```bash +blue reset +``` + +The confirmation defaults to No. For automation, pass `--yes`. From the Ctrl-] control screen, enter `/reset` to review the action and then `/reset yes` to confirm it. + +When you confirm, Blue: + +1. Identifies the tenant by its canonical Control API URL and saves its non-secret local state in a tenant-specific archive. +2. Retains your preferred agent, local mode preferences, governance cache, applied-state fingerprints, managed package data, merge approvals, MCP staging data, and queued session uploads. +3. Removes Blue-owned agent overlays, managed hooks, and package projections. Personal native agent configuration and backup files remain in place. +4. Attempts bounded remote gateway and OAuth cleanup without persisting any rotated credential, then always deletes the local session even when remote cleanup reports an error. Credentials are never put in the archive. +5. Removes the active `blue.toml` and governance cache so policy from the previous tenant cannot be used by a different tenant. + +Blue keeps a separate installation ID for each tenant under `~/.config/blue/identities` on Unix or `%APPDATA%\Blue\identities` on Windows. Reconnecting to the same tenant reuses its ID; connecting to another tenant creates a different one. See [Windows paths and migration](/0.2.0/cli/windows-paths) for the complete native layout. + +### Connect again + +After reset, start Blue normally: + +```bash +blue codex +``` + +Bare `blue` and interactive `blue ` launches detect that no deployment is active and prompt for a Control API URL. If the canonical URL matches a saved tenant, Blue automatically restores that tenant's non-secret state, refreshes its discovery metadata, starts a new login, and regenerates the removed managed overlays. Queued session uploads remain inactive until their tenant is restored. + +Entering a new URL starts with clean tenant state. Running `blue setup` while another tenant is active performs the same archive-and-detach transition before connecting to the new URL. + +## Exit behavior + +For an interactive terminal, the wrapper attaches the agent to a pseudo-terminal and forwards arguments, input, output, resize events, signals, and the native exit code. Redirected or piped execution uses native inherited handles without allocating a pseudo-terminal. Governance or reconciliation failures occur before launch and return a non-zero harness exit status. + +Package reconciliation is independent per package. Successful packages activate even if another package fails, and a failed update retains its last usable version when its adapter remains compatible. The command still exits non-zero and the affected harness cannot launch until every required package is active. + +Packages modified locally are never silently deleted. Removal unregisters them from the governed overlay and moves modified owned content beneath the metaharness package quarantine for inspection. + +## Automation + +Use `blue verify` in machine checks. Use `blue apply --yes` only when the organization has approved noninteractive merges; backups and atomic writes still apply. diff --git a/apps/docs/0.2.0/cli/harnesses.mdx b/apps/docs/0.2.0/cli/harnesses.mdx new file mode 100644 index 0000000..bfd0577 --- /dev/null +++ b/apps/docs/0.2.0/cli/harnesses.mdx @@ -0,0 +1,37 @@ +--- +title: "Supported harnesses" +description: "See how Codex, Claude Code, Kimi Code, and OpenCode receive governed launch overlays." +icon: "code" +--- + +Blue detects supported agents from `PATH`. It keeps the configured default agent reconciled and configures another allowed, installed agent just in time when you launch it through Blue. + +| Harness | Governed configuration strategy | +| --- | --- | +| Codex | A dedicated governed profile and managed configuration file; the native default profile is untouched. | +| Claude Code | Additional settings and MCP files passed only to the governed process. | +| Kimi Code | An isolated `KIMI_CODE_HOME` containing the governed overlay. | +| OpenCode | Explicit configuration and authentication overlay environment variables. | + +See [Managed configuration](/0.2.0/concepts/managed-configuration) for the exact files on each side of that split and for what a governed launch inherits from your own configuration. + +## Direct and governed launches + +```bash +codex # native configuration only +blue codex # organization overlay for this launch +``` + +Installing shims makes the familiar agent command resolve through `blue`. Remove the shims to restore direct resolution. + +## Managed Codex policy + +The example policy pins Codex to `gpt-5.6-sol`, sets reasoning effort to `low`, and disables Fast mode. When Fast mode is disabled, the overlay also selects the default service tier. + +## MCP reconciliation + +Remote MCP entries are suggestions keyed by name. The CLI adds a missing enabled entry but preserves all local entries. A local name collision wins, even when its command or URL differs from the remote suggestion. + +## Session hooks + +Each selected compatibility interval receives a session-upload hook when the global policy contains `session_upload` and that interval supports the required lifecycle event. Unsupported historical intervals still reconcile other policy and report that capture is inactive. Removing the top-level block removes Blue's managed hooks without touching unrelated hooks. diff --git a/apps/docs/0.2.0/cli/troubleshooting.mdx b/apps/docs/0.2.0/cli/troubleshooting.mdx new file mode 100644 index 0000000..b48136c --- /dev/null +++ b/apps/docs/0.2.0/cli/troubleshooting.mdx @@ -0,0 +1,81 @@ +--- +title: "Troubleshooting" +description: "Diagnose authentication, reconciliation, gateway streaming, and session upload failures." +icon: "wrench" +--- + +Start with these commands: + +```bash +blue doctor +blue status +blue verify +``` + +Set `HARNESS_LOG=debug` for more client diagnostics. + + + + In a Unix terminal, run `blue agent ` or `blue ` to approve a fresh installation. Bare `blue` and `blue agent` also list allowed absent agents with an installation hint. Blue shows the exact policy-supported version and npm command; confirmation defaults to No. npm must already be installed, with its global executable directory on PATH. Fresh installs use npm's configured global prefix, while repairs preserve the detected installation method and prefix. + + Blue verifies the PATH winner after installation. If it is missing, unparseable, or incompatible, fix PATH before retrying. Declining or failing installation does not save a new default or write managed agent configuration. Unattended calls refuse installation, and `/agent` inside the running UI directs you to the terminal command. Automatic fresh installation is unavailable on Windows; install a policy-supported release manually there. + + + Run the native CLI with `--version`, then use `blue apply`, `blue status`, or `blue run` to compare the inventory's normalized native harness version and compatibility result with the organization's `version_requirement`. `blue doctor` reports the configuration source, session, revision, allowed harnesses, and each detected path, version, and allowed/denied state, but not the compatibility profile. In an interactive terminal, approve Blue's installation-aware repair prompt (when the installation method supports repair) to install the highest published release inside the policy and certified range, or ask an administrator to change the range. The prompt is reachable from the first-run agent picker and from `blue agent ` as well as from `blue `: a blocked agent stays in those lists, labelled `installed — needs a policy-supported version`. + Interactive `blue apply` can also repair an installed incompatible stored default. `apply --yes`, piped apply, in-UI apply, and the daemon never run installers. For an absent default, first use `blue agent `. + + For Homebrew, legacy Kimi uv, or unrecognized installations, follow the short manual guidance. When supported, Blue prints a policy-constrained npm migration command. Install npm first and remove the old copy through its manager, or use a separate npm prefix and put its executable directory first on PATH. See [manual migration](/0.2.0/concepts/harness-version-compatibility#repair-an-incompatible-installation). Automatic replacement remains unsupported for these methods and for Windows layouts. + + + Blue repairs supported, recognized layouts: npm-compatible global packages at their detected prefix, native Claude through its detected binary, and default standalone Kimi/OpenCode installations through their native installers. Homebrew, legacy Kimi uv, and unrecognized layouts receive manual guidance; Blue never falls back to npm for an unsupported installation method. It may suggest a command for you to migrate manually; that command is not executed by Blue. Matching manually copied npm-compatible packages are eligible; recognition does not prove which installer created them. Native repair still needs npm for read-only exact-release lookup. + + After repair, Blue freshly resolves `PATH`. A changed compatible winner is used immediately. If the winner remains incompatible and a compatible copy is found later, the error names both files. Move the compatible directory earlier on `PATH`, or uninstall the shadowing entry through its owner. Blue does not remove copies automatically. If no compatible copy exists, the error reports the actual path/version and failed repair command. Compare these paths with `command -v claude` (or the affected harness). See [Repair an incompatible installation](/0.2.0/concepts/harness-version-compatibility#repair-an-incompatible-installation) for layout recognition evidence and manual-repair limits. + + + Compare the installed native harness version with the package adapter's variant ranges. Ranges must not overlap. Blue uses the adapter's fallback fields when no variant matches; a variant-only adapter must contain a matching range. The last usable package activation is retained while the affected launch remains blocked. + + + Run `blue verify` and inspect whether another process changes the managed overlay after reconciliation. Identical desired and current values are skipped; recurring prompts indicate a real file or revision difference. + + + Run `blue apply` to reconcile managed values, and confirm you are not invoking an installed shim. Governed settings should live only in launch-scoped overlays. + + + Confirm the server is declared in the configuration file you own rather than in a Blue-owned output, which is rebuilt from policy on every reconcile and discards hand edits. Personal servers belong in `~/.codex/config.toml`, `~/.claude.json`, `~/.kimi-code/mcp.json`, or `~/.config/opencode/opencode.json`. Run `blue apply` afterwards; Kimi Code seeds its isolated home at reconcile time, so an edit there reaches a governed session only after apply. See [Managed configuration](/0.2.0/concepts/managed-configuration). + + + The governance and LiteLLM users must have the same email. The deployment provisions and reconciles one managed key for that user; run `blue gateway` to retry it before fetching personalized policy. + + + Check the inference proxy and LiteLLM logs, confirm the proxy listens on the URL delivered by `blue config`, and verify that the managed key allows the configured model and wire protocol. + + + Confirm the global policy has a top-level `session_upload` block, apply the current revision, and finish a native agent session so its lifecycle hook runs. Then check Control API and object-storage logs for presign, upload, verification, and completion errors. + + + Run `blue status` to identify the package and harness. Confirm the archive URL is reachable, its configured SHA-256 matches the exact bytes, every adapter path exists, and a helper asset covers the current OS and architecture. A previous working version remains available when its adapter is still compatible, but the affected governed launch stays blocked until reconciliation succeeds. + + + `blue status` reports `public package redirects and environment proxies are unsupported` when a public package URL answers with a redirect. Blue downloads the recorded URL and never follows one. A GitHub release-asset URL always redirects to a short-lived signed URL, so the final URL is not a usable substitute; ask an administrator to republish the package from `github:owner/repository@ref` or a managed repository connection. See [public repositories and archives](/0.2.0/admin/managed-packages). + + + `package URL must resolve only to public addresses` means this client's resolver returned at least one non-public address for the package host; Blue rejects the whole answer set rather than falling back to the public addresses in it. Split-horizon or internal DNS that rewrites a public hostname causes this on an otherwise reachable host. The fix is a managed package source connection with its own host allowlist, not a client-side override. + + + Reconciliation detected local changes in metaharness-owned content and disabled the package without deleting those changes. Inspect the quarantine path reported by client status, preserve anything needed, and remove it manually when it is no longer useful. + + + +## Reset authentication + +```bash +blue login --force +``` + +For an accessible session, this rotates to its latest token generation, revokes its gateway and refresh sessions, removes the local credential, and then starts a new device authorization flow. Closing or declining that authorization leaves you signed out. It does not delete configuration overlays or captured sessions. + +`blue login` on its own already asks the service whether the stored session still works, so it re-authorizes without `--force` whenever the service rejects it. A temporary identity or Control API outage stops before opening a browser and preserves a usable local session for retry. + +If the old refresh credential is permanently invalid, Blue tries to revoke that session's gateway binding with its existing access token and then completes a normal replacement login. Other Blue sessions are not affected. An unauthorized cleanup response is treated as already revoked because dashboard-wide revocation invalidates both credentials together. Other cleanup failures produce a warning and the binding expires automatically. If the session may be compromised, ask an administrator to use **Revoke sessions** for the account before signing in again. + +To disconnect the deployment itself, use `blue reset`. Reset archives non-secret tenant state, removes active Blue-owned overlays, and prompts for a Control API URL the next time you run bare `blue` or an interactive `blue ` command. See [Reset a deployment](/0.2.0/cli/commands#reset-a-deployment). diff --git a/apps/docs/0.2.0/cli/windows-paths.mdx b/apps/docs/0.2.0/cli/windows-paths.mdx new file mode 100644 index 0000000..de7c2bd --- /dev/null +++ b/apps/docs/0.2.0/cli/windows-paths.mdx @@ -0,0 +1,37 @@ +--- +title: "Windows paths and migration" +description: "Find Blue configuration, local state, cache data, and command shims on Windows." +icon: "folder-tree" +--- + +Blue uses native Windows Known Folders. Redirected profiles and organization-managed Known Folder locations are supported; Blue does not assume they are beneath `%USERPROFILE%`. + +| Content | Default location | +| --- | --- | +| Configuration, login session, and tenant identities | `%APPDATA%\Blue` | +| Runtime overlays, packages, reconciliation state, and session capture data | `%LOCALAPPDATA%\Blue\Data` | +| Governance cache | `%LOCALAPPDATA%\Blue\Cache` | +| `blue.exe` and command shims | `%LOCALAPPDATA%\Blue\bin` | + +An explicit `XDG_CONFIG_HOME` replaces the `%APPDATA%` parent for Blue configuration. An explicit `XDG_CACHE_HOME` replaces the cache parent. Blue still keeps machine-local runtime state beneath the Local AppData Known Folder. + +The PowerShell installer does not change your persistent `PATH`. Add `%LOCALAPPDATA%\Blue\bin` to your user `PATH`, then open a new terminal. Run `blue doctor` to see the resolved shim directory, its `PATH` position, and the upstream harness executables Blue detects. + +## How Blue finds and starts an agent + +Blue resolves each agent the way `cmd.exe` does: it tries the bare name against every `PATHEXT` extension in turn. An npm global install writes three files — `codex`, `codex.cmd`, and `codex.ps1` — and only the `.cmd` is something Windows can execute, so that is the one Blue detects and runs. Blue starts a `.cmd` wrapper through the command interpreter, the same way `npm` and `node` do. + +One consequence: an argument the interpreter would act on rather than forward — one containing `%`, `&`, `|`, `<`, `>`, `^`, `(`, or `)` — is refused instead of being passed through altered. Spaces, quotes, and backslashes are unaffected. Give such an argument inside the agent, or install a native build of it. + +## Relocate legacy state + +Windows releases using the native layout do not read or automatically migrate `%USERPROFILE%\.config\blue`. If `blue setup` or `blue doctor` reports legacy state, close every Blue and coding-agent process before relocating files. + +1. Move `blue.toml`, `blue.yaml`, `session.json`, `mcp.json`, and `identities` to `%APPDATA%\Blue`. +2. Move `runtime`, `packages`, `package-state`, `package-state.json`, `.blue-transactions`, `locks`, `applied-state.json`, `merge-approvals.json`, and session capture directories to `%LOCALAPPDATA%\Blue\Data`. +3. Move `governance-config.json` to `%LOCALAPPDATA%\Blue\Cache`. +4. Run `blue doctor`, then `blue apply` and `blue verify`. + + + Do not copy junctions, symbolic links, or other reparse points into Blue-managed trees. Blue rejects them during reconciliation and session restoration. + diff --git a/apps/docs/0.2.0/concepts/architecture.mdx b/apps/docs/0.2.0/concepts/architecture.mdx new file mode 100644 index 0000000..06189ca --- /dev/null +++ b/apps/docs/0.2.0/concepts/architecture.mdx @@ -0,0 +1,75 @@ +--- +title: "Architecture" +description: "See how the CLI, dashboard, Control API, inference proxy, database, and blob storage interact." +icon: "diagram-project" +--- + +The repository contains one Rust CLI, a Rust Control API, a Rust inference proxy, and a Next.js administration dashboard. PostgreSQL stores identities, policy revisions, client state, gateway selections, and session metadata. Raw session bytes live in S3-compatible object storage. + +```mermaid +flowchart LR + Developer --> CLI[harness CLI] + Admin --> Dashboard + Dashboard --> Auth[Better Auth] + Dashboard --> API[Control API] + CLI --> Auth + CLI --> API + API --> Catalog[Package catalog] + API --> DB[(PostgreSQL)] + API --> Blob[(S3 or MinIO)] + CLI --> Agent[Codex / Claude / Kimi / OpenCode] + Agent --> Proxy[Inference proxy] + Proxy --> API + Proxy --> Gateway[Your LiteLLM gateway] +``` + +## Configuration delivery + +```mermaid +sequenceDiagram + participant Admin + participant Dashboard + participant API as Control API + participant CLI as harness CLI + Admin->>Dashboard: Save harness settings or publish extension changes + Dashboard->>API: PUT /admin/harnesses/{harness}/managed-config or PUT /admin/governance-extensions + API->>API: Validate and create revision + CLI->>API: GET /governance-config + API->>API: Verify OAuth sid and mint inference JWT + API-->>CLI: Personalized configuration + CLI->>CLI: Verify packages and merge launch-scoped overlays +``` + +The database revision is the source of truth for organization policy. Gateway `proxy_url` and `token` are not stored in that YAML: the Control API injects them into an authenticated user's response at delivery time, and does not persist the inference JWT. + +## Package ownership + +The package catalog supplies reviewed, immutable metadata; package archives remain external HTTPS artifacts. The client verifies each archive digest, rejects unsafe entries, and stores content by package ID and digest under the metaharness root. An ownership manifest records activation and recursive hashes. + +Harness adapters expose package content only through governed launches: Claude plugin arguments, a Codex governed profile, Kimi's isolated home and skill arguments, or OpenCode's explicit overlay. Teardown first removes those references, then deletes unchanged owned content or quarantines drifted content. + +## Gateway inference + +Blue's optional inference proxy connects governed agents to an upstream gateway operated by your organization. Blue does not bundle that gateway; LiteLLM is the first and currently supported adapter. + +```mermaid +sequenceDiagram + participant Agent + participant Proxy as Inference proxy + participant API as Control API + participant LiteLLM as Your LiteLLM gateway + participant Provider + Agent->>Proxy: Request with inference JWT + Proxy->>Proxy: Validate JWT + Proxy->>API: Resolve OAuth session + user + API-->>Proxy: Server-side virtual key + Proxy->>LiteLLM: Request with virtual key + LiteLLM->>Provider: Request with provider credential + Provider-->>Agent: Streamed response through both proxies +``` + +Neither the LiteLLM virtual key nor provider credentials are written to the developer machine. + +## Replaceable boundaries + +The reference backend is optional. Another service can replace it if it implements the published service contract and OAuth expectations. Blob uploads use provider-neutral presign responses even though the reference implementation uses the S3 API. diff --git a/apps/docs/0.2.0/concepts/authentication.mdx b/apps/docs/0.2.0/concepts/authentication.mdx new file mode 100644 index 0000000..0d90d73 --- /dev/null +++ b/apps/docs/0.2.0/concepts/authentication.mdx @@ -0,0 +1,60 @@ +--- +title: "Authentication and authorization" +description: "Learn how dashboard sessions, CLI device authorization, API scopes, and gateway identities are separated." +icon: "key" +--- + +The dashboard and CLI authenticate differently. Better Auth owns both flows, but a browser session cookie is never reused as a CLI credential. + +## Dashboard users + +The workspace runs in either local password mode or managed OIDC mode. The login page asks for email first, then either requests a password or redirects to the configured identity provider. Better Auth issues HTTP-only browser sessions and publishes signing keys for API token verification. OIDC mode permits only active, SCIM-provisioned identities plus the protected password-based bootstrap administrator. + +## CLI device authorization + +```mermaid +sequenceDiagram + participant CLI + participant Auth as Better Auth + participant Browser + participant API as Control API + CLI->>Auth: Request device code + Auth-->>CLI: User code and temporary browser URL + CLI->>Browser: Open temporary link automatically + Browser->>Auth: Confirm matching code and approve + CLI->>Auth: Poll token endpoint + Auth-->>CLI: Access and refresh tokens + CLI->>API: Audience-bound bearer token + API-->>CLI: Authorized response +``` + +The browser URL contains an opaque, short-lived token rather than the user code. The page displays the code read-only for comparison with the terminal and offers explicit authorize and deny actions. If the default browser cannot be launched, the CLI prints the link and continues polling. + +Access tokens expire after 15 minutes. Refresh tokens rotate and are stored with the session in `~/.config/blue/session.json` on Unix or `%APPDATA%\Blue\session.json` on Windows using owner-only permissions. Each refresh replays the resource indicator the grant was issued against, so the renewed token keeps the same Control API audience. `blue logout` attempts remote revocation and always removes the local token file. + +### Session binding and re-authorization + +The device grant is bound to the browser session that approved it, and gateway mode reads that binding every time it mints an inference token. The two lifetimes differ: the browser session lasts 12 hours, the refresh token 30 days. A CLI session can therefore keep refreshing its access token long after the sign-in behind it has expired, and the Control API answers `401` with the reason. + +`blue login` verifies with the service rather than trusting the local expiry, so it reports a rejected session instead of "Already logged in". When the deployment uses device authorization, it starts a new one; `blue` does the same at launch. Refresh, gateway cleanup, and identity requests have bounded timeouts. An unreachable identity or control service is reported as unverified and never triggers a browser flow; retry after the service recovers. + +Use `blue login --force` to re-authorize while a session is still valid — switching accounts, or replacing a session you no longer trust. Blue first rotates the old refresh token, revokes the gateway session with the resulting access token, revokes that latest refresh token, and removes the local session. Only then does it open the replacement authorization. Closing or declining that browser authorization leaves the workstation signed out. + +If the old refresh token is permanently invalid, Blue makes a best-effort attempt to revoke that session's gateway binding with the existing access token and then completes a normal replacement login. It does not revoke sessions on other machines. An unauthorized cleanup response is treated as already revoked because dashboard-wide revocation invalidates the access token and gateway session together. Other cleanup failures produce a warning and the binding expires automatically. For a potentially compromised session, an administrator can use **Revoke sessions** in the member menu to invalidate every browser, CLI, and gateway session for the account before the user signs in again. + +## Scopes + +| Scope | Purpose | +| --- | --- | +| `governance:read` | Read personalized governance policy and gateway key metadata | +| `session:write` | Presign and complete raw-session uploads | +| `client-status:write` | Report installed harnesses and reconciliation health | +| `gateway:resolve` | Resolve validated user/session pairs for the inference proxy's internal gateway requests | + +Administrative endpoints additionally require an administrator role in the same organization. Session visibility is organization-scoped; non-admin users see only their own captured sessions. + +User-management operations invalidate browser, OAuth, and device credentials when roles or access states change. The Control API also records a token cutoff and rejects previously issued bearer tokens, so an administrator claim cannot remain usable until its normal expiry. See [Manage users and invitations](/0.2.0/admin/user-management). + +## Gateway identity + +Gateway access matches the authenticated governance user to LiteLLM by case-insensitive email. The deployment provisions one managed key per user; a missing LiteLLM user produces a provisioning error. Run `blue gateway` to provision or reconcile it. diff --git a/apps/docs/0.2.0/concepts/configuration.mdx b/apps/docs/0.2.0/concepts/configuration.mdx new file mode 100644 index 0000000..ec4f968 --- /dev/null +++ b/apps/docs/0.2.0/concepts/configuration.mdx @@ -0,0 +1,51 @@ +--- +title: "Configuration lifecycle" +description: "Understand how an organization policy becomes a versioned revision, reaches a client, and is reconciled onto a machine." +icon: "sliders" +--- + +Configuration moves through four stages: an administrator saves an organization policy, the Control API versions it, the authenticated client receives a personalized response, and the CLI reconciles harness-specific overlays. + +## Policy and runtime values + +The dashboard owns managed harness settings, MCP entries, and extension packages. The mounted `blue.yaml` owns allowed harnesses, session-upload policy, the gateway section, and the optional curated package catalog. The Control API binds inference access to the source OAuth session while keeping the provisioned gateway credential server-side. + +```yaml +control_api: + gateway_request_logs: + retention_days: 30 + +gateway: + type: litellm + url: env://HARNESS_GATEWAY_URL + inference_proxy_url: env://HARNESS_INFERENCE_PROXY_URL + internal_allowed_client_id: blue-inference-proxy + secret_encryption: + provider: aws-kms + key_id: arn:aws:kms:us-west-2:123456789012:key/example + provisioner: + type: company-litellm + executable_path: /var/run/blue/provisioner/provisioner + executable_sha256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + policy_revision: company-litellm-v1 + +governance: + harnesses: + codex: + managed_config: + model: gpt-5.6-sol + reasoning_effort: medium + fast_mode: false +``` + +The Control API projects `gateway.type` into policy and adds `gateway.proxy_url` and the runtime-only `gateway.token` only to the delivered client response. Server credentials never enter the governance document. The token is a session-bound inference JWT and is never persisted by the Control API. The gateway has no model field; each agent uses its managed model when present or leaves model selection to the agent. `gateway.provisioner.type` selects the built-in LiteLLM implementation or the pinned executable. Gateway-specific key models, teams, budgets, resets, and API behavior belong in that implementation, not in `blue.yaml`. + +## Revisions and caching + +Every successful dashboard save creates a new immutable revision. Clients cache the most recently fetched response and honor `ttl_seconds`. A running governed wrapper watches the optional server-sent revision stream and displays a desktop notification when a restart is needed; older or BYO services fall back to a 60-second check. The active agent is never interrupted or rewritten. Login only stores authentication. Run `blue apply` or `blue daemon` to keep the default agent current; another agent is reconciled when explicitly launched through Blue. + +## Merge behavior + +Reconciliation never rewrites the configuration files you own, and it rebuilds the files Blue owns from scratch each time. See [Managed configuration](/0.2.0/concepts/managed-configuration) for which files fall on each side, what a governed launch inherits from yours, and the full list of merge rules. + +Version-aware reconciliation resolves the installed native CLI to a maintained renderer before any files are changed. See [Harness version compatibility](/0.2.0/concepts/harness-version-compatibility) for pinning, breaking boundaries, and package variants, or [Governance configuration](/0.2.0/reference/governance-config) for the complete shape. diff --git a/apps/docs/0.2.0/concepts/gateway-mode.mdx b/apps/docs/0.2.0/concepts/gateway-mode.mdx new file mode 100644 index 0000000..486c2a4 --- /dev/null +++ b/apps/docs/0.2.0/concepts/gateway-mode.mdx @@ -0,0 +1,222 @@ +--- +title: "Bring your own gateway" +description: "Connect your organization-operated gateway to Blue and understand routing, credentials, and setup." +icon: "route" +--- + +Gateway mode lets you bring your own organization-operated inference gateway. Blue supplies the governance-aware inference proxy and credential exchange; it does not bundle or operate the upstream gateway. LiteLLM is the first and currently only supported gateway type. Governance-only mode leaves inference untouched. Session capture is a separate feature and does not enable or require gateway mode. + +## Compare the modes + +| | Governance-only | Gateway mode | +| --- | --- | --- | +| Enabled by | Omitting top-level `gateway` | Adding one top-level `gateway` section | +| Inference route | Agent directly to its native provider or configured endpoint | Agent to Blue inference proxy, then your LiteLLM gateway, then provider | +| Client credential | The agent's native credential | A session-bound inference JWT | +| Provider and gateway keys | Managed by the developer's native agent setup | Remain in your server-side gateway deployment | +| Governance policy | Models, approvals, MCP, packages, and other managed configuration | The same policy plus launch-scoped gateway wiring | + +The mode is selected once for the deployment policy and applies to every allowed coding agent. + +## How gateway mode works + +```mermaid +sequenceDiagram + participant Agent + participant Proxy as Governance inference proxy + participant API as Control API + participant LiteLLM + participant Provider + Agent->>Proxy: Inference request + inference JWT + Proxy->>Proxy: Validate signature and claims + Proxy->>API: Resolve OAuth session + Blue user + API-->>Proxy: Proxy-only LiteLLM virtual key + Proxy->>LiteLLM: Request + virtual key + Proxy--)API: Request metadata (async) + LiteLLM->>Provider: Request + provider credential + Provider-->>Agent: Stream response through both proxies +``` + +The Control API matches the signed-in governance user to a LiteLLM user by exact, case-insensitive email. The deployment provisions one managed key per user according to provisioner policy. The shipped LiteLLM provisioners use `blue:` as the base key alias and add a numeric suffix if that alias is occupied. The Control API stores the resulting server-side virtual key only in PostgreSQL. + +When the client fetches policy, the Control API requires its OAuth token's `sid`, verifies the active backing Better Auth session, and injects the inference proxy URL plus a session-bound inference JWT into the global gateway block. Each new Blue launch receives a fresh JWT limited to `gateway:infer` and to `gateway.inference_jwt.token_ttl_seconds` (12 hours by default), never outliving the backing session. Fetching policy does not extend that session — the dashboard owns its lifetime. The JWT is never persisted by the Control API, and the client never writes it to its config cache. The wrapper writes agent-specific routing for allowed agents. The inference proxy validates the JWT, resolves its Blue user and OAuth session through the M2M channel, and substitutes the server-side virtual key. + + + Gateway routing applies when an allowed agent is launched through `blue`. Starting the native agent command directly uses its native configuration and credentials. + + +An already-running agent keeps its fixed launch expiry: the JWT is in the child process's environment and is not rotated in flight. Ten minutes before it expires, `blue` shows a countdown in the status row and offers to quit and reload the agent, which mints a fresh token. It never restarts on its own, because that would end an in-flight turn. + +If the browser session ends — you sign out of the dashboard, or it simply expires — the CLI's next policy fetch gets a 401 and `blue` says so directly: a **Session expired — sign in** banner, a prompt to stop the agent and sign in, and `blue doctor` reporting `session : EXPIRED`. Logging back in from a live browser session restores gateway access; JWTs minted before the logout stay rejected. + +Signing back in after `blue logout` reuses the same browser session, so gateway access resumes without a new browser sign-in. + +## How governance-only mode works + +With no top-level `gateway` block, Blue can still manage models, approval behavior, MCP servers, packages, and session-upload policy. It does not set gateway base URLs or inference tokens. Native agents own authentication and send inference to their normal providers or locally configured endpoints. + +For an entirely governance-only deployment: + +- Omit the top-level `gateway` section from `blue.yaml`. +- Do not start the Compose `gateway` profile. +- Configure provider credentials using each agent's native setup. + +When the organization offers gateway mode, open Blue control and run `/direct` to switch the local client to native provider credentials. Blue removes gateway routing and inference tokens from its managed agent profile while continuing to apply models, approval behavior, MCP servers, packages, and session-upload policy. If an agent is running, Blue offers to quit and reload it; keeping the current session delays the runtime change until the next Blue restart. Run `/direct` again to restore organization gateway routing. + +The selection is persisted as `force_governance_only` under `[mode]` in `blue.toml`. A client can only disable gateway routing offered by the service; it cannot enable gateway mode when the service has not declared it. + +## Enable gateway mode + +Gateway mode requires an organization-operated LiteLLM deployment, one Blue configuration section, and the Blue gateway services. The `litellm` adapter is the first supported integration. Contributors adding another type should follow [Gateway adapter architecture](/0.2.0/development/gateway-adapter-architecture) for the client, proxy, provisioner, and verification contracts. + + + + Run your LiteLLM gateway with provider credentials and make it reachable from the Blue services. Create users whose emails match their governance identities; Blue creates and maintains each user's managed key. + + + Add one top-level `gateway` section to `blue.yaml`. Its `type` enables routing for every allowed coding agent; the remaining settings connect the server runtime. + + + +Configure gateway mode once in `blue.yaml`: + +```yaml +gateway: + type: litellm + url: env://HARNESS_GATEWAY_URL + inference_proxy_url: env://HARNESS_INFERENCE_PROXY_URL + internal_allowed_client_id: blue-inference-proxy + inference_jwt: + issuer: https://api.blue.example.com + audience: blue-inference-proxy + private_key_file: /var/run/blue/gateway-jwt/signing-key.pem + secret_encryption: + provider: aws-kms + key_id: arn:aws:kms:us-west-2:123456789012:key/example + provisioner: + type: builtin-litellm +``` + +The inference proxy authenticates to the Control API's internal resolver with a +short-lived OAuth2 **client-credentials** token minted by better-auth (the +dashboard), not a static shared secret. Configure the proxy with +`HARNESS_PROXY_OAUTH_TOKEN_URL`, `HARNESS_PROXY_OAUTH_CLIENT_ID`, +`HARNESS_PROXY_OAUTH_CLIENT_SECRET`, and `HARNESS_PROXY_OAUTH_RESOURCE` (the +Control API audience). The same `HARNESS_PROXY_OAUTH_CLIENT_SECRET` is given to +the dashboard as `HARNESS_INFERENCE_PROXY_CLIENT_ID` / secret so it can seed the +confidential client. + +The internal transport is explicit. `mtls` is the default and recommended mode: +it encrypts resolved gateway credentials and requires the proxy to present a +trusted workload certificate. Mounted certificates rotate without restarting; +invalid rotations retain the last-known-good configuration. The Control API +also pins the token's `sub`, `client_id`, and `azp` claims to the configured +proxy client id. + +The proxy's client certificate and private key may arrive as one combined PEM +(`HARNESS_PROXY_CLIENT_IDENTITY_FILE`) or as a separate pair +(`HARNESS_PROXY_CLIENT_CERT_FILE` and `HARNESS_PROXY_CLIENT_KEY_FILE`), matching +the split files the Control API already takes. Configure exactly one of the two; +setting both, or only half of the pair, is a startup error that names the +offending variable. Rotating either file of a split pair is picked up by the same +30-second reload. + +The Helm chart can issue this pair for you with cert-manager, or mount +certificates from your own PKI. See [Internal certificates](#internal-certificates). + +`insecure-http` is available only as a deliberate trusted-network tradeoff. +OAuth M2M and NetworkPolicy isolation remain mandatory, but virtual keys are +plaintext on the wire and there is no client-certificate check. A VPC or an API +gateway at ingress does not encrypt this east-west hop. Account for compromised +workloads or nodes, packet capture, service misrouting, and policy enforcement +gaps before selecting it. + +| Setting | Purpose | +| --- | --- | +| `HARNESS_GATEWAY_TYPE` | Required inference-proxy adapter key; must match top-level `gateway.type` | +| `HARNESS_GATEWAY_URL` | Upstream inference gateway URL shown in the dashboard overview | +| `HARNESS_INFERENCE_PROXY_URL` | Client-reachable URL injected into personalized harness policy | +| Provisioner | In-process built-in or pinned deployment-trusted executable selected by `gateway.provisioner.type` | +| Secret encryption | Environment key for development or AWS KMS key ID for production envelope encryption | +| `HARNESS_PROXY_OAUTH_CLIENT_SECRET` | Client-credentials secret the inference proxy uses to obtain a Control API token (shared with the dashboard seed) | +| Gateway JWT key ring | Active RS256 private key, plus an optional previous key during a rotation. The Control API works out the published JWKS from them | +| `HARNESS_GATEWAY_JWKS_URL` | M2M-protected internal Control API endpoint used by the proxy to verify inference JWTs | +| `internal_allowed_client_id` | OAuth client id (`sub`) the Control API accepts on the internal resolver; defaults to `blue-inference-proxy` | +| `HARNESS_GATEWAY_LOG_URL` | Internal Control API request-log ingestion endpoint; omit to disable egress | +| `HARNESS_GATEWAY_EVENT_URL` | Internal credential-invalidation event stream; defaults from the resolver URL | +| `HARNESS_GATEWAY_REQUEST_LOG_RETENTION_DAYS` | Request metadata retention in days; defaults to `30` | + +Start the inference proxy and Control API, then verify that the user exists in the gateway. The dashboard **Gateway** page shows runtime readiness and the user's single managed key without exposing its credential. + +Users can provision or reconcile the key on that page or from the CLI: + +```bash +blue gateway +blue apply +blue codex +``` + +Harness launch performs the same lazy ensure operation. Personalized policy cannot be delivered until provisioning succeeds. + +### Internal certificates + +Keep `blue.internalTransport.mode: mtls` unless your threat model explicitly accepts plaintext east-west credentials. The processes check mounted certificate files every 30 seconds, adopt valid rotations without dropping in-flight requests, and retain the last-known-good configuration after an invalid update. + +#### Let the chart issue the certificates + +If cert-manager is installed in the cluster, set `blue.internalTransport.certManager.enabled=true` and skip the rest of this section. The chart declares the `Issuer` and `Certificate` resources, names the two Secrets after the release, and fills in the server SAN from the internal Service name, which are the two values a hand-written manifest gets wrong. cert-manager generates the keys and renews them; nothing secret goes into the release. + +```yaml +blue: + internalTransport: + mode: mtls + certManager: + enabled: true + # Omit issuerRef for a self-signed CA scoped to this release, or point it + # at your own PKI: + # issuerRef: { name: corporate-pki, kind: ClusterIssuer } +``` + +The issuer must be a CA-type issuer. Both sides read `ca.crt` out of their own Secret, and cert-manager writes that key only when the CA is known: true for `SelfSigned` and `CA` issuers, **false for ACME**. `issuerRef.kind` is required alongside `issuerRef.name`: cert-manager defaults it to `Issuer`, which looks only in the release namespace and leaves the Certificate stuck `Pending` if you meant `ClusterIssuer`. + +Under `certManager`, `serverSecret`, `clientSecret`, and `clientSecretFormat` are ignored. Installing cert-manager itself is out of the chart's scope: it is a cluster-scoped singleton with its own CRDs. + +Turning `certManager.enabled` on for a running mTLS install is a CA cutover, not an in-place upgrade. Both Secrets get new content from a new CA and they do not swap atomically, so expect a short window of rejected handshakes; the 30-second reload and last-known-good behaviour keep it from becoming an outage. + +#### Or bring your own certificates + +Set both `blue.internalTransport.serverSecret` and `blue.internalTransport.clientSecret`; empty names render unusable Secret volumes. + +The server Secret holds `ca.crt`, `tls.crt`, and `tls.key`. The client Secret holds `ca.crt` plus the proxy's own identity, in whichever of two layouts your issuer produces: + +| `blue.internalTransport.clientSecretFormat` | Client Secret keys | Use it when | +| --- | --- | --- | +| `combined` (default) | `ca.crt`, `client.pem` | You build `client.pem` yourself by concatenating the certificate and its key | +| `split` | `ca.crt`, `tls.crt`, `tls.key` | Your issuer emits the certificate and key as separate files | +| chart-issued (`certManager.enabled`) | `ca.crt`, `tls.crt`, `tls.key` | The chart declares the Certificates and cert-manager fills the Secrets; the layout is forced to `split` | + +Prefer `split` with an automated issuer. cert-manager, Vault, SPIRE, and `kubectl create secret tls` all write `tls.crt` + `tls.key`, so the Certificate needs no `additionalOutputFormats` stanza. That field is only on by default from cert-manager 1.15, and enabling it earlier takes a cluster-wide feature gate on both the controller and the webhook. `kubectl create secret tls` cannot produce a combined file at all. + +### Scaling the proxy + +The inference proxy holds long-lived streaming connections. Proxy autoscaling in the Helm chart requires an external metrics adapter that exposes `gateway_proxy_active_streams`, and the public ingress in front of the proxy must use a timeout long enough for a full streamed response. Drain proxy connections during rollouts. + +## Disable or change routing + +Remove the top-level `gateway` section to return all agents to governance-only behavior, remove the Gateway dashboard page, and stop gateway personalization. + +Changing provisioner configuration reconciles the managed credential and revokes a superseded gateway identifier. Run `blue apply` or let the daemon reconcile after policy changes. + +## Common failures + +| Symptom | Cause and action | +| --- | --- | +| Gateway page says runtime is incomplete | Set all four runtime settings and restart the Control API and inference proxy. | +| No matching gateway account | Create a LiteLLM user whose email matches the governance identity. | +| Managed key provisioning fails | Run `blue gateway` and inspect the provisioner and gateway logs. | +| Gateway unavailable | Verify the Control API and inference proxy can reach `HARNESS_GATEWAY_URL` and the admin key is valid. | +| Agent still connects directly | Launch it through `blue` and ensure the global `gateway` policy is present. | + + + Keep the LiteLLM admin key, inference-proxy OAuth secret, JWT signing key, virtual keys, and provider credentials out of governance YAML and client configuration. Only the session-bound inference JWT and client-reachable proxy URL are delivered to a developer machine. + diff --git a/apps/docs/0.2.0/concepts/harness-version-compatibility.mdx b/apps/docs/0.2.0/concepts/harness-version-compatibility.mdx new file mode 100644 index 0000000..aabb412 --- /dev/null +++ b/apps/docs/0.2.0/concepts/harness-version-compatibility.mdx @@ -0,0 +1,276 @@ +--- +title: "Harness version compatibility" +description: "Pin native harness versions and understand how Blue selects maintained config and extension translators." +icon: "code-branch" +--- + +Blue does not bundle agent harnesses. It detects the native CLI already on `PATH`, reads its version, and selects a compiled compatibility profile before it writes managed files or launches the process. When an installed version is incompatible, an interactive CLI can offer to run the catalog-defined vendor installer after explicit confirmation. + +## Reconciliation flow + +```mermaid +flowchart LR + A[Detect native CLI] --> B[Parse semantic version] + B --> C{Matches governance range?} + C -- No --> X[Block without writing] + C -- Yes --> D[Select compatibility profile] + D --> E[Select package adapter variant] + E --> F[Fetch and verify into staging] + F --> G[Validate affected-path plan] + G --> H[Commit under per-harness lock] + H --> I[Launch native CLI] +``` + +The selected profile owns the native representation of managed settings, MCP servers, gateway wiring, session hooks, skills, subagents, plugins, helpers, environment variables, and launch arguments. Governance and packages remain declarative inputs. + +## Pin an allowed range + +Set `version_requirement` under the harness policy. Blue uses semantic-version requirement syntax. + +```yaml +minimum_client_version: "0.1.0" +contract_version: 3 +required_capabilities: [adapter_intervals, compiled_harness_registry, transactional_reconcile, versioned_state] +harnesses: + codex: + version_requirement: ">=0.149.0, <0.150.0" + managed_config: + model: gpt-5.6-sol +``` + +An exact pin uses `=0.149.1`. Omitting the field allows any installed version covered by a maintained client profile. The Control API records `minimum_client_version` for rollout visibility and requires the corresponding capabilities, which prevent an older Blue client from silently ignoring the constraint. + +Every profile also has an exclusive certified release ceiling. A release tested through `0.151.0` uses `0.151.1-0` as its ceiling so prereleases of the next patch are blocked too. Blue blocks a newer release until that release is exercised and the ceiling advances. To deliberately accept a newer, untested release, constrain it explicitly and opt in: + +```yaml +required_capabilities: + - adapter_intervals + - compiled_harness_registry + - transactional_reconcile + - unverified_harness_versions + - versioned_state +harnesses: + codex: + version_requirement: ">=0.151.1, <0.152.0" + allow_unverified_versions: true +``` + +The opt-in is rejected without `version_requirement`. Reconciliation and inventory continue with a warning because vendor changes beyond the certified ceiling may produce invalid configuration. + + + A missing, unparsable, or policy-incompatible native harness version fails closed. Blue preserves the last managed state but does not rewrite files or launch that harness with a guessed format. + + +Run `blue apply` or launch through `blue run` to see the normalized version, selected profile, and compatibility error. Blue performs compatibility preflight only for the default or explicitly launched harness before changing its files. + +## Repair an incompatible installation + +In a terminal, `blue run` and `blue apply` identify the detected executable's +installation method before offering repair. The prompt shows the active path, +method, exact release, and command. The default answer is **No**. Blue never +falls back to another installer if the installation method is unrecognized or +unsupported, or if an installer fails. + +| Installation | Recognition evidence | Repair | +| --- | --- | --- | +| npm-compatible global layout (all four harnesses, Unix) | `/bin/` resolves to the expected package under `/lib/node_modules`; package name and `bin` target match the resolved executable, and the package directory equals its canonical path (no linked or redirected package directories) | `npm install -g --prefix @` | +| Claude native (Unix) | Native binary in `~/.local/share/claude/versions/`, reached through `~/.local/bin/claude`; relative symlink chains are resolved | Detected executable: `install ` | +| Homebrew Claude/Codex/OpenCode | Resolved target under the matching `Caskroom` or `Cellar` package, including custom prefixes | Refuse automatic arbitrary-version pinning; manually migrate to npm or use a compatible vendor/Homebrew installation | +| Kimi standalone (Unix) | Regular native binary at the documented default `~/.kimi-code/bin/kimi`, without redirected parent directories | Official installer with exact `--version`, `KIMI_INSTALL_DIR`, and `KIMI_NO_MODIFY_PATH=1`; the child PATH excludes the installation's bin so the installer skips legacy migration | +| Kimi legacy uv tool | Target belongs to a Python environment with `uv-receipt.toml` identifying `kimi-cli` and `pyvenv.cfg` | Refuse: legacy Python versions are not the current Kimi CLI; manually migrate to npm | +| OpenCode standalone (Unix) | Regular native binary at the default `~/.opencode/bin/opencode`, without redirected parent directories | Detected executable: `upgrade --method curl` | +| Codex standalone/release download, custom Kimi/OpenCode roots, pnpm/bun wrappers, Windows wrappers/native layouts, other unrecognized layouts | No supported installation layout | Refuse automatic replacement; manually install a policy-compatible release or migrate to npm | + +A manually copied package matching the npm-compatible layout is also eligible +for npm repair. Recognition uses only filesystem checks and does not prove npm +originally installed the package; detection does not run npm or any other command. + +The standalone installers leave no ownership receipt. Blue limits recognition +to their documented default layout and native executable format; arbitrary files +and release-style filenames do not establish provenance. Windows layouts remain +unsupported for automatic repair until equivalent layout checks are available. + +Supported repair methods currently require **npm for read-only release lookup**, +even for native installations. Blue resolves an exact published version with +`npm view` and rechecks the unchanged policy and certified range before mutation. +A missing npm executable or failed lookup stops repair; Blue does not bootstrap +npm. Unsupported methods and noninteractive runs do not query the registry. + +For unsupported methods, Blue prints a short explanation and a manual npm +migration command when the harness's install plan supports npm. For example, +Codex 0.154.0 installed through Homebrew under the default policy produces: + +```text +codex 0.154.0 is newer than Blue's tested versions. +Installation: Homebrew — /opt/homebrew/bin/codex +Blue cannot automatically install a specific version with Homebrew. + +To migrate to npm, remove this codex installation using Homebrew, then run (POSIX shell or PowerShell): + npm install -g '@openai/codex@>=0.145.0 <0.151.1-0' +Ensure npm's executable directory is on PATH, then retry Blue. +``` + +To migrate manually: + +1. Install npm first; Blue does not install it for you. +2. Remove the old installation through its manager (Homebrew for a Homebrew + installation, uv for legacy Python Kimi). Identify an unknown installation's + installer before removing it. Do not force npm to overwrite another manager's + executable or delete arbitrary binaries. +3. Run the command Blue prints in a POSIX shell or PowerShell, using the selector + from your current policy. These examples are not cmd.exe syntax. npm selects + a published release within that range when you run it; automatic repair + instead resolves and validates an exact version before confirmation. +4. Put npm's executable directory first on the PATH used to launch Blue, then + retry. Global executables live in `/bin` on Unix and `` on + Windows. If retaining the old copy, use a separate npm prefix (add + `--prefix '/your/separate/prefix'` to the printed command) and put that prefix's + executable directory before the old copy. Avoid a shared executable directory. + +The selector intersects the organization's policy with Blue's supported and +certified range, including any explicit unverified-version opt-in. If there is no +compatible intersection, ask an administrator to choose a supported range or +update Blue. Do not substitute `latest` for the printed selector. + +This advice is manual: Blue does not query npm, run the migration, or change PATH +on refusal paths. Migration does not enable automatic repair of Windows layouts. +For otherwise supported installations in a noninteractive run, Blue prints the +existing method's manual repair instructions, preserving the detected npm prefix +or native installer settings. + +Detailed detector reasons, resolved paths, and compatibility profile information +are available with `HARNESS_LOG=debug`. See +[npm install](https://docs.npmjs.com/cli/v11/commands/npm-install/) for range +selectors and [npm folders](https://docs.npmjs.com/files/folders/) for global +executable placement. + +After installation, Blue freshly resolves `PATH` and validates its first upstream +executable. A compatible winner is used immediately, including when its path +changed. If an incompatible winner shadows a compatible copy, the error names +both paths: put the compatible directory earlier on `PATH`, or uninstall the +shadowing copy through its owner. Blue does not remove files or silently launch +a later candidate. If no compatible copy is found, the error reports the actual +winner and version. Reconciliation, inventory refresh, and saved defaults proceed +only after successful verification. + +Vendor references: [Claude setup](https://code.claude.com/docs/en/setup), +[Claude CLI](https://code.claude.com/docs/en/cli-reference), +[Codex installation](https://github.com/openai/codex#installing-and-running-codex), +[OpenCode CLI](https://opencode.ai/docs/cli/), +[OpenCode installer](https://github.com/anomalyco/opencode/blob/dev/install), +[Kimi installer](https://code.kimi.com/kimi-code/install.sh), +[Kimi migration](https://www.kimi.com/code/docs/en/kimi-code-cli/guides/migration.html), +[uv tools](https://docs.astral.sh/uv/guides/tools/), and +[Homebrew FAQ](https://docs.brew.sh/FAQ). Automatic Homebrew pinning is a limit +of Blue's repair implementation. + +Blue-launched sessions suppress each harness's native update check or automatic +updater whenever the effective policy has a ceiling. That includes an explicit +maximum in `version_requirement` and the profile's certified ceiling when +`allow_unverified_versions` is false. Native updates remain enabled only when an +administrator opts into unverified versions with an explicit range that has no +upper bound, such as `>=0.151.1`. This permits an updater to install a release +that Blue has not certified, so reconciliation continues to emit the unverified +version warning. + +Update suppression is scoped to the managed launch: running the native harness +directly retains the user's normal update behavior, and an external package +manager can still replace the installed binary. The selected compatibility +version's atomic operation set owns this control, so a later harness version can +adopt a changed vendor mechanism without altering older intervals. + +Daemon runs, redirected/non-interactive commands, and `blue apply --yes` never install native CLIs. They fail closed and print installation-aware manual guidance for an operator. + +## Breaking-version profiles + +Each harness has an ordered registry of compiled profiles. Every profile has an inclusive `introduced` bound, an optional exclusive breaking-generation `before` bound, and a mandatory exclusive `verified_before` release ceiling. Dispatch may remain open-ended, but certified support never is: a parseable release at or beyond `verified_before` is blocked unless the organization uses the explicit unverified opt-in above. + +For example, introducing a profile at `2.0.0` produces these paths: + +| Installed version | Selected path | +| --- | --- | +| `<2.0.0` | Existing profile retained for older installations | +| `>=2.0.0` and below its certified ceiling | New breaking-generation profile | +| At or above the certified ceiling | Blocked, or warned with explicit policy opt-in | + +Maintainers add the boundary and version specification together, retain the older specification and operation set, and add golden output tests at the versions immediately below and at the boundary. A profile may later be marked deprecated, which continues to work with a warning. Removing it is an explicit Blue release change. + +## Versioned package layouts + +A package's normal adapter fields are its fallback layout. Use `variants` only when the archive exposes different paths for different native harness versions. + +Use adapter-level `introduced` and `before` bounds when the package itself is +available only for part of the harness's history. Blue rejects enabling the +package when the organization harness policy extends outside that availability +range, so administrators can either narrow the harness policy or disable the +package for that harness. + +```yaml +adapters: + codex: + introduced: 1.0.0 + before: 3.0.0 + variants: + - introduced: 1.0.0 + before: 2.0.0 + skills_dir: toolkit/codex-v1 + - introduced: 2.0.0 + before: 3.0.0 + plugin_dir: toolkit/codex-v2 +``` + +Adapter availability and variant intervals are inclusive at `introduced` and exclusive at `before`. Variants must stay inside adapter availability, be ordered, and not overlap. Gaps inside availability are allowed only when the adapter has a fallback layout. + +## Transaction and rollback guarantees + +Blue resolves every harness generation and constructs exact file bodies, modes, removals, environment variables, and launch arguments in memory before active files change. It then validates the plan, snapshots affected paths, and commits under per-harness advisory locks. Package and compatibility state are committed in the same transaction. A failed renderer, package activation, or state commit restores the snapshot; if a later required harness fails, the outer revision transaction restores earlier harness commits too. + +If package selection, validation, download, helper collision, or installation fails, the whole previous package activation remains recorded. Blue activates that last-known-good content only when the stored adapter interval still contains the installed native harness version. Otherwise it retains the content inactive and blocks launch. Obsolete paths are removed only after a successful commit and only when the ownership manifest identifies them as Blue-owned. + +Package and compatibility state files include schema versions. Unknown future schemas fail closed. + +## Maintainer checklist + +To add a generation for an existing harness: + +1. Add a complete `VersionSpec` that reuses the harness family's operation set for unchanged behavior and explicitly marks optional capabilities supported or unsupported. +2. Register its contiguous `[introduced, before)` interval and an exclusive `verified_before` ceiling in order. +3. Add vendor version-output fixtures and golden output immediately below and at the boundary. +4. Advance the current certified ceiling when the E2E harness lock is updated; registry tests require a `next-patch-0` ceiling immediately beyond that pinned stable release. + +If paths, rendering, inspection, package placement, gateway wiring, transcript discovery, installation, or launch behavior changes, add a named atomic operation set in the family module and select it from the new spec. Rust uses this composition in place of class inheritance: shipped specs remain immutable, and a new version can replace behavior without copying the whole adapter. + +To add a harness, add one `gh_common::harness_catalog!` entry and its initial `adapters//` family implementation and version spec. The catalog generates harness identity, enumeration, discovery metadata, and the `gh-config` definition registration. Production API schemas accept catalog keys rather than repeating a closed enum; docs and E2E matrices remain explicit evidence. To support a breaking vendor version, add one specification and one contiguous interval registration. Registry tests enforce unique keys and aliases, valid ordered probes, ordered lifecycle metadata, certified ceilings, and exactly one implementation per interval. Deprecate an interval by changing its lifecycle to `deprecated`; remove it only in an announced client release after policies and package intervals no longer target it. The complete file-level workflow is in [Harness adapter architecture](/0.2.0/development/harness-adapter-architecture). + +Package variants select verified archive content only. They cannot send executable translation logic from the server: the selected compiled compatibility profile still decides how to load every component. + +## Profile transition cleanup + +Blue records the selected profile, normalized version, and generated paths in harness-owned compatibility state. When the selected profile changes, Blue writes the new overlay first and removes stale files only when they were recorded previously and remain inside Blue-owned runtime locations. Native user configuration and untracked files are never cleanup targets. + +See [Governance configuration](/0.2.0/reference/governance-config) for the complete schema and [Troubleshooting](/0.2.0/cli/troubleshooting) for reconciliation diagnostics. + +## Install an absent agent + +In a Unix terminal, `blue `, bare `blue`, and `blue agent` offer +policy-allowed agents that are absent from PATH. Missing agents appear after ready +agents and repair candidates, labelled `not installed — install a policy-supported version`. +Blue resolves an exact npm release inside both the policy and Blue's supported +range, then asks for confirmation with **No** selected by default. The prompt +shows the exact version and `npm install -g @` command. + +Fresh installation uses npm's configured global prefix. npm must already be +installed and its global executable directory must be on PATH. Blue does not +bootstrap npm, change PATH, add sudo, or retry with another installer. Existing +installations continue to use installation-aware repair and retain their owner +and prefix. After either operation, Blue validates the actual PATH winner before +launching or saving a default; an installer exit code alone is not enough. + +Unattended calls never install an absent agent. Inside the running UI, `/agent` +only selects ready agents and directs you to `blue agent ` outside the UI +for installation or repair. Automatic fresh installation is unavailable on Windows. + +Interactive `blue apply` can repair an installed incompatible default with +confirmation. `apply --yes`, piped calls, in-UI apply, and the daemon do not run +installers. An absent default must first be installed through `blue agent `; +apply never installs every missing allowed agent. diff --git a/apps/docs/0.2.0/concepts/managed-configuration.mdx b/apps/docs/0.2.0/concepts/managed-configuration.mdx new file mode 100644 index 0000000..a021ced --- /dev/null +++ b/apps/docs/0.2.0/concepts/managed-configuration.mdx @@ -0,0 +1,80 @@ +--- +title: "Managed configuration" +description: "See which agent configuration files you own, which ones Blue owns, and what a governed launch inherits from your own configuration." +icon: "layer-group" +--- + +Every supported agent ends up with two configurations: the one you already own, and a launch-scoped one Blue generates from the organization policy. A direct `codex` or `claude` launch reads only your own files. A governed `blue codex` launch adds Blue's generated configuration on top, for that launch only. + +## What you own and what Blue owns + +| Harness | Files you own | Files Blue owns | How the managed layer reaches the agent | +| --- | --- | --- | --- | +| Codex | `~/.codex/config.toml` | `~/.codex/blue.config.toml` | Blue launches Codex with `--profile blue`. | +| Claude Code | `~/.claude/settings.json` and `~/.claude.json` | `runtime/claude/settings.json` and `runtime/claude/mcp.json` | Blue passes `--settings` and `--mcp-config` to the governed process. | +| Kimi Code | `~/.kimi-code/config.toml` and `~/.kimi-code/mcp.json` | `runtime/kimi/` | Blue points `KIMI_CODE_HOME` at the generated directory. | +| OpenCode | `~/.config/opencode/opencode.json` and `~/.local/share/opencode/auth.json` | `runtime/opencode/opencode.json` and its sibling package directories | Blue exports `OPENCODE_CONFIG_CONTENT` and `OPENCODE_CONFIG_DIR`. | + +Paths shown as `runtime/…` are relative to Blue's managed data directory, which is `~/.config/blue` on macOS and Linux and `%LOCALAPPDATA%\Blue\Data` on Windows. See [Windows paths and migration](/0.2.0/cli/windows-paths) for the full Windows layout. + +Only Codex places a Blue-owned file next to your own configuration, and it is a separate file with a separate profile. Blue never writes into the files in the first column. + +## What a governed launch inherits + +Blue reads your files; it does not copy them into the managed overlay. What each harness inherits differs. + +For Codex, the governed launch selects the separate `blue` profile, so the organization's model, approval, and sandbox settings come from `~/.codex/blue.config.toml` rather than from your own file. Blue reads your `~/.codex/config.toml` for two things while generating that profile: the names under `[mcp_servers]`, and your `[hooks]` table. A managed MCP entry whose name already exists in your file is skipped entirely, so your command or URL wins even when it differs from the organization's suggestion. Your hooks are copied into the managed profile so they keep running under a governed launch. + +For Claude Code, the generated settings and MCP files are passed as extra launch arguments, so Claude merges them with `~/.claude/settings.json` and `~/.claude.json` at startup. + +For OpenCode, the generated configuration arrives through environment variables and lands in OpenCode's own layered configuration, above your global `~/.config/opencode/opencode.json`. + + + Kimi Code works differently. Blue builds an isolated `KIMI_CODE_HOME` and seeds it from your native `~/.kimi-code/` files when it reconciles, rather than layering at launch. An edit to your own Kimi files therefore reaches a governed session only after the next `blue apply`. + + +A direct launch of the native CLI is unaffected in every case. Nothing Blue generates is visible to it. + +## What Blue regenerates + +Blue rebuilds each owned output from scratch on every reconcile. The generated file is composed from the current policy revision and the parts of your configuration listed above, then written atomically over the previous one. + + + Do not hand-edit a Blue-owned file. Your changes are discarded the next time `blue apply` or the daemon reconciles. Put the change in the file you own instead, or ask an administrator to change the organization policy. + + +The one exception is Codex's `hooks.state` table, where Codex records the hashes of hooks you have approved. Blue carries that table across a rebuild so an unchanged hook stays trusted; a hook whose contents changed still has to be approved again. + +Run `blue verify` to check whether the owned outputs match the current policy revision, and `blue status` to see which harnesses reconciled. + +## Add your own MCP server + +Add the server to the file you own, including any token, and reconcile: + +```toml +# ~/.codex/config.toml +[mcp_servers.internal-search] +command = "npx" +args = ["-y", "@example/internal-search-mcp"] +env = { SEARCH_API_TOKEN = "..." } +``` + +```bash +blue apply +``` + +The entry is yours, so it is never rewritten, backed up, or removed, and the token never leaves your machine. If the organization policy also ships an MCP server named `internal-search`, the collision resolves in your favor and Blue omits the managed entry. + +The same holds for the other harnesses: put personal servers in `~/.claude.json`, `~/.kimi-code/mcp.json`, or `~/.config/opencode/opencode.json`. Kimi Code needs a `blue apply` after the edit before the new server appears in a governed session. + +## Merge behavior + +- Existing native user configuration remains authoritative outside governed launches. +- Remote MCP entries are additive. A local MCP with the same name wins and is never overwritten or removed. +- Existing configuration changes prompt for review unless `--yes` or the explicit noninteractive policy allows them. +- Identical reconciliations do not prompt again. +- Writes are atomic, and changed files receive recoverable backups. +- Package archives are SHA-256 verified and activate independently. An affected governed launch fails closed when one of its required packages cannot activate. +- Removed managed packages are unregistered; modified owned content is quarantined instead of deleted. + +For the per-harness overlay strategy see [Supported harnesses](/0.2.0/cli/harnesses), for how a policy revision reaches your machine see [Configuration lifecycle](/0.2.0/concepts/configuration), and for the complete policy shape see [Governance configuration](/0.2.0/reference/governance-config). diff --git a/apps/docs/0.2.0/deployment/blue-yaml.mdx b/apps/docs/0.2.0/deployment/blue-yaml.mdx new file mode 100644 index 0000000..81bf978 --- /dev/null +++ b/apps/docs/0.2.0/deployment/blue-yaml.mdx @@ -0,0 +1,421 @@ +--- +title: "Configure blue.yaml" +description: "Author the unified deployment configuration for runtime services, governance policy, gateway routing, and managed package catalogs." +icon: "file-code" +--- + +`blue.yaml` is the source-controlled configuration for one Blue deployment. The binary default for `BLUE_CONFIG_FILE` is `blue.yaml`, but every documented container deployment uses the image's fixed `/etc/blue/blue.yaml`; mount the file there. + +The file combines server runtime configuration and the organization policy seed, but Blue does not send the complete file to clients. + +| Top-level section | Purpose | Delivered to clients | +| --- | --- | --- | +| `control_api` | Database, identity, storage, retention, and managed-repository runtime | No | +| `gateway` | Optional inference gateway, proxy, encryption, and provisioner runtime | Only a personalized gateway projection; never server secrets | +| `governance` | Baseline policy for allowed agents, managed configuration, packages, and session capture | Yes, after organization and user personalization | +| `package_catalog` | Curated extension choices shown to administrators | No; a package enters governance only after an administrator selects it | + + + Treat the repository containing `blue.yaml` as deployment configuration, not secret storage. Commit environment references and immutable policy values. Put credentials in your secret manager and expose them to the workload through `blue.existingSecret` or your existing secret delivery system. + + +## How values are resolved + +Runtime settings use this precedence: + +1. A non-empty direct environment variable, such as `HARNESS_DATABASE_URL`. +2. The corresponding scalar in `blue.yaml`. +3. A built-in default, where the setting has one. + +A YAML scalar can reference an environment variable with either supported form: + +```yaml +database_url: os.environ/HARNESS_DATABASE_URL +``` + +```yaml +database_url: env://HARNESS_DATABASE_URL +``` + +`os.environ/NAME` is used by generated exports and examples. `env://NAME` is accepted for compatibility. Startup fails when a referenced variable is absent. Boolean and numeric settings may be native YAML scalars or environment-backed strings. + +Managed repository credential fields also accept `file://absolute/path`, which is useful with secret-store CSI mounts: + +```yaml +private_key: file:///var/run/secrets/blue/github-app.pem +``` + +`file://` is not a general reference form for every runtime field. Use it for managed repository connection fields and `gateway.secret_encryption.key`. + +## Production starting point + +The deployment bundle includes a minimal `blue.yaml`. A production-oriented governance-only configuration has this shape: + +```yaml +control_api: + listen: 0.0.0.0:8080 + database_url: os.environ/HARNESS_DATABASE_URL + + bootstrap: + organization_slug: engineering + organization_name: Engineering + admin_subject: bootstrap-admin + admin_email: os.environ/HARNESS_BOOTSTRAP_ADMIN_EMAIL + + auth: + public_url: https://blue.example.com + session_url: http://blue-blue-dashboard:3000/api/auth/get-session + jwks_url: http://blue-blue-dashboard:3000/api/auth/jwks + issuer: https://blue.example.com/api/auth + audience: https://api.blue.example.com + client_id: blue-cli + + identity: + mode: password + + blob_storage: + bucket: os.environ/HARNESS_BLOB_BUCKET + region: us-west-2 + presign_ttl_seconds: 300 + retention_days: 30 + + package_artifacts: + bucket: os.environ/HARNESS_PACKAGE_BUCKET + + gateway_request_logs: + retention_days: 30 + +governance: + revision: "2026-08-30.1" + contract_version: 3 + required_capabilities: + - adapter_intervals + - compiled_harness_registry + - transactional_reconcile + - versioned_state + minimum_client_version: "0.1.0" + ttl_seconds: 300 + required: true + allowed_harnesses: [codex, claude, kimi, opencode] + harnesses: + codex: + managed_config: + model: gpt-5.6-sol + reasoning_effort: medium + approval_policy: on-request + sandbox_mode: workspace-write + claude: + managed_config: {} + kimi: + managed_config: {} + opencode: + managed_config: {} +``` + +New production baselines use `required: true`, so clients refuse to launch from +an expired cache when the governance service is unavailable. Existing revisions +and an explicitly authored `required: false` remain unchanged during upgrades. + +The Helm values configure the public dashboard and API domains separately. Keep those values consistent with `control_api.auth`: + +```yaml +blue: + publicUrls: + dashboard: https://blue.example.com + controlApi: https://api.blue.example.com + existingSecret: blue-runtime +``` + +## Configure `control_api` + +### Process and bootstrap settings + +| Field | Required | Meaning | +| --- | --- | --- | +| `listen` | No | Control API bind address. The image and chart expect `0.0.0.0:8080`. | +| `internal_listen` | No | Internal resolver bind; defaults to `127.0.0.1:8082` and must be `0.0.0.0:8082` for cross-pod gateway mode. | +| `database_url` | Yes | PostgreSQL connection URL. Prefer `os.environ/HARNESS_DATABASE_URL`. | +| `database.max_connections` | No | Control database pool size; defaults to `30`. Gateway provisioning requires at least `2`. | +| `gateway_request_logs.max_connections` | No | Request-log database pool size; defaults to `5`. | +| `run_background_jobs` | No | Run cleanup, reconciliation, and other background loops; defaults to `true`. | +| `gateway.kms_max_concurrency` | No | Maximum concurrent gateway KMS operations; defaults to `32`. | +| `gateway.kms_timeout_seconds` | No | Gateway KMS operation timeout; defaults to `3`. | +| `bootstrap.organization_slug` | No | Stable identifier for the initial organization. Do not casually change it after deployment. | +| `bootstrap.organization_name` | No | Initial display name for the organization. | +| `bootstrap.admin_subject` | No | Stable subject for the protected bootstrap administrator. | +| `bootstrap.admin_email` | No | Email used to initialize the bootstrap administrator. | + +Bootstrap values initialize a new database. They are not a declarative user-management system for existing organizations; manage members and invitations through the dashboard or SCIM after startup. + +### Authentication endpoints + +| Field | Example | Meaning | +| --- | --- | --- | +| `auth.public_url` | `https://blue.example.com` | Browser-reachable dashboard origin used for invitations and device authorization. | +| `auth.session_url` | `http://blue-blue-dashboard:3000/api/auth/get-session` | Better Auth session endpoint reachable from the Control API. Helm names this `-blue-dashboard`; this example uses release name `blue`. Loopback applies only when both processes share one container. | +| `auth.jwks_url` | `http://blue-blue-dashboard:3000/api/auth/jwks` | Signing-key endpoint reachable from the Control API. Helm names this `-blue-dashboard`; this example uses release name `blue`. | +| `auth.issuer` | `https://blue.example.com/api/auth` | OAuth issuer expected in CLI access tokens. | +| `auth.audience` | `https://api.blue.example.com` | Public Control API origin used as the token audience and discovery URL. | +| `auth.client_id` | `blue-cli` | Public OAuth client identifier used by workstation clients. | + +`BETTER_AUTH_URL`, `BETTER_AUTH_SECRET`, bootstrap password, and OIDC provider credentials are dashboard runtime variables supplied through the Helm secret; they are not replacements for the endpoint contract above. + + + Changing issuer, audience, client ID, or `BETTER_AUTH_SECRET` on an existing deployment invalidates authentication assumptions and may invalidate sessions or signing material. Treat those changes as planned identity migrations. + + +### Identity lifecycle + +```yaml +control_api: + identity: + mode: oidc + scim_bearer_token: os.environ/HARNESS_SCIM_BEARER_TOKEN + group_role_mappings: os.environ/HARNESS_SCIM_GROUP_ROLE_MAPPINGS +``` + +`mode` accepts `password` or `oidc`. OIDC mode requires a SCIM bearer token because Blue uses directory provisioning rather than just-in-time user creation. `group_role_mappings` is a JSON object whose values are `admin` or `member`, for example `{"Blue Admins":"admin"}`. Configure provider ID, issuer, client ID, and client secret as dashboard environment variables. Follow [Set up Okta SSO and SCIM](/0.2.0/admin/identity-provisioning) for the complete lifecycle. + +### Blob and package storage + +```yaml +control_api: + blob_storage: + bucket: blue-sessions-production + region: us-west-2 + presign_ttl_seconds: 300 + retention_days: 30 + package_artifacts: + bucket: blue-packages-production +``` + +| Field | Meaning | +| --- | --- | +| `blob_storage.bucket` | Raw session-upload objects. Required even when capture is initially disabled. | +| `blob_storage.region` | S3 signing region; defaults to `us-east-1`. | +| `blob_storage.endpoint` | Optional internal S3-compatible endpoint used by the service. Omit for AWS S3. | +| `blob_storage.public_endpoint` | Optional client-reachable endpoint embedded in presigned URLs. Use when the internal endpoint is not externally resolvable. | +| `blob_storage.force_path_style` | Enable for providers such as local MinIO that require path-style addressing. | +| `blob_storage.presign_ttl_seconds` | Positive lifetime for upload/download URLs; defaults to 300 seconds. | +| `blob_storage.retention_days` | Positive session-retention deadline stored by Blue. The AWS starter creates a matching lifecycle rule; configure equivalent expiration for other storage backends because Blue does not delete expired objects itself. | +| `package_artifacts.bucket` | Separate non-expiring bucket for immutable archives mirrored from managed repositories; defaults to `package-artifacts`. | + +Use workload identity or ambient AWS credentials rather than static access keys. Keep session and package buckets separate because they have different retention and recovery requirements. + +### Request logs and managed repositories + +`gateway_request_logs.retention_days` controls metadata retention for inference-proxy request logs. It does not store prompt or response bodies. + +Managed repository connections let administrators select packages from private GitHub or Bitbucket repositories without giving repository credentials to clients: + +```yaml +control_api: + package_sources: + connections: + - id: engineering-github + name: Engineering GitHub + provider: github + app_id: os.environ/HARNESS_GITHUB_APP_ID + private_key: file:///var/run/secrets/blue/github-app.pem + organizations: + engineering: [example-org] +``` + +Supported providers are `github`, `bitbucket_cloud`, and `bitbucket_data_center`. GitHub requires `app_id` and `private_key`; Bitbucket requires `token`. Connection IDs must be unique and contain only letters, numbers, dashes, or underscores. The `organizations` map limits which repository namespaces each Blue organization may use. See [Managed repositories](/0.2.0/deployment/managed-repositories) for enterprise URLs, CA bundles, and artifact mirroring. + +## Configure optional gateway mode + +Omit the entire `gateway` section for governance-only operation. Adding it connects Blue to an upstream inference gateway operated by your organization, enables gateway policy for every allowed agent, and requires a provisioner plus encrypted server-side credential storage. Blue does not bundle the upstream gateway. LiteLLM is the first and currently only supported `gateway.type`. + +```yaml +gateway: + type: litellm + url: os.environ/HARNESS_GATEWAY_URL + inference_proxy_url: https://inference.blue.example.com + inference_proxy_health_url: http://127.0.0.1:8081/health + internal_allowed_client_id: blue-inference-proxy + inference_jwt: + issuer: https://api.blue.example.com + audience: blue-inference-proxy + private_key_file: /var/run/blue/gateway-jwt/signing-key.pem + # Only while rotating keys: + # previous_private_key_file: /var/run/blue/gateway-jwt/previous-signing-key.pem + token_ttl_seconds: 43200 + secret_encryption: + provider: aws-kms + key_id: arn:aws:kms:us-west-2:123456789012:key/example + provisioner: + type: builtin-litellm + reconcile_ttl_seconds: 86400 +``` + +| Field | Meaning | +| --- | --- | +| `type` | Gateway policy identifier projected into personalized client policy. | +| `url` | Upstream LiteLLM-compatible gateway reachable from the Control API. | +| `inference_proxy_url` | Public proxy URL written into governed agent overlays. | +| `inference_proxy_health_url` | Internal health URL used by the Control API. | +| `internal_allowed_client_id` | OAuth client id (`sub`) the Control API accepts on the internal resolver from the inference proxy's client-credentials token. Defaults to `blue-inference-proxy`. | +| `inference_jwt.issuer` | Exact issuer placed in inference JWTs and required by the proxy. | +| `inference_jwt.audience` | Exact inference-proxy audience placed in inference JWTs. | +| `inference_jwt.private_key_file` | Active RS256 signing key: an RSA key of at least 2048 bits, as PKCS#8 or PKCS#1 PEM, mounted only in Control API replicas. The Control API works out the published JWKS from it, so there is no JWKS file. Each key's `kid` is its RFC 7638 thumbprint. `HARNESS_GATEWAY_JWT_PRIVATE_KEY_PEM` supplies the PEM inline instead. | +| `inference_jwt.previous_private_key_file` | Optional retired signing key, set only during a rotation. Its public key stays published so tokens it signed keep verifying; it never signs. `HARNESS_GATEWAY_JWT_PREVIOUS_PRIVATE_KEY_PEM` supplies the PEM inline instead. | +| `inference_jwt.token_ttl_seconds` | Inference JWT lifetime; defaults to `43200` (12 hours) and is always clamped to the remaining browser session. The token is written into the agent process environment at spawn and is not rotated in flight, so a shorter value shortens the usable agent run. | +| `secret_encryption.provider` | `aws-kms` for production or `environment` for development. | +| `secret_encryption.key_id` | AWS KMS key used for envelope encryption. | +| `secret_encryption.key` | Base64 environment key required by the development `environment` provider. | +| `provisioner.type` | Built-in or custom executable identifier. The public image supports `builtin-litellm`. | +| `provisioner.executable_path` | Optional absolute path to a custom executable. Required with `executable_sha256`. | +| `provisioner.executable_sha256` | Lowercase SHA-256 pin for the exact custom executable. Required with `executable_path`. | +| `provisioner.policy_revision` | Non-empty policy revision for a custom executable; changing it triggers reconciliation. | +| `provisioner.reconcile_ttl_seconds` | Positive interval before Blue reconciles the managed gateway credential again. | +| `provisioner.timeout_seconds` | Per-invocation custom executable deadline; defaults to 15 seconds. | +| `provisioner.max_concurrency` | Configured provisioner concurrency ceiling; defaults to `8` and is further limited by the database pool. | + +`HARNESS_PROVISIONER_EXECUTABLE_PATH` and `HARNESS_PROVISIONER_EXECUTABLE_SHA256` override the corresponding custom-provisioner YAML fields. + +The inference proxy also requires `HARNESS_GATEWAY_TYPE` with the same value as +`gateway.type`, plus `HARNESS_GATEWAY_JWKS_URL`, +`HARNESS_GATEWAY_JWT_ISSUER`, and `HARNESS_GATEWAY_JWT_AUDIENCE`; it does not +infer a gateway or trust domain from URLs. In Helm, set `blue.gatewayType` and +the `blue.inferenceJwt` values when `blue.enableInferenceProxy=true`. + +Generate the inference JWT signing key once: + +```bash +openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out signing-key.pem +``` + +To rotate it: + +1. Mount the new key as `private_key_file` and the old one as `previous_private_key_file`, then restart the Control API. New tokens use the new key, and the proxy fetches the new `kid` the first time it sees it. +2. After the token lifetime has passed (12 hours by default), remove `previous_private_key_file` and restart again. + +The `deploy/tofu` modules generate the key for you; there you rotate by putting a new entry first in `gateway_jwt_key_versions`. + +The LiteLLM administrator key and inference JWT private key remain runtime secrets; do not put them in governance policy. See [Bring your own gateway](/0.2.0/concepts/gateway-mode) for the authenticated request path, key ring, and operational checks. + +## Configure the governance baseline + +The `governance` section is the deployment-authored baseline delivered to clients after personalization. It controls: + +| Field | Meaning | +| --- | --- | +| `revision` | Required seed value. The Control API replaces it with an immutable stored revision ID whenever it publishes a revision. | +| `contract_version` | Wire contract version. Current clients support version 3. | +| `required_capabilities` | Features a client must support before accepting policy. | +| `minimum_client_version` | Operator-visible rollout floor; required when using harness version ranges or versioned package adapters. | +| `required_client_version` | Optional canonical exact SemVer recommendation for the Blue CLI. Defaults to the control API deployment version and requires `tenant_client_version_pin`. | +| `ttl_seconds` | Client cache and polling interval; defaults to 300 seconds. | +| `required` | When true, clients fail closed instead of launching from stale cached policy. | +| `allowed_harnesses` | Supported agent keys users may launch through Blue. | +| `harnesses` | Per-agent version constraint, managed settings, MCP servers, and package overrides. | +| `session_upload` | Optional global raw-session capture policy. Omit to disable capture. | +| `telemetry` | Optional metadata-only run event sink. | + +Per-agent managed settings are agent-native and version-aware: + +```yaml +governance: + harnesses: + codex: + version_requirement: ">=0.149.0, <0.150.0" + managed_config: + model: gpt-5.6-sol + reasoning_effort: medium + approval_policy: on-request + sandbox_mode: workspace-write + mcp: + - name: engineering-tools + transport: http + url: https://mcp.example.com +``` + +Use [Governance configuration](/0.2.0/reference/governance-config) for the complete client policy schema, package adapters, MCP transports, and harness-specific behavior. + +### Baseline reconciliation + +On startup, the Control API compares: + +1. The baseline previously loaded from `blue.yaml`. +2. The current organization revision, including dashboard edits. +3. The newly mounted `governance` section. + +Non-conflicting deployment changes create a new immutable revision. A dashboard-edited value wins when both the dashboard and the new deployment baseline changed the same path; startup reports the skipped path without logging secret values. Users, sessions, gateway selections, package artifacts, and other accumulated records are not replaced by `blue.yaml`. + +The Control API detects baseline changes from the normalized document, not from the authored `revision` string. You do not need to manually increment that seed value; use source control and deployment image tags to identify authored releases. + + + Make small, reviewable baseline changes and inspect startup reconciliation notices after deployment. Export the current secret-safe YAML from the dashboard before a large refactor so authored configuration reflects intentional dashboard state. + + +## Configure the package catalog + +`package_catalog` defines choices displayed on the dashboard's Extensions page. Catalog entries are not automatically installed. When an administrator selects one, Blue copies its immutable recipe into an organization governance revision. + +```yaml +package_catalog: + packages: + - id: engineering-toolkit + name: Engineering Toolkit + version: "1.4.0" + source_ref: https://artifacts.example.com/blue/engineering-toolkit-1.4.0.tar.gz + sha256: 64-character-lowercase-sha256-digest + adapters: + codex: + skills_dir: skills + claude: + skills_dir: skills + kimi: + skills_dir: skills + opencode: + skills_dir: skills +``` + +Every source must be immutable and SHA-256 pinned. Use `platform_sources` when helper binaries differ by operating system or architecture. Adapter paths must be archive-relative and identify only content Blue knows how to activate; package installation does not run arbitrary installer scripts. + +## Validate and roll out changes + + + + Confirm that database URLs, private keys, tokens, encryption material, bootstrap passwords, and gateway administrator keys are references rather than literals. + + + Run the release bundle's validation workflow. Locally, parse `blue.yaml`, lint the chart, and render Kubernetes resources: + + ```bash + yq eval '.' blue.yaml >/dev/null + helm lint chart/blue -f values.yaml + helm template blue chart/blue -f values.yaml >/dev/null + ``` + + + Review the OpenTofu plan and the organization image diff. Pin the upstream Blue image to the intended release rather than relying on `latest`. + + + Wait for the Helm rollout and inspect Control API logs for YAML parsing, missing references, migration failures, and baseline reconciliation notices. + + + Check the dashboard and Control API health endpoints, sign in as an administrator, then run `blue setup`, `blue login`, `blue status`, and a governed agent launch from a test workstation. + + + + + A complete rollout has healthy service probes, no unresolved environment references, the expected governance revision in the dashboard, and matching desired/applied revisions on a test client. + + +## Recommended Blue CLI release + +The reference control API advertises its deployment version as the exact +recommended Blue CLI release. Set +`governance.required_client_version` to a published, compatible release to +override that default, for example during a rolling upgrade. The default is +applied when serving a document, so old revisions follow new deployments without +being rewritten. An explicit value stays fixed across rollouts. Use canonical +SemVer without a `v` prefix or range operators. `minimum_client_version` remains +informational. + +The `tenant_client_version_pin` capability is required whenever a pin is served. +The `X-Blue-Required-Client-Version` header exposes the same pin on successful +config responses and capability/contract rejections (HTTP 426). Older clients +without this capability cannot consume the document. diff --git a/apps/docs/0.2.0/deployment/managed-repositories.mdx b/apps/docs/0.2.0/deployment/managed-repositories.mdx new file mode 100644 index 0000000..bc95b4a --- /dev/null +++ b/apps/docs/0.2.0/deployment/managed-repositories.mdx @@ -0,0 +1,226 @@ +--- +title: "Managed repository connections" +description: "Configure private GitHub and Bitbucket repositories as organization-managed extension sources." +icon: "folder-tree" +--- + +Managed repository connections let administrators import extension bundles from private GitHub and Bitbucket repositories. The Control API authenticates to the repository, resolves the requested branch or tag to an immutable commit, and mirrors the archive into organization-scoped object storage. + +Repository credentials never appear in governance configuration or reach developer machines. Clients receive an `artifact_id` and SHA-256 digest, then use their existing harness session to request a short-lived object-storage download. + + + Repository archives can contain executable hooks, plugins, and helper binaries. Grant repository access only to reviewed namespaces and inspect the capability mappings before publishing a package. + + +## Prerequisites + +Before adding a connection, prepare: + +- A dedicated, non-expiring S3-compatible bucket for package artifacts. +- The organization slug used by Blue, such as the `control_api.bootstrap.organization_slug` value for the bootstrap organization. +- A read-only GitHub App or Bitbucket access token. +- A mounted PEM CA bundle when an enterprise provider uses a private certificate authority. + +The Compose stack creates the `package-artifacts` MinIO bucket automatically. Production deployments must create the bucket separately and set `HARNESS_PACKAGE_BUCKET`. + +## Configure artifact storage + +Package artifacts must not use the raw-session bucket because session retention policies may delete objects still referenced by historical governance revisions. + +```yaml blue.yaml +control_api: + package_artifacts: + bucket: os.environ/HARNESS_PACKAGE_BUCKET +``` + +Compose fixes the package-artifact bucket name in `deploy/docker-compose.yml`; setting `HARNESS_PACKAGE_BUCKET` in `deploy/.env` does not override it. Change the Compose service environment explicitly when you need another bucket. + +The Control API uses the same S3 endpoint, region, public endpoint, path-style setting, and ambient AWS credentials configured under `control_api.blob_storage`. + +## Configure a provider + +Add connections under `control_api.package_sources.connections`. Connection IDs must contain only letters, numbers, hyphens, and underscores. Each organization entry maps a Blue organization slug to the repository namespaces it may import. + + + + Create a GitHub App, grant it read-only **Contents** permission, and install it on the target organization or account. Select only the repositories Blue should import. + + ```yaml blue.yaml + control_api: + package_sources: + connections: + - id: github-com + name: GitHub + provider: github + app_id: os.environ/HARNESS_GITHUB_APP_ID + private_key: file:///run/secrets/github-app-private-key.pem + organizations: + dev: + - BlocksOrg + ``` + + `app_id` is the GitHub App ID, not the OAuth client ID. Blue creates a repository-scoped installation token when an administrator inspects a source. + + + + Register and install a GitHub App on the Enterprise Server instance. Set `api_base_url` to the REST API root, normally `/api/v3`. + + ```yaml blue.yaml + control_api: + package_sources: + connections: + - id: corporate-github + name: Corporate GitHub + provider: github + api_base_url: https://github.example.com/api/v3 + app_id: os.environ/HARNESS_GHES_APP_ID + private_key: file:///run/secrets/ghes-app-private-key.pem + ca_bundle: file:///run/secrets/corporate-ca.pem + download_hosts: + - github-assets.example.com + organizations: + dev: + - platform-engineering + ``` + + Add an exact hostname to `download_hosts` only when the configured server redirects archive downloads to that trusted hostname. Wildcards, ports, paths, and embedded credentials are rejected. Same-host downloads require no entry. + + + + Create a repository, project, or workspace access token with read-only repository permission. Prefer the narrowest token that covers the approved repositories. + + ```yaml blue.yaml + control_api: + package_sources: + connections: + - id: bitbucket-cloud + name: Bitbucket Cloud + provider: bitbucket_cloud + token: os.environ/HARNESS_BITBUCKET_TOKEN + organizations: + dev: + - product-engineering + ``` + + Namespace entries are Bitbucket workspace slugs. Blue sends the access token as a Bearer token and resolves refs through the Bitbucket Cloud API. + + + + Create a read-only HTTP access token for the repository or project. Set `api_base_url` to the instance root, including any deployment context path. + + ```yaml blue.yaml + control_api: + package_sources: + connections: + - id: corporate-bitbucket + name: Corporate Bitbucket + provider: bitbucket_data_center + api_base_url: https://bitbucket.example.com/bitbucket + token: os.environ/HARNESS_BITBUCKET_DC_TOKEN + ca_bundle: file:///run/secrets/corporate-ca.pem + download_hosts: + - bitbucket-assets.example.com + organizations: + dev: + - PLATFORM + ``` + + Namespace entries are Bitbucket project keys. Archive access requires at least repository read permission. + + + +## Supply deployment secrets + +Environment references use `os.environ/NAME` or `env://NAME`. File references use `file:///absolute/path`. Keep private keys and tokens outside the repository. + +For Compose, add the environment variables and read-only secret mounts to the Control API service: + +```yaml docker-compose.override.yml +services: + control-api: + environment: + HARNESS_GITHUB_APP_ID: "123456" + HARNESS_BITBUCKET_TOKEN: ${HARNESS_BITBUCKET_TOKEN} + volumes: + - ./secrets/github-app-private-key.pem:/run/secrets/github-app-private-key.pem:ro + - ./secrets/corporate-ca.pem:/run/secrets/corporate-ca.pem:ro +``` + + + Connection secrets and referenced files are loaded when the Control API starts. Restart the service after rotating a key, token, URL reference, or CA bundle. Already mirrored artifacts remain installable after provider credentials change. + + +## Control organization access + +The `organizations` map is an authorization boundary, not a display filter. The Control API rejects a namespace before contacting the provider when it is not allowed for the authenticated administrator's organization. + +```yaml +organizations: + dev: + - BlocksOrg + - blocks-internal + security: + - security-engineering +``` + +Namespace comparisons are case-insensitive. A `"*"` entry permits every namespace accessible to the connection credential and should be reserved for single-organization deployments with an equivalent trust boundary. + +## Start and verify the connection + + + + Rebuild or restart the service after changing the server configuration or secrets. + + ```bash + (cd deploy && docker compose up -d --build control-api dashboard) + ``` + + + Confirm the service starts without secret, URL, CA, database migration, or object-storage errors. + + ```bash + (cd deploy && docker compose logs --tail=100 control-api) + curl -fsS http://127.0.0.1:8080/health + ``` + + + Open **Extensions**, select **Add extension**, then choose **Managed repository**. Select the connection, enter `namespace/repository` and a branch, tag, or commit, and select **Inspect source**. + + + Verify that the form shows a resolved commit, archive size, and SHA-256 digest. Add the capability mappings and publish the pending governance revision. + + + Run `blue apply` or launch an allowed harness. The client requests a fresh download with its harness session, verifies the digest, and installs the compatible package contents. + + + +## Configuration fields + +| Field | Required | Description | +| --- | --- | --- | +| `id` | Yes | Stable connection identifier shown in artifact provenance. | +| `name` | No | Dashboard label; defaults to `id`. | +| `provider` | Yes | `github`, `bitbucket_cloud`, or `bitbucket_data_center`. | +| `app_id` | GitHub | GitHub App ID or environment/file reference. | +| `private_key` | GitHub | RSA private key generated for the GitHub App. | +| `token` | Bitbucket | Read-only repository, project, or workspace access token. | +| `api_base_url` | Enterprise | GitHub REST API root or Bitbucket Data Center instance root. | +| `web_base_url` | Optional | Bitbucket Cloud web origin override; omit for `bitbucket.org`. | +| `ca_bundle` | Optional | PEM CA bundle or secret reference for a private enterprise CA. | +| `download_hosts` | Optional | Additional exact trusted cross-host archive redirect destinations. HTTP archive redirects are HTTPS-only and capped at five hops, and credentials are sent only to the original scheme, host, and port. Bitbucket Cloud's Git acquisition path refuses redirects entirely. | +| `organizations` | Yes | Blue organization slugs mapped to allowed repository namespaces. | + +## Troubleshooting + +| Symptom | Resolution | +| --- | --- | +| No managed repository connections appear | Confirm the signed-in organization slug has an entry under `organizations`, then restart the Control API. | +| Namespace is not allowed | Add the exact GitHub owner, Bitbucket workspace, or Bitbucket project key to that organization's allowlist. | +| GitHub App cannot access the repository | Confirm the App is installed on the owner, the repository is selected, and Contents permission is read-only or greater. | +| Ref resolution returns `401` or `403` | Rotate the Bitbucket token or verify the GitHub App installation and provider permissions. | +| Archive redirects to an untrusted host | Verify the provider's archive hostname, then add only that trusted hostname to `download_hosts`. | +| Provider CA bundle is invalid | Mount a PEM bundle readable by the Control API and check the `file://` path. | +| Archive exceeds 100 MiB | Remove unrelated repository content or publish a smaller purpose-built extension archive. | +| Client cannot install a new artifact offline | Restore access to the Control API for the first download. Digest-cached packages continue to work offline. | + +See [Managed packages](/0.2.0/admin/managed-packages) for capability mapping and client installation behavior, and [Production deployment](/0.2.0/deployment/production) for storage and secret-management requirements. diff --git a/apps/docs/0.2.0/deployment/production.mdx b/apps/docs/0.2.0/deployment/production.mdx new file mode 100644 index 0000000..14e029a --- /dev/null +++ b/apps/docs/0.2.0/deployment/production.mdx @@ -0,0 +1,393 @@ +--- +title: "Kubernetes with Helm" +description: "Deploy Blue on EKS with the maintained chart and the AWS OpenTofu starter, then turn on gateway mode." +icon: "server" +--- + +This guide deploys Blue (Control API, worker, dashboard) onto a new EKS cluster, with Postgres on RDS, session and package storage on S3, and Redis on ElastiCache. One AWS load balancer serves HTTPS on two names in a Route 53 hosted zone you already have, for example `blue.example.com` and `api.blue.example.com`. The chart runs with its production checks on. + +You need `tofu`, `aws`, `kubectl`, `helm`, `jq`, `docker`, and AWS credentials that can create VPC, EKS, RDS, ElastiCache, S3, IAM, KMS, ACM and Route 53 records. Run everything from the bundle's `infra/aws` folder, in one terminal. The files this guide writes go in that folder. Everything here costs money every hour. + +Nothing in Blue reads Redis. It is here for the LiteLLM gateway you may run later; drop `include_redis` to save the cost. To land in a cluster or VPC you already run, set `eks_cluster_name` and `vpc_id`; see the [AWS module README](https://github.com/BlocksOrg/blue/blob/main/deploy/tofu/aws/README.md). + +Blue does not need Kubernetes. This is the maintained implementation of the [Deployment contract](/0.2.0/deployment/runtime-contract); any platform that meets the contract works. + +## Before you start +```bash +# Downloads the release bundle, checks it, and moves into the AWS folder. The bundle holds +# the Helm chart at chart/blue and the OpenTofu module at infra/aws. +VERSION=0.2.0 +curl -LO "https://github.com/BlocksOrg/blue/releases/download/v${VERSION}/blue-deployment-v${VERSION}.tar.gz" +curl -LO "https://github.com/BlocksOrg/blue/releases/download/v${VERSION}/SHA256SUMS" +grep "blue-deployment-v${VERSION}.tar.gz" SHA256SUMS | shasum -a 256 -c - +tar -xzf "blue-deployment-v${VERSION}.tar.gz" +cd "blue-deployment-${VERSION}/infra/aws" +``` + +## 1. Create the cluster, databases and certificate +```bash +# Writes the settings file. The names are relative to the hosted zone: with a zone for +# example.com, this gives app.example.com, api.example.com and iproxy.example.com. +export AWS_REGION=us-east-1 # every aws command and Tofu use this + +cat > terraform.tfvars < "$BLUE_HELM_GENERATED_FILE" + +# Pins the image to an exact build. The chart refuses a moving tag in production. +export BLUE_IMAGE_DIGEST="$(docker buildx imagetools inspect ghcr.io/blocksorg/blue:${VERSION} | awk '/^Digest:/{print $2}')" +echo "$BLUE_IMAGE_DIGEST" # must print sha256:...; if empty, stop here +``` +**DNS not in Route 53?** Leave the four domain lines out of `terraform.tfvars`, skip the three `tofu output` lines for the certificate and hostnames, and set them yourself: +```bash +export BLUE_DASHBOARD_HOST=blue.example.com +export BLUE_API_HOST=api.blue.example.com + +# Asks AWS for one certificate covering both names, prints one CNAME per name to create at +# your DNS provider, then waits for AWS to see them. +export BLUE_CERT_ARN="$(aws acm request-certificate \ + --domain-name "$BLUE_DASHBOARD_HOST" --subject-alternative-names "$BLUE_API_HOST" \ + --validation-method DNS --query CertificateArn --output text)" +aws acm describe-certificate --certificate-arn "$BLUE_CERT_ARN" \ + --query 'Certificate.DomainValidationOptions[].ResourceRecord' --output table +aws acm wait certificate-validated --certificate-arn "$BLUE_CERT_ARN" +``` + +## 2. Connect kubectl and prepare the cluster +```bash +# Points kubectl at the new cluster. +eval "$BLUE_KUBECONFIG_COMMAND" + +# Creates the namespace. It must match Tofu's, because the IAM role only trusts this one. +kubectl create namespace "$BLUE_KUBE_NAMESPACE" + +# Tells the cluster how to build a public HTTPS load balancer with our certificate. +kubectl apply -f - < "$BLUE_CONFIG_FILE" < "$BLUE_HELM_VALUES_FILE" <> terraform.tfvars +tofu apply +``` +**DNS not in Route 53?** Skip the `tofu apply` and create two CNAME records at your DNS provider, `$BLUE_DASHBOARD_HOST` and `$BLUE_API_HOST`, both pointing at `$ALB`. + +## 8. Check it, use it, delete it +```bash +# Checks the API is up over HTTPS and can reach S3. New DNS records can take a minute. +curl -fsS "https://$BLUE_API_HOST/ready" +curl -fsS "https://$BLUE_API_HOST/health/object-storage" + +# Opens the dashboard. Log in with the email and password this prints. +echo "$ADMIN_EMAIL / $ADMIN_PW" +open "https://$BLUE_DASHBOARD_HOST" + +# Points your blue CLI to the deployed server. Enter https://$BLUE_API_HOST when asked. +blue setup +blue login && blue doctor + +# Deletes Blue first (this removes the load balancer), then everything in AWS including +# the database and its data. Don't skip the second one. +helm uninstall blue -n "$BLUE_KUBE_NAMESPACE" +tofu destroy +``` +`open` is macOS; on Linux, paste the dashboard address into a browser. Session uploads go straight to S3, so no port-forward is needed. If `tofu destroy` fails on the VPC, the load balancer is still being removed. Wait and retry. If it fails on the buckets, they still have objects in them. Empty them and retry. + +**DNS not in Route 53?** Run `tofu destroy` as above, then delete the two records and the certificate yourself. + +## Changing the config later +Edit `$BLUE_CONFIG_FILE`, then upload it and restart the API and worker. The server compares the new file's content to the stored baseline and publishes a revision when they differ: +```bash +kubectl -n "$BLUE_KUBE_NAMESPACE" create configmap blue-config --from-file=blue.yaml="$BLUE_CONFIG_FILE" \ + --dry-run=client -o yaml | kubectl apply -f - +kubectl -n "$BLUE_KUBE_NAMESPACE" rollout restart deployments +``` + +## Hardening +- Set `deletion_protection` back to `true`, so the database can't be destroyed by accident and gets a final snapshot if it is. +- Add `networkPolicy.externalHttpsCidrs` to your values file with the S3 and STS ranges for your region from `https://ip-ranges.amazonaws.com/ip-ranges.json`, plus any package hosts you allow. The generated settings leave HTTPS open. +- Limit who can reach the cluster API with `cluster_endpoint_public_access_cidrs`, and who can reach the load balancer with `alb.ingress.kubernetes.io/inbound-cidrs`. +- Check the rendered install with [`scripts/verify-deployment.sh`](https://github.com/BlocksOrg/blue/blob/main/scripts/verify-deployment.sh) from the source repository. + +## Backups and upgrades +Back up PostgreSQL, the package bucket, and the session bucket separately; they have different retention and recovery needs. To upgrade, pin the new image digest in the values file and run the step 6 `helm upgrade` again. The chart's pre-upgrade Job runs the database migrations before the new pods start. See the [migration policy](https://github.com/BlocksOrg/blue/blob/main/deploy/runbooks/migrations.md). + +## Gateway mode: route agent inference through Blue + +Continues from step 8 with the cluster still running. Adds Blue's inference proxy at `https://iproxy.`: agents send model calls there, the proxy checks a Blue-issued token, then forwards to your LiteLLM, which holds the provider keys. LiteLLM is already running somewhere you manage, and you have its URL and master key. It must serve a model named `claude-opus-4-8`, the name the governance policy in step 4 hands to agents. Installs cert-manager into the cluster for the internal certificates. + +Read [Bring your own gateway](/0.2.0/concepts/gateway-mode) for how the routing and credentials work. + +## 9. Point Blue at your LiteLLM +```bash +export BLUE_PROXY_HOST="$(tofu output -raw inference_proxy_hostname)" +export LITELLM_URL=https://litellm.example.com # your LiteLLM, reachable from the cluster +export LITELLM_MASTER_KEY=sk-... # its master key + +# Blue only hands out keys to people LiteLLM already knows, matched by email. Adds the admin; +# repeat for each developer. +curl -fsS -X POST "$LITELLM_URL/user/new" -H "Authorization: Bearer $LITELLM_MASTER_KEY" \ + -H 'Content-Type: application/json' -d "{\"user_email\":\"$ADMIN_EMAIL\"}" +``` + +## 10. Add the gateway secrets +```bash +# Refreshes the generated Helm settings: they now name the signing-key secret. +tofu output -json helm_values > "$BLUE_HELM_GENERATED_FILE" + +# Adds LiteLLM's address and master key. The proxy reads the address from here, not from the +# config file. The encryption key is ignored with KMS, but the chart refuses to start without it. +kubectl -n "$BLUE_KUBE_NAMESPACE" patch secret blue-runtime --type merge -p "{\"stringData\":{ + \"HARNESS_GATEWAY_URL\":\"$LITELLM_URL\", + \"HARNESS_LITELLM_ADMIN_KEY\":\"$LITELLM_MASTER_KEY\", + \"HARNESS_GATEWAY_ENCRYPTION_KEY\":\"unused-with-aws-kms\"}}" + +# Copies the token-signing key Tofu made into the cluster. Only the API reads it. +JWT_JSON="$(aws secretsmanager get-secret-value --secret-id "$(tofu output -raw gateway_jwt_secret_arn)" --query SecretString --output text)" +kubectl -n "$BLUE_KUBE_NAMESPACE" create secret generic "$(jq -r .blue.inferenceJwt.secret "$BLUE_HELM_GENERATED_FILE")" \ + --from-literal=signing-key.pem="$(jq -r '."signing-key.pem"' <<<"$JWT_JSON")" +``` + +## 11. Install cert-manager +The proxy and the API talk over an encrypted internal link where each side shows a certificate. The chart declares those certificates; cert-manager makes and renews them, and Blue picks up new files without a restart. +```bash +# One per cluster; skip if it is already there. +helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager --version v1.18.2 \ + --namespace cert-manager --create-namespace --set crds.enabled=true --wait +``` +Already have a PKI, or no cert-manager? See [Internal certificates](/0.2.0/concepts/gateway-mode#internal-certificates) for pointing the chart at your own issuer or supplying the two certificate Secrets yourself. + +## 12. Add the gateway to Blue's config +```bash +# One new top-level section: where LiteLLM is, where the proxy is, how tokens are signed, +# and how stored gateway keys are encrypted (with the KMS key Tofu made). +cat >> "$BLUE_CONFIG_FILE" < "$BLUE_HELM_GATEWAY_VALUES_FILE" < proxy -> LiteLLM -> provider. The dashboard's Gateway page shows the key as ready. +blue login && blue gateway +blue claude +``` +Deleting everything is the same as step 8. LiteLLM is yours and stays up. diff --git a/apps/docs/0.2.0/deployment/runtime-contract.mdx b/apps/docs/0.2.0/deployment/runtime-contract.mdx new file mode 100644 index 0000000..b06ce60 --- /dev/null +++ b/apps/docs/0.2.0/deployment/runtime-contract.mdx @@ -0,0 +1,149 @@ +--- +title: "Deployment contract" +description: "Run Blue on Kubernetes, a container scheduler, or virtual machines by implementing its platform-neutral workload contract." +icon: "list-check" +--- + +Blue does not require Kubernetes. The release image exposes independent process commands that can run on ECS, Nomad, Docker Swarm, virtual machines, or another platform chosen by your operations team. The Helm chart is the maintained Kubernetes implementation of this contract. + +This page defines what a production platform must provide. It does not prescribe scheduling, service discovery, secret-management, ingress, or infrastructure tooling. + +## Release artifacts + +Pin the Blue image by immutable digest: + +```text +ghcr.io/blocksorg/blue@sha256:RELEASE_IMAGE_DIGEST +``` + +The image contains the Control API, dashboard, inference proxy, entrypoint, and health-check helper. Its default command is `all`; pass a component command to run one workload per container. Each GitHub Release also provides a deployment bundle containing `blue.yaml`, the Helm chart, an AWS OpenTofu starter, and optional gateway-provisioner examples. + +## Workload topology + +| Workload | Image command | Container ports | Replicas | Required mode | +| --- | --- | --- | --- | --- | +| Dashboard | `dashboard` | `3000` | One or more | All deployments | +| Control API | `control-api` | `8080`; private `8082` for gateway mode | One or more | All deployments | +| Worker | `control-api` | `8080` for private health checks | Exactly one | All deployments | +| Inference proxy | `inference-proxy` | `8081` | One or more | Gateway mode only | + +Run the Control API replicas with `HARNESS_RUN_BACKGROUND_JOBS=false`. Run the singleton worker with `HARNESS_RUN_BACKGROUND_JOBS=true`, do not route public API traffic to it, and give it the same configuration, database, storage access, and secrets as the Control API. + +The Control API, dashboard, and inference proxy are independently scalable. Keep the worker singleton so cleanup, reconciliation, and other background loops do not run concurrently. + +### All-in-one process + +Leaving the image command empty, or setting it to `all`, runs the Control API and dashboard in one container and optionally starts the inference proxy when `BLUE_ENABLE_INFERENCE_PROXY=true`. + +Use all-in-one mode for local evaluation. A production platform should normally run separate workloads so it can isolate failures, scale components independently, and maintain a singleton worker lifecycle. + +## External dependencies + +Every deployment requires: + +- PostgreSQL for authentication, governance revisions, users, clients, gateway selections, and session metadata +- A non-expiring S3-compatible bucket for immutable package artifacts +- A separate S3-compatible bucket for session objects, with lifecycle retention that matches your policy +- Workload identity or storage credentials with least-privilege access to those buckets +- DNS and TLS for every public endpoint + +Gateway mode also requires an organization-operated LiteLLM service. Blue does not deploy or hard-depend on LiteLLM, Redis, or another gateway in governance-only mode. + +## Configuration and secrets + +Mount the same `blue.yaml` read-only into the Control API and worker. The release image defaults `BLUE_CONFIG_FILE` to: + +```text +/etc/blue/blue.yaml +``` + +The file contains server runtime settings, the governance baseline, the optional gateway definition, and the optional package catalog. Values may be literals or environment references. Direct environment variables take precedence. + +`BLUE_CONFIG_OVERLAY_FILES` may contain a comma-separated list of additional +YAML files. The Control API applies them in order: mappings merge recursively, +while scalar and sequence values replace the base value. Use small overlays for +optional deployment features; keep shared configuration in the canonical file. + +Keep credentials in your platform's secret store and inject them at runtime. A password-mode governance-only deployment needs at least: + +| Setting | Consumers | Purpose | +| --- | --- | --- | +| `HARNESS_DATABASE_URL` | Dashboard, Control API, worker | Shared PostgreSQL connection. Append `?sslmode=require` for databases that refuse plaintext, such as RDS; `sslmode` follows libpq semantics in every component | +| `BETTER_AUTH_SECRET` | Dashboard | Persistent authentication encryption and signing secret | +| `HARNESS_BOOTSTRAP_ADMIN_EMAIL` | Dashboard, Control API, worker | Protected initial administrator identity | +| `HARNESS_BOOTSTRAP_ADMIN_PASSWORD` | Dashboard | Initial administrator password | +| Session and package bucket settings | Control API, worker | Object storage locations and access behavior | +| Storage identity or credentials | Control API, worker | Access to both object-storage buckets | + +Keep `BETTER_AUTH_SECRET` stable after the initial deployment. Follow [Configure blue.yaml](/0.2.0/deployment/blue-yaml) for the full environment mapping, authentication contract, storage endpoints, managed repositories, and gateway secrets. + +An absent `BLUE_ENVIRONMENT` is production. Only local development may set it +to `development`, which permits the documented sample authentication values. +Gateway deployments also set `BLUE_GATEWAY_ENABLED=true` on the dashboard so it +requires the inference proxy's OAuth client secret before opening a listener. +Helm owns both flags; do not inject either through `blue.env`. + +## Service networking + +Publish only the endpoints required by users and clients: + +| Route | Exposure | Consumer | +| --- | --- | --- | +| Dashboard port `3000` | Public HTTPS | Administrators and browser authentication | +| Control API port `8080` | Public HTTPS | Dashboard server and workstation clients | +| Control API port `8082` | Private; gateway mode only | Inference proxy resolver and event traffic | +| Inference proxy port `8081` | Public HTTPS; gateway mode only | Governed native agents | +| Worker port `8080` | Private health checks only | Deployment platform | + +Configure the dashboard with an internal Control API URL and the public Control API origin. Configure the Control API's authentication session and JWKS URLs to reach the dashboard over the private network. The public issuer and audience must exactly match the externally reachable dashboard and Control API origins. + +Do not expose port `8082` through a public load balancer. In gateway mode, restrict it to inference-proxy workloads and use the documented mTLS and OAuth client-credentials controls. + +The Control API's `/governance-config/events` route is a server-sent event stream with a 15-second heartbeat. Disable response buffering for that route and set the upstream idle timeout above the heartbeat interval. Clients fall back to polling when the stream is unavailable. + +## Health checks + +| Workload | Liveness | Readiness | +| --- | --- | --- | +| Dashboard | `GET /api/health` on `3000` | `GET /api/health` on `3000` | +| Control API | `GET /health` on `8080` | `GET /ready` on `8080` | +| Worker | `GET /health` on `8080` | `GET /health` on `8080` | +| Inference proxy | `GET /health` on `8081` | `GET /ready` on `8081` | + +After deployment, verify the two public origins using your own domains: + +```bash +curl --fail-with-body https://api.blue.example.com/health +curl --fail-with-body https://blue.example.com/api/health +``` + +The Control API should return `ok`, and the dashboard health endpoint should succeed. The Control API readiness endpoint verifies database connectivity. A successful liveness response alone does not prove that PostgreSQL or object storage is usable; after deployment, sign in and use the dashboard or CLI dependency-health view as an end-to-end check. + +## Rollout responsibilities + +Your deployment implementation should: + +1. Provision PostgreSQL, both buckets, workload identity, secrets, DNS, and certificates. +2. Deploy the pinned image and mounted configuration with the workload topology above. +3. Wait for readiness before directing traffic to new replicas. +4. Preserve the singleton worker invariant during deploys and restarts. +5. Drain inference-proxy connections during gateway-mode rollouts. +6. Back up PostgreSQL and both storage domains before upgrades. +7. Monitor authentication failures, dependency health, worker failures, stale clients, and gateway failures when enabled. + +Use [Kubernetes with Helm](/0.2.0/deployment/production) for a concrete implementation, or translate this contract into your platform's service, task, job, and load-balancer resources. + +## Optional inference proxy + +Gateway mode adds the `inference-proxy` workload and private Control API port `8082`. It also requires: + +- A top-level `gateway` section in `blue.yaml` +- Matching gateway type settings in the Control API and inference proxy +- An upstream LiteLLM URL and user identities whose emails match Blue users +- Credential encryption using AWS KMS or another supported provider +- A built-in provisioner or digest-pinned deployment-trusted executable +- OAuth client-credentials between the proxy and dashboard token endpoint +- mTLS certificates between the inference proxy and private Control API listener +- Streaming-aware public ingress and connection draining + +See [Bring your own gateway](/0.2.0/concepts/gateway-mode), [Gateway access](/0.2.0/admin/gateway-access), and [Custom gateway provisioners](/0.2.0/admin/custom-gateway-provisioners) for the complete contract. diff --git a/apps/docs/0.2.0/development/contributing.mdx b/apps/docs/0.2.0/development/contributing.mdx new file mode 100644 index 0000000..204fc0e --- /dev/null +++ b/apps/docs/0.2.0/development/contributing.mdx @@ -0,0 +1,86 @@ +--- +title: "Contributing" +description: "Build, test, and extend Blue and its public documentation." +icon: "code-pull-request" +--- + +## Development checks + +Run `scripts/setup.sh` once per clone to install the Lefthook checks for staged Rust, dashboard and website TypeScript, and OpenAPI contract changes. Run the documentation checks below for MDX changes. + +```bash +cargo build --workspace +cargo test --workspace +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +``` + +Dashboard checks run from `apps/dashboard`: + +```bash +npm ci +npm test +npm run lint +npm run build +``` + +### Dashboard UI + +The dashboard uses the stock shadcn/ui `base-nova` preset initialized with shadcn CLI 4.19.0, Tailwind CSS v4, Base UI primitives, CSS-variable theming, Geist, and Lucide icons. `apps/dashboard/components.json` is the registry configuration. + +Add UI primitives through the current CLI from `apps/dashboard`: + +```bash +npx shadcn@latest add dialog +``` + +Use generated components from `@/components/ui` and Tailwind utilities in routes. Keep product composition in `app` or non-`ui` components, and avoid restoring global element styles that override the stock shadcn component variants. + +Documentation checks run from `apps/docs`: + +```bash +npm ci +npm run check:content +npm run check:contract +npm run validate +npm run links +``` + +## Keep contracts synchronized + +`deploy/contract/governance.openapi.yaml` is canonical for the Control API. After changing it: + +```bash +cd apps/docs +npm run sync:contract +``` + +CI rejects a stale `openapi/next.yaml` snapshot. + +After changing SQLx queries, run `scripts/prepare-sqlx.sh`; `scripts/check-sqlx-queries.sh` enforces that the offline metadata remains current. Exercise cross-service journeys with `tests/e2e/run.sh smoke` or `tests/e2e/run.sh full`. + +See the repository's `CONTRIBUTING.md` for the issue-first workflow, Conventional Commit and PR naming, and CLA requirements. + +## Publish a documentation version + +Normal releases are generated by the Release Please finalizer after it updates +the Cargo, OpenAPI, deployment, and allowlisted live-documentation versions. To +create a snapshot manually while preparing a release without the bot, finish +the `Next` documentation, synchronize those versions, then run: + +```bash +cd apps/docs +npm run release:docs -- 0.2.0 +``` + +The command refuses version mismatches and existing releases, freezes the MDX and API contract, and makes the new stable release the default Mintlify version. Do not edit a frozen release except to correct a material documentation or security error. + +Commit the generated `apps/docs/` directory, OpenAPI snapshot, and `docs.json` update before creating the release tag. The release workflow validates that the committed version is an exact snapshot of `Next` and stops before publishing binaries or images when it is missing or stale. + +## Extend the product + +Adding a harness requires a common harness variant, a single-writer configuration adapter, detection metadata, and gateway wiring. Adding a gateway requires a compiled `GatewayAdapter` plus any provider-specific proxy and credential lifecycle support; harness writers should not contain gateway-specific selection logic. + +See [Harness adapter architecture](/0.2.0/development/harness-adapter-architecture) for the version-range contract, operation-set ownership, package compatibility, and required boundary tests. + +See [Gateway adapter architecture](/0.2.0/development/gateway-adapter-architecture) for the compiled registry, service and harness ownership boundaries, provisioning choices, proxy changes, and end-to-end verification. diff --git a/apps/docs/0.2.0/development/gateway-adapter-architecture.mdx b/apps/docs/0.2.0/development/gateway-adapter-architecture.mdx new file mode 100644 index 0000000..bfec6ca --- /dev/null +++ b/apps/docs/0.2.0/development/gateway-adapter-architecture.mdx @@ -0,0 +1,245 @@ +--- +title: "Gateway adapter architecture" +description: "Add a compiled inference gateway integration across Blue's client, proxy, and credential lifecycle." +icon: "route" +--- + +Blue uses one compiled `GatewayAdapter` registry for client configuration and +inference-proxy behavior. Adding an adapter makes the gateway type recognizable; +a production integration also needs a provisioner, deployment configuration, +and end-to-end evidence. + +Gateway adapters are trusted, in-tree Rust code. An organization selects one +with `gateway.type`, but policy cannot load executable adapter behavior at +runtime. Unknown types fail during Control API and inference-proxy startup and +again at the client boundary. + +## Two gateway flows + +The control plane turns an operator choice into agent configuration: + +```text +blue.yaml gateway.type + → Control API validates the compiled registry + → personalized GatewayConfig { type, proxy_url, token, auth_style } + → client registry computes GatewayRoute { base_url, token } + → version-owned HarnessImplementation adds auth placement and wire API + → gh-config commits the ReconcilePlan +``` + +The data plane validates that inference JWT and resolves an upstream credential: + +```text +agent request with session-bound inference JWT + → inference proxy selects HARNESS_GATEWAY_TYPE from the same registry + → Control API resolves the encrypted user credential + → adapter maps path and credential placement + → upstream gateway + → adapter classifies bounded invalid-credential responses + → Control API invalidates and reconciles the credential +``` + + + `HARNESS_GATEWAY_TYPE` is required by the inference-proxy process. It must + match `gateway.type`; there is no implicit LiteLLM fallback. + + +## Contract ownership + +| Layer | Owns | Must not own | +| --- | --- | --- | +| `gh-service::GatewayConfig` | Shared client policy fields | Provider administrator credentials or executable behavior | +| `gh-gateway::GatewayAdapter` | Registry identity, client route validation, upstream path/auth policy, and invalid-credential classification | Network I/O, harness files, or credential bytes | +| `HarnessImplementation` | Version-specific inference-token placement and agent wire protocol | Gateway-type dispatch or upstream-provider behavior | +| Inference proxy | Streaming, limits, credential resolution, request forwarding, and application of adapter decisions | Provider administrator operations | +| `GatewayProvisioner` | Creating, retaining, rotating, and revoking upstream user credentials | Agent configuration or inference forwarding | +| Control API | Server-selected gateway mode, policy personalization, encrypted credentials, and startup validation | Enabling gateway mode from client input | + +`gateway.type` selects the compiled adapter. `gateway.provisioner.type` selects +credential lifecycle behavior and may use a different name. + +## Compiled adapter contract + +Every method is pure, so the same static adapter can be used safely by all Blue +binaries: + +```rust +pub trait GatewayAdapter: Send + Sync { + fn kind(&self) -> &'static str; + fn client_route(&self, gateway: &GatewayConfig) -> Result; + fn upstream_path(&self, path_and_query: &str) -> Result; + fn upstream_credential_placement(&self) -> UpstreamCredentialPlacement; + fn inspect_response_status(&self, status: u16) -> bool; + fn classify_invalid_credential( + &self, + status: u16, + body: &[u8], + ) -> Option; +} +``` + +`GatewayRoute` contains only the Blue inference-proxy URL and inference JWT. The +dispatcher attaches the harness-owned `AuthPlacement` and `wire_api` afterward, +so a gateway cannot replace those decisions. Route and wiring debug output +redacts the token. + +`UpstreamCredentialPlacement` declares bearer authentication or a named header; +it never contains the credential itself. The proxy strips `Authorization`, +`X-API-Key`, and the adapter-declared credential header before applying exactly +one resolved credential. Named credential headers cannot be hop-by-hop headers, +`Host`, or `Content-Length`. + +`InvalidCredentialReason` is a provider-neutral closed enum: `NotFound`, +`Blocked`, or `Revoked`. Return a reason only when the upstream response proves +the credential is unusable. Expiration remains a Control API decision. +`inspect_response_status` prevents the proxy from buffering ordinary streaming +responses. For selected statuses, the body is inspected only when its declared +length is at most 64 KiB. + + + During the compatibility release, internal service payloads carry the new + `upstream_credential` and `reason` fields alongside the deprecated + `virtual_key` and `classification` fields. New services accept either shape + and reject conflicting dual values. Do not use the deprecated names in new + integrations. + + +The source layout is: + +```text +crates/gh-gateway/src/ +├── lib.rs # contract, registry, lookup, dispatch, shared types +├── litellm.rs # complete LiteLLM behavior +└── .rs # one module for each additional compiled adapter +``` + +## Choose the integration path + + + + Use the existing bearer placement when the upstream accepts the resolved + credential as `Authorization: Bearer …`. Return the incoming path unchanged + when the gateway exposes the same API paths as the agent. You still must + define which response statuses are inspected and map only definitive + invalid-credential responses to `NotFound`, `Blocked`, or `Revoked`. + + + Select a named credential header or map the incoming path in the adapter. + If the existing pure output types cannot express the behavior, extend the + shared contract and the proxy application code together. Do not add a + gateway-name branch directly to the proxy or a harness writer. + + + +## Add a gateway + + + + Choose a stable lowercase key. Record supported agent protocols, upstream + paths, credential placement, and the exact status/body evidence that proves + a credential is invalid. Treat rate limits, model denial, and transient + authentication infrastructure failures as ordinary upstream responses, not + invalid credentials. + + + Add `crates/gh-gateway/src/.rs` and implement every method. Validate + required client runtime fields in `client_route`; never perform I/O or store + administrator configuration in the adapter. Add one static instance to + `GATEWAY_ADAPTERS` in `lib.rs`. + + + Prefer the existing digest-pinned executable protocol when it can implement + the provider's ensure and revoke calls. Follow + [Custom gateway provisioners](/0.2.0/admin/custom-gateway-provisioners). Add a + built-in `GatewayProvisioner` module and Control API Cargo feature only when + the provider integration must ship in the public server binary. + + + Reuse `type`, `proxy_url`, `token`, and `auth_style` when possible. + `auth_style` is retained for compatibility and must be `bearer`; it describes + agent-to-Blue authentication, not the adapter's upstream credential header. + New non-secret client fields require coordinated changes to `gh-service`, + the canonical OpenAPI contract, Control API personalization, and the docs + snapshot. Provider credentials never enter client policy. + + + Set the same adapter key in `gateway.type` and `HARNESS_GATEWAY_TYPE`. + Compose passes the latter to the proxy. Helm uses `blue.gatewayType` and + injects it into both the Control API and proxy; the value is required when + `blue.enableInferenceProxy=true`. Add the upstream URL, provisioner + configuration, and secrets through the existing deployment mechanisms. + + + Add registry and adapter unit tests, proxy forwarding tests, provisioner + contract tests, deployment rendering checks, gateway-mode golden plans for + every harness, and an end-to-end request through the real gateway. Retain + governance-only coverage to prove it gains no gateway dependency. + + + +## Change checklist + +| Area | Required change | +| --- | --- | +| Shared adapter | New module, static registry entry, pure behavior tests, and supported-key assertion | +| Control API | No dispatch branch; shared registry validation recognizes the new entry automatically | +| Inference proxy | No gateway-name branch; extend only generic application code when the public adapter outputs are insufficient | +| Provisioning | Executable provisioner artifact and digest, or a feature-gated built-in implementation | +| Deployment | Matching gateway type, upstream URL, proxy settings, provisioner, secrets, and gateway-enabled fixtures | +| Public contract | Update and synchronize OpenAPI only when the client policy shape changes | +| Documentation | Configuration example, support statement, upstream prerequisites, and troubleshooting evidence | + +## Verification + +At minimum, demonstrate: + +- Unique, nonempty lowercase registry keys and lookup from all three consumers. +- Rejection of unknown gateway types and non-`bearer` client `auth_style`. +- Correct client route normalization without exposing an inference JWT in debug + output. +- Correct upstream path and credential header, with incoming authentication + headers removed. +- Unchanged streaming for uninspected responses and bounded inspection for + adapter-selected statuses. +- Precise provider-neutral invalid-credential reasons without treating + authorization or availability errors as revoked credentials. +- Replacement—not appending—of an attacker-supplied adapter credential header. +- New-only, legacy-only, matching dual, and conflicting dual internal wire + payloads during the compatibility release. +- Provision, retain, rotate, revoke, invalidate, and bounded-recovery behavior + without credentials in logs. +- Successful inference and user attribution through every supported harness. + +Run focused checks while developing: + +```bash +cargo test -p gh-gateway +cargo test -p control-api --lib +cargo test -p inference-proxy --bin inference-proxy +cargo test -p gh-config --lib +helm lint deploy/helm +helm template blue deploy/helm \ + --set blue.enableInferenceProxy=true \ + --set blue.gatewayType=litellm \ + --set blue.internalTransport.mode=insecure-http +cd apps/docs +npm run sync:contract # only after changing the canonical OpenAPI contract +npm run validate +``` + +Before review, run the complete workspace checks from +[Contributing](/0.2.0/development/contributing) and the relevant gateway E2E +journey. + +## Invariants + +- The server decides whether gateway mode exists; a client may only downgrade + to governance-only mode. +- `gh-config` remains the only writer of agent configuration files. +- Session-bound inference JWTs may reach clients. Upstream credentials and administrator secrets + may not. +- Adapter decisions are pure and contain no credential bytes. +- Governance-only clients gain no hard dependency on the proxy, provisioner, + database, Redis, or upstream gateway. +- Native CLI arguments, terminal behavior, signals, resize handling, and exit + codes remain unchanged. diff --git a/apps/docs/0.2.0/development/harness-adapter-architecture.mdx b/apps/docs/0.2.0/development/harness-adapter-architecture.mdx new file mode 100644 index 0000000..8d4d698 --- /dev/null +++ b/apps/docs/0.2.0/development/harness-adapter-architecture.mdx @@ -0,0 +1,272 @@ +--- +title: "Harness adapter architecture" +description: "Extend Blue across new agent harnesses and breaking harness-version boundaries." +icon: "code-branch" +--- + +## Version-owned implementations + +`gh_common::harness_catalog!` generates harness identity, enumeration, discovery +metadata, and the config implementation registrations. Add a catalog entry and +an `adapters//` family module containing the interval registrations, +shared writer and inspection code, named atomic operation sets, and an initial +version specification. Close the previous breaking-generation interval when +adding a new version. + +### Contract ownership + +Keep each fact in one layer. Public summaries may be derived from a lower layer, +but contributors must not author the same compatibility rule in two registries. + +| Layer | Owns | Must not own | +| --- | --- | --- | +| `gh_common::harness_catalog!` | Harness key, aliases, label, binary names, discovery and installer metadata | Version-dependent capabilities or component rules | +| `adapters//mod.rs` | Ordered version registrations, family operations, shared writer and inspection functions | Organization policy or server-selected behavior | +| `adapters///VersionSpec` | One version range's operation set, capabilities, component rules, and optional feature strategies | Writes outside the family operation contract | +| `compat.rs` | Policy intersection, breaking-generation dispatch, certification ceilings, install selection | Harness-specific config rendering | +| `packages.rs` | Adapter availability, layout-variant selection, verified package activation | Harness-specific placement decisions | +| `plan.rs` | Pure desired writes, removals, ownership, environment, and launch arguments | Direct filesystem mutation | + +The public harness metadata endpoint retains top-level `capabilities` and +`component_rules` for older consumers. Those fields are derived from the version +specifications: capabilities are the union across generations and component +rules describe the current generation. New consumers should read the matching +`generations[]` entry. + +### Family layout + +A harness family follows this shape: + +```text +crates/gh-config/src/adapters// +├── mod.rs # registrations, named Operations sets, shared behavior +├── inspection.rs # native version/config inspection +├── writer.rs # pure rendering helpers +├── v0_0_0/ +│ └── mod.rs # initial VersionSpec +└── vX_Y_Z/ + └── mod.rs # later breaking-generation VersionSpec +``` + +The directory name identifies the inclusive compatibility boundary. It is not a +copy of the whole adapter. A version specification selects a complete named +operation set from the family and declares optional features explicitly. + +Rust has trait defaults and dynamic dispatch, but no class inheritance or class +method overriding. Harness families therefore use composition: one +`HarnessImplementation` delegates through an immutable `VersionSpec`. Every +spec selects a complete `Operations` table, while `Feature` requires each +optional capability to be either `Supported(strategy)` or +`Unsupported(reason)`. A version that changes only one capability reuses the +existing operation set and changes that field. A version that changes paths, +rendering, inspection, package placement, gateway wiring, transcript discovery, +installation, launch behavior, or native update controls adds a named operation +set in the family module. Existing specs and operation sets remain unchanged. + + + Reusing an operation set is an assertion that every operation remains compatible. + If only an optional feature changes, reuse the operation set and change the + feature strategy in the new `VersionSpec`. + + +Implementations declare `ImplementationPaths`, consume immutable, package-grouped +`ResolvedPackages`, and construct `ReconcilePlan` in memory. The plan contains +bytes, modes, removals, ownership, environment, and launch arguments. Shared plan +helpers serialize documents and expand component trees; they never write a real +or simulated home. `PreparedPackages` and pending activation state stay private +to the engine. + +The engine validates authority, snapshots affected files, applies the plan, and +commits package activation and V4 compatibility state together. Previous ownership +permits stale cleanup but is never added to new ownership. Native migration +permissions are exact paths. Intermediate symlinks and final write symlinks are +rejected; exact authorized symlink removals are snapshotted without following the +link. Revision transactions include prior ownership before any harness commits. + +Installed binaries require successful version detection. Inspection uses persisted +profiles when present; unknown profiles fail. Legacy `*-v1` names explicitly alias +the implementations that preserve their previous behavior. New reconciliation +writes canonical profile names while retaining state schema 4. Cleanup without +state does not infer ownership. Installation selects the newest interval that +intersects the policy and its certified release ceiling, and includes both in +the selector; the CLI probes the installed binary again afterward. + +Installation has two parts. The selected implementation produces the +policy-constrained install selector. The interactive CLI executes it only after +confirmation and verifies the same executable path it detected before repair. +Harnesses with standalone installers may resolve the selector to a concrete +published version and update that installation in place; they must not install a +second binary that remains shadowed on `PATH`. + +Wrapped-launch update suppression is also generation-owned. Each named +`Operations` set declares how its harness version disables native update checks +or automatic installation. The same operation decorates both a newly reconciled +plan and the read-only launch path, preventing their environment and arguments +from diverging. Blue applies the operation when the organization range has a +maximum or the effective limit is the compiled certification ceiling. It skips +the operation only when `allow_unverified_versions: true` accompanies an +explicit range without an upper bound. If a vendor changes or removes its update +control, add a new operation set at that version boundary rather than changing a +shipped set. + +Every generation declares two different upper bounds. `before` is the optional +breaking-format boundary used for dispatch. `verified_before` is the mandatory +exclusive ceiling through which the implementation has been tested. Releases +at or above that ceiling fail closed even when they remain in the same breaking +generation. An organization may deliberately accept that risk only by setting +both an explicit `version_requirement` and `allow_unverified_versions: true`. +Successful reconciliation then emits a warning that vendor changes may break +the generated configuration. The server adds the +`unverified_harness_versions` required capability so older clients cannot +silently ignore the opt-in. + +Support metadata is generation-owned. Each `VersionSpec` exposes its own +component rules and optional capabilities, and package adapters are validated +against every compatible generation reachable through the harness policy. +Adding a capability to one generation therefore does not falsely advertise it +for older generations. + +The central harness catalog owns invariant identity, discovery, and installer +metadata only. Behavioral capabilities and component rules live exclusively in +`VersionSpec`; public top-level summaries are derived from those specs for +backward compatibility. Package adapters separately declare their harness +availability with `introduced` and optional `before` bounds. Layout `variants` +must remain inside that availability interval. + +## Contributor workflows + +### Certify another release without a breaking change + +Use this path when the existing writer, paths, package placement, hooks, gateway +wiring, inspection, and launch behavior still work. + +1. Update the stable harness release in `tests/e2e/agents.lock.json`. +2. Exercise that exact release through the harness matrix and inspect its native + files and launch behavior. +3. Advance only the current registration's exclusive `verified_before` ceiling + to the next patch's `-0` sentinel. For a verified `1.18.25`, use `1.18.26-0`. +4. Update version-output fixtures and rerun registry, golden, and E2E tests. + +Do not create a new version directory merely to advance certification. + +### Add a breaking version boundary + +Use this path when native behavior changes across a release boundary. + +1. Set the previous registration's exclusive `before` to the first affected + version. +2. Add a version directory whose `VersionSpec` begins at that exact version. +3. Reuse an existing named `Operations` set when every operation remains valid. + Otherwise add a new complete operation set in the family module. +4. Declare every optional feature as `Supported(strategy)` or + `Unsupported(reason)` and define the generation's component rules. +5. Add version-output fixtures and golden plans for the release immediately + below the boundary and the release at the boundary, in governance-only and + gateway modes. +6. Retain the previous specification and operation set so older installations + continue to dispatch deterministically. + +Intervals are contiguous and half-open: `[introduced, before)`. The final +breaking generation may omit `before`, but its `verified_before` remains finite. + +### Add a new harness + +1. Add one catalog entry with stable identity, binary probes, and installer + metadata. +2. Create the adapter family, shared writer and inspection modules, initial + operations, and `v0_0_0/VersionSpec`. +3. Implement the entire `HarnessImplementation` contract, including pure plan, + launch, native update controls, gateway wiring, component validation, package + placement, inspection, transcript discovery, and installation. +4. Add the harness to explicit documentation and E2E matrices. Runtime dispatch + and API harness keys come from the compiled catalog and require no second enum. +5. Add golden plans, version parsing fixtures, transaction/rollback coverage, + and at least one unsupported-package assertion. + +### Change package compatibility + +Adapter-level `[introduced, before)` describes when the package works with a +harness. Nested variants describe archive layout changes only inside that +availability interval. A variant-only adapter must cover the complete +availability range contiguously. A fallback layout permits gaps, but overlapping +variants are always invalid. + +Governance validation intersects package availability, variants, harness policy, +breaking generations, and certified ceilings. It also checks the effective +version-selected adapters together so helper-name collisions hidden inside +variants fail before publication. + +```yaml +adapters: + claude: + introduced: 2.0.12 + before: 3.0.0 + variants: + - introduced: 2.0.12 + before: 2.5.0 + plugin_dir: toolkit/claude-v2 + - introduced: 2.5.0 + before: 3.0.0 + plugin_dir: toolkit/claude-v2_5 +``` + +## Production boundaries and evidence + +- Codex: `0.145.0` separates automatic `SessionEnd` upload from the legacy + implementation. [The upstream introduction](https://github.com/openai/codex/commit/7bd44085e1650c406533745348e3c54f072ce5f6) + adds the lifecycle event; earlier inline hooks do not establish support for it. +- Codex: `0.114.0` separates the `SessionStart` boundary. `SessionStart` exists + from `0.114.0` even though `SessionEnd` only arrives at `0.145.0`, so the + `codex-v0_114_0` interval declares the `session_start` capability while leaving + `session_upload` unsupported. On this interval Blue records the + `BLUE_SESSION_ID` → native-session mapping at start and uploads the transcript + from `blue run`'s post-exit fallback rather than a `SessionEnd` hook. See the + upstream [`SessionStart` hook documentation](https://github.com/openai/codex). +- Claude: `1.0.38` introduces hooks and `2.0.12` introduces plugin packaging. + The hooks-only interval accepts hook components but rejects plugin packages, + standalone skills, and agents. Automatic upload is conservatively disabled + throughout that interval: `SessionEnd` arrived at `1.0.85`, so it is not + supported uniformly. These releases are recorded in the + [upstream changelog](https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md). +- Kimi and OpenCode retain one production interval each. Candidate transitions + and the evidence still needed are recorded in + `crates/gh-config/tests/fixtures/future-boundaries.json`; they are not production + boundaries. + +## Contract tests and verification + +`crates/gh-config/src/contract_tests.rs` covers normalized golden plans for each +interval in both operating modes, source-home purity, exact boundaries, profile +aliases, ownership, symlinks, production transitions, activation failure, +revision rollback, and a synthetic two-version definition with different paths, +schemas, component placement, hooks, and launch arguments. Adapter tests also +assert that unchanged versions reuse their family operation table and that +unsupported capabilities carry a nonempty reason. Golden paths and the Blue +executable are normalized to `$HOME` and `$BLUE`. + +Update fixtures intentionally, then rerun without the environment variable: + +```bash +BLUE_UPDATE_GOLDENS=1 cargo test -p gh-config --lib +cargo test -p gh-config --lib +``` + +Registry tests require each certified ceiling to be valid and ensure the active +ceiling is exactly `next-patch-0` beyond the stable release pinned in +`tests/e2e/agents.lock.json`. Updating that lock therefore forces maintainers to +make newly verified support explicit. + +Before review, verify these invariants: + +- Every breaking interval is ordered, contiguous, and selects exactly one spec. +- Every `verified_before` is exclusive and no later than the breaking `before`. +- No version-dependent capability or component rule is authored in the catalog. +- Plans remain pure, and only the transaction engine writes or removes files. +- Package availability covers every harness version allowed by the organization + policy, unless that package is disabled for the harness. +- Installer repair updates and rechecks the originally detected executable. +- Native update controls come from the selected operation set and match between + fresh reconciliation and cached launch resolution. +- Old specs, operation sets, fixtures, and golden plans remain present. + +Run the complete repository checks documented in [Contributing](/0.2.0/development/contributing). diff --git a/apps/docs/0.2.0/development/local-compose.mdx b/apps/docs/0.2.0/development/local-compose.mdx new file mode 100644 index 0000000..5e2970f --- /dev/null +++ b/apps/docs/0.2.0/development/local-compose.mdx @@ -0,0 +1,120 @@ +--- +title: "Local development with Docker Compose" +description: "Run Blue's reference services locally for development and end-to-end evaluation." +icon: "docker" +--- + +This workflow is for repository maintainers and contributors. Follow the [CLI install steps](/0.2.0/cli/commands#install) if your organization already operates Blue. Operators designing a deployment should start with the platform-neutral [Deployment contract](/0.2.0/deployment/runtime-contract); the Compose defaults are not a production baseline. + +## Prerequisites + +- A checkout of the Blue source repository +- Docker with Compose v2 + +## Core stack + +```bash +cd deploy +docker compose up -d --build +docker compose ps +``` + +Run Compose from `deploy/` so it loads `deploy/.env`. Running `docker compose +-f deploy/docker-compose.yml` from the repository root reads a root `.env` +instead and can silently select the password-mode defaults. + +| Service | Local address | Purpose | +| --- | --- | --- | +| Dashboard | `http://127.0.0.1:3000` | Login and administration UI | +| Control API | `http://127.0.0.1:8080` | Policy, gateway, client, and session API | +| PostgreSQL | `127.0.0.1:5433` | Authentication and control-plane state | +| MinIO API | `http://127.0.0.1:9000` | Raw-session object storage | +| MinIO console | `http://127.0.0.1:9001` | Local storage administration | + +Create `deploy/.env` before testing authentication or sharing a development environment: + +```dotenv +HARNESS_BOOTSTRAP_ADMIN_EMAIL=operator@example.com +HARNESS_BOOTSTRAP_ADMIN_PASSWORD=replace-with-a-long-random-password +BETTER_AUTH_SECRET=replace-with-at-least-32-random-bytes +MINIO_ROOT_USER=replace-local-storage-user +MINIO_ROOT_PASSWORD=replace-local-storage-password +# Version label displayed beside the dashboard logo; release builds inject this into the image. +BLUE_DEPLOYMENT_VERSION=0.2.0 +``` + +The dashboard's **Documentation** sidebar link opens the centrally hosted documentation. + +The version beside the dashboard logo identifies the self-hosted deployment image. It is intentionally independent from the centrally distributed `blue` metaharness version shown by `blue version`. + +Compose mounts `deploy/blue.yaml` by default. It is governance-only: gateway routing and session capture are both disabled. Its `package_catalog` section includes a Ponytail recipe pinned to an immutable upstream commit and RTK's official platform-specific release archives; clients verify every archive's SHA-256 before activation. + +## Gateway profile + +The gateway overlay is explicit and selects both the server gateway block and the inference-proxy profile. Blue does not include the upstream gateway in this stack. The Control API recursively merges mappings from `BLUE_CONFIG_OVERLAY_FILES`; overlay scalars and sequences replace their base values. + +To enable gateway mode, start your LiteLLM gateway first. LiteLLM is the first and currently supported gateway integration. Its HTTP API must be available on host port `4001`, and its master key must be present as `LITELLM_MASTER_KEY` in the environment file. Then start Blue with the inference-proxy profile: + +```bash +COMPOSE_PROFILES=gateway \ +BLUE_CONFIG_OVERLAY_FILES=/etc/blue/blue.gateway.yaml \ +docker compose \ + --env-file /path/to/ai-gateway/.env \ + up -d --build +``` + +The inference proxy listens on `http://127.0.0.1:8081` and forwards to the gateway expected on host port `4001`. + +The Compose defaults set the other required runtime values: + +```dotenv +HARNESS_GATEWAY_TYPE=litellm +HARNESS_GATEWAY_URL=http://host.docker.internal:4001 +LITELLM_MASTER_KEY=replace-with-the-running-gateway-master-key +HARNESS_PROXY_OAUTH_CLIENT_SECRET=replace-with-a-long-random-internal-secret +HARNESS_GATEWAY_ENCRYPTION_KEY=replace-with-a-base64-encoded-32-byte-key +HARNESS_GATEWAY_REQUEST_LOG_RETENTION_DAYS=30 +``` + +The inference proxy requires `HARNESS_GATEWAY_TYPE`; Compose defaults it to +`litellm`. When testing another compiled adapter, set this variable to the same +value as top-level `gateway.type` in the `blue.gateway.yaml` overlay. +`HARNESS_LITELLM_BASE_URL` remains a binary-level compatibility alias for one +release, but the shipped Compose configuration uses `HARNESS_GATEWAY_URL`. + +The inference proxy authenticates to the Control API with a short-lived OAuth2 client-credentials token from better-auth. The same `HARNESS_PROXY_OAUTH_CLIENT_SECRET` is passed to the dashboard (to seed the `blue-inference-proxy` confidential client) and to the inference proxy (to obtain tokens). Compose also points `HARNESS_GATEWAY_LOG_URL` at the internal request-log ingestion endpoint. The example uses the pinned `/etc/blue/local-gateway-provisioner.mjs` local executable rather than the built-in provisioner. Editing that file changes its digest and prevents startup until `executable_sha256` in `deploy/blue.gateway.yaml` is updated. Create a LiteLLM user with the same email as the dashboard/CLI identity, then open **Gateway** or run `blue gateway` to provision the user's managed key. + +## Session-capture variants + +Session capture remains independent from gateway routing. Select the capture-only configuration with: + +```bash +BLUE_CONFIG_OVERLAY_FILES=/etc/blue/blue.capture.yaml \ + docker compose up -d --build +``` + +To enable both features, select both overlays together with the gateway profile: + +```bash +COMPOSE_PROFILES=gateway \ +BLUE_CONFIG_OVERLAY_FILES=/etc/blue/blue.gateway.yaml,/etc/blue/blue.capture.yaml \ +docker compose \ + --env-file /path/to/ai-gateway/.env \ + up -d --build +``` + +The local Compose stack explicitly uses +`HARNESS_INTERNAL_TRANSPORT_MODE=insecure-http` between the proxy and Control +API to avoid local certificate setup. M2M authentication is still required, but +resolved virtual keys are plaintext on the Docker network. This is a development +convenience, not the production default. Use Helm's `mtls` mode outside a trusted +local environment, or deliberately accept and document the trusted-network risk. + +## Stop the stack + +```bash +cd deploy +docker compose down +``` + +Named volumes retain PostgreSQL and MinIO data. Adding `--volumes` deletes local state and should be used only when a full reset is intended. diff --git a/apps/docs/0.2.0/introduction.mdx b/apps/docs/0.2.0/introduction.mdx new file mode 100644 index 0000000..16ea168 --- /dev/null +++ b/apps/docs/0.2.0/introduction.mdx @@ -0,0 +1,62 @@ +--- +title: "Blue" +description: "Understand how Blue governs coding-agent configuration and dynamically distributes extensions." +icon: "shield-halved" +--- + +Blue is an open-source wrapper for coding-agent command-line tools. It lets an organization publish one policy, dynamically distribute or revoke approved extensions and MCP servers, reconcile that policy onto developer machines, optionally route inference through its own inference gateway, and optionally capture native session transcripts. Blue does not bundle the upstream gateway: LiteLLM is the first and currently supported integration. + +It supports Codex, Claude Code, Kimi Code, and OpenCode. Developers keep each agent's native terminal experience: arguments, terminal UI, signals, and exit status pass through the wrapper. + +Blue lets an organization adopt multiple agents, model providers, and self-hosted inference while still managing policy, identity, credentials, and activity in one place. Read [Why Blue](https://bluee.sh/manifesto) for the full case for a metaharness and the principles that shape the project. + + + + Understand the governance problem Blue is designed to solve. + + + Deploy the services, connect the CLI, and launch a governed agent. + + + Map the workload contract onto Kubernetes, your scheduler, or virtual machines. + + + +## What it controls + +- Which supported harnesses are allowed. +- Managed model, approval, sandbox, and harness-specific settings. +- Additive organization-managed MCP server definitions. +- Digest-pinned skills, plugins, hooks, subagent definitions, and helper binaries. +- Gateway routing with session-bound inference JWTs. +- Opt-in native session capture and upload to S3-compatible storage. + +## Dynamic extensions are a core capability + +Operators can publish extension and MCP changes from the dashboard without reinstalling Blue or the native coding agents. Each saved change creates a governance revision. Clients reconcile it during `blue apply`, before a governed launch, or during daemon polling; login only updates authentication. + +A revision can: + +- Add or remove organization-managed MCP servers from Blue's launch-scoped overlays. A developer's local MCP with the same name always wins and is never overwritten or removed. +- Activate, update, or deactivate digest-pinned skills, native plugins, hooks, subagents, OpenCode plugin modules, and platform helper binaries. +- Deploy an extension to everyone or selected members, with mappings and settings chosen for each compatible harness. +- Repair missing or changed managed content. When an extension is removed, Blue unregisters it; unchanged Blue-owned content is cleaned up, while locally modified content is quarantined instead of deleted. + +See [Managed packages](/0.2.0/admin/managed-packages) for publishing and removal, and [Configuration lifecycle](/0.2.0/concepts/configuration) for reconciliation behavior. + +## What it does not control + +Blue is configuration governance, not an operating-system sandbox. A determined local user can still edit files or invoke an agent directly unless separate device-management controls prevent it. Direct agent invocations use native user configuration; governed overlays are loaded only through `blue` or installed shims. + + + Session capture can contain prompts, responses, tool inputs, source paths, and secrets present in native transcripts. Operators must disclose collection and retention and restrict access to the stored artifacts. + + +## Choose an operating mode + +| Mode | Inference | Credentials on the client | +| --- | --- | --- | +| Governance-only | Agent talks directly to its configured provider | Native agent credentials | +| Bring your own gateway | Agent talks to the Blue inference proxy, which forwards to your gateway | A session-bound inference JWT | + +Read [Architecture](/0.2.0/concepts/architecture) for the complete data flow. diff --git a/apps/docs/0.2.0/quickstart.mdx b/apps/docs/0.2.0/quickstart.mdx new file mode 100644 index 0000000..05cae4b --- /dev/null +++ b/apps/docs/0.2.0/quickstart.mdx @@ -0,0 +1,147 @@ +--- +title: "Quickstart" +description: "The three things you need to run Blue: the CLI, a config file, and a server." +icon: "rocket" +--- + +Blue governs coding-agent CLIs (Codex, Claude Code, Kimi Code, OpenCode) from one place. Developers keep their native agent. Your organization decides which agents run and how they are configured, and can optionally route their inference through a gateway you operate. + +You need three things: + +| | What it is | Who uses it | +| --- | --- | --- | +| **1. The Blue CLI** | The `blue` binary that launches an agent under policy | Developers | +| **2. A config file** | `blue.yaml`: your policy and the server's settings | Admins and platform | +| **3. A Blue server** | The control plane the CLI connects to | Platform | + +## 1. The Blue CLI + +Developers install one binary and launch their agent through it. Each launch signs in, fetches the latest policy for that developer, applies it as a launch-scoped overlay, verifies any managed extensions, and starts the native agent. Arguments, terminal UI, signals, and exit codes pass straight through. + + +```bash macOS and Linux +curl --proto '=https' --tlsv1.2 -LsSf https://github.com/BlocksOrg/blue/releases/latest/download/install.sh | sh +``` + +```powershell Windows PowerShell +irm https://github.com/BlocksOrg/blue/releases/latest/download/install.ps1 | iex +``` + + +Connect it to your server and launch an agent: + +```bash +blue setup # enter the Control API URL, e.g. https://api.blue.example.com, then sign in +blue doctor # which agents are installed, and what policy allows +blue codex # launch Codex under policy; or claude, kimi, opencode +``` + +Bare `blue` launches your preferred agent. Hold Ctrl and press `]` inside a session for status, health, and reload controls. See [CLI commands](/0.2.0/cli/commands) for installation options and the full command list, including `blue status` and `blue verify` for scripts. + +## 2. The config file + +`blue.yaml` is where your organization's policy starts. Its `governance` section manages your developers' coding-agent configuration: + +- **Which agents are allowed**, and which versions. +- **Default model and settings** per agent: model, reasoning effort, approval policy, sandbox mode, and other agent-native options. +- **MCP servers** added to every governed launch. +- **Extensions**: digest-pinned skills, plugins, hooks, subagents, and helper binaries. +- **Session capture**, if you turn it on. + +Developers get this policy on every launch. Their personal agent config is not overwritten; Blue applies policy as a launch-scoped overlay. After the first deployment, admins edit policy from the dashboard, which creates a new revision without touching the file. + +The file also has a `control_api` section with the server's runtime settings: database, storage, and the public URLs. Secrets never go in the file; `os.environ/NAME` reads them from the environment at startup. + +A minimal governance-only file: + +```yaml +governance: + revision: "2026-09-15.1" + contract_version: 3 + required: true + allowed_harnesses: [codex, claude, kimi, opencode] + harnesses: + codex: + managed_config: + model: gpt-5.6-sol + approval_policy: on-request + claude: + managed_config: + model: claude-opus-4-8 + +control_api: + database_url: os.environ/HARNESS_DATABASE_URL + bootstrap: + admin_email: os.environ/HARNESS_BOOTSTRAP_ADMIN_EMAIL + auth: + public_url: https://blue.example.com # dashboard, as users reach it + audience: https://api.blue.example.com # Control API, as the CLI reaches it + issuer: https://blue.example.com/api/auth + client_id: blue-cli + session_url: http://blue-blue-dashboard:3000/api/auth/get-session # dashboard, as the server reaches it + jwks_url: http://blue-blue-dashboard:3000/api/auth/jwks + blob_storage: + bucket: os.environ/HARNESS_BLOB_BUCKET + region: us-west-2 + package_artifacts: + bucket: os.environ/HARNESS_PACKAGE_BUCKET +``` + +Adding one top-level `gateway` section turns on gateway mode. The release bundle ships a starter with every section filled in. See [Configure blue.yaml](/0.2.0/deployment/blue-yaml) for every field, and [Governance configuration](/0.2.0/reference/governance-config) for the full policy schema. + +## 3. The Blue server + +The server is the control plane. It is one container image run as three workloads, on top of PostgreSQL and two S3-compatible buckets. It reads `blue.yaml` from `/etc/blue/blue.yaml`. + +| Workload | What it does | +| --- | --- | +| Dashboard | Browser sign-in and administration. Issues the OAuth tokens the CLI uses | +| Control API | Serves each developer their personalized policy. The CLI talks to this | +| Worker | One replica of the Control API that runs the cleanup and reconciliation loops | + +**In governance-only mode** the server manages sign-in and membership, stores every policy revision, serves each developer the policy that applies to them, tracks which clients are on which revision, and stores captured sessions if enabled. Inference is untouched: agents keep talking to their own provider with their own credentials. + +**In gateway mode** the server also runs an inference proxy in front of a LiteLLM gateway you operate. It provisions one gateway key per user, keeps that key and the provider keys server-side, and hands each governed launch a short-lived token instead. Agents send inference to the proxy, the proxy swaps the token for the real key, and LiteLLM forwards to the provider. Nothing secret lands on the developer machine. + +### How to deploy + +The application contract is the same on every platform. Your platform team owns placement, networking, secret delivery, scaling, and rollout policy. + + + + Use the maintained chart and optional AWS OpenTofu starter as a reference production implementation. + + + Map the image commands, ports, health checks, configuration, and dependencies onto ECS, Nomad, VMs, or another scheduler. + + + Run the reference stack with Docker Compose before designing a production deployment. + + + + + Docker Compose includes development credentials and trusted-network settings. Use it to evaluate Blue, not as an unchanged production template. + + +Two public HTTPS origins are exposed: the dashboard and the Control API. Gateway mode adds a third for the inference proxy. The [Deployment contract](/0.2.0/deployment/runtime-contract) lists the image commands, ports, health checks, required environment variables, and the external dependencies each mode needs. + + + The dashboard accepts the bootstrap admin login, `blue doctor` shows your agent as allowed, and the agent opens under the organization's policy. + + +## Next steps + + + + Every runtime and policy field, secret references, and reconciliation rules. + + + Workloads, ports, health checks, and dependencies for any platform. + + + Turn on gateway mode and route inference through your LiteLLM. + + + Publish skills, plugins, hooks, and MCP servers from the dashboard. + + diff --git a/apps/docs/0.2.0/reference/auth-endpoints.mdx b/apps/docs/0.2.0/reference/auth-endpoints.mdx new file mode 100644 index 0000000..c78c441 --- /dev/null +++ b/apps/docs/0.2.0/reference/auth-endpoints.mdx @@ -0,0 +1,35 @@ +--- +title: "Authentication endpoints" +description: "Reference the dashboard-origin endpoints used by browser sessions and OAuth device authorization." +icon: "fingerprint" +--- + +Better Auth is mounted below `/api/auth` on the dashboard origin. These routes are distinct from the Control API endpoints in the generated API reference. + +## CLI device flow + +| Method and path | Purpose | +| --- | --- | +| `POST /api/auth/device/code` | Issue a device code, user code, verification URL, polling interval, and expiry. | +| `GET /device/{browser-token}` | Display the short-lived, read-only browser approval screen. | +| `GET, POST /api/device/{browser-token}` | Claim and approve or deny the linked request without accepting an editable user code from the browser. | +| `GET /api/auth/device?user_code=…` | Resolve a pending user code for approval. | +| `POST /api/auth/device/approve` | Approve the device grant using the authenticated browser session. | +| `POST /api/auth/device/deny` | Deny the device grant using the authenticated browser session. | +| `POST /api/auth/oauth2/token` | Exchange a device code or refresh token for audience-bound tokens. | +| `POST /api/auth/oauth2/revoke` | Revoke the refresh-token family during logout. | + +The public native client ID is `blue-cli`. It uses no client secret and requests the Control API URL as its OAuth resource. The device-code response retains the standard `verification_uri_complete` field, but the bundled dashboard rewrites it to an opaque temporary link instead of placing the user code in the query string. + +## Dashboard sessions + +| Method and path | Purpose | +| --- | --- | +| `POST /api/auth/sign-in/email` | Create a dashboard browser session. | +| `POST /api/auth/sign-out` | End the current browser session. | +| `GET /api/auth/get-session` | Resolve the HTTP-only dashboard session for server-side API authorization. | +| `GET /api/auth/jwks` | Publish keys used by the Control API to validate CLI access tokens. | + + + Better Auth owns the detailed request and response contract. Treat these paths as integration endpoints for the bundled dashboard and CLI, not as a general-purpose identity API. + diff --git a/apps/docs/0.2.0/reference/governance-config.mdx b/apps/docs/0.2.0/reference/governance-config.mdx new file mode 100644 index 0000000..8e3cbff --- /dev/null +++ b/apps/docs/0.2.0/reference/governance-config.mdx @@ -0,0 +1,154 @@ +--- +title: "Governance configuration" +description: "Reference the organization policy fields delivered to Blue clients." +icon: "file-code" +--- + +This is the complete policy delivered to clients. The mounted deployment YAML supplies the baseline; the Control API merges dashboard-published packages, package overrides, and MCP servers into new revisions without overwriting saved conflicts. Unknown managed-config keys are preserved for forward compatibility, while top-level contract fields are validated. + +```yaml +revision: "organization-managed" +contract_version: 3 +required_capabilities: [adapter_intervals, compiled_harness_registry, transactional_reconcile, versioned_state, gateway_inference_jwt] +ttl_seconds: 300 +required: true +allowed_harnesses: [codex, claude, kimi, opencode] +minimum_client_version: "0.1.0" + +session_upload: + presign_url: https://control.example.com/session-uploads/presign + +telemetry: + sink_url: https://control.example.com/telemetry + +gateway: + type: litellm + auth_style: bearer + +packages: + - id: organization-toolkit + name: Organization Toolkit + version: "1.0.0" + source_ref: https://artifacts.example.com/agent-packages/toolkit-1.0.0.tar.gz + sha256: "64-character-lowercase-sha256-digest" + platform_sources: + linux-x86_64: + source_ref: https://artifacts.example.com/agent-packages/toolkit-linux-x86_64.tar.gz + sha256: "64-character-lowercase-sha256-digest" + settings: + mode: standard + adapters: + codex: + plugin_dir: toolkit + claude: + plugin_dir: toolkit + kimi: + skills_dir: toolkit/skills + +harnesses: + codex: + version_requirement: ">=0.149.0, <0.150.0" + package_overrides: + organization-toolkit: + enabled: true + settings: + mode: strict + managed_config: + model: gpt-5.6-sol + reasoning_effort: medium + fast_mode: false + approval_policy: never + sandbox_mode: workspace-write + mcp: + - name: source-control + url: https://mcp.example.com/api + transport: streamable-http + env: { MCP_REGION: us-west-2 } + disabled: false +``` + +## Top-level fields + +| Field | Meaning | +| --- | --- | +| `revision` | Immutable server revision returned to clients. The server replaces editor input with its stored revision identifier. | +| `contract_version` | Governance wire contract used for capability negotiation. | +| `required_capabilities` | Client features required before this document may replace the cache. | +| `ttl_seconds` | Cache lifetime before a client checks for a newer revision. | +| `required` | When enabled, clients fail closed without fresh configuration. | +| `allowed_harnesses` | Harness names permitted by policy. | +| `harnesses` | Per-harness policy keyed by supported harness name. | +| `minimum_client_version` | Operator-visible rollout floor; it does not replace capability enforcement. | +| `required_client_version` | Optional canonical exact SemVer recommendation for the Blue CLI. Defaults to the control API deployment version and requires `tenant_client_version_pin`. | +| `packages` | Organization-selected, immutable extension packages and their harness adapters. | +| `gateway` | Global inference routing policy applied to every allowed coding agent. | +| `session_upload` | Global portable-session capture policy; compatible profiles upload integrity-checked bundles while older clients remain accepted as non-resumable raw artifacts. | +| `telemetry` | Optional metadata event destination containing `sink_url`. | + +## Harness policy + +`managed_config` contains model and harness-specific settings. `mcp` is additive. `package_overrides` may enable or disable an organization package and provide adapter settings; it cannot replace the source, version, or digest. `minimum_client_version` remains visible to operators, while `required_capabilities` is the enforcement mechanism for contract features. + +Managed Codex launches default to `approval_policy: on-request`, preserving native approval prompts while the configured sandbox remains active. An explicit `approval_policy` remains authoritative. The provider-neutral `auto_approve: true` selects `never` only when no explicit Codex approval policy is present; `auto_approve: false` and an absent value both select `on-request`. + +When upgrading, configurations that explicitly set `approval_policy`, +`auto_approve`, or `required` keep that choice. A deployment that previously +relied on Codex's omitted approval setting must add `approval_policy: never` or +`auto_approve: true` if unattended execution is intentional. New production +baselines set `required: true`; use an explicit `required: false` only when +launching from stale cached governance during a control-plane outage is an +accepted risk. + +The presence of top-level `gateway` enables gateway mode for every allowed coding agent and connects Blue to an upstream gateway operated by your organization. Blue does not bundle that gateway. `litellm` is the first and currently only supported type. The Control API injects `proxy_url` and the authenticated user's session-bound inference `token`; each agent inherits its model from its own `managed_config`, or leaves model selection to the agent when none is managed. `auth_style` remains on the wire, but `bearer` is the only supported client-to-proxy value. + +OpenCode is the exception to leaving model selection to the agent. Its governed runtime config merges over the user's own, so an unpinned model would survive next to the governed provider it no longer resolves against. When OpenCode has no managed model, Blue keeps the agent's native model if the gateway serves it, otherwise pins a governed default, and warns at launch so the gap stays visible. Set `managed_config.available_models` to the list your gateway actually serves to control that catalog. + +## Managed packages + +Harness policies may declare a semver `version_requirement`. Blue parses the installed CLI's native version output and fails before changing files or launching when the version is missing, unparsable, or outside that range. Package adapters may add ordered `variants` with inclusive `introduced` and optional exclusive `before` boundaries; the top-level adapter fields are the fallback layout. See [Harness version compatibility](/0.2.0/concepts/harness-version-compatibility) for interval migration and lifecycle behavior. + +Packages can contain skills, hooks, agent definitions, plugins, and helper executables. Every archive is pinned by SHA-256 and declares explicit archive-relative adapters rather than running an installer script. + +| Adapter field | Meaning | +| --- | --- | +| `introduced` | Inclusive harness-version lower bound where the package adapter is available; defaults to `0.0.0`. | +| `before` | Optional exclusive harness-version upper bound for adapter availability. | +| `plugin_dir` | Agent-native plugin root. Claude receives it through `--plugin-dir`; Codex registers it in the governed profile. | +| `skills_dir` | Exact skill folder containing `SKILL.md`, or a directory containing several skill folders. | +| `agents_dir` | Directory containing agent/subagent Markdown definitions. | +| `hooks_file` | Agent-native hook fragment included in the governed runtime overlay. | +| `plugins` | Agent-native plugin module paths, primarily for OpenCode. | +| `helpers` | Executables keyed by command name and `os-architecture` package path. | +| `variants` | Ordered, non-overlapping layout intervals contained within adapter availability. Each selected variant replaces the fallback component fields. | + +Adapter availability describes whether the package supports a harness release; +variants describe how its archive layout changes across supported releases. A +variant-only adapter must cover its full availability range without gaps. When +fallback component fields exist, unmatched points inside availability use that +fallback. Governance publication rejects a package whose availability does not +cover the organization policy for an enabled harness. + +Package-level `platform_sources` maps exact `os-architecture` keys to alternate source, artifact ID, and digest triples. Package-level `settings` supplies defaults that per-harness overrides can replace. + +Package paths must be relative, remain inside the verified archive, and exist on the selected platform. Links and special archive entries are rejected. Package settings are exposed to the governed child as `HARNESS_PACKAGE__SETTINGS` JSON. + +Each MCP server has a `name` and either a stdio `command` (plus optional `args` and `env`) or a remote `url` (plus optional `transport`). Set `disabled: true` to retain a definition without activating it. + +Standalone skills can live anywhere in an archive and do not require a native plugin root. Blue generates harness-owned activation metadata for Codex and Claude. Codex and Claude agents or hooks still use the native package root named by `plugin_dir`. Kimi can load standalone agent directories and a TOML hook fragment. OpenCode hooks are plugin modules listed under `plugins`. + +The dashboard can inspect `github:owner/repository@ref` for a public GitHub repository, a public HTTPS archive, or a deployment-managed GitHub/Bitbucket connection. Public GitHub refs resolve to immutable codeload URLs; clients download public sources directly and verify their exact digest. Managed repositories are resolved to an immutable commit and mirrored into organization-scoped object storage. Governance contains an `artifact_id` and exact digest; clients use their existing harness session to request a fresh short-lived download and never receive repository credentials. + +Removing a package disables it for subsequent governed launches. Unmodified owned content is deleted; locally changed content is moved to quarantine and reported as drift. + +## Recommended Blue CLI release + +The reference control API advertises its deployment version as the exact +recommended Blue CLI release. Set +`governance.required_client_version` to a published, compatible release to +override that default. The value remains an exact canonical SemVer, not a range. +`minimum_client_version` remains informational. + +The `tenant_client_version_pin` capability is required whenever a pin is served. +The `X-Blue-Required-Client-Version` header exposes the same pin on successful +config responses and capability/contract rejections (HTTP 426). Older clients +without this capability cannot consume the document. diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 152f9d8..8b076ed 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -61,77 +61,78 @@ "navigation": { "versions": [ { - "version": "0.1.0", + "version": "0.2.0", "groups": [ { "group": "Start here", "pages": [ - "0.1.0/introduction", - "0.1.0/quickstart" + "0.2.0/introduction", + "0.2.0/quickstart" ] }, { "group": "Deployment", "pages": [ - "0.1.0/deployment/runtime-contract", - "0.1.0/deployment/production", - "0.1.0/deployment/blue-yaml", - "0.1.0/deployment/managed-repositories" + "0.2.0/deployment/runtime-contract", + "0.2.0/deployment/production", + "0.2.0/deployment/blue-yaml", + "0.2.0/deployment/managed-repositories" ] }, { "group": "Concepts", "pages": [ - "0.1.0/concepts/architecture", - "0.1.0/concepts/configuration", - "0.1.0/concepts/harness-version-compatibility", - "0.1.0/concepts/gateway-mode", - "0.1.0/concepts/authentication" + "0.2.0/concepts/architecture", + "0.2.0/concepts/configuration", + "0.2.0/concepts/managed-configuration", + "0.2.0/concepts/harness-version-compatibility", + "0.2.0/concepts/gateway-mode", + "0.2.0/concepts/authentication" ] }, { "group": "CLI", "pages": [ - "0.1.0/cli/commands", - "0.1.0/cli/harnesses", - "0.1.0/cli/windows-paths", - "0.1.0/cli/troubleshooting" + "0.2.0/cli/commands", + "0.2.0/cli/harnesses", + "0.2.0/cli/windows-paths", + "0.2.0/cli/troubleshooting" ] }, { "group": "Administration", "pages": [ - "0.1.0/admin/dashboard", - "0.1.0/admin/user-management", - "0.1.0/admin/identity-provisioning", - "0.1.0/admin/managed-packages", - "0.1.0/admin/configuration", - "0.1.0/admin/gateway-access", - "0.1.0/admin/custom-gateway-provisioners", - "0.1.0/admin/session-capture" + "0.2.0/admin/dashboard", + "0.2.0/admin/user-management", + "0.2.0/admin/identity-provisioning", + "0.2.0/admin/managed-packages", + "0.2.0/admin/configuration", + "0.2.0/admin/gateway-access", + "0.2.0/admin/custom-gateway-provisioners", + "0.2.0/admin/session-capture" ] }, { "group": "Development", "pages": [ - "0.1.0/development/local-compose", - "0.1.0/development/contributing", - "0.1.0/development/harness-adapter-architecture", - "0.1.0/development/gateway-adapter-architecture" + "0.2.0/development/local-compose", + "0.2.0/development/contributing", + "0.2.0/development/harness-adapter-architecture", + "0.2.0/development/gateway-adapter-architecture" ] }, { "group": "Reference", "pages": [ - "0.1.0/reference/governance-config", - "0.1.0/reference/auth-endpoints" + "0.2.0/reference/governance-config", + "0.2.0/reference/auth-endpoints" ] }, { "group": "API reference", "openapi": { - "source": "/openapi/0.1.0.yaml", - "directory": "0.1.0/api-reference" + "source": "/openapi/0.2.0.yaml", + "directory": "0.2.0/api-reference" } } ] @@ -212,6 +213,82 @@ } } ] + }, + { + "version": "0.1.0", + "groups": [ + { + "group": "Start here", + "pages": [ + "0.1.0/introduction", + "0.1.0/quickstart" + ] + }, + { + "group": "Deployment", + "pages": [ + "0.1.0/deployment/runtime-contract", + "0.1.0/deployment/production", + "0.1.0/deployment/blue-yaml", + "0.1.0/deployment/managed-repositories" + ] + }, + { + "group": "Concepts", + "pages": [ + "0.1.0/concepts/architecture", + "0.1.0/concepts/configuration", + "0.1.0/concepts/harness-version-compatibility", + "0.1.0/concepts/gateway-mode", + "0.1.0/concepts/authentication" + ] + }, + { + "group": "CLI", + "pages": [ + "0.1.0/cli/commands", + "0.1.0/cli/harnesses", + "0.1.0/cli/windows-paths", + "0.1.0/cli/troubleshooting" + ] + }, + { + "group": "Administration", + "pages": [ + "0.1.0/admin/dashboard", + "0.1.0/admin/user-management", + "0.1.0/admin/identity-provisioning", + "0.1.0/admin/managed-packages", + "0.1.0/admin/configuration", + "0.1.0/admin/gateway-access", + "0.1.0/admin/custom-gateway-provisioners", + "0.1.0/admin/session-capture" + ] + }, + { + "group": "Development", + "pages": [ + "0.1.0/development/local-compose", + "0.1.0/development/contributing", + "0.1.0/development/harness-adapter-architecture", + "0.1.0/development/gateway-adapter-architecture" + ] + }, + { + "group": "Reference", + "pages": [ + "0.1.0/reference/governance-config", + "0.1.0/reference/auth-endpoints" + ] + }, + { + "group": "API reference", + "openapi": { + "source": "/openapi/0.1.0.yaml", + "directory": "0.1.0/api-reference" + } + } + ] } ] } diff --git a/apps/docs/next/cli/commands.mdx b/apps/docs/next/cli/commands.mdx index ce5f7e8..58af12e 100644 --- a/apps/docs/next/cli/commands.mdx +++ b/apps/docs/next/cli/commands.mdx @@ -24,7 +24,7 @@ The installer detects your platform, downloads the matching release asset, verif | Variable | Effect | | --- | --- | -| `BLUE_VERSION=0.1.0` | Install a specific release instead of the latest. | +| `BLUE_VERSION=0.2.0` | Install a specific release instead of the latest. | | `BLUE_INSTALL_DIR=/path` | Install somewhere other than `~/.local/bin`. | | `BLUE_UPDATE_PATH=0` | Do not add the default install directory to your shell profile. | diff --git a/apps/docs/next/deployment/production.mdx b/apps/docs/next/deployment/production.mdx index 55b5579..80b697a 100644 --- a/apps/docs/next/deployment/production.mdx +++ b/apps/docs/next/deployment/production.mdx @@ -16,7 +16,7 @@ Blue does not need Kubernetes. This is the maintained implementation of the [Dep ```bash # Downloads the release bundle, checks it, and moves into the AWS folder. The bundle holds # the Helm chart at chart/blue and the OpenTofu module at infra/aws. -VERSION=0.1.0 +VERSION=0.2.0 curl -LO "https://github.com/BlocksOrg/blue/releases/download/v${VERSION}/blue-deployment-v${VERSION}.tar.gz" curl -LO "https://github.com/BlocksOrg/blue/releases/download/v${VERSION}/SHA256SUMS" grep "blue-deployment-v${VERSION}.tar.gz" SHA256SUMS | shasum -a 256 -c - diff --git a/apps/docs/next/development/contributing.mdx b/apps/docs/next/development/contributing.mdx index fcd7f7c..74530f0 100644 --- a/apps/docs/next/development/contributing.mdx +++ b/apps/docs/next/development/contributing.mdx @@ -70,7 +70,7 @@ the `Next` documentation, synchronize those versions, then run: ```bash cd apps/docs -npm run release:docs -- 0.1.0 +npm run release:docs -- 0.2.0 ``` The command refuses version mismatches and existing releases, freezes the MDX and API contract, and makes the new stable release the default Mintlify version. Do not edit a frozen release except to correct a material documentation or security error. diff --git a/apps/docs/next/development/local-compose.mdx b/apps/docs/next/development/local-compose.mdx index 33a2964..4192d50 100644 --- a/apps/docs/next/development/local-compose.mdx +++ b/apps/docs/next/development/local-compose.mdx @@ -40,7 +40,7 @@ BETTER_AUTH_SECRET=replace-with-at-least-32-random-bytes MINIO_ROOT_USER=replace-local-storage-user MINIO_ROOT_PASSWORD=replace-local-storage-password # Version label displayed beside the dashboard logo; release builds inject this into the image. -BLUE_DEPLOYMENT_VERSION=0.1.0 +BLUE_DEPLOYMENT_VERSION=0.2.0 ``` The dashboard's **Documentation** sidebar link opens the centrally hosted documentation. diff --git a/apps/docs/openapi/0.2.0.yaml b/apps/docs/openapi/0.2.0.yaml new file mode 100644 index 0000000..3062328 --- /dev/null +++ b/apps/docs/openapi/0.2.0.yaml @@ -0,0 +1,2023 @@ +openapi: 3.1.0 +info: + title: Blue — Service Contract + version: "0.2.0" + description: > + The client-side contract the `blue` CLI expects from the provisioned + service. The reference `control-api` implements it; any BYO service that + honors these shapes can be swapped in. Gateway-mode inference JWT + issuance is OPTIONAL — governance-only deployments need only + `GET /governance-config`. CLI callers use OAuth 2.0 access tokens issued + through RFC 8628 device authorization; dashboard callers use an HTTP-only + Better Auth session cookie. + +servers: + - url: https://harness.example.com + description: Replace with the Control API URL for your deployment. + +tags: + - name: System + description: Service health and authenticated identity. + - name: Configuration + description: Personalized governance policy and client reconciliation state. + - name: Gateway + description: Per-user managed inference-gateway credential lifecycle. + - name: Sessions + description: Raw-session upload, metadata, and download lifecycle. + - name: Administration + description: Organization-administrator policy and client operations. + - name: User management + description: Organization user lifecycle, session revocation, and invitation operations. + - name: Identity provisioning + description: SCIM 2.0 user and group provisioning for an external identity provider. + +security: + - oauthDevice: [] + +paths: + /.well-known/metaharness: + get: + operationId: discoverMetaharness + tags: [System] + summary: Discover Control API and OAuth device-flow settings + description: Public, versioned bootstrap metadata containing no credentials or organization policy. + security: [] + responses: + "200": + description: Metaharness deployment discovery document + content: + application/json: + schema: { $ref: "#/components/schemas/MetaharnessDiscovery" } + + /health: + get: + operationId: health + tags: [System] + summary: Liveness probe + security: [] + responses: + "200": { description: OK } + + /ready: + get: + operationId: ready + tags: [System] + summary: Readiness probe + security: [] + responses: + "200": { description: Ready } + "503": { description: Database unavailable or schema incompatible } + + /health/schema: + get: + operationId: schemaHealth + tags: [System] + summary: Check database schema compatibility + security: [] + responses: + "200": { description: Schema is compatible with this application generation } + "503": { description: "Schema is incomplete, corrupted, or requires a newer application" } + + /health/worker: + get: + operationId: workerHealth + tags: [System] + summary: Check singleton background-worker freshness + security: [] + responses: + "200": { description: Worker is fresh or background work is disabled } + "503": { description: Worker has not completed work within its freshness budget } + + /health/object-storage: + get: + operationId: objectStorageHealth + tags: [System] + summary: Check session and package object stores + security: [] + responses: + "200": { description: Both object stores are reachable } + "503": { description: At least one object store is unavailable } + + /health/credential-resolver: + get: + operationId: credentialResolverHealth + tags: [System] + summary: Check the gateway credential resolver store + security: [] + responses: + "200": { description: Resolver storage is healthy or gateway mode is disabled } + "503": { description: Resolver storage is unavailable } + + /metrics: + get: + operationId: metrics + tags: [System] + summary: Prometheus metrics + security: [] + responses: + "200": + description: Prometheus text exposition + content: + text/plain: + schema: { type: string } + + /health/dependencies: + get: + operationId: dependencyHealth + tags: [System] + summary: Check Control API dependencies + description: Checks PostgreSQL, blob buckets, and the inference proxy when configured. Available to administrators and members for CLI health diagnostics. + responses: + "200": + description: Dependency health report; inspect the top-level status for degradation. + content: + application/json: + schema: { $ref: "#/components/schemas/DependencyHealthResponse" } + + /branding: + get: + operationId: getBranding + tags: [System] + summary: Fetch deployment-wide dashboard branding + security: [] + responses: + "200": + description: Logo and favicon URLs used by authenticated and logged-out pages + content: + application/json: + schema: { $ref: "#/components/schemas/Branding" } + + /auth/me: + get: + operationId: getCurrentIdentity + tags: [System] + summary: Resolve the current OAuth token or dashboard session + responses: + "200": { description: Current identity } + "401": { description: "Missing, expired, or revoked service session" } + + /gateway/jwks: + get: + operationId: getGatewayJwks + tags: [Gateway] + summary: Get inference JWT verification keys + security: [] + responses: + "200": { description: Active and retained gateway verification keys } + "404": { description: Gateway mode is disabled } + + /gateway/session/revoke: + post: + operationId: revokeGatewaySession + tags: [Gateway] + summary: Revoke the gateway session bound to the current OAuth session + responses: + "204": { description: Gateway session revoked } + "401": { description: Access token is invalid or has no sid claim } + + /governance-config: + get: + operationId: getGovernanceConfig + tags: [Configuration] + summary: Fetch the governance config for the authenticated user/org + description: > + Returns which harnesses are allowed, per-harness managed config, + MCP/skills, and — in gateway mode — a session-bound inference JWT embedded in + the global `gateway` block. Response is scoped to the caller's + org/groups (server-side). + responses: + "200": + description: The governance config + headers: + X-Blue-Required-Client-Version: + description: Exact recommended Blue CLI release, identical to required_client_version in the document. + schema: { type: string } + content: + application/json: + schema: { $ref: "#/components/schemas/GovernanceConfig" } + "403": { description: Authenticated email has no provisioned gateway account } + "409": { description: Gateway key provisioning is required } + "426": + description: Client contract version or capabilities are insufficient + headers: + X-Blue-Required-Client-Version: + description: Exact recommended Blue CLI release, provided even when the document cannot be consumed. + schema: { type: string } + + /harness-metadata: + get: + operationId: getHarnessMetadata + tags: [Configuration] + summary: Return the trusted compiled harness registry + responses: + "200": + description: Harness aliases, labels, capabilities, and generation boundaries + content: + application/json: + schema: + type: object + required: [contract_version, harnesses] + properties: + contract_version: { type: integer } + harnesses: + type: array + items: { $ref: "#/components/schemas/HarnessMetadata" } + "401": { description: Missing/invalid session token } + + /governance-config/events: + get: + operationId: watchGovernanceConfigRevisions + tags: [Configuration] + summary: Watch organization governance revision changes + description: Optional SSE invalidation stream. It emits the current revision immediately, then emits newly committed revisions for the authenticated organization. Clients must continue to fetch configuration from `/governance-config`. + responses: + "200": + description: Server-sent `revision` events whose JSON data contains the immutable revision ID + content: + text/event-stream: + schema: { type: string } + "401": { description: Missing/invalid session token } + "403": { description: "Missing `governance:read` scope" } + + /gateway/status: + get: + operationId: getGatewayStatus + tags: [Gateway] + summary: Describe gateway policy and runtime readiness without contacting the gateway + responses: + "200": + description: Gateway mode state and per-harness routing policy + content: + application/json: + schema: { $ref: "#/components/schemas/GatewayStatusResponse" } + + /gateway/proxy/health: + post: + operationId: checkGatewayProxyHealth + tags: [Gateway] + summary: Ping the configured inference proxy + description: Administrator-only server-side health request that does not expose gateway credentials. + responses: + "200": + description: Current reachability and latency of the inference proxy + content: + application/json: + schema: { $ref: "#/components/schemas/GatewayProxyHealthResponse" } + "400": { description: Inference proxy is not configured } + "403": { description: Administrator access required } + + /gateway/key: + get: + operationId: getGatewayKey + tags: [Gateway] + summary: Get the authenticated user's managed gateway-key status + description: Credential and inference JWT values are never returned. + responses: + "200": + description: Managed gateway-key status and non-secret metadata + content: + application/json: + schema: { $ref: "#/components/schemas/GatewayKeyResponse" } + + /gateway/key/ensure: + post: + operationId: ensureGatewayKey + tags: [Gateway] + summary: Provision or reconcile the authenticated user's managed gateway key + description: Invokes the configured built-in or executable provisioner when the credential is missing, stale, failed previously, or its configuration or policy revision changed. Manual retries bypass provisioning backoff. + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + manual: { type: boolean, default: false, description: Explicitly retry an errored or invalid credential and bypass recovery cooldown. } + responses: + "200": + description: Current managed gateway-key status + content: + application/json: + schema: { $ref: "#/components/schemas/GatewayKeyResponse" } + "403": { description: The provisioner reports that no gateway account exists for the authenticated user } + + /gateway/key/validate: + post: + operationId: validateGatewayKey + tags: [Gateway] + summary: Validate the authenticated user's stored upstream gateway key + description: Contacts the provisioner at most once per minute. A confirmed invalid credential is cleared and reported as invalid; replacement requires a subsequent ensure call. A missing credential is reported without contacting the provisioner. + responses: + "200": + description: Current managed gateway-key status + content: + application/json: + schema: { $ref: "#/components/schemas/GatewayKeyResponse" } + "403": { description: The provisioner reports that no gateway account exists for the authenticated user } + + /gateway/request-logs: + get: + operationId: listGatewayRequestLogs + tags: [Gateway] + summary: List organization inference-proxy request metadata + description: Administrator-only retained request metadata. Bodies, query strings, credentials, and arbitrary headers are never returned or retained. + parameters: + - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } } + - { name: per_page, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } } + - name: q + in: query + description: Case-insensitive literal search across path, model, profile name, repository, branch, and run ID. + schema: { type: string, maxLength: 200 } + - { name: user_id, in: query, schema: { type: string, format: uuid } } + - { name: profile_id, in: query, schema: { type: string } } + - { name: model, in: query, schema: { type: string } } + - { name: harness, in: query, schema: { type: string } } + - { name: result, in: query, schema: { $ref: "#/components/schemas/GatewayRequestResult" } } + - { name: occurred_from, in: query, description: Inclusive UTC calendar date., schema: { type: string, format: date } } + - { name: occurred_to, in: query, description: Inclusive UTC calendar date., schema: { type: string, format: date } } + - { name: sort, in: query, schema: { type: string, enum: [occurred_desc, occurred_asc], default: occurred_desc } } + responses: + "200": + description: A numbered page of retained request metadata + content: + application/json: + schema: { $ref: "#/components/schemas/GatewayRequestLogPage" } + "400": { description: Invalid list parameters } + "403": { description: Administrator access required } + + /gateway/request-logs/facets: + get: + operationId: getGatewayRequestLogFacets + tags: [Gateway] + summary: List organization request-log filter values + description: Administrator-only filter values for the organization request-log list. + parameters: + - { name: user_q, in: query, description: Case-insensitive email substring; user results are limited to 10., schema: { type: string, maxLength: 200 } } + - { name: selected_user_id, in: query, description: Relevant selected user to prioritize in the bounded result., schema: { type: string, format: uuid } } + responses: + "200": + description: Visible users, profiles, models, harnesses, and result categories + content: + application/json: + schema: { $ref: "#/components/schemas/GatewayRequestLogFacets" } + "403": { description: Administrator access required } + + /client-status: + post: + operationId: reportClientStatus + tags: [Configuration] + summary: Report a local client's applied revision and managed-file health + security: + - oauthDevice: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ClientStatusReport" } + responses: + "204": { description: Status recorded } + + /session-uploads/presign: + post: + operationId: presignSessionUpload + tags: [Sessions] + summary: Create a short-lived session artifact upload request + description: > + Reference path for the global session_upload.presign_url. Implementations + may host this endpoint elsewhere. A typical AWS implementation returns + an S3 presigned PUT and any headers that were included in its signature. + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/SessionUploadPresignRequest" } + responses: + "200": + description: Short-lived provider-specific upload request + content: + application/json: + schema: { $ref: "#/components/schemas/PresignedUpload" } + "401": { description: Missing/invalid session token } + + /session-uploads: + get: + operationId: listSessionUploads + tags: [Sessions] + summary: List captured sessions visible to the caller + description: > + Organization administrators see their organization for audit; members see + owned and shared sessions. Resumable mode always applies own/shared picker visibility. Callers may use cursor parameters or + numbered page parameters, but must not combine the two modes. + parameters: + - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100 } } + - { name: cursor, in: query, schema: { type: string, format: uuid } } + - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } } + - { name: per_page, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } } + - { name: q, in: query, description: Case-insensitive literal substring matched against native session ID and working directory., schema: { type: string, maxLength: 200 } } + - { name: harness, in: query, schema: { type: string } } + - { name: user_id, in: query, schema: { type: string, format: uuid } } + - { name: status, in: query, schema: { type: string, enum: [pending, complete, superseded, failed] } } + - { name: updated_from, in: query, description: Inclusive UTC calendar date., schema: { type: string, format: date } } + - { name: updated_to, in: query, description: Inclusive UTC calendar date., schema: { type: string, format: date } } + - { name: sort, in: query, schema: { type: string, enum: [updated_desc, updated_asc], default: updated_desc } } + - { name: resumable, in: query, description: Return only owned or explicitly shared portable sessions; administrator audit access is not applied., schema: { type: boolean } } + responses: + "200": + description: A cursor page or numbered page of database-registered captured sessions + content: + application/json: + schema: { $ref: "#/components/schemas/CapturedSessionList" } + "400": { description: Invalid or incompatible list parameters } + + /session-uploads/facets: + get: + operationId: getSessionUploadFacets + tags: [Sessions] + summary: List users and harnesses available as captured-session filters + description: Facets use the same organization and ownership visibility rules as the session list. + parameters: + - { name: user_q, in: query, description: Case-insensitive email substring; user results are limited to 10., schema: { type: string, maxLength: 200 } } + - { name: selected_user_id, in: query, description: Relevant selected user to prioritize in the bounded result., schema: { type: string, format: uuid } } + responses: + "200": { description: Visible users and harnesses that own at least one captured session } + + /session-uploads/members: + get: + operationId: searchSessionShareMembers + tags: [Sessions] + summary: Search active organization members for session sharing + parameters: + - { name: q, in: query, description: Case-insensitive email substring; results are limited to 10., schema: { type: string, maxLength: 200 } } + responses: + "200": { description: Up to 10 matching active organization members other than the caller } + + /session-uploads/{id}: + get: + operationId: getSessionUpload + tags: [Sessions] + summary: Get captured-session metadata and artifact history + parameters: + - { $ref: "#/components/parameters/SessionId" } + responses: + "200": { description: Captured session detail } + "403": { description: Session is outside the caller's visibility } + + /session-uploads/{id}/complete: + post: + operationId: completeSessionUpload + tags: [Sessions] + summary: Verify the uploaded object and commit its metadata + description: The control API HEADs blob storage and checks size and SHA-256 metadata before making the artifact current. + parameters: + - { $ref: "#/components/parameters/SessionId" } + responses: + "200": { description: Artifact registered as complete } + "400": { description: Blob is absent or its metadata does not match } + + /session-uploads/{id}/download: + post: + operationId: createSessionDownload + tags: [Sessions] + summary: Create a short-lived download request for the current artifact + parameters: + - { $ref: "#/components/parameters/SessionId" } + responses: + "200": { description: Provider-specific presigned download URL } + "410": { description: The artifact retention deadline has passed } + + /session-uploads/{id}/sharing: + get: + operationId: getSessionSharing + tags: [Sessions] + summary: Get the current sharing mode and active recipients + parameters: + - { $ref: "#/components/parameters/SessionId" } + responses: + "200": { description: "Sharing mode, edit permission, recipient count, and active recipient identities" } + "403": { description: Session is outside the caller's visibility } + put: + operationId: updateSessionSharing + tags: [Sessions] + summary: Replace owner-managed session sharing grants + description: Idempotently sets private, workspace-wide, or selected-active-member access. Only the owner may call this endpoint. + parameters: + - { $ref: "#/components/parameters/SessionId" } + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [mode] + properties: + mode: { type: string, enum: [private, workspace, selected] } + user_ids: { type: array, items: { type: string, format: uuid }, default: [] } + responses: + "200": { description: Sharing grants replaced } + "403": { description: Caller is not the session owner } + + /admin/governance-config: + get: + operationId: getAdminGovernanceConfig + tags: [Administration] + summary: Read the organization's current editable YAML configuration + responses: + "200": + description: Current YAML and revision + content: + application/json: + schema: { $ref: "#/components/schemas/AdminConfig" } + put: + operationId: updateAdminGovernanceConfig + tags: [Administration] + summary: Validate and save a new governance configuration revision + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/UpdateAdminConfig" } + responses: + "200": + description: Saved revision + content: + application/json: + schema: { $ref: "#/components/schemas/AdminConfig" } + "409": { description: Base revision is stale } + + /admin/governance-config/revisions: + get: + operationId: listAdminGovernanceRevisions + tags: [Administration] + summary: List recent governance configuration revisions + responses: + "200": + description: Up to 100 revisions, newest first + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/GovernanceRevision" } + + /admin/blue-config/export: + get: + operationId: exportBlueConfig + tags: [Administration] + summary: Export the mounted deployment configuration with current governance + responses: + "200": + description: Deployable YAML with literal secrets replaced by environment references + content: + application/json: + schema: { $ref: "#/components/schemas/BlueConfigExport" } + + /admin/branding: + put: + operationId: updateBranding + tags: [Administration] + summary: Replace deployment-wide dashboard branding + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/Branding" } + responses: + "200": + description: Saved branding settings + content: + application/json: + schema: { $ref: "#/components/schemas/Branding" } + "400": { description: A URL is invalid or too long } + "403": { description: Administrator access required } + + /admin/package-catalog: + get: + operationId: listManagedPackageCatalog + tags: [Administration] + summary: List curated extension packages available to the organization + responses: + "200": + description: Digest-pinned package catalog + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/ManagedPackage" } + + /admin/package-source/inspect: + post: + operationId: inspectManagedPackageSource + tags: [Administration] + summary: Resolve, hash, and optionally mirror a managed repository source + requestBody: + required: true + content: + application/json: + schema: + oneOf: + - type: object + required: [source_ref] + properties: + source_ref: + type: string + description: Public GitHub source in `github:owner/repository@ref` form or an immutable public HTTPS `.tar.gz` URL + examples: + - "github:BlocksOrg/agent-extensions@v1.2.0" + - "https://packages.example/extension-1.2.0.tar.gz" + - type: object + required: [connection_id, repository, ref] + properties: + connection_id: { type: string } + repository: { type: string, example: "owner/repository" } + ref: { type: string, example: "v1.2.3" } + responses: + "200": + description: Immutable source metadata for administrator confirmation + content: + application/json: + schema: + type: object + required: [source_ref, sha256, size_bytes] + properties: + source_ref: { type: string, format: uri } + artifact_id: { type: string, format: uuid } + resolved_commit: { type: string } + sha256: { type: string, pattern: "^[a-f0-9]{64}$" } + size_bytes: { type: integer, maximum: 104857600 } + "400": { description: "Source is unsafe, unreachable, too large, or invalid" } + + /admin/package-source/connections: + get: + operationId: listManagedPackageSourceConnections + tags: [Administration] + summary: List repository connections allowed for the administrator's organization + responses: + "200": + description: Safe connection metadata; credentials are never returned + content: + application/json: + schema: + type: array + items: + type: object + required: [id, name, provider, namespaces] + properties: + id: { type: string } + name: { type: string } + provider: { type: string, enum: [github, bitbucket_cloud, bitbucket_data_center] } + namespaces: { type: array, items: { type: string } } + + /admin/governance-extensions: + put: + operationId: updateManagedExtensions + tags: [Administration] + summary: Save packages, per-harness overrides, and MCP servers as a new revision + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/UpdateManagedExtensions" } + responses: + "200": + description: Saved governance revision + content: + application/json: + schema: { $ref: "#/components/schemas/AdminConfig" } + "409": { description: Base revision is stale } + + /admin/harnesses/{harness}/managed-config: + get: + operationId: getHarnessManagedConfig + tags: [Administration] + summary: Read one coding agent's managed configuration as YAML + parameters: + - name: harness + in: path + required: true + schema: { type: string, description: Harness key from the compiled registry } + responses: + "200": + description: Current managed configuration + content: + application/json: + schema: { $ref: "#/components/schemas/HarnessManagedConfig" } + put: + operationId: updateHarnessManagedConfig + tags: [Administration] + summary: Save one coding agent's managed configuration as a new revision + parameters: + - name: harness + in: path + required: true + schema: { type: string, description: Harness key from the compiled registry } + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/UpdateHarnessManagedConfig" } + responses: + "200": + description: Saved governance revision + content: + application/json: + schema: { $ref: "#/components/schemas/HarnessManagedConfig" } + "400": { description: Harness or managed configuration is invalid } + "409": { description: Base revision is stale } + + /admin/harnesses/managed-configs: + get: + operationId: listHarnessManagedConfigs + tags: [Administration] + summary: Read every supported coding agent's managed configuration as YAML + responses: + "200": + description: Managed configurations from one governance revision + content: + application/json: + schema: { $ref: "#/components/schemas/HarnessManagedConfigs" } + + /package-artifacts/{id}/download: + post: + operationId: downloadManagedPackageArtifact + tags: [Governance] + summary: Create a short-lived download request for an organization-scoped package artifact + parameters: + - { name: id, in: path, required: true, schema: { type: string, format: uuid } } + responses: + "200": + description: Fresh presigned object-storage request + content: + application/json: + schema: + type: object + required: [url, method, headers, expires_in_seconds] + properties: + url: { type: string, format: uri } + method: { type: string, enum: [GET] } + headers: { type: object, additionalProperties: { type: string } } + expires_in_seconds: { type: integer } + "404": { description: Artifact is not referenced by a currently deployed package visible to the authenticated user } + + /admin/identity/status: + get: + operationId: getAdminIdentityStatus + tags: [User management] + summary: Get the deployment-managed identity and provisioning status + description: Returns non-secret OIDC mode and SCIM configuration metadata. Credential values are never returned. + security: [{ oauthDevice: [] }, { dashboardSession: [] }] + responses: + "200": + description: Managed identity status + content: + application/json: + schema: { $ref: "#/components/schemas/IdentityStatusResponse" } + "403": { description: Administrator role required } + + /admin/users: + get: + operationId: listAdminUsers + tags: [User management] + summary: List users in the administrator's organization + description: Includes active, suspended, and access-removed users so historical ownership remains visible. + security: [{ oauthDevice: [] }, { dashboardSession: [] }] + parameters: + - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } } + - { name: per_page, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } } + - { name: q, in: query, description: "Case-insensitive email or subject substring", schema: { type: string, maxLength: 200 } } + - { name: role, in: query, schema: { $ref: "#/components/schemas/GovernanceRole" } } + - { name: status, in: query, schema: { $ref: "#/components/schemas/AdminUserStatus" } } + - { name: provisioning_source, in: query, schema: { type: string, enum: [local, scim] } } + responses: + "200": + description: Users + content: + application/json: + schema: { $ref: "#/components/schemas/AdminUserPage" } + "403": { description: Administrator role required } + + /admin/users/options: + get: + operationId: listAdminUserOptions + tags: [User management] + summary: List non-removed organization users for selection controls + security: [{ oauthDevice: [] }, { dashboardSession: [] }] + responses: + "200": + description: User options + content: + application/json: + schema: { type: array, items: { $ref: "#/components/schemas/AdminUser" } } + post: + operationId: searchAdminUserOptions + tags: [User management] + summary: Search active and already-selected organization users + security: [{ oauthDevice: [] }, { dashboardSession: [] }] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + q: { type: string } + selected_user_ids: { type: array, items: { type: string, format: uuid } } + responses: + "200": + description: At most ten matching user options + content: + application/json: + schema: { type: array, items: { $ref: "#/components/schemas/AdminUser" } } + + /admin/users/{id}: + get: + operationId: getAdminUser + tags: [User management] + summary: Get an organization user + security: [{ oauthDevice: [] }, { dashboardSession: [] }] + parameters: [{ $ref: "#/components/parameters/UserId" }] + responses: + "200": + description: User + content: + application/json: + schema: { $ref: "#/components/schemas/AdminUser" } + "404": { description: User does not exist in the administrator's organization } + patch: + operationId: updateAdminUser + tags: [User management] + summary: Change a user's role or active/suspended state + description: Role and status changes revoke existing browser and CLI credentials. Removed users must be invited again. + security: [{ oauthDevice: [] }, { dashboardSession: [] }] + parameters: [{ $ref: "#/components/parameters/UserId" }] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/UpdateAdminUser" } + responses: + "200": + description: Updated user + content: + application/json: + schema: { $ref: "#/components/schemas/AdminUser" } + "409": { description: Self/protected/last-administrator guard or invalid lifecycle transition } + delete: + operationId: deleteAdminUser + tags: [User management] + summary: Delete a user's account access while retaining governance history + description: Deletes the Better Auth identity, memberships, credentials, and gateway selection. The identifiable governance user and historical revisions, clients, and captured sessions remain. + security: [{ oauthDevice: [] }, { dashboardSession: [] }] + parameters: [{ $ref: "#/components/parameters/UserId" }] + responses: + "204": { description: Access removed; repeated deletion is idempotent } + "409": { description: Self/protected/last-administrator guard } + + /admin/users/{id}/sessions/revoke: + post: + operationId: revokeAdminUserSessions + tags: [User management] + summary: Revoke all browser, OAuth, and device sessions for a user + security: [{ oauthDevice: [] }, { dashboardSession: [] }] + parameters: [{ $ref: "#/components/parameters/UserId" }] + responses: + "204": { description: All credentials revoked } + "409": { description: User account has already been removed } + + /admin/invitations: + get: + operationId: listAdminInvitations + tags: [User management] + summary: List organization invitations + security: [{ oauthDevice: [] }, { dashboardSession: [] }] + parameters: + - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } } + - { name: per_page, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } } + - { name: status, in: query, schema: { type: string, enum: [outstanding] } } + - { name: q, in: query, description: "Case-insensitive email substring", schema: { type: string, maxLength: 200 } } + - { name: role, in: query, schema: { $ref: "#/components/schemas/GovernanceRole" } } + responses: + "200": + description: Invitations, newest first + content: + application/json: + schema: { $ref: "#/components/schemas/AdminInvitationPage" } + post: + operationId: createAdminInvitation + tags: [User management] + summary: Invite an email address to create an organization account + description: The recipient email and role are fixed. The recipient supplies only a password during acceptance. + security: [{ oauthDevice: [] }, { dashboardSession: [] }] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/CreateAdminInvitation" } + responses: + "201": + description: Invitation created + content: + application/json: + schema: { $ref: "#/components/schemas/AdminInvitationIssued" } + "409": { description: Email is already a member or has a pending invitation } + + /admin/invitations/{id}: + get: + operationId: getAdminInvitation + tags: [User management] + summary: Get an organization invitation + security: [{ oauthDevice: [] }, { dashboardSession: [] }] + parameters: [{ $ref: "#/components/parameters/InvitationId" }] + responses: + "200": + description: Invitation + content: + application/json: + schema: { $ref: "#/components/schemas/AdminInvitation" } + "404": { description: Invitation does not exist in the administrator's organization } + delete: + operationId: cancelAdminInvitation + tags: [User management] + summary: Cancel a pending invitation + security: [{ oauthDevice: [] }, { dashboardSession: [] }] + parameters: [{ $ref: "#/components/parameters/InvitationId" }] + responses: + "204": { description: Invitation canceled; repeated cancellation is idempotent } + "409": { description: Accepted invitations cannot be canceled } + + /admin/invitations/{id}/resend: + post: + operationId: resendAdminInvitation + tags: [User management] + summary: Resend and renew a pending or expired invitation for 24 hours + security: [{ oauthDevice: [] }, { dashboardSession: [] }] + parameters: [{ $ref: "#/components/parameters/InvitationId" }] + responses: + "200": + description: Renewed invitation + content: + application/json: + schema: { $ref: "#/components/schemas/AdminInvitation" } + "409": { description: Invitation has already been accepted or canceled } + + /admin/invitations/{id}/regenerate: + post: + operationId: regenerateAdminInvitation + tags: [User management] + summary: Replace an outstanding invitation with a new invitation link + description: Invalidates the prior link immediately, retains its pinned email and role, and renews expiry for 24 hours. + security: [{ oauthDevice: [] }, { dashboardSession: [] }] + parameters: [{ $ref: "#/components/parameters/InvitationId" }] + responses: + "200": + description: Replacement invitation issued + content: + application/json: + schema: { $ref: "#/components/schemas/AdminInvitationIssued" } + "404": { description: Invitation does not exist in the administrator's organization } + "409": { description: "Invitation is accepted or canceled, or invitations are disabled" } + + /admin/client-status: + get: + operationId: listAdminClientStatus + tags: [Administration] + summary: List and filter local client status reports for the organization + parameters: + - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } } + - { name: per_page, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } } + - { name: q, in: query, description: "Case-insensitive literal substring matched against hostname, instance ID, and user email.", schema: { type: string, maxLength: 200 } } + - { name: user_id, in: query, schema: { type: string, format: uuid } } + - { name: harness, in: query, description: Matches harness names and structured harness inventory entries., schema: { type: string } } + - { name: health, in: query, schema: { type: string, enum: [current, attention] } } + - { name: last_seen_from, in: query, description: Inclusive UTC calendar date., schema: { type: string, format: date } } + - { name: last_seen_to, in: query, description: Inclusive UTC calendar date., schema: { type: string, format: date } } + - { name: sort, in: query, schema: { type: string, enum: [last_seen_desc, last_seen_asc], default: last_seen_desc } } + responses: + "200": + description: Numbered page of client status reports + content: + application/json: + schema: { $ref: "#/components/schemas/AdminClientStatusPage" } + "400": { description: Invalid list parameters } + + /admin/client-status/facets: + get: + operationId: getAdminClientStatusFacets + tags: [Administration] + summary: List users and harnesses available as client-status filters + parameters: + - { name: user_q, in: query, description: Case-insensitive email substring; user results are limited to 10., schema: { type: string, maxLength: 200 } } + - { name: selected_user_id, in: query, description: Relevant selected user to prioritize in the bounded result., schema: { type: string, format: uuid } } + responses: + "200": + description: Organization-scoped client status facets + content: + application/json: + schema: { $ref: "#/components/schemas/AdminClientStatusFacets" } + + /admin/client-status/{id}: + delete: + operationId: deleteAdminClientStatus + tags: [Administration] + summary: Remove an organization client inventory record + parameters: + - { name: id, in: path, required: true, schema: { type: string, format: uuid } } + responses: + "204": { description: Client inventory record removed } + "404": { description: Client inventory record not found } + + /scim/v2/Users: + get: + operationId: listScimUsers + tags: [Identity provisioning] + summary: List or equality-filter provisioned users + security: [{ scimBearer: [] }] + parameters: + - { name: filter, in: query, schema: { type: string } } + - { name: startIndex, in: query, schema: { type: integer, minimum: 1, default: 1 } } + - { name: count, in: query, schema: { type: integer, minimum: 0, maximum: 200, default: 100 } } + responses: + "200": { description: SCIM ListResponse containing User resources } + post: + operationId: createScimUser + tags: [Identity provisioning] + summary: Provision a passwordless user + security: [{ scimBearer: [] }] + requestBody: + required: true + content: + application/scim+json: + schema: { $ref: "#/components/schemas/ScimUser" } + responses: + "201": { description: User provisioned } + "409": { description: "Duplicate userName, externalId, or email" } + + /scim/v2/Users/{id}: + parameters: [{ $ref: "#/components/parameters/UserId" }] + get: + operationId: getScimUser + tags: [Identity provisioning] + security: [{ scimBearer: [] }] + responses: { "200": { description: SCIM User }, "404": { description: User not found } } + put: + operationId: replaceScimUser + tags: [Identity provisioning] + security: [{ scimBearer: [] }] + requestBody: + required: true + content: { application/scim+json: { schema: { $ref: "#/components/schemas/ScimUser" } } } + responses: { "200": { description: Replaced SCIM User } } + patch: + operationId: patchScimUser + tags: [Identity provisioning] + security: [{ scimBearer: [] }] + requestBody: + required: true + content: { application/scim+json: { schema: { $ref: "#/components/schemas/ScimPatch" } } } + responses: { "200": { description: Updated SCIM User } } + delete: + operationId: deleteScimUser + tags: [Identity provisioning] + security: [{ scimBearer: [] }] + responses: { "204": { description: Authentication access removed } } + + /scim/v2/Groups: + get: + operationId: listScimGroups + tags: [Identity provisioning] + security: [{ scimBearer: [] }] + parameters: + - { name: filter, in: query, schema: { type: string } } + - { name: startIndex, in: query, schema: { type: integer, minimum: 1 } } + - { name: count, in: query, schema: { type: integer, minimum: 0, maximum: 200 } } + responses: { "200": { description: SCIM ListResponse containing Group resources } } + post: + operationId: createScimGroup + tags: [Identity provisioning] + security: [{ scimBearer: [] }] + requestBody: + required: true + content: { application/scim+json: { schema: { $ref: "#/components/schemas/ScimGroup" } } } + responses: { "201": { description: Group provisioned } } + + /scim/v2/Groups/{id}: + parameters: [{ $ref: "#/components/parameters/UserId" }] + get: + operationId: getScimGroup + tags: [Identity provisioning] + security: [{ scimBearer: [] }] + responses: { "200": { description: SCIM Group } } + put: + operationId: replaceScimGroup + tags: [Identity provisioning] + security: [{ scimBearer: [] }] + requestBody: + required: true + content: { application/scim+json: { schema: { $ref: "#/components/schemas/ScimGroup" } } } + responses: { "200": { description: Replaced SCIM Group } } + patch: + operationId: patchScimGroup + tags: [Identity provisioning] + security: [{ scimBearer: [] }] + requestBody: + required: true + content: { application/scim+json: { schema: { $ref: "#/components/schemas/ScimPatch" } } } + responses: { "200": { description: Updated SCIM Group and memberships } } + delete: + operationId: deleteScimGroup + tags: [Identity provisioning] + security: [{ scimBearer: [] }] + responses: { "204": { description: Group removed and affected roles recomputed } } + + /scim/v2/ServiceProviderConfig: + get: + operationId: getScimServiceProviderConfig + tags: [Identity provisioning] + security: [{ scimBearer: [] }] + responses: { "200": { description: Supported SCIM capabilities } } + + /scim/v2/ResourceTypes: + get: + operationId: listScimResourceTypes + tags: [Identity provisioning] + security: [{ scimBearer: [] }] + responses: { "200": { description: User and Group resource types } } + + /scim/v2/Schemas: + get: + operationId: listScimSchemas + tags: [Identity provisioning] + security: [{ scimBearer: [] }] + responses: { "200": { description: Core User and Group schemas } } + +components: + parameters: + SessionId: + name: id + in: path + required: true + schema: { type: string, format: uuid } + UserId: + name: id + in: path + required: true + schema: { type: string, format: uuid } + InvitationId: + name: id + in: path + required: true + schema: { type: string, format: uuid } + securitySchemes: + oauthDevice: + type: http + scheme: bearer + bearerFormat: JWT + description: > + OAuth 2.0 access token obtained through RFC 8628 at + /api/auth/device/code and /api/auth/oauth2/token. The API enforces + governance:read, session:write, and client-status:write as appropriate. + dashboardSession: + type: apiKey + in: cookie + name: better-auth.session_token + description: HTTP-only Better Auth dashboard session cookie. + scimBearer: + type: http + scheme: bearer + description: Deployment-managed bearer token dedicated to the workspace SCIM client. + + schemas: + MetaharnessDiscovery: + type: object + required: [version, control_api_url, oauth] + properties: + version: { type: integer, const: 1 } + control_api_url: + type: string + format: uri + description: Canonical Control API URL and OAuth resource indicator. + oauth: + type: object + required: [issuer, client_id, scopes] + properties: + issuer: { type: string, format: uri } + client_id: { type: string, minLength: 1 } + scopes: + type: array + items: { type: string, minLength: 1 } + + Branding: + type: object + required: [logo_url, favicon_url] + properties: + logo_url: + type: [string, "null"] + format: uri + maxLength: 2048 + description: Absolute HTTP or HTTPS URL for the dashboard logo + favicon_url: + type: [string, "null"] + format: uri + maxLength: 2048 + description: Absolute HTTP or HTTPS URL for the browser favicon + + AdminConfig: + type: object + required: [revision, yaml, managed_yaml, document, package_audiences, created_at] + properties: + revision: { type: string, format: uuid, example: 251e6087-98cf-4861-9668-f09f537d5e4d } + yaml: { type: string, description: Validated organization policy template } + managed_yaml: { type: string, description: Editable policy projection with extension-owned fields omitted } + document: { $ref: "#/components/schemas/GovernanceConfig" } + package_audiences: + type: object + description: Revision-scoped deployment audience keyed by package ID; absent historical records are returned as organization-wide. + additionalProperties: { $ref: "#/components/schemas/PackageAudience" } + created_at: { type: string, format: date-time } + + UpdateAdminConfig: + type: object + required: [base_revision, managed_yaml] + properties: + base_revision: { type: string, format: uuid, example: 251e6087-98cf-4861-9668-f09f537d5e4d } + managed_yaml: { type: string, description: Extension-free organization policy; published extensions are merged by the server } + + UpdateManagedExtensions: + type: object + required: [base_revision, packages] + properties: + base_revision: { type: string, format: uuid } + packages: + type: array + items: { $ref: "#/components/schemas/ManagedPackage" } + package_audiences: + type: object + description: Optional package audiences keyed by selected package ID; omitted entries deploy organization-wide. + additionalProperties: { $ref: "#/components/schemas/PackageAudience" } + package_overrides: + type: object + additionalProperties: + type: object + additionalProperties: { $ref: "#/components/schemas/PackageOverride" } + mcp: + type: object + additionalProperties: + type: array + items: { $ref: "#/components/schemas/McpServer" } + + UpdateHarnessManagedConfig: + type: object + required: [base_revision, managed_config_yaml] + properties: + base_revision: { type: string, format: uuid } + managed_config_yaml: + type: string + description: YAML mapping containing only the selected harness's managed settings + version_requirement: { type: [string, "null"], description: Optional semver range enforced for the installed harness } + allow_unverified_versions: { type: boolean, default: false, description: Permit versions beyond Blue's certified ceiling when constrained by an explicit version_requirement } + + HarnessManagedConfig: + type: object + required: [revision, harness, managed_config_yaml, allow_unverified_versions] + properties: + revision: { type: string, format: uuid } + harness: { type: string, description: Harness key from the compiled registry } + managed_config_yaml: { type: string } + version_requirement: { type: [string, "null"] } + allow_unverified_versions: { type: boolean } + + HarnessManagedConfigs: + type: object + required: [revision, configurations, allow_unverified_versions] + properties: + revision: { type: string, format: uuid } + configurations: + type: object + additionalProperties: { type: string } + version_requirements: + type: object + additionalProperties: { type: [string, "null"] } + allow_unverified_versions: + type: object + additionalProperties: { type: boolean } + + GovernanceRevision: + type: object + required: [revision, created_at, origin] + properties: + revision: { type: string, format: uuid } + created_at: { type: string, format: date-time } + created_by_email: { type: [string, "null"], format: email } + origin: { type: string, enum: [bootstrap, deployment, dashboard] } + + BlueConfigExport: + type: object + required: [yaml, redactions] + properties: + yaml: { type: string, description: Complete deployable Blue YAML with current organization governance } + redactions: + type: array + items: + type: object + required: [path, environment_variable] + properties: + path: { type: string } + environment_variable: { type: string } + + IdentityStatusResponse: + type: object + required: [auth_mode, scim] + properties: + auth_mode: { type: string, enum: [password, oidc] } + scim: + type: object + required: [configured, group_role_mappings] + properties: + configured: { type: boolean, description: True when a SCIM bearer credential is configured. } + group_role_mappings: + type: object + description: Exact SCIM group names mapped to governance roles. + additionalProperties: { $ref: "#/components/schemas/GovernanceRole" } + + AdminUser: + type: object + required: [id, subject, email, role, active, status, protected, provisioning_source, managed, created_at, updated_at] + properties: + id: { type: string, format: uuid } + subject: { type: string } + email: { type: string, format: email, example: developer@example.com } + role: { $ref: "#/components/schemas/GovernanceRole" } + active: { type: boolean, description: Compatibility projection of status == active } + status: { $ref: "#/components/schemas/AdminUserStatus" } + protected: { type: boolean, description: "Deployment-managed identity that cannot be demoted, suspended, or deleted" } + provisioning_source: { type: string, enum: [local, scim] } + managed: { type: boolean, description: True when role and lifecycle are controlled through SCIM } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + + AdminUserPage: + type: object + required: [items, page, per_page, total, total_pages] + properties: + items: { type: array, items: { $ref: "#/components/schemas/AdminUser" } } + page: { type: integer, minimum: 1 } + per_page: { type: integer, minimum: 1, maximum: 100 } + total: { type: integer, minimum: 0 } + total_pages: { type: integer, minimum: 0 } + + GovernanceRole: + type: string + enum: [admin, member] + + AdminUserStatus: + type: string + enum: [active, suspended, removed] + + UpdateAdminUser: + type: object + minProperties: 1 + properties: + role: { $ref: "#/components/schemas/GovernanceRole" } + status: + type: string + enum: [active, suspended] + + CreateAdminInvitation: + type: object + required: [email, role] + properties: + email: { type: string, format: email, example: developer@example.com } + role: { $ref: "#/components/schemas/GovernanceRole" } + + AdminInvitation: + type: object + required: [id, email, role, status, expires_at, created_at] + properties: + id: { type: string, format: uuid } + email: { type: string, format: email } + role: { $ref: "#/components/schemas/GovernanceRole" } + status: { type: string, enum: [pending, accepted, canceled, expired] } + inviter_email: { type: [string, "null"], format: email } + expires_at: { type: string, format: date-time } + created_at: { type: string, format: date-time } + + AdminInvitationIssued: + allOf: + - { $ref: "#/components/schemas/AdminInvitation" } + - type: object + required: [invitation_url] + properties: + invitation_url: { type: string, format: uri } + + AdminInvitationPage: + type: object + required: [items, page, per_page, total, total_pages] + properties: + items: { type: array, items: { $ref: "#/components/schemas/AdminInvitation" } } + page: { type: integer, minimum: 1 } + per_page: { type: integer, minimum: 1, maximum: 100 } + total: { type: integer, minimum: 0 } + total_pages: { type: integer, minimum: 0 } + + ScimUser: + type: object + required: [schemas, userName, active] + properties: + schemas: { type: array, items: { type: string }, example: ["urn:ietf:params:scim:schemas:core:2.0:User"] } + id: { type: string, format: uuid, readOnly: true } + externalId: { type: [string, "null"] } + userName: { type: string } + name: + type: object + properties: + givenName: { type: [string, "null"] } + familyName: { type: [string, "null"] } + emails: + type: array + items: + type: object + required: [value] + properties: + value: { type: string, format: email } + type: { type: string } + primary: { type: boolean } + active: { type: boolean } + + ScimGroup: + type: object + required: [schemas, displayName] + properties: + schemas: { type: array, items: { type: string }, example: ["urn:ietf:params:scim:schemas:core:2.0:Group"] } + id: { type: string, format: uuid, readOnly: true } + externalId: { type: [string, "null"] } + displayName: { type: string } + members: + type: array + items: + type: object + required: [value] + properties: + value: { type: string, format: uuid } + display: { type: string, readOnly: true } + + ScimPatch: + type: object + required: [schemas, Operations] + properties: + schemas: { type: array, items: { type: string }, example: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"] } + Operations: + type: array + items: + type: object + required: [op] + properties: + op: { type: string, enum: [add, remove, replace] } + path: { type: string } + value: {} + + AdminClientStatus: + allOf: + - $ref: "#/components/schemas/ClientStatusReport" + - type: object + required: [id, user_email, first_seen_at, last_seen_at, activity_status] + properties: + id: { type: string, format: uuid } + user_email: { type: string, format: email, example: developer@example.com } + first_seen_at: { type: string, format: date-time } + last_seen_at: { type: string, format: date-time } + activity_status: { type: string, enum: [recent, stale] } + + AdminClientStatusPage: + type: object + required: [items, page, per_page, total, total_pages, current_revision] + properties: + items: { type: array, items: { $ref: "#/components/schemas/AdminClientStatus" } } + page: { type: integer, minimum: 1 } + per_page: { type: integer, minimum: 1, maximum: 100 } + total: { type: integer, minimum: 0 } + total_pages: { type: integer, minimum: 0 } + current_revision: { type: [string, "null"], description: Latest governance revision for the organization. } + + AdminClientStatusFacets: + type: object + required: [users, harnesses] + properties: + users: + type: array + items: + type: object + required: [id, email] + properties: + id: { type: string, format: uuid } + email: { type: string, format: email } + harnesses: { type: array, items: { type: string } } + + GatewayKeyResponse: + type: object + required: [enabled, email, status] + properties: + enabled: { type: boolean } + email: { type: string, format: email } + status: { type: string, enum: [ready, missing, invalid, recovering, error] } + alias: { type: [string, "null"] } + external_id: { type: [string, "null"], description: "Opaque gateway identifier, never the credential value." } + expires_at: { type: [string, "null"], format: date-time } + last_reconciled_at: { type: [string, "null"], format: date-time } + error: { type: [string, "null"] } + invalidated_at: { type: [string, "null"], format: date-time } + invalidation_reason: { type: [string, "null"] } + next_retry_at: { type: [string, "null"], format: date-time } + + GatewayHarnessStatus: + type: object + required: [name, gateway_type] + properties: + name: { type: string, example: codex } + gateway_type: { type: string, example: litellm } + + GatewayStatusResponse: + type: object + required: [enabled, runtime_configured] + properties: + enabled: { type: boolean, description: True when the global gateway policy enables gateway mode. } + runtime_configured: { type: boolean, description: True when all required server-side gateway settings are present. } + inference_proxy_url: { type: [string, "null"], format: uri, description: Administrator-only inference proxy URL. } + upstream_gateway_url: { type: [string, "null"], format: uri, description: Administrator-only upstream inference gateway URL. } + provisioner_type: { type: [string, "null"], description: Administrator-only built-in or executable provisioner selected by configuration. } + harnesses: { type: array, description: Administrator-only routed harness details., items: { $ref: "#/components/schemas/GatewayHarnessStatus" } } + runtime_checks: + description: Administrator-only configuration-presence checks. Credential values are never returned. + oneOf: + - $ref: "#/components/schemas/GatewayRuntimeChecks" + - type: "null" + + GatewayProxyHealthResponse: + type: object + required: [status, checked_at, latency_ms] + properties: + status: { type: string, enum: [healthy, unhealthy] } + checked_at: { type: string, format: date-time } + latency_ms: { type: integer, minimum: 0 } + http_status: { type: [integer, "null"], minimum: 100, maximum: 599 } + error: { type: [string, "null"] } + + DependencyHealthCheck: + type: object + required: [name, status, latency_ms] + properties: + name: { type: string } + status: { type: string, enum: [healthy, unhealthy] } + latency_ms: { type: integer, minimum: 0 } + error: { type: [string, "null"] } + + DependencyHealthResponse: + type: object + required: [status, checked_at, checks] + properties: + status: { type: string, enum: [healthy, degraded] } + checked_at: { type: string, format: date-time } + checks: + type: array + items: { $ref: "#/components/schemas/DependencyHealthCheck" } + + GatewayRuntimeChecks: + type: object + required: [gateway_policy, gateway_url, inference_proxy_url, provisioner, secret_encryption, internal_auth, inference_jwt_signing] + properties: + gateway_policy: { type: boolean } + gateway_url: { type: boolean } + inference_proxy_url: { type: boolean } + provisioner: { type: boolean } + secret_encryption: { type: boolean } + internal_auth: { type: boolean } + inference_jwt_signing: { type: boolean } + + GatewayRequestResult: + type: string + enum: [success, redirect, client_error, server_error, transport_error] + + GatewayRequestLog: + type: object + required: [id, user_id, user_email, occurred_at, method, path, result, upstream_latency_ms] + properties: + id: { type: string, format: uuid } + user_id: { type: string, format: uuid } + user_email: { type: string, format: email } + profile_id: { type: [string, "null"] } + profile_name: { type: [string, "null"] } + occurred_at: { type: string, format: date-time } + method: { type: string } + path: { type: string, description: Request path without query parameters. } + model: { type: [string, "null"] } + http_status: { type: [integer, "null"], minimum: 100, maximum: 599 } + result: { $ref: "#/components/schemas/GatewayRequestResult" } + upstream_latency_ms: { type: integer, format: int64, minimum: 0, description: Time until upstream response headers or a transport failure. } + harness: { type: [string, "null"] } + repository: { type: [string, "null"] } + branch: { type: [string, "null"] } + commit_sha: { type: [string, "null"] } + dirty: { type: [boolean, "null"] } + run_id: { type: [string, "null"] } + + GatewayRequestLogPage: + type: object + required: [items, page, per_page, total, total_pages] + properties: + items: { type: array, items: { $ref: "#/components/schemas/GatewayRequestLog" } } + page: { type: integer, minimum: 1 } + per_page: { type: integer, minimum: 1, maximum: 100 } + total: { type: integer, minimum: 0 } + total_pages: { type: integer, minimum: 0 } + + GatewayRequestLogFacets: + type: object + required: [users, profiles, models, harnesses, results] + properties: + users: + type: array + items: + type: object + required: [id, email] + properties: + id: { type: string, format: uuid } + email: { type: string, format: email } + profiles: + type: array + items: + type: object + required: [id] + properties: + id: { type: string } + name: { type: [string, "null"] } + models: { type: array, items: { type: string } } + harnesses: { type: array, items: { type: string } } + results: { type: array, items: { $ref: "#/components/schemas/GatewayRequestResult" } } + + ClientStatusReport: + type: object + required: [instance_id, client_version, platform, applied, files_ok] + properties: + instance_id: { type: string, example: workstation-f3d151 } + hostname: { type: [string, "null"], example: developer-laptop } + client_version: { type: string, example: 0.1.0 } + platform: { type: string, example: macos } + config_revision: { type: [string, "null"] } + applied: { type: boolean } + files_ok: { type: boolean } + harnesses: + type: array + items: + oneOf: + - type: string + description: Applied harness name + - $ref: "#/components/schemas/HarnessInventoryEntry" + packages: + type: array + items: { $ref: "#/components/schemas/PackageStatus" } + reconciliation_attempt: + oneOf: + - $ref: "#/components/schemas/ReconciliationAttempt" + - type: "null" + error: { type: [string, "null"] } + + ReconciliationAttempt: + type: object + description: A failed JIT reconciliation attempted before launching a harness. + required: [harness, revision] + properties: + harness: { type: string, description: Harness key from the compiled registry } + revision: { type: string } + GovernanceConfig: + type: object + required: [revision, allowed_harnesses] + properties: + revision: { type: string, description: "Reconcile key; daemon acts on change", example: 251e6087-98cf-4861-9668-f09f537d5e4d } + contract_version: { type: integer, default: 1, description: Governance wire contract version } + required_capabilities: + type: array + items: { type: string, enum: [adapter_intervals, compiled_harness_registry, tenant_client_version_pin, transactional_reconcile, unverified_harness_versions, versioned_state] } + minimum_client_version: { type: string, description: Operator-visible rollout floor; required capabilities enforce compatibility } + required_client_version: + type: string + description: Canonical exact SemVer of the recommended Blue CLI release. Defaults to the control API deployment version. Requires tenant_client_version_pin capability. + example: "0.1.0" + ttl_seconds: { type: integer, description: Client cache TTL (default 300), example: 300 } + required: { type: boolean, description: "If true, client fails closed without fresh config" } + allowed_harnesses: + type: array + items: { type: string, description: Harness key from the compiled registry } + harnesses: + type: object + additionalProperties: { $ref: "#/components/schemas/HarnessPolicy" } + packages: + type: array + items: { $ref: "#/components/schemas/ManagedPackage" } + gateway: { $ref: "#/components/schemas/GatewayConfig" } + session_upload: { $ref: "#/components/schemas/SessionUploadConfig" } + telemetry: + type: object + properties: + sink_url: { type: string } + + HarnessInventoryEntry: + type: object + required: [name, api_allowed, client_supported, installed, reconciled] + properties: + name: { type: string } + api_allowed: { type: boolean } + client_supported: { type: boolean } + installed: { type: boolean } + path: { type: string } + raw_version: { type: string } + version: { type: string } + compatibility_profile: { type: string } + compatibility_deprecated: { type: boolean } + compatibility_error: { type: string } + compatibility_warning: { type: string } + reconciled: { type: boolean } + + HarnessMetadata: + type: object + required: [key, aliases, label, description, binary_names, install_command_template, capabilities, component_rules, generations] + properties: + key: { type: string } + aliases: { type: array, items: { type: string } } + label: { type: string } + description: { type: string } + binary_names: { type: array, items: { type: string } } + install_command_template: { type: string } + capabilities: { type: array, description: "Backward-compatible union derived from all generation specifications", items: { type: string } } + component_rules: + type: object + description: Backward-compatible summary derived from the current generation; prefer generations[].component_rules. + required: [agents_require_plugin, hooks_require_plugin, hooks_as_plugin_modules] + properties: + agents_require_plugin: { type: boolean } + hooks_require_plugin: { type: boolean } + hooks_as_plugin_modules: { type: boolean } + generations: + type: array + items: + type: object + required: [profile, introduced, verified_before, lifecycle, capabilities, component_rules] + properties: + profile: { type: string } + introduced: { type: string } + before: { type: [string, "null"], description: Exclusive upper bound of the breaking compatibility generation } + verified_before: { type: string, description: Exclusive upper bound of releases certified against this implementation } + lifecycle: { type: string, enum: [supported, deprecated] } + capabilities: { type: array, items: { type: string } } + component_rules: + type: object + required: [agents_require_plugin, hooks_require_plugin, hooks_as_plugin_modules] + properties: + agents_require_plugin: { type: boolean } + hooks_require_plugin: { type: boolean } + hooks_as_plugin_modules: { type: boolean } + + HarnessPolicy: + type: object + properties: + version_requirement: { type: string, description: Semver range required for the locally installed harness } + allow_unverified_versions: { type: boolean, default: false, description: Permit matching releases beyond Blue's certified ceiling; requires version_requirement } + managed_config: { $ref: "#/components/schemas/ManagedConfig" } + mcp: + type: array + description: > + Additive MCP suggestions. An enabled entry is installed only when + the local harness has no MCP with the same name; existing local + MCPs are never overwritten, disabled, or removed. + items: { $ref: "#/components/schemas/McpServer" } + package_overrides: + type: object + additionalProperties: { $ref: "#/components/schemas/PackageOverride" } + + ManagedPackage: + type: object + required: [id, version, source_ref, sha256] + properties: + id: { type: string, pattern: "^[a-z0-9-]+$" } + name: { type: string } + version: { type: string } + source_ref: { type: string, description: Immutable HTTPS or local .tar.gz archive } + artifact_id: { type: string, format: uuid, description: Organization-scoped mirrored artifact } + sha256: { type: string, pattern: "^[A-Fa-f0-9]{64}$" } + platform_sources: + type: object + description: Exact `-` archive overrides; clients require a matching entry when this map is present. + additionalProperties: { $ref: "#/components/schemas/PackageSource" } + settings: { type: object, additionalProperties: true } + adapters: + type: object + additionalProperties: { $ref: "#/components/schemas/PackageAdapter" } + + PackageSource: + type: object + required: [source_ref, sha256] + properties: + source_ref: { type: string, description: Immutable HTTPS or local .tar.gz archive } + artifact_id: { type: string, format: uuid, description: Organization-scoped mirrored artifact } + sha256: { type: string, pattern: "^[A-Fa-f0-9]{64}$" } + + PackageAudience: + type: object + required: [scope, user_ids] + properties: + scope: { type: string, enum: [organization, users] } + user_ids: + type: array + uniqueItems: true + items: { type: string, format: uuid } + + PackageAdapter: + type: object + description: > + All paths are archive-root-relative. skills_dir may identify an exact + skill folder or a collection of skill folders. Codex/Claude agents and + hooks require plugin_dir; OpenCode hooks are declared as plugin modules. + properties: + introduced: { type: string, description: Inclusive harness-version lower bound for adapter availability; defaults to 0.0.0 } + before: { type: string, description: Exclusive harness-version upper bound for adapter availability } + plugin_dir: { type: string } + skills_dir: { type: string } + agents_dir: { type: string } + hooks_file: { type: string } + plugins: { type: array, items: { type: string } } + helpers: + type: object + additionalProperties: { $ref: "#/components/schemas/PlatformAsset" } + variants: + type: array + description: Ordered half-open layouts selected within adapter availability; intervals must not overlap or leave gaps without a fallback. + items: { $ref: "#/components/schemas/PackageAdapterVariant" } + + PackageAdapterVariant: + type: object + required: [introduced] + properties: + introduced: { type: string, description: Inclusive semantic-version lower bound } + before: { type: string, description: Exclusive semantic-version upper bound; omit for open-ended } + plugin_dir: { type: string } + skills_dir: { type: string } + agents_dir: { type: string } + hooks_file: { type: string } + plugins: { type: array, items: { type: string } } + helpers: + type: object + additionalProperties: { $ref: "#/components/schemas/PlatformAsset" } + + PlatformAsset: + type: object + properties: + paths: + type: object + description: Archive paths keyed by os-architecture, with optional default. + additionalProperties: { type: string } + + PackageOverride: + type: object + properties: + enabled: { type: boolean } + settings: { type: object, additionalProperties: true } + + PackageStatus: + type: object + required: [id, version, sha256, harness, state] + properties: + id: { type: string } + version: { type: string } + sha256: { type: string } + harness: { type: string } + state: { type: string, enum: [applied, drifted, pending, failed, quarantined] } + error: { type: [string, "null"] } + + ManagedConfig: + type: object + description: Known keys are typed; unknown keys are preserved forward-compatibly. + properties: + model: { type: string } + approval_policy: { type: string } + sandbox_mode: { type: string } + auto_approve: { type: boolean } + reasoning_effort: + type: string + description: Harness model thinking/reasoning effort, such as medium. + fast_mode: + type: boolean + description: Whether accelerated service-tier selection is available; Codex maps false to features.fast_mode=false and service_tier=default. + additionalProperties: true + + GatewayConfig: + type: object + required: [type] + description: > + Present ⇒ gateway mode for every allowed harness; omit ⇒ governance-only. + proxy_url and token are runtime fields injected by the control API + when it delivers the policy to an authenticated client. + properties: + type: { type: string, description: Deployment-defined gateway adapter identifier. } + proxy_url: { type: string, description: Runtime inference proxy URL injected at delivery time } + token: + type: string + description: > + Runtime-only, session-bound inference JWT. The inference proxy validates it + and swaps it for an upstream gateway credential. The control API injects it + into the authenticated client's config response and never persists it. + auth_style: + type: string + enum: [bearer] + default: bearer + description: Client-to-proxy authentication style; bearer is the only supported value. + + McpServer: + type: object + required: [name] + properties: + name: { type: string } + command: { type: string } + args: { type: array, items: { type: string } } + env: { type: object, additionalProperties: { type: string } } + url: { type: string } + transport: { type: string } + disabled: { type: boolean } + + SessionUploadConfig: + type: object + required: [presign_url] + description: > + Global opt-in portable-session capture. When present, every compatible + supported harness registers its native hook. The authenticated endpoint returns a + short-lived provider-specific blob upload request. + properties: + presign_url: { type: string, format: uri } + + SessionUploadPresignRequest: + type: object + required: [harness, session_id, sha256, size_bytes, content_type] + properties: + harness: { type: string, description: Harness key from the compiled registry } + compatibility_profile: { type: string, example: claude-v1, description: "Compiled harness adapter profile that produced the upload. When omitted, the upload is recorded without a profile." } + session_id: { type: string, example: codex-session-20260827 } + sha256: { type: string, pattern: "^[a-f0-9]{64}$", example: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } + size_bytes: { type: integer, example: 48217 } + content_type: { type: string, example: application/x-ndjson } + cwd: { type: [string, "null"] } + artifact_format: { type: string, enum: [legacy-raw, blue-session-bundle-v1], default: legacy-raw } + resumable: { type: boolean, default: false } + title: { type: [string, "null"], maxLength: 256 } + summary: { type: [string, "null"], maxLength: 1024 } + captured_at_unix_ms: { type: integer, minimum: 0 } + repository_root: { type: [string, "null"], maxLength: 4096 } + repository_remote: { type: [string, "null"], maxLength: 4096 } + + PresignedUpload: + type: object + required: [upload_id, status, headers] + properties: + upload_id: { type: string, format: uuid } + status: { type: string, enum: [pending, complete] } + upload_url: { type: [string, "null"], format: uri } + method: { type: [string, "null"], enum: [PUT, POST, null] } + headers: + type: object + additionalProperties: { type: string } + complete_url: { type: [string, "null"], description: Authenticated endpoint called after blob upload } + + CapturedSession: + type: object + required: [id, user_id, user_email, harness, compatibility_profile, native_session_id, artifact_format, resumable, sharing_mode, share_recipient_count, updated_at] + properties: + id: { type: string, format: uuid } + user_id: { type: string, format: uuid } + user_email: { type: string, format: email } + harness: { type: string } + compatibility_profile: { type: string } + native_session_id: { type: string } + cwd: { type: [string, "null"] } + artifact_format: { type: string } + resumable: { type: boolean } + title: { type: [string, "null"] } + summary: { type: [string, "null"] } + source_captured_at: { type: [string, "null"], format: date-time } + repository: + type: object + properties: + root: { type: [string, "null"] } + remote: { type: [string, "null"] } + sharing_mode: { type: string, enum: [private, workspace, selected] } + share_recipient_count: { type: integer, minimum: 0 } + shared: { type: boolean, description: Present in resumable picker mode. } + updated_at: { type: string, format: date-time } + status: { type: [string, "null"] } + size_bytes: { type: [integer, "null"] } + content_type: { type: [string, "null"] } + retention_expires_at: { type: [string, "null"], format: date-time } + + CapturedSessionList: + type: object + required: [items] + properties: + items: { type: array, items: { $ref: "#/components/schemas/CapturedSession" } } + next_cursor: { type: [string, "null"], format: uuid } + page: { type: integer } + per_page: { type: integer } + total: { type: integer } + total_pages: { type: integer } diff --git a/apps/docs/openapi/next.yaml b/apps/docs/openapi/next.yaml index 051db45..3062328 100644 --- a/apps/docs/openapi/next.yaml +++ b/apps/docs/openapi/next.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: Blue — Service Contract - version: "0.1.0" + version: "0.2.0" description: > The client-side contract the `blue` CLI expects from the provisioned service. The reference `control-api` implements it; any BYO service that diff --git a/crates/gh-cli/Cargo.toml b/crates/gh-cli/Cargo.toml index ea39ca3..b3d9818 100644 --- a/crates/gh-cli/Cargo.toml +++ b/crates/gh-cli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "gh-cli" # Released independently from the self-hosted Blue deployment image. -version = "0.1.0" +version = "0.2.0" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/deploy/consumer/.github/workflows/deploy.yml b/deploy/consumer/.github/workflows/deploy.yml index 4fe72f7..de0b78c 100644 --- a/deploy/consumer/.github/workflows/deploy.yml +++ b/deploy/consumer/.github/workflows/deploy.yml @@ -30,7 +30,7 @@ jobs: - run: tofu -chdir=infra/aws init - run: tofu -chdir=infra/aws apply -auto-approve - run: tofu -chdir=infra/aws output -json helm_values > generated-values.json - - run: docker build --build-arg BLUE_VERSION=0.1.0 -t "$IMAGE:${GITHUB_SHA}" . + - run: docker build --build-arg BLUE_VERSION=0.2.0 -t "$IMAGE:${GITHUB_SHA}" . - run: docker push "$IMAGE:${GITHUB_SHA}" # The module either created the cluster or attached to an existing one; # either way it reports which, so the workflow needs no cluster variable. diff --git a/deploy/contract/governance.openapi.yaml b/deploy/contract/governance.openapi.yaml index 051db45..3062328 100644 --- a/deploy/contract/governance.openapi.yaml +++ b/deploy/contract/governance.openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: Blue — Service Contract - version: "0.1.0" + version: "0.2.0" description: > The client-side contract the `blue` CLI expects from the provisioned service. The reference `control-api` implements it; any BYO service that diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index a33c122..0d61cf1 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -78,7 +78,7 @@ services: build: context: ../apps/dashboard args: - BLUE_DEPLOYMENT_VERSION: ${BLUE_DEPLOYMENT_VERSION:-0.1.0} + BLUE_DEPLOYMENT_VERSION: ${BLUE_DEPLOYMENT_VERSION:-0.2.0} environment: BLUE_ENVIRONMENT: development CONTROL_API_URL: http://control-api:8080 diff --git a/deploy/helm/Chart.yaml b/deploy/helm/Chart.yaml index 1d2c50b..9bf80d0 100644 --- a/deploy/helm/Chart.yaml +++ b/deploy/helm/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: blue description: Self-hosted governance metaharness control plane type: application -version: 0.1.0 -appVersion: "0.1.0" +version: 0.2.0 +appVersion: "0.2.0" home: https://github.com/BlocksOrg/blue sources: - https://github.com/BlocksOrg/blue