build: bump secretspec to 0.20.0 #2

Merged
kennysheridan merged 932 commits from build/update-secretspec-0.20.0 into main 2026-09-05 19:24:44 -07:00

Summary

  • Bring the fork forward to the OpenBao/Doppler successor branch and bump package metadata to 0.20.0.
  • Update Cargo workspace metadata, Cargo lock package entries, Nix packages, and language SDK package metadata.
  • Update SwiftPM binary metadata to the upstream v0.20.0 artifact checksum.

Validation

  • bash scripts/sync-sdk-versions.sh 0.20.0
  • cargo fmt
  • nixpkgs-fmt flake.nix
  • nix eval .#packages.x86_64-linux.secretspec.version --raw -> 0.20.0
  • nix build .#packages.x86_64-linux.secretspec
  • ./result/bin/secretspec --version -> secretspec 0.20.0

Notes

  • Forgejo main was at the older 0.6.2 fork state, so this PR includes the full OpenBao/Doppler successor update plus the 0.20.0 bump.
  • cargo test --workspace is blocked locally because secretspec-php-native requires php in PATH.
  • cargo test -p secretspec -p secretspec-derive ran 1469 passing tests before failing one existing SOPS age fixture decryption test: provider::sops::tests::test_sops_single_file_get_ini.
  • cargo clippy -p secretspec -p secretspec-derive --all-targets -- -D warnings fails on existing clippy warnings unrelated to this version metadata bump.
  • cargo test -p secretspec-derive and nix build .#packages.x86_64-linux.secretspec-derive exceeded local command timeouts after compilation had progressed.

Rollout / Rollback

  • Rollout: merge this PR, then update consuming flakes to the new Forgejo revision.
  • Rollback: revert this update or pin consumers back to the previous Forgejo revision.

Release-please impact: no release expected

