Skip to content

feat(fctl): add portable Auth plugin contracts - #158

Closed
Dav-14 wants to merge 12 commits into
mainfrom
codex/mvp5-auth-transpose
Closed

Dav-14 wants to merge 12 commits into
mainfrom
codex/mvp5-auth-transpose

Conversation

@Dav-14

@Dav-14 Dav-14 commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Adds the portable Auth command/auth facets, exact SDK pinning, command catalogue, sensitive-output contracts, and stable RenderHints for CLI consumers.

Scope

Confined to plugins/fctl/, plus the flake and Justfile entries that pin and reach the plugin's own toolchain. No change to the Auth service code.

Validation

  • Plugin unit/race tests pass against the pinned fctl SDK
  • Plugin coverage 85.7% (minimum 80%)
  • nix develop --command just pre-commit passes locally

Known issues

The Dirty check fails: the pinned fctl SDK is unreachable from CI

plugins/fctl/scripts/with-fctl-sdk.sh resolves the pinned SDK through
FCTL_SDK_ROOT, which no workflow sets and which assumes a developer
checkout. CI therefore reaches the plugin gate with nothing to point at:

FCTL_SDK_ROOT is required
error: Recipe `fctl-audit-tidy` failed on line 46 with exit code 2

formancehq/fctl-v2-poc is private and a repository-scoped CI token cannot
clone it, so supplying a credential is not a viable fix. The established
remedy in this codebase is a committed SDK bundle, pinned by commit provenance
and verified by the same SDK and WIT content hashes — the approach already
green in formancehq/connectivity and formancehq/ledger.

Local runs pass only because they point FCTL_SDK_ROOT at a checkout; that
green does not carry to CI.

  • Commit the verified fctl SDK bundle and make FCTL_SDK_ROOT an override, so the plugin gate runs in CI without a credential

This is the only remaining red check. The earlier crates.io HTTP 403 failure is
resolved: the Rust authoring toolchain no longer loads in the default dev
shell, so ordinary Go jobs no longer build it.

The plugin pinned the fctl SDK to a personal fork
(Dav-14/fctl-v2-poc @ 545521bf). Repin to the canonical
formancehq/fctl-v2-poc at e9b1395f, the MVP5 integration tip.

Both hashes were recomputed from that revision through the same
projection the validator uses: `git archive` of the locked sdkPath and
witPath, then `nix hash path --type sha256 --sri` over pkg/plugin and
`shasum -a 256` over the canonical WIT. The WIT hash is unchanged, so
the plugin's vendored contract still matches.

The authoring toolchain is byte-identical across both revisions
(rust 1.91.1, wasm-tools 1.239.0, componentize-go 0.4.1, wasi-virt
448f6df8, same wasi-virt patch), so only the provenance revision moves.

`go mod tidy` promotes google.golang.org/protobuf to a direct
requirement under the new SDK.
The catalogue declared Auth product major 1, which contradicts both
authoritative sources: the Stack v3.2 service-info authority binds Auth
to image v2.5.0 (product major 2), and this repository's own latest
release tag is v2.5.0. No published Stack composition serves an Auth
major 1.

Because ValidateExecuteRequest checks the attested major against the
declared compatibility set, the wrong declaration did not merely
mislabel the surface: a host attesting major 1 was admitted and reached
product traffic. Tests now pin the corrected major at both boundaries
and prove major 1 is refused before any host request is emitted.
`just fctl-audit-lint` could not run while the SDK lock pointed at a
fork revision unavailable to this checkout: the wrapper aborted at pin
validation before golangci-lint started. Repinning makes the recipe
reachable, which surfaces two pre-existing errcheck violations.

Both writes are genuinely unrecoverable — a failed diagnostic write to
stderr and a deferred close on a read-only file — so the returns are
discarded explicitly rather than handled.
Seven of the nine Auth commands now declare an explicit, ordered
RenderHints.Table for the human view. Columns are a compact subset of
scalar properties the emitted public result really carries; nested
containers, unbounded free text and display-once credential material are
excluded. PublicOutputSchema is untouched, so --output json and
--output yaml keep returning the complete public result.

deleteClient and deleteSecret return 204 with no declared response
schema and emit a canonical empty object, so auth clients delete and
auth clients secrets delete are left without a hint rather than given an
invented column.

Coherence is not asserted from a hand-written list: the new catalogue
tests execute each command against the widest documented fixture and
read the property set back out of the adapter's own emitted envelope,
with the fixtures pinned to the generated client's JSON tags.
The render-hint tests could pass without proving much, and two decisions
behind the column lists were inferred rather than recorded.

Header casing is now a recorded decision, not an inference. Both sources
that actually spell a header agree on Title Case: shipped fctl v3 prints
`ID Name Description Public Permissions` and `ID Subject Email` for the
two Auth list commands, and the fctl SDK's own RenderHints examples use
`{Header: "Display Name"}`, `{Header: "Name"}`, `{Header: "ID"}`. The
host's strings.ToUpper in renderTable is the no-layout fallback these
hints exist to replace and never touches an authored Header, so it is
not a counter-example. The declared headers are unchanged; the test now
derives the expected header from the field, so a new column cannot
introduce a different casing.

The compact-detail trade-off is now documented and asserted. Hinting
show/create/update means a host that honours the hints renders four
scalars instead of ten properties, and that is intended: the table is a
compact scalar summary, and every dropped field stays in the emitted
public result and in PublicOutputSchema. The new test fails if a column
resolves to a container, if the omitted set stops matching the recorded
decision, or if an omitted property leaves the public result.

Coherence no longer depends on the schemas being permissive. A Field is
backed when the emitted public result resolves it to a scalar or the
schema declares it, and a command with no public output schema is itself
a violation — erasing the schema on all nine commands now fails in core
rather than only in the component descriptor test. Path resolution is
generic and collection-transparent, matching the dotted Field paths in
the SDK's examples, and is proven directly rather than through a
catalogue that would not exercise it. The schema bytes of all nine
commands are pinned, so a narrowing fails here too.

Fixtures are pinned to every generated type they encode, adding
components.Secret and components.ClientSecret to the two already
covered; the ClientSecret fixture was missing `metadata`. The hintless
classification now reports the real condition instead of its inverse,
reports every offender instead of the first, and fails on a command
classified neither way. A want entry naming no catalogue command is no
longer skipped silently.

Verified without Nix through an equivalent go.work against the pinned
SDK: race suite, coverage gate at 87.3%, guest entrypoint, vet, gofmt.
Seven mutations confirm each guard bites.
@flemzord

flemzord commented Oct 8, 2026

Copy link
Copy Markdown
Member

Closed automatically: bulk PR cleanup

@flemzord flemzord closed this Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants