Problem
SecretSpec 0.20 adds the Rust-first Spec, SpecBuilder, Profile, and Secret API, allowing applications to own their secret declarations in code instead of a secretspec.toml file. Other language SDKs currently cannot use an equivalent inline declaration: the shared native boundary only accepts a filesystem path and resolution options.
We should expose inline specs to the FFI/native SDK layer without mirroring the Rust builder as a wide, stateful C API. This also needs to leave room for future operations beyond whole-profile resolve/report.
There is an important compatibility hazard in adding a plain optional spec field to the existing request. JsonRequest currently accepts unknown fields. If a newer SDK sends spec to an older secretspec-ffi, the old library can silently ignore it, omit path, walk up from the current directory, and resolve an unrelated filesystem manifest. Response schema checking does not detect this when the response shape itself remains compatible.
Proposed direction
Keep the native ABI JSON-in/JSON-out, and add one generic, extensible entry point rather than one C function per SpecBuilder operation:
char *secretspec_call(const char *request_json);
For example:
{
"request_version": 1,
"operation": "resolve",
"source": {
"kind": "inline",
"spec_version": 1,
"base_dir": "/logical/project",
"spec": {}
},
"options": {
"profile": "production",
"scope": "api",
"reason": "application startup"
}
}
The exact names are open to design, but the contract should have:
- a tagged source union (
search, path, or inline), with invalid combinations impossible;
- an independently versioned inline-spec wire format;
- an explicit request/operation version separate from resolve/report response schema versions;
- a logical
base_dir for relative provider paths, matching Secrets::from_spec_at;
- strict rejection of unknown operations, versions, and declaration fields;
- a dedicated wire representation rather than serializing the private
Config, Spec, or compiled manifest directly;
- language-native builders that serialize the wire document and submit it in one call, rather than native handles and incremental C mutation functions.
Keep secretspec_resolve as the existing compatibility entry point. The presence of a new symbol gives SDKs safe capability detection: an SDK requiring inline specs cannot accidentally send them to an older library that ignores the input. If we retain only secretspec_resolve instead, the design needs an equally reliable old-library rejection/capability mechanism.
Future additions could then be new versioned operations such as validation, inspection, named resolution, or schema generation without expanding the C symbol surface each time.
Acceptance criteria
Problem
SecretSpec 0.20 adds the Rust-first
Spec,SpecBuilder,Profile, andSecretAPI, allowing applications to own their secret declarations in code instead of asecretspec.tomlfile. Other language SDKs currently cannot use an equivalent inline declaration: the shared native boundary only accepts a filesystempathand resolution options.We should expose inline specs to the FFI/native SDK layer without mirroring the Rust builder as a wide, stateful C API. This also needs to leave room for future operations beyond whole-profile resolve/report.
There is an important compatibility hazard in adding a plain optional
specfield to the existing request.JsonRequestcurrently accepts unknown fields. If a newer SDK sendsspecto an oldersecretspec-ffi, the old library can silently ignore it, omitpath, walk up from the current directory, and resolve an unrelated filesystem manifest. Response schema checking does not detect this when the response shape itself remains compatible.Proposed direction
Keep the native ABI JSON-in/JSON-out, and add one generic, extensible entry point rather than one C function per
SpecBuilderoperation:For example:
{ "request_version": 1, "operation": "resolve", "source": { "kind": "inline", "spec_version": 1, "base_dir": "/logical/project", "spec": {} }, "options": { "profile": "production", "scope": "api", "reason": "application startup" } }The exact names are open to design, but the contract should have:
search,path, orinline), with invalid combinations impossible;base_dirfor relative provider paths, matchingSecrets::from_spec_at;Config,Spec, or compiled manifest directly;Keep
secretspec_resolveas the existing compatibility entry point. The presence of a new symbol gives SDKs safe capability detection: an SDK requiring inline specs cannot accidentally send them to an older library that ignores the input. If we retain onlysecretspec_resolveinstead, the design needs an equally reliable old-library rejection/capability mechanism.Future additions could then be new versioned operations such as validation, inspection, named resolution, or schema generation without expanding the C symbol surface each time.
Acceptance criteria
secretspec_resolvewith an explicit capability/version gate.Spec::builderandSpec::from_toml.base_dirbehavior matchesSecrets::from_spec_at.