Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Directory.Packages.local.props
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
(see nuget.config). Contract id: fsgg-contracts (registry/dependencies.yml). -->
<ItemGroup>
<PackageVersion Include="FS.GG.Contracts" Version="7.6.0" />
<!-- Local retained candidate only; package qualification and public availability remain separate. -->
<PackageVersion Include="FS.GG.SDD.Commands" Version="2.3.0" />
<PackageVersion Include="FS.GG.SDD.Artifacts" Version="2.3.0" />
</ItemGroup>

<!-- Runtime: rich terminal rendering + a minimal read-only TUI for the F27 human projections.
Expand Down
2 changes: 1 addition & 1 deletion docs/atlas/catalog.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"records":["7b7c04c85d989e7136e526e749bfdd35d75ada3890e855a15906c0e400f13f03"],"schema":"fsgg.atlas/1"}
{"records":["7637505ba1959d7eeda94c4cb039fb17150c473cec3024045d384294f9b74c8a","7b7c04c85d989e7136e526e749bfdd35d75ada3890e855a15906c0e400f13f03"],"schema":"fsgg.atlas/1"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"citations":[{"path":"src/FS.GG.SDD.Commands/CatalogScaffoldPolicy.fs","repository":"FS-GG/FS.GG.SDD","revision":"c8aea9289a6cc410f076823bf1219a02314f00a7","sha256":"d47db7c167aeb169af86788f85864d72ed428f7d22dac11537294b71b50c9c2f"},{"path":"src/FS.GG.SDD.Commands/ProviderCapabilityAdmission.fsi","repository":"FS-GG/FS.GG.SDD","revision":"c8aea9289a6cc410f076823bf1219a02314f00a7","sha256":"b741374ce237334d806688b13733388e0442821287070f3a1a4cf403fff1ed54"}],"conflictsWith":[],"id":"governance/provider-loader-package-digest-boundary","kind":"dependency","recordedAt":"2026-10-15T08:55:26Z","repository":"FS-GG/FS.GG.Governance","schema":"fsgg.atlas/1","status":"recorded","summary":"Verify ProviderContext selectors accept raw 64-character hexadecimal SHA256, preserving spelling. The retained SDD Commands CatalogScaffoldPolicy.parse API at the cited source instead compares its expectedRawDigest with a lowercase sha256:-prefixed digest of the policy byte array. A consumer must first validate the selected raw digest against the captured bytes, then supply the recomputed prefixed representation to the actual codec. ProviderCapabilityAdmission.request consumes independent Policy, selected platform and original descriptor and returns the complete Config ResolutionRequest; parsing or admission establishes declarations, not execution or policy authority. This is immutable source inspection; it supplies no runtime/package qualification, publication or installed acceptance.","supersedes":[],"tags":["governance","sdd","provider","loader","digest"],"title":"Verify raw digest selectors require adaptation to the SDD package policy codec"}
26 changes: 26 additions & 0 deletions docs/roadmaps/governance-423-neutral-capability-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,29 @@ pending and retain the original caller owner and failure evidence. The existing
Program/Verify integration, stronger profiles, coherent SDD consumer joins and C3/C4 milestones remain
open. The owning Atlas archive records `governance/provider-capture-retention-boundary`; retrieve it
with the Atlas helper and verify its immutable source citations before reuse.


### GOV423-C2 phase-2 package loader preparation (2026-10-15)

Source preparation targets the retained SDD Commands/Artifacts 2.3.0 candidate from
run `37951954780`, artifact `11626413573`, source
`c8aea9289a6cc410f076823bf1219a02314f00a7`. Selection is not package qualification
or public availability. The package-backed adapter decodes immutable observations from the
original Ready capture session, strictly parses schema-2 provenance and independently selected
policy, binds policy/platform/admission correspondence, and recomputes the whole Config request
before catalog selection, cache or execution. Literal argv stays typed throughout.

The provider interpreter drives the existing non-passing Verify planning transition. Program
parses all explicit provider selectors and supplies a caller-owned-session adapter; it never
acquires or releases that session. Standalone `main` explicitly returns input-unavailable for
provider mode because an independently admitted outer capture lifetime and teardown owner are
not yet supplied. This is an unresolved production-entry prerequisite, not usable provider-mode
availability. Legacy invocations retain the existing interpreter path.

