Skip to content

feat(profile): distribution profile system (Phase 4)#55

Open
TechAlchemistX wants to merge 2 commits intomainfrom
feat/v0.4-profile-system
Open

feat(profile): distribution profile system (Phase 4)#55
TechAlchemistX wants to merge 2 commits intomainfrom
feat/v0.4-profile-system

Conversation

@TechAlchemistX
Copy link
Copy Markdown
Owner

Summary

The v0.4 headline feature. A "profile" is a TOML config fragment served over HTTPS that teams publish once and every engineer installs with one command:

secretenv profile install acme-defaults
secretenv doctor  # now sees every team backend

Profiles land in <config_dir>/profiles/<name>.toml and are auto-merged into the active Config on load. They fill gaps — never override: the user's own config.toml always wins on conflicts.

New CLI surface

Command Purpose
profile install <name> [--url <url>] Fetch https://secretenv.io/profiles/<name>.toml (or --url), validate, persist
profile list [--json] Show installed profiles with source + install time
profile update [<name>] Re-fetch using If-None-Match for conditional fetch (no name = all)
profile uninstall <name> Remove the .toml and .meta.json sidecar

Merge model

  • User config.toml always wins.
  • Profiles (in alphabetical order, in-memory via HashMap::entry().or_insert()) fill gaps.
  • Malformed profile = error naming the offending filename; no partial load.
  • Missing profiles/ dir = silent no-op.

Storage

$XDG_CONFIG_HOME/secretenv/
├── config.toml              ← user's own config (wins on conflicts)
└── profiles/
    ├── acme-defaults.toml
    ├── acme-defaults.meta.json   ← {source_url, etag, installed_at}
    └── ...

Fetching

Uses curl subprocess — consistent with the backends' CLI-spawn pattern, no new HTTP dep. Response body validates as a Config fragment before touching disk. file:// URLs are supported (for offline testing + local staging).

Security posture (v0.4)

Unsigned profiles over HTTPS. Signing + central index + list --available are deferred to v0.5+. Full threat-model discussion in docs/profiles.md §"Security considerations".

Tests

  • secretenv-core: 5 new merge-semantics tests.
  • secretenv-cli unit: 14 new tests (name validation, URL resolution, install/update/uninstall/list against file:// URLs, ETag parsing, RFC-3339 formatter).
  • secretenv-cli integration: 4 new subprocess tests driving install → list → doctor → uninstall.
  • Workspace totals: 505 → 531 (+26).

Out of scope (deferred)

  • 3 sample profiles committed to secretenv-site/profiles/ (needs DNS live).
  • Live end-to-end profile install acme-defaults against the canonical host (same blocker).
  • Both close as part of Phase 7 aggregate release smoke.

Dependencies

Hits on main first: PR #53 (secretenv.io rename) and PR #54 (Phase 5 harness). Rebase onto those once merged — the overlap is the secretenv.io string in DEFAULT_BASE_URL which this PR already writes as .io.

Test plan

  • cargo fmt --all -- --check clean
  • cargo clippy --workspace --all-targets -- --deny warnings clean
  • cargo test --workspace → 531 pass
  • Manual end-to-end: install via file://, verify doctor auto-sees the profile backend, uninstall cleanly
  • secretenv profile list --json emits parseable JSON
  • secretenv profile install ../evil rejects the path-traversal name

🤖 Generated with Claude Code

TechAlchemistX and others added 2 commits April 20, 2026 22:39
…/ uninstall (Phase 4)

The v0.4 headline feature. A "profile" is a TOML config fragment served
over HTTPS (default base `https://secretenv.io/profiles`, overridable
via `SECRETENV_PROFILE_URL` or per-invocation `--url`) that teams can
publish once and every engineer installs with:

    secretenv profile install acme-defaults

Profiles land in `<config_dir>/profiles/<name>.toml` and are
auto-merged into the active `Config` on load. They fill gaps — never
override: the user's own `config.toml` always wins where both define
the same key. Among profiles, alphabetical filename order decides
conflicts. This makes profiles a safe default-defining layer that
can't silently change local behavior.

New CLI surface:

    secretenv profile install <name> [--url <url>]
    secretenv profile list [--json]
    secretenv profile update [<name>]   # no name → update all
    secretenv profile uninstall <name>

Storage:

    profiles/<name>.toml       ← the profile body
    profiles/<name>.meta.json  ← source_url + ETag + installed_at

`update` uses `If-None-Match: <stored-etag>` for conditional re-fetch.
On 304 the local file is untouched (`UpdateOutcome::UpToDate`); on 200
the file + sidecar are replaced (`UpdateOutcome::Refreshed`).

Fetching uses `curl` (subprocess) rather than a new HTTP client dep —
consistent with every backend's CLI-spawn pattern. Fetched bodies are
validated as `Config` fragments via `toml::from_str::<Config>` before
being written, so a malformed profile never lands on disk.

Config side: `Config::load` + `Config::load_from` now walk the
`profiles/` directory adjacent to the active config path, merging
every `*.toml` in alphabetical order. New public helpers
`default_config_path_xdg()` and `profiles_dir_for(config_path)` give
the CLI one source of truth for where profiles live relative to a
given config file — no XDG logic duplicated in the CLI layer. A
missing `profiles/` dir is a silent no-op; a missing `config.toml`
with a populated `profiles/` still merges cleanly (load() path only).

Security posture (v0.4): unsigned profiles over HTTPS. Signing +
central index + `list --available` are deferred to v0.5+ — rationale
+ mitigations in `docs/profiles.md` §"Security considerations".

Tests:
- 5 new core tests exercise the merge semantics (gap-fill, user-wins
  conflict, alphabetical profile order, malformed-profile error
  surfaces filename, missing-dir silent).
- 14 new CLI-crate unit tests cover name validation, URL resolution
  (explicit > env > default), install/update/uninstall/list against
  `file://` URLs, ETag parsing edge cases, RFC-3339 formatter.
- 4 new integration tests (tests/cli.rs) drive the subprocess against
  a tempdir profile served via `file://`: install → list → doctor
  round-trip + JSON list + path-traversal rejection + uninstall errors.

Workspace tests: 505 → 531 (+26).

DNS side is user-provisioned. `secretenv.io/profiles/*` will be
stood up via S3 + CloudFront once nameserver propagation completes;
until then users can test via `--url file://` or `--url <any-HTTPS>`.

Phase 4 DoD items deferred to a follow-up PR:
- 3 sample profiles committed to secretenv-site (pending DNS).
- Live end-to-end `install acme-defaults` via the canonical host.
Both will close as part of the Phase 7 aggregate release smoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Local clippy passed with --all-features form; the CI form
(`cargo clippy --all-targets --workspace`) surfaces `manual_map`
pedantic lint on `SystemTime::duration_since` result.

Same CI-vs-local discipline pattern as v0.4 Phase 2 (see
feedback_git_workflow memory — always run CI's exact clippy form
locally before pushing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant