Skip to content

Protection layer: 4,865 lines in the tree, no command reaches it, no spec on main, no tracking issue #63

Description

@vishr

The protection layer is 4,865 lines of non-test code in the tree, reachable from no command, with its design document no longer on main and no issue tracking it. This is that issue.

It also records a mistake made in #51, which removed one file of that layer.

What is in the tree

19 non-test files, 4,865 lines, against 20 test files and 2,759 lines:

Area Non-test lines
internal/onebox/ — evidence, state, envelopes, install, identity, resources, credentials, S3 target, active volume 3,435
internal/app/protection_schema.go, protection_artifacts.go 619
internal/engine/protection_lock.go, scheduled_protection.go, protection_credentials.go 546
internal/journal/protection.go 184
cmd/ob-scheduled-runner/main.go 81
Total 4,865

The test coverage is real and passing. That is what has kept it invisible: unused cannot flag a function a test calls, so none of this appears in any gate.

None of it is reachable

ob --help lists no backup, restore, or protection command. Checking the entry points, each one appears only in the file that defines it:

Symbol Production callers outside its own file
ApplyScheduledInstall none
EnableProtection none
AcquireProtectionLock none
InstallProtectionCredentialFile none
ValidateS3Target none anywhere
BuildScheduledInstallPlan none anywhere

cmd/ob-scheduled-runner is not built by anything. .goreleaser.yaml declares a single build, main: ./cmd/ob, and no Justfile recipe or workflow step compiles the runner. The design binds artifacts to a runner digest (Names.ProtectionRunnerPath, internal/onebox/runner_policy.go), and the binary that would satisfy it is in no release artifact.

The docs are honest about it

This is not a documentation gap. site/src/content/docs/status/capabilities.mdx states the position clearly, in two places:

  • Schema only, under a caution box reading "Your editor is ahead of the engine here. These three blocks are published in the JSON Schema and accepted by ob validate. Nothing behind them executes." — covering backup_targets and services.<name>.protection.
  • Not owned at all"Backups. Onebox does not take them." and "Restore proof. No drill is run, and no service can currently prove a backup would restore."

docs/product.md and the README agree. ob doctor actively reports the absence of backups for every workload holding durable data.

So the product's public position is correct and consistent. What is missing is any record of the code.

The design document is not on main

The work was specified. openspec/changes/complete-oss-production-baseline/ held the proposal, design, task list, and seven spec files including specs/managed-data-protection/spec.md, whose purpose reads:

Defines trustworthy, driver-native protection for Onebox-run data services, including encrypted off-host backups, verified restores, restore drills, and evidence-based service-tier graduation.

That directory is absent from HEAD. It survives in tag v2026.8.0, so nothing is lost, but it is not reachable from main and nothing in the repository points at it:

git show v2026.8.0:openspec/changes/complete-oss-production-baseline/tasks.md
git show v2026.8.0:openspec/changes/complete-oss-production-baseline/specs/managed-data-protection/spec.md

tasks.md records 26 complete and 122 outstanding, of which 27 outstanding sit in sections 5–7 — the protection, restore, and image-publication slices. Those outstanding tasks are exactly the missing surface:

  • 5.6 — backup create, backup list, backup inspect, replay inspection, protection status
  • 6.2 — restore prepare
  • 6.4 — restore test
  • 6.6 — restore cutover with durable phase markers
  • 7.2 — the image publication pipeline the runner digest policy depends on

How it got here

PR Merged Scope
#22 feat(protection): add lifecycle foundation 2026-08-08 OpenSpec sections 1–4: schema contracts, envelopes, driver capability records, lifecycle state, "canonical locks, fencing, journals, credentials, runner envelopes, and systemd units"
#23 feat(backup): add S3 target adapter the S3-compatible target
#33 fix: make durable data mean one thing, and the backup gate satisfiable gate correctness

#22's own description sets the expectation plainly: "Database backup and restore engines follow as separate slices." This is staged work that stopped between slices, not abandoned code.

What #51 got wrong

#51 deleted internal/engine/protection_units.go (274 lines) plus its test, on the reasoning that ProtectionUnitTarget had no implementation outside an in-memory test double and no caller anywhere. That reasoning was accurate and the conclusion was wrong, for two reasons found only after the fact:

  1. It was a listed deliverable of feat(protection): add lifecycle foundation #22. That PR enumerates "systemd units" among what it implements. Judged against "has a caller today" it looked like dead code; judged against the plan it was slice 1 of 3, waiting on the slices that install and invoke it.
  2. It held the stronger of the two ownership models. It embedded X-Onebox-Application / -Environment / -Service metadata in each unit and refused to overwrite a unit whose filename matched but whose ownership did not. internal/engine/schedule.go, which survives and is live, matches on filename prefix alone — which is the substance of Four derived names use hyphen joins the naming contract forbids #56 and Stale timer sweep skips removal when disable fails #57.

It is recoverable in full:

git show f9ef6bc^:internal/engine/protection_units.go
git show f9ef6bc^:internal/engine/protection_units_test.go

The deletion also left the layer half-removed: one file gone, 4,784 lines of its siblings standing, on no consistent principle.

What to decide

The code is not the problem. The problem is that 4,865 lines of a specified, part-built subsystem sit in the tree with no issue, no spec on main, and a linter configuration that cannot see it — so the next person to run a dead-code pass reaches the same wrong conclusion #51 did, only for more of it.

Three coherent options:

A. Restore and track it as staged work. Bring protection_units.go back, bring openspec/changes/complete-oss-production-baseline/ back onto main (or a trimmed docs/design/ equivalent), and open one issue per outstanding slice. Cost is low and it makes the state legible. This is the recommendation, because #22 committed to the follow-on slices and the specification for them already exists.

B. Remove the layer. If protection is not being finished, remove all 4,865 lines and their tests, drop backup_targets and services.<name>.protection from the published schema, and delete the "Schema only" rows. This is defensible and honest, but it is a breaking schema change and discards work with a written design.

C. Freeze it deliberately. Keep the code, add a package-level comment on each file naming it as staged work behind managed-data-protection, and add a //go:build tag or an unused-exclusion entry so its status is stated in the code rather than inferred from test coverage. Cheapest, and it at least stops the next accidental deletion.

Whichever is chosen, two things should happen regardless:

  • The design document should be reachable from main, not only from a tag.
  • cmd/ob-scheduled-runner either gets built and shipped, or is removed. A binary that no release contains and no recipe compiles is not a dependency anything can rely on.

Verification

Every claim above was checked against the tree at 81d0ead:

  • Line counts from wc -l over the file set named in the table
  • Caller checks via rg -l '\bSymbol\(' internal/ cmd/ --type go -g '!*_test.go'
  • ob --help from the built binary
  • .goreleaser.yaml build list read directly
  • Spec content and task counts from git show v2026.8.0:openspec/...
  • PR scope from gh pr view 22, 23, 33

Related: #50 (the audit that surfaced the dead code), #59 (the sub-issue for the deletion), #56 and #57 (the systemd ownership gaps the deleted file addressed).

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions