Skip to content

A top-level schema: in envshield.yml is the project contract (V2-1) - #95

Merged
rabbilyasar merged 1 commit into
mainfrom
rabbilyasar/v2-1-top-level-schema
Oct 28, 2026
Merged

rabbilyasar merged 1 commit into
mainfrom
rabbilyasar/v2-1-top-level-schema

Conversation

@rabbilyasar

Copy link
Copy Markdown
Owner

Closes #18

In one line: envshield.yml can name one project contract with a top-level schema:, so services no longer need their own schema: line. Existing layouts are unchanged.

Problem

Every service had to name its own schema, and there was no single project-level contract for init, service add and service discover to build on (#19, #44).

Design

  • Which schema a service uses: its own schema: if it has one (legacy, unchanged); otherwise the top-level schema:. Layouts can be mixed. One interpreter, config_manager.service_schema_ref, decides this everywhere:
    • live: get_service_schema_path, get_schema_users, the default-file helper for unresolvable peers;
    • at a revision: schema_snapshot, so schema diff and check --since work on the new layout. Leaving the revision path out would have been the BL-005 shape: correct for one caller only.
  • Directories: on the project contract every service needs dir, except a project's only service, which defaults to .. Duplicate dir and unscoped secrets with more than one user hit the existing shared-schema errors unchanged.
  • Neither key: get_service_schema_path still returns None, so service list, uninstall and service remove don't crash on a broken entry. Loading, directory resolution and hooks raise one error naming both options.
  • init writes the top-level form and gives the service an explicit dir: .. Without it, a later service add, which writes a legacy own-schema service until V2-2 · Append-only contract writer; init, service add/discover and import join the project contract #19, would make the first service's dir required after the fact.
  • A bare name: entry (YAML null) is a service with no settings of its own.

Alternatives

  • Raising from get_service_schema_path for a registered service with no schema: rejected, because service list and uninstall would crash on a broken entry instead of reporting it.
  • Leaving dir out of the generated config: rejected, because it breaks the init → service add path.

Security

  • The top-level path goes through the same project-containment check as other envshield.yml paths (../outside.toml is rejected).
  • Secret scoping is unchanged: users of the project contract are schema users, so an unscoped secret with more than one user still fails closed.

Compatibility

Projects with per-service schema: behave exactly as before. All existing tests pass unchanged. Newly generated envshield.yml files use the top-level form.

Tests

  • 17 new tests in envshield/tests/test_config_manager.py::TestProjectContract, covering:
    • schema path, users and dir for every resolution case the issue lists (top-level only, per-service only, mixed, single service without dir, several without dir, neither key);
    • a contract service beside a legacy one still needs dir;
    • an unknown service is still "not found";
    • duplicate dir, unscoped secrets with one and with two users, invalid top-level values;
    • the generated config loads;
    • the revision lookup projects per service.
  • Full suite: 2341 passed, 1 skipped. ruff check and ruff format --check clean.
  • End to end on a fixture: init, service add, service list, check, schema diff and check --since. Every command in a bare-entry project runs without a traceback.

Real-repository trial (13 repos; recorded in BACKLOG.md above BL-208)

  • Every fresh init writes the top-level form, schema diff against HEAD exits 0 everywhere, and no command produced a traceback.
  • service add of a second service works on all 13. The first service checks as before; the new one reports only that its schema file doesn't exist yet.
  • Speed is unchanged. On Dify, back to back: main 9.7 s, branch 9.5 s. The trial's slower raw times came from the test suite running concurrently.
  • No secret value in any command output.

Found, not fixed here

P0, BL-208 / #94: init writes values it doesn't recognize as secret into env.schema.toml as defaultValue. That includes a password inside a URL (redis://:pass@host) and, on the trial repositories, names such as ..._PASSPHRASE and ..._CREDENTIALS. It predates this PR (the importer is untouched) and blocks any release.

Product / market impact

Foundation for Phase 2, one contract per project (§1: "one file to maintain"). Not a differentiator by itself.

🤖 Generated with Claude Code

https://claude.ai/code/session_01376EvgPvifpHL8LP5BYoPc

…tract (V2-1)

A service without its own `schema:` uses the top-level `schema:`. One
interpreter, config_manager.service_schema_ref, answers which schema a
service entry uses, for the live lookups (get_service_schema_path,
get_schema_users, the default-file helper) and for revision lookups in
schema_snapshot, so `schema diff` and `check --since` work on the new
layout too. Services with their own `schema:` behave exactly as before,
and the two layouts mix.

On the project contract every service needs `dir`, except a project's
only service, which defaults to `.`. A registered service with neither
key gets an error naming both options. `init` writes the top-level form
and gives the service an explicit `dir: .`, so a later `service add`
doesn't make it required after the fact.

Closes #18. Records the real-repository trial and new evidence on
BL-208 (P0, #94, predates this change).
@rabbilyasar
rabbilyasar merged commit 0f2be72 into main Oct 28, 2026
4 checks passed
@rabbilyasar
rabbilyasar deleted the rabbilyasar/v2-1-top-level-schema branch October 28, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

V2-1 · Top-level schema: in envshield.yml

1 participant