## Summary - Bring the fork forward to the OpenBao/Doppler successor branch and bump package metadata to `0.20.0`. - Update Cargo workspace metadata, Cargo lock package entries, Nix packages, and language SDK package metadata. - Update SwiftPM binary metadata to the upstream `v0.20.0` artifact checksum. ## Validation - `bash scripts/sync-sdk-versions.sh 0.20.0` - `cargo fmt` - `nixpkgs-fmt flake.nix` - `nix eval .#packages.x86_64-linux.secretspec.version --raw` -> `0.20.0` - `nix build .#packages.x86_64-linux.secretspec` - `./result/bin/secretspec --version` -> `secretspec 0.20.0` ## Notes - Forgejo `main` was at the older `0.6.2` fork state, so this PR includes the full OpenBao/Doppler successor update plus the `0.20.0` bump. - `cargo test --workspace` is blocked locally because `secretspec-php-native` requires `php` in `PATH`. - `cargo test -p secretspec -p secretspec-derive` ran 1469 passing tests before failing one existing SOPS age fixture decryption test: `provider::sops::tests::test_sops_single_file_get_ini`. - `cargo clippy -p secretspec -p secretspec-derive --all-targets -- -D warnings` fails on existing clippy warnings unrelated to this version metadata bump. - `cargo test -p secretspec-derive` and `nix build .#packages.x86_64-linux.secretspec-derive` exceeded local command timeouts after compilation had progressed. ## Rollout / Rollback - Rollout: merge this PR, then update consuming flakes to the new Forgejo revision. - Rollback: revert this update or pin consumers back to the previous Forgejo revision. Release-please impact: no release expected
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Secrets can now be auto-generated when missing by adding `type` and
`generate` fields to the secret config. Supported types: password, hex,
base64, uuid, and command. Generation triggers during check/run when a
secret is missing and stores the value via the configured provider.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
List all missing secrets upfront with descriptions before prompting,
add step counter ([1/3]), show profile and provider in header, and
use inquire::Password for consistent masked input. Remove rpassword
dependency in favor of inquire which was already used elsewhere.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch from `directories` to `etcetera` crate so that XDG_CONFIG_HOME
is respected on macOS. Existing configs at ~/Library/Application Support/
are automatically migrated to ~/.config/secretspec/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allow sharing secrets across projects by customizing the storage path
via URI (e.g., keyring://secretspec/shared/{profile}/{key}), matching
the existing OnePassword and LastPass behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Collapse nested if statements using let chains, replace manual Default
impls with derive, use std::slice::from_ref instead of clone, use
io::Error::other, remove redundant closures and identity maps, and
replace unnecessary lazy evaluation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Walk up the directory tree to find the nearest secretspec.toml, similar
to cargo and git. Also adds -f/--file flag and SECRETSPEC_FILE env var
to explicitly specify the config file path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a new provider for AWS Secrets Manager, following the same patterns
as the existing GCSM provider. Uses the standard AWS SDK credential chain
for authentication and stores secrets as secretspec/{project}/{profile}/{key}.

Closes #16

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Support AWS profiles via the URI username position (awssm://profile@region),
matching the onepassword provider's account@vault pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use Handle::try_current() to detect an existing runtime and
block_in_place instead of creating a nested one.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use block_in_place when already inside a tokio runtime, matching the
GCSM provider fix from c811246.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: add AWS Secrets Manager provider (awssm://)
Both cloud providers had identical block_on methods for bridging
async/sync contexts. Move to a shared function in provider::block_on
and make tokio a non-optional dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: claude release command output fuzziness
feat: add HashiCorp Vault / OpenBao provider
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract inheritance and secret generation into dedicated concept pages,
add a concepts overview page, deduplicate content between profiles and
providers pages, fix broken /concepts/inheritance/ link, add missing
providers to landing page, and add Documentation hero button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Providers like OnePassword and LastPass require authentication before
they can be used. Previously, auth was checked inside get/set methods,
meaning the user could be prompted to enter secret values only to have
the operation fail afterwards due to missing authentication.

Introduce a PreflightGuard that wraps every provider at construction
time (via TryFrom). It runs an optional preflight check exactly once
(cached via OnceCell) before the first provider operation. Providers
declare their preflight via the register_provider! macro:

    register_provider! {
        ...
        preflight: check_auth,
    }

The preflight mechanism is not on the Provider trait, so providers
cannot bypass it and callers do not need to remember to call it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Generates RSA private keys in PKCS1 PEM format. Defaults to 2048 bits,
configurable via generate = { bits = 4096 }.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Override get_batch on AwssmProvider to use the AWS BatchGetSecretValue
API, reducing N sequential GetSecretValue calls to ceil(N/20) batched
calls. For a project with 30 secrets this means 2 API calls instead
of 30, plus only 1 client construction instead of 30.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: batch fetching for AWS Secrets Manager provider
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Implement a native BWS provider using the bitwarden SDK v2.0.0 (async,
feature-gated behind `--features bws`).

- URI format: bws://<project-uuid>
- Auth via BWS_ACCESS_TOKEN environment variable
- Flat key names with project UUID providing namespace isolation
- OnceLock-based client caching (login once, reuse across calls)
- OnceLock-based secret list caching (single list_by_project + get_by_ids)
- Full read-write support (get, set, get_batch)
- Unit tests for config parsing, provider metadata, and error handling

Implements PLAN.md Issues #1, #3, #4.

Co-authored-by: Claude <noreply@anthropic.com>
Skip instead of using unsafe env var manipulation when the token is
already set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Avoids pulling CLI and provider features into consumers that only need
the derive macro.
chore(derive): disable default features on secretspec dependency
Setting a secret via the dotenv provider used to corrupt neighboring
entries that held JSON-shaped or otherwise quoted values: the serializer
in serde-envfile wrapped strings in "..." without escaping their
contents, so `FOO="{\"bar\":\"baz\"}"` came back as `FOO="{"bar":"baz"}"`
on the next `set`.

Pin serde-envfile to a cachix fork that escapes `\`, `"`, `$`, and `\n`
per dotenvy's weak-quote grammar. The fix is staged upstream in
lucagoslar/serde-envfile#6; once it releases we can drop the pin.

Fixes #74.
feat: add AppRole authentication to vault provider
`secretspec run` called `std::process::exit` while the
`ValidatedSecrets` value (which owns the `NamedTempFile`s for
`as_path = true` secrets) was still in scope. `process::exit` skips
destructors, so the `tempfile` cleanup never ran and `/tmp/.tmp*`
files leaked along with their secret contents.

Split `run` into a thin wrapper that calls `process::exit` and a
private `run_command` that returns the exit code, so RAII drops
the temp files before exit. Add a regression test that captures
the path the child saw via `$CERT_DATA` and asserts it is gone
after `run_command` returns.

Fixes #71

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The --provider flag (and SECRETSPEC_PROVIDER env var) was silently
ignored on every code path that consulted a per-secret or per-profile
providers chain — set, get, import, check, run. set always wrote to
chain[0], leaving users no way to redirect writes without editing the
toml. The override now consistently wins: writes target the chosen
provider, reads query only it (no chain fallback). The flag also
accepts alias names declared in the global providers map.

Fixes #81.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-secret `providers = [...]` chains now behave as a true fallback chain
when an upstream provider errors (e.g. a 403 from a vault the current user
cannot access). Previously the first provider's error short-circuited the
whole operation; now the error is logged as a warning and the next provider
in the chain is tried. The original error is only surfaced if every
provider in the chain failed, so genuine outages still bubble up. Fixes #83.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The provider resolvers in `Secrets` consulted the env var before the
builder-set value, but the CLI forwards `--provider` via `set_provider`.
The env var therefore overrode the explicit CLI flag, contradicting the
usual CLI > env precedence. Swap the order in `resolve_provider_override`
and `get_provider` so the builder value wins.

Fixes #77.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace serde_envfile::to_string with a small in-tree dotenv
serializer so the crate can be published to crates.io. The
serializer applies the same escapes the fork added (backslash,
double quote, dollar, newline) and matches dotenvy's accepted
escape set, with sorted keys for stable diffs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switches the workspace `reqwest` feature from `rustls-tls` to
`rustls-tls-native-roots` so Vault / OpenBao servers fronted by a
private CA work without modification when that CA is installed in the
OS trust store, and `SSL_CERT_FILE` / `SSL_CERT_DIR` are honored for
bundles that are not installed system-wide. Fixes #85.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat: add Protonpass provider (protonpass://)
When OP_SESSION_<account> is exported (typically from a prior
eval $(op signin)) and the token has expired, op refuses to fall
back to the 1Password desktop app's biometric unlock and instead
returns "account is not signed in" — even when the user has the
desktop integration enabled. Other tools (Pulumi ESC, Dagger) avoid
this because they steer users toward service-account tokens or the
Connect HTTP API rather than shelling out to op.

Strip OP_SESSION_* from spawned op processes at both call sites
(single-item get/set and the parallel batch fetcher) so the
desktop socket handles auth when the shell session is stale.
Service-account-token usage is unaffected. Connect env vars
(OP_CONNECT_HOST/TOKEN) are intentionally left untouched since
Connect is a legitimate auth mode.

Also retitle the auth-required and install-not-found help text to
recommend desktop integration first, service-account tokens second,
and manual signin third; recognize the "account is not signed in"
phrasing emitted by op when delegated-session auth fails; document
the Linux setgid prerequisite for desktop integration.

Fixes #80.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end testing against an actual desktop-integrated 1Password
account showed that `op whoami` returns "account is not signed in"
even when `op vault list`, `op item get`, etc. work fine over the
desktop unlock socket — `whoami` only reports the state of an
explicit `op signin` session, not the delegated session managed by
the desktop app.

Our preflight check called `op whoami`, so every secret operation
failed with a misleading "not signed in" error before any real op
command ran. This is the actual root cause of #80; the OP_SESSION_*
strip in the previous commit is still defensible (community-reported
override behavior) but does not by itself unblock desktop-integration
users.

Switch the preflight probe to `op vault list --format json`, which
exercises the same access path the rest of the provider uses.

Verified locally:
  - Without the fix: secretspec get fails with "authentication required".
  - With the fix:    secretspec get returns the stored value via biometric.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(onepassword): probe auth via vault list so desktop integration works
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`secretspec check` previously rendered optional-but-unset secrets with
the same green `✓` as present-and-set ones and counted them in the
"found" total. The screenshot in #72 showed five green checkmarks and
"5 found, 0 missing" even though one of the secrets had `required =
false` and no value in the backing provider — visually indistinguishable
from a fully provisioned setup.

The data was already there: `validate()` returns `missing_optional` on
the success path, but `display_validation_success` iterated the profile
and ignored it. `display_validation_errors` rendered the right `○
(optional)` icon but still bucketed those names into `found_count`.

Now both paths route through a shared `format_summary` helper that
appends `, N optional` only when at least one optional secret is unset,
so the all-set case keeps its previous `X found, Y missing` form.

Fixes #72.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three call sites in secrets.rs were wrapping profile-not-found in
SecretNotFound, producing the confusing
  Secret 'Profile 'default' not found' not found
when a project's secretspec.toml didn't define the resolved profile.
Replace with the dedicated InvalidProfile variant via a shared helper
that also lists the available profiles. Surfaced in #79.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`echo -n ''` is not POSIX-portable: macOS /bin/sh (which is dash)
prints "-n" literally instead of suppressing the newline, causing
`test_generate_command_empty_output` to pass unexpectedly on macOS.

Replace with `printf ''` which produces zero bytes on all platforms.
fix(test): use portable command for empty-output test
Landing page, quick-start, README, and llms-txt description were
missing the newer providers. Also sync the `config init` example
output with the registry-backed descriptions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous list missed index.mdx, quick-start.mdx, the llms-txt
description, and the README's `config init` example — exactly the
files that drifted when Proton Pass and BWS were added. Calls out
the dual-update locations (list + example output) explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Provider aliases (e.g. `op_infra = "onepassword://Infra"`) could previously
only be declared in the per-user `~/.config/secretspec/config.toml`, which
forced every developer and CI runner to replicate the mapping by hand and
made it impossible to share via VCS.

Add an optional top-level `[providers]` table to `secretspec.toml`.
Aliases declared there are visible to per-secret `providers = [...]`
chains and to `--provider` / `SECRETSPEC_PROVIDER`, and are merged with
the existing user-level `[defaults.providers]` map. On name conflicts
the project entry wins so a team's checked-in mapping cannot be silently
shadowed by a stale local config. `extends = [...]` carries inherited
entries through the same merge rules.

`defaults.provider` and `defaults.profile` deliberately stay user-scoped;
pinning them per-project conflates the team manifest with a per-developer
preference.

Docs also corrected: every existing example showed user-level aliases as
`[providers]` at the root of `~/.config/secretspec/config.toml`, but that
form silently deserializes to `defaults.providers = None` (the field is
nested inside `GlobalDefaults`). `secretspec config provider add` writes
the correct nested form, so CLI-only users were unaffected, but hand-edits
based on the docs did nothing. All user-config examples now use
`[defaults.providers]`; project-level examples (which IS top-level) stay
as `[providers]`.

Closes #79; addresses the "share aliases via VCS" half of #90.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(config): allow [providers] alias map in secretspec.toml
feat(awssm): support prefix in provider config
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the splash-template index with a custom Astro page modeled on
the devenv-docs-v2 design: hero with paired terminal previews, scrolling
provider marquee, three-step setup, bento feature grid, and showcase
sections for the provider switcher, profiles, fallback chains, type-safe
SDK, and migration.

Also adds:
- Hero override that suppresses the Starlight-injected page-title H1 on
  splash pages
- SocialIcons override that renders a release version pill (read from
  the workspace Cargo.toml) next to the GitHub link

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The standalone GitHub social icon is replaced by a pill that combines
the GitHub mark with the live star count, linking to the repo. A second
pill shows the workspace version and links to the release tag.

Star count is fetched at build time via authenticated GitHub API when
GITHUB_TOKEN is set, with an unauthenticated fetch and a gh-CLI call as
fallbacks. All fetch paths degrade silently to no pill on failure so
builds never break.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The star pill was baked in at build time, so it showed a stale count until
the next deploy. Move it to a runtime fetch from our own /api/stars route, a
Cloudflare Worker that proxies GitHub with edge caching (cf.cacheTtl) so the
GitHub API is hit at most ~once per hour per colo instead of once per visitor.

The pill renders hidden and a hoisted script reveals it once populated; any
failure leaves it hidden. The version pill stays build-time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generate `secretspec.toml` on `init` with a `toml_edit` document builder
instead of hand-interpolating strings, so keys and string values are quoted
and escaped by the library. This makes the following inputs round-trip
correctly instead of producing unparseable or silently wrong TOML:

* control characters (notably U+007F) in name/description/default, which a
  manual escaper let through and the TOML parser then rejected
* a secret name containing a dot (dotenvy accepts e.g. FOO.BAR), which as a
  bare key parsed as a nested/dotted key and silently collapsed to a secret
  named FOO on round-trip
* a configured project.extends, which was never written at all

Secrets are emitted as inline tables and profiles/secrets are sorted for
deterministic output, preserving the helpful comments in the generated file.

Also remove the conflicting `-f` short flag for `--from` (`-f` is the global
`--file` option; the duplicate panicked in debug builds and was ambiguous in
release), and update the CLI reference docs to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add unit tests for the config/validation error paths that were bypassed
because tests always built valid configs:
- is_valid_identifier accept/reject table
- Config::validate guards (empty name, no profiles, empty profile,
  invalid secret name) plus a valid-config case
- Secret::validate branches (missing/empty description, required+default,
  generate-without-type, unknown type, command-type-requires-command)
- GenerateConfig::is_enabled
- ValidationErrors Display and has_errors (required vs optional/defaults)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add no-network unit tests for provider logic that previously had zero
coverage, using the existing ProviderUrl(Url::parse(...)) pattern:
- ProviderUrl percent decode/encode and ProviderInfo display
- keyring/pass: format_service / format_entry_name (default + custom
  prefix), TryFrom scheme validation, and uri() encoding round-trips
- onepassword: TryFrom field parsing (account@vault, +token scheme via
  username/password, localhost-ignore, unknown-scheme error),
  get_vault_name, format_item_name, and uri() (account round-trip plus a
  guard that the service account token is never leaked by uri())

Notes a dead match arm found while testing: the "1password" scheme guard
in OnePasswordConfig::try_from is unreachable because Url::parse rejects
digit-leading schemes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add direct tests for previously untested Secrets entry points, using a
dotenv provider over a temp .env (no network):
- check(true) returns Ok with the resolved secret when the required
  secret is present
- check(true) returns RequiredSecretMissing when it is absent (no_prompt
  avoids the interactive path)
- run_command propagates the child's exit code verbatim (exit 3 -> 3,
  true -> 0, false -> 1)
- resolve_profile(Some(unknown)) returns InvalidProfile listing the
  available profiles

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test: expand coverage; fix init TOML serialization and -f flag conflict
Below Starlight's 50rem breakpoint the whole header right-group is hidden
and socials only live in the hamburger menu. Reveal just the social
cluster in the always-visible top bar (GitHub stars pill + Discord),
keep the theme/language pickers in the menu, and hide the version pill.
On mobile the logo is pushed left and the search box is reordered to be
the last icon, clearing the fixed hamburger button on sidebar pages.

Also add a dev-only Vite middleware that serves /api/stars during
`astro dev`, mirroring worker.js, so the star pill populates locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the detect-coding-agent crate (used for AI-agent detection in the
require_reason policy) and pins the devenv Rust toolchain to stable 1.92.0
to satisfy the crate's MSRV (was resolving to 1.91.1 from nixpkgs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update from the deprecated @beta action to @v1: rename direct_prompt to
prompt and add REPO/PR NUMBER context now required for automated reviews.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix: pass-cli >= 2.1.0 introduced agent sessions that reject audited item
operations (item view/create/delete) unless PROTON_PASS_AGENT_REASON is set,
which made existing secrets appear missing under an agent session. The Proton
Pass provider now sets this variable on every pass-cli invocation.

Add a reason for secret access, surfaced as:
  - CLI: global --reason flag and SECRETSPEC_REASON env var
  - SDK: Secrets::with_reason(), backed by a new Provider::set_reason trait
    method (default no-op; forwarded through the Arc/preflight wrappers)
The reason is resolved as explicit reason, then PROTON_PASS_AGENT_REASON, then
a secretspec-versioned default, and is forwarded to providers that audit-log.

Add the [project].require_reason policy in secretspec.toml. It accepts "agents"
(default: require a reason only when an AI coding agent is detected), true
(require it from every caller), or false. Enforced inside secretspec at every
public access method, so it applies uniformly to humans, CI, and any agent and
cannot be bypassed. Agent detection is delegated to the detect-coding-agent
crate plus a SECRETSPEC_AGENT opt-in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
astro dev's vite/esbuild dependency pre-scan extracts inline scripts more
crudely than astro build: it latched onto the literal "<script>" inside the
frontmatter comment and parsed the following prose ("below) from our own ...")
as a script, failing with `Expected ";" but found ")"` and aborting the dep
scan. astro build was unaffected (it uses the compiler's extraction), which is
why this stayed latent until a fresh astro 5.16.9 install ran in CI. Reword the
comment to "the inline script below" so no stray tag token remains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reasons for secret access + Proton Pass 2.1 agent-session fix
- inherit `require_reason` through `extends`: a parent config's policy now
  applies to children that leave it unspecified (it was silently dropped, so a
  shared base config could not set the policy fleet-wide). The field is now
  `Option<RequireReason>` and is merged in `Config::merge_with`.
- ProtonPass: a blank/whitespace session reason no longer shadows a usable
  `PROTON_PASS_AGENT_REASON`; each source is normalized before falling through.
  Precedence logic is split into a pure `resolve_reason` for hermetic testing.
- give a precise, located parse error for a wrong-typed `require_reason`
  (e.g. `require_reason = 1`) via a hand-written visitor instead of an untagged enum.
- route both `pass-cli` invocation paths through one `pass_cli_command` helper so
  the agent-reason env wiring cannot drift between the single-shot and batch paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the starlight-blog plugin (0.25.x, compatible with Starlight 0.34),
extend the docs collection schema with blogSchema, and add a starter
welcome post. Generates /blog, per-post pages, author pages and an RSS feed.

Surface the latest post in a banner at the top of the landing page, inside
the hero so the grid backdrop runs behind it from just under the navbar.
Fix the content-panel padding that was offsetting the hero grid and keep the
tile's top/bottom spacing symmetric.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Recolor the lockup to a soft off-white for dark theme and wire up
Starlight's light/dark logo pair so it swaps with the active theme.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stack the flag pill and date on a top row with the date pushed right, hide
the summary, and let "Read post" flow inline as the tail of the heading at a
slightly smaller size. Desktop layout is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: custom bitwarden instance support
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record every secret operation to a local audit log so access is reviewable
after the fact. On by default.

Operations covered:
- get / set: one event per secret (key, serving provider, outcome).
- check: one event listing every secret checked, with the overall outcome.
- run: one event with the injected secret set and the executed program
  (argv[0] only — arguments may contain secrets), logged once the child has
  started (or as an error if it fails to start or validation fails) so blocked
  and failed runs are auditable.
- import: one event with the copied secrets and the source provider.

Each event is one JSON Lines record: timestamp, action, project, profile,
secret name(s), redacted provider URI, outcome, reason, and actor (OS user +
detected coding agent). All events from one invocation share a session_id.
Secret values are never written; credentials embedded in provider URIs are
redacted.

The log is a single file capped at 1 MiB via file-rotate (truncated and
restarted at the cap; no rotated backups). Audit failures never block secret
access — they warn on stderr and continue — and the first write discloses the
log location.

Auditing is a per-machine/operator concern, configured under the top-level
[audit] table in the user-global config (~/.config/secretspec/config.toml:
enabled, path, max_size_bytes), not the project's secretspec.toml. A cloned
repository therefore cannot redirect or silence the audit log.

Add `secretspec audit` to read the log with --project, --action, --tail/-n,
and --json filters, printing a colored one-line summary per entry.

Includes docs: new Audit Logging concept page, configuration and CLI reference,
frontpage card, and README entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: local audit log for secret access
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the placeholder welcome post with the 0.12 announcement and switch
the blog author to Domen Kožar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "run secretspec from a subdirectory" feature (#59) had no tests for its
two core paths — walking up to the nearest secretspec.toml and loading via a
relative --file path — and CI only ran on Ubuntu/macOS. A Windows user reported
both auto-detection and `-f ../secretspec.toml` failing, which the existing
suite could never catch.

- Factor find_config_file into find_config_file_from(start) so the walk can be
  tested against an explicit directory without mutating the process-global CWD.
- Add a shared CWD_GUARD mutex to serialize current-directory-mutating tests
  (the CWD is shared across test threads); retrofit the existing dotenv test.
- Add tests: walk-up to nearest ancestor, missing-manifest reporting, and
  relative --file resolution (bare filename and ../-relative parent).
- Add a native windows-latest `cargo test --all` CI job, since the devenv/Nix
  job cannot run on Windows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three Windows-only test failures surfaced by the new Windows CI job:

- dotenv:// URIs built from absolute paths (dotenv://C:\path\.env) failed
  to parse with "invalid port number" because the drive-letter colon was
  read as a host:port separator. Windows absolute paths are now encoded as
  an opaque host so they round-trip through the URL intact.
- The audit log could not reset at its size cap: set_len on the append-only
  handle is denied by Windows. Truncate through a separate write handle.
- An audit-config test hardcoded a Unix absolute path, which Path::is_absolute
  rejects on Windows; pick a platform-appropriate absolute path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The declare_secrets! compile-error tests assert text that embeds the OS
filesystem error string, which Windows renders differently ("The system cannot
find the file/path specified" vs "No such file or directory", sometimes with a
different os error number). trybuild matches a single .stderr per .rs file with
no per-platform variant, so add identical .rs sources under tests/ui/windows/
paired with Windows snapshots and select the directory by target_os.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test: cover config discovery and run on Windows CI
Relative dotenv paths (e.g. `dotenv:.config/.env`) were resolved against
the process's current working directory rather than the directory
containing secretspec.toml. Running `secretspec run --file
../secretspec.toml` from a subdirectory therefore loaded the config
correctly but looked for the referenced .env file under the
subdirectory, failing with "Secret is required but not set" (#59).

Add `Provider::with_base_dir`, a default no-op, implemented by the
dotenv provider to rebase relative paths onto the project root. Secrets
records the config directory (kept logical, not canonicalized, so a
relative --file stays cwd-relative and Windows \\?\ prefixes are never
introduced) and applies it before any provider I/O. Absolute paths are
unaffected, and running from the project root is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs: make marquee items link to provider docs
- remove underline from marquee item links
  - fix stale CLAUDE.md reference to `providerIcons`, now `providerMetadata`
docs: remove underline from marquee item links
pass-cli 2.0.3 (protonpass/pass-cli commit 1c09fd8) changed the JSON
shape of `item list --output json`: the item title moved from a nested
`content.title` to a top-level `title`, and the per-item `content`
object was dropped from list output. The provider required `content`, so
the parse failed on newer pass-cli and silently fell back to an empty
item list, making secretspec report active secrets as missing.

Accept both the legacy (<= 2.0.2) nested and new (>= 2.0.3) top-level
list shapes via a dedicated ProtonPassListItem struct with a title()
accessor, and add regression tests for both layouts.

Fixes #104

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(protonpass): support pass-cli >= 2.0.3 item list shape
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Strongly type devStarsApi plugin
docs: setup type checking on build
Surface the resolution waterfall the resolver already computes as a stable,
versioned, machine-readable contract that never carries secret values. This is
phase 1 of polyglot language support: the per-secret provenance type every
later phase (FFI, codegen, SDKs) depends on, shipped standalone as the
check --explain/--json quick win.

- New public ResolutionReport / SecretResolution / ResolutionStatus types
  (schema_version 1), serde-serializable, with to_explain_string() and
  all_required_present() helpers.
- validate_audited now records per-secret provenance (status, the serving
  provider's credential-free URI, generated, default_applied, as_path) instead
  of discarding it; entries sorted by name for deterministic output.
- ValidatedSecrets and ValidationErrors carry the resolution and expose
  report(), so the report is available on both success and missing-required.
- check --json (versioned JSON) and check --explain (human trace) skip the
  prompt-for-missing flow and exit non-zero when a required secret is missing,
  so CI can gate on them. No secret values are ever printed.
- Canonical JSON Schema committed at schema/resolution-report.schema.json.
- Golden wire-format test plus an end-to-end provenance test through a real
  dotenv backend; CLI reference and CHANGELOG updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 2, slice 1: the authoritative value-carrying resolution output the C ABI
and other-language SDKs consume. The FFI crate (next slice) is a thin wrapper
over this, keeping resolution logic in one place.

- New Secrets::resolve() -> ResolveResponse, building on phase 1 provenance:
  per-secret value (or persisted temp-file path for as_path), source
  (provider/generated/default), and the serving provider's credential-free
  URI. On a missing required secret it returns an empty secrets map plus
  missing_required, mirroring the derive crate's load().
- New public ResolveResponse / ResolvedSecret / ResolvedSource types
  (schema_version 1, BTreeMap for deterministic key order) with is_ok() and
  without_values().
- secretspec resolve --json prints the payload (values to stdout, meant to be
  piped); --no-values emits the same structure value-free. Exits non-zero when a
  required secret is missing.
- Canonical JSON Schema at schema/resolve-response.schema.json; CLI reference
  and CHANGELOG updated; tests cover values, provenance, missing-required, and
  as_path path persistence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 2, slice 2: the in-process boundary other-language SDKs bind to. A
deliberately narrow, JSON-in/JSON-out C ABI keeps every binding thin and keeps
resolution logic in the secretspec crate alone.

- Three-function surface: secretspec_resolve(request_json) -> response_json,
  secretspec_free(ptr), secretspec_abi_version(). Request and response are the
  versioned JSON contract; the response envelope separates transport failure
  ({"ok":false,"error":{kind,message}}) from a successful resolution
  ({"ok":true,"response":ResolveResponse}) that still reports missing_required.
- Panics are caught at the boundary (never unwind across FFI); returned strings
  are caller-owned and freed via secretspec_free; null and bad input are handled.
- Hand-written C header at secretspec-ffi/include/secretspec.h; crate builds as
  cdylib + staticlib + rlib.
- SecretSpecError::kind() promoted to pub for typed SDK error handling.
- Tests drive the real extern \"C\" entry points (values, no_values,
  missing-required, invalid input, missing manifest); a committed smoke.c plus a
  drafted per-platform ffi-build workflow build and smoke-test the cdylib on
  linux/macos/windows (native per-runner; portable packaging is follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 3, slice 1: the single brain for typed-accessor generation. Every
generator (the Rust derive macro and the future TS/Python/Go/Ruby emitters)
needs the same manifest decisions; computing them in one place stops drift.

- build_ir(&Config) -> CodegenIr reduces a manifest to a language-neutral IR:
  project name, sorted profile list, the union field set (optional if optional-in
  or missing-from any profile, a path if as_path in any profile), and the
  per-profile raw (non-merged) field sets.
- Faithfully reproduces derive macro semantics, including the long-standing quirk
  that an unspecified `required` is treated as optional (differs from the runtime
  resolver) so generated output stays stable.
- IR types are serde-serializable for emitters and tooling. Unit tests cover
  union optionality, missing-in-profile, as_path-in-any, per-profile exactness,
  descriptions, and the empty-profiles default case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 3, slice 2: validate the IR against the known-good consumer and remove the
duplicated typing logic, so the derive macro and the future TS/Python/Go/Ruby
emitters share one brain.

- declare_secrets now calls secretspec::codegen::build_ir(&config) once and
  sources every typing decision from it: the union struct fields, per-profile
  enum variants, the Profile list, and the load_profile arms. The empty-profiles
  special case disappears because the IR already models it.
- Removed the derive's own is_secret_optional / is_field_optional_across_profiles
  / is_field_as_path / analyze_field_types / get_profile_variants; the token
  emitters now read optional/as_path straight off the IR (only the Rust type
  mapping stays local). Dropped the unit tests that covered those moved helpers
  (now tested in secretspec::codegen).
- Generated API is unchanged: 15 derive unit + 3 trybuild UI + 12 integration
  tests pass, and the example crate resolves through the generated builder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 4: the first non-Rust consumer, proving the whole stack from a generic
dlopen caller (the same C ABI path Go/purego and Ruby/ffi will reuse).

- secretspec-py binds secretspec-ffi via cffi: marshals a JSON request to
  secretspec_resolve, parses the envelope, frees the buffer. No resolution logic
  in Python; every provider is inherited from the Rust core.
- Mirrors the derive crate's vocabulary: SecretSpec.builder().with_provider()
  .with_profile().with_reason().load() -> Resolved(.secrets/.provider/.profile),
  plus set_as_env(). MissingRequiredError vs SecretSpecError(.kind) separate a
  missing required secret from a transport failure. as_path yields a file path.
- Library discovery via SECRETSPEC_FFI_LIB, a wheel-bundled copy, or a Cargo
  target dir. pyproject packages it; README documents it.
- pytest suite (6 tests) drives the real cdylib end to end: values, default
  source, missing-optional, set_as_env, missing-required, as_path, invalid input.
  conftest builds the crate and locates the library automatically.
- devenv.nix now provides Python + cffi + pytest + maturin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes phase 4: the codegen half of the Python reference SDK, so typed
accessors and the runtime SDK ship together. The emitter is a thin template over
the shared codegen IR, so it cannot drift from the derive macro or future
language emitters.

- codegen::python::emit(&CodegenIr) -> String generates a module that mirrors
  the derive crate's shape over the runtime SDK: a SecretSpec union dataclass
  plus one <Profile>Secrets dataclass per profile, each with a builder-style
  load(). Idiomatic Python: snake_case attributes typed str / Optional[str] /
  Path, required pulled directly, optional guarded, as_path wrapped in Path.
- New `secretspec codegen --lang python [-o FILE]` CLI command (value-free;
  reads only the manifest via the now-non-test-gated Secrets::config()).
- Rust test asserts the emitted types/assignments; two Python e2e tests generate
  a module via the CLI, import it, and resolve through the generated accessors
  (union + profile-pinned, including as_path). conftest builds the CLI too.
- Generated code avoids `from __future__ import annotations` so it is robust when
  imported/exec'd in any context. CLI reference and CHANGELOG updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 5: the Go binding over the same C ABI, reusing the generic dlopen path the
Python SDK validated (here in a static language, for the devops/k8s audience).

- secretspec-go binds secretspec-ffi via purego (dlopen, no cgo): marshals a
  JSON request to secretspec_resolve, reads the C string, frees it. No
  resolution logic in Go; every provider comes from the Rust core.
- Mirrors the derive vocabulary with idiomatic Go (PascalCase): New()
  .WithProvider().WithProfile().WithReason().Load() -> *Resolved with
  Provider/Profile/Secrets and SetAsEnv(). *MissingRequiredError vs *Error{Kind}
  separate a missing required secret from a transport failure. as_path yields a
  readable file path.
- Library discovery via SECRETSPEC_FFI_LIB or a Cargo target dir. Tests (gofmt
  clean) drive the real cdylib end to end via a TestMain that builds and locates
  it: abi version, values+provenance, missing-required, as_path, invalid input.
- devenv.nix now provides Go.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 5: the Ruby binding over the same C ABI. Uses stdlib Fiddle (dlopen)
rather than the ffi gem, so there is no native gem build; same generic C ABI
path as Python/Go.

- secretspec-rb binds secretspec-ffi via Fiddle: marshals a JSON request to
  secretspec_resolve, reads the C string, frees it. No resolution logic in Ruby.
- Mirrors the derive vocabulary idiomatically:
  Secretspec::SecretSpec.builder.with_provider.with_profile.with_reason.load ->
  Resolved(#provider/#profile/#secrets) plus set_as_env!. MissingRequiredError
  vs Error(#kind) separate a missing required secret from a transport failure.
  as_path yields a readable file path.
- Library discovery via SECRETSPEC_FFI_LIB or a Cargo target dir. minitest suite
  (6 tests) drives the real cdylib end to end, building/locating it first.
- gemspec + README; devenv.nix now provides Ruby.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 5: the Node binding over the same C ABI, via koffi (dlopen), keeping Node
on the identical generic C ABI path as Python/Go/Ruby. (napi-rs remains the
future production-distribution option; koffi keeps the reference uniform.)

- secretspec-node binds secretspec-ffi via koffi: marshals a JSON request to
  secretspec_resolve, decodes the C string, frees it. No resolution logic in JS.
- Mirrors the derive vocabulary idiomatically (camelCase):
  SecretSpec.builder().withProvider().withProfile().withReason().load() ->
  Resolved(provider/profile/secrets) plus setAsEnv(). MissingRequiredError vs
  SecretSpecError(.kind) separate a missing required secret from a transport
  failure. as_path yields a readable file path. TypeScript types in index.d.ts.
- Library discovery via SECRETSPEC_FFI_LIB or a Cargo target dir. node:test
  suite (6 tests) drives the real cdylib end to end, building/locating it first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The capstone of phase 5: prove the Python, Go, Ruby, and Node SDKs agree. They
are all thin clients over one C ABI, so the risk is in each SDK's parsing and
exposure, not the resolver. This suite locks that down.

- conformance/fixtures/* are self-contained cases (manifest + .env +
  expected.json). Each SDK resolves them and projects its result to a canonical
  shape (profile, per-secret {value, source, as_path}, missing lists), then
  asserts equality with expected.json. For as_path secrets the canonical value
  is the materialized file's contents, so it is deterministic across languages.
- Each SDK runs the fixtures inside its own native runner (pytest, go test,
  minitest, node:test), reading conformance/ relative to the repo root. All four
  pass the same two fixtures (basic: provider + default + optional-missing;
  as_path).
- Fixtures cover successful resolutions; error behavior stays in per-SDK suites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
conformance/run.sh builds the secretspec-ffi cdylib once, points every SDK at it
via SECRETSPEC_FFI_LIB (so they don't each rebuild), runs all four conformance
suites in their native runners, and prints a combined PASS/FAIL/SKIP summary.
Exits non-zero if any language fails; a missing toolchain is SKIP, not FAIL.
README documents it as the one-command entry point.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the hand-written per-language codegen emitter approach (the
`codegen --lang python` command and the WIP Go/Ruby/TypeScript emitters) with a
single JSON Schema emitter, so we no longer maintain a typed-accessor generator
per language. quicktype turns the schema into idiomatic types AND deserializers
for any language; we maintain only a tiny generic `fields()` helper per SDK.

- `secretspec codegen --lang ...` becomes `secretspec schema`: emits a JSON
  Schema (draft-06) with a `SecretSpec` union type plus one `<Profile>Secrets`
  per profile, property names = secret names, optionals nullable. Driven by the
  same shared IR (so it can't drift from the derive macro).
- Each runtime SDK gains a generic `fields()` returning a flat
  `{SECRET_NAME: value}` map (the file path for as_path): Python/Ruby return the
  map, Go/Node also expose a JSON variant (FieldsJSON / fieldsJson) for
  quicktype's bytes/string deserializers.
- The loader the user writes is one line, e.g.
  `SecretSpec.from_dict(resolved.fields())`. quicktype owns naming, optionality,
  and converters for all current and future languages.
- Deleted codegen::{python,go,ruby,typescript} and their emitter tests; added a
  schema emitter test. Python e2e now drives the real pipeline:
  `secretspec schema | quicktype --lang python` then
  `SecretSpec.from_dict(resolved.fields())`. CLI reference + CHANGELOG updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a SDKs workflow that builds the cdylib + CLI once and runs every SDK's full
test suite (unit + cross-language conformance + the schema/quicktype codegen
pipeline) via scripts/ci-sdks.sh, so the Python/Go/Ruby/Node bindings cannot
silently rot. The prior CI (`devenv test` -> cargo test --all) covered only the
Rust crates, including secretspec-ffi, but none of the language SDKs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The polyglot SDKs were undocumented (the docs site had only a Rust SDK page and
the README never mentioned them). Adds a docs page per SDK (quick start, error
model, the schema/quicktype typed-access pattern, library discovery), wires them
into the sidebar, and adds a "Language SDKs" section to the README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 6 / pillar A, Python: make the Python SDK installable without a separate
native build by bundling the secretspec-ffi cdylib into the wheel. The SDK
already prefers a bundled secretspec/_lib/ over SECRETSPEC_FFI_LIB / a Cargo
target dir, so this wires up the build.

- scripts/stage-cdylib.sh builds the cdylib (release) and stages it into
  secretspec/_lib/ (gitignored) per OS, including the Windows .dll case.
- setup.py forces a platform (non-pure) wheel tagged py3-none-<platform> so pip
  installs the right native library per OS/arch; metadata stays in pyproject.
- Verified locally: the produced wheel is py3-none-linux_x86_64, contains
  secretspec/_lib/libsecretspec_ffi.so, and a clean install (no env var, outside
  the repo) loads the bundled lib and resolves a secret.
- Drafted python-wheels.yml: a per-platform matrix (linux x86_64/aarch64, macOS
  x86_64/aarch64, windows) that stages the cdylib, builds the wheel, and smoke
  tests it. NOTE: Linux wheels are tagged linux_*, not manylinux_*; PyPI
  publishing still needs an auditwheel-repair step to vendor the cdylib's system
  deps (libdbus from keyring) and make glibc portable. That is the remaining
  follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 6 / pillar A, Ruby: make the Ruby SDK installable without a separate
native build by bundling the secretspec-ffi cdylib into a platform gem.

- The SDK now prefers a vendored vendor/<lib> (staged into a platform gem) over
  SECRETSPEC_FFI_LIB / a Cargo target dir.
- scripts/stage-cdylib.sh builds the cdylib (release) and stages it into
  vendor/ (gitignored) per OS, incl. the Windows .dll case.
- The gemspec includes vendor/* and sets Gem::Platform::CURRENT when the lib is
  staged, so `gem build` produces a platform gem (else a pure-Ruby gem).
- Verified locally: built secretspec-0.12.0-x86_64-linux.gem, and a clean
  install (no env var, outside the repo) loaded the bundled lib and resolved a
  secret. Existing Ruby suite still green.
- Drafted ruby-gems.yml: per-platform matrix that stages, builds, and smoke
  tests the gem. NOTE: like the wheels, a portable Linux gem needs a baseline
  build (e.g. rake-compiler-dock) to vendor the cdylib's system deps (libdbus)
  and glibc; that is the remaining follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 6 / pillar A, Go: let `go get` work with no native build by embedding the
secretspec-ffi cdylib per platform (go:embed) and extracting it to a temp file
at first use for purego to dlopen.

- Per-platform embedded_<os>_<arch>.go files (linux/darwin/windows x amd64/arm64)
  embed lib/secretspec_ffi_<os>_<arch>.<ext>; embedded.go extracts it to a
  content-addressed temp path. findLibrary prefers SECRETSPEC_FFI_LIB, then the
  embedded lib, then a Cargo target dir.
- Gated behind the `embed_lib` build tag: the default build (CI, `go test`, a
  plain checkout) compiles a nil-stub and needs no staged binary, so nothing
  breaks; only release/distribution builds pass `-tags embed_lib` with the
  libraries staged.
- scripts/stage-cdylib.sh builds + stages the lib under the build-tagged name.
  Verified locally: default `go test` green, and a tagged build outside the repo
  with no SECRETSPEC_FFI_LIB embeds the lib and resolves a secret.
- Drafted go-embed.yml (per-platform build + embedded smoke test). NOTE: the
  embedded libs are ~34 MB each; a release commits them via git-LFS (they are
  gitignored here) and flips embedding on by default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 6 / pillar A, Node: replace the koffi (dlopen) binding with a napi-rs
native addon that embeds the resolver, so `npm install` needs no cdylib or
SECRETSPEC_FFI_LIB. This is the standard way to ship a Rust-backed npm package.

- New `secretspec::resolve_json(&str) -> String` in the core: the shared
  JSON-in/JSON-out boundary (request -> response envelope). secretspec-ffi is
  refactored into a thin wrapper over it (and drops its serde deps), so the C
  ABI and the napi addon define the envelope contract in exactly one place.
- New secretspec-node-native crate (napi-rs) exposing resolve()/abiVersion()
  over resolve_json; a napi cdylib is a valid Node addon, so scripts/build-addon.sh
  is just `cargo build` + rename to secretspec.node.
- index.js now requires ./secretspec.node instead of koffi; the JS API
  (builder, Resolved, fields/fieldsJson, errors) is unchanged. Dropped the koffi
  and unused typescript deps; the package has no runtime npm dependencies.
- Test harness builds the addon instead of the cdylib; all 8 Node tests (incl.
  conformance) pass, and the full cross-language suite stays green.
- Drafted node-addon.yml (per-platform addon build + smoke test). NOTE: full npm
  distribution publishes per-platform addon packages (the pattern @napi-rs/cli
  automates); that publish wiring is the remaining follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 6 / pillar A follow-up: turn the per-platform build workflows into release
pipelines that produce portable artifacts and publish on a version tag, plus a
RELEASE.md runbook.

- Python (python-wheels.yml): build Linux wheels inside a manylinux_2_28
  container and repair with auditwheel (vendors the cdylib's libdbus/glibc),
  native macOS/Windows wheels, and publish to PyPI via Trusted Publishing (OIDC).
- Ruby (ruby-gems.yml): add a publish job that `gem push`es the platform gems
  (RUBYGEMS_API_KEY). Portable-Linux gem build noted as a follow-up.
- Go: add secretspec-go/.gitattributes so the embedded libs are git-LFS tracked
  when a release commits them.
- RELEASE.md documents each ecosystem's build approach, publish mechanism,
  required secrets, and known gaps (Ruby portable build; Go git-LFS + manual
  commit; Node multi-platform npm via @napi-rs/cli optional packages).

UNVALIDATED: these are cross-platform CI + registry-credential pipelines that
have not been run; they need a CI iteration and the documented repo secrets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the loose end where only Python had an automated codegen test (Go/Ruby/TS
were verified by hand). Each SDK now runs the full pipeline in its native runner:
secretspec schema -> quicktype -> typed deserializer over the SDK's fields().

- Schema emitter reworked to a single-root object (the union by default, or a
  profile's fields via `schema --profile`). quicktype only emits a converter for
  the ROOT type, so the previous Manifest wrapper / $ref root gave JS/Go no
  usable `toSecretSpec`/`UnmarshalSecretSpec` and mis-named the type. Pair with
  `quicktype --top-level SecretSpec`.
- New e2e tests: Go (temp module, UnmarshalSecretSpec(FieldsJSON)); Ruby
  (dry-struct, from_dynamic!(fields)); Node (quicktype --lang javascript,
  toSecretSpec(fieldsJson())); Python updated to --top-level. All gated on npx.
- ci-sdks.sh runs all Ruby test files; CLI docs + SDK pages + CHANGELOG updated
  for --top-level and --profile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SDK section jumped straight into per-language pages with no explanation of
how the polyglot stack works. Adds an Overview page (one Rust resolver, thin
clients over the C ABI / napi addon, the shared runtime API and error model,
typed access via schema+quicktype, and the bundled-library distribution model)
and wires it as the first item in the SDK sidebar. Docs site builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The landing page only showcased the Rust SDK. Adds a "Use it from any language"
showcase section after it, with the shared builder API in Python, Node.js, Go,
and Ruby, plus the one-resolver/thin-client framing and links to the SDK
overview and the schema+quicktype typed-access path. Landing page builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The landing page had a standalone "Compile-time secrets in Rust" section
adjacent to the new "Language SDKs" section, so Rust read as separate from the
SDKs when it is one of them. Folded the Rust main.rs example into the single
Language SDKs section as its compile-time highlight, after the Python/Node/Go/
Ruby snippets. One coherent SDK section; landing page builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Per-profile JSON Schema (`schema --profile <p>`) now allows additional
  properties: `resolve --profile <p>` returns the profile's own secrets plus
  those inherited from `default`, which the per-profile type intentionally does
  not list (matching the derive macro), so a strict quicktype deserializer would
  otherwise reject a valid resolve result. The union schema stays exhaustive.
- `resolve_json` now catches panics itself, so both native boundaries that
  funnel through it (the C ABI and the napi-rs Node addon) return the same
  `{"ok":false,"error":...}` envelope on an internal panic.
- `secretspec::codegen` exposes one shared `capitalize`, used by both the schema
  emitter and the derive macro (was a byte-identical copy in each), so profile
  type-name casing can never drift.
- `build_ir` computes the union field set in a single pass instead of
  re-scanning every profile per field; `validate`/`resolve` resolve each
  secret's merged config once per pass instead of twice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Load nil-checks the response and validates `schema_version` against the
  version this SDK was built for, so a skewed library is reported rather than
  nil-panicked or silently misparsed.
- SetAsEnv skips secrets with no usable value (e.g. under no_values) instead of
  exporting an empty string, via a new `usable()` helper.
- extractEmbedded uses an owner-only (0o700) temp dir and reuses the cached
  cdylib only when its content hash matches, not just its size, closing a
  predictable-path load and a stale-file reuse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- _resolve_response checks the response is present and validates schema_version,
  raising SecretSpecError on mismatch instead of KeyError / silent misparse.
- _load uses double-checked locking so concurrent first callers do not race to
  dlopen.
- Dropped the divergent `source = "provider"` default (other SDKs pass it
  through); added with_no_values to the builder for parity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- set_as_env! skips secrets with no usable value instead of `ENV[name] = nil`,
  which would delete the variable.
- load nil-checks the response and validates schema_version, raising
  Secretspec::Error on mismatch.
- ensure_loaded guards the one-time dlopen with a Mutex and re-checks @loaded
  inside the lock.
- Added with_no_values to the builder for parity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- load nil-checks the response and validates schema_version, throwing
  SecretSpecError on mismatch.
- setAsEnv skips secrets with no usable value instead of coercing null to the
  string "null".
- Added withNoValues to the builder (and index.d.ts) for parity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
uname under git-bash/msys reports MINGW*/MSYS*/CYGWIN*; map those to
secretspec_ffi.dll so the cross-language conformance gate can run on Windows,
where the FFI artifact already ships.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
validation_report_provider_uri returned the override and per-secret alias
URIs verbatim, and this branch newly serializes that into the provider field
of the resolution report (check --json/--explain) and the resolve response
(resolve --json, every SDK's response.provider). A user-authored alias or
--provider override embedding a credential (vault+token:s3cr3t@host,
vault://host?token=...) therefore leaked into machine-readable output and
across the FFI boundary, even though the sibling source_provider and the warn
path already redact it.

Route both raw returns through redact_uri_strict; add a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The napi resolve binding was synchronous, so resolving from a network-backed
provider (1Password, LastPass) blocked the Node event loop for the whole
round-trip. Add a resolveAsync binding that runs resolve_json on the libuv
threadpool (napi AsyncTask) and a Builder.loadAsync() that awaits it. The
synchronous load() is unchanged; loadAsync() reports a clear error against an
older addon that lacks the binding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When SECRETSPEC_FFI_LIB is unset, the Go, Python, and Ruby SDKs walk up to a
Cargo target/ directory to find the library. They preferred release over
debug, so a stale release build silently shadowed the debug build a developer
had just produced (surfacing later as a confusing schema-version mismatch).
Within the nearest target/, pick the candidate with the newest mtime instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
package.json ships no prebuilt addon and has no per-platform publish wiring
(the CI workflow flags this as a follow-up), so the "npm install needs no
native build" claim in the changelog and SDK docs was unbacked. Reword to say
the addon is built from the Rust core via scripts/build-addon.sh and that
prebuilt per-platform npm packages are a follow-up. Also record the credential
redaction, loadAsync, and cdylib-discovery fixes under Unreleased.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
no_values now routes through a new Secrets::resolve_without_values, which never
exposes a secret value or persists an as_path temp file, so no secret byte
crosses the boundary and as_path resolution leaves nothing on disk. Previously
the resolver fully materialized every value (and persisted every as_path temp
file) and only then stripped them.

Adds Secrets::report() and a mode:"report" request on the shared resolve_json
boundary: a value-free ResolutionReport (per-secret status and provenance) that,
unlike resolve, reports a missing required secret as a status rather than failing
the call. This is the inventory/preflight view the CLI exposes as check --json,
now reachable from every language SDK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Go Fields()/FieldsJSON() now emit JSON null for a value-less secret instead of
  the empty string "", matching Python/Ruby/Node; Fields() returns
  map[string]*string and a new Usable() distinguishes absent from empty. Node
  index.d.ts types fields() as Record<string, string | null>.
- Every SDK gains a cleanup affordance for the persisted as_path temp files: Go
  Resolved.Close(), Python close()/context manager, Ruby close()/load block,
  Node dispose()/Symbol.dispose.
- Every SDK gains report() (Node also reportAsync()) over the value-free report,
  which never fails on a missing required secret.
- Conformance gains no_values and report dimensions (the latter asserts
  source_provider presence), locking the cross-language contract so a divergence
  like the Go ""-vs-null one cannot ship again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A thin client over the secretspec-ffi C ABI, linked at build time via the GHC
FFI. Mirrors the other SDKs: a builder (withPath/withProvider/withProfile/
withReason/withNoValues) plus load/report, returning a Resolved
(get/fields/fieldsJson/setAsEnv/close) or a value-free Report. A missing required
secret throws MissingRequiredError; other failures throw SecretSpecError with a
stable errorKind. as_path secrets come back as a readable file path.

Wires GHC into devenv, the cross-language conformance runner (all three
dimensions), and ci-sdks.sh; adds a schema -> quicktype -> typed codegen e2e test
(quicktype's Haskell target); and a Haskell SDK CI workflow that builds/tests on
PR and publishes to Hackage on a version tag. Adds docs (SDK page, sidebar,
overview, landing) and README entries. The cdylib is linked at build time
(--extra-lib-dirs) and must be on the runtime loader path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The value-free surfaces (Secrets::report(), resolve_without_values(), the FFI
no_values/report requests, and check --json/--explain) ran the full resolution,
so they minted and stored a brand new secret in the provider for any declared
generate secret that was not yet set, and failed outright on a read-only
provider. Thread a Materialize flag through validate_audited so those entry
points share the identical resolution logic but skip its two side effects: a
generatable-but-absent secret is reported as it would resolve (generated)
without being created, and no as_path secret is written to a temp file.

Also: a per-secret provider chain whose primary provider errors and whose
fallback chain has no value now surfaces that provider error, exactly as a
single-provider failure already did, instead of silently reporting the secret
as missing_required, so machine consumers can tell an outage from an
unprovisioned secret.

Route check --json/--explain through report() (removing a duplicated inline
copy of its construction).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three robustness fixes plus a distribution correction:

* Embedded (-tags embed_lib) cdylibs are extracted into a per-user, owner-only
  cache directory (os.UserCacheDir) whose privacy is verified before use,
  instead of a predictably named directory under the shared system temp dir.
  This closes a local attacker file swap (TOCTOU) that could run attacker code
  in the process on a shared host, and avoids noexec temp mounts. An embedded
  git LFS pointer (from a botched release) is rejected with a clear error rather
  than fed to dlopen.

* A missing symbol in the loaded library no longer panics: purego.RegisterLibFunc
  panics are recovered and returned as a load error, so an incompatible cdylib
  does not escape sync.Once and leave the loader poisoned with nil pointers.

* A zero-value Builder (var b Builder, not via New()) no longer panics with a
  nil-map write in its WithX setters; the request map initializes lazily.

Distribution moves to the system library model: git LFS plus the module proxy
cannot ship a working library (the proxy serves LFS pointer text), so it is no
longer prescribed. Consumers provide the cdylib via SECRETSPEC_FFI_LIB or vendor
it for an embed_lib build. RELEASE.md, .gitattributes, .gitignore, the workflow,
README, and docs updated accordingly.

Tests release as_path temp files so repeated runs leave no secret files behind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
callNative copied the response out and freed it in straight-line IO, so an
asynchronous exception (e.g. a System.Timeout.timeout around load/report)
arriving between the call returning and the free leaked the native, secret
bearing response buffer. Install the free under mask and run it via finally so
it always executes.

The conformance test now closes its value-carrying Resolved so as_path temp
files do not accumulate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The value-carrying as_path and conformance tests in the Python, Ruby, and Node
suites resolved the as_path fixture but never disposed the result, so each run
left another 0400 secret-bearing temp file behind (only the no_values variants
cleaned up). Close/dispose the result, matching the no_values tests: Python via
try/finally close(), Ruby via the block form of load that closes in ensure, and
Node via try/finally dispose().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The value-carrying ResolveResponse stays the SDK boundary, but only over
the secretspec-ffi C ABI. Not shipping it as a CLI verb keeps the
command surface verb-level auditable: check never prints a value, get
prints exactly one (per-key audited and reason-gated), and bulk value
extraction never becomes a pipeable plaintext artifact. Adding the
subcommand back later is backwards compatible; removing it after people
script against it would not be.

Secrets::resolve()/resolve_without_values()/resolve_json(), the FFI
crate, the SDKs, and the committed JSON Schema are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pin the Rust version in a single rust-toolchain.toml consumed by both
devenv (languages.rust.toolchainFile) and the native CI runners via
rustup, so released artifacts build with the same compiler CI tests
against.

Scope the artifact workflows (ffi, node, python, ruby, go) to PR changes
in their own directories; core resolver changes are already verified on
PRs by the devenv-based test.yml and sdks.yml, and the full matrices
still run on tags and manual dispatch.

Install devenv in the Claude review workflow and allowlist devenv
commands so the reviewer can build and run tests instead of reviewing
blind.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SDKs job fills the ~14GB free on a hosted ubuntu runner (Nix store +
full debug build with the AWS/GCP/Bitwarden provider stacks) and dies
with ENOSPC, so drop the preinstalled toolchains we never use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same ENOSPC as the SDKs job: the cdylib + CLI debug build with the full
provider stacks plus the Nix store overflows the hosted runner's disk,
this time so badly the runner could not even write its own logs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The macos-13 label is no longer provisioned by GitHub, so every
x86_64-apple-darwin artifact job sat queued forever while the rest of
the matrix passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three Windows breakages surfaced by the first complete artifact CI runs:

- Provider specs like dotenv://C:\path\.env failed with "invalid port
  number" because C: parsed as a URL host:port. Drive-letter paths are
  now carried in the URL path component (forward-slash separators) and
  the dotenv provider strips the URL's leading slash from them.
- The Go SDK never compiled on Windows: purego.Dlopen and RTLD_* exist
  only on Unix. The library open is now split into build-tagged files,
  using syscall.LoadLibrary on Windows.
- The Node codegen test spawned npx, which is npx.cmd on Windows and
  unreachable without a shell.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Go SDK segfaults in the purego call path on x86_64 darwin and Apple
has moved the platform on; stop building Intel macOS artifacts (FFI
cdylib, wheels, gems, Node addon, Go embed lib) rather than debugging a
dying target. Intel mac users can still build from source via the
system-library path.

Pin the remaining macOS runners to macos-latest instead of macos-14 so
the next image retirement does not silently strand jobs in the queue
like macos-13 did; artifact compatibility comes from rustc's deployment
target, not the runner OS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
node --test runs the three test files in parallel processes; each one's
ensureAddon() kicked off build-addon.sh when secretspec.node was absent,
and the final `cp` truncated the addon in place while a sibling process
could already have it mapped, killing it with SIGBUS (seen once in the
SDKs workflow). Install via temp file + rename so an existing mapping
keeps its inode, and build once up front in ci-sdks.sh so the test
processes never race to build at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Export SECRETSPEC_FFI_STATICLIB / _INCLUDE / _NATIVE_LIBS (the archive's
transitive system libs, captured from `rustc --print native-static-libs`, never
hardcoded) from ci-sdks.sh and conformance/run.sh, so each SDK can statically
link libsecretspec_ffi.a instead of dlopening the cdylib.

Pin the musl targets in rust-toolchain.toml and wire the musl cross-toolchain
into devenv by absolute path in env (NOT in packages, which would inject
musl-static libdbus into the host NIX_LDFLAGS and corrupt the glibc build). Add
setuptools to the Python venv (cffi needs it on 3.12+); drop the unused maturin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stage libsecretspec_ffi.a alone on --extra-lib-dirs (so -lsecretspec_ffi resolves
to the archive, not the co-located .so) and pass its native deps via
--ghc-options=-optl. The Rust resolver is embedded in the binary, so no runtime
loader path is needed. SecretSpec.hs is unchanged (ccall safe is linkage-agnostic).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switch cffi from ABI/dlopen mode to out-of-line API mode (_build_ffi.py): compile
a CPython extension that statically links libsecretspec_ffi.a, reusing the existing
secretspec.h. No bundled cdylib, no SECRETSPEC_FFI_LIB, no runtime discovery. The
extension targets the limited API (py_limited_api), so one cp39-abi3 wheel per
platform serves all CPython >= 3.9. Building a wheel now needs a Rust toolchain
and a C compiler.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the Fiddle/dlopen binding with a thin mkmf C extension (ext/secretspec)
that statically links libsecretspec_ffi.a and exposes the C ABI to Ruby. Nothing
to locate at runtime, no SECRETSPEC_FFI_LIB. Distributed as a platform gem that
bundles the prebuilt archive + header + native-libs manifest; gem install
compiles only the ~40-line C glue, so one platform gem serves every Ruby ABI.
Install now needs a C compiler + Ruby headers (+ libdbus).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Node SDK already statically embeds the resolver in its napi-rs addon; the
README still described the old koffi/dlopen/SECRETSPEC_FFI_LIB model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refactor the binding behind hooks (ensureLoaded/nativeResolve/nativeABIVersion):
binding_purego.go (!static) keeps the default dlopen path, binding_cgo.go (static)
links libsecretspec_ffi.a in via cgo. On Linux the archive is built for musl and
combined with -extldflags -static for a fully-static executable. The default
go get path is unchanged (purego, no cgo, toolchain-free), so static is strictly
additive. stage-staticlib.sh stages the archive + header + generated cgo LDFLAGS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Windows cargo-test job (added on main) reads the golden JSON via
include_str!, which on Windows is checked out with CRLF while serde_json
emits LF; normalize both sides so the wire-format assertion is
line-ending agnostic.

Also sync the lockfile: the rebase bumped the workspace to 0.12.1 but
left secretspec-ffi and secretspec-node-native at 0.12.0 in Cargo.lock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Go fully-static smoke test aborted under `set -o pipefail`: `ldd` exits
non-zero on a static binary, and the pipe propagated that even though grep
matched, so the binary never ran. Capture ldd's output first, then assert it
reports no dynamic dependencies.

Drop the Windows legs from the Ruby gem and Python wheel matrices: both static
linkers assume the Unix `lib<name>.a` archive, but Windows uses MSVC `<name>.lib`
(Python) or needs the `x86_64-pc-windows-gnu` target (MinGW Ruby). Proper
Windows packaging is a documented follow-up. SDK behavior stays fully covered on
every PR by the devenv-based sdks.yml; these workflows only build release
artifacts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each language SDK duplicated the envelope validation (ok check, null
response check, schema version check) between its resolve and report
code paths. Extract a single parametrized helper per SDK, keyed on a
resolve/report label that selects the schema version and labels the
mismatch message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace checkEnvelope, which validated pre-decomposed fields and returned
only an error, with a generic parseEnvelope that owns the unmarshal and
returns the inner response. This collapses the two near-identical envelope
types into one generic envelope[R] and removes the var schemaVersion *int
nil-pointer dance from both Load and Report, bringing Go in line with the
Node, Python, and Ruby SDKs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Ruby SDK is a statically-linked native C extension now, not Fiddle;
correct that across the README, Ruby SDK page, and SDK overview, and add
Haskell to the SDKs CI comment.

Rewrite the Unreleased changelog at a user-facing altitude: consolidate the
five language SDKs into one entry, keep the new schema/check --json/ffi
surfaces, and drop the intra-release implementation churn (bugs fixed in code
that is new this cycle and never shipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(import): resolve provider aliases
PR #113 fixed `import` by expanding its source alias at the call site.
Generalize that fix: resolve provider aliases inside build_provider
itself, the single point all in-module provider construction funnels
through, so no future caller can reintroduce the #112 class of bug by
forgetting to expand an alias. The import call site and get_provider
drop their now-redundant resolve_provider_spec calls; resolution is a
no-op on already-resolved URIs, so the per-secret chain paths are
unchanged.

Report the defined aliases when a bare token matches neither a built-in
provider nor an alias, so a mistyped import source gets the same
guidance the per-secret provider chain already gives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pass has no CLI flag for the store location; PASSWORD_STORE_DIR is the
only mechanism. The pass provider now accepts a store_dir query parameter
(e.g. pass://?store_dir=/path/to/store), applied as PASSWORD_STORE_DIR
scoped to each pass invocation without mutating secretspec's environment.

Closes #115

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(pass): support custom store directory via store_dir URI param
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
actions: bump cachix-action pinned version to 17
- go: verify the euid-scoped base dir in the world-writable /tmp fallback,
  not just the content-addressed leaf, so an attacker who pre-creates the
  predictable base cannot rename secretspec-ffi/ between extraction and
  dlopen. extractBaseDir now flags whether the base needs verification, so
  the 0755 ~/.cache primary path is left untouched.
- go: nil-guard Builder.req before marshaling so a zero-value Builder.Load()
  sends {} instead of the null that serde rejects.
- ruby: run secretspec_resolve under rb_thread_call_without_gvl (copying the
  request first), so network-backed providers no longer freeze other threads.
- haskell: set empty-string secrets via System.Environment.Blank.setEnv so a
  value of "" is exported rather than unset, matching the other SDKs.
- scripts: strip the semver prerelease/build suffix before writing cabal's
  PVP-only version: field.
- rust: reattach try_generate_secret's rustdoc and drop the stray
  Windows-path comment misplaced on provider_from_url.
- docs: refresh SDK docs, RELEASE.md, the staging script, the schema
  description, and the conformance README for the final static-link /
  no-LFS / FFI-only-contract design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the cffi/setup.py build (a statically-linked CPython extension over
libsecretspec_ffi.a) with a pyo3 extension crate (secretspec-py-native) that
calls secretspec::resolve_json directly, mirroring secretspec-node's napi-rs
crate. Sidesteps maturin's own cffi-bindings mode, which builds a cdylib
loaded via dlopen at runtime -- the opposite of what the static-link design
was for. Static linking, no separate library to ship, and cp39-abi3 wheels
are all preserved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
#	secretspec/src/provider/mod.rs
Ruby: switch RubyGems auth from a stored RUBYGEMS_API_KEY to Trusted
Publishing (OIDC) via rubygems/configure-rubygems-credentials.

Node: adopt @napi-rs/cli for the per-platform optional-package npm layout
(secretspec-<platform> packages referenced via the main package's
optionalDependencies), replacing the hand-rolled single-addon build. Wire a
real npm publish job using npm Trusted Publishing (OIDC) -- node-addon.yml
previously only built and uploaded the addon as a CI artifact, with no
publish step at all.

RELEASE.md gains a 'Before your first release' walkthrough covering the
one-time bootstrap each registry actually needs (PyPI/RubyGems support a
pending publisher configured ahead of time; npm has no such mechanism and
needs a manual first publish before Trusted Publishing can be attached;
Hackage has no OIDC support at all yet). Also fixes a few passages that had
drifted from the maturin+pyo3 Python conversion and adds the Haskell section
that was missing entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
node-addon.yml has an explicit npm ci step; ci-sdks.sh (the devenv-based SDKs
workflow) never gained one when build-addon.sh switched to napi build, so it
failed with the napi binary missing from node_modules/.bin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RubyGems: rubygems/configure-rubygems-credentials has no role-to-assume
requirement for the standard flow -- that input is for an unrelated advanced
use case. RubyGems matches trusted publishers by repo + workflow filename +
environment only, so ruby-gems.yml now sets environment: release (matching
the pending publisher's Environment field) instead of a fabricated role ID.

Node: node-addon.yml's publish job was missing contents: read and an explicit
registry-url on actions/setup-node, both present in npm's own documented
Trusted Publishing example.

Also adds npm-bootstrap.yml, a one-time workflow_dispatch-only workaround for
npm's lack of a "pending publisher" mechanism (unlike PyPI/RubyGems): it
builds all 4 platform addons fresh and publishes all 5 npm packages using a
temporary NPM_BOOTSTRAP_TOKEN secret, so that token never has to pass through
local tooling. Delete this file once the bootstrap publish succeeds and
Trusted Publishers are configured for all 5 packages.

Also syncs SDK package versions to the current workspace version (0.12.2).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Node 22 (the plain nixpkgs default) bundles npm 10.x, which mishandles npm
Trusted Publishing's OIDC handshake and can even misreport a brand new
package's first publish as a 404 (hit this live while bootstrap-publishing
the Node SDK's npm packages). CI already worked around this with an explicit
npm upgrade step; this makes the local devenv shell match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
All 5 npm packages (secretspec + the 4 platform packages) are now published
and each has a Trusted Publisher configured, so npm-bootstrap.yml has served
its purpose and is removed. package.json's optionalDependencies (added by an
earlier napi pre-publish run) and package-lock.json are now committed since
the packages they reference actually exist. RELEASE.md's npm section updated
to "already done", matching crates.io.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The lockfile's optionalDependencies entries were bare unresolved stubs
(no version/resolved/integrity) because they were generated by npm install
before secretspec-linux-x64-gnu/-linux-arm64-gnu/-darwin-arm64/-win32-x64-msvc
existed on the registry. npm ci rejected this as out-of-sync in CI (Node
addon, SDKs workflows). Regenerating now that all 4 packages are actually
published resolves them properly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
go run's ephemeral temp-dir binaries intermittently lack a proper Mach-O UUID
load command on macOS, which newer dyld rejects ("missing LC_UUID load
command", abort trap). This is a pre-existing, unrelated flake in
go-embed.yml's darwin_arm64 job (reproduced across several unrelated commits
going back before this session's work) -- an explicitly built binary doesn't
hit this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The go build + exec swap from the previous commit didn't fix it -- the same
dyld abort happened on an explicitly built binary too, which ruled out
go run specifically as the cause. The actual root cause is a known Go
linker bug: Go's internal linker doesn't emit a Mach-O LC_UUID load command,
and macOS 15+ dyld refuses to load binaries without one. -linkmode=external
forces the system linker, which does emit it. Valid here since this job
builds natively on macos-latest, not cross-compiling.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add language SDKs (Python, Ruby, Go, Node, Haskell) over a JSON-over-FFI core
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The napi-generated per-platform packages inherited an empty repository.url,
so npm Trusted Publishing rejected them with a 422 provenance error. Point
the package at the GitHub repo and let the publish job run on workflow_dispatch
so a single SDK can be re-published without re-tagging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cargo-dist defaulted to a `cargo build --workspace` release build, which now
compiles the pyo3 `secretspec-py-native` extension. That cdylib does not link
standalone on macOS (undefined _PyExc_* symbols), failing the macOS release
build. precise-builds builds only the `secretspec` app package by package.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A secret can name one externally managed secret by its store's own
coordinates, replacing SecretSpec's {project}/{profile}/{key} naming
for that secret:

    [profiles.production]
    DATABASE_URL = { description = "...", ref = { item = "db", field = "password" }, providers = ["prod_op"] }

`item` is required; `field` (1Password field label, Vault KV field,
AWS JSON key, keyring account), `vault`/`section` (1Password), and
`version` (GCSM) refine it where the store supports them. All 11
providers resolve refs and reject coordinates they have no equivalent
for. A string ref (e.g. a pasted op:// URI) fails with the exact table
translation to write instead, and provider URIs stay store addresses
only (item paths on them error the same way).

The coordinates supply naming only; routing follows the same rules as
every secret (the --provider/SECRETSPEC_PROVIDER override, the
secret's providers chain, defaults), so refs compose with fallback
chains and an override can redirect them, e.g. at a dotenv fixtures
file during tests. Chain entries may be inline scheme:// URIs without
declaring an alias. Writes are symmetric where the backend allows it:
`set` and `check` prompting write through the coordinates in place
(1Password, keyring, pass, dotenv, bws, Proton Pass, LastPass); Vault,
AWS SM, and GCSM refs are read only. ref cannot be combined with
generate.

Internally the Provider trait speaks one address vocabulary: every
operation takes an Address value (Convention{project, profile, key}
or Native coordinates), each provider compiles the convention into
its native coordinates via a new convention_address method, and reads
resolve both forms through the same coordinate path. A provider
declares the ref coordinates it honors with supported_coords and the
shared resolver rejects the rest, so a store whose secrets have no
sub components gets that for free; check_writable replaces allows_set,
returning the reason a write is refused rather than a bare false (a
store writable through the convention layout no longer reports itself
read only when it declines a ref). Batch reads take addresses too
(get_many replaces the convention-only get_batch), so refs batch
through a store's bulk surface where it has one (AWS
BatchGetSecretValue, the single bws/Proton Pass/1Password listings)
and otherwise fetch concurrently, identical coordinates once. CLI
auth probes (1Password, LastPass, Proton Pass) are shared per
account/session across instances; audit events record the coordinates
in a new `ref` field; and manifest validation now runs on every load.
Docs cover the ref table, per provider translation, and routing
semantics across the configuration/CLI/provider references.

Closes #64

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Config validation now runs on every load (including `secretspec schema`),
so each codegen fixture manifest needs a description on every secret.
Covers the Go, Ruby, Node, and Haskell suites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Secret References concept page under Concepts, grouped next to
Secret Generation in the sidebar, and trim the configuration reference
section to pure specification that points at it. Feature `ref` in a
landing-page showcase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A `ref` names where a secret lives; `generate` mints an initial value and
sets it there when missing. The two are orthogonal, so drop the validation
that rejected the combination.

Generation already resolves the secret's address, so a generated value is
written to the ref coordinates automatically; the write is now attributed to
the ref in the audit log too. Docs no longer call the combination out as a
special case, since it just works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the 0.14 release post to the docs blog: what `ref` is, why it takes
provider-independent coordinates instead of a pasted store URL, how routing
and write-through work, and the upgrade notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: native secret references (`ref`) as provider independent coordinates
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
npm 12's in-place global self-install leaves libnpmpublish's bundled
sigstore dependency missing, breaking `napi pre-publish` provenance
with "Cannot find module 'sigstore'". Pin to npm@^11.5.1, which still
satisfies the Trusted Publishing minimum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Isolate the "decide what to do" half of secret resolution into a new
`plan` module (slice 1 of #124). `Secrets::build_plan` merges the
profile, computes each secret's effective config, resolves its address
(native `ref` coords or convention naming) and provider route, and
groups secrets by primary store — all without touching a provider.
Batch resolution consumes the plan through an executor that derives
nothing itself, and `get`, `set`, the prompt loop, and `import` plan
through the same per-secret deriver, so single-secret and batch
decisions cannot drift. Planning is unit-testable without any provider.

Behavior fixes along the way:

- A `providers` fallback chain is tried strictly in order: each link is
  resolved only when a read reaches it, and a broken link (an undefined
  alias, an unreachable store) is skipped with a warning so a working
  provider later in the chain still answers.
- A `ref` routed at a single store is checked up front against the
  store's supported coordinates, failing fast instead of at fetch time;
  a multi-store chain still validates per store as it is walked.
- Chain entries accept bare provider names and `scheme:path` shorthand,
  the same specs `--provider` accepts, and a `1password` misspelling
  reaches the parser's "use onepassword instead" correction.
- `get` and `set` record an audit Error event when routing fails to
  resolve, matching the batch path; `import` prints its summary in
  sorted name order.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Drop PlannedAddress: a planned secret's address is derived from its
  config and name on demand. Collapse Route to a primary+fallback struct
  (an override is just a single-entry chain) and derive primary-store
  groups from the routes instead of storing index lists.
- Fold the single-store ref coordinate check into execute_plan, running
  it on the group providers the executor already builds, right before
  the fetches spawn: same fail-before-any-store-is-contacted guarantee,
  one construction, one failure policy.
- Build the plan from the sorted names validate already computes for
  audit keys, and let spec_names_known_provider raise the corrective
  1password error itself so a misspelled chain primary fails up front
  like any other invalid primary.
- Route every get/set failure through record_key_error (now carrying
  the native ref coordinates), drop the dead by-value Profile iterator,
  and stop re-sorting import's already-sorted summary names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
core: split secret resolution into a pure plan and an executor
A thin PHP client (`cachix/secretspec`) that mirrors the other language SDKs'
builder API and resolves secrets through the one Rust core, so it inherits
every provider with no PHP-side logic. It reaches the resolver over the shared
JSON envelope through two native backends, preferring whichever is available:

- the `secretspec-php-native` extension (ext-php-rs), which embeds the resolver
  and works under PHP-FPM with no `ffi.enable`, like `ext-redis`; and
- a runtime `ext-ffi` fallback that dlopens the `secretspec-ffi` library.

The Composer manifest is the repo-root `composer.json` (so Packagist can read
it straight from the monorepo) with `vendor-dir` pointing into `secretspec-php/`
so the tooling stays there. `vendor/bin/secretspec-install-lib` fetches the
ext-ffi library on demand. Covered by phpunit (both backends) plus the shared
cross-language conformance fixtures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire PHP into conformance/run.sh and scripts/ci-sdks.sh so every push exercises
it against a freshly built resolver: the cross-language conformance fixtures via
the ext-ffi backend, and the full phpunit suite under both the ext-ffi fallback
and the native extension.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Publish the client straight from the monorepo — Packagist reads the repo-root
composer.json, so there is no split/mirror repo, workflow, or token. php-ext.yml
builds prebuilt extension binaries per PHP minor x platform and attaches them to
the release; ffi-build.yml also attaches the per-target secretspec-ffi library
(with a sha256) that the SDK's install-lib command downloads. RELEASE.md
documents the one-time Packagist registration and the known gaps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the PHP SDK doc page (quick start plus Laravel, Symfony, and plain-PHP
integration, typed access, and the two native backends), and list PHP in the SDK
overview, the docs sidebar, the landing page, and the README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `cargo test --all` Windows job tried to build secretspec-php-native, which
ext-php-rs cannot build on the bare runner (no PHP dev toolchain, and it rejects
the runner's PHP version); exclude that crate there — the PHP SDK is covered by
sdks.yml. Fix php-ext.yml's matrix so php x target actually cross-products (the
old form only produced Windows jobs), and scope it to Linux + macOS; a Windows
extension build is deferred to a follow-up (Windows users have the ext-ffi
backend), noted in RELEASE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The macOS linker errors on the undefined Zend symbols a PHP extension leaves for
dlopen time, failing the extension build (ld: symbol(s) not found for arm64). Add
a build.rs that passes `-undefined dynamic_lookup` on macOS only (the flag pyo3
and ext-php-rs need); Linux/ELF is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an akv:// provider backed by Azure Key Vault, gated behind a new akv Cargo feature (included in default features alongside gcsm/awssm/vault/bws). Convention secrets map to Azure Key Vault's [0-9a-zA-Z-] secret-name charset via secretspec--{project}--{profile}--{key}, with underscores rewritten to hyphens. Authentication is chosen via ?auth=env|cli|managed_identity|workload_identity, defaulting to a service principal from AZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRET and falling back to the signed-in Azure CLI session.

Version pinning and user-assigned managed identity are not yet supported. Updates docs (new providers/akv.md page plus the other tracked locations) and CHANGELOG.md.
Capture the ordering the live Packagist + release-asset paths need (merge,
register, dev-main smoke, tag, then verify both backends against the real
release) — the steps CI cannot exercise until a tag exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Give Native one authority for the platform cdylib file name and have the
ext-ffi installer reuse it, so the downloaded copy and the loader can no
longer drift. Collapse the list-of-one library-name lookup, borrow the
request JSON as &str across the native boundary, and factor the repeated
builder wiring and request-field setters into shared helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add kms_key_id and tag.NAME=VALUE query parameters to the awssm URI,
e.g. awssm://prod@us-east-1?kms_key_id=alias/my-key&tag.team=platform.

Both are applied only when secretspec creates a secret (CreateSecret);
PutSecretValue accepts neither, and a pre-existing secret keeps the key
and tags it was created with. This unblocks accounts that enforce a
customer-managed KMS key or a "tag-on-create" guardrail, where an SCP or
IAM condition denies CreateSecret unless required aws:RequestTag/* tags
are present in the same call.

Tags are stored in a BTreeMap so uri() reconstructs them in a stable,
sorted order for the audit log.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add PHP SDK (Laravel, Symfony, plain PHP)
Removed unnecessary dollar sign from curl command.
Fix curl command formatting in README
- azure_identity::ClientSecretCredential::new expects tenant_id: &str and
  an azure_core::credentials::Secret, not the secrecy::SecretString used
  elsewhere in this provider; pass &tenant_id and construct a Secret.
- provider/mod.rs was missing the closing brace for the url_tests module.
Added updated Cargo.lock file
std::env::vars() panics on any non-UTF-8 entry, and environment variables
are arbitrary bytes on Unix. Capture the child environment with vars_os()
and keep it as OsString end to end, so non-UTF-8 parent variables are
passed through to the child untouched instead of aborting the process.
Resolved secrets are overlaid on top, overwriting same-named variables.

Fixes #140

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CDSY4yJXvof9Am8acxzHf
fix: don't panic in run when the environment contains non-UTF-8 variables
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
feat(awssm): support KMS key and tags on secret creation
Build the Linux addons inside manylinux_2_28 containers with libdbus
compiled in statically (new vendored-dbus cargo feature), so the
published addon loads on any distro with glibc >= 2.28 and needs no
system libdbus (issue #136). The container also gets dbus-devel because
the SDK test suite rebuilds the CLI with default features. A post-build
step fails the job on portability regressions, checking the version
reference table so file-level ABI markers like GLIBC_ABI_DT_RELR are
caught alongside per-symbol glibc version needs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the field-level profile merge into Secret::resolved, shared by
resolve_secret_config and Config::validate, so validation checks the
same effective config resolution acts on: overrides inherit description
and type from the default profile, while conflicts that only appear
after merging (generate plus an inherited or defaults-supplied default)
are rejected at load instead of silently generating a random value.
required=true plus an inline default stays a raw-entry rule so an
override may still supply a default for a secret the default profile
requires. Profiles validate default-first in sorted order, so errors
are attributed deterministically to the profile declaring the offending
field, and check/run list secrets in stable name-sorted order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post-review cleanup, no behavior change:

- Config::validate checks the default profile explicitly before the
  plain-sorted rest, instead of encoding "default first" in a tuple
  comparator plus a per-iteration branch
- Secret::resolved merges field by field through one inherit helper
  instead of two near-duplicate match arms
- drop the dead Profile::validate wrapper (validated profiles as
  standalone, contradicting override inheritance; zero callers)
- share the sorted name union via Profile::sorted_secret_names_with;
  check/run listings use a new effective_secrets helper instead of
  deep-cloning a merged profile just for its key set, and plan.rs
  reuses effective_secret_config instead of an inline resolve+expect
- CI portability check reads objdump output once; docs field notes
  replace four levels of footnote asterisks
- test cleanups: drop a duplicate fixture, rebuild the config inside
  the determinism loop so each iteration gets a fresh hash seed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- is_not_found_error now checks the typed HTTP status (e.http_status() ==
  Some(StatusCode::NotFound)) instead of string-matching the error message,
  which missed genuine 404s.
- AZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRET: blank values are now
  treated as unset, and a partially-specified principal errors (naming the
  missing var) instead of silently falling back to the Azure CLI session.
- Project/profile/key components that sanitize to a '--' (a literal '--' or
  a '__') are rejected, since they'd be indistinguishable from the
  project/profile/key delimiter and could collide with another secret.
- Native ref items are now validated against Azure's secret-name charset
  (but never rewritten) before any network I/O.
- Added an explicit ?suffix= query param for sovereign clouds, alongside the
  existing dotted-hostname form; AkvConfig tracks it so uri() round-trips.
- AuthMethod::as_str()/FromStr replace the duplicated auth-method mappings
  in TryFrom and uri().
- provider/tests.rs: added an akv arm to the generic integration harness
  (gated on AKV_TEST_VAULT), instead of panicking via the generic fallback.
- Docs: fixed the misleading dotenv-import example on the akv provider page,
  and the managed identity wording in reference/providers.md to say
  system-assigned, matching the provider page.
Validate secrets on their merged effective config; manylinux node addons
Introduce a per-provider "bootstrap env overlay" — an in-memory
HashMap<String, SecretString> injected at construction — that credential
reads consult after the process environment. This is the delivery
mechanism for letting a provider's own credentials come from another
provider, without ever calling std::env::set_var (which would leak them
into the child environment of `secretspec run`).

The overlay is handed to the concrete provider value inside the
registration factory, before any Arc/Box wrapping, because a &mut self
hook cannot be forwarded through the blanket impl Provider for Arc<T> —
a preflight provider wrapped as Box<Arc<P>> would otherwise silently
receive the default no-op.

bws and vault consult the overlay lazily via a shared env-wins helper;
onepassword fills its existing service_account_token field when unset.
The overlay is empty everywhere in production for now, so every path
behaves exactly as before; a test proves factory injection reaches the
preflight-wrapped onepassword provider.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Change the provider-alias map value from a bare String to a ProviderAlias
struct carrying the URI plus an optional bootstrap-credential `env` map.
This lets an alias declare which environment variables the provider needs
(e.g. an access token) and where to source them, the config surface for
letting a provider's credentials come from another provider:

    [providers]
    keyring = "keyring://"
    bws = { uri = "bws://project-uuid", env = { BWS_ACCESS_TOKEN = "keyring" } }

Deserialization accepts both the bare-string and table forms via a manual
Visitor (deny_unknown_fields inside the table arm for precise errors,
rather than an untagged enum). Serialization emits an env-less alias back
as a bare string, so existing configs round-trip unchanged.

The `env` map is parsed and stored but not yet consulted; wiring it into
resolution follows. Consumers still read the alias URI as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make each alias `env` entry a BootstrapSource rather than a bare provider
string, so a bootstrap credential is located the same way any secret is:
a provider spec plus an optional `ref` giving native coordinates. This
replaces the earlier idea of a dedicated `providers/{alias}/{VAR}` storage
convention with the mechanism the rest of the tool already uses.

    [providers]
    # bare string: read from the provider at the convention path
    bws = { uri = "bws://proj", env = { BWS_ACCESS_TOKEN = "keyring" } }
    # table: pin the exact location with `ref`
    vault = { uri = "vault://kv?auth=approle", env = {
      VAULT_ROLE_ID = { provider = "onepassword", ref = { vault = "Infra", item = "approle", field = "role_id" } },
    } }

A ref-less source round-trips back to the bare string form. The `env` map
is still parsed and stored but not yet consulted; resolution follows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make an alias's `env` map take effect: when a provider is built from an
alias that declares bootstrap credentials, each variable is fetched from
its source provider (at a `ref` location or the convention path) and
handed to the store as an overlay it reads before the environment. A
variable already set in the environment wins and is not fetched; a
declared credential that cannot be found is a hard error naming how to fix
it. Bootstrap chains are validated at plan time and limited to one hop,
which also makes cycles impossible.

To keep an alias's `env` reachable at construction, routing and grouping
now key on the primary spec rather than its resolved URI, so two aliases
that share a URI but declare different credentials no longer merge into one
group. The string `TryFrom` for a provider gains a shared
`provider_from_spec` body so construction can carry the overlay.

Applies to per-secret `providers` chains (primary and fallback) and the
default provider. An explicit `--provider` override is resolved to a URI
before routing, so it reads credentials from the environment rather than a
chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the onboarding commands for provider bootstrap credentials:

- `config provider login <alias>` prompts (hidden input) for each bootstrap
  credential the alias declares in `env` and stores it in the source
  provider at the exact location resolution reads it from, so the next
  operation can authenticate. Reports where each was stored and suggests
  `check` to verify. A read-only source is rejected up front.
- `config provider add` gains a repeatable `--env VAR=PROVIDER` flag to
  declare bare-string bootstrap sources from the command line (use `ref`
  by editing the config).

Backed by two library methods on the resolver — `bootstrap_credentials`
(what an alias needs) and `store_bootstrap_credential` (write one to its
source) — so the store and read paths share their addressing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Bootstrap Credentials" section to the providers concept page
explaining how an alias's `env` map sources a provider's own credentials
from another provider, the env-wins/no-leak/one-hop behavior, and the
`login` flow. Document the alias table form and `env` sources in the
configuration reference, and `config provider login` plus `add --env` in
the CLI reference. Cross-link the bws, vault, and onepassword provider
pages to it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An explicit --provider <alias> or SECRETSPEC_PROVIDER=<alias> override was
resolved to a URI before routing, losing the alias's bootstrap env, so the
very command `config provider login` recommends (`check --provider <alias>`)
skipped the stored credentials. Routes now carry the raw spec as the build
key and keep the resolved URI for display and the report.

Also tightens the rest of the bootstrap plumbing:

- validate bootstrap sources (known provider, one hop) on every construction
  path via resolve_bootstrap_overlay, not just plan time chain primaries
- memoize resolved overlays per spec and reuse one source provider per
  distinct source, so credentials are fetched once per invocation
- resolve the 1Password bootstrap token where it is consumed instead of
  mutating the config, so uri() keeps the scheme the user configured
- share one env wins predicate between the resolver and providers; a set
  but empty environment variable now counts as unset on both sides
- extract shared provider construction and BootstrapSource address and
  location helpers so the read and write paths cannot drift
- simplify the CLI alias construction and dedupe test alias maps

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The bootstrap overlay memo is keyed by (profile, spec) and every provider
  build is addressed under the operation's profile, so convention-path
  credentials at {project}/{profile}/{VAR} never bleed across profiles.
- Bootstrap source reads and login stores are audited with a bootstrap
  marker; stores pass the require_reason gate and clear the memo, so a
  rotated credential takes effect immediately.
- Providers statically declare the variables they read through the overlay
  (bootstrap_vars in register_provider!); an alias declaring a variable its
  provider never reads warns instead of fetching a silently ignored value.
- Bootstrap source specs are redacted in prompts and errors, and
  onepassword folds a hash of the effective token (not its plaintext) into
  the preflight cache key; the factory-injection test asserts injection via
  scope-key differences accordingly.
- The prompt-for-missing header names the default provider from the
  registry without constructing it, so a bootstrapped default alias no
  missing secret routes to cannot fail or fetch during display.
- Bootstrap source validation composes the underlying resolution error
  (alias listings, the onepassword spelling hint) and rejects unknown
  schemes at validation time; env = {} normalizes to "no bootstrap env".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cleanups from a reuse/simplification/efficiency/altitude review of the
branch, with no behavior change:

- ProviderAlias.env is a plain map (empty means no bootstrap
  credentials), removing the Option normalization and the Some/None
  handling at every consumer
- one registry scheme lookup (registration_for_scheme) shared by spec
  checks, bootstrap_vars, display names, and provider construction
- resolve_bootstrap_overlay looks the alias up once and passes env down;
  sorted_bootstrap_entries is the single ordering rule for fetches,
  validation errors, and login prompts
- store_bootstrap_credential audits through audit_write_result like
  every other write path
- provider bootstrap variable names are consts shared between the
  registration and the read sites so the two cannot drift
- both release workflows publish assets via
  scripts/upload-release-asset.sh; the PHP extension build/stage mapping
  lives only in secretspec-php/scripts/build-ext.sh (now honoring
  CARGO_TARGET_DIR), called by php-ext.yml and ci-sdks.sh

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(provider): bootstrap credentials sourced from another provider
Follow-up cleanups on the manifest-inheritance refactor:

- Secrets::load_from compiled the effective manifest twice: validate()
  built one internally and dropped it, then load_from built a second copy
  to store. Config::validate_and_compile now returns the manifest it already
  built, so a load compiles it exactly once. validate() stays a thin wrapper.

- resolve_profile built a synthetic Profile, cloning every effective Secret
  config, but all four callers used only the sorted key list. Replaced with
  resolve_profile_secret_names, returning sorted names straight from the
  compiled BTreeMap with no config clones.

- Removed the now-unused public Config::merge_with / Profile::merge_with. The
  extends loader folds documents through overlay_with alone, so the self-wins
  merge helpers had no remaining callers. The require_reason inheritance test
  now exercises overlay_with, the path production actually uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up cleanups on the effective-manifest refactor, no behavior change:

- Embed the CompiledSecret in PlannedSecret instead of re-flattening its
  config/missing/declared_required into parallel fields, so the compiled
  invariant travels with the plan and cannot drift.
- Move the ProfileDefaults field-by-field inheritance onto the type as
  inherit_missing_from, so overlay_with no longer duplicates its field list.
- Add Secret::would_generate as the single source of truth for the
  generate-enabled predicate, shared by manifest compilation and validation.
- Union a profile's own and inherited secret names through a BTreeSet rather
  than a Vec plus manual sort/dedup.
- Refresh doc comments that still described the pre-refactor merge paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Anish Pallati <i@anish.land>
Assisted-by: Claude Opus 4.8
Provider credentials were stored on a profile-scoped convention path, so a
credential saved via `config provider login` (which runs under the session
profile) was invisible when the provider was used under a different profile and
resolution hard-errored "credential not found". Provider auth belongs to the
alias, not a profile, so convention-path credentials now use a fixed,
profile-independent scope for both storing and resolving.

Config `extends` resolved relative paths against the manifest's canonicalized
target directory, so a symlinked manifest inherited from the wrong location.
Relative `extends` now resolve against the manifest's referenced directory
again; cycle detection still keys on the canonical path.

The Azure Key Vault partial-service-principal error now also names the semantic
provider credentials (tenant_id / client_id / client_secret), not only the
AZURE_* environment variables, so users who configured the alias credential map
know which input is missing.

Also clarifies the resolution report's `required` field: a secret with a
committed default or generator is not required even when marked required in one
profile and overridden with a default in another.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Anish Pallati <i@anish.land>
refactor: unify manifest inheritance semantics
feat: add secretspec export command
Signed-off-by: Anish Pallati <i@anish.land>
Assisted-by: Claude Fable 5
feat: add gopass provider
Follow-ups from reviewing the export command:

- gha: percent-encode add-mask data (%->%25, CR->%0D, LF->%0A) so the
  runner masks the true secret; a value containing %25/%0D/%0A was
  registered under a different string and leaked in logs
- export: write to an injected sink so a broken pipe returns an audited
  error instead of panicking (and the formatters become testable/usable
  from an SDK); the CLI passes a locked stdout
- export: persist as_path temp files before emitting output, so a
  persist failure aborts up front rather than handing out paths that are
  then deleted while the audit still records success
- gha: roll back a partial $GITHUB_ENV write so a truncated heredoc
  cannot corrupt env parsing for later steps in the job
- blank provider/profile overrides are now trimmed, not just checked for
  emptiness, so a padded value (e.g. a $(cat file) trailing newline)
  cannot select a nonexistent profile/provider; the rule lives in one
  non_blank helper shared by the setters, env fallbacks, and reason
- derive: the generated builder trims/ignores a blank SECRETSPEC_PROFILE
  instead of hard-erroring, matching Secrets
- json export is now compact; docs describe the actual dotenv/json output
- dotenv export serializes the pre-sorted entries directly, without
  rebuilding and re-sorting a map or re-copying secret values
- add CHANGELOG entry for the export command

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The crate publish job runs cargo in the full devenv shell, which ran the
runner out of disk and killed it mid-publish. Reclaim the same space
test.yml already frees before populating /nix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds an `infisical` provider for Infisical Cloud and self-hosted instances,
gated behind an `infisical` Cargo feature (in the default set alongside
gcsm/awssm/vault/bws/akv). It speaks Infisical's v4 REST API over reqwest,
which the vault provider already depends on, so the feature adds no new
dependency and leaves Cargo.lock untouched.

A SecretSpec profile names the Infisical environment, so a `production`
profile reads the `production` environment. Projects whose environments do
not correspond to profiles pin one with `?env=`; the profile names the
folder as well, so two profiles can never share a secret either way.
Secrets live at `/secretspec/{project}/{profile}` and keys are stored
verbatim, Infisical constraining them no further than being non-empty.
Folder names are narrower, so a project or profile it cannot spell is
refused rather than rewritten.

Authenticates as a machine identity via Universal Auth, or with a
ready-made token; service tokens are deprecated upstream and unsupported.
Not-found is read from the HTTP status rather than the message text, and
secrets sharing a folder are fetched in one list call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The C ABI is the only place in the repo that does not know its own request
contract has a `mode` field. Both the module docs and the shipped header
documented the request as exactly five fields, so `mode` — and with it the
value-free report — was undiscoverable from C. The header is not incidental:
`stage-staticlib.sh` copies it into the Go SDK's `include/`, so it is the
artifact a native consumer reads.

Every SDK already depends on `mode` (`secretspec-hs`, `-go`, `-rb` and `-php`
reach it through this very ABI), and `tests/c_abi.rs` had the same blind spot as
the docs: it covered `resolve`, `no_values`, missing-required, bad JSON and a
missing manifest, but never `report`.

The trap worth naming is that `no_values` looks like it does the same job. It
does not: it blanks the values but keeps the resolve shape, whose `secrets` is
a name-keyed object that carries no requiredness and is emptied when a required
secret is missing — the one case a preflight check exists to describe. A report
answers with an array of per-secret entries carrying `name`, `required` and
`status`, and lists every declared secret regardless.

Documentation and tests only; no behaviour change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs(ffi): document the request `mode` field, and test it
Five review findings, each verified against the code or Infisical's own source
before being accepted.

- The standalone `infisical` feature did not compile: `ProviderUrl::port()` and
  `query_pairs()` are gated on the providers that use them, and this one never
  added itself. `cfg(test)` masked it, so the suite could not see it. CI now
  checks every provider feature standalone, which is the only thing that can
  catch this class — all eight pass.

- Batch resolution dropped imported secrets. A folder's imports come back in a
  separate `imports` array (`includeImports` defaults on, so they were already
  in the response, just ignored), and only `secrets` was read. `check` and `run`
  go through `get_many`, so an imported secret read as missing while `get`
  returned it. Merged with the precedence of the CLI's `InjectRawImportedSecret`:
  a direct secret wins over an import, and a later import wins over an earlier
  one. Withheld imported values are refused exactly as direct ones are.

- `OnceLock` stored the finished token but did not serialize the exchange: every
  concurrent caller passed `get()`, awaited its own `login()`, and then raced in
  `get_or_init`. A batch read fetches distinct addresses on separate threads, so
  surplus Universal Auth exchanges were routine — and a hard failure for a client
  secret with a one-use limit. Now a `tokio::sync::OnceCell`, whose
  `get_or_try_init` runs one initializer at a time.

- The landing page advertised 14 providers as current-release behaviour. The
  released count is 13; only the marquee entry is added, carrying `(0.16+)`,
  matching how gopass was listed while it was unreleased (66bbab1).

- Infisical's legacy `INFISICAL_API_URL` is honoured after `INFISICAL_DOMAIN`,
  matching their CLI's `GetEnvDomain` precedence, so an instance already
  configured for their tool is not silently redirected to US Cloud. An invalid
  domain now names the variable that actually set it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `features` job runs on a bare runner, where `keyring` cannot build: it links
libdbus for its secret-service transport on Linux, and only the devenv job gets
that from its shell. Installs it the way `go-embed.yml` and `ffi-build.yml`
already do.

The job passed locally because the nix shell it was developed in supplied dbus
and a PKG_CONFIG_PATH — the environment hid the missing dependency, which is the
same shape of mistake the job exists to catch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: add Infisical provider (infisical://)
docs: clarify provider guides
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the long-lived NUGET_API_KEY secret with an OIDC token exchange
(NuGet/login), matching how the npm, PyPI, and RubyGems publish jobs work.
The nuget environment now only holds NUGET_USER, the nuget.org profile
name that owns the trusted publishing policy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add C# SDK
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs: update navigation versions and landing page
Add composed secrets
Integrate Bitwarden Password Manager (bw CLI) as a secretspec
provider. The provider supports vault-wide item access across all
Bitwarden item types (Login, Card, Identity, SSH Key, Secure Note)
with smart field extraction, URI-based configuration
(bitwarden://[org@]collection?server=...&type=...&field=...),
and session-based authentication via BW_SESSION.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Upstream now ships a native BWS SDK provider (bws.rs), so the
unified Password Manager + Secrets Manager provider must be reduced
to Password Manager only. Remove the BitwardenService enum,
BWS-specific config fields, execute_bws_command method, and
bws:// scheme handling from register_provider! and TryFrom.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Upstream's Provider trait has evolved from dynamic dispatch with
(project, key, profile) signatures to a static-registration pattern
with Address, ProviderUrl, and ProviderCredentials. Rewrite
BitwardenConfig's TryFrom to accept &ProviderUrl, add
convention_address/supported_coords/with_credentials/uri, and
update get/set to accept Address<'_> parameters resolved via
resolve_coords. Field resolution respects the native address
coordinate first, then BITWARDEN_DEFAULT_FIELD, then config.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Wire the bitwarden module into the provider registry with a bw
feature flag (bw = ["dep:base64"], enabled by default). The base64
dependency encodes JSON for the bw CLI's item create/edit stdin
path. Add four unit tests covering provider creation, collection,
org-collection URI parsing, and bws:// scheme rejection.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
The bitwarden.md page was drafted for a dual-service (bw + bws)
provider. Strip empty code blocks, the Secrets Manager access token
section, and a stray bullet left behind after BWS content removal.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Add .pi/ to .gitignore for pi-agent task output.
The collapsible_if lint in config::migrate_macos_config is a
pre-existing upstream warning surfaced by clippy during this
branch's development.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Replace the manual setup requirements with an auto-provisioning
setup_test_data() function that creates Bitwarden items if they
don't exist, and registers an EXIT trap to clean them up unless
--keep-test-data is passed.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Replace invalid TOML keys ("Deploy SSH Key", etc.) with valid
bw_integration_test_* identifiers and ref = { item, field }
mapping, matching the new Provider API address model.

Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Harden .NET SDK distribution matrix
The entry landed in the already-released 0.15.0 section during the
rebase; the provider targets the next release.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The provider wraps the `bw` CLI, so the id now matches the tool it
drives and the existing cargo feature, mirroring how the bws provider
is named after the bws CLI. URIs change from bitwarden:// to bw://.
The provider is unreleased, so there is no compatibility impact.

Module, source file, and docs page follow the id (provider::bw,
providers/bw.md). BITWARDEN_DEFAULT_TYPE/FIELD env vars keep their
prefix to stay clear of the bw CLI's own BW_* namespace.
Add the Bitwarden Password Manager provider to every listing the
adding-providers checklist names: sidebar and llms.txt description,
concepts and reference tables (with security row), landing page
metadata/hero/bento (provider count 14 -> 15), quick-start and README
config-init examples, and the rustdoc provider summary. All entries
carry the 0.16+ label; the provider page gains the version notice.

The landing grid's existing plain 'Bitwarden' label becomes 'Bitwarden
Secrets Manager' to disambiguate the two Bitwarden providers.
Release 0.16.0
bw can prompt on stdin (e.g. for the master password when a session is
missing or expired). stdin is null or closed on our invocations so a
prompt could not hang us, but --nointeraction makes bw fail fast with
its own clear error instead of a confusing EOF-related one, which
matters in CI and other headless contexts.
secretspec-derive depends on secretspec, so the main crate must be
published to crates.io first. The loop published secretspec-derive first,
which failed to resolve secretspec ^0.16.0 from the registry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix crates publish order
Prepare SecretSpec 0.16 post and composition syntax
Implements the FUTURE WORK plan from bitwarden_integration.sh:
vaultwarden + caddy TLS proxy (the 2026+ bw CLI refuses plain http
servers, so an internal self-signed cert is required), fixture account
registered via the identity API (vaultwarden_bootstrap.py implements the
client-side registration crypto bw doesn't expose), bw CLI isolated via
BITWARDENCLI_APPDATA_DIR so the developer's real config is untouched.
No repository secrets needed; works on fork PRs.

Verified locally: full bitwarden_integration.sh suite 19/19 PASSED
end-to-end from a cold start (containers, registration, login, suite,
cleanup) on macOS + colima.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CDSY4yJXvof9Am8acxzHf
Show provider and SDK counts on landing page
Signed-off-by: Anish Pallati <i@anish.land>
Assisted-by: Claude Opus 4.8
Signed-off-by: Anish Pallati <i@anish.land>
Signed-off-by: Anish Pallati <i@anish.land>
feat: add JWT auth to the vault provider
Signed-off-by: Anish Pallati <i@anish.land>
Assisted-by: Claude Opus 4.8
Signed-off-by: Anish Pallati <i@anish.land>
Assisted-by: Claude Opus 4.8
docs: document secretspec-update
0.16 shipped with the Infisical provider, so the "upcoming / not
available" framing on the provider page is stale. Replace it with the
durable "Available since SecretSpec 0.16" wording, matching how gopass
reads after its 0.15 release, and align the reference heading to the
released-provider form (plain heading + version note, no marker in the
security table).

The (0.16+) minimum-version markers in provider lists, tables, the
sidebar blurb, and the config-init examples stay: those are durable
"requires 0.16 or later" markers, exactly like gopass's (0.15+).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs(infisical): mark the provider available since 0.16, not upcoming
Python (fixes #177): publish a cp39-abi3 Windows x64 wheel to PyPI. The
old follow-up note claimed the wheel was blocked on vendoring provider
system deps; that is Linux-only (libdbus) -- keyring uses Windows
Credential Manager natively and the core crate already ships on MSVC.

Ruby: publish an x64-mingw-ucrt platform gem. RubyInstaller's devkit
links with MinGW, which cannot consume MSVC archives, so the gem
bundles a staticlib from the x86_64-pc-windows-gnu target (declared in
rust-toolchain.toml). The windows-gnu link line references import
libraries that ship inside cargo registry crates (libwindows.*.a,
libwinapi_*.a) and exist in no MinGW distribution; the shared
scripts/copy-mingw-import-libs.sh bundles exactly the referenced ones
into vendor/ and extconf.rb adds vendor/ to the linker search path.

PHP: publish Windows x64 extension DLLs for PHP 8.2/8.3/8.4. Requires
ext-php-rs 0.15 (0.13 leaves Zend data symbols without dllimport
linkage on Windows), nightly Rust (PHP's Windows ABI uses the
vectorcall calling convention, gated per crate via cfg_attr), and
rust-lld (PHP's loader refuses modules linked with a newer MSVC linker
than the php.exe core). ext-php-rs downloads the matching PHP devel
pack during the build; its 0.15 module builder needs explicit
wrap_function! registration.

Haskell: validate the SDK on Windows in CI with a native rustup +
MSYS2 + GHC job. The staged windows-gnu archive gets three fixups for
GHC 9.6's older bundled toolchain: drop the prebuilt std's sectionless
.dwo members, strip the .drectve -exclude-symbols directives its
ld.lld rejects, and alias nanosleep64 back to nanosleep (time_t is
64-bit on x86_64 either way) so GHC's winpthreads satisfies it; GHC's
bundled mingw/bin joins PATH so the test binary finds
libwinpthread-1.dll at runtime. Hackage publishing now gates on the
Windows job.

CI: cache Rust builds in the packaging workflows (Swatinem/rust-cache
for the cargo builds, maturin-action's sccache inside the manylinux
containers) and surface the Haskell test-suite log on failure.

Docs: new development/sdks page (architecture, packaging workflows,
platform matrix, the MSVC-vs-MinGW toolchain split, checklists for
adding a platform or an SDK) and a user-facing platform support table
in the SDK overview, with the new Windows artifacts labeled 0.17+.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Windows support across the SDK packages
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add post on separating secrets from configuration
The dotenv provider wrote any ref item verbatim while reading with
dotenvy's grammar ([A-Za-z_][A-Za-z0-9_.]*), so one accepted write of an
unrepresentable name (e.g. a dash smuggled in by `ref = { item = ... }`)
poisoned the whole file: every later read or write of any secret in the
store failed to parse at that line.

Validate the name against dotenvy's exact grammar in set, get, and
check_writable, so the CLI refuses before prompting for a value and a
ref written for a lenient store fails loudly instead of corrupting the
file. Interior dots stay accepted: dotenvy reads them back, and files
written by the lenient JS dotenv family may contain them.

Adds a write/read symmetry contract test to the shared provider suite:
a set that reports success must be readable back by get, and a rejected
write must leave previously stored secrets intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dotenv: reject variable names the parser cannot read back
Allow required to accept named at_least_one and exactly_one groups, including overlapping memberships. Validate and report group failures consistently across check, run, and SDK resolution.

Examples:

PASSWORD = { description = "Account password", required = { at_least_one = "account_auth" } }
ACCESS_TOKEN = { description = "Access token", required = { at_least_one = "account_auth" } }

GITHUB_TOKEN = { description = "GitHub token", required = { exactly_one = "github_auth" } }
GITHUB_APP_KEY = { description = "GitHub App key", required = { exactly_one = "github_auth" } }
Support named secret presence constraint groups
Give OpenBao its own provider identity, feature flag, configuration precedence, and integration-test registration while sharing the compatible KV protocol client with Vault.

Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
Strip trailing address separators before constructing API paths and propagate namespaces to AppRole and JWT login exchanges for both Vault-compatible providers.

Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
Document OpenBao configuration, authentication, references, and the 0.17 compatibility boundary across every provider listing and entry point.

Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
Sanitize environment-derived server addresses before they reach requests or audit output, while retaining effective non-secret configuration in canonical Vault-compatible provider URIs. Also make the OpenBao development token example deterministic.

Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
feat(openbao): add first-class OpenBao provider
docs: add “But I Use SOPS” blog post
# Conflicts:
#	CHANGELOG.md
#	Cargo.lock
#	docs/astro.config.ts
#	docs/src/content/docs/concepts/providers.md
#	docs/src/content/docs/reference/providers.md
#	docs/src/pages/index.astro
#	secretspec/Cargo.toml
feat: add age provider
# Conflicts:
#	docs/astro.config.ts
Support systemd-credentials provider
Add KeePass KDBX provider
Signed-off-by: Anish Pallati <i@anish.land>
Assisted-by: Claude Opus 4.8
feat: add secretspec-action for GitHub and Forgejo Actions
Remove unavailable-version wording
One test per finding (R1 linked-field type 3 poisons writes; R2 named
custom field lost on create; R3 wrong default field on non-login update;
R4 case-sensitive update vs case-insensitive read). Each reports
REPRODUCED/FIXED and the set exits 0 only when all findings are fixed —
opt-in via RUN_REGRESSIONS=1 in the harness until then.

Verified against disposable Vaultwarden: currently 0 fixed / 4 reproduced,
matching the review descriptions verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CDSY4yJXvof9Am8acxzHf
Use the official BWS CLI instead of the SDK
Add an explicit global config namespace
test: fully-automated integration run against disposable Vaultwarden
Linked fields (type 3) caused deserialization to fail with
'Unknown field type: 3', which poisoned all reads and writes
whenever any item in the vault contained a linked field.

Add Linked variant to BitwardenFieldType enum and unit tests
covering items with linked fields alongside other field types.
ProviderUrl::query_pairs was gated behind awssm, infisical, kdbx,
openbao, vault, and test — but not bw.  Building with
--no-default-features --features bw failed to compile because
the bw provider's TryFrom impl calls url.query_pairs().

Add bw to the cfg gate so the provider builds in isolation.
The bw CLI does not read BW_SERVER; self-hosted servers must be
configured via 'bw config server' (which requires being logged out).
Replace the three BW_SERVER env var sites with an ensure_server_configured
check that runs 'bw status' to detect mismatches and provides clear
remediation steps when the configured server doesn't match.
When a collection is configured via bw://collection-id, org@collection,
or BITWARDEN_COLLECTION, the provider now passes --collectionid to
'bw list items' in both get_from_password_manager and
set_to_password_manager, preventing cross-collection item collisions.

Add unit tests for collection_id parsing from URI host, org@collection,
and query parameter forms.
When creating Login, Card, or Identity items with a field name that
doesn't match any built-in field (e.g. field=api_key), the value was
incorrectly stored in the item's default field (password, card number,
or email). On subsequent reads, the custom field lookup returned nothing.

Now unknown field names are stored as named custom fields in the fields
array, matching the pattern already used by create_secure_note_item.
When updating an existing item without an explicit field, the provider
always fell back to 'password', regardless of item type. This caused
Secure Notes, Cards, Identities, and SSH Keys to write into a custom
'password' field while the getter read from the type-specific default
(notes, number, email, private_key), breaking read-after-write.

Now the fallback matches the getter's default for each item type.
The read path (extract_from_custom_fields) matches field names case-
insensitively, but the write path (update_custom_field_in_json) used
case-sensitive comparison. Updating 'api_key' when the item had
'API_KEY' created a duplicate field; subsequent reads found the
original (stale) value first.

Now update_custom_field_in_json uses eq_ignore_ascii_case, matching
the read path's behavior.
secretspec opened a fresh reqwest::Client per Vault/OpenBao request and
get_each fanned out one thread per unique address with no cap. A cold
burst of dozens of secrets therefore opened one TCP(+TLS) handshake
each; behind reverse proxies (e.g. Envoy Gateway) that storm has been
observed to drop part of the burst with "Failed to connect to Vault".

- KvProvider: OnceLock<Client> + http(), matching the Infisical pattern
- get_each: wave-based fan-out, default concurrency 12
- SECRETSPEC_PROVIDER_CONCURRENCY overrides the cap (≥ 1)
- unit tests for env parsing and peak in-flight ≤ cap
- docs + changelog

Measured against a reverse-proxied OpenBao (78 secrets):
  hostname HTTPS path: ~9/15 OK under rapid stress
  loopback HTTP path: 15/15 OK
Shared client + concurrency cap addresses the proxy path class of failure.
Follow-up after live stress against a reverse-proxied OpenBao (78 secrets
via Envoy):

  stock secretspec 0.14:     0/15 OK
  patched (client+cap12):   12/15 OK
  patched (client+cap8):    18/20 OK
  patched loopback HTTP:    15/15 OK

- Default SECRETSPEC_PROVIDER_CONCURRENCY 12 → 8 (best measured default)
- send_with_connect_retry: up to 3 attempts on connect/timeout only
- Match Infisical http() docstring; docs use bullet style
- Keep wave fan-out without special-casing last singleton chunk
New `scaleway://` provider backed by Scaleway Secret Manager's v1beta1
REST API. Authenticates with an API secret key (secret_key credential or
SCW_SECRET_KEY), targets a region (URI host / SCW_DEFAULT_REGION / fr-par)
and project (?project_id= / SCW_DEFAULT_PROJECT_ID). Convention secrets are
stored in the folder hierarchy secretspec/{project}/{profile} with the key
as the secret name, since Scaleway names cannot contain slashes. Native refs
select a JSON key via `field` and a revision via `version`, and are read-only.

Gated behind the `scaleway` feature (on by default).
Completes the provider documentation checklist: adds Scaleway to the
landing-page provider grid and the quick-start `config init` output, so the
provider name no longer drifts between listings.
A scaleway URI with a path (scaleway://fr-par/team) parsed but the path
was ignored: uri() dropped it, so reads/writes silently fell back to the
root secretspec/... hierarchy. Reject it like the other cloud providers;
folders are configured via ?path=. Adds a regression test.

Also add scaleway to the config init selector samples in the landing
page and README so they match the provider registry.
fix(vault/openbao): reuse HTTP client and cap get_many concurrency
The bw CLI may return linkedId as an integer (e.g. 100), but the
BitwardenField struct deserialized it as Option<String>, causing
deserialization to fail whenever any item in the vault contained a
linked field with an integer ID. Change linked_id to
Option<serde_json::Value> to accept both string and integer forms.

Add test for integer linkedId deserialization.
Add Scaleway Secret Manager provider
Implements the design @domenkozar spelled out on #137: a `[scopes]` table
names membership-only subsets of a profile's secrets, and `check`/`run`/`export`
take `--scope` (env `SECRETSPEC_SCOPE`). The resolved set is the intersection of
the selected profile and the scope's secret list; scopes never change a secret's
required/default/providers or its `{project}/{profile}/{key}` storage address.

Resolution filters at `resolve_profile_secret_names`, the single worklist
upstream of validation, planning, prompting, generation, and audit, so all of
them scope at once and an unknown scope fails before any provider is touched.

`run --scope` additionally strips scope-excluded secrets from the child via
`Command::env_remove`, which overrides inheritance — so a value the parent shell
already exported (a devenv `secretspec run`, a prior `eval "$(secretspec
export)"`) cannot leak into the launched process. This is the isolation point
@domenkozar flagged; filtering the injected map alone is insufficient because the
child inherits the real parent environment.

Static validation rejects a scope that lists a secret no profile declares;
selecting an undefined scope is a runtime error listing the defined ones.

The derive macro is intentionally unchanged (scope is a resolution-time filter,
option 1 from the #137 discussion); the typed-SDK surface is deferred pending a
steer on the open macro question.

Tests: config parsing/validation/overlay, resolution intersection, unknown-scope
error, value resolution skipping excluded required secrets, and an end-to-end
run test asserting an excluded parent-exported secret does not reach the child.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve the transitive dependency closure of the visible set so an in-scope
composed secret can build its value from out-of-scope inputs, then restrict
every output surface — the value map, the temp files backing as_path values,
the per-secret report, and the missing/default lists — back to the visible set,
so those inputs resolve without ever being exposed to the scope. A secret that
is neither visible nor a dependency of one is never planned, so no provider is
contacted for it.

Broaden `run --scope` scrubbing to every manifest-declared secret outside the
visible set, across all profiles rather than only the selected one, so a value
inherited from another profile's environment cannot leak into the child.

Short-circuit an empty scope (or empty intersection) before any provider is
built. Generated typed loaders opt out of the ambient SECRETSPEC_SCOPE via
set_ignore_ambient_scope, since a generated struct always expects the full
profile. Surface the active scope in the resolve response and resolution report
(optional, omitted when unscoped). Document whole-value scope replacement under
project extends.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address an adversarial review of the composition-aware scope change:

- Interactive `check --scope` prompted for (and on entry overwrote) the
  out-of-scope dependencies of an in-scope composed secret: the visible-only
  resolution list made their already-resolved status look unresolved to the
  prompt planner, so it descended into the hidden leaves and solicited them by
  name. Restrict interactive prompting to the visible set (new
  scoped_promptable_missing), so a scope never names or writes a secret it
  hides.
- Correct the configuration reference: typed loaders cannot be scoped — the
  generated builder exposes no scope method — so they always resolve the full
  profile rather than "scope explicitly through the builder".
- Add coverage: scoped prompting excludes hidden dependencies; run --scope
  scrubs a composed secret's raw dependencies from the child environment
  (a separate code path from the resolution output filter); export --scope
  emits only the visible set; the active scope is surfaced in the resolve
  response and resolution report and omitted when unscoped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebasing onto main brings scopes together with the presence constraints added
in #184, and the two need a defined interaction.

Constraints are now evaluated after the output is narrowed to the visible set,
over the group members that set exposes:

- A group with no visible member is not the scoped consumer's concern and is
  not enforced.
- A group with visible members is enforced over those alone, so a scope never
  inherits a guarantee that rests on a secret it hides: `at_least_one` satisfied
  profile-wide by GCP_KEY still fails a scope showing only an absent AWS_KEY.
- `exactly_one` stays enforced whenever two visible members are both present.
  Scoping narrows what is judged, not whether it is judged.
- A secret fetched only as a hidden composition input never counts as present,
  and a violation names only visible members -- preserving the invariant that a
  scope never discloses a secret it hides.

The pre-existing guard that skips constraints for `get`'s deliberately partial
plan is unchanged; a scoped resolution is partial too, but it is whole-profile
validation of a declared subset, so it is opted back in explicitly.

Also retarget the release wording from 0.16 (now released without scopes) to
0.17 across the configuration, CLI, and inheritance docs, and update the scope
tests to the `${NAME}` composition syntax adopted in 09df71a.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decides the three questions left open on #163, and fixes the one that turned
out to be a bug rather than a preference.

Audit records the *accessed* set for a scoped `check`, including a composition
input the scope hides. The log exists to capture provider access, and recording
only the visible names would understate what was read. `run` still records what
it injected -- the visible set -- because the two events record different facts:
one a read, the other an injection.

An `as_path` secret's resolved value is its temp-file path, so a visible
composition built from a hidden `as_path` input embeds that path in its own
value. Filtering the temp files by the visible set deleted that file and handed
the consumer a dangling path. Temp files are no longer filtered: every accessed
non-visible secret is by construction a dependency of a visible one, so keeping
its file is exactly what the composition needs. This matches composition's
existing contract -- a composed DSN already carries its inputs' content in
derived form -- while the input stays out of the environment.

A secret the scope admits but that does not resolve is deliberately not
scrubbed. It is inside the visible set, so a parent-exported value is inherited
exactly as it would be with no scope active; scoping decides which secrets are
in play, not the semantics of one it admits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback on #193:

- import() went through resolve_profile_secret_names, which honors an
  ambient SECRETSPEC_SCOPE. Since import has no --scope (the scope surface
  is check/run/export), an inherited variable could silently narrow the
  copy to a scoped subset. Split out profile_secret_names_unscoped and
  route import through it.

- A scoped constraintViolation.secrets can be narrowed to one visible
  member, but resolution-report.schema.json required minItems: 2 and
  described it as all declared members, so check --scope --json could emit
  a payload failing the canonical schema. Relax to minItems: 1, reword the
  description, and validate a serialized scoped violation against the
  schema in tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The four scope run tests drive a child via `sh -c 'printf ... > $path'`
with a Windows temp path in the redirection; the backslashes get mangled
inside sh, the expected file is never created, and the follow-up
`fs::read_to_string(...).unwrap()` panics with NotFound on Windows CI.
This matches the existing `#[cfg(unix)]` gate on
`test_run_cleans_up_as_path_temp_files`, which uses the same pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* run --scope decided scrubbing from the visible set (scope ∩ profile), so a
  secret the scope explicitly lists was unset whenever the selected profile did
  not declare it. That contradicted the documented rule that a secret the scope
  admits is never scrubbed, and it broke reusing one scope across profiles that
  declare different subsets. Membership decides now, while a composition
  dependency the scope leaves out is still scrubbed.

* set() listed the available secrets through the scope-filtered path, so an
  ambient SECRETSPEC_SCOPE hid names from its error message even though set has
  no --scope and writes out-of-scope secrets fine. An undefined ambient scope
  was worse: the ? returned InvalidScope before record_key_error, dropping the
  audit event for an attempted write. Routed through
  profile_secret_names_unscoped, like import.

* An empty `[scopes.x] secrets = []` validated and then guaranteed nothing: it
  contacts no provider, so check --scope x reported a clean "0 found, 0
  missing" while run --scope x started the command with every manifest secret
  scrubbed and none injected. validate_scopes now requires at least one member
  and rejects blank or repeated entries and blank scope names. An empty
  intersection with the selected profile stays valid, since a scope is meant to
  be reused across profiles that declare different subsets.

* A blank --scope could not clear an inherited SECRETSPEC_SCOPE: set_scope
  dropped the blank and resolution read the environment anyway, so
  `--scope ""` narrowed and scrubbed. The CLI now reads a blank value as "no
  scope, and do not consult the environment". An absent flag, an explicit
  scope, and untyped SDK/FFI resolution are unchanged.

* A fallback-chain warning named a secret the scope hides
  (`warning: provider ... failed for DB_PASSWORD`), disclosing exactly what the
  output filter removed, while prompting was carefully filtered for the same
  reason. execute_plan now passes a diagnostic label: a secret outside the
  output filter is called "a hidden composition input", which is what it always
  is by construction, while a visible secret and every unscoped secret keeps
  its own name. A provider's own error string may still embed the address it
  searched; that is documented rather than scrubbed.

* effective_secrets swallowed InvalidScope with .ok().flatten(), degrading a
  scope resolution failure into no filtering at all, which would display the
  whole profile under a scope. It propagates now.

* Dropped the dead secret-name key on execute_plan's temp files, along with the
  comment contradicting the one that explains why temp files are deliberately
  not filtered; removed two per-resolution clones of the secret-name vector;
  and switched the typed_scope_env fixture from a predictable temp_dir() path
  to TempDir.

* Pinned two contracts the PR stated but nothing held down: `get` has no
  --scope and must not be narrowed by an active one (the shape that regressed
  for set), and an empty scope intersection reports an empty `provider`.

* Documented the limits this leaves: exactly_one cannot detect a violation
  involving a hidden member, so a scoped check can pass on a profile that is
  globally invalid; export --scope emits the scoped subset without unsetting
  anything, unlike run --scope; and `provider` is empty in resolve and report
  results when nothing was resolved. Added the missing Scopes section to the
  Rust SDK page and a pointer in the SDK overview.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat: secret scopes (--scope), membership-only subsets of a profile
The provider no longer sets BW_SERVER, since the bw CLI ignores it. Explain
that self-hosted servers are configured with 'bw config server' while logged
out, and that bw://?server= records the expected address and is verified
before each operation rather than configuring the CLI.

Adds a Self-hosted servers section and a Server Mismatch error entry to the
provider guide, a clarifying note plus prerequisite in the provider
reference, and aligns the rustdoc authentication steps.
A cached alias is a complete provider route: `fallback` names the
authoritative providers in read order, and `cache` names a local store
that answers first while a value stays fresh. Reads serve a fresh entry
without constructing or contacting the authoritative providers; writes go
to the first fallback and then refresh the cache. `secretspec cache clear`
invalidates entries by hand.

Routes are validated when they are planned, not when a store is first
touched:

- the cache must be a distinct store from the route's own authoritative
  providers, compared by canonical provider URI so equivalent spellings of
  one store cannot disguise a cache as its own source. Sharing one would
  overwrite the secret with the cache entry on the first refresh, then
  serve that entry back as the value;
- the cache must be a store SecretSpec can delete from — keyring, pass,
  gopass, dotenv, or a Vault/OpenBao KV v2 mount — because every form of
  invalidation is a delete, and a cache nothing can invalidate is worse
  than none. Providers declare the capability in `register_provider!`, so
  the check is a registry lookup and planning still opens no store;
- a cached alias must be the only entry in a `providers` list, in any
  position. Accepted elsewhere in a chain it would be dropped at read time
  with a warning and writes would go to the wrong store;
- `max_age` is parsed when the configuration loads, so an unusable
  duration cannot reach planning.

An entry says who owns it: a marker, the project, and the profile. A cache
store can be shared — a flat dotenv file gives every project the same key
for a given secret name, and a store may hold values SecretSpec never
wrote — so an address is not evidence of ownership. Reads and refreshes
change only what they can show is ours, and `cache clear` reports a
foreign entry rather than deleting it. An entry carrying the marker but no
readable payload is unmistakably ours and gets replaced, which is what
makes recovering from a truncated write safe.

A cached value never outlives the write that superseded it. A failed
refresh, a cache that could not be constructed, and a write that bypassed
the route with `--provider` all invalidate the entry; when even that
fails, the warning names the `cache clear` to run. An entry no read can
serve — expired, or written for a different route — is deleted when found
rather than skipped, because a refresh only replaces it when the
authoritative read succeeds on a pass that materializes values, and
nothing revisits it in between.

The envelope's write time is what enforces `max_age` on stores that cannot
expire anything. Where the store can, `max_age` is applied there too:
Vault and OpenBao set the KV v2 path's `delete_version_after`, so a copy
of someone else's secret stops existing at that age even if SecretSpec is
never run again. Their `delete` goes through the metadata endpoint, so no
soft-deleted version keeps a cleared value recoverable, and it refuses a
`ref`, whose path is managed outside SecretSpec.

`cache clear` reports how many entries it actually removed, ignores
provider overrides — an exported `SECRETSPEC_PROVIDER` would otherwise
make it a silent no-op — and clears what it can before reporting a store
it could not. Cache writes are audited as `cache_refresh` and removals as
`cache_clear`, so a read that refreshed its cache is never mistaken for a
secret write. Caches are read one store at a time through `get_many`
rather than once per secret.

Closes #199.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four bws tests wrote a fake `bws` script and immediately executed it,
which failed intermittently with "Text file busy". The kernel refuses to
execve a file any process has open for writing, and libtest runs this
crate's tests as threads of one process: `fork` copies the descriptor
table, so a subprocess spawned by another thread during our write kept
that descriptor open until its own exec.

Retrying on ETXTBSY would have waited for the window to close. Writing
the script to a scratch file and having a short-lived subprocess copy it
into place means our descriptor table never holds the executable at all,
so there is no window. chmod needs no descriptor, and the scratch file is
never executed.

The four copies of write-chmod-exec collapse into one helper on the way.

Was 2 failures in 11 runs of `provider::`; now 0 in 20.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add cached provider aliases
The file is upstream's commented-out example scaffold and is unrelated to
this branch. A 'git add -A' while committing the linkedId fix swept in its
local deletion, which would have removed it from the repository via the PR.

Restored byte-for-byte from upstream/main.
tests/vaultwarden_harness.sh shells out to 'docker' in six places and lists
it as a required dependency, but nothing supplied it, so a fresh devenv
shell fails the harness dependency check.

Use docker-client rather than docker: the harness only needs the CLI, and
the container runtime is developer-supplied (Docker Desktop, colima, or a
podman machine exposing /var/run/docker.sock). Note that split in the
harness header so the prerequisite is discoverable from the script itself.
The server guard parsed `bw status` as line-oriented text, looking for a
"Server URL:" prefix, but the command emits JSON — `is_authenticated` in the
same file already parsed it that way. The prefix never matched, so the guard
always took its not-found branch and every operation on a `bw://?server=`
provider failed. C2 had replaced a silently ignored setting with an
unconditional failure.

Read `serverUrl` from the parsed JSON instead, and treat null as the public
cloud, which is how the CLI reports that state; comparing null literally
would have rejected an explicit `?server=https://vault.bitwarden.com`.
Compare addresses through a normalizer that erases only differences which
cannot change which server is addressed (trailing slash, surrounding
whitespace, scheme-default port, scheme and host case), leaving path case
significant so the guard cannot wave through a different server.

Also:
- check the exit status before parsing, and report stderr when it is non-zero
- stay silent when the CLI is absent, so execute_bw_command's installation
  instructions surface instead of a vaguer message from the guard
- name the public cloud in the mismatch message rather than printing a URL
  the user never configured
- memoize the outcome so bw status is spawned once per process instead of
  once per CLI invocation
- apply the guard in update_item_with_json and create_item_from_template,
  which drive the CLI directly; they were previously covered only because a
  guarded read happened to precede them

Parsing and comparison are pure functions with unit tests, including the
verbatim bw status output from bitwarden-cli 2025.11.0 as a fixture. The
previous text-parsing assumption is now an explicit error case.
Creation, update, and unqualified reads each carried their own idea of which
field to use when the caller named none, and the three disagreed. That is the
shape of both remaining findings, so replace them with a single
BitwardenItemType::default_field.

Creation resolved the default through default_field_for_hint, which guessed
from the item name and, for Card and Identity, fell through to the name
itself. Storing named fields as custom fields then made that guess land in a
custom field named after the item, which an unqualified read never consults:
'set' succeeded and the following 'get' found nothing. Update used a
hardcoded table that named the note body for secure notes, while reads prefer
a 'value' custom field, so updating an item created by an earlier version
left 'get' returning the stale value.

The name-derived guessing is removed rather than mirrored. Reads resolve a
field from the address, BITWARDEN_DEFAULT_FIELD, or the provider URI and never
consult the name, so a name-derived write target could not be read back --
'set MY_TOTP' on a login wrote login.totp, which no unqualified read reaches.
Each entry in the new table is the field the matching extract_from_* method
already looks at first, so writes now land where reads begin; the existing
read fallbacks are untouched, since they are what lets hand-created vault
items resolve.

The five create_*_item methods become pure *_template builders, with the one
create_item_from_template call hoisted into create_new_item. That removes five
duplicate call sites, collapses the SSH key path's duplicated template and
early return, and lets a write be checked against a read in-process.

Adds seven tests: the default table pinned, a create/read and an
update/create/read round-trip across all five item types, an explicitly named
custom field across all five, independence from the item name, and a legacy
secure note carrying both a 'value' field and a body -- the only case that
distinguishes 'value' from the note body once creation and update agree. Each
was checked against a mutation of the table to confirm it fails.

Also drops the clippy if_same_then_else warning, which was in the deleted
heuristic, and corrects the provider guide's field table, which claimed Card
and Identity require an explicit field when nothing enforced that.
Clarify audit reason defaults
providers: implement SOPS provider
Release 0.17.0
Fix Rust and Go release packaging
Make release checksums portable
Fix wrong_self_convention on BitwardenItemType::to_u8/BitwardenFieldType::to_u8
(Copy types should take self, not &self) and drop an orphaned doc comment
left over from a deleted function that triggered empty_line_after_doc_comment.
No behavior change; 636/636 lib tests still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
C1 (bw wouldn't compile with --no-default-features --features bw) was only
caught by manual testing; the CI job meant to catch exactly this class of
regression didn't include bw in its provider list. kdbx and age are also
missing from this list but are being raised with Domen separately since it's
his CI job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
C3's fix passed the collection straight to `bw list items --collectionid`,
which accepts only a UUID. Since `bw://myorg@dev-secrets` reads as a pair of
names, that address matched nothing: get returned None and set created a
duplicate in the personal vault.

Names and ids are now both accepted and resolved against `bw list
organizations` / `bw list collections`, memoized once per process. An id is
validated rather than trusted, so a typo fails with the collections that do
exist instead of silently returning no items. A collection addressed alone
supplies its own organization, and when both are given the organization acts
as scope and assertion: it disambiguates a collection name that occurs in
several organizations, and disagreeing with the collection's real
organization is an error.

Searches now send at most one filter. `bw list` combines multiple filters
with OR, so sending --organizationid alongside --collectionid widens the
search to the whole organization rather than narrowing it, which would make
every collection in an organization address the same items and let set
overwrite a same-named item in a sibling collection. A collection id already
identifies its organization, so nothing is lost. Item creation still receives
both ids, since those place the item rather than filter a query.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verifying collection addressing needed an organization with two collections,
which the Vaultwarden harness did not have — so nothing exercised the code
path at all.

The `bw` CLI cannot create an organization, so vaultwarden_bootstrap.py grows
a --create-org mode that does it through the API, reusing the registration
crypto already there plus an RSA-OAEP EncString for the organization key.
Collections the CLI can create, so the harness makes those with `bw create
org-collection`.

The new bitwarden_collection_addressing.sh puts an item of the same name in
two collections with different values. That fixture is what makes the tests
conclusive: without name resolution neither address finds anything, and if
both scope filters were sent at once the CLI's OR would make the two
addresses return the same superset, so the dev and prod assertions could not
both hold. It also checks that `set` into one collection leaves its sibling
untouched, and that a newly created item is filed where it was addressed.
Skips itself when the fixture is absent, so it stays runnable against a
personal vault.

Also documents in bitwarden_integration.sh what folding the three bw scripts
into one entry point would take, by invocation rather than duplication.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CLI prints nothing at all when it cannot decrypt what it holds — a stale
BW_SESSION does this, and so does a vault that was never synced. Parsing that
as JSON failed with "could not parse `bw list collections` output", which
points at the wrong thing entirely.

Empty output now counts as an empty list, so address resolution reports "No
collection matching 'dev-secrets'" and the listing that follows says to check
BW_SESSION and run `bw sync --force`. Genuinely malformed output is still
reported as malformed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The never-two-filters rule in search_filter_args justified itself by citing
`bw list --help`. Replaced with the observation it now rests on, measured
against bitwarden-cli 2025.11.0: in an organization holding one item that
belongs to one collection and not the other, filtering by the empty
collection alone returns nothing, but adding --organizationid to the same
command returns the item the collection does not contain.

That is OR, and it is why a second filter cannot be restored on the
assumption that two filters narrow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reads secrets from a Dashlane vault through the Dashlane CLI. Convention
secrets read the item titled secretspec/{project}/{profile}/{key}; a ref
names an existing item by title or identifier with an optional field.

The provider is read-only because dcli is: it has no create, add, set,
update, or delete subcommand for any item type, so check_writable states
that reason and set returns it before the CLI prompts for a value.

Lookups go through the -o json listers rather than dcli read. The listers
match by case-insensitive substring, so exact matching happens here, and
an ambiguous title is refused instead of resolved arbitrarily the way
dcli read resolves it. That also gives get_many one listing per content
type instead of one subprocess per secret.

Closes #128

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Vaultwarden 1.37.0 (web-vault 2026.6.4) returns the account's RSA public key
at accountKeys.publicKeyEncryptionKeyPair.publicKey; the top level now carries
only "key" and "privateKey". Organization creation stopped at the profile
fetch, so the harness could never build the org fixture.

Try the measured path first and the older locations after, then fall back to
scanning the response, so the next relocation prints where it found the key
instead of failing outright. The failure message now lists the paths the
response actually contains -- the old one truncated the body mid-base64 and
showed nothing useful.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The organization is created through the API behind the CLI's back, so `bw`
only learns about it -- and about the key it needs to decrypt anything inside
it -- on the next sync. One sync was not reliably enough: the fixture step
failed in 2 of 4 runs.

The failure was also actively misleading. `bw` fell back to an interactive
master-password prompt, read the base64 item payload that was piped to it as
if it were typed input, and reported "Invalid master password" -- pointing at
credentials that were never wrong.

Poll until the organization actually appears, and pass --nointeraction so any
future variant of this errors out instead of prompting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`[project].require_reason` defaults to "agents", so every get and set in these
scripts is policy-denied when they run under a coding agent. The regression
script does not distinguish a denial from a real defect: it reported all four
PR #166 findings as REPRODUCED purely on the strength of the policy error,
which reads as four provider regressions that do not exist.

Declare the reason -- the intended way through the gate -- rather than
disabling the policy. A reason supplied by the caller still wins. With this in
place the findings report 4 fixed, 0 reproduced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This script writes secretspec.toml into the current directory and `rm -f`s it
on the way out, and vaultwarden_harness.sh runs it from the repository root --
where the project's own secretspec.toml lives. Every harness run therefore
destroyed a tracked file; 859ef5e exists only to restore it after this
happened once already.

Move an existing file aside before writing the test config and put it back
from the EXIT trap, so the run is non-destructive from any directory.

Also declare SECRETSPEC_REASON here for the same reason as the sibling
scripts: `[project].require_reason` defaults to "agents", which otherwise
policy-denies every get and set the suite makes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The unit tests prove parsing and matching against fabricated fixtures.
These prove the same code survives a real vault and a real dcli: the
JSON shape, the `dcli status` wording, and the fields a live item
carries are all outside this repository's control.

They stay #[ignore]d permanently rather than as a gap to close. dcli
offers no way to register a device without a real Dashlane account, and
the vault is read-only, so a test cannot create the item it would then
read; the one that needs a specific item takes its name from the
environment and skips when unset.

Counts and lengths only — no title or value is printed, and the operator
test asserts the value does not survive into a Debug rendering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop three const docs that only repeated their own name, the struct's
install and CI-setup sections (the provider page owns those, and this
module is not published in rustdoc), and an inline comment restating the
match arms below it.

Collapse ItemType::subcommand and ItemType::as_str, which had identical
bodies and both had callers, so a reader no longer has to check whether
they differ.

Fix a comment citing `dcli p` as an accepted item type: it is dcli's own
alias but this provider does not take it.
Match the house shape for `ref` examples, which carry `description` and
`providers` on every other provider page.

Stop implying an item's identifier comes only from `dcli password`: a
secret or a note is listed by its own subcommand.

Fix two claims. The reference page called the provider "cloud sync",
which reads as though a get hits the network; dcli reads a locally
synced vault and syncs hourly on its own. And Dashlane Secrets are a
Business plan feature, not a vague "business accounts" one — Dashlane's
own docs say so, so the pages now use their name for it.

Drop a line restating that referenced items are read-only, which the
intro and the at-a-glance table already establish, and trim the
troubleshooting entry that repeated the intro's list of missing dcli
subcommands.
With no content type pinned, `get` searched secrets, then notes, then
logins, and `lookup` returned an error the moment a matching item lacked
the `field` a ref named. A note and a login titled the same -- both named
after the service -- meant the note aborted the search before the login
that carried the field was ever listed.

`lookup` now reports a missing field as a distinct outcome rather than an
error. `get` and `get_many` search every content type first and raise it
only if none produced the value, so the error still catches a typo in
`secretspec.toml` without hiding a reachable secret.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Verified against dcli 6.2628.1 on Linux: an unregistered CLI prompts for
an email address, and the closed stdin turns that into

    error: User force closed the prompt with 0 null

Neither of the guessed substrings, "not logged in" and "No device
configuration", appears anywhere in it, so a user with no device
registered got that message raw instead of instructions. Match the
prompt refusal, which every interactive step produces -- an email, a
second factor, the master password of a locked vault.

Split the guidance in two, now that the causes are distinguishable: a
`dcli status` reporting `Logged in: no` means authentication, while a
closed prompt also covers a locked vault.

The unit test pins the verbatim message so a reworded match cannot
silently regress.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`check_auth` returns early on device keys by design, so on a service
device `preflight_accepts_a_registered_unlocked_cli` passed without
reading `dcli status` at all -- the one configuration where the status
parser most needs covering is the one where the test went quiet. It now
reports the skip and how to exercise the parser instead.
Isolate dcli state per injected credential. dcli reads
DASHLANE_SERVICE_DEVICE_KEYS only inside getLocalConfigurationWithoutDB,
which upstream reaches only when its state directory holds no device row;
with one present, getLocalConfiguration takes that row's login and the
variable is ignored. On a machine already logged in interactively, or
across two aliases carrying different keys, that silently read the wrong
identity's vault. Each credential now gets its own HOME/APPDATA, named
after a hash of the keys rather than the keys.

Match dcli's read precedence. It resolves a name as secrets[0] ??
credentials[0] ?? notes[0], so a login outranks a note of the same
title; the search order had notes second and returned a different value
than dcli would for the same name.

Fold titles with to_lowercase rather than eq_ignore_ascii_case, as dcli
compares them with JavaScript's toLowerCase. An ASCII-only fold left
`Überblick` unreachable as `überblick`.

Correct the sync claims. Every lister enters connectAndPrepare, which
syncs when the last sync is over an hour old, so a read can reach the
network without an explicit `dcli sync`. The provider docs and the
security table said otherwise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`dcli secret` arrived in October 2023. An older CLI reports
`error: unknown command 'secret'` and exits 1, and because secrets are
searched first, that error propagated out of `get` before logins or
notes were tried -- a provider that failed entirely for someone whose
secrets sit in notes. An unknown subcommand now yields no items of that
content type instead.

Secrets are the lister this matters for: newest, and a Business-plan
feature, so most vaults hold none. Their `content` field mapping comes
from `VaultSecret` in dcli's own types rather than an observed vault,
which the code now records.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fix Linux Go SDK libdbus packaging
Resolving the credential-scoped state directory now returns an error instead
of None, so a read aborts rather than handing the device keys to a dcli
pointed at the inherited HOME -- the unisolated case this guards against.

The directory is also created here, owner-only, instead of by dcli: observed
with dcli 6.2628.1 under a 022 umask, dcli leaves dashlane-cli/ at 0755 and
its userdata.db, which holds the device row and the synced vault, at 0644.

Docs no longer promise strictly offline reads for injected credentials.
'dcli configure disable-auto-sync true' records the setting against the
device in the state directory it runs from, so it does not reach the
per-credential one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every fixture in this suite is built with raw `bw`, so nothing exercised an
item the provider itself created. The creation tests asserted only on the
"Secret ... saved" message, which cannot tell a stored secret from a discarded
one -- the exact blind spot R2 lived in.

For each of the five item types: create through the provider, read it back,
update the item the provider just made, and read it back again. The secrets
carry no `ref`, so the item is named after the key and the provider both
writes and finds it on its own terms.

This found a real defect on its first run: SSH key items were unreadable after
create. See ashebanow/secretspec#3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An SSH key item has to carry a non-empty string in all three of privateKey,
publicKey and keyFingerprint. `ssh_key_template` set the two it was not
writing to null, and no server accepts that: Bitwarden cloud refuses the
request ("invalid type: unit value, expected a valid string"), while
Vaultwarden 1.37.0 accepts the upload, stores `sshKey: null` and silently
discards the secret -- `set` reports success and `get` then returns
"[error: cannot decrypt]".

Measured on both servers: arbitrary non-empty strings are accepted, so this is
about presence rather than parseable key material. Initialise all three
members to "(not set by SecretSpec)" and overwrite only the addressed one.

Pinned by two tests. ssh_key_template_never_emits_a_null_member walks every
addressable field, including an unrecognised one that routes to `fields[]`
while the sshKey object still has to be well-formed;
ssh_key_template_puts_the_secret_in_the_addressed_member checks the
placeholders do not displace the value.

Fixes ashebanow/secretspec#3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c59a8f1 applied clippy's collapsible_if suggestion to migrate_macos_config
in secretspec/src/config.rs and added .pi/ to .gitignore. Neither belongs
in the Bitwarden provider PR: config.rs is a file the provider never
touches, and .pi/ is a local scratch directory.

The lint is a warning, not an error. Upstream's clippy hook runs without
-D warnings, so main carries the same warning today and CI stays green;
nothing here depends on the fix. Reverting restores both files to their
upstream state, so the PR diff no longer touches them.

.pi/ is excluded through .git/info/exclude instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Upstream #195 converted the bws provider to shell out to the `bws` CLI and
removed the `bitwarden` SDK crate along with 799 lines of Cargo.lock. This
branch's earlier merge of upstream/main (d8865aa) kept our side of
secretspec/Cargo.toml, restoring `bitwarden` and `rustls` as optional
dependencies and `bws = ["dep:bitwarden", "dep:rustls"]`.

Nothing uses either one. bws.rs at this commit is the CLI implementation with
no reference to the SDK, and `rustls` appears nowhere in secretspec/src. The
declarations pulled roughly 99 crates (bitwarden-*, rusqlite, zxcvbn, mockall
and their trees) into every build and accounted for almost all of this PR's
lockfile churn: the Cargo.lock delta drops from +1453 to +10/-9.

Restores `bws = []` to match upstream. The `base64` dependency and the `bw`
feature stay, since bw.rs uses them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in secret scopes (--scope), the Scaleway Secret Manager provider,
capped get_each concurrency (SECRETSPEC_PROVIDER_CONCURRENCY, default 8) and
the Vault/OpenBao client reuse and connect retries.

Four conflicts, each one both sides appending to the same list:

- secretspec/Cargo.toml: default features, ours adds `bw`, upstream adds
  `scaleway`; kept both.
- docs/astro.config.ts: the starlightLlmsTxt provider sentence; kept both
  entries in their respective positions.
- CHANGELOG.md: two Unreleased provider bullets; kept both, upstream's first
  so its entry stays where it was written.
- Cargo.lock: regenerated.

bw.rs did not conflict, and the Provider trait is unchanged: upstream's
provider/mod.rs edit is internal to get_each. The bw provider does not
override get_many, so it now inherits the concurrency cap, which suits a
provider that spawns one `bw` CLI process per get.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat: add Dashlane provider (dashlane://)
The `secretspec config init` mini-terminal on the landing page listed
`bw: Bitwarden Password Manager` with no version, in both the hero and the
copy further down. Every other unreleased provider in those same blocks
carries one (kdbx, openbao, age, systemd-credential), and quick-start.mdx
already labelled bw, so the landing page was the odd one out.

The sidebar entry spelled its version inline in the label. Every other
versioned entry in astro.config.ts uses a badge, so this one now does too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`bw create org-collection` returns the id the server assigned, but
`bw list collections` answers from the locally-synced vault. A single blind
`bw sync` after creating dev-secrets and prod-secrets was enough most of the
time; when it wasn't, the fixture still printed both ids and the collection
addressing suite failed 9 of 11 with "No collection matching 'prod-secrets' is
visible", naming only the organization's `default` collection.

Poll until both collections are listable, the same shape as the organization
wait immediately above, and fail naming them if they never arrive.

Collection addressing goes from 2 passed / 9 failed to 11 passed / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings the branch up to the real current main, which had moved 42 commits
past the ref the previous merge used: the 0.17.0 release, the Dashlane and
SOPS providers, cached provider aliases with fallback routes, and the Go SDK
libdbus and release-packaging fixes.

Six conflicts, each one both sides adding a provider to the same list:

- secretspec/Cargo.toml: default features, ours adds `bw`, upstream adds
  `sops`; kept both.
- .github/workflows/test.yml: the standalone-feature loop; kept both.
- secretspec/src/provider/mod.rs: the module doc provider list; kept both.
- docs/astro.config.ts: the starlightLlmsTxt sentence; took upstream's, which
  adds Dashlane and SOPS, and reinserted Bitwarden Password Manager.
- docs/src/pages/index.astro: providerMetadata; took upstream's entries,
  including their rename of the `bws` label to plain "Bitwarden", and added
  ours alongside.
- CHANGELOG.md: upstream cut 0.17.0, so its Unreleased section is now the
  short post-release one while ours still held everything 0.17.0 shipped.
  Took upstream's file whole and reinserted only the bw bullet under the new
  Unreleased, rather than reconciling two divergent Unreleased sections.

The merge also carries upstream's removal of `.envrc` (7cb830b, direnv
replaced by `devenv allow`) and the empty root `secretspec.toml` (a479b4f).
Those are the only two deletions.

bw.rs did not conflict and the Provider trait is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Upstream removed the checked-in root secretspec.toml in a479b4f, so two claims
in this comment no longer describe the repository: that the harness runs "where
the project's own secretspec.toml lives", and that the unconditional write "has
already deleted that tracked file".

The guard still matters, for a reason that does not depend on upstream tracking
the file. Anyone dogfooding secretspec inside its own checkout can have an
untracked secretspec.toml at the root, and upstream's own
tests/cli-integration.sh writes one there too.

Comment only. The guard is already conditional and correctly no-ops when there
is nothing to preserve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tree has no generative testing crate today: no proptest, quickcheck,
arbitrary or loom. Dev-only, so nothing reaches a shipped binary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
format_secret_name states injectivity in prose and is guarded by assert_ne!
over one pair each -- the counterexamples already found (b__c/c__d, b-/_C,
API_KEY/api_key). The properties state the claim over the whole component
domain instead, so the next collision fails a test rather than a user's get.

Injectivity is checked through a left inverse rather than sampled pairs: two
independently generated triples essentially never collide, so that phrasing
passes against a scheme that is provably not injective. Decoding every name
back to its triple fails on the first case under a lossy scheme -- the old
`_`->`-` mapping fails it immediately.

The generator weights short `_`/`-` components deliberately. Every collision
this scheme has had lived in those characters against the `--` delimiter, and
an unbiased alphanumeric generator reaches that region far too rarely to find
one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The set's own doc says it "makes ProviderUrl::encode_query a true inverse of
that parsing, so query values round-trip" -- a claim about every string,
exercised today by one provider's list of eight hand-chosen values.

The characters that break it are the ones form-urlencoded parsing claims: `&`
splits a pair, `+` becomes a space, `%` starts an escape, `#` ends the query.
Each mangles a value silently rather than failing, so the store a provider
talks to is not the one the URI named.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Upstream documentation suggests sops always parses the `path_regex` attribute as a regular expression. This subtly causes the un-escaped `.` (wildcard operator) to match against any character other than line endings.

Updating this in the docs reminds the reader to be cognizant of regex syntax.

This is consistent with the regex escape pattern found in sops documentation here: https://getsops.io/docs/usage/identities/config-file/
validate_env_key is reached through flat_item, which resolves a convention
address and a native one into the same string, so the advice to "rename the
`ref` item" was unconditional. For a secret declared in secretspec.toml it
points at something the user never wrote.

The comment above it explained the message by saying convention names come
from validated declarations, so only refs reach here. That does not hold:
is_valid_identifier is Unicode-aware, so `café` is a legal secret name and
dotenv cannot store it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(dotenv): name the manifest secret, not a ref the config may not have
Enable aws-config/credentials-login for awssm feature
docs: escape regex dot operator in .sops.yaml example
test: property tests for the naming and encoding invariants
Keep derived as_path files alive
Add Keeper Secrets Manager provider
The review found six ways an address could resolve to the wrong secret.
All six shared a cause: the provider treated a name as a search term
rather than as a coordinate.

- Reads took the first result of `bw list --search`, which matches
  substrings across name, username, URI and notes. A request for
  API_KEY could answer with API_KEY_OLD, non-deterministically.
- Writes fell back to a substring match when no exact name existed, so
  setting API_KEY with only OLD_API_KEY present overwrote OLD_API_KEY
  and never created anything. Unrecoverable, and reported as success.
- `?type=` was consulted only when creating an item, so it could not
  tell a Card from a same-named Login on a read or an update.
- An explicit Secure Note field that was absent fell through to the
  legacy `value` field and then to the note body, answering a request
  for one secret with another. The other four item types already
  returned None here.
- Creation did not recognise the built-in field aliases that reading
  and updating do, so `set --field exp_month` stored a custom field
  while `get` read the untouched card.expMonth and found nothing.
- `uri()` dropped `type`, `field` and `folder`. SecretSpec fingerprints
  cached routes with that string, so repointing a source from
  `?field=password` to `?field=api_key` left the cached password fresh
  and served it for the API key.

Item resolution is now one function used by reads and writes alike:
full-name match, then the addressed type, then a hard error listing the
colliding ids rather than a guess. This is what `bw get item` does; the
CLI accepts a substring only because it prints the candidates for a
human to choose between, a backstop a config file does not have.

Names still fold case, because `bw` folds case -- with `to_lowercase`
rather than `eq_ignore_ascii_case`, so `ÜBERBLICK` stays addressable as
`überblick`. Organization and collection resolution now folds the same
way; it was ASCII-only, and diverged from the CLI for non-ASCII names.

Also found while verifying: `?folder=` was dropped from `uri()` too,
`server=` was interpolated without percent-encoding, and `bw://?org=`
alone rendered as `bw://myorg@`, whose empty host re-parses to no
organization at all.

An unsupported `?type=` or an unknown query key is now rejected when the
address is parsed. Both were discarded silently, so `?type=sshkee`
created a Login and `?feild=api_key` did nothing whatsoever.

The alias tables that reading, updating and creating each maintained by
hand are now one table, which is what let them drift apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three faults, each of which made a run report something other than what
happened.

`ensure_item` adopted a pre-existing item as a fixture when the name was
already taken. The names were ordinary enough to collide in a real vault
-- "Test Database", "GitHub API", "Payment Gateway" -- and the suite
advertises running against one. The adopted id never reached
CREATED_ITEM_IDS, so cleanup neither deleted nor restored it, while a
later test replaced its password. Fixtures now sit behind a distinctive
prefix and the suite refuses to touch a name it did not create.

Cleanup swept with `bw list --search`, which matches notes, usernames and
URIs as well as names, so any vault item merely mentioning the prefix was
deleted. It now matches on the name prefix alone. Verified against a
fixture list: "My Real Bank" and "Notes about secretspec-it stuff"
survive where they previously would not have.

The suite counted failures rather than propagating them, so it ended on a
successful `echo` and exited 0 no matter what. vaultwarden_harness.sh
reads that status, which left the documented harness unable to fail on a
provider regression.

The harness now runs every suite and takes the worst status instead of
aborting on the first failure under `set -e` -- one unrelated integration
failure used to hide every regression finding, which is precisely the
report you want when checking what is still reproduced. It also isolates
XDG_CONFIG_HOME the way it already isolates BITWARDENCLI_APPDATA_DIR: a
`[defaults] profile` in the developer's own config failed every get and
set with "Invalid profile", which reads as total provider failure and has
nothing to do with the provider. Reported by bsorescu.

R5-R12 extend the findings script for this review round, in the shape R1-R4
already use. Each reproduces against a live vault before its fix and
reports FIXED after, so the harness stays the thing that decides.

The abort path disarms cleanup explicitly. It currently touches nothing
only because the EXIT trap is installed one line after setup_test_data;
moving that trap earlier -- which reads like a safety improvement --
would turn a check that refuses to modify someone's item into one that
deletes it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Case handling is decided per provider in this repo, not globally: env.md
documents case-sensitive matching, dashlane.md case-insensitive, and each
justifies itself by reference to its own backing store. bw.md said how
organization and collection names match but nothing about how the `item`
in a `ref` does, so the rule a user needs was the one not written down.

Records full-name, case-insensitive matching, the refusal to guess
between same-named items, and that `?type=` narrows reads and writes
alike -- with the 0.18+ label these pages need, since the site publishes
from main ahead of the release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	.github/workflows/test.yml
#	CHANGELOG.md
#	docs/astro.config.ts
#	secretspec/Cargo.toml
Add AWS Parameter Store provider
Compare cache targets with credential-free physical storage identities so Vault and OpenBao configurations cannot disguise the same endpoint, namespace, and mount as distinct stores. Keep route fingerprints configuration-sensitive so authentication and provider changes still invalidate cached values.

Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
Add Swift SDK support
fix(cache): detect Vault-compatible store aliases
Upstream's error-reporting pass (15e3f72) wrapped stringified errors with
context and routed them through `display_error_chain` across sixteen
providers. It could not touch this one, which does not exist upstream, so
after the merge `bw` was the only provider not following the convention.

Three sites returned the CLI's stderr as the entire error. A `get` or a
`set` runs several `bw` calls, so the reader was left to guess which had
failed, and the exit status was dropped on the floor. These now name the
invocation and its status, and fall back to stdout when stderr is empty --
`bw` is not consistent about which stream carries a diagnostic. Naming the
command is safe: secret values never reach argv, only base64 JSON on stdin.

`display_error_chain` does not help there, because a subprocess's output is
not a `std::error::Error` and has no `source()` to walk. It does help at the
seven sites that stringified a real error bare -- spawn, stdin write, wait,
and UTF-8 decoding -- where the underlying cause was being discarded. Four
sites that already had context keep their wording and gain the chain; two of
them wrap a SecretSpecError whose Io variant carried a source that was lost.

The "bw is not installed" branch and the two authentication messages are
untouched: they are better than anything generic, and stay ahead of it. A
test covers the install instructions, since that is the sort of thing a
rewrap quietly eats.

The new tests synthesize a finished process, which needs ExitStatusExt and
so is unix-only, gated the way bws gates its own process tests. What they
assert is string formatting, which does not vary by platform.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`get` narrowed with `bw list items --search <name>` and treated an empty
result as "no such item". That search is the CLI's own fuzzy matcher, and
it has been wrong: before bitwarden/clients e1aa943b (2026-07-13, first
released in CLI 2026.7.0), `searchCiphersBasic` stripped diacritics from
the query but not from the item names it compared against. On any older
CLI -- 2026.6.0 shipped three weeks earlier -- `--search überblick`
returned nothing for an item named `Überblick`, so the candidate was
discarded before this provider ever compared it, and the secret was
reported missing.

An empty narrowed result now means "the prefilter matched nothing" rather
than "the secret is absent", and the read falls back to the full listing.
The narrowing stays, because it is worth having on a large vault; it is
just no longer authoritative. A genuine miss costs a second `bw` call as
a result, which is the deliberate trade.

`set` has always listed unfiltered, so both paths now go through one
`list_items` helper and consider the same candidates -- the asymmetry
where a read could rule out an item a write would have found is gone.

Not addressed: `to_lowercase` compares without normalizing, so a name
that round-trips as NFD still will not match an NFC `ref`. That is ours
rather than the CLI's, needs a normalization dependency, and no report so
far points at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R5, R6, R7, R11 and R12 all discarded `mk_item`'s exit status, and the
script runs without `set -e`, so a fixture that was never created reported
exactly what a provider that could not find one reported: `got ''`. That
made a third-party report of R11 impossible to diagnose from its output --
the umlaut item is the only non-ASCII value in the file, so it is the one
fixture whose creation can fail where the others' cannot, and the two
explanations were indistinguishable.

`require_item` now creates a fixture and confirms the CLI can see it under
that exact name. When it cannot, it lists what did land, with byte-level
hex for anything non-ASCII, so a name mangled on the way in -- NFC arriving
as NFD -- is visible rather than inferred.

The harness also notes when `bw` predates 2026.7.0, whose
`searchCiphersBasic` fixed the diacritic normalization that R11 turned out
to depend on. A note rather than a gate: the provider no longer depends on
that fix, so an older CLI is precisely the environment worth exercising,
and refusing to run there would hide the case instead of testing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	docs/astro.config.ts
#	secretspec/Cargo.toml
Allow writing AWS Parameter Store refs
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
fix(providers): reuse Vault-compatible logins per operation
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
Record release 0.17.1
The 0.17+ version is already shown via the sidebar badge
docs: remove redundant version suffix from SOPS label
SECRETSPEC_BIN points the bw test suites at a pre-built secretspec
binary and skips the local cargo build. Needed to run the suites
against an instrumented coverage build; the default behavior is
unchanged.
- scripts/coverage-bitwarden.sh: instrumented build (-C instrument-coverage),
  bw unit tests, vaultwarden-harness suites, profile merge, per-file report,
  and history append. Run via 'devenv shell -- scripts/coverage-bitwarden.sh'.
- scripts/worst-functions.py: per-function coverage drill-down with demangled
  names (--n, --sort pct|missed, --targets).
- scripts/coverage-bitwarden.md: historical coverage tables.

Coverage on this branch: bw.rs unit lines 75.2% (unit+all 86.5%). See
ashebanow/secretspec#5 for the 90% goal.
Unit-test the extraction, mutation and resolution helpers that need no
bw subprocess: every named-field alias and its case folding, the
unqualified-read fallbacks, exact-vs-partial custom-field matching,
custom-field creation (and hidden-vs-text storage), convention
addressing, scope placement, and the env/config resolution precedence.

Unit-test line coverage for bw.rs: 75.2% -> 82.6% (issue #5; 80% floor
met). The remaining gap to 90% is the subprocess-spawning paths, which
need a fake bw fixture on PATH.
feat(providers): support custom Vault-compatible auth mounts
The middle ladder rung is a strict subset of unit+all and adds no
information; keep unit, unit+integration, unit+all.
The last pure-path gaps: the explicit password selector, the legacy
value-field fallbacks when an item lacks its data object, non-builtin
custom-field reads on card/identity/ssh, the identity username default,
and the ambiguous-organization error message.

Unit-test line coverage for bw.rs: 82.6% -> 84.25% (issue #5).
The not-logged-in and locked-vault match arms in is_authenticated could
never fire: execute_bw_command rewrites that stderr into its own messages
before the case-sensitive checks ran, and the locked rewrite spells
"vault is locked" in lowercase. A locked or logged-out vault therefore
surfaced as an error rather than the documented Ok(false), and get/set
reported different text than the auth-required guidance used elsewhere.

Match both the raw phrases and the rewrites, case-insensitively.

refs #5
Unit tests could not exercise any code that spawns `bw`: a real CLI would
answer from — and write to — the developer's own vault. A test-only `bw`
shim (tests/fixtures/bw-shim.sh) is installed into a per-test directory put
first on PATH; it answers fixture files, records every invocation (argv plus
any base64 stdin payload) in invocations.log, and can inject failures.

The FakeBw harness (PATH under a mutex, BITWARDENCLI_APPDATA_DIR isolated,
restored even on panic) drives 49 new behavioral tests covering
check_server, execute_bw_command and its error mapping, is_authenticated
states, list/get/create/edit flows, scope resolution, the search fall-back,
and the NotFound/CLI-missing branches — the last paths to 90% unit coverage.

refs #5
find_test_bin globbed secretspec-* in the deps dir, which accumulates
thousands of .rcgu.o files: the expansion exceeds macOS ARG_MAX, ls fails
silently, and the report stage died. It also picked among several plausible
test binaries in filesystem order, sometimes choosing a stale one and
reporting 0%. Selection is now a Python one-liner that picks the newest
executable by mtime (portable, no ARG_MAX).

cmd_unit/cmd_suite now wipe their own raw dirs first: previously raw/unit
accumulated profraws across builds, so a rebuilt binary was merged against
stale counters that llvm-profdata drops as mismatched, understating
coverage (a batch measured 66% before the clean run, 95% after).

refs #5
Replaces the padlock mark and Arial wordmark with the new document/keyhole
icon and geometric SECRETSPEC wordmark.
Refresh SecretSpec brand logo
# Conflicts:
#	CHANGELOG.md
feat: add Bitwarden Password Manager provider
The fake bw is an extensionless POSIX shell script installed on PATH, and
on Windows Command::new("bw") goes through CreateProcess, which cannot
execute such a file. The shim tests can never run there: every spawn hit
the missing-CLI path, and the first failed assertion panicked while
holding BW_SHIM_LOCK, poisoning the mutex and cascading PoisonError into
all 47 other shim tests (cachix/secretspec#166 windows job).

A compiled fake is not an option either: these are unit tests with access
to provider internals, and CARGO_BIN_EXE_* is only available to
integration tests. Gate the harness and its tests to cfg(unix); the pure
helpers (extractors, deserializers, round-trips) still run on Windows.
is_authenticated folded the missing-CLI error into "not authenticated":

because its guard matched the loose substrings "bw login"/"bw unlock",
which also appear in the install instructions ("…run 'bw login' and 'bw
unlock' to authenticate"). On a machine without bw every get/set then
reported a bogus 'Please run bw login and bw unlock' prompt instead of
the install error. This also turned one failing shim test into a 48-test
PoisonError cascade on the Windows CI job (cachix/secretspec#166).

Match only the phrases unique to the not-logged-in and locked states, and
add a test locking in that a missing CLI propagates as an error.
fix(bw): gate fake-CLI tests to unix; report a missing bw CLI
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
fix(providers): honor JWT default roles
Add secret declaration command
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
fix(providers): allow AppRole without SecretID
Add provider-backed declaration discovery
Add stored secret deletion and import cleanup
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Release 0.18.0
The `age` provider was added in version 0.17. The section of the docs that lists provider credentials was missing an entry for the recently added age provider.

This documents the singular credential accepted by `age` to this docs page; it is the `identity` credential; it is placed meaningfully next to all the other provider credential keys.
docs: document v0.17 `age` provider credentials
Speed up Rust CI
Centralize provider credential documentation
Release 0.18.0: finish pre-release setup
Publish SecretSpec 0.18 announcement
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
ci(actions): pin external actions to commit SHAs
Bumps [cachix/cachix-action](https://github.com/cachix/cachix-action) from 16 to 17.
- [Release notes](https://github.com/cachix/cachix-action/releases)
- [Changelog](https://github.com/cachix/cachix-action/blob/master/RELEASE.md)
- [Commits](https://github.com/cachix/cachix-action/compare/v16...5f2d7c5294214f71b873db4b969586b980625e71)

---
updated-dependencies:
- dependency-name: cachix/cachix-action
  dependency-version: '17'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
fix(devenv): use maintained Docker 29 client
build(deps): bump cachix/cachix-action from 16 to 17
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.4.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.4.0...3d3c42e5aac5ba805825da76410c181273ba90b1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.6.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](40f1582b24...b7ad1dad31)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 7.0.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](ea165f8d65...043fb46d1a)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4.3.1 to 6.0.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](67a3573c9a...a98b56852c)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
build(deps): bump actions/checkout from 4.4.0 to 7.0.1
build(deps): bump actions/upload-artifact from 4.6.2 to 7.0.1
build(deps): bump actions/setup-go from 5.6.0 to 7.0.0
build(deps): bump actions/setup-dotnet from 4.3.1 to 6.0.0
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.3.0 to 8.0.1.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4.3.0...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
fix(keyring): avoid concurrent initialization race
build(deps): bump actions/download-artifact from 4.3.0 to 8.0.1
`render` substituted `{project}` and then `{profile}` into its own
output, so a value that looks like the other placeholder was substituted
a second time. A project literally named `{profile}` turned
`secrets/{project}/{profile}.yaml` into
`secrets/production/production.yaml` — a different file on disk than the
one configured. Neither project nor profile names are validated, so both
are user-controlled.

Walking the template once copies substituted values out rather than
rescanning them. Unknown placeholders and unclosed braces are kept
literal instead of panicking: `validate` rejects both, but
`SopsPathPattern` also derives `Deserialize`, which does not run it.

Adds unit tests for `SopsPathPattern`, which had none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAd1mkoXUH8D1CZT99F1P1
fix(providers): render the SOPS path template in one pass
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
ci(dependabot): group non-major action updates
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](49933ea528...8207627860)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.6.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](a26af69be9...5fda3b95a4)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
`uri()` reported only the first segment of the item template, and that
string is read back as a provider spec: `secretspec-derive` hands it to
`set_provider`, and cached routes are fingerprinted with it. A template
therefore read back as something else entirely -
`Shared/{project}/{profile}/{key}` as the default
`secretspec/{project}/{profile}/{key}`, a different folder, and
`Work/TeamA/{key}` as the literal item `Work`, one item for every secret
in the profile. Templates differing below their first segment also
fingerprinted alike, so repointing a cached route kept serving the old
template's values until they expired.

Keyring carried the same truncation until dd755d2, which fixed keyring
and re-encoded lastpass's truncated folder without removing the
truncation.

The removed `folder == "Shared"` branch is what mapped `Shared/...` onto
the default template. It dates to 1dad1c7, where keyring's `uri()` was an
unconditional constant too, so it does not appear to encode LastPass
semantics.

`entry_container_identity` is added because the trait reserves it for
providers whose public URI carries an addressing template, which this
change makes true of lastpass.

Adds `uri()` tests for this provider, which had none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(lastpass): report the full item template from uri()
build(deps): bump actions/setup-node from 4.4.0 to 7.0.0
build(deps): bump actions/setup-python from 5.6.0 to 7.0.0
Fix Azure Key Vault fallback resolution performance
Add secret encoding
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
ci(workflows): harden release input handling
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
- Adds pkg-config support in the Haskell SDK
- Adds missing system frameworks for macOS builds to the Haskell SDK
Cabal seems to have a bug where it reverses the options list when multiple occurances of ghc-options are used.
That breaks `-framework` pairs on macOS.
Support linking the SDKs against secretspec-ffi via pkg-config
docs: add Docs button to the header
Move SDK snippets into Cargo examples, share them with the documentation
site, and refresh the guide to match the current typed API.
docs: front page feature showcase animation
Upgrade cargo-dist to 0.29.0 so aarch64-pc-windows-msvc can be
cross-compiled on the existing windows-2022 runners.
Add Windows ARM64 CLI release artifacts
docs: use compiled examples in the Rust SDK guide
Store cache expiration deadlines
ci(workflows): pin build container images
Fix mobile landing hero flow
examples: remove redundant examples in favor of examples in secretspec-derive
Add provider-scoped secret refs
Make SDK documentation examples buildable
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
ci(workflows): restrict token permissions
Add the 0.19+ inline cache form so a provider alias can declare a cache policy directly on its authoritative URI while retaining its credentials.

Make single-secret resolution route-aware, redact same-store errors, and reject leaf-only route uses before credential-provider I/O.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Represent leaf, inline-cached, and fallback-cached aliases as distinct non-exhaustive enum variants. Preserve the existing TOML forms while removing impossible URI, fallback, and cache combinations from route planning.
Document the 0.19+ inline cache form alongside cached fallback routes, then add a practical workflow for measuring and diagnosing remote-provider latency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
Fix provider ref profile inheritance
ci(workflows): stop persisting checkout credentials
Signed-off-by: Roel de Cort <roel.decort@adfinis.com>
ci(workflows): verify rustup bootstrap
Add null provider for defaults and ephemeral generation
feat: attach cache to provider aliases
Allow profiles to opt out of default inheritance
Add file provider and generic JSON secret extraction
Add a passbolt:// provider backed by the community-maintained go-passbolt-cli. Convention secrets map to a resource named secretspec/{project}/{profile}/{key} with the value in the password field; refs can select an existing resource by UUID or exact name and address the standard password, username, URI, and description fields.

Support SecretSpec provider credentials, shared authentication preflight, batched reads, and folder-bounded declaration discovery. Document the provider and its known CLI, MFA, resource-type, process-argument, and folder-permission limitations for SecretSpec 0.19.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(provider): add Passbolt provider via go-passbolt-cli
docs: Use SDK examples as documentation sources
pass-cli 2.2.4 removed the `test` subcommand the provider ran as its
preflight before every read and write, so every Proton Pass operation
failed with `unrecognized subcommand 'test'`.

The session check now tries `pass-cli info` first and falls back to
`pass-cli test`, so a single build works across pass-cli releases that
disagree about which check exists. `info` is preferred because it runs
behind pass-cli's authentication gate and reports whether a valid session
is present, while `test` only proved that Proton's servers were
reachable. Only a missing subcommand advances the chain: an
unauthenticated session is the probe's answer and is returned as is. A
pass-cli carrying neither check is reported as incompatible with the
SecretSpec release instead of passing the CLI's usage text through.

Also documents the pass-cli compatibility history and how to pin a tested
build, since Proton support states that backward incompatible changes can
ship in patch releases without advance notice.

Fixes #279

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The unsupported-coordinate error tells the user to drop the coordinate from the
ref. That is the right fix only when every endpoint should share one address.
When the coordinate is meaningful to the store the ref was written for — a
Bitwarden or 1Password item field, say — and another store simply organizes the
secret differently, dropping it is lossy or impossible, and the message leaves
no way forward.

Since 0.19 there is one: `refs.<alias>` or an alias `ref` template gives that
endpoint its own address. Name it in the error, next to the existing remedy.

The reported case in #266 (a manifest with `field` refs importing into a store
without `field`) now ends at a message that points at the mechanism that solves
it instead of at the one edit the user cannot make.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(providers): offer a per-provider address when a coordinate is unsupported
Closes #312
Docs copyable examples
docs: standardize terminal examples
etcetera's Windows strategy ignores XDG_CONFIG_HOME, so the subprocess never
saw the user config the test writes and the import failed with no provider
backend configured. Point the config home at APPDATA there too, and write
config.toml to the extra `config` component that strategy nests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fix import alias warning test on Windows
docs: standardize product names
Prompt for missing secrets during run
this reworks getting started to be more accessible by focusing on
the simplest concepts one by one
docs: validate internal links
docs: rework getting started section
docs: standardize provider guides
Bumps the workspace and every SDK packaging file to 0.19.0, refreshes the
SwiftPM checksum against a rebuilt XCFramework, and dates the changelog.

Beyond the version bump this release carries two API changes made on the
release branch:

- Secrets::resolve_named resolves one secret and the inputs it composes from,
  so an unrelated missing required secret no longer sinks the call the way the
  batch resolve() does, and the result tells an undeclared name apart from a
  declared secret with no value. Secrets::with_default_reason fills in a reason
  only when none is in effect. `secretspec get` becomes a printer over that same
  path rather than a second single-secret implementation, keeping its unscoped
  surface and its audit coordinates.
- Provider URIs no longer carry credentials. A URI with a password is rejected
  for every scheme, and onepassword+token:// no longer takes the token in its
  userinfo; the errors name the credential each provider accepts. A
  specification that fails to parse is redacted before it is reported.

Windows ARM64 CLI artifacts are deliberately not part of this release: dist
refuses a target whose standalone updater it cannot fetch, and axoupdater
publishes no aarch64-pc-windows-msvc binary (axodotdev/axoupdater#313).
Shipping it would have meant dropping the updater everywhere, so the target
stays out of dist and those hosts keep running the x86_64 build under
emulation, exactly as on 0.18. See the restore-windows-arm64 branch for
bringing it back once upstream ships that asset.

Granular history for the two API changes is preserved in #315.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Release 0.19.0
Restores what 0.19.0 dropped: cli-windows-arm64.yml cross-compiles the CLI on
windows-2022, packages it exactly as dist does, and attaches it to the release,
while dist keeps the updater for the five targets it can serve.

This is the workaround, not the fix. It exists only because dist refuses a
target whose standalone updater it cannot fetch and axoupdater publishes no
aarch64-pc-windows-msvc binary (axodotdev/axoupdater#313). Once that asset
ships, delete this workflow and put the target back in dist's `targets`
instead, adding always-use-latest-updater if dist's pinned axoupdater still
predates the release carrying it.

The changelog entry opens a new unreleased section, since 0.19.0 ships without
this, and states what the installer actually does on Windows ARM64: it selects
the x86_64 build, which runs under emulation, so the native archive is a direct
download.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two defects made v0.19.0 ship without its CLI binaries or XCFramework.

The changelog illustrated the rejected URI form as `scheme://user:secret@host`.
The runner masks credential-shaped URLs, cargo-dist embeds the changelog in its
manifest, and GitHub then refused to pass that manifest between jobs at all:

    ##[warning]Skip output 'val' since it may contain secret.

Every job gated on `fromJson(needs.plan.outputs.val)` skipped, so no archives
and no installer were built, while the release was still created and announced.
The example is now unmistakably a placeholder, in the changelog and in the
matching docs sentence.

The SwiftPM checksum was computed before the resolver changes and the merges
from main landed, and the XCFramework embeds the compiled FFI, so it no longer
described the tagged tree. It is updated to the checksum the tag build produced
from that tree, which the publish job correctly refused to accept as-is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Republishes 0.19.0's command-line artifacts. 0.19.0 published to every language
registry but its GitHub Release carried no CLI archives, no installer, and no
XCFramework, so the install script and SwiftPM resolution both failed for it.
The library and CLI behave exactly as in 0.19.0; Windows ARM64 artifacts, which
landed on main after 0.19.0 was cut, ship here.

The SwiftPM checksum is deliberately not updated in this commit: the XCFramework
embeds the compiled FFI, so it has to be computed from the final tree and
committed immediately before the tag. Computing it too early is what broke
0.19.0's Swift publish.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SHA-256 of the CSecretSpec.xcframework.zip built from the 0.19.1 tree. It
differs from 0.19.0's because the version string is compiled into the FFI the
XCFramework embeds, so the bump alone changes the archive. Committed last, with
no code landing after it, which is the ordering 0.19.0 got wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deletion decrypts the blob, removes one key, and re-encrypts to the
current recipients, mirroring set. A missing blob or absent key reports
Ok(false) idempotently — without creating the file or re-encrypting the
unchanged plaintext under fresh randomness.

The registry declares deletes: true, so the plan-time cache-store gate
now admits age: an age file can back a cached provider alias — an
encrypted-at-rest cache with no keyring daemon or OS keychain — and
secretspec delete / import --delete-source work against it. The gate's
"Cache into one of:" list picks the provider up automatically via
deleting_provider_names().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds age to every hardcoded delete-capable/cache-eligible provider
listing and to its own page's Access row, version-labeled (0.20+) per
the docs convention for unreleased capabilities.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Release 0.19.1
Age provider delete
cli: add shell completions
Add Fly.io secrets provider
Document manual KDBX entry setup
Refactor provider module
onepassword: missing (optional) values for degrade performance from batch fetch to per-item fetch
A ref names a folder and key, and Infisical partitions a project by
environment as well, so `locate` had nothing to resolve one with unless
the URI pinned `?env=`. Every native address without it failed, which
made the alias ref template unusable here: a template exists to vary an
address by profile, and pinning `?env=` fixes the environment for all of
them, so flat naming cost one alias per environment.

The profile is the environment a convention address in the same run would
read, so a ref now falls back to it, and `?env=` still wins where it is
set. One consequence is worth stating: a ref pointed at an environment
that does not exist now reads as an unset secret rather than failing,
since Infisical answers a missing secret, folder, environment and project
with the same 404.

The profile reaches the provider through a new `Provider::set_profile`,
applied at the construction chokepoint from the profile the caller
already resolved. It takes `&self`, like `set_reason`: the hook is shared
by every provider, and a preflight-enabled one is wrapped as
`Box<Arc<P>>`, which a `&mut self` hook cannot reach through. It is
session context, never naming, so `uri` and `storage_identity` are
unchanged and a store keeps one cache identity across profiles.

A credential source is built without one, deliberately. A credential
belongs to its alias rather than to a profile, and `CredentialSource`
promises to round-trip whichever profile stores and reads it — so a
`ref`-addressed credential would otherwise be written to one environment
and looked for in another. Such a ref keeps needing `?env=`.

Infisical also describes its write target with the environment now, since
the environment is half the destination and, unpinned, appears in neither
the coordinates nor the URI the preview is otherwise built from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(infisical): read a ref's environment from the profile
Co-authored-by: domenkozar <126339+domenkozar@users.noreply.github.com>
Fix provider path boundary joining
Co-authored-by: domenkozar <126339+domenkozar@users.noreply.github.com>
feat: add structured caller context
The documented IAM policy put secretsmanager:BatchGetSecretValue in a
statement scoped to arn:aws:secretsmanager:*:*㊙️secretspec/*. AWS
treats it as an account-level action that takes no resource, so a
resource-scoped statement never grants it.

get_many() batches through BatchGetSecretValue (awssm.rs:330, reached from
secrets.rs:2346), and that is the path check and run resolve through — so
anyone copying the policy hit AccessDeniedException on the two most common
commands.

Split into two statements: the batch action on "*", everything else still
scoped to the secretspec prefix. ListSecrets is deliberately not added; the
provider addresses secrets with secret_id_list rather than filters, and only
the filter form requires it.

The note now says why "*" is required and, since granting anything on "*"
invites a second look, why it does not widen access: AWS still requires
GetSecretValue for every secret a batch returns, and that stays ARN-scoped.

Closes #342
check_deletable only resolved an address's coordinates and returned
Ok, saying nothing about whether the provider implements delete at
all. Every provider inheriting the default delete (which errors)
still passed the deletion preflight, so import --delete-source could
write the destination during its copy phase and only then discover
in its deletion phase that the source provider cannot delete.

supports_delete defaults to false in lockstep with the default
delete, and the ten providers that override delete (age, dotenv,
file, fly, gopass, keeper, keyring, openbao, pass, vault) now
override it too. check_deletable rejects providers that answer false
before resolving coordinates, sharing the same error delete itself
returns.
property_type only emitted a property's type, so a secret's declared
description never reached the generated schema even though the IR
already carries it (build_union/build_profile_fields populate
IrField.description from the manifest). quicktype turns a JSON Schema
"description" into a native docstring in every target language, so
this was a silent gap between what the manifest declares and what a
generated SDK type documents.

property_type now inserts a "description" key when the field has one,
and omits it otherwise rather than emitting an empty or null value.
fix(provider): add Provider::supports_delete capability
feat(codegen): emit description in JSON Schema properties
Three call sites select one value out of a JSON document and then render it:
the awssm and scaleway providers, which take a flat `field` key, and
Secrets::extract_stored_value, which takes a JSON Pointer. All three had
grown the same match -- string clones, everything else stringifies -- so a
JSON null became the four-character secret "null".

On a provider that is wrong. A null carries no value, so the secret is not
set there and the provider chain should continue; instead it satisfied a
required secret and reached the program as a password spelled n-u-l-l. bw
and dashlane already treat a null as absent.

On an extract it is right, and deliberate: the pointer names one location
and reports what the document holds there.
test_json_extract_resolves_structured_values_after_decoding pins that
end to end, so it is preserved.

Rendering now lives in one place, crate::json_field, with the two policies
named rather than left to coincide: render() for an extract pointer, and
render_field() for a provider lookup, which returns None for a null.
Selection stays with each caller, since a flat key and a JSON Pointer are
not interchangeable -- a field literally named "a/b" would change meaning
under pointer syntax.

Tests cover both policies, that they agree on every non-null value, the
provider null cases, and that an extract still renders a null. Dropping the
null arm from render_field fails five of them.
Fixes #73
fix(awssm,scaleway): a JSON null field is no value, not the string "null"
Resolved.close() exists so secret-bearing temp files do not outlive the
result. The Python and Ruby SDKs stopped at the first file the OS refused
to remove, so every later secret stayed on disk — the outcome close() is
there to prevent — and the caller had no way to learn which ones survived.

Demonstrated with three as_path secrets where the second cannot be
removed: two of the three files remained, contents intact.

The Go and .NET SDKs already do this correctly, recording the first
failure and attempting the rest (secretspec.go:152 firstErr,
Models.cs:90 firstError). .NET catches IOException specifically, the
ordinary Windows sharing violation raised when another process still
holds the file open, so a failed delete is already treated as an
expected condition elsewhere in the project. Both SDKs here now follow
that same contract: clean up everything, then raise the first error.

Ruby additionally no longer skips a dangling symlink. The guard was
File.delete(path) if File.exist?(path), and File.exist? follows
symlinks, so a broken link reported absent and was never removed;
rescuing Errno::ENOENT instead also closes the check-then-delete race.

Tests cover the ordinary path, idempotency, that a refusal does not
strand the other files, and that the first error is the one raised.
Reverting either fix fails the stranding test specifically.
fix(sdk): close() must attempt every as_path file
Correct typos and improve clarity in the blog post about forking dotenvy.
fix(bws): use vault host for bare provider URIs
docs(awssm): grant BatchGetSecretValue on "*", not a secret ARN
ci: skip irrelevant suites and parallelize SDK tests
ci: reuse native SDK builds
Add Rust-first Spec API
fix(infisical): diagnose missing environments
fix(node): release AWS state before process exit
feat(provider): add Azure App Configuration
The Google Cloud Secret Manager convention joined project, profile, and
key with single hyphens, so `my-app/prod/K` and `my/app-prod/K` both
addressed `secretspec-my-app-prod-K`. Two logical secrets shared one
stored value, and neither address could tell it had collided.

Store convention secrets as `secretspec2--{project}--{profile}--{key}`.
Components may contain single internal hyphens but cannot start or end
with one or contain `--`, so a delimiter can never be consumed or
overlapped and every accepted triple maps to a distinct id. The versioned
prefix keeps the new namespace clear of every id 0.19 could produce.
Property tests cover the injectivity the collision fix depends on.

Reading a secret stored by 0.19 falls back to the old id when the new one
holds no value, and warns once per run. The fallback only reads: nothing
is created, copied, or deleted, so the upgrade needs no permission a 0.19
project did not already have, and no write can race a concurrent writer
or shadow a newer value. Writes always use the new id, so `secretspec
set` is what moves a secret, after which reads stop consulting the legacy
id. The 0.19 secret is left in place for rollback and should only be
deleted once its value has been written under the new id.

The legacy lookup is deliberately best effort. Secret-level IAM answers
PERMISSION_DENIED rather than NOT_FOUND for an id nobody was granted a
binding on, which would otherwise report an unset secret as a failed
read. A name that 0.19 accepted but this layout cannot represent, such as
a project containing `--`, still reads its 0.19 secret with a warning
instead of failing every operation; the naming error names the rename
that restores writes.

Explicit `ref` addresses are native and unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(gcsm): migrate convention names lazily
chore: add jq to devenv packages for integration testing
Rust's runtime ignores SIGPIPE, so a closed output pipe surfaces as an
EPIPE write error rather than terminating the process. `secretspec` then
fails where every other Unix tool exits quietly:

    $ secretspec export --format dotenv | head
    Error:   x Failed to export secrets
      |-> IO error: Broken pipe (os error 32)
      `-> Broken pipe (os error 32)          # exit 1

    $ secretspec check --json | head
    thread 'main' panicked at library/std/src/io/stdio.rs:1166:9:
    failed printing to stdout: Broken pipe (os error 32)   # exit 101

Resetting the disposition to SIG_DFL in the binary entry point fixes
every stdout-writing command at once, rather than teaching each call
site to special-case EPIPE. Both commands above now terminate on signal
13 with empty stderr; unpiped output is unchanged.

This is the follow-up promised in #373, where the same broken-pipe
behavior came up on the `check` path.

libc is a new direct dependency, declared only under cfg(unix). It was
already in the lock file transitively, so nothing new is vendored.
Bumps [Swatinem/rust-cache](https://github.com/swatinem/rust-cache) from e18b497796c12c097a38f9edb9d0641fb99eee32 to f0d9c3887740aee45f6153b24b3a6b815192ec16.
- [Release notes](https://github.com/swatinem/rust-cache/releases)
- [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md)
- [Commits](e18b497796...f0d9c38877)

---
updated-dependencies:
- dependency-name: Swatinem/rust-cache
  dependency-version: f0d9c3887740aee45f6153b24b3a6b815192ec16
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
chore(deps): bump Swatinem/rust-cache from e18b497796c12c097a38f9edb9d0641fb99eee32 to f0d9c3887740aee45f6153b24b3a6b815192ec16
Publish SecretSpec through WinGet
The landing page held the renderer main thread at ~70% of wall clock while
idle, 44% of it in style recalculation, so the hero reel dropped frames.

Three causes:

* `--scroll` was an inherited custom property mutated every frame on the reel
  track, invalidating the style of ~900 descendants. Each one re-resolved four
  `color-mix()` calls, a two layer `box-shadow`, and a `font-weight` calc that
  forced text reshaping, which pulled layout in as well.
* `landing-pipeline-flow` animated `left` and `landing-pipeline-arrowhead-glow`
  animated `filter`. Both are infinite and both kept running off screen.
* The provider list demo had no IntersectionObserver, so it cycled for the whole
  session, reading getBoundingClientRect twice on every tick.

The reel scroll position is now a non-inherited registered property, the per
slot scale and fade are Web Animations the compositor runs, the accent ring
cross fades via `opacity` on its own layer, and the arrow pulse rides a rail so
it moves with a composited `translate`. Main thread busy time drops to ~12% and
style recalculation to ~2.6%. Rendering is unchanged: screenshots differ from
the previous output by at most 0.006% of channels.

Also fixes two bugs found while verifying: `prefers-reduced-motion` never
stopped the reel, because the IntersectionObserver's first callback restarted it
after the check ran, and `autoTick` could read a stale scroll position while a
tween was still in flight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clicking a provider or SDK in the hero reel never navigated. `onPointerDown`
captured the pointer on every press, and with pointer capture active the browser
retargets the following `click` to the capturing element, so it landed on the
reel container rather than the slot's own anchor. The pointer is now captured
only once the drag threshold is crossed, which restores ordinary link
behaviour: navigation, ctrl-click, middle click, the context menu, and Tab plus
Enter. Clicking an off-centre pill used to recentre it and swallow the click, so
that step is gone too; any pill now opens its page.

Wheel and trackpad also browse a reel now, matching a drag pixel for pixel and
settling on a whole slot once the gesture stops. The page does not scroll while
the cursor is over a reel.

Fixes a mobile trap as well: the reel is laid out horizontally under 900px, but
`touch-action: none` meant a vertical swipe starting on it scrolled nothing, so
a reader could be stuck unable to scroll past the hero. It is `pan-y` there and
`pan-x` on desktop, where the axes are the other way round.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pulse ran down the shaft and stopped, and the head flared all at once, so
nothing connected the two. Two sparks now launch from the point, where the line
ends, and run outward along each of the head's strokes as it flares.

The shaft pulse also used to overshoot. It stopped at a fixed offset, but the
pulse is drawn from its centre, so its leading edge and halo carried past the
point, and at the same length as the whole head it covered the arrow on arrival.
It is shorter now, with a tighter halo, and stops where its leading edge lands on
the point. The sparks fire as it lands rather than a beat later.

The head, the sparks and the pulse's stopping point are all derived from
`--pipeline-head-centre`, `--pipeline-head-reach` and `--pipeline-pulse-length`
instead of hardcoded offsets repeated per arrow and per orientation, so they
cannot drift apart.

Everything is `translate` and `opacity`, so the whole arrow stays on the
compositor, and all eight parts stay inert under `prefers-reduced-motion`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The subtitle now points at the posts that argue each half of the pitch:
"Declare secrets" to Secrets Don't Belong in Config, the provider
coupling clause to But I Use SOPS, and the environment variable clause
to Where .env Went Wrong.

Drop the provider count from the sentence; the Providers button and the
bento stat already carry it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
But I Use SOPS now sits alongside the other two posts in the
declarative intro, so the section that names the posts by title covers
all three arguments.

The meta description no longer quotes a provider count; the Providers
button and the bento stat carry it on the page itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(cli): restore the default SIGPIPE disposition
Route generated code through the runtime support re-exports so macro
consumers only need the runtime and derive crates. Keep generated runtime paths
canonical and leave renamed-runtime configuration out of this change.
Simplify derive macro setup
`extract` could only read a JSON document, so a secret stored inside an
INI file had to be extracted by whatever wrote it. INI now joins JSON as
a stored format, selecting an unsectioned key with `/key` and a
named-section key with `/section/key`.

The pointer keeps RFC 6901 escaping per segment, so `~1` selects a
literal `/` and `~0` a literal `~`, but the shape is restricted to those
two depths: an INI document has no deeper structure to address, and a
pointer that implies one is rejected at validation rather than silently
missing at resolution. Values stay strings and backslashes stay literal,
so a Windows path survives extraction unchanged.

Extraction failures name only the secret, format, pointer, and parser
location, never the stored document or the selected value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(extract): select values from INI documents
`declare_secrets!`'s generated builder gets a `prompt_missing(bool)` method
mirroring `Secrets::ensure_secrets` on the untyped API: when a required
secret is missing and stdin is a real terminal, it prompts and stores the
value instead of failing with `RequiredSecretMissing`. Off by default, no
behavior change for existing callers.
Preserve TOML edits through SpecBuilder
Rust: interactive prompting for missing secrets for macro
fix(bw): isolate convention secrets by project and profile
fix(run): forward signals to child process
feat(spec): expose JSON Schema generation
Add native x64 and ARM64 musl addons, libc-aware npm package selection, native musl CI builds, Alpine smoke coverage, and release documentation.

Refs #383
Integration: git credentials
The value-free surfaces -- `check --json`, `check --explain`, `report()`,
and `resolve_without_values()` -- deliberately mint nothing, yet reported a
required `generate` secret as resolved whenever it was absent, on the
grounds that a real resolve would generate it. That let
`check --no-prompt --explain` exit 0 against a store that holds no value.

A required generatable secret whose store keeps what it mints is now
reported `missing_required` until a pass provisions it. Generation still
resolves where nothing has to be provisioned: an optional secret, or one
routed to a store that never retains a generated value (`null`), decided
from the pure `generated_value_persistence` capability rather than any
provider I/O. The value-carrying paths are unchanged and still mint,
store, and resolve.

`check --explain` now prints `will generate` instead of `generated`, since
that surface mints nothing.
fix(check): report unprovisioned required generated secrets as missing
Integration: docker credentials
Add Cloudflare Secrets Store provider
Port the Doppler CLI provider onto current upstream while retaining OpenBao support and scoped credential injection.
feat(provider): add Doppler to current SecretSpec
Provide a reproducible flake package for the OpenBao and Doppler upgrade.
build: package static Linux secretspec
feat(openbao): add TLS pinning support
build: bump secretspec to 0.20.0
Some checks failed
Pre-release / plan (pull_request) Has been cancelled
Pre-release / build-global-artifacts (pull_request) Has been cancelled
Pre-release / aarch64-pc-windows-msvc (pull_request) Has been cancelled
Pre-release / Publish the Windows ARM64 CLI (pull_request) Has been cancelled
Python wheels / wheel aarch64 (manylinux) (pull_request) Has been cancelled
Python wheels / wheel x86_64 (manylinux) (pull_request) Has been cancelled
Python wheels / wheel macos-aarch64 (pull_request) Has been cancelled
Python wheels / wheel windows-x86_64 (pull_request) Has been cancelled
Python wheels / publish to PyPI (pull_request) Has been cancelled
Ruby gems / arm64-darwin (pull_request) Has been cancelled
Ruby gems / aarch64-linux (pull_request) Has been cancelled
Ruby gems / x86_64-linux (pull_request) Has been cancelled
Ruby gems / x64-mingw-ucrt (pull_request) Has been cancelled
Ruby gems / publish to RubyGems (pull_request) Has been cancelled
SDKs / sdks (pull_request) Has been cancelled
SDKs / Swift SDK (pull_request) Has been cancelled
Swift SDK / macOS native libraries (pull_request) Has been cancelled
Swift SDK / XCFramework and Swift tests (pull_request) Has been cancelled
Swift SDK / Publish Swift XCFramework (pull_request) Has been cancelled
Test / Classify changes (pull_request) Has been cancelled
Test / Documentation (pull_request) Has been cancelled
Test / tests (macos-latest) (pull_request) Has been cancelled
Test / tests (ubuntu-latest) (pull_request) Has been cancelled
Test / features (pull_request) Has been cancelled
Test / package (pull_request) Has been cancelled
Test / windows (pull_request) Has been cancelled
Release / plan (pull_request) Has been cancelled
Release / build-global-artifacts (pull_request) Has been cancelled
Release / host (pull_request) Has been cancelled
Release / announce (pull_request) Has been cancelled
02fdd77375
merge forgejo main into secretspec update
Some checks failed
Pre-release / win-x64 (pull_request) Has been cancelled
Pre-release / linux-musl-arm64 (pull_request) Has been cancelled
Pre-release / linux-musl-x64 (pull_request) Has been cancelled
Pre-release / NuGet package (pull_request) Has been cancelled
Pre-release / consume linux-arm64 (pull_request) Has been cancelled
Pre-release / consume linux-x64 (pull_request) Has been cancelled
Pre-release / consume osx-arm64 (pull_request) Has been cancelled
Pre-release / consume osx-x64 (pull_request) Has been cancelled
Pre-release / consume win-arm64 (pull_request) Has been cancelled
Pre-release / consume win-x64 (pull_request) Has been cancelled
Pre-release / consume linux-musl-arm64 (pull_request) Has been cancelled
Pre-release / consume linux-musl-x64 (pull_request) Has been cancelled
Pre-release / publish to NuGet (pull_request) Has been cancelled
Pre-release / macOS native libraries (pull_request) Has been cancelled
Pre-release / XCFramework and Swift tests (pull_request) Has been cancelled
Pre-release / Publish Swift XCFramework (pull_request) Has been cancelled
Pre-release / plan (pull_request) Has been cancelled
Pre-release / build-global-artifacts (pull_request) Has been cancelled
Pre-release / aarch64-pc-windows-msvc (pull_request) Has been cancelled
Pre-release / Publish the Windows ARM64 CLI (pull_request) Has been cancelled
Python wheels / publish to PyPI (pull_request) Has been cancelled
Ruby gems / publish to RubyGems (pull_request) Has been cancelled
Swift SDK / XCFramework and Swift tests (pull_request) Has been cancelled
Swift SDK / Publish Swift XCFramework (pull_request) Has been cancelled
Test / tests (macos-latest) (pull_request) Has been cancelled
Test / tests (ubuntu-latest) (pull_request) Has been cancelled
Test / windows (pull_request) Has been cancelled
Release / build-global-artifacts (pull_request) Has been cancelled
Release / host (pull_request) Has been cancelled
Release / announce (pull_request) Has been cancelled
55a990557f
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kennysheridan/secretspec-fork!2
No description provided.