diff --git a/docs/governance-design/neutral-capability-bindings.md b/docs/governance-design/neutral-capability-bindings.md
index e549803a..a58ab96b 100644
--- a/docs/governance-design/neutral-capability-bindings.md
+++ b/docs/governance-design/neutral-capability-bindings.md
@@ -270,3 +270,28 @@ diagnostic. It emits no legacy execution or store effects and writes no provider
Program and Interpreter remain on the legacy route. Package-backed parsing, independent policy
conversion, pre-dispatch revalidation, qualified provider execution and evidence consumers remain
open. This planning join does not supply actual producer-to-Verify acceptance or complete C2.
+
+### Controlled capture retention boundary (2026-10-15)
+
+The [capture interpreter](../../src/FS.GG.Governance.VerifyCommand/ProviderContextCapture.fs)
+keeps native operations and its monotonic clock on each original session. Production `prepare`
+selects the existing Linux calls; friend-visible test construction installs inert operations before
+acquisition. Each native result carries the errno captured immediately by its wrapper. No global
+switch changes an existing session. Controlled sessions refuse native root-lease duplication;
+production retains the held-root lease and direct-execution borrower fences introduced in
+[#453](https://github.com/FS-GG/FS.GG.Governance/pull/453).
+
+A task remains registered before acquisition and stays with the same caller owner after work or
+cleanup expiry. A returning read rechecks the original work boundary before accepting its bytes.
+Inspection neither waits for that task nor renews its ends. Settlement still requires released
+model state, zero retained descriptors and root leases, no close uncertainty and completed actual
+activity. A failed close is never retried, including after another owner reuses its numeric identity.
+
+The ten [controlled interpreter cases](../../tests/FS.GG.Governance.VerifyCommand.Tests/ProviderContextCaptureTests.fs)
+cover finite success/revalidation, pending reads with prior bounded evidence, cancellation, pending
+revalidation and borrowing, uncertain close, caps, failure ordering, binding drift and one-use capture.
+Paused inert reads have finite gates released and joined in `finally`; uncertain closes use only
+synthetic descriptors. These controls exercise the real interpreter but do not demonstrate a blocked
+kernel read, a physical close failure or whole-worker teardown. The existing physical fixture stays
+opt-in and must not run an indefinitely retained uncertain-close case without separately qualified
+teardown. Physical retention, provider loader/Verify acceptance and C2 completion remain open.
diff --git a/docs/roadmaps/governance-423-neutral-capability-bindings.md b/docs/roadmaps/governance-423-neutral-capability-bindings.md
index 9e6dc185..da0b92e7 100644
--- a/docs/roadmaps/governance-423-neutral-capability-bindings.md
+++ b/docs/roadmaps/governance-423-neutral-capability-bindings.md
@@ -325,3 +325,32 @@ not a new candidate, and no-push candidates cannot be promoted by this route.
This source amendment does not qualify a release archive, dispatch, publish, reconcile original
holds or complete C2/C3/C4. Runtime carrier publication, exact ReferenceGateSet 1.8.0 adoption,
Templates/public receivers and broader release completion retain their applicable evidence gates.
+
+### GOV423-C2 controlled retention source window (2026-10-15)
+
+Owner: Governance source lane. Status: source prepared; hosted qualification pending. C2 remains
+unchecked. This extends the existing capture milestone and preserves all earlier failures and custody
+unknowns. The historical 15-case report above describes #449; #453 subsequently added held-root lease
+coverage and reported 20 physical controls. Those reports do not qualify blocked kernel reads or
+uncertain physical closes.
+
+The bounded seven-path change adds internal per-session native/clock operations, immutable ownership
+facts and ten finite controls through the actual capture interpreter. Native result/errno association,
+original task registration, one-use acquisition, byte caps, cooperative comparisons and the public
+signature remain preserved. A post-read work check prevents a late read from accepting new bytes.
+Synthetic sessions cannot duplicate native leases; production's outstanding lease fences stay intact.
+The surface guard explicitly rejects exported operation/test types and entry points.
+
+Local validation is limited to source, exact touch set, diff and relative links. No local CLR or native
+fixture execution is admitted. Normal PR CI may qualify compilation and controlled/default tests with
+physical capture still pending. Preserve its first failing run and exact successor head; hosted green
+establishes source qualification only. The source PR and its exact checks supply delivery evidence;
+publication, installed adoption and physical acceptance remain separate.
+
+The remaining physical gate needs an accountable supplying owner, an authentic regular-file blocked
+read accepted by production acquisition, independently observed uncertain close and demonstrated
+whole-worker/resource teardown. No backend is qualified by this window. Keep those physical cases
+pending and retain the original caller owner and failure evidence. The existing provider loader,
+Program/Verify integration, stronger profiles, coherent SDD consumer joins and C3/C4 milestones remain
+open. Atlas capture remains an asynchronous owning-archive follow-up for the programme integrator:
+`governance/provider-capture-retention-boundary`.
diff --git a/src/FS.GG.Governance.VerifyCommand/FS.GG.Governance.VerifyCommand.fsproj b/src/FS.GG.Governance.VerifyCommand/FS.GG.Governance.VerifyCommand.fsproj
index ba27ae6e..5a8fa99f 100644
--- a/src/FS.GG.Governance.VerifyCommand/FS.GG.Governance.VerifyCommand.fsproj
+++ b/src/FS.GG.Governance.VerifyCommand/FS.GG.Governance.VerifyCommand.fsproj
@@ -28,6 +28,10 @@
false
+
+
+
+
diff --git a/src/FS.GG.Governance.VerifyCommand/ProviderContextCapture.fs b/src/FS.GG.Governance.VerifyCommand/ProviderContextCapture.fs
index 932a8e73..cb5f15e6 100644
--- a/src/FS.GG.Governance.VerifyCommand/ProviderContextCapture.fs
+++ b/src/FS.GG.Governance.VerifyCommand/ProviderContextCapture.fs
@@ -133,6 +133,38 @@ module ProviderContextCapture =
[]
extern int close(int fd)
+ type internal Operations =
+ { Current: unit -> FS.GG.Governance.GateExecution.Model.MonotonicInstant
+ OpenAt: int -> string -> uint64 -> uint64 -> int64 * int
+ Stat: int -> byte[] -> int * int
+ OpenRead: string -> int * int
+ Read: int -> byte[] -> int64 * int
+ Seek: int -> int64 * int
+ Close: int -> int * int }
+ type internal Ownership =
+ { PendingActivity: bool; RetainedDescriptors: int; CloseUncertain: bool; BorrowedRoots: int }
+ let nativeOperations : Operations =
+ { Current = FS.GG.Governance.GateExecution.Interpreter.currentInstant
+ OpenAt = fun directory path flags resolve ->
+ let mutable how=Native.OpenHow(flags,resolve)
+ let result=Native.openat2(437L,directory,path,&how,24UL)
+ result,System.Runtime.InteropServices.Marshal.GetLastPInvokeError()
+ Stat = fun fd bytes ->
+ let result=Native.statx(fd,"",4352,0x17ffu,bytes)
+ result,System.Runtime.InteropServices.Marshal.GetLastPInvokeError()
+ OpenRead = fun path ->
+ let result=Native.openReadOnly(path,0x80000)
+ result,System.Runtime.InteropServices.Marshal.GetLastPInvokeError()
+ Read = fun fd bytes ->
+ let result=Native.read(fd,bytes,uint64 bytes.Length)
+ result,System.Runtime.InteropServices.Marshal.GetLastPInvokeError()
+ Seek = fun fd ->
+ let result=Native.lseek(fd,0L,0)
+ result,System.Runtime.InteropServices.Marshal.GetLastPInvokeError()
+ Close = fun fd ->
+ let result=Native.close fd
+ result,System.Runtime.InteropServices.Marshal.GetLastPInvokeError() }
+
type Metadata =
{ Identity: ObjectIdentity; Mode: uint16; Size: uint64
ModifiedSeconds: int64; ModifiedNanoseconds: uint32
@@ -143,6 +175,8 @@ module ProviderContextCapture =
PathFd: int; DataFd: int; Metadata: Metadata; Observation: DocumentObservation }
type OwnerState =
{ Request: Request
+ Operations: Operations
+ NativeLeaseAllowed: bool
Sync: obj
mutable Model: Model
Fds: System.Collections.Generic.HashSet
@@ -158,32 +192,31 @@ module ProviderContextCapture =
member _.Operation = state.Request.Operation
member _.State = state
- let current () = FS.GG.Governance.GateExecution.Interpreter.currentInstant()
+ let current (state:OwnerState) = state.Operations.Current()
let transition (state:OwnerState) msg = lock state.Sync (fun () -> state.Model <- update msg state.Model |> fst)
let report (state:OwnerState) cause = transition state (FailureObserved cause)
- let errno () = System.Runtime.InteropServices.Marshal.GetLastPInvokeError() |> string
let fault role detail = raise (CaptureFault(AcquisitionFailed(role,detail)))
let checkWork (state:OwnerState) =
- let now=current()
+ let now=current state
if state.Request.Cancellation.IsCancellationRequested then raise (CaptureFault CancellationRequested)
if now.Ticks>=state.Request.Budget.WorkEnd.Ticks then raise (CaptureFault WorkDeadlineReached)
if lock state.Sync (fun () -> Option.isSome state.Model.FirstFailure) then raise (CaptureFault OperationInProgress)
- let own (state:OwnerState) role fd =
- if fd<0 then fault role ("native acquisition errno="+errno())
+ let own (state:OwnerState) role (fd,error) =
+ if fd<0 then fault role ("native acquisition errno="+string error)
lock state.Sync (fun () -> state.Fds.Add fd |> ignore)
fd
let pathOpen (state:OwnerState) role directory path flags resolve =
checkWork state
- let mutable how=Native.OpenHow(flags,resolve)
- let fd=Native.openat2(437L,directory,path,&how,24UL)
+ let fd,error=state.Operations.OpenAt directory path flags resolve
if fd<0L then
- if System.Runtime.InteropServices.Marshal.GetLastPInvokeError()=38 then raise (CaptureFault(NativePrerequisiteUnavailable "openat2 unavailable"))
- fault role ("openat2 errno="+errno())
- own state role (int fd)
+ if error=38 then raise (CaptureFault(NativePrerequisiteUnavailable "openat2 unavailable"))
+ fault role ("openat2 errno="+string error)
+ own state role (int fd,error)
let metadata (state:OwnerState) role fd =
checkWork state
let bytes=Array.zeroCreate 256
- if Native.statx(fd,"",4352,0x17ffu,bytes)<>0 then fault role ("statx errno="+errno())
+ let result,error=state.Operations.Stat fd bytes
+ if result<>0 then fault role ("statx errno="+string error)
let u32 offset=System.BitConverter.ToUInt32(bytes,offset)
let u64 offset=System.BitConverter.ToUInt64(bytes,offset)
let mask=u32 0
@@ -211,8 +244,9 @@ module ProviderContextCapture =
root
let closeTracked (state:OwnerState) fd =
// Linux close failure is not retried: numeric fd reuse cannot establish continued ownership.
- if (current()).Ticks>=state.Request.Budget.CleanupEnd.Ticks then raise (CaptureFault CleanupDeadlineReached)
- if Native.close fd=0 then lock state.Sync (fun () -> state.Fds.Remove fd |> ignore)
+ if (current state).Ticks>=state.Request.Budget.CleanupEnd.Ticks then raise (CaptureFault CleanupDeadlineReached)
+ let result,_=state.Operations.Close fd
+ if result=0 then lock state.Sync (fun () -> state.Fds.Remove fd |> ignore)
else
lock state.Sync (fun () -> state.CloseUncertain <- true)
raise (CaptureFault OwnedActivitiesPending)
@@ -220,13 +254,14 @@ module ProviderContextCapture =
let observed=metadata state role fd
if observed.Mode &&& 0xf000us <> 0x8000us || not (sameObject original observed) then fault role "held object is not the selected regular file"
checkWork state
- let data=own state role (Native.openReadOnly("/proc/self/fd/"+string fd,0x80000))
+ let data=own state role (state.Operations.OpenRead ("/proc/self/fd/"+string fd))
let reopened=metadata state role data
if not (sameObject original reopened) then fault role "procfd reopen changed object"
data
let reset (state:OwnerState) role fd =
checkWork state
- if Native.lseek(fd,0L,0)<>0L then fault role ("held regular-file reset errno="+errno())
+ let result,error=state.Operations.Seek fd
+ if result<>0L then fault role ("held regular-file reset errno="+string error)
let rawDigest (bytes: byte[]) count =
System.Security.Cryptography.SHA256.HashData(System.ReadOnlySpan(bytes,0,count))
|> System.Convert.ToHexString |> fun digest -> digest.ToLowerInvariant()
@@ -240,9 +275,11 @@ module ProviderContextCapture =
reset state role fd
while reading do
checkWork state
- let amount=Native.read(fd,scratch,uint64 scratch.Length)
- if amount<0L then fault role ("read errno="+errno())
- elif amount=0L then reading <- false
+ let amount,error=state.Operations.Read fd scratch
+ if amount<0L then fault role ("read errno="+string error)
+ // A returning read cannot add bytes after its original work end or an observed failure.
+ checkWork state
+ if amount=0L then reading <- false
elif amount>int64 (capacity-count) then raise (CaptureFault(ByteLimitExceeded role))
else
System.Array.Copy(scratch,0,bytes,count,int amount)
@@ -256,9 +293,10 @@ module ProviderContextCapture =
reset state document.Role document.DataFd
while reading do
checkWork state
- let amount=Native.read(document.DataFd,scratch,uint64 scratch.Length)
- if amount<0L then fault document.Role ("comparison read errno="+errno())
- elif amount=0L then reading <- false
+ let amount,error=state.Operations.Read document.DataFd scratch
+ if amount<0L then fault document.Role ("comparison read errno="+string error)
+ checkWork state
+ if amount=0L then reading <- false
elif amount>int64 (document.Observation.Bytes.Length-count) then raise (CaptureFault(InputChanged document.Role))
else
for index=0 to int amount-1 do
@@ -311,7 +349,7 @@ module ProviderContextCapture =
let descriptors,uncertain,borrowed=lock state.Sync (fun () -> state.Fds |> Seq.toList,state.CloseUncertain,state.RootLeases.Count>0)
if not uncertain && not borrowed then
for fd in descriptors do closeTracked state fd
- if (current()).Ticks>=state.Request.Budget.CleanupEnd.Ticks then report state CleanupDeadlineReached
+ if (current state).Ticks>=state.Request.Budget.CleanupEnd.Ticks then report state CleanupDeadlineReached
transition state OwnedResourcesSettled
let runOwned (state:OwnerState) action =
try action()
@@ -336,7 +374,7 @@ module ProviderContextCapture =
let mutable waiting=true
while waiting do
let completed=lock state.Sync (fun () -> state.Activity |> Option.forall(fun task -> task.IsCompleted))
- let now=current()
+ let now=current state
if now.Ticks>=state.Request.Budget.CleanupEnd.Ticks then
report state CleanupDeadlineReached
waiting <- false
@@ -349,7 +387,7 @@ module ProviderContextCapture =
if state.Request.Cancellation.IsCancellationRequested then report state CancellationRequested
System.Threading.Thread.Sleep 1
let snapshot (state:OwnerState) =
- let now=current()
+ let now=current state
lock state.Sync (fun () ->
{Operation=state.Request.Operation;Model=state.Model;Roots=state.RootObservations
Documents=state.Documents |> List.map(fun held -> held.Observation)
@@ -357,16 +395,23 @@ module ProviderContextCapture =
WorkEndReached=now.Ticks>=state.Request.Budget.WorkEnd.Ticks
CleanupEndReached=now.Ticks>=state.Request.Budget.CleanupEnd.Ticks
CancellationObserved=state.Request.Cancellation.IsCancellationRequested})
- let prepare (request:Request) =
- let errors=validateRequest (current()) request
+ let prepareWith nativeLeaseAllowed (operations:Operations) (request:Request) =
+ let errors=validateRequest (operations.Current()) request
let platform=System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux)
&& System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture=System.Runtime.InteropServices.Architecture.X64
&& request.Selection.Platform="linux-x64"
- let errors=if platform then errors else errors @ [UnsupportedPlatform]
+ let errors=if not nativeLeaseAllowed || platform then errors else errors @ [UnsupportedPlatform]
if not (List.isEmpty errors) then Error errors
else
- Ok(CaptureSession({Request=request;Sync=obj();Model=init();Fds=System.Collections.Generic.HashSet()
+ Ok(CaptureSession({Request=request;Operations=operations;NativeLeaseAllowed=nativeLeaseAllowed;Sync=obj();Model=init();Fds=System.Collections.Generic.HashSet()
Roots=[];RootObservations=[];Documents=[];Activity=None;CloseUncertain=false;RootLeases=System.Collections.Generic.List<_>()}))
+ let prepare request = prepareWith true nativeOperations request
+ let internal prepareControlled operations request = prepareWith false operations request
+ let internal ownership (session:CaptureSession) : Ownership =
+ let state=session.State
+ lock state.Sync (fun () ->
+ {PendingActivity=state.Activity |> Option.exists(fun task -> not task.IsCompleted)
+ RetainedDescriptors=state.Fds.Count;CloseUncertain=state.CloseUncertain;BorrowedRoots=state.RootLeases.Count})
let inspect (session:CaptureSession) = snapshot session.State
let capture (session:CaptureSession) =
let state=session.State
@@ -412,15 +457,15 @@ module ProviderContextCapture =
lock state.Sync (fun () ->
let active=state.Activity |> Option.exists(fun task -> not task.IsCompleted)
if active || state.CloseUncertain || state.RootLeases.Count>0 then
- if (current()).Ticks>=state.Request.Budget.CleanupEnd.Ticks then report state CleanupDeadlineReached
+ if (current state).Ticks>=state.Request.Budget.CleanupEnd.Ticks then report state CleanupDeadlineReached
Error OwnedActivitiesPending
elif state.Model.Phase=Released && state.Fds.Count=0 then Ok()
elif state.Fds.Count=0 then
- if (current()).Ticks>=state.Request.Budget.CleanupEnd.Ticks then report state CleanupDeadlineReached
+ if (current state).Ticks>=state.Request.Budget.CleanupEnd.Ticks then report state CleanupDeadlineReached
transition state ReleaseRequested
transition state OwnedResourcesSettled
Ok()
- elif (current()).Ticks>=state.Request.Budget.CleanupEnd.Ticks then
+ elif (current state).Ticks>=state.Request.Budget.CleanupEnd.Ticks then
report state CleanupDeadlineReached
Error CleanupDeadlineReached
else
@@ -433,6 +478,7 @@ module ProviderContextCapture =
let state = session.State
lock state.Sync (fun () ->
if original.Model.Phase<>Ready || original.Model.FirstFailure.IsSome || state.Model.Phase<>Ready then Error CaptureNotReady
+ elif not state.NativeLeaseAllowed then Error (NativePrerequisiteUnavailable "controlled sessions cannot duplicate native directory leases")
else
match state.Roots |> List.tryFind(fun root -> root.Path=state.Request.RepositoryRoot) with
| None -> Error CaptureNotReady
diff --git a/src/FS.GG.Governance.VerifyCommand/ProviderContextCapture.fsi b/src/FS.GG.Governance.VerifyCommand/ProviderContextCapture.fsi
index 8b27eca4..56ee4e50 100644
--- a/src/FS.GG.Governance.VerifyCommand/ProviderContextCapture.fsi
+++ b/src/FS.GG.Governance.VerifyCommand/ProviderContextCapture.fsi
@@ -93,3 +93,19 @@ module ProviderContextCapture =
/// Capture release refuses while the borrowed lease or its direct execution borrowers remain owned.
val borrowRepositoryRoot:
CaptureSession -> Result
+
+ /// Per-session inert interpreter controls. Each return pairs the native result with its immediate errno.
+ /// Installed before acquisition; never selected by production prepare or a process-global switch.
+ type internal Operations =
+ { Current: unit -> FS.GG.Governance.GateExecution.Model.MonotonicInstant
+ OpenAt: int -> string -> uint64 -> uint64 -> int64 * int
+ Stat: int -> byte[] -> int * int
+ OpenRead: string -> int * int
+ Read: int -> byte[] -> int64 * int
+ Seek: int -> int64 * int
+ Close: int -> int * int }
+ type internal Ownership =
+ { PendingActivity: bool; RetainedDescriptors: int; CloseUncertain: bool; BorrowedRoots: int }
+ /// No platform/native call; request validation still uses this session's original clock and ends.
+ val internal prepareControlled: Operations -> Request -> Result
+ val internal ownership: CaptureSession -> Ownership
diff --git a/tests/FS.GG.Governance.VerifyCommand.Tests/ProviderContextCaptureTests.fs b/tests/FS.GG.Governance.VerifyCommand.Tests/ProviderContextCaptureTests.fs
index ccb74370..b64b9a7b 100644
--- a/tests/FS.GG.Governance.VerifyCommand.Tests/ProviderContextCaptureTests.fs
+++ b/tests/FS.GG.Governance.VerifyCommand.Tests/ProviderContextCaptureTests.fs
@@ -416,3 +416,201 @@ let actualCases = baseActualCases @ [
let nativeTests =
testList "ProviderContextCapture Actual" (actualCases |> List.map(fun (name,run) ->
if nativeEnabled then testCase name run else ptestCase name run))
+
+// Controlled real interpreter: synthetic descriptors and finite gates, no kernel acquisition.
+// Every paused activity is unblocked and joined in finally, even after an assertion fails.
+type private ControlledFixture(blockRead: int, failClose: bool) =
+ let entered = new Threading.ManualResetEventSlim(false)
+ let resume = new Threading.ManualResetEventSlim(false)
+ let descriptors = Collections.Generic.Dictionary()
+ let positions = Collections.Generic.Dictionary()
+ let closes = Collections.Generic.List()
+ let mutable next = 1000
+ let mutable reads = 0
+ let mutable ticks = 10L
+ let mutable changed = false
+ let payload = Text.Encoding.UTF8.GetBytes "bounded-document"
+ let digest = Security.Cryptography.SHA256.HashData(payload) |> Convert.ToHexString |> fun s -> s.ToLowerInvariant()
+ let allocate path =
+ next <- next + 1
+ descriptors.Add(next,path)
+ positions.Add(next,0)
+ next
+ let write (bytes:byte[]) offset (value:byte[]) = Array.Copy(value,0,bytes,offset,value.Length)
+ let operations : Operations =
+ { Current = fun () -> {now with Ticks=Threading.Interlocked.Read(&ticks)}
+ OpenAt = fun directory path _ _ ->
+ let absolute = if directory = -100 then path else descriptors[directory]+"/"+path
+ int64 (allocate absolute),0
+ Stat = fun fd bytes ->
+ let path=descriptors[fd]
+ let directory=path="/repo" || path="/host"
+ write bytes 0 (BitConverter.GetBytes 0x17ffu)
+ write bytes 28 (BitConverter.GetBytes (if directory then 0x4000us else 0x8000us))
+ let identity = if path="/repo" then 1UL elif path="/host" then 2UL elif path.Contains("provenance") then 3UL else 4UL
+ write bytes 32 (BitConverter.GetBytes (if changed && not directory then identity+10UL else identity))
+ write bytes 40 (BitConverter.GetBytes (if directory then 0UL else uint64 payload.Length))
+ write bytes 144 (BitConverter.GetBytes 1UL)
+ 0,0
+ OpenRead = fun path ->
+ let original=Int32.Parse(path.Substring(path.LastIndexOf('/')+1))
+ allocate descriptors[original],0
+ Read = fun fd bytes ->
+ reads <- reads + 1
+ if reads=blockRead then
+ entered.Set()
+ if not (resume.Wait(TimeSpan.FromSeconds 5.)) then failwith "finite controlled read gate expired"
+ let position=positions[fd]
+ let count=min bytes.Length (payload.Length-position)
+ Array.Copy(payload,position,bytes,0,count)
+ positions[fd] <- position+count
+ int64 count,0
+ Seek = fun fd -> positions[fd] <- 0; 0L,0
+ Close = fun fd -> closes.Add fd; (if failClose then -1,5 else 0,0) }
+ member _.Operations = operations
+ member _.Request = {request with Selection={request.Selection with ProvenanceSha256=digest;PolicySha256=digest}}
+ member _.Advance value = Threading.Interlocked.Exchange(&ticks,value) |> ignore
+ member _.ChangeBinding() = changed <- true
+ member _.CloseAttempts = closes |> Seq.toList
+ member _.ReuseUncertainIdentity() =
+ // Simulate another owner's reuse of the failed close's numeric descriptor.
+ descriptors[closes[0]] <- "/unrelated-owner"
+ positions[closes[0]] <- 0
+ member _.WaitBlocked() = Expect.isTrue (entered.Wait(TimeSpan.FromSeconds 3.)) "controlled read entered within finite bound"
+ member _.Resume() = resume.Set()
+ interface IDisposable with
+ member _.Dispose() = resume.Dispose(); entered.Dispose()
+
+let private controlledSession (fixture:ControlledFixture) request =
+ match prepareControlled fixture.Operations request with
+ | Ok session -> session
+ | Error errors -> failtestf "controlled prepare refused: %A" errors
+let private joined (task:Threading.Tasks.Task<'a>) =
+ Expect.isTrue (task.Wait(TimeSpan.FromSeconds 8.)) "actual controlled activity returned within finite bound"
+ task.GetAwaiter().GetResult()
+let private settleControlled session =
+ Expect.isTrue
+ (Threading.SpinWait.SpinUntil((fun () -> not (ownership session).PendingActivity),TimeSpan.FromSeconds 3.))
+ "actual owner task finished"
+let private releaseControlled session =
+ release session |> ignore
+ settleControlled session
+ Expect.equal (release session) (Ok ()) "settled finite owner releases"
+ Expect.equal (inspect session).Settlement ResourcesReleased "all release predicates hold"
+let private pausedControl action =
+ use fixture = new ControlledFixture(3,false)
+ let session=controlledSession fixture fixture.Request
+ let caller=Threading.Tasks.Task.Run(fun () -> capture session)
+ try
+ fixture.WaitBlocked()
+ action fixture session caller
+ finally
+ fixture.Resume()
+ joined caller |> ignore
+ settleControlled session
+
+[]
+let controlledTests =
+ testList "ProviderContextCapture Controlled interpreter" [
+ testCase "finite success and original-object revalidation" (fun () ->
+ use fixture = new ControlledFixture(0,false)
+ let session=controlledSession fixture fixture.Request
+ Expect.equal (capture session).Model.Phase Ready "real comparison flow ready"
+ Expect.equal (revalidate session).Model.Phase Ready "same objects revalidated"
+ Expect.equal (inspect session).Documents.Length 2 "complete selected set"
+ releaseControlled session)
+ testCase "pending read retains original task descriptors and bounded evidence" (fun () ->
+ pausedControl (fun fixture session caller ->
+ let before=inspect session
+ Expect.equal before.Documents.Length 1 "first document retained before policy block"
+ Expect.isTrue (before.Documents[0].Bytes.Length<=fixture.Request.Limits.PerFileBytes) "bounded original evidence"
+ Expect.isTrue (ownership session).PendingActivity "registered actual activity"
+ fixture.Advance 100L
+ Expect.isTrue (Threading.SpinWait.SpinUntil((fun () -> (inspect session).Model.FirstFailure=Some WorkDeadlineReached),TimeSpan.FromSeconds 2.)) "original work end observed"
+ fixture.Advance 200L
+ let retained=joined caller
+ Expect.equal retained.Settlement Retained "cleanup expiry retains custody"
+ Expect.equal retained.Model.FirstFailure (Some WorkDeadlineReached) "first cause retained"
+ Expect.contains retained.Model.SecondaryFailures CleanupDeadlineReached "later cleanup cause retained"
+ Expect.isGreaterThan (ownership session).RetainedDescriptors 0 "descriptors retained"
+ Expect.equal (release session) (Error OwnedActivitiesPending) "release refuses pending read"
+ fixture.Resume()
+ settleControlled session
+ let late=inspect session
+ Expect.equal late.Documents before.Documents "no replacement output after failed read"
+ Expect.equal late.Model.FirstFailure retained.Model.FirstFailure "late read cannot erase first cause"
+ Expect.equal late.Settlement Retained "expired cleanup cannot release descriptors"
+ Expect.equal (release session) (Error CleanupDeadlineReached) "original cleanup end never renews"))
+ testCase "cancellation before cleanup settles actual activity" (fun () ->
+ use fixture = new ControlledFixture(1,false)
+ use cancelled = new Threading.CancellationTokenSource()
+ let session=controlledSession fixture {fixture.Request with Cancellation=cancelled.Token}
+ let caller=Threading.Tasks.Task.Run(fun () -> capture session)
+ try
+ fixture.WaitBlocked()
+ cancelled.Cancel()
+ Expect.isTrue (Threading.SpinWait.SpinUntil((fun () -> (inspect session).Model.FirstFailure=Some CancellationRequested),TimeSpan.FromSeconds 2.)) "cancellation observed"
+ finally fixture.Resume(); joined caller |> ignore; settleControlled session
+ Expect.equal (inspect session).Model.FirstFailure (Some CancellationRequested) "first cause survives retirement"
+ releaseControlled session)
+ testCase "pending revalidation cannot replace task or create readiness" (fun () ->
+ pausedControl (fun fixture session caller ->
+ let retry=Threading.Tasks.Task.Run(fun () -> revalidate session)
+ try
+ Expect.isTrue (Threading.SpinWait.SpinUntil((fun () -> (inspect session).Model.FirstFailure=Some OperationInProgress),TimeSpan.FromSeconds 2.)) "pending revalidation recorded before expiry"
+ finally fixture.Advance 200L
+ let rejected=joined retry
+ Expect.equal rejected.Model.Phase Retiring "pending revalidation refuses"
+ Expect.equal rejected.Model.FirstFailure (Some OperationInProgress) "pending task is original cause"
+ Expect.equal rejected.Settlement Retained "same owner retained"
+ joined caller |> ignore))
+ testCase "pending borrow refuses and keeps same owner" (fun () ->
+ pausedControl (fun fixture session caller ->
+ fixture.Advance 200L
+ Expect.isError (borrowRepositoryRoot session) "pending owner gives no lease"
+ Expect.equal (ownership session).BorrowedRoots 0 "no native borrowing attempted"
+ Expect.isTrue (ownership session).PendingActivity "original task remains"
+ joined caller |> ignore))
+ testCase "uncertain close stays sticky and synthetic identity is never retried" (fun () ->
+ use fixture = new ControlledFixture(0,true)
+ let session=controlledSession fixture fixture.Request
+ let observation=capture session
+ Expect.equal observation.Model.FirstFailure (Some OwnedActivitiesPending) "failed native-shaped close retained"
+ let attempts=fixture.CloseAttempts
+ Expect.equal attempts.Length 1 "one close attempt"
+ fixture.ReuseUncertainIdentity()
+ for _ in 1..3 do
+ Expect.equal (release session) (Error OwnedActivitiesPending) "uncertain numeric identity cannot retire"
+ Expect.isError (borrowRepositoryRoot session) "uncertainty cannot create lease"
+ Expect.equal fixture.CloseAttempts attempts "no numeric descriptor retry"
+ Expect.isTrue (ownership session).CloseUncertain "sticky uncertainty"
+ Expect.equal (inspect session).Settlement Retained "no false settlement")
+ testCase "byte limit first cause survives completed retirement" (fun () ->
+ use fixture = new ControlledFixture(0,false)
+ let session=controlledSession fixture {fixture.Request with Limits={PerFileBytes=1;AggregateBytes=2}}
+ Expect.equal (capture session).Model.FirstFailure (Some(ByteLimitExceeded "provenance")) "actual read enforces cap"
+ Expect.isEmpty (inspect session).Documents "over-limit bytes never become observations"
+ releaseControlled session)
+ testCase "digest failure precedes close uncertainty" (fun () ->
+ use fixture = new ControlledFixture(0,true)
+ let session=controlledSession fixture {fixture.Request with Selection={fixture.Request.Selection with ProvenanceSha256=String('0',64)}}
+ let observation=capture session
+ Expect.equal observation.Model.FirstFailure (Some(RawDigestMismatch "provenance")) "original digest failure preserved"
+ Expect.contains observation.Model.SecondaryFailures OwnedActivitiesPending "independent close failure preserved"
+ Expect.equal (release session) (Error OwnedActivitiesPending) "uncertainty retains owner")
+ testCase "changed original binding refuses revalidation" (fun () ->
+ use fixture = new ControlledFixture(0,false)
+ let session=controlledSession fixture fixture.Request
+ Expect.equal (capture session).Model.Phase Ready "ready before drift"
+ fixture.ChangeBinding()
+ Expect.equal (revalidate session).Model.FirstFailure (Some(InputChanged "provenance")) "original-object comparison rejects drift"
+ releaseControlled session)
+ testCase "one-use acquisition cannot replace retained original evidence" (fun () ->
+ use fixture = new ControlledFixture(0,false)
+ let session=controlledSession fixture fixture.Request
+ let first=capture session
+ let second=capture session
+ Expect.equal second.Model.FirstFailure (Some CaptureAlreadyConsumed) "no second acquisition"
+ Expect.equal second.Documents first.Documents "same bounded original bytes"
+ releaseControlled session)
+ ] |> testSequenced
diff --git a/tests/FS.GG.Governance.VerifyCommand.Tests/SurfaceDriftTests.fs b/tests/FS.GG.Governance.VerifyCommand.Tests/SurfaceDriftTests.fs
index e4b4457c..443380e1 100644
--- a/tests/FS.GG.Governance.VerifyCommand.Tests/SurfaceDriftTests.fs
+++ b/tests/FS.GG.Governance.VerifyCommand.Tests/SurfaceDriftTests.fs
@@ -65,6 +65,19 @@ let tests =
unexpected)
}
+ test "capture operation injection and ownership facts remain internal" {
+ let exported=verifyCommand.GetExportedTypes()
+ let names=exported |> Array.choose(fun t -> Option.ofObj t.FullName)
+ for shape in ["Operations"; "Ownership"; "Native"; "OwnerState"] do
+ Expect.isFalse
+ (names |> Array.exists(fun name -> name.StartsWith("FS.GG.Governance.VerifyCommand.ProviderContextCapture+"+shape,System.StringComparison.Ordinal)))
+ ("capture implementation shape escaped: "+shape)
+ let captureModule=exported |> Array.find(fun t -> t.Name="ProviderContextCapture")
+ let publicMethods=captureModule.GetMethods(System.Reflection.BindingFlags.Public ||| System.Reflection.BindingFlags.Static)
+ for name in ["prepareControlled"; "ownership"] do
+ Expect.isFalse (publicMethods |> Array.exists(fun method -> method.Name=name)) ("test access escaped: "+name)
+ }
+
test "VerifyCommand references VerifyJson (not AuditJson) and no kernel/host/cli" {
let referenced =
verifyCommand.GetReferencedAssemblies()