diff --git a/cmd/ob-docgen/main.go b/cmd/ob-docgen/main.go index 4c82bdb9..c6744d74 100644 --- a/cmd/ob-docgen/main.go +++ b/cmd/ob-docgen/main.go @@ -299,7 +299,7 @@ var blocks = []block{ "Setting replicas, resources, volumes, published ports or drain behaviour", }}, {Key: "services", Title: "services", Order: 20, Status: statusShipped, - Summary: "Supporting services Onebox runs for you — the eleven built-in drivers, their versions, settings and persistence.", + Summary: "Supporting services Onebox runs for you: the eleven built-in drivers, their versions, settings and persistence.", ReadWhen: []string{ "Adding a database, cache or queue", "Deciding between a managed service and a daemon workload", @@ -327,7 +327,7 @@ var blocks = []block{ Summary: "Who owns the ingress proxy, which image runs it, and how TLS is resolved.", ReadWhen: []string{"Taking over the Traefik configuration, or turning routing off entirely"}}, {Key: "hooks", Title: "hooks", Order: 80, Status: statusShipped, - Summary: "Commands run at a lifecycle seam — bootstrap, pre_release, post_release, post_deploy — or keyed by a job name to replace that job's command.", + Summary: "Commands run at a lifecycle seam (bootstrap, pre_release, post_release, post_deploy) or keyed by a job name to replace that job's command.", ReadWhen: []string{"Running a command around a deploy or a rollback"}}, {Key: "registries", Title: "registries", Order: 90, Status: statusShipped, Summary: "Named container registries and the local environment variables holding their credentials.", @@ -336,7 +336,7 @@ var blocks = []block{ Summary: "Named webhooks that receive selected operation and scheduled-job outcomes.", ReadWhen: []string{"Sending deploy outcomes to Slack, Discord or an incident tool"}}, {Key: "backupTargets", Slug: "backup-targets", Title: "backupTargets", Order: 200, Status: statusShipped, - Summary: "User-owned off-host S3-compatible repositories a protected service writes its backups to. Executable for the postgres driver; every other driver refuses a policy rather than accepting one it cannot honour.", + Summary: "User-owned off-host S3-compatible repositories a protected service writes its backups to. Executable for the postgres driver; every other driver refuses a policy it cannot honour.", ReadWhen: []string{"Declaring where a database's backups go", "Understanding why Onebox refuses a backup target that shares the protected host"}}, {Key: "externalServices", Slug: "external-services", Title: "externalServices", Order: 210, Status: statusSchemaOnly, Summary: "Typed dependencies operated outside Onebox, whose lifecycle and backups stay external. Accepted by the loader; not yet executable.", @@ -897,13 +897,9 @@ func renderDriverPage() string { fmt.Fprintln(&buf, "with `unknown_service_driver`, listing the drivers Onebox can run and directing") fmt.Fprintln(&buf, "you to a `daemon` workload.") fmt.Fprintln(&buf) - fmt.Fprintln(&buf, "That is deliberate: inventing an image from a name produces a container that") + fmt.Fprintln(&buf, "Inventing an image from a name produces a container that") fmt.Fprintln(&buf, "starts and stores nothing durable.") fmt.Fprintln(&buf) - fmt.Fprintln(&buf, "The runtime and connection tables are generated from the private driver catalogue.") - fmt.Fprintln(&buf, "The guidance and limitations below remain deliberately authored in the generator:") - fmt.Fprintln(&buf, "they are product explanations, not facts the catalogue can prove.") - fmt.Fprintln(&buf) fmt.Fprintln(&buf, "## Runtime catalogue") fmt.Fprintln(&buf) @@ -932,7 +928,7 @@ func renderDriverPage() string { driver.Name, escapeCell(serviceDriverTypicalUses[driver.Name]), connectionPartsCell(driver.ConnectionParts)) } fmt.Fprintln(&buf) - fmt.Fprintln(&buf, "A part the driver does not have — a database on a cache — is omitted rather than") + fmt.Fprintln(&buf, "A part the driver does not have (a database on a cache) is omitted, never") fmt.Fprintln(&buf, "written empty. Every driver has a password part; `redis` and `valkey` have a") fmt.Fprintln(&buf, "user part because Redis 6+ authenticates the built-in `default` user, and a URL") fmt.Fprintln(&buf, "with an empty username fails AUTH outright.") @@ -963,8 +959,8 @@ func renderDriverPage() string { fmt.Fprintln(&buf) fmt.Fprintln(&buf, "- **The service outlives every release.** Its own Compose project; no deploy and") fmt.Fprintln(&buf, " no rollback stops it or removes its volume.") - fmt.Fprintln(&buf, "- **The credential is generated on the server, once.** Not in your project, the") - fmt.Fprintln(&buf, " generated runtime, or the digest. Never rotated by a re-apply.") + fmt.Fprintln(&buf, "- **The credential is generated on the server, once.** It is absent from your project,") + fmt.Fprintln(&buf, " the generated runtime, and the digest. A re-apply never rotates it.") fmt.Fprintln(&buf, "- **The version binds into the release digest**, so a database upgrade under an") fmt.Fprintln(&buf, " untouched application cannot pass unnoticed.") fmt.Fprintln(&buf) @@ -988,13 +984,12 @@ func renderDriverPage() string { fmt.Fprintln(&buf) fmt.Fprintln(&buf, ":::caution[`mongodb` runs a standalone server, not a replica set]") fmt.Fprintln(&buf, "Change streams and multi-document transactions need a replica set. An application") - fmt.Fprintln(&buf, "using either will connect, authenticate, and *then* fail — Rocket.Chat reports") + fmt.Fprintln(&buf, "using either will connect, authenticate, and then fail. Rocket.Chat reports") fmt.Fprintln(&buf, "`The $changeStream stage is only supported on replica sets`.") fmt.Fprintln(&buf) fmt.Fprintln(&buf, "Onebox does not configure a replica set, so an application that needs one wants a") - fmt.Fprintln(&buf, "`daemon` workload it owns. This is a limitation of the driver rather than of the") - fmt.Fprintln(&buf, "contract, and it is stated here because the failure appears in the application's") - fmt.Fprintln(&buf, "logs rather than in anything Onebox says.") + fmt.Fprintln(&buf, "`daemon` workload it owns. The failure appears in the application's logs,") + fmt.Fprintln(&buf, "not in Onebox output.") fmt.Fprintln(&buf, ":::") fmt.Fprintln(&buf) @@ -1002,14 +997,14 @@ func renderDriverPage() string { fmt.Fprintln(&buf, "`postgres` is the one driver with an executable backup contract: declaring") fmt.Fprintln(&buf, "`backup` on it gives a continuous off-host archive and point-in-time recovery.") fmt.Fprintln(&buf, "Every other driver declares `policy_qualified: false`, so a backup policy on it") - fmt.Fprintln(&buf, "is refused at `ob validate` rather than accepted and left to do nothing — and its") + fmt.Fprintln(&buf, "is refused at `ob validate`; its") fmt.Fprintln(&buf, "data lives only on this host until you copy it off yourself.") fmt.Fprintln(&buf) fmt.Fprintln(&buf, "Onebox also refuses a major version change a driver cannot perform in place,") fmt.Fprintln(&buf, "rather than replacing the container and leaving the data intact and unreachable.") fmt.Fprintln(&buf) - fmt.Fprintln(&buf, "For `postgres`, declaring `backup` changes this — see") - fmt.Fprintln(&buf, "[Shipped vs proposed](/docs/reference/capabilities).") + fmt.Fprintln(&buf, "For `postgres`, a `backup` block changes this once `ob backup enable` succeeds;") + fmt.Fprintln(&buf, "see [Declaring is not enabling](/docs/guides/backups#declaring-is-not-enabling).") fmt.Fprintln(&buf, ":::") fmt.Fprintln(&buf) @@ -1055,12 +1050,12 @@ func renderErrorPage() string { fmt.Fprintln(&buf, generatedMarker) fmt.Fprintln(&buf) - fmt.Fprintln(&buf, "A code is a promise: it names a kind of failure that keeps meaning the same thing") + fmt.Fprintln(&buf, "A code names a kind of failure that keeps meaning the same thing") fmt.Fprintln(&buf, "across releases. Branch on the code, never on the sentence.") fmt.Fprintln(&buf) fmt.Fprintln(&buf, "## Validation codes") fmt.Fprintln(&buf) - fmt.Fprintln(&buf, "Adding one is a deliberate act. A test refuses any code the loader emits that is") + fmt.Fprintln(&buf, "A test refuses any code the loader emits that is") fmt.Fprintln(&buf, "not enumerated, and any enumerated code that nothing emits, so this table cannot") fmt.Fprintln(&buf, "drift in either direction.") fmt.Fprintln(&buf) @@ -1078,14 +1073,14 @@ func renderErrorPage() string { fmt.Fprintln(&buf, "## Operation codes") fmt.Fprintln(&buf) - fmt.Fprintln(&buf, "Raised while a command runs, rather than while the project file is read. These") + fmt.Fprintln(&buf, "Raised during command execution, not project-file loading. These") fmt.Fprintln(&buf, "are the codes a deploy, a rollback, a secret rotation or an audited exec puts in") fmt.Fprintln(&buf, "the `error.code` field, so they are the set to branch on when automating.") fmt.Fprintln(&buf) - fmt.Fprintln(&buf, "A blank command is deliberate: some failures have no honest Onebox command that") - fmt.Fprintln(&buf, "resolves them. A local artifact that could not be written is fixed on the") + fmt.Fprintln(&buf, "A blank command means no Onebox command resolves the failure. A local artifact") + fmt.Fprintln(&buf, "that could not be written is fixed on the") fmt.Fprintln(&buf, "filesystem, not by running the tool again. An angle-bracketed command names a") - fmt.Fprintln(&buf, "step to complete rather than a line to run verbatim.") + fmt.Fprintln(&buf, "step to complete, not a line to run verbatim.") fmt.Fprintln(&buf) fmt.Fprintln(&buf, "| Code | Means | Guidance role | Command |") fmt.Fprintln(&buf, "| --- | --- | --- | --- |") @@ -1178,7 +1173,7 @@ func renderCLIPage(ctx context.Context, obBin string) (string, error) { // actually asking when they pick between them. fmt.Fprintln(&buf, "## Which command changes nothing") fmt.Fprintln(&buf) - fmt.Fprintln(&buf, "Many commands change nothing — `status`, `logs`, `audit`, `schema`, `version` and `canonical` all") + fmt.Fprintln(&buf, "Many commands change nothing: `status`, `logs`, `audit`, `schema`, `version` and `canonical` all") fmt.Fprintln(&buf, "just report. These five are the ones that get confused with each other, because each one") fmt.Fprintln(&buf, "answers \"would this deploy work?\" from a different place.") fmt.Fprintln(&buf) diff --git a/site/src/content/docs/docs/concepts/evidence-not-declaration.mdx b/site/src/content/docs/docs/concepts/evidence-not-declaration.mdx index bb57e386..22cba0fd 100644 --- a/site/src/content/docs/docs/concepts/evidence-not-declaration.mdx +++ b/site/src/content/docs/docs/concepts/evidence-not-declaration.mdx @@ -1,27 +1,23 @@ --- title: Evidence, not declaration description: Why a field in a file is never allowed to assert that something is true on the host. -summary: The principle that separates what you declared from what is observed — and how it shapes approval, tiers, drift, and structured output. +summary: "The principle that separates what you declared from what is observed, and how it shapes approval, tiers, drift, and structured output." sidebar: order: 2 read_when: - "Understanding why an approval is separate from a plan" - "Understanding why declaring protection would not establish it" - - "Designing something that has to report status honestly" + - "Designing something that has to report status accurately" --- -A configuration file records what you **want**. It cannot record what is **true**. -Most tools blur that line, and the blur is where operators get hurt: a field -called `backup: true` reads as a fact about the host when it is only a sentence -in a file. - -Onebox keeps the two apart everywhere. +A configuration file records what you want, not what is true on the host; +`backup: true` is only a line in a file. Onebox keeps the two apart. ## Approval is not model intent > A statement that the user approved is data, not authority. -The current CLI requires a deliberate local human ceremony bound to the exact +The current CLI requires a local human ceremony bound to the exact sealed plan, operator label, server, observed state, inputs, risk, and expiry. `ob plan` and `ob approve` are separate commands so the reviewed object and the answer cannot drift unnoticed. The resulting `local_cli` artifact is @@ -52,13 +48,12 @@ while, **all at once**: Any one of those expiring returns the service to `Run` immediately. Historical proof stays visible but cannot be mistaken for current protection. -That is a deliberately expensive bar. A cheaper one — trusting the policy block — -would produce a green tier for a service whose backups have been failing for a -month. +Trusting the policy block alone would report a protected service even if its +backups had been failing for a month. :::note[Tier and envelope are separate] -`Managed` would always be accompanied by the recovery kind — `snapshot`, `pitr` -or `cold` — plus the observed recovery point and measured restore time. Otherwise +`Managed` would always be accompanied by the recovery kind (`snapshot`, `pitr` +or `cold`) plus the observed recovery point and measured restore time. Otherwise "managed Redis" reads as point-in-time recovery it does not have. ::: @@ -67,17 +62,15 @@ or `cold` — plus the observed recovery point and measured restore time. Otherw Read commands do not repair, converge, prune or create. `ob status` and `ob doctor` read evidence; they never trigger the check that produces it. -In the proposed assurance layer this is structural rather than promised: the -check process has no converge methods in its operation graph at all. +In the proposed assurance layer, the check process has no converge methods +in its operation graph. ## Origins are published -`ob canonical` annotates every value you did not write — `# default`, -`# shorthand`, `# override`. The same idea appears in structured output, which -labels authored, default, environment-override, observed and derived values. - -Knowing that `retainReleases: 5` was Onebox's choice rather than yours is the -difference between reviewing a configuration and reading one. +See [origin annotations](/docs/get-started/reading-it-back#ob-canonical-is-the-one-to-read-in-review) +for `ob canonical`, including `# default`, `# shorthand`, `# override` and defaults +such as `retainReleases: 5`. Structured output labels authored, default, +environment-override, observed and derived values. ## Checkout provenance @@ -99,9 +92,7 @@ worthless once that state may have moved. ## What this costs -Honest evidence produces worse-looking status pages. A service that oscillates +Observed evidence can produce worse-looking status pages. A service that oscillates between `Managed` and `Run` because a drill keeps being deferred for disk space looks broken, where a tool that trusted the declaration would look fine. -The alternative is worse. A green light that does not depend on anything being -true is not a green light. diff --git a/site/src/content/docs/docs/concepts/generated-compose.mdx b/site/src/content/docs/docs/concepts/generated-compose.mdx index 09b18b63..d069fd5f 100644 --- a/site/src/content/docs/docs/concepts/generated-compose.mdx +++ b/site/src/content/docs/docs/concepts/generated-compose.mdx @@ -1,6 +1,6 @@ --- title: Why Compose is generated -description: Why the runtime is derived from your declaration rather than read from a file you wrote. +description: Why Onebox derives the runtime from your declaration. summary: Compose is an artifact Onebox generates, not an input it reads. What that buys, and how you get out. sidebar: order: 3 @@ -18,18 +18,15 @@ Under the [ownership boundary](/docs/concepts/ownership-boundary), Onebox owns t runtime. Reading a runtime you wrote inverts that: you own it, and Onebox merely operates something it did not design and cannot fully reason about. -Concretely, that costs three things: +The Compose file and Onebox annotations could disagree, with nothing to +arbitrate between them. -**Two descriptions that can disagree.** The Compose file would say one thing, -the Onebox annotations another, and nothing would arbitrate. - -**No derivation.** Onebox could not choose the container name, the volume name, +Onebox could not choose the container name, the volume name, the router rule or the network, because you already would have. So it could not guarantee they are consistent, collision-free, or stable across releases. -**No honest defaults.** `strategy: rolling` needs a health check to gate it. When -the health check lives in a file Onebox merely reads, "is this rollout gated?" -becomes a question rather than a fact. +`strategy: rolling` needs a health check to gate it. Reading a health check from +an authored file would leave Onebox unable to guarantee that the rollout is gated. ## What generation buys @@ -43,15 +40,15 @@ becomes a question rather than a fact. them; if an unmanaged endpoint remains attached, destruction stops without deleting Onebox's recovery state or releasing host ownership. Once a resource exists its name cannot change without migration, and a foreign resource already - holding a derived name is refused rather than adopted. + holding a derived name is refused, not adopted. - **Digest binding.** The rendered Compose is bound into the plan, so what was reviewed is what executes. - **Refusals that mean something.** `strategy_ungated`, `route_collision`, `stateful_replicas` are all statements about the generated runtime, which is the only thing that will actually run. - **Typed routing.** Two workloads claiming the same entrypoint, protocol, hostname - and path is `route_collision`, rather than a proxy that accepts both and routes - to one with nothing saying which. + and path produce `route_collision`. This prevents the proxy from accepting both + and routing to one without saying which. ## A workload name is its identity @@ -75,9 +72,7 @@ ob preview # print the generated runtime, change nothing ob eject # write it out and hand it over, permanently ``` -`ob eject` strips the overlay, so what lands in your repository is ordinary -Compose — not Compose carrying Onebox labels you would then have to unpick — and -your workloads are repointed at it with your comments intact. +See [Eject](/docs/guides/eject) for the output and what remains on the host. ## And the escape hatch remains @@ -91,8 +86,7 @@ workloads: ``` Refusals apply only where the referenced service contradicts something Onebox -owns — `extends`, a fixed `container_name`, `network_mode`, labels in the `onebox.` +owns: `extends`, a fixed `container_name`, `network_mode`, labels in the `onebox.` or `traefik.` namespaces. See [Adopt an existing Compose file](/docs/guides/compose). -The result is a contract that generates by default and defers on request, rather -than one that can only do one of the two. +The contract generates by default and defers on request. diff --git a/site/src/content/docs/docs/concepts/ownership-boundary.mdx b/site/src/content/docs/docs/concepts/ownership-boundary.mdx index 96df4f97..9019fe8e 100644 --- a/site/src/content/docs/docs/concepts/ownership-boundary.mdx +++ b/site/src/content/docs/docs/concepts/ownership-boundary.mdx @@ -1,11 +1,11 @@ --- title: The ownership boundary description: Why one sentence about ownership drives every other decision in the product. -summary: The organising principle — you own your application containers, Onebox owns everything else — and the three design consequences that follow from it. +summary: The organising principle (you own your application containers, Onebox owns everything else) and the three design consequences that follow from it. sidebar: order: 1 read_when: - - "Understanding why the project file declares intent rather than describing containers" + - "Understanding why the project file declares application intent" - "Explaining the product's design rationale" --- @@ -16,25 +16,21 @@ Inside that boundary, the proxy, TLS, networks, release staging, supporting data services, schedules, locks, and the evidence needed to operate them are not your application, and therefore sit on Onebox's side of the line. -That sentence is the **direction**, not an inventory. What is owned today is -listed on [Shipped vs proposed](/docs/reference/capabilities), and the gap between the -two is stated rather than blurred. +That sentence states the direction. What is owned today is listed on +[Shipped vs proposed](/docs/reference/capabilities). ## Three consequences -This is the organising principle behind everything else. - -**It is why the project file declares intent rather than describing containers.** +**It is why the project file declares application intent.** If Onebox owns the runtime, then describing the runtime is Onebox's job, and asking you to do it as well would mean two descriptions that can disagree. **It is why Compose became a generated artifact.** Compose was once an input Onebox read. Under this boundary, reading a file you wrote would mean you own the -runtime and Onebox merely operates it — which is the opposite arrangement. See +runtime and Onebox merely operates it, reversing that ownership. See [Why Compose is generated](/docs/concepts/generated-compose). -**It is why a database is something you select rather than something you -configure.** `services: {postgres: 17}` is sufficient because the image, port, +**It is why you select a database and Onebox configures it.** `services: {postgres: 17}` is sufficient because the image, port, data path, health check and credential all sit on Onebox's side of the line. ## The boundary needs a name on it @@ -42,7 +38,7 @@ data path, health check and credential all sit on Onebox's side of the line. "Onebox owns everything else on the box" is only meaningful if exactly one application is doing the owning. Two Onebox applications on one host would both believe they own the proxy, the shared network, the release store and the host -lock — and the second one to converge would quietly undo the first. +lock, and the second one to converge would quietly undo the first. So ownership is written down. `ob bootstrap` records the owning application and the environment that claimed the host, and every mutating command reads that @@ -64,29 +60,28 @@ it, leaving Onebox's state available for a safe retry. A different *environment* of the same application is refused too, with `host_environment_mismatch`, and that refusal is the less obvious one. Every -runtime name Onebox derives — the Compose project, container names, volume -names — carries the application and not the environment, because one box runs +runtime name Onebox derives (the Compose project, container names, volume +names) carries the application and not the environment, because one box runs one application. Point staging at production's server and nothing collides: staging finds names that look like its own and adopts production's containers and volumes. The owner record is the only place that difference is visible, which is why it records the environment. A host claimed before environments were recorded carries the application alone. -That still identifies the owner, so it is honoured rather than refused; the next +That still identifies the owner, so it is honoured; the next `ob bootstrap` completes the record. -`ob bootstrap` is the one mutation that may claim an unowned host — that is what -it is for. Every other mutation requires the record to exist and to name this +`ob bootstrap` is the one mutation that may claim an unowned host. Every other mutation requires the record to exist and to name this application, so a mistyped target cannot silently adopt a machine by deploying to it. `ob preflight` reads without claiming, and reports an unowned host as -ready rather than as a failure. +ready. The check fails closed in both directions. An owner file that cannot be read is -not treated as an absent one — "unowned" and "unreadable" would otherwise be the +not treated as an absent one; "unowned" and "unreadable" would otherwise be the same answer, and the wrong one is destructive. Full destruction releases the record, which is what makes a host reusable for a -different application rather than permanently claimed. Partial destruction does +different application. Partial destruction does not: while volumes or service credentials remain, the record stays, because every command is gated on it and releasing early would strand the owner outside their own data. @@ -94,36 +89,23 @@ their own data. ## The boundary cuts both ways If Onebox owns a supporting database, it must say exactly when that ownership -includes recovery. A boundary that only ever expands is a marketing claim, not -an engineering one. +includes recovery. PostgreSQL has an executable backup contract, but declaring it is only a request. Protection begins after `ob backup enable` has installed the runtime, enabled continuous archiving, and taken the first base backup. Workload volumes are not covered, and every other durable driver refuses a backup policy rather -than accepting one it cannot honour. `ob doctor` reports every durable surface -that still has no off-host copy, because silence there would read as approval. +than accepting one it cannot honour. See [unprotected data](/docs/reference/capabilities#not-owned-at-all). -:::caution[The failure this prevents] -A product description that reads as a capability list is how an operator ends up -believing their database is backed up by something that has never taken a backup. -::: +See [why capability status is published](/docs/reference/capabilities#why-the-distinction-is-published). ## Where the line is not -Onebox owns the box, not the world: - -- A customer with root can still bypass it. -- A compromised host can lie about its own evidence. -- Onebox does not claim universal reversibility, high availability, or - protection from an adversarial infrastructure provider. - -Those are limits of the arrangement, not gaps in it. +See the [bounded safety guarantees](/docs/reference/policies#safety-claims-are-bounded). ## Supporting services and guarantee levels -Breadth and honesty are reconciled by making the guarantee level part of the -contract and visible wherever the service appears. A service is either +The guarantee level is part of the contract and shown wherever the service appears. A service is either user-authored or Onebox-managed, never both. Every durable driver runs pinned, persisted, health-gated, capped, observed, and @@ -132,5 +114,5 @@ contract today: continuous archiving, point-in-time restore, repository status, and an on-demand recovery drill. Other drivers remain explicit about the missing contract and refuse `backup` at validation. -The ordering is the point: a declaration is never presented as proof that +A declaration is never presented as proof that protection is running. See [Evidence, not declaration](/docs/concepts/evidence-not-declaration). diff --git a/site/src/content/docs/docs/concepts/safety-envelope.mdx b/site/src/content/docs/docs/concepts/safety-envelope.mdx index ff2ba7d8..d5a9a5d7 100644 --- a/site/src/content/docs/docs/concepts/safety-envelope.mdx +++ b/site/src/content/docs/docs/concepts/safety-envelope.mdx @@ -1,6 +1,6 @@ --- title: The safety envelope -description: Locks, fencing, journals, drift, expiry — the machinery that makes an interrupted operation recoverable. +description: "Locks, fencing, journals, drift, expiry: the machinery that makes an interrupted operation recoverable." summary: "How Onebox makes execution resumable and reversible: application and host locks, fencing epochs, append-only journals, state-bound plans, and what each protects against." sidebar: order: 5 @@ -17,11 +17,10 @@ and files on the host. ## The pieces **Locks.** An application lock, and a host lock above it. Operations that would -interfere serialize rather than race. +interfere are serialized. **Fencing epochs.** A stale process that wakes up after losing its lock cannot -act: its epoch no longer matches. This is what makes "the CLI lost the -connection" survivable rather than dangerous. +act: its epoch no longer matches. This prevents a stale process from acting after the CLI loses its connection. The epoch is durable authority, not a disposable counter. Onebox treats only a genuinely absent epoch file as a first run. If the file exists but is unreadable, @@ -65,18 +64,17 @@ An approval says: *this exact change, against this exact observed state, is authorised*. The moment the observed state may have moved, the approval is describing something that no longer exists. -Fifteen minutes is short enough that "the plan I approved" and "the state on the -host" are still the same claim. Longer, and approval quietly degrades into -permission to do something nobody looked at. +The fifteen-minute expiry limits how long an approval can refer to an earlier +observation. After expiry, the operator must review a new plan. ## Retry is safe by construction Mutations are idempotent under retry. Retrying with the same operation identity -resumes or returns the existing terminal result rather than creating an untracked +resumes or returns the existing terminal result without creating an untracked duplicate. That property is why the CLI is usable from CI and from an agent: a lost -connection is an inconvenience, not an incident. A retry first inspects the +connection can be retried safely. A retry first inspects the operation identity and uses resume or status behaviour; it never blindly starts a second deploy. @@ -96,20 +94,19 @@ and the first deploy does so for PostgreSQL services with declared extensions. ## What the envelope does not cover :::danger[Bounded claims] -Onebox does **not** claim universal reversibility, high availability, or -protection from an adversarial infrastructure provider. +See the [bounded safety guarantees](/docs/reference/policies#safety-claims-are-bounded). ::: - **A customer with root can bypass Onebox.** The envelope constrains Onebox's own operations, not the machine. - **A compromised host can lie about its own evidence.** Everything here assumes - the host reports honestly about itself. + the host reports accurately about itself. - **Rolling deployment cannot make a failed host available.** It avoids - interruption while the box is healthy. That is all it claims. + interruption while the box is healthy. - **Arbitrary external side effects are not reversible.** An email sent by a migration stays sent. -## `ob exec` is deliberately outside it +## The safety boundary of `ob exec` ```sh ob exec --reason "" -- @@ -119,20 +116,19 @@ ob exec --reason "" -- is indistinguishable from an intrusion, so the reason is part of the record before the command runs. -What Onebox guarantees here is narrow but real: exec takes the application lock +Exec takes the application lock and writes the mutation fence before it resolves a container, so it cannot run inside a workload a concurrent deploy is replacing, and it holds that lock for the life of the stream. It records the operator, the target and its kind, the exact container, the reason, a digest of the command, and the outcome. -What it does not do is claim the result. Nothing exec changes belongs to a +Onebox makes no claim about the result. Nothing exec changes belongs to a release, so nothing it changes survives the next deploy in any tracked sense, and nothing about it is reversible by rollback. Every other command states up front whether it contacts the server and whether it changes anything; `ob exec` -is the one row without a definite answer, and that is the point rather than an -omission. +is the one row without a definite answer. -The command text itself never enters the durable record — only its digest — +The command text itself never enters the durable record; only its digest does, because an operator pasting a one-off command is not thinking about whether it contains a credential. @@ -144,5 +140,5 @@ ob audit # recorded operations ob doctor --output json # local safety setup and runner compatibility ``` -None of these mutate. Observation cannot converge — see +None of these mutate. Observation cannot converge; see [Evidence, not declaration](/docs/concepts/evidence-not-declaration). diff --git a/site/src/content/docs/docs/concepts/what-onebox-refuses.mdx b/site/src/content/docs/docs/concepts/what-onebox-refuses.mdx index 55fc0656..fe18bf0a 100644 --- a/site/src/content/docs/docs/concepts/what-onebox-refuses.mdx +++ b/site/src/content/docs/docs/concepts/what-onebox-refuses.mdx @@ -1,7 +1,7 @@ --- title: What Onebox refuses description: The refusals worth knowing, and the principle behind all of them. -summary: Onebox fails at load rather than silently weakening a declaration. The pattern, the refusals most likely to surprise you, and why force cannot override them. +summary: Onebox refuses declarations it cannot honour. The pattern, the refusals most likely to surprise you, and why force cannot override them. sidebar: order: 4 read_when: @@ -9,10 +9,7 @@ read_when: - "Deciding whether a refusal is a bug or a design decision" --- -The pattern is one sentence: - -> A declaration Onebox cannot honour fails at load, rather than becoming -> something weaker that looks like it worked. +> Onebox refuses declarations it cannot honour. Every refusal below follows from it. The full list is the [error code catalogue](/docs/reference/errors), generated from the binary: validation @@ -21,55 +18,49 @@ command runs, and the lifecycle failure contract. ## The ones most likely to surprise you -**`strategy_ungated`** — asking for `strategy: rolling` without a health check. +**`strategy_ungated`**: asking for `strategy: rolling` without a health check. A rolling release stands the newcomer up and waits for it to report healthy; with nothing to wait for, "rolling" would silently mean "recreate with extra -steps". So it is refused rather than downgraded. +steps". So it is refused. -**`schedule_untranslatable`** — a cron expression whose meaning the host's -scheduler cannot preserve. A day-of-month *and* a day-of-week together is the -usual case: cron treats it as "either matches", so `0 2 1 * 1` fires on the 1st -**and** every Monday. Refused at load rather than running on days nobody chose. +**`schedule_untranslatable`**: a cron expression whose meaning the host's +scheduler cannot preserve. See [exact cron translation](/docs/guides/cron-jobs#cron-is-translated-exactly-or-refused). -**`unknown_service_driver`** — a driver outside the closed set of eleven. -Inventing an image from a name produces a container that starts and stores -nothing durable, which is worse than an error. +**`unknown_service_driver`**: a driver outside the closed set of eleven. +See the [driver catalogue](/docs/reference/drivers). -**`connection_variable_claimed`** — an authored value claiming a name a -managed-service connection supplies. The credential is generated on the server -and exists nowhere else; ordering could not protect it, so validation does. +**`connection_variable_claimed`**: an authored value claiming a name a +managed-service connection supplies. See [connection-variable validation](/docs/guides/databases#reaching-a-pooler-or-a-read-replica). -**`route_collision`** — two workloads claiming the same entrypoint, protocol, +**`route_collision`**: two workloads claiming the same entrypoint, protocol, hostname and path. The proxy would accept both and route to one, and nothing would say which. -**`stateful_replicas`** — a workload keeping durable state asking for more than +**`stateful_replicas`**: a workload keeping durable state asking for more than one replica. -**`override_not_permitted`** — an environment override touching anything outside +**`override_not_permitted`**: an environment override touching anything outside the permitted set. An environment may override exactly `replicas`, `resources`, `env`, `envFiles`, `strategy` and `routes`; everything else, `image` and -`command` and `volumes` among them, is refused. It is an allow-list rather than -a deny-list because the question is not "which fields are dangerous" but "which -fields can differ while this is still the same application" — a staging -environment that can swap the image is a different application wearing the same -name. +`command` and `volumes` among them, is refused. It is an allow-list of fields that can differ while this is still the same +application; a staging environment with a different image is a different +application under the same name. -**`service_settings_unsupported`** — a setting declared for a driver with no +**`service_settings_unsupported`**: a setting declared for a driver with no mechanism to read it. Silently ignoring it would leave you believing it applied. -**A rolling workload publishing a host port** — two replicas cannot both hold +**A rolling workload publishing a host port**: two replicas cannot both hold `8080` on the host, so the roll would either fail halfway or quietly become a recreate. Either give the workload `strategy: recreate`, or drop `publishedPorts` and route to it through the proxy. This one arrives as a plain -`project_invalid` today rather than a dedicated code. +`project_invalid` today, with no dedicated code. -**`host_owner_mismatch`** — a second application trying to mutate a host that +**`host_owner_mismatch`**: a second application trying to mutate a host that already belongs to one. Not a load-time refusal: it comes from the host itself, before anything changes. See [the ownership boundary](/docs/concepts/ownership-boundary). -**`host_environment_mismatch`** — a second *environment* of the same application +**`host_environment_mismatch`**: a second *environment* of the same application trying to mutate a host another environment claimed. Container and volume names carry the application, not the environment, so the two would not collide: the newcomer would adopt what is already there. See [the ownership @@ -77,20 +68,12 @@ boundary](/docs/concepts/ownership-boundary). ## Where a refusal is about safety, not tidiness -Application rollback, data recovery, and reversal of an external side effect are -different operations with different guarantees. Onebox classifies the risk and -refuses when evidence or a driver contract is insufficient. - -:::danger[Force cannot make an unsupported operation safe] -A force flag changes what you are permitted to attempt. It does not change what -the engine can prove. Where safety cannot be proven, the operation stops and -exposes honest recovery choices instead of proceeding. -::: +See the [bounded safety guarantees](/docs/reference/policies#safety-claims-are-bounded) +and the [recovery overrides](/docs/guides/rollback#when-recovery-refuses). A missing or invalid job result from a migration becomes `changed=unknown` and halts **before** workload replacement, unless a strong or break-glass local confirmation -authorized that exact plan. The unknown is treated as the dangerous case, because -it is. +authorized that exact plan. An unknown result is treated as the dangerous case. ## Refusals carry the way out @@ -108,11 +91,6 @@ classified as **diagnostic**, **next**, or **resolving**. It is validated to sta with `ob ` and to contain no credential material. An agent can branch on the code without mistaking a read-only inspection for a repair. -## Codes are promises - -> A code names a kind of failure that will keep meaning the same thing across -> releases. +## Stable error codes -Adding one is a deliberate act. A test refuses any code the loader emits that is -not enumerated, and any enumerated code that nothing emits — so the catalogue -cannot drift in either direction. Branch on the code, never on the sentence. +See the [error-code contract](/docs/reference/errors). diff --git a/site/src/content/docs/docs/get-started/first-deploy.mdx b/site/src/content/docs/docs/get-started/first-deploy.mdx index fc856aef..0132909a 100644 --- a/site/src/content/docs/docs/get-started/first-deploy.mdx +++ b/site/src/content/docs/docs/get-started/first-deploy.mdx @@ -1,7 +1,7 @@ --- title: Your first deploy description: From an existing Compose file to a released application, in one pass. -summary: The full path — scaffold ob.yml, inspect what Onebox understood, plan, approve, deploy — with the plan and local-confirmation artifacts explained. +summary: The full path (scaffold ob.yml, inspect what Onebox understood, plan, approve, deploy) with the plan and local-confirmation artifacts explained. sidebar: order: 3 read_when: @@ -11,9 +11,6 @@ read_when: import { Steps, FileTree } from '@astrojs/starlight/components'; -This is the whole path, once. Every command below either states that it contacts -nothing, or is explicit about what it changes. - ## Before you start You need: @@ -24,13 +21,10 @@ You need: available to that SSH account - the server's host key already recorded in `known_hosts` -If the server has no public SSH port, name the bastion it is reached through — -see [Deploy through a jump host](/docs/guides/jump-host). +If the server has no public SSH port, name the bastion it is reached through; see [Deploy through a jump host](/docs/guides/jump-host). -The first three steps are local and contact nothing. `ob bootstrap` is the first -command that changes the server. `ob preflight` then reads the provisioned host -without changing it or contacting a registry; the plan after it is also -read-only. +The first three steps are local; later steps state when they contact or change +the server. ## Deploy once @@ -47,9 +41,8 @@ read-only. that match what `ob validate` will say. :::caution - `ob init` is a starting point, not permission to deploy. Review workload - types, persistence semantics, readiness, job data effects, and the environment - server before running a plan. + Review the scaffolded `ob init` output before running a plan: workload types, + persistence semantics, readiness, job data effects, and the environment server. ::: 2. **Check the contract. Nothing is contacted.** @@ -77,9 +70,8 @@ read-only. `ob canonical` marks each value with where it came from: `# default` for one Onebox chose, `# shorthand` for a top-level key it moved into a workload, `# override` for one an environment supplied. `ob preview` prints the Compose - runtime it will generate. Read both before the first deploy — the difference - between a value someone chose and one that appeared by itself is what a - person checking a production configuration needs to see. + runtime it will generate. Read both before the first deploy; see + [origin annotations](/docs/get-started/reading-it-back#ob-canonical-is-the-one-to-read-in-review). 4. **Prepare the host.** This contacts and changes the server. @@ -87,7 +79,7 @@ read-only. ob bootstrap ``` -5. **Check host readiness.** This contacts the server but changes nothing. +5. **Check host readiness.** This contacts the provisioned server but changes nothing and contacts no registry. ```sh ob preflight @@ -210,7 +202,7 @@ moving data. ## Why plan and local confirmation are separate -The split creates a deliberate review ceremony and binds the answer to one exact +The split creates a review ceremony and binds the answer to one exact sealed plan. `ob approve` records a short-lived local confirmation whose digest detects modification; it is not authenticated identity or proof that the actor requesting the change was technically unable to create it. Environments that @@ -227,8 +219,8 @@ ob deploy --output ndjson --plan ob-plan.json --approval ob-approval.json ``` `ob approve` always prompts and there is no flag to skip it. A routine plan asks -yes or no; one that touches data — a migration, a destructive job, or an -unknown data effect — asks for the plan's identity to be typed back: the release +yes or no; one that touches data (a migration, a destructive job, or an +unknown data effect) asks for the plan's identity to be typed back: the release ID for a deploy, the job name for a job run. It reads the answer from stdin without checking for a terminal, so a pipeline supplies `y`, or that identity for the stronger ceremony. Anything else records nothing and exits cancelled. diff --git a/site/src/content/docs/docs/get-started/install.mdx b/site/src/content/docs/docs/get-started/install.mdx index 2d51f546..ec502e69 100644 --- a/site/src/content/docs/docs/get-started/install.mdx +++ b/site/src/content/docs/docs/get-started/install.mdx @@ -116,7 +116,7 @@ sudo dnf install ./onebox_2026.10.0-alpha_linux_amd64.rpm These are downloadable package files, not hosted APT or RPM repositories. WinGet is not published yet. Homebrew and Scoop metadata live in the dedicated -`labstack/homebrew-tap` and `labstack/scoop-bucket` repositories rather than the +`labstack/homebrew-tap` and `labstack/scoop-bucket` repositories, outside the Onebox source repository. :::caution[`go install` is not a release channel] @@ -174,10 +174,8 @@ build, test, formatting and check targets. Both commands support `--output json`. :::note[Why a checkout build can be refused] -When an environment sets `minOneboxVersion`, commit-derived and dirty -checkout builds **fail closed** — they are not released runners. That is -deliberate: an environment that pins a minimum is asking for a runner whose -identity can be checked, and a dirty working tree has none. +See [runner and plan-schema policy](/docs/reference/policies#runner-and-plan-schema-policy) +for how `minOneboxVersion` applies to checkout builds. ::: ## What the host needs @@ -185,29 +183,29 @@ identity can be checked, and a dirty working tree has none. A Linux server you can reach over SSH, with Docker Engine, Docker Compose, and the Docker Buildx plugin available to the configured SSH account. Onebox uses `docker buildx imagetools inspect` to bind tagged workload images to immutable -registry digests. There is no Onebox agent to install on the host — the CLI -connects over SSH, and scheduled work runs from host timers rather than a +registry digests. There is no Onebox agent to install on the host. The CLI +connects over SSH, and scheduled work runs from host timers without a resident process. All three are asserted as one set, by `ob bootstrap`, by `ob preflight`, and by every deploy, so a host that passes one of them passes the others. Each reports the version it observed alongside the result. This includes a project whose workload images are all digest-pinned: such a project never needs Buildx to -resolve anything, and is still refused on a host without it, because gates that -disagree about which host is deployable is the failure this set exists to -prevent. +resolve anything, and is still refused on a host without it, so the gates agree about which host is deployable. Distribution packages are a common source of a partial install. Ubuntu's `docker.io` gives a working daemon and no Buildx at all, and its `docker-buildx` 0.30.1 accepts `imagetools inspect --format`, ignores it, and exits 0. Neither is caught by a version comparison, so the guard is in two parts: the preflight probe -requires the client to advertise `--format`, and planning validates the digest it -actually returns, which is what catches a client that advertises the option and -ignores it. +reads only `docker buildx imagetools inspect --help` and requires the client to +advertise `--format`. It resolves no image and uses no registry quota. Missing +Buildx or missing support produces an image-resolver remedy. Planning validates +the returned digest, catching clients that advertise the option but ignore it +before they can produce a misleading registry error. Onebox does not download or run a Docker installer implicitly. Install Docker through your normal operator-managed provisioning before `ob bootstrap`. If you -deliberately want Onebox to invoke a pinned or configuration-managed installer, +want Onebox to invoke a pinned or configuration-managed installer, declare it as a remote bootstrap hook: ```yaml @@ -223,12 +221,7 @@ error before registry login, proxy setup, services, or evidence publication. A local bootstrap hook runs on the CLI machine and therefore cannot provision the remote host. -Run `ob preflight` after host provisioning and before the first plan. Its -Buildx probe reads only `docker buildx imagetools inspect --help`; it neither -resolves an image nor uses registry quota. It fails with an image-resolver -remedy when Buildx is missing or does not advertise the required `--format` -capability. Planning also validates the formatted output, so a client that -accepts but ignores the option cannot produce a misleading registry error. +Run `ob preflight` after host provisioning and before the first plan. `ob bootstrap` prepares the host. It is the one command that contacts and changes a server before any application exists. diff --git a/site/src/content/docs/docs/get-started/overview.mdx b/site/src/content/docs/docs/get-started/overview.mdx index df5dbc53..bc5eed28 100644 --- a/site/src/content/docs/docs/get-started/overview.mdx +++ b/site/src/content/docs/docs/get-started/overview.mdx @@ -1,7 +1,7 @@ --- title: What Onebox is description: The ownership boundary, the scope, and who this is for. -summary: What Onebox owns on the box, what stays yours, and the scope it deliberately refuses. Read before deciding whether it fits. +summary: What Onebox owns on the box, what stays yours, and the scope it refuses. Read before deciding whether it fits. sidebar: order: 1 read_when: @@ -10,10 +10,9 @@ read_when: --- Onebox is the production-operations layer for an application intentionally -running on one server. It supplies typed perception, bounded intent, durable -state, exact approval, and a fenced execution engine — so that releasing, -recovering, and maintaining that application is something a person or an agent -can do without assembling it from shell scripts. +running on one server. It supplies state-bound plans, exact approvals, locks, fencing, and durable +journals so a person or an agent can release, recover, and maintain the +application without assembling those operations from shell scripts. ## The ownership boundary @@ -24,7 +23,7 @@ Inside that boundary, Onebox owns the generated application runtime: the proxy, TLS, networks, release staging, supporting data services, scheduled jobs, locks, and the evidence needed to operate them safely. -That sentence is the **direction**, not an inventory. Owned today: +That sentence states the direction. Owned today: - host bootstrap and the container runtime check - the proxy and its TLS @@ -37,7 +36,7 @@ That sentence is the **direction**, not an inventory. Owned today: **Not owned today:** host or Docker installation, workload-volume backups, backup contracts for non-PostgreSQL drivers, unattended full recovery drills, -and log rotation. Onebox says so rather than implying otherwise. +and log rotation. :::caution[Why the distinction matters] A declared backup policy is not protection. PostgreSQL becomes protected only @@ -48,11 +47,11 @@ and taken the first base backup. ## Scope One application per environment, on one active production host. That application -may have as many workloads as it needs — a server, workers, jobs, databases, -caches, a proxy — but the unit Onebox owns is the application, and a host runs +may have as many workloads as it needs (a server, workers, jobs, databases, +caches, a proxy) but the unit Onebox owns is the application, and a host runs one of them. -That last part is enforced, not advisory. `ob bootstrap` writes an owner record +`ob bootstrap` enforces this by writing an owner record on the host, and every command that changes anything checks it first. Point a second application at the same machine and it refuses before touching anything: @@ -62,7 +61,7 @@ host is owned by application "shop"; application "blog" cannot mutate it The record is released only when nothing of the application is left: volumes removed, and the proxy removed too if this application manages it. Keeping it -while data survives is deliberate — ownership is what lets you come back and run +while data survives lets you come back and run `ob destroy --volumes`, and a host released early is one whose remaining volumes its owner can no longer reach. `ob destroy` tells you which command releases it. `ob preflight` reports the current owner, and passes on a host nobody has claimed @@ -80,12 +79,11 @@ Onebox is **not**: ## The interface is the CLI -The CLI is the product interface, for people and for agents. It is not a fallback -or a transitional path. +The CLI is the product interface, for people and for agents. There is no MCP surface. A read-only tool list constrains nothing when every mutation goes through the CLI anyway and the agent can run `ob deploy` in a -shell — the boundary it appears to provide does not exist. Point an agent at the +shell; the boundary it appears to provide does not exist. Point an agent at the `ob` binary the way you would point it at `git` or `gh`. ## Where to go next @@ -93,4 +91,4 @@ shell — the boundary it appears to provide does not exist. Point an agent at t - [Install](/docs/get-started/install), then [your first deploy](/docs/get-started/first-deploy). - [The ownership boundary](/docs/concepts/ownership-boundary) explains why this framing drives the rest of the design. -- [Shipped vs proposed](/docs/reference/capabilities) is the honest inventory. +- [Shipped vs proposed](/docs/reference/capabilities) lists the current capabilities. diff --git a/site/src/content/docs/docs/get-started/reading-it-back.mdx b/site/src/content/docs/docs/get-started/reading-it-back.mdx index ff980823..e1e5d5db 100644 --- a/site/src/content/docs/docs/get-started/reading-it-back.mdx +++ b/site/src/content/docs/docs/get-started/reading-it-back.mdx @@ -1,7 +1,7 @@ --- title: Reading the file back description: The four commands that contact nothing, and what each one answers. -summary: ob validate, canonical, preview and schema — what each answers, and why origin annotations matter when reviewing a production configuration. +summary: "ob validate, canonical, preview and schema: what each answers, and why origin annotations matter when reviewing a production configuration." sidebar: order: 4 read_when: @@ -23,15 +23,15 @@ Four commands contact nothing and change nothing. Run them before you ever run It prints every effective value with its origin annotated: -- `# default` — Onebox chose it -- `# shorthand` — you wrote it at the top level and it became a workload field -- `# override` — an environment override supplied it +- `# default` (Onebox chose it +- `# shorthand`) you wrote it at the top level and it became a workload field +- `# override`: an environment override supplied it -That distinction is the point. The difference between a value someone chose and +The difference between a value someone chose and one that appeared by itself is exactly what a person checking a production configuration needs to see, and it is invisible in the file you wrote. -## `ob schema` keeps your editor honest +## `ob schema` supports editor validation ```sh ob schema --out onebox.schema.json @@ -54,17 +54,16 @@ type is what `ob validate` tells you afterwards. :::caution[One place your editor is ahead of the engine] The published schema includes `backupTargets`, `services..backup` and `externalServices`. Your editor will complete them and `ob validate` accepts -them, but the behaviour behind them is not yet executable — see +them, but the behaviour behind them is not yet executable; see [Shipped vs proposed](/docs/reference/capabilities). ::: ## Reading the whole command surface -An operator can afford to find out by trying. An agent cannot. Every command's -help states whether it contacts the server and whether it changes anything, and +So an agent need not find out by trying, every command's help states whether it contacts the server and whether it changes anything, and the [structured output contracts](/docs/reference/policies#structured-output-contracts) classify every command that accepts `--output`, with the modes each one takes. -The odd row there is `ob exec`, the only command that refuses JSON, and that is -the point: it runs outside the journal and the safety regime, and nothing it -changes belongs to any release. +`ob exec` is the only command in that table that refuses JSON. It records +invocation evidence but makes no claim about the result, and nothing it changes +belongs to any release. See [its safety boundary](/docs/concepts/safety-envelope#the-safety-boundary-of-ob-exec). diff --git a/site/src/content/docs/docs/guides/backups.mdx b/site/src/content/docs/docs/guides/backups.mdx index a67363fa..e13afc58 100644 --- a/site/src/content/docs/docs/guides/backups.mdx +++ b/site/src/content/docs/docs/guides/backups.mdx @@ -1,7 +1,7 @@ --- title: Back up a database description: Continuous off-host backups for PostgreSQL, and how to prove they restore. -summary: How to declare a backup policy and a repository, what `ob backup enable` actually does, and why a backup nobody has restored is a hypothesis. +summary: How to declare a backup policy and a repository, what `ob backup enable` actually does, and how to verify recovery. sidebar: order: 3 read_when: @@ -44,7 +44,7 @@ and changing its endpoint is one edit. ## Declaring is not enabling A `backup` block is a request. Until `ob backup enable` succeeds, the service -renders as an ordinary unprotected server — `ob validate` accepts the policy and +renders as an ordinary unprotected server: `ob validate` accepts the policy and nothing archives. ```console @@ -60,12 +60,11 @@ That one command pins the image by registry digest. That image contains the compatible WAL-G executable; Onebox stages only the generated credential adapter on the host. It decrypts and installs the destination credentials, restarts the server with WAL archiving on, installs the timers, and takes the first base -backup. **It is not finished until that base backup exists** — WAL archiving +backup. **It is not finished until that base backup exists**: WAL archiving with nothing to replay onto recovers nothing, and reporting success there would be telling you the database is protected at the moment it is not. -The restart is a real restart. Enabling is a maintenance action, not a -configuration change. +Enabling restarts the server, so treat it as maintenance. The scheduled base backup does not give up on its first fault. A push streams gigabytes through many multipart uploads and aborts on the first part the @@ -77,7 +76,7 @@ says so, and the journal has each attempt's reason. ## The credential file -The encrypted file your target names needs three entries — the two you named, +The encrypted file your target names needs three entries: the two you named, plus the repository key: ```sh @@ -93,7 +92,7 @@ the host. ## Retention is derived -`retention` has two floors and Onebox keeps whichever is larger — the number of +`retention` has two floors and Onebox keeps whichever is larger: the number of generations, or enough of them to span the window at your schedule's rate: | schedule | window | generations kept | @@ -106,9 +105,8 @@ generations, or enough of them to span the window at your schedule's rate: ## What the repository can recover -Every figure comes from the repository, not from the project — and the policy is -printed beside it, because "is my promise being kept" is the question you came -with. +Every figure comes from the repository, not from the project, and the declared +policy is printed beside it for comparison. ```console $ ob backup status database @@ -122,10 +120,10 @@ BACKUP COMPLETED FROM WAL base_000000010000000000000007 2026-08-19T02:00:09Z 000000010000000000000007 ``` -A repository younger than its window says so rather than implying otherwise: +A repository younger than its window reports: `shorter than the declared window — this repository does not reach that far back -yet`. That is not a fault on a service enabled this morning, but it is not the -promise either, and only the report can tell you which one you are looking at. +yet`. On a newly enabled service this is expected, but the declared window is not +yet covered. The report shows whether it is covered. Status also asks the server whether it is still archiving. A correct WAL-G binary in the pinned PostgreSQL image says the tooling is in place; it says nothing about @@ -134,8 +132,7 @@ installed. Anything that drifted is printed as a `drift` line above the figures. ## The archive check -A base backup plus a gapped WAL stream recovers to the backup and no further — -a nightly snapshot wearing the label of point-in-time recovery. `ob backup +A base backup plus a gapped WAL stream recovers to the backup and no further: effectively a nightly snapshot, not point-in-time recovery. `ob backup verify` is the check for that, and the timer from `drill.schedule` runs the same one unattended. @@ -157,14 +154,14 @@ itself and is reported on the next run if it does not. ## Moving the repository Editing a target's bucket or endpoint, or pointing the service at a different -target, takes effect at the next `ob backup enable` — and Onebox says what that +target, takes effect at the next `ob backup enable`: and Onebox says what that means: ```console → database now archives to backup target "s3://elsewhere/shop_database". What "s3://onebox-backups/shop_database" holds is untouched, but this repository starts from the backup being taken now, so the declared recovery window begins here ``` -By default, everything else — a restore, a drill, status, retention — keeps +By default, everything else (a restore, a drill, status, retention) keeps reading the repository the service was actually bound to, so editing a target cannot silently point a recovery at a repository the history is not in. Status, drill, and restore can also select an older cluster generation explicitly. @@ -177,7 +174,7 @@ PostgreSQL's own system identifier: s3://onebox-backups/shop_database/clusters/7513211627332151223 ``` -That is not decoration. Every fresh cluster starts its write-ahead log at +Every fresh cluster starts its write-ahead log at `000000010000000000000001`, so a new database archiving where an old one left off would write object names that already exist with different contents. The archiver refuses to overwrite them, and because PostgreSQL archives strictly in @@ -200,14 +197,12 @@ $ ob backup drill database A drill recovers into a throwaway volume, proves the cluster opens and answers a query, then discards it. The live service is never touched. It runs the same code -as a real restore and stops before the last step — a drill exercising its own -path would prove the drill works, not the backups. +as a real restore and stops before the last step, so the drill tests the recovery path used for backups. :::caution[The scheduled drill verifies; it does not restore] The timer installed from `drill.schedule` runs an archive-continuity check, not a full recovery: the orchestration lives in `ob`, and Onebox puts no agent on your -host. Run `ob backup drill` from CI on the cadence your policy declares. A backup -nobody has restored is a hypothesis. +host. Run `ob backup drill` from CI on the cadence your policy declares. ::: ## Recovering @@ -217,7 +212,7 @@ $ ob backup restore database --to 2026-08-19T17:31:44Z --confirm database ``` The recovered cluster is built beside the live one and has to start, promote and -answer a query before anything touches the running database — so a repository +answer a query before anything touches the running database, so a repository that cannot recover fails while the database it would have replaced is still serving. The data being replaced is copied aside under a dated volume name and retained after restore. Only a later explicit `ob destroy --volumes` removes @@ -228,8 +223,7 @@ for the approval flow to bind to, so the guard is the name of the thing being replaced. Without `--to`, recovery goes to the newest recoverable point. With `--to`, -Onebox picks the newest base backup that finished at or before your point — -replay only moves forward, so a later base can never reach an earlier moment. A +Onebox picks the newest base backup that finished at or before your point; replay only moves forward, so a later base can never reach an earlier moment. A point older than everything in the repository is refused, and the refusal tells you when the oldest backup finished. @@ -270,19 +264,18 @@ $ ob backup disable database --confirm database Archiving stops, the timers go, the service restarts unprotected, and the destination credentials are removed from the host. **The repository is not -touched** — but reading or recovering from it needs backup enabled again, +touched**, but reading or recovering from it needs backup enabled again, because the tooling and credentials that reach it live in the protected service. Re-enabling the *same* database returns to the same repository: the cluster's identifier has not changed, so neither has its generation. Re-enabling after the -volume has been replaced does not, for the reason in the caution above — recover +volume has been replaced does not, for the reason in the caution above; recover first if what you want is the old history. ## What is not covered Only the `postgres` driver has an executable contract, on versions 17 and 18. -Every other driver refuses a `backup` policy at validate rather than accepting -one it cannot honour. A workload's own volume is never copied anywhere — `ob +Every other driver refuses a `backup` policy it cannot honour at validate. A workload's own volume is never copied anywhere: `ob doctor` names every workload holding durable data. A managed service declared `persistence: {mode: Ephemeral}` is listed as a pass because its data is disposable; doctor does not suggest adding a backup. Services with durable or diff --git a/site/src/content/docs/docs/guides/compose.mdx b/site/src/content/docs/docs/guides/compose.mdx index de0f8fed..c0ad7947 100644 --- a/site/src/content/docs/docs/guides/compose.mdx +++ b/site/src/content/docs/docs/guides/compose.mdx @@ -17,8 +17,8 @@ ob init ``` It reads the Compose file you already have and writes an `ob.yml` that describes -the same application in Onebox's terms. Review it — workload roles, persistence -semantics, readiness, job data effects, and the environment's server — before +the same application in Onebox's terms. Review it (workload roles, persistence +semantics, readiness, job data effects, and the environment's server) before running a plan. ## When the model does not fit @@ -32,15 +32,13 @@ workloads: compose: docker-compose.yml#legacy ``` -The named service is copied verbatim with Onebox's overlay applied. This is the -escape hatch, and it is deliberate — Onebox can operate any containerised +The named service is copied verbatim with Onebox's overlay applied. Onebox can operate any containerised application inside its envelope when health, rollout, persistence and job -semantics are declared honestly, and `compose:` covers the rest. +semantics are declared accurately, and `compose:` covers the rest. ## Six refusals, and why -Each of these contradicts something Onebox owns, so it fails at load rather than -producing a runtime that disagrees with itself. +Each of these contradicts something Onebox owns, so it fails at load. | Code | The referenced service… | | --- | --- | @@ -54,11 +52,10 @@ producing a runtime that disagrees with itself. Plus the structural ones: `compose_ref_malformed` (not `path#service`), `compose_file_unreadable`, `compose_file_unparsable`, `compose_service_missing`. -## An adopted workload is not a second-class one +## Adopted workloads resolve like inline ones A workload's source never changes what it receives. An application adopted -through `compose:` resolves exactly what an application declared inline resolves -— the same environment file precedence, the same connection wiring, the same +through `compose:` resolves exactly what an application declared inline resolves: the same environment file precedence, the same connection wiring, the same release behaviour. The one difference is precedence *inside* the container: the referenced Compose @@ -67,8 +64,7 @@ service's own `env_file` sits at the bottom of the stack, below the resolved ## Migrating away from adoption -Adoption is a resting place, not a destination. When you can describe the -workload in the schema, do — you get health-gated rollout, typed routing, derived +When you can describe the workload in the schema, move it there. You get health-gated rollout, typed routing, derived names and drift detection that a verbatim copy cannot give you. If you want to go the other way entirely, see [Eject](/docs/guides/eject). diff --git a/site/src/content/docs/docs/guides/cron-jobs.mdx b/site/src/content/docs/docs/guides/cron-jobs.mdx index b27cd7d2..94b49a00 100644 --- a/site/src/content/docs/docs/guides/cron-jobs.mdx +++ b/site/src/content/docs/docs/guides/cron-jobs.mdx @@ -44,7 +44,7 @@ result. Set a longer duration for jobs that legitimately need it. `catchUp` defaults to `true`: if the host was off at the scheduled time, the timer runs once after it returns. Set it to `false` for time-sensitive work that -should be skipped rather than run late. +should be skipped if it cannot run on time. ## Retry inside one firing @@ -112,7 +112,7 @@ cleanup preserves the leased Compose document, environment files, and release-bound mounts until the job exits. `ob status` reports the policy, the pinned release, start time, and timeout while it runs. -This is deliberately fail-closed. `deployLock: Pinned` is accepted only for a +`deployLock: Pinned` is accepted only for a Onebox-rendered job declaring `dataEffect: None`. Migration, destructive, unknown-effect, and adopted-Compose jobs remain exclusive because pinning their files cannot prove that a concurrent deployment is safe for the data or @@ -223,8 +223,8 @@ workstation's. ``` The last one declares a day-of-month **and** a day-of-week. Cron treats that as -"either matches" — it fires on the 1st and on every Monday. Onebox refuses it at -load rather than running on days nobody chose. +"either matches": it fires on the 1st and on every Monday. Onebox refuses it at +load because the host scheduler cannot preserve that meaning. `timezone` takes an IANA zone name and defaults to `UTC`. Generated timers use one-second accuracy so systemd does not coalesce distinct @@ -251,7 +251,7 @@ firing, it does not kill work in progress. The pause is recorded on the host, not in the project, which is what lets it survive a deploy. Reconciliation will not start a paused timer, so a pause cannot be undone by someone else's release. `--reason` is required, and is -kept with the operator and the time, because a job that is deliberately not +kept with the operator and the time, because a job that is paused and not running is indistinguishable from one that is broken: ```text @@ -275,8 +275,7 @@ logged. Both take `--break-lock`, because a running job holds the application lock for its whole run and a crashed deploy leaves one behind. Resuming starts the timer again; the next run is the next scheduled elapse. It -does not run the job now, and does not make up firings missed while paused — -`ob job run` is how you ask for an immediate run. +does not run the job now, and does not make up firings missed while paused; `ob job run` is how you ask for an immediate run. Deleting a job from the project clears its pause along with its units, so a name reused later does not come back stopped for a reason from a previous diff --git a/site/src/content/docs/docs/guides/databases.mdx b/site/src/content/docs/docs/guides/databases.mdx index 1b70dd8f..e98dd3ab 100644 --- a/site/src/content/docs/docs/guides/databases.mdx +++ b/site/src/content/docs/docs/guides/databases.mdx @@ -50,10 +50,8 @@ skip it on a live application, the next deploy stops at preflight with `service "postgres" is not running — start it with ob bootstrap or ob service apply`. :::caution[Mount changes are refused by default] -If applying a service would detach or replace a volume that holds data, it stops -rather than proceeding. Passing `--allow-destructive-mounts` says you have -accepted that. A major version transition has no override at all — that is a -migration, not a setting. +If applying a service would detach or replace a volume that holds data, it stops. Passing `--allow-destructive-mounts` says you have +accepted that. A major version transition has no override; perform it as a migration. ::: The service's name is its driver unless `driver` says otherwise, so a second @@ -111,7 +109,7 @@ package has a license unsuitable for a general customer database image. Use a dedicated TimescaleDB service until the upstream Apache package is clean. An existing backup-protected PostgreSQL 18 service may still be pinned to the -older official image. Migrate it deliberately: disable backup while the old +older official image. To migrate it: disable backup while the old project file is still active, add the extension declaration and deploy, then enable backup again: @@ -127,8 +125,8 @@ transition. Eleven drivers are supported: `postgres`, `mysql`, `mariadb`, `redis`, `valkey`, `mongodb`, `rabbitmq`, `minio`, `meilisearch`, `clickhouse`, `nats`. Anything -else is refused with `unknown_service_driver` — guessing an image from a name -produces a container that starts and stores nothing durable. +else is refused with `unknown_service_driver`; see +the [driver catalogue](/docs/reference/drivers). ## Wire the connection @@ -171,7 +169,7 @@ services: The right-hand side is a connection part: `url`, `host`, `port`, `user`, -`password`, `database`. A part the driver does not have — a database on a cache — +`password`, `database`. A part the driver does not have (a database on a cache) is omitted rather than written empty. `needs` also decides release order when `deployment.order` is absent. @@ -182,8 +180,7 @@ is omitted rather than written empty. Deploy can recreate PostgreSQL services with declared extensions when their configuration changes; otherwise, deploy and rollback leave services running. Neither removes their volumes. -- **Its credential is generated on the server, once.** Not in your project, not - in the generated runtime, not in the digest. Never rotated by a re-apply. +- **Its credential follows the [driver credential contract](/docs/reference/drivers#what-a-declaration-gets-you).** - **Its version binds into the release digest**, so a database upgrade under an untouched application cannot pass unnoticed. @@ -208,8 +205,8 @@ carries the credential and the credential never travels. :::caution[Shadowing a connection variable is refused] Claiming a name a managed-service connection supplies fails with `connection_variable_claimed`, naming both the variable and the service. The -credential exists nowhere else, so nothing authored may claim its name — ordering -cannot protect it, so validation does. +credential exists nowhere else, so nothing authored may claim its name. Validation prevents the +collision before release ordering can affect it. ::: ## When to use a daemon workload instead @@ -226,21 +223,20 @@ Use a daemon when you need something outside the driver set, or a topology the driver does not provide. :::caution[MongoDB runs standalone, not a replica set] -Change streams and multi-document transactions need a replica set. An application -using either will connect, authenticate, and *then* fail — in its own logs, not -in anything Onebox says. If you need one, run a `daemon` workload you own. +See the [MongoDB driver limitations](/docs/reference/drivers#known-limitations) +for applications that need a replica set. ::: :::caution[Back it up] A `postgres` service can declare `backup` and Onebox will keep a continuous -off-host archive it can recover to a point in time — see +off-host archive it can recover to a point in time; see [backups](/docs/guides/backups). Declaring the policy is not enough on its own: `ob backup enable` is what establishes it, and until then the service renders as an ordinary unprotected server. Every other driver declares `policy_qualified: false`, so a backup policy on -it is refused rather than quietly ignored, and copying its data off the box is -your responsibility — a `job` workload on a `schedule` is the usual answer. +it is refused, and copying its data off the box is +your responsibility: a `job` workload on a `schedule` is the usual answer. ::: Full field list: [`services`](/docs/reference/fields/services). diff --git a/site/src/content/docs/docs/guides/eject.mdx b/site/src/content/docs/docs/guides/eject.mdx index 7d5d3ae9..4f2b3ad2 100644 --- a/site/src/content/docs/docs/guides/eject.mdx +++ b/site/src/content/docs/docs/guides/eject.mdx @@ -14,19 +14,17 @@ ob eject ``` It writes the generated runtime into your repository and hands it over -permanently. The overlay is stripped, so the file is ordinary Compose — not -Compose with Onebox labels you would then have to unpick — and your workloads are +permanently. The overlay is stripped, so the file is ordinary Compose, and your workloads are repointed at it with your comments intact. `--out` chooses the destination. It defaults to a free name beside the project, -so an existing file is stepped around rather than written over. An explicit +so it avoids overwriting an existing file. An explicit `--out` that already exists is refused; `--overwrite` is the way past that. ## Why the exit is a command -A tool that generates your runtime is asking for trust in proportion to how hard -it is to leave. Making the exit a first-class, tested command is how that trust -is earned rather than requested. +Because Onebox generates your runtime, leaving must be possible, so the exit +is a tested command. It also means you can inspect the real thing at any time without committing to anything: @@ -55,10 +53,8 @@ ob eject # write it out and hand it over ## After ejecting You own the Compose file, the container names, the routing labels, and the -lifecycle. Onebox's release staging, journal, drift detection, approval gating -and rollback no longer apply to those workloads — that is the trade you have -chosen, and it is the same trade you would have had if you had never adopted -Onebox at all. +lifecycle. If you operate those workloads outside Onebox, its release staging, journal, +drift detection, approval gating and rollback no longer apply to them. If you want to tear the application down instead of handing it over: diff --git a/site/src/content/docs/docs/guides/environment-variables.mdx b/site/src/content/docs/docs/guides/environment-variables.mdx index b0c531e5..530326b3 100644 --- a/site/src/content/docs/docs/guides/environment-variables.mdx +++ b/site/src/content/docs/docs/guides/environment-variables.mdx @@ -1,7 +1,7 @@ --- title: Environment variables description: Every variable Onebox sets or reads in hooks, jobs, backup helpers, and local tooling. -summary: The ONEBOX_ namespace — what Onebox injects into hooks and jobs, and what it reads from your shell and CI. +summary: "The ONEBOX_ namespace: what Onebox injects into hooks and jobs, and what it reads from your shell and CI." sidebar: order: 9 read_when: @@ -15,7 +15,7 @@ else's hook script. Your application's own variables are never touched. A workload that expects `FANOUT_*`, `DATABASE_URL`, or `OTEL_EXPORTER_OTLP_ENDPOINT` receives exactly -those keys, unchanged — Onebox passes workload environment through rather than +those keys, unchanged. Onebox passes workload environment through rather than namespacing it. ## Injected into local hooks @@ -31,7 +31,7 @@ process can borrow: | `ONEBOX_SERVER` | `user@host`, valid as an OpenSSH destination | | `ONEBOX_SSH_USER` | The SSH user | | `ONEBOX_SSH_PORT` | The SSH port, separate because `user@host:port` is invalid for both `ssh` and `rsync` | -| `ONEBOX_SSH_JUMP` | The jump host, empty on a direct connection — see [Deploy through a jump host](/docs/guides/jump-host) | +| `ONEBOX_SSH_JUMP` | The jump host, empty on a direct connection: see [Deploy through a jump host](/docs/guides/jump-host) | | `ONEBOX_RELEASE_DIR` | Absolute path of the release directory on the server | | `ONEBOX_RELEASE_ID` | The release identifier | @@ -73,11 +73,11 @@ printf '{"schema_version":"onebox.run/job-result/v1alpha1","changed":true}' > "$ `ONEBOX_LOCAL` is the only one of these Onebox itself reads; the rest are build and release controls read by `just` and the release script. -Contributor and test controls — `ONEBOX_E2E`, `ONEBOX_E2E_*`, `ONEBOX_SERVER_E2E`, -and `ONEBOX_UPDATE_VERDICTS` — follow the same prefix. `just e2e` sets +Contributor and test controls (`ONEBOX_E2E`, `ONEBOX_E2E_*`, `ONEBOX_SERVER_E2E`, +and `ONEBOX_UPDATE_VERDICTS`) follow the same prefix. `just e2e` sets `ONEBOX_E2E` for you; the rest are set by hand when running a suite or refreshing a frozen fixture. Nothing in this namespace reaches a generated runtime. Generation reads no process environment at all, so the same project renders the same Compose on -your laptop and in CI, and a digest keeps meaning something. +your laptop and in CI, so its digest is reproducible. diff --git a/site/src/content/docs/docs/guides/jump-host.mdx b/site/src/content/docs/docs/guides/jump-host.mdx index df392a7c..543a0288 100644 --- a/site/src/content/docs/docs/guides/jump-host.mdx +++ b/site/src/content/docs/docs/guides/jump-host.mdx @@ -19,7 +19,7 @@ environments: jump: deploy@bastion.example.com ``` -`jump` sits beside `server` and takes the same two forms it does — the one-line +`jump` sits beside `server` and takes the same two forms it does: the one-line `user@host` above, or an object when you want to be explicit: ```yaml @@ -33,14 +33,14 @@ environments: ``` The user and port are optional in both forms: port 22 is used when you name -none, and `$USER` supplies the user — `ob` does not read `~/.ssh/config`, so a +none, and `$USER` supplies the user. `ob` does not read `~/.ssh/config`, so a `User` or `ProxyJump` written there has no effect on it. An environment with no `jump` connects directly, exactly as before. An IPv6 bastion is bracketed in the one-line form, where the brackets are what separate the address from the port, and bare in the object form, where `host` and `port` are already separate fields. Brackets carried over into `host` are -stripped rather than refused: +stripped: ```yaml jump: "deploy@[2001:db8::1]:2222" @@ -63,16 +63,15 @@ Both hops are verified and authenticated separately: 4. The **server's** host key is checked against `known_hosts`, independently. 5. Onebox authenticates to the server. -A trusted bastion does not vouch for the server. Trusting one is not trusting -the other, and neither key is ever accepted implicitly. +A trusted bastion does not vouch for the server. Neither key is accepted implicitly. The SSH agent is never forwarded. Your local agent may sign for either hop, but its socket is not exposed to the bastion, so a compromised jump host cannot borrow your identity to reach anything else. Exactly one hop is supported. There is no `ProxyCommand`, and Onebox does not -read `~/.ssh/config` — a `ProxyJump` there has no effect on `ob`, because the -transport dials and verifies on its own rather than shelling out to `ssh`. +read `~/.ssh/config`: a `ProxyJump` there has no effect on `ob`, because the +transport dials and verifies on its own without shelling out to `ssh`. ## Enrolling both host keys @@ -95,7 +94,7 @@ the bastion at the moment you enrolled it. Comparing it against the key printed on the server's own console is the stronger move where you can. If the server listens on a non-default port, `known_hosts` must be keyed with -it — `ssh-keyscan -p 2222` writes the `[host]:port` form Onebox looks up. +it; `ssh-keyscan -p 2222` writes the `[host]:port` form Onebox looks up. ## The route is part of the plan @@ -115,19 +114,18 @@ Errors name the hop and the stage, so a failure points at one thing to fix: | Error begins | What failed | |---|---| -| `jump ssh …:` (no stage) | The bastion could not be resolved, connected to, or finished a handshake with — including a timeout | +| `jump ssh …:` (no stage) | The bastion could not be resolved, connected to, or finished a handshake with, including a timeout | | `jump ssh …: host key:` | The bastion's key is missing from, or disagrees with, `known_hosts` | | `jump ssh …: authenticate:` | The bastion refused your key | -| `target ssh …: not reachable from the jump host:` | The bastion connected but could not reach the server — wrong private address, or the bastion's own policy forbids the forward | +| `target ssh …: not reachable from the jump host:` | The bastion connected but could not reach the server: wrong private address, or the bastion's own policy forbids the forward | | `target ssh …: host key:` | The **server's** key is missing or mismatched, even though the bastion is trusted | | `target ssh …: authenticate:` | The server refused your key | -A failure that names no stage is a transport problem — a timeout, a cancelled -command, or a peer that hung up — never a rejected key. +A failure that names no stage is a transport problem (a timeout, a cancelled +command, or a peer that hung up) never a rejected key. -The distinction between the last four is the one worth internalising: a working -bastion tells you nothing about the server, and Onebox will not let a trusted -first hop paper over a problem with the second. +The last four stages are per hop: a working bastion says nothing about the +server. Onebox checks each hop independently. ## Local hooks diff --git a/site/src/content/docs/docs/guides/migrations.mdx b/site/src/content/docs/docs/guides/migrations.mdx index d15c2b2d..6e3b29c4 100644 --- a/site/src/content/docs/docs/guides/migrations.mdx +++ b/site/src/content/docs/docs/guides/migrations.mdx @@ -117,9 +117,9 @@ ob deploy \ ### The backup report The report template is projected directly from the plan, with protected -resources already filled in and deliberately invalid `REPLACE-...` values. It +resources already filled in and invalid `REPLACE-...` values. It uses `onebox.run/backup-report/v1alpha1` and records artifact, integrity, -restore-test, and key-usability observations — never backup bytes or secrets. +restore-test, and key-usability observations: never backup bytes or secrets. | Field | What it holds | | --- | --- | @@ -134,7 +134,7 @@ restore-test, and key-usability observations — never backup bytes or secrets. | `resources[].restore_test` | `state` is `passed` or `not_tested`. `passed` **requires** `method`, `tested_at` and a lowercase `sha256:` `validation_digest`; `not_tested` refuses all three | | `key_material[]` | one per entry in the plan's `required_key_material`: `name`, `backup_id`, `created_at`, `integrity`, and `usability` (`method`, `validated_at`, `validation_digest`) | -Unknown fields are refused, so a typo is named rather than ignored. +Unknown fields are refused, so a typo is reported. ## When you have to go anyway @@ -144,14 +144,13 @@ ob deploy --plan ob-plan.json --approval ob-approval.json \ ``` An audited override requires the exact plan's strong or break-glass local -confirmation. It is recorded, not silent. +confirmation. The override is recorded. ## Approval ceremony Migrations and unknown data effects use the **strong ceremony**, where the operator types back what the plan acts on: the release ID for a deploy, the job -name for a job run. That is the difference between confirming a routine deploy -and confirming one that may not be reversible. A migration job asks whether or +name for a job run. A migration job asks whether or not approval policy is enabled. ## Release-wide policy @@ -162,7 +161,7 @@ deployment: ``` `expand-only` is your promise, not a check Onebox performs. It declares that -every migration in this project is additive — that a release running the old +every migration in this project is additive: that a release running the old code against the new schema still works. Onebox never inspects your SQL; it takes the declaration and, on the strength of it, keeps the rollback gate **open** after a `dataEffect: Migration` job runs, so a failed verify can roll diff --git a/site/src/content/docs/docs/guides/rollback.mdx b/site/src/content/docs/docs/guides/rollback.mdx index bc681b72..1dee160b 100644 --- a/site/src/content/docs/docs/guides/rollback.mdx +++ b/site/src/content/docs/docs/guides/rollback.mdx @@ -1,6 +1,6 @@ --- title: Roll back a release -description: Rollback, resume and abort — three different operations with three different guarantees. +description: "Rollback, resume and abort: three different operations with three different guarantees." summary: When to use rollback, resume and abort, what each guarantees, and why a data effect can make a rollback refuse. sidebar: order: 5 @@ -10,8 +10,8 @@ read_when: - "Understanding why a rollback was refused" --- -Three commands, three different situations. Choosing the wrong one is the usual -way an incident gets worse. +Rollback, resume and abort handle different situations; choosing the wrong one +is the usual way an incident gets worse. | Situation | Command | What it does | | --- | --- | --- | @@ -49,12 +49,8 @@ strategy. Abort uses the same fail-closed rule. ## What rollback does not do :::danger[Application rollback is not data recovery] -Application rollback, data recovery, and reversal of an external side effect are -different operations with different guarantees. Onebox classifies the risk and -**refuses** when evidence or a driver contract is insufficient. - -No flag turns an unsupported operation into a safe one. The override flags below -do not widen what Onebox can do — each one records that you have taken +See the [bounded safety guarantees](/docs/reference/policies#safety-claims-are-bounded). The override flags below +do not widen what Onebox can do; each one records that you have taken responsibility for one specific judgement it refused to make for you. ::: @@ -65,9 +61,8 @@ Concretely: removes its volume. That protects your data; it also means a schema migration is still applied after you roll the application back. - **A job with `dataEffect: Migration` or `destructive` constrains the gate.** - That is what the field is for. - **A job whose result evidence is missing becomes `changed=unknown`** and halts - before workload replacement rather than proceeding on an assumption. + before workload replacement. ## Resume and abort work from durable state @@ -84,8 +79,8 @@ ob abort --output ndjson Activation splits the two halves of a deploy. Interrupted before it, the release never took effect and `ob resume` replays the phases that remain. Interrupted -after it — retention, schedule sync or the `post_deploy` hook failed while the -new release was already current and healthy — there is nothing to replay: +after it (retention, schedule sync or the `post_deploy` hook failed while the +new release was already current and healthy) there is nothing to replay: `ob resume` completes only those remaining steps, skipping any the journal already records as done, and the release keeps serving throughout. The manifest carries a failed operation outcome in the meantime, so a healthy release and an @@ -98,8 +93,8 @@ labels all agree. One disagreement returns `finalize_refused`: no step runs and no release, symlink or workload changes. Only the newest deploy is recoverable. Once a later deploy reaches a terminal -state of its own — finished, aborted, or automatically rolled back — the older -interrupted one is history rather than pending work, and resuming it would +state of its own (finished, aborted, or automatically rolled back) the older +interrupted one is no longer pending work, and resuming it would re-activate a release the host has already moved past. `ob status` stops reporting it; `ob audit` still shows the run. @@ -111,7 +106,7 @@ is refused on its superseded manifest before any workload is touched. Two refusals have an override, and each grants exactly one thing. -**`migration_gate_closed`** — the interrupted release ran a job or hook whose +**`migration_gate_closed`**: the interrupted release ran a job or hook whose data effects are rollback-unknown, so `ob abort` will not roll the application back on its own. Fix forward and `ob resume`, or, if you know the old code reads the current data correctly, take responsibility explicitly: @@ -120,7 +115,7 @@ the current data correctly, take responsibility explicitly: ob abort --break-migration-gate --output ndjson ``` -**A stale operation lock** — a previous runner died holding it. Inspect the +**A stale operation lock**: a previous runner died holding it. Inspect the holder in `ob status` first; the lock records who took it and when. If it is genuinely abandoned: @@ -129,12 +124,12 @@ ob abort --break-lock --output ndjson ``` `--break-lock` is on six commands: `deploy`, `bootstrap`, `abort`, `job run`, -`service apply` and `proxy apply`. Not every mutating command has it — `resume`, +`service apply` and `proxy apply`. Not every mutating command has it: `resume`, `destroy` and `secrets push` do not. It breaks the application and host locks only. The backup lock has no override and clears on TTL expiry or when the same operation returns. -**An unreadable or invalid fencing epoch** — there is deliberately no override. +**An unreadable or invalid fencing epoch**: there is no override. The application authority is `//epoch`; each backup service has a second authority at `//backup/locks/.epoch`. Do not delete either file or replace it with `0` or `1`: that can recreate a fence still held @@ -151,7 +146,7 @@ guarantee the epoch exists to provide. Rolling deployment can avoid interruption while the host is healthy. It cannot make a failed host available. Recovery onto another host is a distinct, -evidence-backed workflow — not failover, and not something rollback reaches. +evidence-backed workflow: not failover, and not something rollback reaches. ## Inspecting what happened diff --git a/site/src/content/docs/docs/guides/secrets.mdx b/site/src/content/docs/docs/guides/secrets.mdx index 56c45721..68fe5da7 100644 --- a/site/src/content/docs/docs/guides/secrets.mdx +++ b/site/src/content/docs/docs/guides/secrets.mdx @@ -23,10 +23,9 @@ runtime: Whether an entry is encrypted is a property of the **entry**, not of the workload reading it. The only provider is `sops`. -## Rule 1 — which list does a workload resolve? +## Rule 1: which list does a workload resolve? -Exactly one, from the most specific declaration present. Lists **replace** rather -than extend. +Exactly one, from the most specific declaration present. Lists **replace**; they do not extend. | | Scope | Declared at | | --- | --- | --- | @@ -55,18 +54,18 @@ environments: `envFiles: []` means the workload receives nothing, which is different from declaring no list at all. `ob canonical` shows which you wrote, at every scope. -## Rule 2 — what wins inside the container? +## Rule 2: what wins inside the container? Lowest precedence first: 1. the referenced Compose service's own `env_file`, for a `compose:` workload 2. the resolved `envFiles` entries, in order 3. managed-service connection files -4. the service's `environment` — your inline `env`, or the referenced service's +4. the service's `environment`: your inline `env`, or the referenced service's Level 4 outranks the rest because the container runtime places `environment` above `env_file`, and a generated runtime cannot contradict the runtime that -reads it. Shadowing an entry that way is legitimate — it is your most specific +reads it. Shadowing an entry that way is legitimate; it is your most specific statement about that container. :::caution[Except a connection variable] @@ -77,8 +76,7 @@ name. ## Who receives the default list -The project's or environment's list reaches the application's own workloads — -`application`, `worker`, `job` — and **not** a `daemon`. +The project's or environment's list reaches the application's own workloads: `application`, `worker`, `job`: and **not** a `daemon`. Application configuration should not land in infrastructure by default: a database does not want your Stripe key, it wants `POSTGRES_PASSWORD`, which @@ -87,11 +85,12 @@ exactly it. The common upstream layout shares one file across every service, so converting something like authentik or Immich means naming it on the database too. That -costs a line; the opposite default costs a credential in a place nobody looks. +costs a line; the opposite default would put a credential in a container that +does not use it. ## Encrypted entries -The plaintext may be an environment file or a flat YAML map — both render to the +The plaintext may be an environment file or a flat YAML map: both render to the same thing. A nested map, or one declaring no values at all, is refused. ``` @@ -101,7 +100,7 @@ API_TOKEN=value # or: API_TOKEN: value Values are read literally: a password containing `$` is not expanded. Each encrypted entry is decrypted into its own file inside the release when the -release is staged, and stays there. That is deliberate — a scheduled job fires +release is staged, and stays there. A scheduled job fires from the host's timer with no Onebox process alive, including after a reboot, and must resolve the values the deploy resolved. @@ -121,7 +120,7 @@ difference is `secret_declaration_not_deployed`. Add, remove, reorder, or move a declaration with `ob deploy`; secret push changes values only. It also refuses when the current release carries no opaque secret generation at -all — `secret_generation_not_deployed`, which is what a release staged before +all (`secret_generation_not_deployed`), which is what a release staged before generations existed looks like. Both refusals resolve the same way: run `ob deploy` first. @@ -143,15 +142,13 @@ SOPS exit status 200 means the editor made no change. In JSON mode ## What never leaves your machine No resolved value from any entry appears in the project file, generated -runtime metadata, plan, or Onebox-generated structured fields — plaintext -included. Container logs and `ob exec` output remain operator-controlled +runtime metadata, plan, or Onebox-generated structured fields, including plaintext. Container logs and `ob exec` output remain operator-controlled passthrough and can reveal whatever the process prints. -> Plaintext is not less sensitive than encrypted, only less protected. +Plaintext entries are as sensitive as encrypted ones, only less protected. -Two things Onebox deliberately does not model: the `*_FILE` convention some -images use to read a secret from a path, and values a local build hook reads — -those are not container environment. +Onebox does not model the `*_FILE` convention some +images use to read a secret from a path, and values a local build hook reads; those are not container environment. ## Assert before you deploy diff --git a/site/src/content/docs/docs/guides/upgrade-to-onebox-names.mdx b/site/src/content/docs/docs/guides/upgrade-to-onebox-names.mdx index 11f4a946..848cb3b3 100644 --- a/site/src/content/docs/docs/guides/upgrade-to-onebox-names.mdx +++ b/site/src/content/docs/docs/guides/upgrade-to-onebox-names.mdx @@ -16,52 +16,6 @@ Onebox does not look for the old names, labels, paths or units, read from them, or remove them. Prepare the host with the steps below **before** the first command with the new version. -## What changes - -| Resource | Before | After | -| --- | --- | --- | -| State directory | `/var/lib/ob/shop` | `/var/lib/onebox/app` | -| Host state | `/var/lib/ob/_host` | `/var/lib/onebox/_host` | -| Owner record | `shop` or `shop production` | `shop production` only | -| Ownership labels | `ob.app`, `ob.workload`, `ob.service`, … | `onebox.app`, `onebox.workload`, `onebox.service`, … | -| Managed service container | `shop-postgres-1` | `onebox-postgres` | -| Restore-drill container | `shop-postgres-restore-1` | `onebox-postgres-restore` | -| Volumes | `ob_shop_postgres_data` | `onebox_postgres_data` | -| Service Compose project | `ob_shop_postgres` | `onebox_postgres` | -| Service network | `ob_shop` | `onebox_services` | -| Ingress network | `ob-ingress` | `onebox-ingress` | -| Scheduled job units | `ob-shop-nightly` | `onebox-job-nightly` | -| Backup units | `ob-backup-shop-production-postgres-backup` | `onebox-backup-postgres-backup` | -| Proxy routers | `shop_web_r0` | `onebox_web_r0` | -| Job run journal identifier | `ob-run` | `onebox-run` | -| Files Onebox keeps beside its own | `.ob-schedule.lease`, `.ob-secret-generations/`, `.ob-tmp` | `.onebox-schedule.lease`, `.onebox-secret-generations/`, `.onebox-tmp` | -| Drain marker in containers | `/tmp/ob-drain` | `/tmp/onebox-drain` | -| Release snapshot | `ob.snapshot.yml` | `onebox.snapshot.yml` | -| Workload containers | `shop-web-1` | unchanged, but labelled `onebox.app` | -| Application network | `shop_default` | unchanged, but labelled `onebox.app` | - -Application names may no longer be `onebox` or begin with `onebox-`. Service -names may not be `proxy`, `discovery`, `ingress` or `services`. Custom Traefik objects may -not begin with `onebox_`. The default `basePath` is now `/var/lib/onebox`; if -you set your own, the application's state moves to `/app`. Host state -is always `/var/lib/onebox/_host` now, whatever `basePath` says. - -## What happens without these steps - -- **Every command reports an unowned host.** The owner record is read from - `/var/lib/onebox/_host/owner`, which does not exist yet. `ob bootstrap` would - claim the host again from scratch. -- **Deploys are refused.** `shop-web-1` and `shop_default` still carry the old - `ob.app` label, so preflight reports them as held by a resource Onebox does - not own. Docker cannot relabel a container or network. -- **Unprotected services start empty** if the deploy gets that far. A service - without `backup` starts on a new, empty `onebox_` volume. -- **Protected PostgreSQL refuses to apply**, because its recorded data volume is - missing. -- **Old timers keep firing.** The `ob-shop-*` and `ob-backup-shop-*` units stay - enabled and run jobs and backups against the old state directory, next to the - new units. - ## Move the host Run the host steps as root. Set `APP` to your application name and `ENV` to the @@ -165,3 +119,49 @@ but not for `_host`: that moves from `/_host` to Releases deployed before the upgrade stay in `releases/`, but their Compose files name the old volumes and paths. Do not roll back to them; roll back only to releases deployed after step 6. + +## What changes + +| Resource | Before | After | +| --- | --- | --- | +| State directory | `/var/lib/ob/shop` | `/var/lib/onebox/app` | +| Host state | `/var/lib/ob/_host` | `/var/lib/onebox/_host` | +| Owner record | `shop` or `shop production` | `shop production` only | +| Ownership labels | `ob.app`, `ob.workload`, `ob.service`, … | `onebox.app`, `onebox.workload`, `onebox.service`, … | +| Managed service container | `shop-postgres-1` | `onebox-postgres` | +| Restore-drill container | `shop-postgres-restore-1` | `onebox-postgres-restore` | +| Volumes | `ob_shop_postgres_data` | `onebox_postgres_data` | +| Service Compose project | `ob_shop_postgres` | `onebox_postgres` | +| Service network | `ob_shop` | `onebox_services` | +| Ingress network | `ob-ingress` | `onebox-ingress` | +| Scheduled job units | `ob-shop-nightly` | `onebox-job-nightly` | +| Backup units | `ob-backup-shop-production-postgres-backup` | `onebox-backup-postgres-backup` | +| Proxy routers | `shop_web_r0` | `onebox_web_r0` | +| Job run journal identifier | `ob-run` | `onebox-run` | +| Files Onebox keeps beside its own | `.ob-schedule.lease`, `.ob-secret-generations/`, `.ob-tmp` | `.onebox-schedule.lease`, `.onebox-secret-generations/`, `.onebox-tmp` | +| Drain marker in containers | `/tmp/ob-drain` | `/tmp/onebox-drain` | +| Release snapshot | `ob.snapshot.yml` | `onebox.snapshot.yml` | +| Workload containers | `shop-web-1` | unchanged, but labelled `onebox.app` | +| Application network | `shop_default` | unchanged, but labelled `onebox.app` | + +Application names may no longer be `onebox` or begin with `onebox-`. Service +names may not be `proxy`, `discovery`, `ingress` or `services`. Custom Traefik objects may +not begin with `onebox_`. The default `basePath` is now `/var/lib/onebox`; if +you set your own, the application's state moves to `/app`. Host state +is always `/var/lib/onebox/_host` now, whatever `basePath` says. + +## What happens without these steps + +- **Every command reports an unowned host.** The owner record is read from + `/var/lib/onebox/_host/owner`, which does not exist yet. `ob bootstrap` would + claim the host again from scratch. +- **Deploys are refused.** `shop-web-1` and `shop_default` still carry the old + `ob.app` label, so preflight reports them as held by a resource Onebox does + not own. Docker cannot relabel a container or network. +- **Unprotected services start empty** if the deploy gets that far. A service + without `backup` starts on a new, empty `onebox_` volume. +- **Protected PostgreSQL refuses to apply**, because its recorded data volume is + missing. +- **Old timers keep firing.** The `ob-shop-*` and `ob-backup-shop-*` units stay + enabled and run jobs and backups against the old state directory, next to the + new units. diff --git a/site/src/content/docs/docs/reference/capabilities.mdx b/site/src/content/docs/docs/reference/capabilities.mdx index 4560988c..5f9d84ac 100644 --- a/site/src/content/docs/docs/reference/capabilities.mdx +++ b/site/src/content/docs/docs/reference/capabilities.mdx @@ -1,7 +1,7 @@ --- title: Shipped vs proposed description: What the current binary does, what the schema accepts but cannot execute, and what is only declared intent. -summary: The honest inventory — every capability marked shipped, schema-only, or intent-only, with what each means for someone relying on it. +summary: "The capability inventory: every capability marked shipped, schema-only, or intent-only, with what each means for someone relying on it." sidebar: order: 1 read_when: @@ -18,7 +18,7 @@ This page is the reconciliation. Three states: | State | Means | | --- | --- | | **Shipped** | The current binary does this. | -| **Schema only** | The loader validates it and it is in the published JSON Schema, so your editor completes it — but the behaviour is an open proposal. Declaring it changes nothing on the server. | +| **Schema only** | The loader validates it and it is in the published JSON Schema, so your editor completes it, but the behaviour is an open proposal. Declaring it changes nothing on the server. | | **Intent only** | Validated and carried into plans, but the local engine runs nothing continuous for it. | ## Shipped @@ -102,12 +102,11 @@ directions. ## Not owned at all **Workload volumes.** A workload's own durable data is not copied anywhere. -`ob doctor` reports it for every workload holding some, because silence there -would read as approval. Managed services are different: one declaring -`backup` is backed up continuously and recoverable to a point in time — see +`ob doctor` reports it for every workload holding some, so unprotected data is visible. Managed services are different: one declaring +`backup` is backed up continuously and recoverable to a point in time; see the backup reference. Only the `postgres` driver has an executable contract today; every other driver declares `policy_qualified: false` and its backup -policy is refused rather than accepted and ignored. +policy is refused. For PostgreSQL, the digest-pinned Onebox image carries the compatible WAL-G executable. The host receives generated configuration only; no backup binary is @@ -115,7 +114,7 @@ installed by Onebox. **Restore proof.** `ob backup drill` proves it on demand: it recovers the repository into a throwaway volume, waits for the cluster to promote, and makes -it answer a query — the same code a real restore runs, stopped before the last +it answer a query: the same code a real restore runs, stopped before the last step. What is *not* run is an unattended drill. Onebox puts no agent on the host, so the timer from `drill.schedule` runs the archive-continuity check instead, and the full proof is yours to run from CI on the cadence your policy @@ -130,10 +129,9 @@ a distinct, evidence-backed workflow, not failover. ## Why the distinction is published -> A product direction that reads as a capability list is how an operator ends up -> believing their database is backed up by something that has never taken a -> backup. +A product direction presented as a capability list can lead an operator to +believe their database is backed up before any backup has been taken. A capability ships only when its design is agreed, its tests pass, and the generated reference and this page are updated from proposed to implemented. -Until then it appears here, in this column, and not in the other one. +Until then it remains listed as proposed. diff --git a/site/src/content/docs/docs/reference/cli.mdx b/site/src/content/docs/docs/reference/cli.mdx index 68eb36d0..8c9433bf 100644 --- a/site/src/content/docs/docs/reference/cli.mdx +++ b/site/src/content/docs/docs/reference/cli.mdx @@ -18,7 +18,7 @@ Generated from the binary, so it cannot describe a flag the CLI does not have. ## Which command changes nothing -Many commands change nothing — `status`, `logs`, `audit`, `schema`, `version` and `canonical` all +Many commands change nothing: `status`, `logs`, `audit`, `schema`, `version` and `canonical` all just report. These five are the ones that get confused with each other, because each one answers "would this deploy work?" from a different place. diff --git a/site/src/content/docs/docs/reference/drivers.mdx b/site/src/content/docs/docs/reference/drivers.mdx index a32b708d..0d221c8e 100644 --- a/site/src/content/docs/docs/reference/drivers.mdx +++ b/site/src/content/docs/docs/reference/drivers.mdx @@ -18,13 +18,9 @@ The driver set is **closed**. A declaration naming a driver outside it is refuse with `unknown_service_driver`, listing the drivers Onebox can run and directing you to a `daemon` workload. -That is deliberate: inventing an image from a name produces a container that +Inventing an image from a name produces a container that starts and stores nothing durable. -The runtime and connection tables are generated from the private driver catalogue. -The guidance and limitations below remain deliberately authored in the generator: -they are product explanations, not facts the catalogue can prove. - ## Runtime catalogue | Driver | Image repository | Port | Data path | URL scheme | Health check | @@ -57,7 +53,7 @@ they are product explanations, not facts the catalogue can prove. | `redis` | Cache, queue | `url` `host` `port` `user` `password` | | `valkey` | Cache, queue | `url` `host` `port` `user` `password` | -A part the driver does not have — a database on a cache — is omitted rather than +A part the driver does not have (a database on a cache) is omitted, never written empty. Every driver has a password part; `redis` and `valkey` have a user part because Redis 6+ authenticates the built-in `default` user, and a URL with an empty username fails AUTH outright. @@ -86,8 +82,8 @@ reads. - **The service outlives every release.** Its own Compose project; no deploy and no rollback stops it or removes its volume. -- **The credential is generated on the server, once.** Not in your project, the - generated runtime, or the digest. Never rotated by a re-apply. +- **The credential is generated on the server, once.** It is absent from your project, + the generated runtime, and the digest. A re-apply never rotates it. - **The version binds into the release digest**, so a database upgrade under an untouched application cannot pass unnoticed. @@ -109,27 +105,26 @@ than silently ignored. :::caution[`mongodb` runs a standalone server, not a replica set] Change streams and multi-document transactions need a replica set. An application -using either will connect, authenticate, and *then* fail — Rocket.Chat reports +using either will connect, authenticate, and then fail. Rocket.Chat reports `The $changeStream stage is only supported on replica sets`. Onebox does not configure a replica set, so an application that needs one wants a -`daemon` workload it owns. This is a limitation of the driver rather than of the -contract, and it is stated here because the failure appears in the application's -logs rather than in anything Onebox says. +`daemon` workload it owns. The failure appears in the application's logs, +not in Onebox output. ::: :::caution[Only `postgres` is backed up] `postgres` is the one driver with an executable backup contract: declaring `backup` on it gives a continuous off-host archive and point-in-time recovery. Every other driver declares `policy_qualified: false`, so a backup policy on it -is refused at `ob validate` rather than accepted and left to do nothing — and its +is refused at `ob validate`; its data lives only on this host until you copy it off yourself. Onebox also refuses a major version change a driver cannot perform in place, rather than replacing the container and leaving the data intact and unreachable. -For `postgres`, declaring `backup` changes this — see -[Shipped vs proposed](/docs/reference/capabilities). +For `postgres`, a `backup` block changes this once `ob backup enable` succeeds; +see [Declaring is not enabling](/docs/guides/backups#declaring-is-not-enabling). ::: ## Anything else diff --git a/site/src/content/docs/docs/reference/errors.mdx b/site/src/content/docs/docs/reference/errors.mdx index 2382e082..5748fa54 100644 --- a/site/src/content/docs/docs/reference/errors.mdx +++ b/site/src/content/docs/docs/reference/errors.mdx @@ -13,12 +13,12 @@ read_when: {/* Generated by cmd/ob-docgen. Do not edit by hand. */} -A code is a promise: it names a kind of failure that keeps meaning the same thing +A code names a kind of failure that keeps meaning the same thing across releases. Branch on the code, never on the sentence. ## Validation codes -Adding one is a deliberate act. A test refuses any code the loader emits that is +A test refuses any code the loader emits that is not enumerated, and any enumerated code that nothing emits, so this table cannot drift in either direction. @@ -102,14 +102,14 @@ command. ## Operation codes -Raised while a command runs, rather than while the project file is read. These +Raised during command execution, not project-file loading. These are the codes a deploy, a rollback, a secret rotation or an audited exec puts in the `error.code` field, so they are the set to branch on when automating. -A blank command is deliberate: some failures have no honest Onebox command that -resolves them. A local artifact that could not be written is fixed on the +A blank command means no Onebox command resolves the failure. A local artifact +that could not be written is fixed on the filesystem, not by running the tool again. An angle-bracketed command names a -step to complete rather than a line to run verbatim. +step to complete, not a line to run verbatim. | Code | Means | Guidance role | Command | | --- | --- | --- | --- | diff --git a/site/src/content/docs/docs/reference/fields/backup-targets.mdx b/site/src/content/docs/docs/reference/fields/backup-targets.mdx index f85a4a6a..d503e8db 100644 --- a/site/src/content/docs/docs/reference/fields/backup-targets.mdx +++ b/site/src/content/docs/docs/reference/fields/backup-targets.mdx @@ -1,7 +1,7 @@ --- title: "backupTargets" -description: "User-owned off-host S3-compatible repositories a protected service writes its backups to. Executable for the postgres driver; every other driver refuses a policy rather than accepting one it cannot honour." -summary: "User-owned off-host S3-compatible repositories a protected service writes its backups to. Executable for the postgres driver; every other driver refuses a policy rather than accepting one it cannot honour." +description: "User-owned off-host S3-compatible repositories a protected service writes its backups to. Executable for the postgres driver; every other driver refuses a policy it cannot honour." +summary: "User-owned off-host S3-compatible repositories a protected service writes its backups to. Executable for the postgres driver; every other driver refuses a policy it cannot honour." status: shipped generated: true sidebar: diff --git a/site/src/content/docs/docs/reference/fields/hooks.mdx b/site/src/content/docs/docs/reference/fields/hooks.mdx index 480dcff7..a1671f2d 100644 --- a/site/src/content/docs/docs/reference/fields/hooks.mdx +++ b/site/src/content/docs/docs/reference/fields/hooks.mdx @@ -1,7 +1,7 @@ --- title: "hooks" -description: "Commands run at a lifecycle seam — bootstrap, pre_release, post_release, post_deploy — or keyed by a job name to replace that job's command." -summary: "Commands run at a lifecycle seam — bootstrap, pre_release, post_release, post_deploy — or keyed by a job name to replace that job's command." +description: "Commands run at a lifecycle seam (bootstrap, pre_release, post_release, post_deploy) or keyed by a job name to replace that job's command." +summary: "Commands run at a lifecycle seam (bootstrap, pre_release, post_release, post_deploy) or keyed by a job name to replace that job's command." status: shipped generated: true sidebar: diff --git a/site/src/content/docs/docs/reference/fields/services.mdx b/site/src/content/docs/docs/reference/fields/services.mdx index 4e840ed2..f54adffb 100644 --- a/site/src/content/docs/docs/reference/fields/services.mdx +++ b/site/src/content/docs/docs/reference/fields/services.mdx @@ -1,7 +1,7 @@ --- title: "services" -description: "Supporting services Onebox runs for you — the eleven built-in drivers, their versions, settings and persistence." -summary: "Supporting services Onebox runs for you — the eleven built-in drivers, their versions, settings and persistence." +description: "Supporting services Onebox runs for you: the eleven built-in drivers, their versions, settings and persistence." +summary: "Supporting services Onebox runs for you: the eleven built-in drivers, their versions, settings and persistence." status: shipped generated: true sidebar: diff --git a/site/src/content/docs/docs/reference/naming.mdx b/site/src/content/docs/docs/reference/naming.mdx index 9e02c116..adb25eb3 100644 --- a/site/src/content/docs/docs/reference/naming.mdx +++ b/site/src/content/docs/docs/reference/naming.mdx @@ -1,7 +1,7 @@ --- title: Naming conventions description: The rules every command, flag, field and error code in Onebox follows. -summary: How Onebox names things — casing, number, mood, and which words are reserved. Written down so a new field or flag has an answer before the review. +summary: "How Onebox names things: casing, number, mood, and which words are reserved. Written down so a new field or flag has an answer before the review." status: shipped sidebar: order: 210 @@ -12,19 +12,19 @@ read_when: These describe what the surface already does, so a new field or flag has an answer before anyone has to review it. Where a rule has known exceptions they -are named inline — a rule with a hidden exception is worse than no rule. +are named inline. ## The project file **Casing.** Every field is `snake_case`. Abbreviations are admitted only where the short form is the name people use: protocol and format tokens (`tls`, `http`, `tcp`, `udp`, `url`), `env`, `cron`, `args`, `config`, and the recovery -term `pitr`. Invented shortenings are not — no `cfg`, no `dir`, no `max`. +term `pitr`. Invented shortenings are not: no `cfg`, no `dir`, no `max`. **Number follows shape.** A field holding a map of things is plural; a field holding one object is singular. The exceptions are mass nouns, where the plural -names a single settings object rather than a collection — `resources`, -`credentials`, `overrides`, `logs`, `metrics`, `alerts` — and `env`, which is a +names a single settings object rather than a collection (`resources`, +`credentials`, `overrides`, `logs`, `metrics`, `alerts`) and `env`, which is a map named for the thing it configures. | Plural | Singular | @@ -33,7 +33,7 @@ map named for the thing it configures. Arrays are plural too: `routes`, `envFiles`, `volumes`, `publishedPorts`, `needs`, `checks`, `env_checks`. Four are not, because the singular reads -as the thing being stated rather than a list: `order`, `on`, `require`, +as the thing being stated: `order`, `on`, `require`, `present`. **Booleans announce their direction.** A boolean that grants permission is @@ -47,8 +47,7 @@ require_migration_backup: true # demands managed: false # state ``` -**One grammar for time.** Every duration is a string with the same syntax — -`30s`, `5m`, `1h30m`, `14d`. No field carries its unit in its name, and no +**One grammar for time.** Every duration is a string with the same syntax: `30s`, `5m`, `1h30m`, `14d`. No field carries its unit in its name, and no duration is an integer. ## Runtime resources @@ -87,7 +86,7 @@ There is no ordinal, because none of them has replicas. Managed services are `onebox-postgres-restore`; the host proxy is `onebox-proxy` and its discovery controller `onebox-discovery`. The application is not in these names: a host runs one application, and the `onebox.app` label records which. So `docker ps` -reads as two groups — your images under your application's name, and what +reads as two groups: your images under your application's name, and what Onebox provides under `onebox-`. The same namespace covers the host's other shared resources: the ingress network @@ -134,7 +133,7 @@ service Compose projects, the service network and restore-drill resources: `onebox.app` label records which, and `ob destroy` releases the host only after removing them. They join with underscores because a component or volume name cannot contain one, so each name maps back to exactly one component and -volume — a hyphen, which names may contain, could not promise that. The one +volume: a hyphen, which names may contain, could not promise that. The one exception is the application's own Compose network, `shop_default`, which is the name Compose gives it. @@ -146,11 +145,11 @@ the name Compose gives it. ## Commands -**A command is an imperative verb, or a noun that prints.** Nothing in between. -The split is grammatical, not about mutation — `preview` and `validate` are +**A command is an imperative verb, or a noun that prints.** +The split is grammatical, not about mutation; `preview` and `validate` are imperative verbs that change nothing. -| Verbs — you tell it to act | Nouns — you ask it for a fact | +| Verbs: you tell it to act | Nouns: you ask it for a fact | | --- | --- | | `deploy` `rollback` `abort` `resume` `approve` `bootstrap` `destroy` `eject` `exec` `init` `plan` `preview` `validate` | `status` `logs` `audit` `doctor` `schema` `version` `preflight` `canonical` | @@ -159,7 +158,7 @@ Groups that hold subcommands are nouns: `job`, `proxy`, `secrets`, and (`ob service apply` converges the `services:` block) and plural only when its subcommands genuinely act on the whole set at once (`ob secrets push`). -**An artifact keeps one word across all three of its surfaces** — the command +**An artifact keeps one word across all three of its surfaces**: the command that produces it, the flag that consumes it, and its filename. | Producer | Consumer flag | Default filename | @@ -167,11 +166,11 @@ that produces it, the flag that consumes it, and its filename. | `ob plan` | `--plan` | `ob-plan.json` | | `ob approve` | `--approval` | `ob-approval.json` | | `ob job plan` | `ob job run --plan` | `ob-job-plan.json` | -| `ob plan --backup-report-out` | `--backup-report` | none — the path is required | +| `ob plan --backup-report-out` | `--backup-report` | none; the path is required | The first three have a default path, so `--out` is optional. `--backup-report-out` is the exception: it has no default, and no report -template is written unless you name a path — and then only when the plan +template is written unless you name a path, and then only when the plan actually requires one. `ob-backup-report.json` is a convention the guidance and the error messages suggest, not a default the binary supplies. `ob job plan` takes the same `--backup-report-out`, on the same terms. @@ -179,7 +178,7 @@ takes the same `--backup-report-out`, on the same terms. **The bypass gets harder as the blast radius grows.** `ob deploy` takes `-y` to skip its prompt. `ob destroy` has no flag that skips its confirmation: the application name must be typed back. (It reads that from stdin, so it can still -be piped — the point is that no flag turns it off.) +be piped; no flag turns it off.) ## Flags @@ -188,7 +187,7 @@ prints. Three forms, three behaviours: | Form | Means | Example | | --- | --- | --- | -| bare noun | **also** do this — additive, and the only additive flags are destructive | `--volumes` `--proxy` | +| bare noun | **also** do this: additive, and the only additive flags are destructive | `--volumes` `--proxy` | | `-only` suffix | print this **instead** of the default output | `--digest-only` `--origins-only` | | adjective or `no-` prefix | modify how the command runs | `--raw` `--overwrite` `--redeploy` `--no-rollback` | @@ -208,8 +207,8 @@ output is the global `--output`, never a per-command `--json`. `_`, `snake_case`, so codes for one subsystem sort together. The lifecycle contract follows this. The loader's own codes largely predate it -and several are condition-first — `unknown_field`, `no_environment`, -`unknown_service_driver` — so the environment codes do not sort together today. +and several are condition-first (`unknown_field`, `no_environment`, +`unknown_service_driver`) so the environment codes do not sort together today. New codes should be subject-first. ``` @@ -227,7 +226,7 @@ are pinned: | Word | Means | Never means | | --- | --- | --- | | **server** | the machine you deploy to, in the project file and in every artifact | a backup repository | -| **target** | a backup destination — `backupTargets`, `backup.target` | the machine you deploy to | +| **target** | a backup destination: `backupTargets`, `backup.target` | the machine you deploy to | | **workload** | a container Onebox runs from your declaration | a supporting service, a Compose service | `ob exec` and `ob logs` take a `` because those are two diff --git a/site/src/content/docs/docs/reference/policies.mdx b/site/src/content/docs/docs/reference/policies.mdx index fd5a4c1d..a857bfca 100644 --- a/site/src/content/docs/docs/reference/policies.mdx +++ b/site/src/content/docs/docs/reference/policies.mdx @@ -34,7 +34,7 @@ environments: `ob doctor` reports whether the runner selected by `PATH` is compatible. :::caution[Commit-derived and dirty builds fail closed] -When a minimum version is configured, they are refused — they are not released +When a minimum version is configured, they are refused because they are not released runners. An environment that pins a minimum is asking for a runner whose identity can be checked, and a dirty working tree has none. ::: @@ -63,10 +63,10 @@ changed or expired plan or report needs a new confirmation. Migrations, destructive effects and unknown data effects use the **strong ceremony**, where the operator types back what the plan acts on: the release ID for a deploy, the job name for a job run. A deploy mints the release it names, -while a job runs inside the release already serving — which every job planned -against that release shares — so the job is what identifies it. +while a job runs inside the release already serving (which every job planned +against that release shares) so the job is what identifies it. -A operator job carrying one of those effects asks whether or not approval policy +An operator job carrying one of those effects asks whether or not approval policy is enabled. Policy decides only whether a deploy that is not critical asks at all. @@ -124,8 +124,7 @@ stores command bytes or passthrough output. ### Exit codes Three codes, and the set is closed. Every failure is `1`: there is no second -failure code, because which failure it was is the error `code` in the envelope -rather than the exit status. +failure code, because which failure it was is the error `code` in the envelope. The exit status does not distinguish failures. | Code | Means | | --- | --- | @@ -163,7 +162,7 @@ really has it. Adding one without documenting it fails the build, and so does documenting one that does not exist. The identities above are not held to that standard. Only -`onebox.run/cli/v1alpha1` is asserted, and only in one direction — that the page +`onebox.run/cli/v1alpha1` is asserted, and only in one direction: that the page mentions it somewhere. The other five are maintained by hand and could drift without failing the build. ::: @@ -183,7 +182,9 @@ editor tells you while you type is what `ob validate` tells you afterwards. Application rollback, data recovery, and reversal of an external side effect are different operations with different guarantees. Onebox classifies risk and refuses when evidence or a driver contract is insufficient. **A force flag cannot -turn an unsupported operation into a safe one.** +turn an unsupported operation into a safe one.** Override flags permit a specific +attempt; they do not change what the engine can prove. When evidence is +insufficient, Onebox stops and reports the available recovery choices. A customer with root can still bypass Onebox, and a compromised host can lie about its own evidence. Onebox does not claim universal reversibility, high diff --git a/site/src/content/docs/docs/reference/project-file.mdx b/site/src/content/docs/docs/reference/project-file.mdx index 86d1b158..f18d4c29 100644 --- a/site/src/content/docs/docs/reference/project-file.mdx +++ b/site/src/content/docs/docs/reference/project-file.mdx @@ -1,6 +1,6 @@ --- title: Project file -description: The shape of ob.yml — required keys, shorthand forms, and where each block lives. +description: "The shape of ob.yml: required keys, shorthand forms, and where each block lives." summary: Overview of the onebox.run/v1alpha2 Application resource, its envelope, spec blocks, and environment-value precedence rules. sidebar: order: 1 @@ -82,7 +82,6 @@ opaque notes in `metadata.annotations`; annotations do not change the runtime. ## Shorthand A scalar form accepted by a contract version remains accepted for that version. -These are contract, not convenience that changes within a version. | Written as | Means | | --- | --- | @@ -99,8 +98,8 @@ These are contract, not convenience that changes within a version. ## Environment values: two rules **Which list a workload resolves.** Exactly one, from the most specific -declaration present — override, then workload, then environment, then project. -Lists replace rather than extend. +declaration present: override, then workload, then environment, then project. +Lists replace; they do not extend. **What wins inside the container.** Lowest first: a `compose:` workload's own `env_file`; the resolved `envFiles` entries in order; managed-service connection @@ -111,7 +110,7 @@ Full explanation, including why level four outranks the rest: ## What Onebox generates -**Names** — workload containers are uniformly numbered: `shop-web-1` and +**Names**: workload containers are uniformly numbered: `shop-web-1` and `shop-web-2`. Containers Onebox runs from its own images are unnumbered: `onebox-postgres` for a managed service and `onebox-proxy` for the proxy. Persistent and provider names include `onebox_postgres`, `onebox_postgres_data`, `shop_default` (the external application network), @@ -119,9 +118,9 @@ Full explanation, including why level four outranks the rest: once a persistent resource exists its name cannot change without migration. A foreign resource already holding a derived name is refused, not adopted. -**Layout** — `/var/lib/onebox/app/releases/`, plus `current`, `journal`, and -`services`. `basePath` moves this per environment. Host state — the owner -record, host lock and proxy — stays in `/var/lib/onebox/_host` whatever +**Layout** (`/var/lib/onebox/app/releases/`, plus `current`, `journal`, and +`services`. `basePath` moves this per environment. Host state) the owner +record, host lock and proxy: stays in `/var/lib/onebox/_host` whatever `basePath` says, so that one host has one owner. ### Bind-mount lifetimes @@ -150,7 +149,7 @@ An absolute source is external host state. Onebox mounts it but neither creates, backs up nor removes it; provision and protect that path separately. For data Onebox should own, prefer a managed named volume instead. -**The proxy** — if anything is routed, Onebox runs Traefik and writes its static +**The proxy**: if anything is routed, Onebox runs Traefik and writes its static configuration. When Onebox owns that static configuration, HTTPS routes receive managed response compression by default; Traefik skips bodies below its threshold, responses already encoded, and `text/event-stream` so streaming is @@ -193,7 +192,7 @@ In both modes, the filenames `onebox.yml`, `onebox.yaml`, and middleware, and HTTP or TCP router or service names beginning with the derived `_` prefix are reserved for generated output. `ob deploy`, `ob bootstrap`, and `ob proxy apply` refuse an incompatible configuration with migration -guidance rather than silently starting a proxy with missing or stale routes. +guidance, preventing a proxy from starting with missing or stale routes. ### Wildcard host routes @@ -315,7 +314,7 @@ the generated router. HTTP and TCP routes both support middleware references; the referenced middleware must match the route's protocol. :::caution[Compose is an output, not an input] -A Compose file you wrote cannot be the contract — the declaration is, and Onebox +A Compose file you wrote cannot be the contract: the declaration is, and Onebox generates the runtime from it. Individual services can still be adopted with `compose: docker-compose.yml#service`, and Onebox generates around them. See [Why Compose is generated](/docs/concepts/generated-compose) and diff --git a/site/src/content/docs/index.mdx b/site/src/content/docs/index.mdx index 14dbe84e..0fa089a8 100644 --- a/site/src/content/docs/index.mdx +++ b/site/src/content/docs/index.mdx @@ -9,7 +9,7 @@ template: splash # know both halves are the same word. # # Overriding here is the mechanism that works. Emitting a from the Head -# component instead produces two of them, with Starlight's own — the wrong one — +# component instead produces two of them, with Starlight's own: the wrong one — # first; that was measured, not assumed. Starlight dedupes a frontmatter `title` # tag against its default, so this one replaces it rather than joining it. # @@ -37,8 +37,7 @@ hero: title: Deploys you can read before they happen. tagline: | Onebox turns <code>ob.yml</code> into a sealed deployment plan for one Linux - server. Review exactly what will change, approve it, then deploy over SSH — - with health gates, rollback, and no agent on the host. + server. Review exactly what will change, approve it, then deploy over SSH, with health gates, rollback, and no agent on the host. actions: - text: See a real deploy link: /docs/get-started/first-deploy @@ -81,19 +80,19 @@ The plan records what Onebox observed and what it intends to change. The approva <Lifecycle> -1. `ob validate` — **Declare** +1. `ob validate`: **Declare** Read the project file, resolve every shorthand, contact nothing. -2. `ob plan` — **Observe and propose** +2. `ob plan`: **Observe and propose** Inspect the host, compare it to the declaration, and seal the result: the typed operation graph, exact config, host state, image digests and rendered Compose. -3. `ob approve` — **Confirm, locally** +3. `ob approve`: **Confirm, locally** - A tamper-evident confirmation covering that plan, target, risk and expiry. Migrations use the strong ceremony, where the operator types back what the plan acts on — the release ID for a deploy, the job name for a job run. + A tamper-evident confirmation covering that plan, target, risk and expiry. Migrations use the strong ceremony, where the operator types back what the plan acts on: the release ID for a deploy, the job name for a job run. -4. `ob deploy` — **Execute under a lock** +4. `ob deploy`: **Execute under a lock** Health-gated rollout, traffic drain, verification, then a versioned release with an explicit predecessor. @@ -109,13 +108,13 @@ Declare application intent instead of maintaining generated runtime. Turn fields <Derivation> -- `shop-web-1` — the container, with a one-based replica ordinal that is never omitted +- `shop-web-1`: the container, with a one-based replica ordinal that is never omitted -- `shop_default` — the application's own Compose network +- `shop_default`: the application's own Compose network -- `onebox-proxy` — non-root, socketless Traefik, plus an isolated discovery controller, routing and TLS +- `onebox-proxy`: non-root, socketless Traefik, plus an isolated discovery controller, routing and TLS -- `/var/lib/onebox/app` — releases, current, journal and services +- `/var/lib/onebox/app`: releases, current, journal and services </Derivation> @@ -151,9 +150,9 @@ Deployments, data and generated runtime share one evidence-backed lifecycle. [Sh <Section eyebrow="What it refuses"> -## Refusing is the feature. +## What Onebox refuses -Onebox stops when it cannot preserve the meaning of your declaration. These are refusals, not warnings buried after a deploy. +Onebox stops when it cannot preserve the meaning of your declaration. Onebox refuses these before anything deploys. <Refusals> @@ -167,13 +166,13 @@ Onebox stops when it cannot preserve the meaning of your declaration. These are **A cron whose meaning would change** - A schedule Onebox cannot preserve exactly, rather than one approximated. A job that runs at nearly the right time is a job that ran at the wrong time. + A schedule Onebox cannot translate exactly is refused, not approximated. - `at plan` **A promise it cannot keep** - A backup policy on a driver that cannot honour one, and a major version change a driver cannot perform in place — rather than replacing the container and leaving the data intact and unreachable. + A backup policy on a driver that cannot honour one, and a major version change a driver cannot perform in place, preventing it from replacing the container and leaving the data intact and unreachable. </Refusals> @@ -183,17 +182,17 @@ Onebox stops when it cannot preserve the meaning of your declaration. These are ## The boundary is explicit. -Onebox is deliberately for one application on one host. These limits matter before you trust it with production. +Onebox is for one application on one host. These limits matter before you trust it with production. <Rows> - **Backups cover managed services, not workload volumes** - A service declaring `backup` — PostgreSQL today — is archived off-host and restorable to a point in time. A workload's own volume is not, and `ob doctor` says so for every workload holding durable data. + A service declaring `backup` (PostgreSQL today) is archived off-host and restorable to a point in time. A workload's own volume is not, and `ob doctor` says so for every workload holding durable data. - **`mongodb` runs a standalone server, not a replica set** - An application needing change streams or multi-document transactions will connect, authenticate, and then fail — in its own logs, not in anything Onebox says. + See the [MongoDB driver limitations](/docs/reference/drivers#known-limitations) before using an application that needs a replica set. - **One host, and no failover** diff --git a/site/src/content/docs/privacy.mdx b/site/src/content/docs/privacy.mdx index 07dc31fb..477865e8 100644 --- a/site/src/content/docs/privacy.mdx +++ b/site/src/content/docs/privacy.mdx @@ -14,7 +14,6 @@ This page is about **this website**, `onebox.run`. It is not about the `ob` binary. Onebox runs on your machine and talks to your server over SSH; it has no phone-home, no licence check and no usage beacon, and it reports nothing to us about the hosts you deploy to, the workloads you run, or the plans you apply. -Nothing on this page changes that, because nothing on this page is in it. ## Website analytics @@ -36,7 +35,7 @@ removed the header, and the browsers that still send one leave it off by default, so it says nothing reliable about what a reader wants. Which of these applies is decided from the country Cloudflare reports for your -connection. If that lookup fails, the site asks rather than assumes. +connection. If that lookup fails, the site asks for consent. ### What is sent @@ -53,15 +52,15 @@ Two things are cut down before they leave the browser: - **The referrer is reduced to its site.** We record that you came from a search engine, not what you searched for. -Google's advertising features are switched off — no Google Signals, no ad -personalisation, and ad data redaction is on — so the data is not used to build +Google's advertising features are switched off (no Google Signals, no ad +personalisation, and ad data redaction is on) so the data is not used to build an advertising profile of you. ### What is never sent Nothing you enter anywhere is collected, because this site has no forms, no accounts, no search box that reports what you typed, and no session recording or -heatmaps. It does not attempt to identify you. And, to say it once more: no +heatmaps. It does not attempt to identify you. No hostname, address, credential, plan or release from your own Onebox deployments passes through this site. @@ -91,5 +90,5 @@ you allow analytics. Questions about this page belong in [an issue](https://github.com/labstack/onebox/issues). The site's source, including the analytics code described here, is in -[the repository](https://github.com/labstack/onebox/tree/main/site) — this page -describes what that code does, and the code is the thing you can check. +[the repository](https://github.com/labstack/onebox/tree/main/site); this page +describes what that code does.