Focused source controls cover actual package codecs, raw digests, strict UTF-8/schema refusal,
independent policy/platform correspondence and the Program-to-planning join. Fixtures are
synthetic declarations and observations, not execution evidence. Local CLR, restore/lock
regeneration, compilation and test execution were held under the programme resource limit;
these controls remain unverified. Package-only qualification, exact-head hosted validation,
physical capture acceptance, production owner integration, source/evidence revalidation and
the real SDD-to-Verify/GateExecution journey remain open. C2 remains unchecked.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
<Compile Include="ProviderContext.fs" />
<Compile Include="ProviderContextCapture.fsi" />
<Compile Include="ProviderContextCapture.fs" />
<Compile Include="ProviderContextLoader.fsi" />
<Compile Include="ProviderContextLoader.fs" />
<Compile Include="Loop.fsi" />
<Compile Include="Loop.fs" />
<Compile Include="Interpreter.fsi" />
Expand All @@ -59,6 +61,8 @@

<ItemGroup>
<PackageReference Include="FSharp.Core" />
<PackageReference Include="FS.GG.SDD.Commands" />
<PackageReference Include="FS.GG.SDD.Artifacts" />
</ItemGroup>

<!--
Expand Down
6 changes: 6 additions & 0 deletions src/FS.GG.Governance.VerifyCommand/Interpreter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,9 @@ module Interpreter =
// Drive init → update* to Done: execute every requested Effect via `step`, feed each result Msg back
// into the pure `update`, accumulate new effects, repeat. Stops at Done or quiescence. NEVER throws.
CommandHost.drive (fun (m: Loop.Model) -> m.Phase = Loop.Done) (step ports) Loop.update m0 eff0

let runProviderPlanning (ports: Ports) (request: Loop.RunRequest) (provider: Loop.ProviderPlanningInput) : Loop.ProviderModel =
let model, effects = Loop.initProviderPlanning request provider
CommandHost.drive
(fun (m: Loop.ProviderModel) -> m.Legacy.Phase = Loop.Done)
(step ports) Loop.updateProviderPlanning model effects
4 changes: 4 additions & 0 deletions src/FS.GG.Governance.VerifyCommand/Interpreter.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,7 @@ module Interpreter =
/// result `Msg` back into `Loop.update`, and stop at `Done`. Returns the terminal `Loop.Model` (carrying
/// the decided `ExitDecision` — including a `Blocked` verdict). TOTAL — never throws.
val run: ports: Ports -> request: Loop.RunRequest -> Loop.Model

/// Drive the actual provider planning transition with the existing effect interpreter.
/// Valid declarations remain non-passing; no provider execution or evidence acceptance is supplied.
val runProviderPlanning: ports: Ports -> request: Loop.RunRequest -> provider: Loop.ProviderPlanningInput -> Loop.ProviderModel
2 changes: 1 addition & 1 deletion src/FS.GG.Governance.VerifyCommand/Loop.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ module Loop =
val parse: argv: string list -> Result<RunRequest, UsageError>

/// Validate explicit provider options, then parse the original token stream with their boundaries
/// retained. This pure API creates no ports; Program remains on legacy parse until loader admission.
/// retained. This pure API creates no ports; Program routes provider requests without falling back to legacy execution.
val parseInvocation: argv: string list -> Result<Invocation, InvocationError>

/// Initial state plus the first requested effect(s) for a valid request (Principle IV `init`).
Expand Down
70 changes: 58 additions & 12 deletions src/FS.GG.Governance.VerifyCommand/Program.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// The thin host entry for `fsgg verify` (F056): parse argv → build the real ports → run the interpreter →
// print any diagnostics to stderr → return the mapped exit code. ALL decision logic lives in the pure
// `Loop`; ALL I/O in the `Interpreter`. A `parse` rejection exits 2 BEFORE any port is built, so a usage
// `Loop`; ALL I/O in the `Interpreter`. A parse rejection exits 2 BEFORE any port is built, so a usage
// error (including an unrecognized profile or a forbidden `--mode`) writes no artifact and starts no
// git/filesystem work. A blocked verdict is NOT a diagnostic — it is reported in the summary and surfaces as
// exit 1. Verify is NOT the merge authority (FR-017): a passing `fsgg verify` does not authorize a merge.
Expand All @@ -26,17 +26,63 @@ let categoryToken (decision: Loop.ExitDecision) : string =
| Loop.InputUnavailable -> "input-unavailable"
| Loop.ToolError -> "tool-error"

[<EntryPoint>]
let main argv =
match Loop.parse (List.ofArray argv) with
| Error err ->
eprintfn "fsgg verify: %s" (usageMessage err)
// The caller of this seam owns any original capture session for the whole invocation.
// A loader rejection precedes real-port construction, catalog reads and legacy execution.
let internal runWithProviderLoader
(loadProvider: Loop.RunRequest -> ProviderContext.Selection -> Result<Loop.ProviderPlanningInput, string>)
(makePorts: string -> Interpreter.Ports)
(report: string -> unit)
argv =
match Loop.parseInvocation (List.ofArray argv) with
| Error(Loop.LegacyUsage err) ->
report ("fsgg verify: " + usageMessage err)
Loop.exitCode Loop.UsageError'
| Error(Loop.ProviderUsage err) ->
report (sprintf "fsgg verify: provider selection rejected: %A" err)
Loop.exitCode Loop.UsageError'
| Ok request ->
let ports = Interpreter.realPorts request.Repo
let model = Interpreter.run ports request
| Ok invocation ->
let outcome =
match invocation.Provider with
| None -> Ok(Interpreter.run (makePorts invocation.Request.Repo) invocation.Request)
| Some selection ->
match loadProvider invocation.Request selection with
| Error message -> Error message
| Ok provider ->
let model = Interpreter.runProviderPlanning (makePorts invocation.Request.Repo) invocation.Request provider
Ok model.Legacy
match outcome with
| Error message ->
report ("fsgg verify [input-unavailable]: " + message)
Loop.exitCode Loop.InputUnavailable
| Ok model ->
for diagnostic in model.Diagnostics do
report (sprintf "fsgg verify [%s]: %s" (categoryToken diagnostic.Category) diagnostic.Message)
Loop.exitCode model.Exit

for d in model.Diagnostics do
eprintfn "fsgg verify [%s]: %s" (categoryToken d.Category) d.Message
// The supplying caller retains every returned session, including failures, through its own teardown.
// This adapter performs no acquisition, renewal, release, disposal or provider execution.
let internal runWithCapturedProvider
(supply: Loop.RunRequest -> ProviderContext.Selection -> Result<ProviderContextCapture.CaptureSession * FS.GG.Governance.GateRun.Plan.ProviderCommandContext, string>)
makePorts report argv =
let load request selection =
match supply request selection with
| Error message -> Error message
| Ok(session, context) ->
let observation = ProviderContextCapture.inspect session
let root = System.IO.Path.GetFullPath request.Repo
if context.RepoRoot <> root
|| not (observation.Roots |> List.exists (fun r -> r.RequestedPath = root && r.CurrentBindingMatches = Some true)) then
Error "provider command context does not bind the original captured repository root"
else
match ProviderContextLoader.load selection session with
| Error errors -> Error(sprintf "provider declaration admission rejected: %A" errors)
| Ok resolution -> Ok ({ ResolutionRequest = resolution; CommandContext = context }: Loop.ProviderPlanningInput)
runWithProviderLoader load makePorts report argv

Loop.exitCode model.Exit
[<EntryPoint>]
let main argv =
// Standalone capture cannot yet establish its externally owned lifetime/teardown contract.
// Preserve that exact missing prerequisite instead of starting an unowned native activity.
let unavailable _ _ =
Error "provider capture requires an independently admitted caller-owned lifetime and teardown boundary; standalone provider execution is unavailable"
runWithProviderLoader unavailable Interpreter.realPorts (eprintfn "%s") argv
87 changes: 87 additions & 0 deletions src/FS.GG.Governance.VerifyCommand/ProviderContextLoader.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
namespace FS.GG.Governance.VerifyCommand

open System
open System.Security.Cryptography
open System.Text
open FS.GG.SDD.Commands
open FS.GG.SDD.Artifacts

module internal ProviderContextLoader =
module CB = FS.GG.Governance.Config.CapabilityBindings
module Capture = ProviderContextCapture

let diagnostic path message : Fsgg.ProviderCatalog.Diagnostic =
{ Code = "verify.providerContextInvalid"; Path = path; Message = message }

let refused path message = Error [ diagnostic path message ]
let digest (bytes: byte array) = Convert.ToHexString(SHA256.HashData bytes).ToLowerInvariant()
let sameDigest a b = String.Equals(a, b, StringComparison.OrdinalIgnoreCase)

let decode (selection: ProviderContext.Selection) (provenanceBytes: byte array) (policyBytes: byte array) =
try
if not (sameDigest (digest provenanceBytes) selection.ProvenanceSha256) then
refused "$.provenance" "Selected raw provenance digest does not match captured bytes."
elif not (sameDigest (digest policyBytes) selection.PolicySha256) then
refused "$.policy" "Selected raw policy digest does not match captured bytes."
else
match CatalogScaffoldPolicy.parse ("sha256:" + digest policyBytes) policyBytes with
| Error errors -> Error errors
| Ok policy ->
let text = UTF8Encoding(false, true).GetString provenanceBytes
match ScaffoldProvenanceDocument.parse text with
| Error errors -> Error errors
| Ok(ScaffoldProvenanceDocument.Legacy _) ->
refused "$.provenance.schemaVersion" "Explicit provider mode requires schema-2 catalog provenance."
| Ok(ScaffoldProvenanceDocument.Catalog provenance) ->
let declaration = provenance.Declaration
let admission = declaration.Admission
let identityMatches (recorded: CatalogScaffoldProvenance.Identity) (selected: CatalogScaffoldPolicy.Identity) =
recorded.Id = selected.Id && recorded.Version = selected.Version && recorded.Digest = selected.Digest
let selectedFormats = policy.SupportedEvidenceFormats |> List.map (fun f -> f.Id, f.Version) |> List.sort
let recordedFormats = admission.SupportedFormats |> List.map (fun f -> f.Id, f.Version) |> List.sort
let selectedMappings = policy.EvidenceMappings |> List.map (fun m -> m.Format, m.Id, m.Version) |> List.sort
let recordedMappings = admission.EvidenceMappings |> List.map (fun m -> m.Format, m.Id, m.Version) |> List.sort
let policyMatches =
identityMatches declaration.Policy policy.Identity
&& identityMatches declaration.EvidenceMap policy.EvidenceMapIdentity
&& List.sort admission.RequiredCapabilityIds = List.sort policy.RequiredCapabilityIds
&& List.sort admission.SemanticOnlyCapabilityIds = List.sort policy.SemanticOnlyCapabilityIds
&& List.sort admission.SupportedPlatforms = List.sort policy.KnownPlatforms
&& List.sort admission.SupportedEnvironmentIds = (policy.EnvironmentBindings |> List.map (fun e -> e.Id) |> List.sort)
&& recordedFormats = selectedFormats && recordedMappings = selectedMappings
if admission.Platform <> selection.Platform || provenance.Observation.Platform <> selection.Platform then
refused "$.platform" "Selected platform differs from provenance declaration or observation."
elif declaration.Budgets.AdmittedPreflightSeconds > policy.MaximumPreflightSeconds
|| declaration.Budgets.AdmittedScaffoldSeconds > policy.MaximumScaffoldSeconds then
refused "$.policy.budgets" "Recorded admission exceeds the independently selected policy ceilings."
elif not policyMatches then
refused "$.policy" "Provenance admission differs from the independently selected policy."
else
match ProviderCapabilityAdmission.request policy selection.Platform declaration.Descriptor with
| Error errors -> Error errors
| Ok request ->
// Whole-request refusal precedes catalog selection, cache reads and empty selection.
match CB.resolve request with
| CB.Rejected errors ->
errors |> List.map (fun d -> diagnostic ("$.bindings." + d.CapabilityId + "." + d.Field) d.Message) |> Error
| CB.Resolved _ -> Ok request
with
| :? DecoderFallbackException -> refused "$.provenance" "Captured provenance is not strict UTF-8."

let load (selection: ProviderContext.Selection) (session: Capture.CaptureSession) =
let observation = Capture.inspect session
if observation.Model.Phase <> Capture.Ready || not observation.Model.CaptureConsumed
|| observation.Model.FirstFailure.IsSome || not (List.isEmpty observation.Model.SecondaryFailures)
|| observation.Settlement <> Capture.Retained || observation.WorkEndReached
|| observation.CleanupEndReached || observation.CancellationObserved
|| List.isEmpty observation.Roots || (observation.Roots |> List.exists (fun r -> r.CurrentBindingMatches <> Some true)) then
refused "$.capture" "Original capture is not ready for declaration admission; its caller retains ownership."
else
let selected role path expected =
observation.Documents
|> List.filter (fun d -> d.Role = role && d.SelectedPath = path && sameDigest d.RawSha256 expected && d.ComparisonPasses > 0)
match selected "provenance" selection.ProvenancePath selection.ProvenanceSha256,
selected "policy" selection.PolicyPath selection.PolicySha256 with
| [ provenance ], [ policy ] when observation.Documents.Length = 2 ->
decode selection (provenance.Bytes |> Seq.toArray) (policy.Bytes |> Seq.toArray)
| _ -> refused "$.capture.documents" "Exactly the selected, compared provenance and policy documents are required."
14 changes: 14 additions & 0 deletions src/FS.GG.Governance.VerifyCommand/ProviderContextLoader.fsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace FS.GG.Governance.VerifyCommand

/// Package-backed declaration admission. The original capture owner remains with the caller.
module internal ProviderContextLoader =
/// Strict byte decoding and independent policy binding; no recorded resolved set is trusted.
val decode:
ProviderContext.Selection -> byte array -> byte array ->
Result<FS.GG.Governance.Config.CapabilityBindings.ResolutionRequest, Fsgg.ProviderCatalog.Diagnostic list>

/// Read immutable observations from the original ready session; never acquire or release it.
/// This establishes declaration identity only, not current source or execution acceptance.
val load:
ProviderContext.Selection -> ProviderContextCapture.CaptureSession ->
Result<FS.GG.Governance.Config.CapabilityBindings.ResolutionRequest, Fsgg.ProviderCatalog.Diagnostic list>
Loading
Loading