build: bump secretspec to 0.20.0 #2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "build/update-secretspec-0.20.0"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
0.20.0.v0.20.0artifact checksum.Validation
bash scripts/sync-sdk-versions.sh 0.20.0cargo fmtnixpkgs-fmt flake.nixnix eval .#packages.x86_64-linux.secretspec.version --raw->0.20.0nix build .#packages.x86_64-linux.secretspec./result/bin/secretspec --version->secretspec 0.20.0Notes
mainwas at the older0.6.2fork state, so this PR includes the full OpenBao/Doppler successor update plus the0.20.0bump.cargo test --workspaceis blocked locally becausesecretspec-php-nativerequiresphpinPATH.cargo test -p secretspec -p secretspec-deriveran 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 warningsfails on existing clippy warnings unrelated to this version metadata bump.cargo test -p secretspec-deriveandnix build .#packages.x86_64-linux.secretspec-deriveexceeded local command timeouts after compilation had progressed.Rollout / Rollback
Release-please impact: no release expected
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>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>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>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.op vault listinstead ofop whoamicba3379f63Fix: 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>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>secretspec resolve --json0ee7c0ffb8Phase 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>secretspec codegen --lang python(IR-driven typed accessors) 98e71cb149Phase 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>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>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>- 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>- 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>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>ref) as provider independent coordinates 9313621648A 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>refcompose withgeneratea425b4fadeAdds 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.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>reffe6f1c1e8eMake 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>config provider loginandadd --envfor bootstrap 0b1e9ef0d1- 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>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>modefield, and test it 4e0fe25cc0Replace 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>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>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" } }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).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>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>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>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.ageprovider credentials 0b280e933b`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`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 untildd755d2, 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 to1dad1c7, 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>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>pass-cli info7ecfcc5618Two 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>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>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.