Skip to content

Expose inline Spec declarations through a versioned FFI request API #361

Description

@domenkozar

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

  • Decide whether to add a generic native call or extend secretspec_resolve with an explicit capability/version gate.
  • Define and document the request envelope and inline-spec schema.
  • Older native libraries fail explicitly when an SDK attempts to use inline specs; they never fall back to a filesystem manifest.
  • Inline specs use the same validation and compilation path as Spec::builder and Spec::from_toml.
  • base_dir behavior matches Secrets::from_spec_at.
  • Path/search-based SDK behavior remains backward compatible.
  • Add C ABI tests for inline resolution, invalid versions/operations, mutually exclusive sources, and old-runtime compatibility behavior.
  • Add conformance coverage for at least one dynamically loaded SDK before rolling the feature out to all official SDKs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions