diff --git a/.github/workflows/complete-record-source.yml b/.github/workflows/complete-record-source.yml index db3b291..b1d6b98 100644 --- a/.github/workflows/complete-record-source.yml +++ b/.github/workflows/complete-record-source.yml @@ -36,6 +36,10 @@ jobs: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" test -z "$(git status --porcelain --untracked-files=all)" + - name: Run the portable seed helper source controls + shell: bash + run: python3 -I -B tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/run-source-controls.py + - name: Set up the exact .NET SDK uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 with: diff --git a/.github/workflows/prearm-source-controls.yml b/.github/workflows/prearm-source-controls.yml index ec15285..176892c 100644 --- a/.github/workflows/prearm-source-controls.yml +++ b/.github/workflows/prearm-source-controls.yml @@ -17,6 +17,9 @@ on: - tests/Broker.Browser.Live.Tests/** - tests/Broker.NativeProof/tactical-native-journey.spec.js - tests/Broker.NativeProof/tactical-native-readiness.test.mjs + - tests/Broker.NativeProof/one-unit-prearm.test.mjs + - tests/Broker.NativeProof/Policy/build-stock-selection.mjs + - tests/Broker.NativeProof/Policy/build-stock-selection.test.mjs workflow_dispatch: permissions: @@ -64,6 +67,10 @@ jobs: shell: bash run: node --test tests/Broker.NativeProof/tactical-native-readiness.test.mjs + - name: Run the one-unit and compiler-capture source controls + shell: bash + run: node --test tests/Broker.NativeProof/one-unit-prearm.test.mjs tests/Broker.NativeProof/Policy/build-stock-selection.test.mjs + - name: Restore and build the live boundary source shell: bash env: diff --git a/docs/roadmaps/barc-01-useful-play.md b/docs/roadmaps/barc-01-useful-play.md index b55d543..052d0f4 100644 --- a/docs/roadmaps/barc-01-useful-play.md +++ b/docs/roadmaps/barc-01-useful-play.md @@ -390,3 +390,101 @@ capacity and prospective operation authority remain required; all useful-play journeys remain **0/6**. Admission telemetry attempt `bar-stock-correspondence-source-admission-20261003` returned `not-configured`; usage is Unknown. + +## StockSelection compiler capture source — 2026-10-04 + +The canonical [StockSelection producer](../../tests/Broker.NativeProof/Policy/build-stock-selection.mjs) +accepts `--capture-dir ` for prospective qualification. +That route retains separate raw restore/Fable streams, actual nested arguments, +working directory and closed child environment, Fable 5.18 `--verbose` evaluated +source/reference hashes, the temporary output graph and literal runtime-import +rewrite correspondence. Existing builds without capture keep their cleanup route. +The capture records prior descriptors as historical and publishes a new descriptor +only after successful compiler and runtime-byte guards. A captured compiler graph +is observed after compilation and must join an independently frozen parent input +census; it does not establish compiler/package authenticity by itself. + +Seventeen [production-call controls](../../tests/Broker.NativeProof/Policy/build-stock-selection.test.mjs) +pass on the existing Node 24.8 interpreter with every SDK/compiler launch mocked. +The optional `--nuget-config ` argument passes the exact reviewed +stock feed configuration to nested restore and hashes it before/after compilation. +The original 1MiB nested output limit remains enforced. Controls cover explicit +config arguments, drift, invalid paths and duplicate flags as well as raw streams, +retained partial failures, timeout/signal refusal, stale +previous descriptors, capture collisions, graph links, runtime mismatch, missing +verbose receipts and authored input drift. No restore, compiler, browser, engine +or game operation was executed. Authentic producer tools and fresh bounded build +admission remain pending. This source slice changes neither runtime authority nor +historical READY/map cause Unknown; useful native play remains **0/6**. + + +## One-produced-unit source qualification — 2026-10-04 + +The [accepted-state diagnostic](../../src/Broker.Protocol/HighBarCoordinatorService.fs) +retains a cloned update only inside the accepted owning-generation fence. The +[optional raw journal](../../tests/Broker.NativeProof/LiveHost.fs) records complete +snapshots, actual creation builder identity, finish events and correlated native +dispatch with bounded run/source/writer/generation provenance. Unsupported create +and finish deltas still invalidate the browser baseline until a complete native +replacement arrives. No new RPC, public contract or native command is introduced. + +The fresh locked Release build passed naturally with the exact SDK/runtime and +signed dependency closure. Six [compiled loopback controls](../../tests/Broker.NativeProof/AttributionControls.fs) +passed through the actual gRPC service and reducer, including stale-generation +refusal and replacement recovery. Full compiler-input, PE and portable-PDB proof +binds the changed compiled bodies to frozen source digest +`adedbff5fe71daab722b6f145e69a10e3e882a2954fdbb5c2c689a5c7b2a4ac7`; +it does not attribute them to the earlier base commit. Separately, actual installed +API 0.2.1 preparation passed genuine imports and exactly one Playwright CLI +list-mode discovery through the unchanged no-process-descendants profile. Binding +change, late input drift, missing capture, extra input and symlink controls refused. +No browser, actor or game ran. Failed predecessor attempts remain failed; their +independently passed phases are reused by exact physical custody. The final capsule +source seal is `f67fae3aa72ea99db0463c41d4512d4b7b0ebb17c04b4160493d4fa530e3f397`. + +This is **source-qualified preparation**, with local routine source delivery still +pending parent PR admission and native merge readback. The compiled product bytes +are unchanged during integration; the new roadmap text requires no compile replay. +`actorEligible=false`, `unitProducedAccepted=false`, and useful native play **0/6** +remain explicit. The [one-unit oracle](../../tests/Broker.NativeProof/tactical-native-journey.spec.js) +requires a genuinely qualified native event source and no-autonomous configuration +for the exact run/source/writer/process/channel/generation before Arm. Controlled +loopback events and passive fixture comments cannot grant that qualification. + +The next source preparation joins the fresh host/receiver to the existing policy's +nineteen named managed, provenance and runtime roles, exact placement/search layout +and physical custody; it also binds the selected external-control plugin and +passive fixture/configuration to actual loaded identities. Root separately admits +a bounded attribution/startup prerequisite and any later one-shot Append Count1 +operation. Predispatch final-read remains a fence; actual creation/finish and a +separate later terminal native sample must prove exactly one product. Full six +useful-play journeys, publication and installed adoption remain open. Native usage +coverage remains unknown where the observer rejected canonical native identity. + +### BARC-01.5f pre-Arm producer source amendment (2026-10-04) + +The six preparation phases are source-qualified through the retained v5 managed +build/attribution controls, v6 compiler/PE/PDB proof and v9 installed capsule +qualification. Their earlier failed overall attempts remain failed. The qualified +managed bodies remain unchanged; this successor changes the one-unit JS consumer +and prepares a private read-only pre-Arm qualifier. Its controlled Python checks +prove source predicates only. Changed JS controls and installed list discovery +still require separate root admission; old discovery qualifies its original bytes. + +Qualification must retain an actual earlier finite nonempty seed queue, native +factory creation/finish, unsupported-event invalidation and later complete +replacement/empty repeat-off evidence. Browser readiness then freezes a fresh +same-run/generation empty baseline containing those finished seed lifetimes and +rechecks source/configuration/PID custody before its single Arm and Append Count1. +This removes the prior nonempty-seed/browser timing contradiction. Missing real +basis/lifetime joins, extra production/dispatch, drift or expiry refuse; no fixture +boolean, extra seed, stale-basis retry or receipt rewrite supplies acceptance. + +The existing canonical nineteen-role PolicyClosure and full dependency/runtime +census are unchanged. Final product/helper graph composition remains blocked on a +qualified genuine successor provenance producer; old build receipts cannot be +relabeled for this candidate. Root owns the finite startup, transition and later +one-shot browser admissions, effect-specific authority checks and PR/integration. +No native startup, browser, actor or production operation was admitted here. +**actorEligible=false; unitProducedAccepted=false; useful play 0/6.** BARC-01.5f +and the original six useful-play journeys remain open. diff --git a/src/Broker.Protocol/HighBarCoordinatorService.fs b/src/Broker.Protocol/HighBarCoordinatorService.fs index 3f64c30..c2d9b45 100644 --- a/src/Broker.Protocol/HighBarCoordinatorService.fs +++ b/src/Broker.Protocol/HighBarCoordinatorService.fs @@ -8,6 +8,9 @@ open Highbar.V1 module HighBarCoordinatorService = + // Read-only opt-in diagnostics; no new RPC or public service surface. + let private acceptedStateDiagnostics = new System.Diagnostics.DiagnosticListener("FSBar.HighBar.AcceptedState") + type Config = { expectedSchemaVersion: string ownerRule: BrokerState.OwnerRule @@ -360,6 +363,15 @@ module HighBarCoordinatorService = BrokerState.refreshLiveness now service.hub | WireConvert.KeepAliveOnly -> BrokerState.refreshLiveness now service.hub + // Keep generation fencing through the readonly cloned diagnostic. + if acceptedStateDiagnostics.IsEnabled("AcceptedState") then + let disposition = + match result with + | WireConvert.NewSnapshot _ -> "materialized" + | WireConvert.Invalidated _ -> "invalidated" + | WireConvert.Gap _ -> "gap" + | WireConvert.KeepAliveOnly -> "liveness-only" + acceptedStateDiagnostics.Write("AcceptedState", box (struct(owningGeneration, upd.Clone(), disposition))) true else false) if accepted then diff --git a/tests/Broker.Browser.Live.Tests/LiveIntegrationTests.fs b/tests/Broker.Browser.Live.Tests/LiveIntegrationTests.fs index 68c86d8..0e36d1c 100644 --- a/tests/Broker.Browser.Live.Tests/LiveIntegrationTests.fs +++ b/tests/Broker.Browser.Live.Tests/LiveIntegrationTests.fs @@ -551,13 +551,69 @@ let tests = testList "production live boundary" [ Expect.equal nativeDispatch.Result.Status LiveResultStatus.Applied "the one terminal dispatch remains applied" Expect.equal nativeDispatch.Result.ParentId brokerResult.Result.ParentId "both native stages retain the reserved parent" + // Results and observations share the production output channel. Await a + // precise result without assuming it is the next WebSocket message. + let awaitParentResult (request: SubmitLiveIntent) stage = task { + use deadline = new CancellationTokenSource(TimeSpan.FromSeconds 3.0) + let seen = ResizeArray() + let describe () = String.concat ", " seen + let rec next remaining = task { + if remaining = 0 then + return failtestf "awaiting %A exhausted eight envelopes; received [%s]" stage (describe()) + else + let bytes = Array.zeroCreate 65536 + let mutable count = 0 + let mutable complete = false + while not complete do + if count = bytes.Length then + failtest "result wait exceeded the bounded WebSocket frame capacity" + let! frame = socket.ReceiveAsync(Memory(bytes, count, bytes.Length-count), deadline.Token).AsTask() + Expect.equal frame.MessageType WebSocketMessageType.Binary "result wait accepts only binary protocol frames" + count <- count + frame.Count + complete <- frame.EndOfMessage + let envelope = LiveServerEnvelope.Parser.ParseFrom(bytes, 0, count) + match envelope.BodyCase with + | LiveServerEnvelope.BodyOneofCase.Result -> + let result = envelope.Result + seen.Add(sprintf "Result(%A)" result.Stage) + Expect.equal result.ParentId request.ParentId (sprintf "exact pending parent; received [%s]" (describe())) + Expect.equal result.InputId request.InputId "result preserves the exact pending input" + Expect.equal result.Stage stage (sprintf "required result order; received [%s]" (describe())) + Expect.equal result.Controller request.Controller "result preserves the complete controller identity" + Expect.equal result.Module request.Module "result preserves the complete module identity" + Expect.equal result.Basis request.Basis "result preserves the exact submitted basis" + return envelope + | LiveServerEnvelope.BodyOneofCase.Observation -> + let observed = envelope.Observation + Expect.isNotNull observed.Preview "interleaved observation includes its typed preview" + Expect.isNotNull observed.Basis "interleaved observation includes its typed basis" + seen.Add(sprintf "Observation(%d)" observed.Preview.Sequence) + // No new metadata is published during these parents. Only + // the already paired current observation can interleave. + Expect.equal observed.Basis raceObservation.Observation.Basis "interleaved observation has the exact paired owning-generation basis" + Expect.equal observed.Preview.SessionId controller.SessionId "interleaved observation belongs to this authenticated session" + Expect.equal observed.Preview.Sequence observed.Basis.StateSequence "interleaved preview and basis have the same sequence" + Expect.equal observed.Preview.Validity raceObservation.Observation.Preview.Validity "interleaved observation retains current validity without a new gap" + Expect.sequenceEqual observed.Units raceObservation.Observation.Units "interleaved observation retains exact unit lifetimes" + Expect.equal observed.Tactical raceObservation.Observation.Tactical "interleaved tactical evidence retains the exact paired basis" + return! next (remaining-1) + | unexpected -> + return failtestf "awaiting %A received unexpected %A; preceding [%s]" stage unexpected (describe()) + } + try + return! next 8 + with :? OperationCanceledException -> + return failtestf "awaiting %A exceeded three seconds; received [%s]" stage (describe()) + } + let completeAdditionalParent dispatchSequence = task { let request=submit.Clone() request.ParentId<-ByteString.CopyFrom(Guid.NewGuid().ToByteArray()) request.InputId<-ByteString.CopyFrom(Guid.NewGuid().ToByteArray()) do! send socket (LiveClientEnvelope(Submit=request)) - let! (broker: LiveServerEnvelope)=receive socket + let! (broker: LiveServerEnvelope)=awaitParentResult request LiveResultStage.BrokerAdmission Expect.equal broker.Result.Stage LiveResultStage.BrokerAdmission "the next parent is admitted on the same socket" + Expect.equal broker.Result.Status LiveResultStatus.Accepted "the next parent is accepted before awaiting its native child" let! more=commandCall.ResponseStream.MoveNext(CancellationToken.None) Expect.isTrue more "the next parent reaches the live native channel" let nextChild=commandCall.ResponseStream.Current @@ -572,8 +628,9 @@ let tests = testList "production live boundary" [ report.ChannelIncarnation<-nextChild.Binding.Value.CommandChannelIncarnation report.Result<-ValueSome admitted let! (_:CommandBatchResultReportAck)=coordinator.ReportCommandBatchResultAsync(report).ResponseAsync - let! (nativeAccepted:LiveServerEnvelope)=receive socket + let! (nativeAccepted:LiveServerEnvelope)=awaitParentResult request LiveResultStage.NativeAdmission Expect.equal nativeAccepted.Result.Stage LiveResultStage.NativeAdmission "native admission follows broker admission" + Expect.equal nativeAccepted.Result.Status LiveResultStatus.Accepted "the matching child receives accepted native admission" let applied=CommandDispatchEvent.empty() applied.BatchSeq<-nextChild.Batch.Value.BatchSeq applied.ClientCommandId<-nextChild.Batch.Value.ClientCommandId.Value @@ -591,7 +648,7 @@ let tests = testList "production live boundary" [ state.Frame<-applied.Frame state.Delta<-delta do! push.RequestStream.WriteAsync state - let! (terminal:LiveServerEnvelope)=receive socket + let! (terminal:LiveServerEnvelope)=awaitParentResult request LiveResultStage.NativeDispatch Expect.equal terminal.Result.Stage LiveResultStage.NativeDispatch "native dispatch remains terminal" Expect.equal terminal.Result.Status LiveResultStatus.Applied "the next parent completes exactly once" } diff --git a/tests/Broker.NativeProof/AttributionControls.fs b/tests/Broker.NativeProof/AttributionControls.fs new file mode 100644 index 0000000..68fe8b0 --- /dev/null +++ b/tests/Broker.NativeProof/AttributionControls.fs @@ -0,0 +1,164 @@ +namespace Broker.NativeProof + +open System +open System.Collections.Concurrent +open System.Diagnostics +open System.Net +open System.Net.Sockets +open System.Threading +open Grpc.Net.Client +open Broker.Core +open Broker.Protocol +open Highbar.V1 + +// Compiled service controls use a controlled loopback producer, never a game. +module AttributionControls = + let private require condition message = if not condition then invalidOp message + + let private snapshot sequence frame = + let state = StateSnapshot.empty() + state.FrameNumber <- frame + let unit = OwnUnit.empty() + unit.UnitId <- 7u + unit.DefId <- 303u + unit.BuildProgress <- 1f + let position = Vector3.empty() + position.X <- 11f + position.Y <- 7f + position.Z <- 23f + unit.Position <- ValueSome position + state.OwnUnits.Add unit + let update = StateUpdate.empty() + update.Seq <- sequence + update.Frame <- frame + update.Snapshot <- state + update + + let private lifecycle sequence frame created = + let event = DeltaEvent.empty() + if created then + let value = UnitCreatedEvent.empty() + value.UnitId <- 8 + value.BuilderId <- 7 + event.UnitCreated <- value + else + let value = UnitFinishedEvent.empty() + value.UnitId <- 8 + event.UnitFinished <- value + let delta = StateDelta.empty() + delta.Events.Add event + let update = StateUpdate.empty() + update.Seq <- sequence + update.Frame <- frame + update.Delta <- delta + update + + let run () = task { + use lifetime = new CancellationTokenSource(TimeSpan.FromSeconds 25.) + let rows = ConcurrentQueue() + let subscriptions = ConcurrentBag() + let listenerObserver = + { new IObserver with + member _.OnNext listener = + if listener.Name = "FSBar.HighBar.AcceptedState" then + let observer = + { new IObserver> with + member _.OnNext value = + if value.Key = "AcceptedState" then + rows.Enqueue(unbox value.Value) + member _.OnError error = raise error + member _.OnCompleted() = () } + subscriptions.Add(listener.Subscribe observer) + member _.OnError error = raise error + member _.OnCompleted() = () } + use all = DiagnosticListener.AllListeners.Subscribe(listenerObserver) + let portSelection = new TcpListener(IPAddress.Loopback, 0) + portSelection.Start() + let port = (portSelection.LocalEndpoint :?> IPEndPoint).Port + portSelection.Stop() + let audit = ConcurrentQueue() + let! host = ServerHost.start { ServerHost.defaultOptions with listenAddress=sprintf "127.0.0.1:%d" port } (Version(1,0)) audit.Enqueue lifetime.Token + let waitFor name condition = task { + let stop = DateTimeOffset.UtcNow.AddSeconds 8. + while not(condition()) do + if DateTimeOffset.UtcNow > stop then invalidOp ("attribution control timeout: " + name) + do! System.Threading.Tasks.Task.Delay(10,lifetime.Token) + } + let row sequence frame = + rows.ToArray() |> Array.find(fun struct(_,value,_) -> value.Seq=sequence && value.Frame=frame) + try + use channel = GrpcChannel.ForAddress(sprintf "http://127.0.0.1:%d" port) + let producer = HighBarCoordinator.HighBarCoordinatorClient(channel) + let heartbeat name = + let request = HeartbeatRequest.empty() + request.PluginId <- name + request.SchemaVersion <- "1.0.0" + producer.HeartbeatAsync(request,cancellationToken=lifetime.Token).ResponseAsync + let! _ = heartbeat "attribution-old" + use oldPush = producer.PushStateAsync(cancellationToken=lifetime.Token) + do! oldPush.RequestStream.WriteAsync(snapshot 1UL 10u) + do! waitFor "initial snapshot" (fun () -> rows.Count=1) + let struct(generation,baseline,disposition) = row 1UL 10u + require (generation<>Guid.Empty && disposition="materialized" && baseline.Snapshot.OwnUnits.[0].UnitId=7u) "actual baseline/generation missing" + printfn "ATTRIBUTION_CONTROL_PASS baseline-generation" + + do! oldPush.RequestStream.WriteAsync(lifecycle 2UL 11u true) + do! waitFor "created" (fun () -> rows.Count=2) + let struct(createdGeneration,created,createdDisposition) = row 2UL 11u + require (createdGeneration=generation && createdDisposition="invalidated" && created.Delta.Events.[0].UnitCreated.BuilderId=7 && created.Delta.Events.[0].UnitCreated.UnitId=8) "creation provenance/invalidation changed" + require (audit.ToArray() |> Array.exists(function Audit.AuditEvent.CoordinatorStateInvalidated _ -> true | _ -> false)) "actual hub invalidation missing" + printfn "ATTRIBUTION_CONTROL_PASS creation-invalidates-with-builder" + + do! oldPush.RequestStream.WriteAsync(lifecycle 3UL 12u false) + do! waitFor "finished" (fun () -> rows.Count=3) + let struct(finishedGeneration,finished,finishedDisposition) = row 3UL 12u + require (finishedGeneration=generation && finishedDisposition="invalidated" && finished.Delta.Events.[0].UnitFinished.UnitId=8) "completion/invalidation changed" + printfn "ATTRIBUTION_CONTROL_PASS completion-retains-invalid-baseline" + + let replacement = snapshot 4UL 13u + let product = OwnUnit.empty() + product.UnitId <- 8u + product.DefId <- 304u + product.BuildProgress <- 1f + let productPosition = Vector3.empty() + productPosition.X <- 13f + productPosition.Y <- 7f + productPosition.Z <- 25f + product.Position <- ValueSome productPosition + replacement.Snapshot.OwnUnits.Add product + do! oldPush.RequestStream.WriteAsync replacement + do! waitFor "full replacement" (fun () -> rows.Count=4) + let struct(replacementGeneration,replaced,replacementDisposition) = row 4UL 13u + require (replacementGeneration=generation && replacementDisposition="materialized" && replaced.Snapshot.OwnUnits.Count=2) "full replacement missing" + let view,_ = WireConvert.applyHighBarStateUpdate baseline WireConvert.emptyRunningView + let invalid,_ = WireConvert.applyHighBarStateUpdate created view + require (not(WireConvert.hasValidBaseline invalid)) "unsupported creation falsely materialized" + let invalidFinished,_ = WireConvert.applyHighBarStateUpdate finished invalid + require (not(WireConvert.hasValidBaseline invalidFinished)) "finish falsely repairs baseline" + let repaired,_ = WireConvert.applyHighBarStateUpdate replaced invalidFinished + require (WireConvert.hasValidBaseline repaired) "complete replacement failed to repair" + printfn "ATTRIBUTION_CONTROL_PASS full-replacement-recovers" + + // Let the existing owning-generation watchdog detach this controlled stream. + do! waitFor "old generation detach" (fun () -> BrokerState.session host.Hub |> Option.isNone) + let! _ = heartbeat "attribution-new" + use newPush = producer.PushStateAsync(cancellationToken=lifetime.Token) + do! newPush.RequestStream.WriteAsync(snapshot 1UL 100u) + do! waitFor "new generation" (fun () -> rows.Count=5) + let struct(newGeneration,_,newDisposition) = row 1UL 100u + require (newGeneration<>generation && newDisposition="materialized") "replacement generation identity reused" + printfn "ATTRIBUTION_CONTROL_PASS new-generation-distinct" + + do! oldPush.RequestStream.WriteAsync(lifecycle 5UL 999u true) + do! System.Threading.Tasks.Task.Delay(100,lifetime.Token) + require (rows.Count=5) "stale stream emitted accepted attribution" + let tick = BrokerState.session host.Hub |> Option.bind(fun session -> (Session.toReading DateTimeOffset.UtcNow session).telemetry) |> Option.map _.tick + require (tick=Some 100L) "stale stream contaminated replacement" + printfn "ATTRIBUTION_CONTROL_PASS stale-generation-refused" + printfn "ATTRIBUTION_CONTROLS_COMPLETE passed=6 controlledProducer=true gameLaunched=false" + return 0 + finally + lifetime.Cancel() + (host :> IAsyncDisposable).DisposeAsync().AsTask().GetAwaiter().GetResult() + for subscription in subscriptions do subscription.Dispose() + } diff --git a/tests/Broker.NativeProof/Broker.NativeProof.fsproj b/tests/Broker.NativeProof/Broker.NativeProof.fsproj index cde018a..1116d11 100644 --- a/tests/Broker.NativeProof/Broker.NativeProof.fsproj +++ b/tests/Broker.NativeProof/Broker.NativeProof.fsproj @@ -8,6 +8,7 @@ + diff --git a/tests/Broker.NativeProof/LiveHost.fs b/tests/Broker.NativeProof/LiveHost.fs index f70efd6..3005d26 100644 --- a/tests/Broker.NativeProof/LiveHost.fs +++ b/tests/Broker.NativeProof/LiveHost.fs @@ -154,6 +154,65 @@ module LiveHost = trace.Flush();journalComplete<-true) let write (value: objnull) = writeKind "event" value if stock then writeKind "header" (box {|selectedCase="stock-smoke-count1"|}) + // Optional bounded raw attribution journal. It grants no commands, Arm or gameplay. + // This source requires fresh managed qualification; old assemblies cannot emit it. + use nativeAttributionSubscription = + match Environment.GetEnvironmentVariable("BARC_ONE_UNIT_RAW_EVENTS") |> Option.ofObj with + | None -> { new IDisposable with member _.Dispose() = () } + | Some path -> + if not stock || not(Path.IsPathFullyQualified path) || File.Exists path then invalidOp "new private stock attribution path required" + let stream = openStockJournal path + let journal = new StreamWriter(stream) + let gate = obj() + let mutable sequence = 0UL + let mutable totalBytes = 0 + let subscriptions = ResizeArray() + let append generation stateSequence frame disposition kind (value: objnull) = lock gate (fun () -> + sequence <- sequence + 1UL + let row = JsonSerializer.Serialize {|schema="fsbar.barc-one-unit-raw-state-journal/v1";runId=runId;sequence=string sequence;writer=writer;source=source;acceptedGeneration=string generation;stateSequence=string stateSequence;nativeFrame=frame;disposition=disposition;kind=kind;value=value|} + let bytes = System.Text.Encoding.UTF8.GetByteCount row + 1 + if sequence > 8192UL || bytes > 32768 || totalBytes > 4*1024*1024-bytes then + lifetime.Cancel() + invalidOp "native attribution journal bound exceeded" + totalBytes <- totalBytes + bytes + journal.WriteLine row + journal.Flush()) + let listenerObserver = { new IObserver with + member _.OnNext listener = + if listener.Name = "FSBar.HighBar.AcceptedState" then + let observer = { new IObserver> with + member _.OnNext diagnostic = + if diagnostic.Key = "AcceptedState" then + let struct(generation, update, disposition) = unbox diagnostic.Value + match update.Payload with + | ValueSome(Highbar.V1.StateUpdate.Types.Payload.Snapshot snapshot) -> + let units = snapshot.OwnUnits |> Seq.map(fun u -> {|unitId=u.UnitId;definitionId=u.DefId;underConstruction=u.UnderConstruction;buildProgress=u.BuildProgress|}) |> Seq.toArray + append generation update.Seq update.Frame disposition "snapshot" (box {|units=units|}) + | ValueSome(Highbar.V1.StateUpdate.Types.Payload.Delta delta) -> + for event in delta.Events do + match event.Kind with + | ValueSome(Highbar.V1.DeltaEvent.Types.Kind.UnitCreated created) -> + append generation update.Seq update.Frame disposition "unit-created" (box {|unitId=created.UnitId;builderId=created.BuilderId|}) + | ValueSome(Highbar.V1.DeltaEvent.Types.Kind.UnitFinished finished) -> + append generation update.Seq update.Frame disposition "unit-finished" (box {|unitId=finished.UnitId|}) + | ValueSome(Highbar.V1.DeltaEvent.Types.Kind.CommandDispatch dispatch) -> + append generation update.Seq update.Frame disposition "command-dispatch" (box {|batchSequence=string dispatch.BatchSeq;correlationId=string dispatch.ClientCommandId;commandIndex=dispatch.CommandIndex;unitId=dispatch.TargetUnitId;status=string dispatch.Status;dispatchFrame=dispatch.Frame;commandChannelIncarnation=dispatch.ChannelIncarnation|}) + | _ -> () + | _ -> () + member _.OnError _ = lifetime.Cancel() + member _.OnCompleted() = () } + lock gate (fun () -> subscriptions.Add(listener.Subscribe observer)) + member _.OnError _ = lifetime.Cancel() + member _.OnCompleted() = () } + let all = System.Diagnostics.DiagnosticListener.AllListeners.Subscribe listenerObserver + { new IDisposable with + member _.Dispose() = + all.Dispose() + lock gate (fun () -> + for subscription in subscriptions do subscription.Dispose() + journal.Flush() + stream.Flush(true) + journal.Dispose()) } let! host = ServerHost.start { ServerHost.defaultOptions with listenAddress=grpcAddress } (Version(1,0)) ignore lifetime.Token try let lobby: Lobby.LobbyConfig = diff --git a/tests/Broker.NativeProof/Policy/build-stock-selection.mjs b/tests/Broker.NativeProof/Policy/build-stock-selection.mjs index 96a0beb..6a76783 100644 --- a/tests/Broker.NativeProof/Policy/build-stock-selection.mjs +++ b/tests/Broker.NativeProof/Policy/build-stock-selection.mjs @@ -1,35 +1,87 @@ import { createHash } from "node:crypto"; -import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync, readdirSync, lstatSync, existsSync } from "node:fs"; import { tmpdir } from "node:os"; -import { dirname, join, resolve } from "node:path"; +import { dirname, join, resolve, relative, isAbsolute } from "node:path"; import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; -const policyRoot=dirname(fileURLToPath(import.meta.url)), proofRoot=dirname(policyRoot), repoRoot=resolve(proofRoot,"../.."), temporary=mkdtempSync(join(tmpdir(),"stock-selection-fable-")), outputRoot=join(policyRoot,"generated"), hash=bytes=>createHash("sha256").update(bytes).digest("hex"), read=path=>readFileSync(path), relativeToRepo=path=>path.slice(repoRoot.length+1).replaceAll("\\","/"); - -try{ - const tools=JSON.parse(readFileSync(join(repoRoot,".config/dotnet-tools.json"),"utf8")),fableVersion=tools.tools.fable.version; - if(fableVersion!=="5.18.0")throw new Error(`expected pinned Fable 5.18.0, found ${fableVersion}`); - const restore=spawnSync("dotnet",["restore",join(policyRoot,"StockSelection.fsproj"),"--locked-mode"],{cwd:repoRoot,encoding:"utf8"}); - if(restore.status!==0)throw new Error(`locked policy restore failed\n${restore.stdout}${restore.stderr}`); - const result=spawnSync("dotnet",["fable",join(policyRoot,"StockSelection.fsproj"),"--outDir",temporary,"--noCache","--noRestore"],{cwd:repoRoot,encoding:"utf8"}); - if(result.status!==0)throw new Error(`Fable compilation failed\n${result.stdout}${result.stderr}`); - const compiledPath=join(temporary,"StockSelection.js"),libraryRoot=join(proofRoot,"node_modules/@fable-org/fable-library-js"); - let compiled=readFileSync(compiledPath,"utf8"); - const imports=[...compiled.matchAll(/\.\/fable_modules\/fable-library-js\.5\.18\.0\/([^"']+)/g)].map(match=>match[1]); - if(imports.length===0)throw new Error("generated policy did not declare its Fable runtime imports"); - for(const imported of new Set(imports)){ - const copied=read(join(temporary,"fable_modules/fable-library-js.5.18.0",imported)),installed=read(join(libraryRoot,imported)); - if(!copied.equals(installed))throw new Error(`Fable runtime import differs from pinned npm package: ${imported}`); - } - compiled=`${compiled.replaceAll("./fable_modules/fable-library-js.5.18.0/","@fable-org/fable-library-js/").trimEnd()}\n`; - mkdirSync(outputRoot,{recursive:true}); - const generatedPath=join(outputRoot,"StockSelection.js"),generatedBytes=Buffer.from(compiled); - writeFileSync(generatedPath,generatedBytes); - const inputs=["Policy/Directory.Build.props","Policy/StockSelection.fs","Policy/StockSelection.fsproj","Policy/StockSelection.packages.lock.json","Policy/build-stock-selection.mjs","package.json","package-lock.json"].map(path=>{const absolute=join(proofRoot,path);return{path:`tests/Broker.NativeProof/${path}`,sha256:hash(read(absolute))}}); - const descriptor={schema:"fsbar.stock-selection-policy-artifact/v1",compiler:{tool:"fable",version:fableVersion},runtime:{package:"@fable-org/fable-library-js",version:"2.8.0"},inputs,output:{path:relativeToRepo(generatedPath),sha256:hash(generatedBytes)}}; - writeFileSync(join(outputRoot,"stock-selection-policy.json"),`${JSON.stringify(descriptor,null,2)}\n`); - process.stdout.write(`stock-selection-policy-generated ${descriptor.output.sha256}\n`); -}finally{ - rmSync(temporary,{recursive:true,force:true}); +const ownPolicyRoot=dirname(fileURLToPath(import.meta.url)); +const hash=bytes=>createHash("sha256").update(bytes).digest("hex"); +const fail=message=>{throw new Error(message)}; +const need=(value,message)=>{if(!value)fail(message)}; +const read=path=>readFileSync(path); +const safeRelative=path=>!isAbsolute(path)&&path!==""&&!path.split(/[\\/]/).includes(".."); +const environmentKeys=["PATH","HOME","LANG","LC_ALL","TMPDIR","DOTNET_ROOT","DOTNET_ROLL_FORWARD","DOTNET_EnableDiagnostics","DOTNET_GENERATE_ASPNET_CERTIFICATE","DOTNET_CLI_HOME","DOTNET_CLI_TELEMETRY_OPTOUT","DOTNET_SKIP_FIRST_TIME_EXPERIENCE","DOTNET_NOLOGO","DOTNET_PROCESSOR_COUNT","NUGET_PACKAGES","NUGET_HTTP_CACHE_PATH","MSBUILDDISABLENODEREUSE"]; +function regular(path){const s=lstatSync(path);need(s.isFile()&&!s.isSymbolicLink()&&s.nlink===1,"regular single-link compiler input/output required");return s} +function beneath(root,path){ + need(safeRelative(path),"closed relative member path");let parent=root;need(lstatSync(parent).isDirectory()&&!lstatSync(parent).isSymbolicLink(),"member root symlink/type");for(const part of path.split("/").slice(0,-1)){parent=join(parent,part);const s=lstatSync(parent);need(s.isDirectory()&&!s.isSymbolicLink(),"member ancestor symlink/type")}return join(root,path); +} +function rows(root){ + const out=[]; + function visit(path){for(const name of readdirSync(path).sort()){const full=join(path,name),s=lstatSync(full);need(!s.isSymbolicLink(),"temporary graph symlink");if(s.isDirectory())visit(full);else{regular(full);out.push({path:relative(root,full).replaceAll("\\","/"),bytes:s.size,sha256:hash(read(full))});need(out.length<=20000,"temporary graph leaf bound")}}} + visit(root);need(out.reduce((n,x)=>n+x.bytes,0)<=1073741824,"temporary graph byte bound");return out; +} +function freshCapture(path){ + need(isAbsolute(path),"capture-dir must be absolute");path=resolve(path);need(!existsSync(path),"capture-dir must be fresh");let parent=dirname(path); + while(true){const s=lstatSync(parent);need(s.isDirectory()&&!s.isSymbolicLink(),"capture ancestor symlink/type");if(parent===dirname(parent))break;parent=dirname(parent)} + mkdirSync(path,{mode:0o700});return path; +} +export function evaluatedInputs(raw,cwd){ + // Fable5.18 Main.fs prints metadata followed by OtherOptions and SourceFiles. + // Preserve the raw log as authority; parsed receipts do not invent pre-compile observations. + const text=raw.toString("utf8"),start=text.indexOf("F# PROJECT: ");need(start>=0&&text.indexOf("F# PROJECT: ",start+1)<0,"one verbose compiler project graph required"); + const block=text.slice(start),lines=block.split(/\r?\n/);need(lines.length>=6&&lines[1].startsWith("FABLE LIBRARY: ")&&lines[2].startsWith("TARGET FRAMEWORK: ")&&lines[3].startsWith("OUTPUT TYPE: ")&&lines[4]==="","typed Fable verbose graph header"); + const entries=[];for(let i=5;i!x.startsWith("-")&&/\.fsi?$/.test(x));const references=entries.filter(x=>x.startsWith("-r:")).map(x=>x.slice(3));need(sources.length>0&&references.length>0,"nonempty actual source/reference graph"); + need(entries.every(x=>x.startsWith("-")||/\.fsi?$/.test(x)),"untyped verbose compiler graph row"); + const pin=(path,role)=>{const absolute=resolve(cwd,path);const s=regular(absolute);return{role,compilerPath:path,absolutePath:absolute,bytes:s.size,sha256:hash(read(absolute)),observation:"after compiler; parent must join pre/post frozen input census"}}; + return{project:lines[0].slice(12),fableLibrary:lines[1].slice(15),targetFramework:lines[2].slice(18),outputType:lines[3].slice(13),options:entries.filter(x=>x.startsWith("-")),sources:sources.map(x=>pin(x,"source")),references:references.map(x=>pin(x,"reference")),rawSHA256:hash(raw),headerCharacterOffset:start}; +} +export function buildStockSelection({captureDir=null,nugetConfig=null,policyRoot=ownPolicyRoot,environment=process.env}={},run=spawnSync){ + const proofRoot=dirname(policyRoot),repoRoot=resolve(proofRoot,"../.."),outputRoot=join(policyRoot,"generated"),capture=captureDir?freshCapture(captureDir):null; + const temporary= capture?join(capture,"fable-output"):mkdtempSync(join(tmpdir(),"stock-selection-fable-")); + if(capture)mkdirSync(temporary,{mode:0o700}); + const save=(name,value)=>{if(capture)writeFileSync(join(capture,name),Buffer.isBuffer(value)?value:`${JSON.stringify(value,null,2)}\n`,{mode:0o600,flag:"wx"})}; + const childEnv=capture?Object.fromEntries(environmentKeys.filter(k=>environment[k]!==undefined).map(k=>[k,environment[k]])):environment; + if(capture){mkdirSync(join(capture,"tmp"),{mode:0o700});childEnv.TMPDIR=join(capture,"tmp")} + const descriptorPath=join(outputRoot,"stock-selection-policy.json"),originalDescriptor=existsSync(descriptorPath)?{sha256:hash(read(descriptorPath)),bytes:read(descriptorPath).length}:null; + const phases=[];let outcome={schema:"fsbar.stock-selection-build-capture/v1",state:"FAILED",captureComplete:false,publishedNewDescriptor:false,originalDescriptor,temporaryRetained:Boolean(capture)},failure=null; + function invoke(name,argv){ + const started=new Date().toISOString(),result=run("dotnet",argv,{cwd:repoRoot,env:childEnv,maxBuffer:1048576,timeout:120000,encoding:null}); + const stdout=Buffer.from(result.stdout??""),stderr=Buffer.from(result.stderr??"");save(`${name}.stdout.raw`,stdout);save(`${name}.stderr.raw`,stderr); + const receipt={name,executable:"dotnet",argv,cwd:repoRoot,environment:childEnv,startedUTC:started,finishedUTC:new Date().toISOString(),actualChildPID:result.pid??null,status:result.status,signal:result.signal??null,error:result.error?{code:result.error.code??null,message:result.error.message}:null,stdout:{bytes:stdout.length,sha256:hash(stdout)},stderr:{bytes:stderr.length,sha256:hash(stderr)}};phases.push(receipt);save(`${name}.json`,receipt); + need(!result.error&&result.status===0&&!result.signal,`${name} failed; raw streams retained in capture-dir`);need(stdout.length<=1048576&&stderr.length<=1048576,"child output bound");return{stdout,stderr}; + } + try{ + const configPin=()=>{if(!nugetConfig)return null;need(isAbsolute(nugetConfig),"nuget-config must be absolute");const absolute=resolve(nugetConfig);let parent=dirname(absolute);while(true){const st=lstatSync(parent);need(st.isDirectory()&&!st.isSymbolicLink(),"NuGet config ancestor symlink/type");if(parent===dirname(parent))break;parent=dirname(parent)}const st=regular(absolute);return{path:absolute,bytes:st.size,sha256:hash(read(absolute))}};const configBefore=configPin();if(configBefore)save("nuget-config-before.json",configBefore); + const tools=JSON.parse(read(join(repoRoot,".config/dotnet-tools.json"))),version=tools.tools.fable.version;need(version==="5.18.0",`expected pinned Fable 5.18.0, found ${version}`); + const inputPaths=["Policy/Directory.Build.props","Policy/StockSelection.fs","Policy/StockSelection.fsproj","Policy/StockSelection.packages.lock.json","Policy/build-stock-selection.mjs","package.json","package-lock.json"]; + const authored=()=>inputPaths.map(path=>{const absolute=join(proofRoot,path);regular(absolute);return{path:`tests/Broker.NativeProof/${path}`,sha256:hash(read(absolute))}});const before=authored();save("authored-before.json",before); + invoke("restore",["restore",join(policyRoot,"StockSelection.fsproj"),"--locked-mode",...(configBefore?["--configfile",configBefore.path]:[])]); + const result=invoke("fable",["fable",join(policyRoot,"StockSelection.fsproj"),"--outDir",temporary,"--noCache","--noRestore",...(capture?["--verbose"]:[])]); + if(capture)save("evaluated-inputs.json",evaluatedInputs(result.stdout,repoRoot)); + const compiledPath=join(temporary,"StockSelection.js"),libraryRoot=join(proofRoot,"node_modules/@fable-org/fable-library-js");regular(compiledPath);const original=read(compiledPath);let compiled=original.toString("utf8"); + const imports=[...compiled.matchAll(/\.\/fable_modules\/fable-library-js\.5\.18\.0\/([^"']+)/g)].map(match=>match[1]);need(imports.length>0,"generated policy did not declare its Fable runtime imports");const importedModules=[]; + for(const imported of new Set(imports)){need(safeRelative(imported),"runtime import traversal/absolute path");const copiedPath=beneath(temporary,`fable_modules/fable-library-js.5.18.0/${imported}`),installedPath=beneath(libraryRoot,imported);regular(copiedPath);regular(installedPath);const copied=read(copiedPath),installed=read(installedPath);need(copied.equals(installed),`Fable runtime import differs from pinned npm package: ${imported}`);importedModules.push({path:imported,temporaryPath:copiedPath,installedPath,bytes:copied.length,sha256:hash(copied)})} + const configAfter=configPin();need(JSON.stringify(configBefore)===JSON.stringify(configAfter),"explicit NuGet config drift");if(configAfter)save("nuget-config-after.json",configAfter); + const inputs=authored();need(JSON.stringify(inputs)===JSON.stringify(before),"authored compiler input drift");save("authored-after.json",inputs);if(capture)save("temporary-graph.json",rows(temporary)); + compiled=`${compiled.replaceAll("./fable_modules/fable-library-js.5.18.0/","@fable-org/fable-library-js/").trimEnd()}\n`;const generatedBytes=Buffer.from(compiled),generatedPath=join(outputRoot,"StockSelection.js"); + save("rewrite-correspondence.json",{schema:"fsbar.stock-selection-import-rewrite/v1",original:{path:compiledPath,sha256:hash(original)},rewritten:{path:generatedPath,sha256:hash(generatedBytes)},transform:"literal Fable library prefix replacement, trimEnd, one final LF",imports,importedModules}); + if(existsSync(outputRoot))need(lstatSync(outputRoot).isDirectory()&&!lstatSync(outputRoot).isSymbolicLink(),"generated output directory type/symlink");else mkdirSync(outputRoot,{recursive:true});for(const path of [generatedPath,descriptorPath])if(existsSync(path))regular(path); + const descriptor={schema:"fsbar.stock-selection-policy-artifact/v1",compiler:{tool:"fable",version},runtime:{package:"@fable-org/fable-library-js",version:"2.8.0"},inputs,output:{path:relative(repoRoot,generatedPath).replaceAll("\\","/"),sha256:hash(generatedBytes)}}; + writeFileSync(generatedPath,generatedBytes);writeFileSync(descriptorPath,`${JSON.stringify(descriptor,null,2)}\n`); + outcome={...outcome,state:"COMPLETED",captureComplete:Boolean(capture),publishedNewDescriptor:true,descriptorSHA256:hash(read(descriptorPath)),output:descriptor.output};return descriptor; + }catch(error){failure=error;throw error} + finally{ + if(capture){try{save("retained-graph.json",rows(temporary))}catch(error){outcome={...outcome,state:"FAILED",captureComplete:false,graphCustodyFailure:error.message};if(!failure)throw error}finally{save("result.json",{...outcome,failure:failure?.message??null,phases:phases.map(x=>x.name),qualificationBoundary:"Source correctness only until original compiler/tools/packages/input census and actual parent admission are independently accepted"})}} + else rmSync(temporary,{recursive:true,force:true}); + } +} +export function cli(args){ + const options={};need(args.length%2===0,"usage: build-stock-selection.mjs [--capture-dir ] [--nuget-config ]"); + for(let i=0;i{ + calls.push({executable,argv,options});if(argv[0]==='restore')return{status:0,stdout:Buffer.from('restore\r\n'),stderr:Buffer.from([0,1,2])};const output=argv[argv.indexOf('--outDir')+1];mkdirSync(join(output,'fable_modules/fable-library-js.5.18.0'),{recursive:true});writeFileSync(join(output,'fable_modules/fable-library-js.5.18.0/Array.js'),'runtime');writeFileSync(join(output,'StockSelection.js'),'import { x } from "./fable_modules/fable-library-js.5.18.0/Array.js";\n'); + return{status:0,stdout:Buffer.from(`F# PROJECT: tests/Broker.NativeProof/Policy/StockSelection.fsproj\nFABLE LIBRARY: ${output}/fable_modules\nTARGET FRAMEWORK: net10.0\nOUTPUT TYPE: Library\n\n -r:${reference}\n ${policy}/StockSelection.fs\n\n`),stderr:Buffer.from('compiler-warning\n')}; + }; + return{root,repo,proof,policy,capture,calls,run,reference}; +} +function using(body){const f=fixture();try{body(f)}finally{rmSync(f.root,{recursive:true,force:true})}} +const read=(f,n)=>JSON.parse(readFileSync(join(f.capture,n),'utf8')); +test('real production capture retains raw streams, argv/env, evaluated graph and rewrite',()=>using(f=>{ + const descriptor=buildStockSelection({policyRoot:f.policy,captureDir:f.capture,environment:{PATH:'/mock/no-launch',HOME:'/unchanged',TOKEN:'secret'}},f.run);assert.equal(f.calls.length,2);assert.ok(f.calls[1].argv.includes('--verbose'));assert.equal(f.calls[1].options.env.HOME,'/unchanged');assert.equal(f.calls[1].options.env.TOKEN,undefined);assert.equal(f.calls[1].options.env.TMPDIR,join(f.capture,'tmp')); + assert.deepEqual(readFileSync(join(f.capture,'restore.stderr.raw')),Buffer.from([0,1,2]));assert.equal(read(f,'fable.json').cwd,f.repo);assert.equal(read(f,'evaluated-inputs.json').sources.length,1);assert.equal(read(f,'evaluated-inputs.json').references.length,1);assert.equal(read(f,'result.json').captureComplete,true);assert.ok(existsSync(join(f.capture,'fable-output/StockSelection.js')));assert.equal(read(f,'rewrite-correspondence.json').rewritten.sha256,descriptor.output.sha256); +})); +test('restore error retains exact raw failure and no new descriptor',()=>using(f=>{const mock=()=>({status:1,stdout:Buffer.from('out'),stderr:Buffer.from('fail')});assert.throws(()=>buildStockSelection({policyRoot:f.policy,captureDir:f.capture},mock),/restore failed/);assert.equal(read(f,'result.json').captureComplete,false);assert.equal(read(f,'result.json').publishedNewDescriptor,false);assert.equal(readFileSync(join(f.capture,'restore.stderr.raw'),'utf8'),'fail');assert.equal(existsSync(join(f.policy,'generated/stock-selection-policy.json')),false)})); +test('signal/timeout records refusal and partial compiler graph retained',()=>using(f=>{const mock=(...args)=>{const x=f.run(...args);return args[1][0]==='fable'?{...x,status:null,signal:'SIGTERM',error:{code:'ETIMEDOUT',message:'timeout'}}:x};assert.throws(()=>buildStockSelection({policyRoot:f.policy,captureDir:f.capture},mock),/fable failed/);assert.equal(read(f,'fable.json').signal,'SIGTERM');assert.equal(read(f,'fable.json').error.code,'ETIMEDOUT');assert.ok(read(f,'retained-graph.json').length>0);assert.equal(read(f,'result.json').captureComplete,false)})); +test('capture collision refuses before mock launches',()=>using(f=>{mkdirSync(f.capture);assert.throws(()=>buildStockSelection({policyRoot:f.policy,captureDir:f.capture},f.run),/fresh/);assert.equal(f.calls.length,0)})); +test('capture parent symlink refuses before mock launches',()=>using(f=>{const actual=join(f.root,'actual');mkdirSync(actual);symlinkSync(actual,join(f.root,'link'));assert.throws(()=>buildStockSelection({policyRoot:f.policy,captureDir:join(f.root,'link/capture')},f.run),/symlink/);assert.equal(f.calls.length,0)})); +test('runtime hash mismatch fails without publication',()=>using(f=>{writeFileSync(join(f.proof,'node_modules/@fable-org/fable-library-js/Array.js'),'foreign');assert.throws(()=>buildStockSelection({policyRoot:f.policy,captureDir:f.capture},f.run),/differs/);assert.equal(read(f,'result.json').publishedNewDescriptor,false)})); +test('temporary graph symlink fails with retained custody failure',()=>using(f=>{const mock=(...args)=>{const x=f.run(...args);if(args[1][0]==='fable')symlinkSync(f.reference,join(f.capture,'fable-output/foreign'));return x};assert.throws(()=>buildStockSelection({policyRoot:f.policy,captureDir:f.capture},mock),/symlink/);assert.equal(read(f,'result.json').captureComplete,false);assert.match(read(f,'result.json').graphCustodyFailure,/symlink/)})); +test('temporary graph hardlink fails',()=>using(f=>{const mock=(...args)=>{const x=f.run(...args);if(args[1][0]==='fable')linkSync(f.reference,join(f.capture,'fable-output/foreign'));return x};assert.throws(()=>buildStockSelection({policyRoot:f.policy,captureDir:f.capture},mock),/single-link/);assert.equal(read(f,'result.json').captureComplete,false)})); +test('missing actual verbose graph cannot publish descriptor',()=>using(f=>{const mock=(...args)=>({...f.run(...args),stdout:Buffer.from('no graph')});assert.throws(()=>buildStockSelection({policyRoot:f.policy,captureDir:f.capture},mock),/verbose/);assert.equal(read(f,'result.json').publishedNewDescriptor,false)})); +test('authored drift during compiler run fails',()=>using(f=>{const mock=(...args)=>{const x=f.run(...args);if(args[1][0]==='fable')writeFileSync(join(f.policy,'StockSelection.fs'),'changed');return x};assert.throws(()=>buildStockSelection({policyRoot:f.policy,captureDir:f.capture},mock),/drift/);assert.equal(read(f,'result.json').captureComplete,false)})); +test('old descriptor remains explicitly historical on compiler failure',()=>using(f=>{mkdirSync(join(f.policy,'generated'));writeFileSync(join(f.policy,'generated/stock-selection-policy.json'),'old');assert.throws(()=>buildStockSelection({policyRoot:f.policy,captureDir:f.capture},()=>({status:1,stdout:'',stderr:'error'})),/restore failed/);assert.equal(read(f,'result.json').originalDescriptor.bytes,3);assert.equal(read(f,'result.json').publishedNewDescriptor,false);assert.equal(readFileSync(join(f.policy,'generated/stock-selection-policy.json'),'utf8'),'old')})); +test('untyped graph and missing references fail',()=>{assert.throws(()=>evaluatedInputs(Buffer.from('F# PROJECT: p\nFABLE LIBRARY: f\nTARGET FRAMEWORK: t\nOUTPUT TYPE: o\n\n bad\n'),'/'),/source\/reference/)}); +test('unknown CLI options refuse before production build',()=>assert.throws(()=>cli(['--foreign']),/usage|CLI/)); + +test('explicit config is the actual nested restore argument and byte-pinned before/after',()=>using(f=>{const config=join(f.root,'stock.NuGet.Config');writeFileSync(config,'');buildStockSelection({policyRoot:f.policy,captureDir:f.capture,nugetConfig:config},f.run);assert.deepEqual(f.calls[0].argv.slice(-2),['--configfile',config]);assert.deepEqual(read(f,'nuget-config-before.json'),read(f,'nuget-config-after.json'));assert.equal(f.calls[0].options.maxBuffer,1048576)})); +test('explicit config drift refuses descriptor publication',()=>using(f=>{const config=join(f.root,'stock.NuGet.Config');writeFileSync(config,'original');const mock=(...args)=>{const x=f.run(...args);if(args[1][0]==='fable')writeFileSync(config,'changed');return x};assert.throws(()=>buildStockSelection({policyRoot:f.policy,captureDir:f.capture,nugetConfig:config},mock),/config drift/);assert.equal(read(f,'result.json').publishedNewDescriptor,false)})); +test('wrong config path/type/link refuses before nested launch',()=>using(f=>{for(const config of ['relative',join(f.root,'absent'),f.root]){assert.throws(()=>buildStockSelection({policyRoot:f.policy,nugetConfig:config},f.run));assert.equal(f.calls.length,0)}const config=join(f.root,'linked');symlinkSync(f.reference,config);assert.throws(()=>buildStockSelection({policyRoot:f.policy,nugetConfig:config},f.run),/regular/);assert.equal(f.calls.length,0)})); +test('explicit duplicate config CLI is refused',()=>assert.throws(()=>cli(['--nuget-config','/a','--nuget-config','/b']),/CLI/)); diff --git a/tests/Broker.NativeProof/Program.fs b/tests/Broker.NativeProof/Program.fs index f4f1064..469e238 100644 --- a/tests/Broker.NativeProof/Program.fs +++ b/tests/Broker.NativeProof/Program.fs @@ -231,6 +231,7 @@ module Program = try (if argv.Length > 0 && argv[0] = "--live-host" then LiveHost.run "barc-live-v1" argv[1..] elif argv.Length > 0 && argv[0] = "--tactical-live-host" then LiveHost.run "barc-live-tactical-v1" argv[1..] + elif argv = [| "--attribution-controls" |] then AttributionControls.run () elif argv.Length > 0 && argv[0] = "--stock-tactical-live-host" then LiveHost.run "barc-live-tactical-stock-v1" argv[1..] else run listenAddress) |> fun task -> task.GetAwaiter().GetResult() with ex -> diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/fixture/LuaRules/Gadgets/barc_stock_queue_fixture.lua b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/fixture/LuaRules/Gadgets/barc_stock_queue_fixture.lua new file mode 100644 index 0000000..7de1f25 --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/fixture/LuaRules/Gadgets/barc_stock_queue_fixture.lua @@ -0,0 +1,213 @@ +function gadget:GetInfo() + return { + name = "HighBar BARC Stock Queue Fixture", + desc = "Setup-only actors and bounded pre-readiness production for BARC-01.5 stock qualification", + author = "HighBar", + version = "3", + date = "2026-10-01", + license = "MIT", + layer = 1003, + enabled = true, + } +end + +-- Finite observations only. Hex strings prevent whitespace/log framing ambiguity. +local options = Spring.GetModOptions() or {} +local nonce = options.highbar_barc_prearm_nonce +local prearm = type(nonce) == "string" and #nonce >= 1 and #nonce <= 64 + and nonce:match("^[%w._-]+$") ~= nil +local function hex(value) + if type(value) ~= "string" or #value > 4096 then return nil end + if value == "" then return "-" end + return (value:gsub(".", function(c) return string.format("%02x", string.byte(c)) end)) +end +local function observe(kind, frame, fields) + if not prearm then return end + local keys = {} + for key in pairs(fields) do keys[#keys + 1] = key end + table.sort(keys) + local row = "BARC_PREARM_V1 kind=" .. kind .. " nonce=" .. nonce .. " frame=" .. tostring(frame) + for _, key in ipairs(keys) do row = row .. " " .. key .. "=" .. tostring(fields[key]) end + Spring.Echo(row) +end +local function rejected(frame, reason) observe("refused", frame, {reason=reason}) end +local paths = {"LuaRules/Gadgets/barc_stock_queue_fixture.lua", "LuaRules/Gadgets/barc_stock_queue_reader.lua"} + +if not gadgetHandler:IsSyncedCode() then + local observed = false + function gadget:GameFrame(frame) + if observed or not prearm or frame < 1 then return end + observed = true + -- Unsynced only: synced GetAIInfo returns deliberately substituted identities. + local teams = Spring.GetTeamList() + if type(teams) ~= "table" or #teams > 256 then return rejected(frame, "team_inventory") end + for _, team in ipairs(teams) do + local id, name, host, short, version, aiOptions = Spring.GetAIInfo(team) + if id ~= nil then + if type(aiOptions) ~= "table" or short == "UNKNOWN" or short == "SYNCED_NOSHORTNAME" then + return rejected(frame, "ai_identity") + end + local keys, encoded = {}, {} + for key in pairs(aiOptions) do keys[#keys + 1] = key end + if #keys > 32 then return rejected(frame, "ai_options_bound") end + table.sort(keys) + for _, key in ipairs(keys) do + local k, value = hex(key), hex(aiOptions[key]) + if not k or not value then return rejected(frame, "ai_options") end + encoded[#encoded + 1] = k .. ":" .. value + end + if not hex(name) or not hex(short) or not hex(version) then return rejected(frame, "ai_strings") end + observe("ai", frame, {team=team, id=id, name=hex(name), host=host, + short=hex(short), version=hex(version), options=hex(table.concat(encoded, ","))}) + end + end + for _, key in ipairs({"Headless", "LogFlush", "LogFlushLevel"}) do + local value = Spring.GetConfigInt(key) + if type(value) ~= "number" or value % 1 ~= 0 then return rejected(frame, "effective_config") end + observe("config", frame, {key=hex(key), type="int", value=hex(tostring(value))}) + end + for _, path in ipairs(paths) do + local archive = VFS.GetArchiveContainingFile(path, VFS.ZIP) + local absolute = VFS.GetFileAbsolutePath(path, VFS.ZIP) + if archive == nil and absolute == nil then return rejected(frame, "vfs_lookup") end + observe("vfs", frame, {path=hex(path), archive=hex(archive or ""), absolute=hex(absolute or "")}) + end + end + return +end + +local OPT_ENABLE = "highbar_barc_stock_fixture" +local OPT_TEAM = "highbar_barc_stock_team" +local OPT_ENEMY_TEAM = "highbar_barc_stock_enemy_team" +local PREFIX = "highbar_barc_stock_fixture" +local OLD_PRODUCTION_COUNT = 3 +local done = false +local phase = "actors" +local selected = nil + +local function enabled(value) + return value == true or value == 1 or value == "1" +end + +local function fail(reason) + done = true + rejected(Spring.GetGameFrame(), reason) + Spring.Echo(string.format("%s kind=setup status=failed reason=%s", PREFIX, reason)) +end + +local function liveTeam(teamID) + if type(teamID) ~= "number" or teamID < 0 or teamID % 1 ~= 0 then return false end + local _, _, isDead = Spring.GetTeamInfo(teamID, false) + return isDead == false +end + +local function create(name, x, z, facing, teamID) + local def = UnitDefNames[name] + if not def or type(def.id) ~= "number" then return nil, "missing_definition_" .. name end + local y = Spring.GetGroundHeight(x, z) + local unitID = Spring.CreateUnit(name, x, y, z, facing, teamID) + if type(unitID) ~= "number" then return nil, "create_failed_" .. name end + return unitID, nil +end + +local function setup(frame) + local options = Spring.GetModOptions() or {} + if not enabled(options[OPT_ENABLE]) then return end + local teamID = tonumber(options[OPT_TEAM]) + local enemyTeamID = tonumber(options[OPT_ENEMY_TEAM]) + if not liveTeam(teamID) or not liveTeam(enemyTeamID) or teamID == enemyTeamID then + return fail("invalid_distinct_live_teams") + end + + local factory, factoryError = create("armvp", 1024, 1024, "south", teamID) + if not factory then return fail(factoryError) end + local builder, builderError = create("armck", 1248, 1024, "south", teamID) + if not builder then return fail(builderError) end + local enemy, enemyError = create("corak", 1536, 1024, "north", enemyTeamID) + if not enemy then return fail(enemyError) end + if factory == builder or factory == enemy or builder == enemy then + return fail("actor_identity_collision") + end + + Spring.SetTeamResource(teamID, "ms", 10000) + Spring.SetTeamResource(teamID, "es", 10000) + Spring.SetTeamResource(teamID, "m", 8000) + Spring.SetTeamResource(teamID, "e", 8000) + Spring.SetUnitAlwaysVisible(enemy, true) + + local oldDefinition = UnitDefNames.armfav + local distinctDefinition = UnitDefNames.armflash + if not oldDefinition or not distinctDefinition or oldDefinition.id == distinctDefinition.id then + return fail("production_definitions_missing_or_equal") + end + if not prearm then return fail("prearm_nonce_required") end + for _, path in ipairs(paths) do + local bytes = VFS.LoadFile(path, VFS.ZIP) + if type(bytes) ~= "string" or #bytes == 0 or #bytes > 131072 then return fail("vfs_content_bound") end + local hash = VFS.CalculateHash(bytes, 1) -- actual engine SHA-512, never SHA-256 + if type(hash) ~= "string" or #hash ~= 128 or not hash:match("^[0-9a-f]+$") then return fail("vfs_hash") end + observe("content", frame, {path=hex(path), bytes=#bytes, sha512=hash}) + end + if not hex(Game.mapName) or not hex(Game.gameName) or type(Game.mapChecksum) ~= "string" + or type(Game.modChecksum) ~= "string" then return fail("archive_identity") end + observe("archives", frame, {map=hex(Game.mapName), game=hex(Game.gameName), + map_sha512=Game.mapChecksum, game_sha512=Game.modChecksum}) + selected = {factory=factory, builder=builder, enemy=enemy, team=teamID, enemyTeam=enemyTeamID, + seed=oldDefinition.id, product=distinctDefinition.id} + GG.BARCPrearmEmpty = {nonce=nonce, factory=factory, team=teamID, observedFrame=nil} + observe("actors", frame, {team=teamID, enemy=enemyTeamID, factory=factory, builder=builder, + target=enemy, seed=oldDefinition.id, product=distinctDefinition.id}) + phase = "await-empty" +end + +local function enqueue(frame) + local factory, oldDefinition = selected.factory, {id=selected.seed} + -- Set passive before issuing any order; a refusal can never trigger a retry. + done = true + phase = "passive" + for index = 1, OLD_PRODUCTION_COUNT do + local optionsBits = 0 + if Spring.GiveOrderToUnit(factory, -oldDefinition.id, {}, optionsBits) == false then + return fail("old_production_order_refused") + end + observe("order", frame, {factory=factory, ordinal=index, definition=oldDefinition.id, count=1, options=optionsBits}) + end + local queue = Spring.GetFactoryCommands(factory, OLD_PRODUCTION_COUNT + 1) or {} + if #queue ~= OLD_PRODUCTION_COUNT then return fail("old_production_count_changed") end + for index = 1, OLD_PRODUCTION_COUNT do + if not queue[index] or queue[index].id ~= -oldDefinition.id then + return fail("old_production_order_changed") + end + end + + for index = 1, OLD_PRODUCTION_COUNT do + local command = queue[index] + if type(command.tag) ~= "number" or command.tag < 0 or command.tag % 1 ~= 0 + or type(command.options) ~= "table" or command.options.coded ~= 0 then return fail("queue_fields") end + observe("queue", frame, {factory=factory, ordinal=index, definition=oldDefinition.id, + tag=command.tag, options=command.options.coded}) + end + observe("complete", frame, {factory=factory, count=OLD_PRODUCTION_COUNT, + empty_frame=GG.BARCPrearmEmpty.observedFrame}) + local teamID, enemyTeamID, builder, enemy = selected.team, selected.enemyTeam, selected.builder, selected.enemy + local distinctDefinition = {id=selected.product} + + done = true + Spring.Echo(string.format( + "%s kind=ready frame=%d team=%d enemy_team=%d factory=%d builder=%d visible_enemy=%d old_definition=%d distinct_product=%d old_production_count=%d post_ready_mutation=zero", + PREFIX, frame, teamID, enemyTeamID, factory, builder, enemy, + oldDefinition.id, distinctDefinition.id, OLD_PRODUCTION_COUNT)) +end + +function gadget:GameFrame(frame) + if done or frame < 1 then return end + if phase == "actors" then return setup(frame) end + if phase == "await-empty" then + local observed = GG.BARCPrearmEmpty + if observed and observed.nonce == nonce and observed.factory == selected.factory + and type(observed.observedFrame) == "number" and frame > observed.observedFrame then + observe("empty", observed.observedFrame, {factory=selected.factory, observed_frame=observed.observedFrame}) + enqueue(frame) + end + end +end diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/fixture/LuaRules/Gadgets/barc_stock_queue_reader.lua b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/fixture/LuaRules/Gadgets/barc_stock_queue_reader.lua new file mode 100644 index 0000000..ec2359f --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/fixture/LuaRules/Gadgets/barc_stock_queue_reader.lua @@ -0,0 +1,323 @@ +function gadget:GetInfo() + return { + name = "BARC Stock Queue Reader", + desc = "Bounded team-scoped stock production and rally observation", + author = "FS-GG", + version = "1", + date = "2026-10-01", + license = "MIT", + layer = 0, + enabled = true, + } +end + +-- Stock AICallback::CallRules selects LuaRules' synced handle. The generic +-- RecvSkirmishAIMessage comment does not change that explicit dispatch route. +if not gadgetHandler:IsSyncedCode() then + return +end + +local REQUEST_ROUTE = "BARC_QUEUE_REQUEST/" +local REQUEST_MAGIC = "BARC_QUEUE_REQUEST/1" +local RESPONSE_MAGIC = "BARC_QUEUE_RESPONSE/1" +local BRIDGE = "barc-stock-queue-reader-v1" +local MAX_BYTES = 8192 +local MAX_LINE_BYTES = 512 +local MAX_ENTRIES = 64 +local PROBE_ENTRIES = 65 +local MAX_PARAMS = 16 +local MAX_TOTAL_PARAMS = 256 +local HEX = "0123456789abcdef" + +local function byteBand(a, b) + local result, place = 0, 1 + for _ = 1, 8 do + if a % 2 == 1 and b % 2 == 1 then result = result + place end + a, b, place = math.floor(a / 2), math.floor(b / 2), place * 2 + end + return result +end + +local function byteBxor(a, b) + local result, place = 0, 1 + for _ = 1, 8 do + if a % 2 ~= b % 2 then result = result + place end + a, b, place = math.floor(a / 2), math.floor(b / 2), place * 2 + end + return result +end + +local function wordFromHex(value) + local word = {} + for index = 1, 8, 2 do word[#word + 1] = assert(tonumber(value:sub(index, index + 1), 16)) end + return word +end + +local function wordXor(a, b, c) + local result = {} + for index = 1, 4 do + local value = byteBxor(a[index], b[index]) + result[index] = c and byteBxor(value, c[index]) or value + end + return result +end + +local function wordAnd(a, b) + return {byteBand(a[1], b[1]), byteBand(a[2], b[2]), byteBand(a[3], b[3]), byteBand(a[4], b[4])} +end + +local function wordNot(a) + return {255 - a[1], 255 - a[2], 255 - a[3], 255 - a[4]} +end + +local function wordShiftRight(a, count) + local result, whole, bits = {}, math.floor(count / 8), count % 8 + for index = 1, 4 do + local source = index - whole + local value = source >= 1 and math.floor(a[source] / (2 ^ bits)) or 0 + if bits > 0 and source > 1 then value = value + (a[source - 1] % (2 ^ bits)) * (2 ^ (8 - bits)) end + result[index] = value + end + return result +end + +local function wordShiftLeft(a, count) + local result, whole, bits = {}, math.floor(count / 8), count % 8 + for index = 1, 4 do + local source = index + whole + local value = source <= 4 and (a[source] * (2 ^ bits)) % 256 or 0 + if bits > 0 and source < 4 then value = value + math.floor(a[source + 1] / (2 ^ (8 - bits))) end + result[index] = value + end + return result +end + +local function wordRotateRight(a, count) + return wordXor(wordShiftRight(a, count), wordShiftLeft(a, 32 - count)) +end + +local function wordAdd(...) + local values, result, carry = {...}, {}, 0 + for index = 4, 1, -1 do + local total = carry + for valueIndex = 1, #values do total = total + values[valueIndex][index] end + result[index], carry = total % 256, math.floor(total / 256) + end + return result +end + +local function wordHex(value) + local chars = {} + for index = 1, 4 do + local byte = value[index] + chars[#chars + 1] = HEX:sub(math.floor(byte / 16) + 1, math.floor(byte / 16) + 1) + chars[#chars + 1] = HEX:sub((byte % 16) + 1, (byte % 16) + 1) + end + return table.concat(chars) +end + +local SHA_K_HEX = { + "428a2f98","71374491","b5c0fbcf","e9b5dba5","3956c25b","59f111f1","923f82a4","ab1c5ed5", + "d807aa98","12835b01","243185be","550c7dc3","72be5d74","80deb1fe","9bdc06a7","c19bf174", + "e49b69c1","efbe4786","0fc19dc6","240ca1cc","2de92c6f","4a7484aa","5cb0a9dc","76f988da", + "983e5152","a831c66d","b00327c8","bf597fc7","c6e00bf3","d5a79147","06ca6351","14292967", + "27b70a85","2e1b2138","4d2c6dfc","53380d13","650a7354","766a0abb","81c2c92e","92722c85", + "a2bfe8a1","a81a664b","c24b8b70","c76c51a3","d192e819","d6990624","f40e3585","106aa070", + "19a4c116","1e376c08","2748774c","34b0bcb5","391c0cb3","4ed8aa4a","5b9cca4f","682e6ff3", + "748f82ee","78a5636f","84c87814","8cc70208","90befffa","a4506ceb","bef9a3f7","c67178f2", +} +local SHA_K = {} +for index = 1, #SHA_K_HEX do SHA_K[index] = wordFromHex(SHA_K_HEX[index]) end + +local function sha256(message) + local bytes = {} + for index = 1, #message do bytes[index] = message:byte(index) end + local bitLength = #bytes * 8 + bytes[#bytes + 1] = 0x80 + while (#bytes % 64) ~= 56 do bytes[#bytes + 1] = 0 end + local lengthBytes = {} + for index = 8, 1, -1 do + lengthBytes[index] = bitLength % 256 + bitLength = math.floor(bitLength / 256) + end + for index = 1, 8 do bytes[#bytes + 1] = lengthBytes[index] end + + local h = {} + for index, value in ipairs({"6a09e667","bb67ae85","3c6ef372","a54ff53a","510e527f","9b05688c","1f83d9ab","5be0cd19"}) do + h[index] = wordFromHex(value) + end + for offset = 1, #bytes, 64 do + local w = {} + for index = 0, 15 do + local base = offset + index * 4 + w[index] = {bytes[base], bytes[base + 1], bytes[base + 2], bytes[base + 3]} + end + for index = 16, 63 do + local x, y = w[index - 15], w[index - 2] + local s0 = wordXor(wordRotateRight(x, 7), wordRotateRight(x, 18), wordShiftRight(x, 3)) + local s1 = wordXor(wordRotateRight(y, 17), wordRotateRight(y, 19), wordShiftRight(y, 10)) + w[index] = wordAdd(w[index - 16], s0, w[index - 7], s1) + end + local a,b,c,d,e,f,g,hh = h[1],h[2],h[3],h[4],h[5],h[6],h[7],h[8] + for index = 0, 63 do + local s1 = wordXor(wordRotateRight(e, 6), wordRotateRight(e, 11), wordRotateRight(e, 25)) + local choice = wordXor(wordAnd(e, f), wordAnd(wordNot(e), g)) + local t1 = wordAdd(hh, s1, choice, SHA_K[index + 1], w[index]) + local s0 = wordXor(wordRotateRight(a, 2), wordRotateRight(a, 13), wordRotateRight(a, 22)) + local majority = wordXor(wordAnd(a, b), wordAnd(a, c), wordAnd(b, c)) + local t2 = wordAdd(s0, majority) + hh,g,f,e,d,c,b,a = g,f,e,wordAdd(d, t1),c,b,a,wordAdd(t1, t2) + end + h[1],h[2],h[3],h[4] = wordAdd(h[1],a),wordAdd(h[2],b),wordAdd(h[3],c),wordAdd(h[4],d) + h[5],h[6],h[7],h[8] = wordAdd(h[5],e),wordAdd(h[6],f),wordAdd(h[7],g),wordAdd(h[8],hh) + end + local parts = {} + for index = 1, 8 do parts[index] = wordHex(h[index]) end + return table.concat(parts) +end + +local function packedHex(bytes, littleEndian) + if type(bytes) ~= "string" or #bytes ~= 4 then return nil end + local chars = {} + if littleEndian then + for index = 4, 1, -1 do chars[#chars + 1] = wordHex({0, 0, 0, bytes:byte(index)}):sub(7, 8) end + else + for index = 1, 4 do chars[#chars + 1] = wordHex({0, 0, 0, bytes:byte(index)}):sub(7, 8) end + end + return table.concat(chars) +end + +local onePacked = VFS.PackF32(1.0) +local oneNativeHex = packedHex(onePacked, false) +local PACK_LITTLE_ENDIAN = oneNativeHex == "0000803f" +local PACK_ENDIAN_VALID = PACK_LITTLE_ENDIAN or oneNativeHex == "3f800000" +local PACK_ZERO_VALID = PACK_ENDIAN_VALID + and packedHex(VFS.PackF32(0.0), PACK_LITTLE_ENDIAN) == "00000000" + and packedHex(VFS.PackF32(-1 / math.huge), PACK_LITTLE_ENDIAN) == "80000000" + +local function float32Hex(value) + if type(value) ~= "number" or value ~= value or value == math.huge or value == -math.huge then + return nil, "nonfinite-float" + end + if not PACK_ENDIAN_VALID then return nil, "float-pack-unsupported" end + local encoded = packedHex(VFS.PackF32(value), PACK_LITTLE_ENDIAN) + if not encoded then return nil, "float-pack-unsupported" end + if value == 0 and not PACK_ZERO_VALID then return nil, "negative-zero-unsupported" end + return encoded +end + +local function integerInRange(value, minimum, maximum) + return type(value) == "number" and value == math.floor(value) and value >= minimum and value <= maximum +end + +local function splitLines(message) + if message:sub(-1) ~= "\n" then return nil end + local lines, position = {}, 1 + while position <= #message do + local newline = message:find("\n", position, true) + if not newline then return nil end + if newline - position + 1 > MAX_LINE_BYTES then return nil end + lines[#lines + 1] = message:sub(position, newline - 1) + position = newline + 1 + end + return lines +end + +local function parseRequest(message) + if type(message) ~= "string" or #message > MAX_BYTES or message:find("\0", 1, true) then return nil end + for index = 1, #message do if message:byte(index) > 127 then return nil end end + local lines = splitLines(message) + if not lines or #lines ~= 6 or lines[6] ~= "end" then return nil end + local length = lines[2]:match("^length=(%d%d%d%d%d%d%d%d)$") + local bridge = lines[3]:match("^bridge=([%w%-]+)$") + local domain = lines[4]:match("^domain=([%l]+)$") + local unitText = lines[5]:match("^unit=(%-?%d+)$") + local unit = unitText and tonumber(unitText) or nil + if not length or tonumber(length) ~= #message or not bridge or not domain or not integerInRange(unit, 0, 2147483647) then return nil end + if domain ~= "production" and domain ~= "rally" then return nil end + local reason + if lines[1] ~= REQUEST_MAGIC then reason = lines[1]:sub(1, #REQUEST_ROUTE) == REQUEST_ROUTE and "wrong-version" or "malformed" + elseif bridge ~= BRIDGE then reason = "missing-bridge" end + return { bridge=bridge, domain=domain, unit=unit, reason=reason } +end + +local function response(requestHash, domain, unit, status, reason, rows) + local lines = { RESPONSE_MAGIC, "length=00000000", "bridge=" .. BRIDGE, "request-sha256=" .. requestHash, + "status=" .. status, "domain=" .. domain, "unit=" .. tostring(unit) } + if reason then lines[#lines + 1] = "reason=" .. reason end + lines[#lines + 1] = "count=" .. tostring(#rows) + for _, row in ipairs(rows) do lines[#lines + 1] = "row=" .. row end + lines[#lines + 1] = "end" + local document = table.concat(lines, "\n") .. "\n" + document = document:gsub("length=00000000", string.format("length=%08d", #document), 1) + if #document > MAX_BYTES then return nil end + for _, line in ipairs(lines) do if #line + 1 > MAX_LINE_BYTES then return nil end end + return document +end + +local function unavailable(message, request, reason) + return response(sha256(message), request.domain, request.unit, "unavailable", reason, {}) +end + +local function encodeRows(domain, commands) + if type(commands) ~= "table" then return nil, "malformed" end + if #commands > MAX_ENTRIES then return nil, "overflow" end + local rows, totalParams = {}, 0 + for index = 1, #commands do + local command = commands[index] + local options = type(command) == "table" and command.options or nil + local params = type(command) == "table" and command.params or nil + if type(options) ~= "table" or type(params) ~= "table" + or not integerInRange(command.id, -2147483648, 2147483647) + or not integerInRange(options.coded, 0, 65535) + or not integerInRange(command.tag, -2147483648, 2147483647) then return nil, "malformed" end + if #params > MAX_PARAMS then return nil, "overflow" end + totalParams = totalParams + #params + if totalParams > MAX_TOTAL_PARAMS then return nil, "overflow" end + local encoded = {} + for paramIndex = 1, #params do + local bits, reason = float32Hex(params[paramIndex]) + if not bits then return nil, reason end + encoded[paramIndex] = bits + end + local row = table.concat({domain, tostring(command.id), tostring(options.coded), tostring(command.tag), table.concat(encoded, ",")}, "|") + if #row + 5 > MAX_LINE_BYTES then return nil, "oversize" end + rows[index] = row + end + return rows +end + +function gadget:RecvSkirmishAIMessage(aiTeam, message) + if type(message) ~= "string" or message:sub(1, #REQUEST_ROUTE) ~= REQUEST_ROUTE then return nil end + local request = parseRequest(message) + if not request then return nil end + if request.reason then return unavailable(message, request, request.reason) end + if not Spring.ValidUnitID(request.unit) or Spring.GetUnitTeam(request.unit) ~= aiTeam then + return unavailable(message, request, "wrong-team") + end + local unitDefID = Spring.GetUnitDefID(request.unit) + if not unitDefID or not UnitDefs[unitDefID] or UnitDefs[unitDefID].isFactory ~= true then + return unavailable(message, request, "not-factory") + end + local commands + if request.domain == "production" then commands = Spring.GetFactoryCommands(request.unit, PROBE_ENTRIES) + else commands = Spring.GetUnitCommands(request.unit, PROBE_ENTRIES) end + local rows, reason = encodeRows(request.domain, commands) + if not rows then return unavailable(message, request, reason) end + local result = response(sha256(message), request.domain, request.unit, "ok", nil, rows) + if result then + -- Read-only optional handshake, after a successfully encoded real empty response. + -- No orders, no wire/response changes; absent/inert without the selected fixture. + local options = Spring.GetModOptions() or {} + local observed = GG.BARCPrearmEmpty + if request.domain == "production" and #rows == 0 and observed + and observed.factory == request.unit and observed.team == aiTeam + and observed.nonce == options.highbar_barc_prearm_nonce + and (options.highbar_barc_stock_fixture == "1" or options.highbar_barc_stock_fixture == 1 + or options.highbar_barc_stock_fixture == true) and observed.observedFrame == nil then + observed.observedFrame = Spring.GetGameFrame() + end + return result + end + return unavailable(message, request, "oversize") +end diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/fixture/tests/fixtures/barc_stock_queue_reader.baseline.lua b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/fixture/tests/fixtures/barc_stock_queue_reader.baseline.lua new file mode 100644 index 0000000..3c86241 --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/fixture/tests/fixtures/barc_stock_queue_reader.baseline.lua @@ -0,0 +1,310 @@ +function gadget:GetInfo() + return { + name = "BARC Stock Queue Reader", + desc = "Bounded team-scoped stock production and rally observation", + author = "FS-GG", + version = "1", + date = "2026-10-01", + license = "MIT", + layer = 0, + enabled = true, + } +end + +-- Stock AICallback::CallRules selects LuaRules' synced handle. The generic +-- RecvSkirmishAIMessage comment does not change that explicit dispatch route. +if not gadgetHandler:IsSyncedCode() then + return +end + +local REQUEST_ROUTE = "BARC_QUEUE_REQUEST/" +local REQUEST_MAGIC = "BARC_QUEUE_REQUEST/1" +local RESPONSE_MAGIC = "BARC_QUEUE_RESPONSE/1" +local BRIDGE = "barc-stock-queue-reader-v1" +local MAX_BYTES = 8192 +local MAX_LINE_BYTES = 512 +local MAX_ENTRIES = 64 +local PROBE_ENTRIES = 65 +local MAX_PARAMS = 16 +local MAX_TOTAL_PARAMS = 256 +local HEX = "0123456789abcdef" + +local function byteBand(a, b) + local result, place = 0, 1 + for _ = 1, 8 do + if a % 2 == 1 and b % 2 == 1 then result = result + place end + a, b, place = math.floor(a / 2), math.floor(b / 2), place * 2 + end + return result +end + +local function byteBxor(a, b) + local result, place = 0, 1 + for _ = 1, 8 do + if a % 2 ~= b % 2 then result = result + place end + a, b, place = math.floor(a / 2), math.floor(b / 2), place * 2 + end + return result +end + +local function wordFromHex(value) + local word = {} + for index = 1, 8, 2 do word[#word + 1] = assert(tonumber(value:sub(index, index + 1), 16)) end + return word +end + +local function wordXor(a, b, c) + local result = {} + for index = 1, 4 do + local value = byteBxor(a[index], b[index]) + result[index] = c and byteBxor(value, c[index]) or value + end + return result +end + +local function wordAnd(a, b) + return {byteBand(a[1], b[1]), byteBand(a[2], b[2]), byteBand(a[3], b[3]), byteBand(a[4], b[4])} +end + +local function wordNot(a) + return {255 - a[1], 255 - a[2], 255 - a[3], 255 - a[4]} +end + +local function wordShiftRight(a, count) + local result, whole, bits = {}, math.floor(count / 8), count % 8 + for index = 1, 4 do + local source = index - whole + local value = source >= 1 and math.floor(a[source] / (2 ^ bits)) or 0 + if bits > 0 and source > 1 then value = value + (a[source - 1] % (2 ^ bits)) * (2 ^ (8 - bits)) end + result[index] = value + end + return result +end + +local function wordShiftLeft(a, count) + local result, whole, bits = {}, math.floor(count / 8), count % 8 + for index = 1, 4 do + local source = index + whole + local value = source <= 4 and (a[source] * (2 ^ bits)) % 256 or 0 + if bits > 0 and source < 4 then value = value + math.floor(a[source + 1] / (2 ^ (8 - bits))) end + result[index] = value + end + return result +end + +local function wordRotateRight(a, count) + return wordXor(wordShiftRight(a, count), wordShiftLeft(a, 32 - count)) +end + +local function wordAdd(...) + local values, result, carry = {...}, {}, 0 + for index = 4, 1, -1 do + local total = carry + for valueIndex = 1, #values do total = total + values[valueIndex][index] end + result[index], carry = total % 256, math.floor(total / 256) + end + return result +end + +local function wordHex(value) + local chars = {} + for index = 1, 4 do + local byte = value[index] + chars[#chars + 1] = HEX:sub(math.floor(byte / 16) + 1, math.floor(byte / 16) + 1) + chars[#chars + 1] = HEX:sub((byte % 16) + 1, (byte % 16) + 1) + end + return table.concat(chars) +end + +local SHA_K_HEX = { + "428a2f98","71374491","b5c0fbcf","e9b5dba5","3956c25b","59f111f1","923f82a4","ab1c5ed5", + "d807aa98","12835b01","243185be","550c7dc3","72be5d74","80deb1fe","9bdc06a7","c19bf174", + "e49b69c1","efbe4786","0fc19dc6","240ca1cc","2de92c6f","4a7484aa","5cb0a9dc","76f988da", + "983e5152","a831c66d","b00327c8","bf597fc7","c6e00bf3","d5a79147","06ca6351","14292967", + "27b70a85","2e1b2138","4d2c6dfc","53380d13","650a7354","766a0abb","81c2c92e","92722c85", + "a2bfe8a1","a81a664b","c24b8b70","c76c51a3","d192e819","d6990624","f40e3585","106aa070", + "19a4c116","1e376c08","2748774c","34b0bcb5","391c0cb3","4ed8aa4a","5b9cca4f","682e6ff3", + "748f82ee","78a5636f","84c87814","8cc70208","90befffa","a4506ceb","bef9a3f7","c67178f2", +} +local SHA_K = {} +for index = 1, #SHA_K_HEX do SHA_K[index] = wordFromHex(SHA_K_HEX[index]) end + +local function sha256(message) + local bytes = {} + for index = 1, #message do bytes[index] = message:byte(index) end + local bitLength = #bytes * 8 + bytes[#bytes + 1] = 0x80 + while (#bytes % 64) ~= 56 do bytes[#bytes + 1] = 0 end + local lengthBytes = {} + for index = 8, 1, -1 do + lengthBytes[index] = bitLength % 256 + bitLength = math.floor(bitLength / 256) + end + for index = 1, 8 do bytes[#bytes + 1] = lengthBytes[index] end + + local h = {} + for index, value in ipairs({"6a09e667","bb67ae85","3c6ef372","a54ff53a","510e527f","9b05688c","1f83d9ab","5be0cd19"}) do + h[index] = wordFromHex(value) + end + for offset = 1, #bytes, 64 do + local w = {} + for index = 0, 15 do + local base = offset + index * 4 + w[index] = {bytes[base], bytes[base + 1], bytes[base + 2], bytes[base + 3]} + end + for index = 16, 63 do + local x, y = w[index - 15], w[index - 2] + local s0 = wordXor(wordRotateRight(x, 7), wordRotateRight(x, 18), wordShiftRight(x, 3)) + local s1 = wordXor(wordRotateRight(y, 17), wordRotateRight(y, 19), wordShiftRight(y, 10)) + w[index] = wordAdd(w[index - 16], s0, w[index - 7], s1) + end + local a,b,c,d,e,f,g,hh = h[1],h[2],h[3],h[4],h[5],h[6],h[7],h[8] + for index = 0, 63 do + local s1 = wordXor(wordRotateRight(e, 6), wordRotateRight(e, 11), wordRotateRight(e, 25)) + local choice = wordXor(wordAnd(e, f), wordAnd(wordNot(e), g)) + local t1 = wordAdd(hh, s1, choice, SHA_K[index + 1], w[index]) + local s0 = wordXor(wordRotateRight(a, 2), wordRotateRight(a, 13), wordRotateRight(a, 22)) + local majority = wordXor(wordAnd(a, b), wordAnd(a, c), wordAnd(b, c)) + local t2 = wordAdd(s0, majority) + hh,g,f,e,d,c,b,a = g,f,e,wordAdd(d, t1),c,b,a,wordAdd(t1, t2) + end + h[1],h[2],h[3],h[4] = wordAdd(h[1],a),wordAdd(h[2],b),wordAdd(h[3],c),wordAdd(h[4],d) + h[5],h[6],h[7],h[8] = wordAdd(h[5],e),wordAdd(h[6],f),wordAdd(h[7],g),wordAdd(h[8],hh) + end + local parts = {} + for index = 1, 8 do parts[index] = wordHex(h[index]) end + return table.concat(parts) +end + +local function packedHex(bytes, littleEndian) + if type(bytes) ~= "string" or #bytes ~= 4 then return nil end + local chars = {} + if littleEndian then + for index = 4, 1, -1 do chars[#chars + 1] = wordHex({0, 0, 0, bytes:byte(index)}):sub(7, 8) end + else + for index = 1, 4 do chars[#chars + 1] = wordHex({0, 0, 0, bytes:byte(index)}):sub(7, 8) end + end + return table.concat(chars) +end + +local onePacked = VFS.PackF32(1.0) +local oneNativeHex = packedHex(onePacked, false) +local PACK_LITTLE_ENDIAN = oneNativeHex == "0000803f" +local PACK_ENDIAN_VALID = PACK_LITTLE_ENDIAN or oneNativeHex == "3f800000" +local PACK_ZERO_VALID = PACK_ENDIAN_VALID + and packedHex(VFS.PackF32(0.0), PACK_LITTLE_ENDIAN) == "00000000" + and packedHex(VFS.PackF32(-1 / math.huge), PACK_LITTLE_ENDIAN) == "80000000" + +local function float32Hex(value) + if type(value) ~= "number" or value ~= value or value == math.huge or value == -math.huge then + return nil, "nonfinite-float" + end + if not PACK_ENDIAN_VALID then return nil, "float-pack-unsupported" end + local encoded = packedHex(VFS.PackF32(value), PACK_LITTLE_ENDIAN) + if not encoded then return nil, "float-pack-unsupported" end + if value == 0 and not PACK_ZERO_VALID then return nil, "negative-zero-unsupported" end + return encoded +end + +local function integerInRange(value, minimum, maximum) + return type(value) == "number" and value == math.floor(value) and value >= minimum and value <= maximum +end + +local function splitLines(message) + if message:sub(-1) ~= "\n" then return nil end + local lines, position = {}, 1 + while position <= #message do + local newline = message:find("\n", position, true) + if not newline then return nil end + if newline - position + 1 > MAX_LINE_BYTES then return nil end + lines[#lines + 1] = message:sub(position, newline - 1) + position = newline + 1 + end + return lines +end + +local function parseRequest(message) + if type(message) ~= "string" or #message > MAX_BYTES or message:find("\0", 1, true) then return nil end + for index = 1, #message do if message:byte(index) > 127 then return nil end end + local lines = splitLines(message) + if not lines or #lines ~= 6 or lines[6] ~= "end" then return nil end + local length = lines[2]:match("^length=(%d%d%d%d%d%d%d%d)$") + local bridge = lines[3]:match("^bridge=([%w%-]+)$") + local domain = lines[4]:match("^domain=([%l]+)$") + local unitText = lines[5]:match("^unit=(%-?%d+)$") + local unit = unitText and tonumber(unitText) or nil + if not length or tonumber(length) ~= #message or not bridge or not domain or not integerInRange(unit, 0, 2147483647) then return nil end + if domain ~= "production" and domain ~= "rally" then return nil end + local reason + if lines[1] ~= REQUEST_MAGIC then reason = lines[1]:sub(1, #REQUEST_ROUTE) == REQUEST_ROUTE and "wrong-version" or "malformed" + elseif bridge ~= BRIDGE then reason = "missing-bridge" end + return { bridge=bridge, domain=domain, unit=unit, reason=reason } +end + +local function response(requestHash, domain, unit, status, reason, rows) + local lines = { RESPONSE_MAGIC, "length=00000000", "bridge=" .. BRIDGE, "request-sha256=" .. requestHash, + "status=" .. status, "domain=" .. domain, "unit=" .. tostring(unit) } + if reason then lines[#lines + 1] = "reason=" .. reason end + lines[#lines + 1] = "count=" .. tostring(#rows) + for _, row in ipairs(rows) do lines[#lines + 1] = "row=" .. row end + lines[#lines + 1] = "end" + local document = table.concat(lines, "\n") .. "\n" + document = document:gsub("length=00000000", string.format("length=%08d", #document), 1) + if #document > MAX_BYTES then return nil end + for _, line in ipairs(lines) do if #line + 1 > MAX_LINE_BYTES then return nil end end + return document +end + +local function unavailable(message, request, reason) + return response(sha256(message), request.domain, request.unit, "unavailable", reason, {}) +end + +local function encodeRows(domain, commands) + if type(commands) ~= "table" then return nil, "malformed" end + if #commands > MAX_ENTRIES then return nil, "overflow" end + local rows, totalParams = {}, 0 + for index = 1, #commands do + local command = commands[index] + local options = type(command) == "table" and command.options or nil + local params = type(command) == "table" and command.params or nil + if type(options) ~= "table" or type(params) ~= "table" + or not integerInRange(command.id, -2147483648, 2147483647) + or not integerInRange(options.coded, 0, 65535) + or not integerInRange(command.tag, -2147483648, 2147483647) then return nil, "malformed" end + if #params > MAX_PARAMS then return nil, "overflow" end + totalParams = totalParams + #params + if totalParams > MAX_TOTAL_PARAMS then return nil, "overflow" end + local encoded = {} + for paramIndex = 1, #params do + local bits, reason = float32Hex(params[paramIndex]) + if not bits then return nil, reason end + encoded[paramIndex] = bits + end + local row = table.concat({domain, tostring(command.id), tostring(options.coded), tostring(command.tag), table.concat(encoded, ",")}, "|") + if #row + 5 > MAX_LINE_BYTES then return nil, "oversize" end + rows[index] = row + end + return rows +end + +function gadget:RecvSkirmishAIMessage(aiTeam, message) + if type(message) ~= "string" or message:sub(1, #REQUEST_ROUTE) ~= REQUEST_ROUTE then return nil end + local request = parseRequest(message) + if not request then return nil end + if request.reason then return unavailable(message, request, request.reason) end + if not Spring.ValidUnitID(request.unit) or Spring.GetUnitTeam(request.unit) ~= aiTeam then + return unavailable(message, request, "wrong-team") + end + local unitDefID = Spring.GetUnitDefID(request.unit) + if not unitDefID or not UnitDefs[unitDefID] or UnitDefs[unitDefID].isFactory ~= true then + return unavailable(message, request, "not-factory") + end + local commands + if request.domain == "production" then commands = Spring.GetFactoryCommands(request.unit, PROBE_ENTRIES) + else commands = Spring.GetUnitCommands(request.unit, PROBE_ENTRIES) end + local rows, reason = encodeRows(request.domain, commands) + if not rows then return unavailable(message, request, reason) end + local result = response(sha256(message), request.domain, request.unit, "ok", nil, rows) + if result then return result end + return unavailable(message, request, "oversize") +end diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/fixture/tests/test_prearm_observation.py b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/fixture/tests/test_prearm_observation.py new file mode 100644 index 0000000..d8c58a7 --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/fixture/tests/test_prearm_observation.py @@ -0,0 +1,22 @@ +"""Pure controlled Lua-call semantic model; no Lua/native execution or evidence.""" +import unittest,pathlib,hashlib +ROOT=pathlib.Path(__file__).resolve().parents[1]/'LuaRules/Gadgets' +class Fixture(unittest.TestCase): + def model(self,frames,observed=None,options=0): + phase='actors';orders=[];factory=10 + for frame in frames: + if phase=='actors':phase='await-empty';continue + if phase=='await-empty'and observed is not None and frame>observed: + phase='passive' + for ordinal in range(1,4):orders.append((factory,2,1 if options==0 else 5,options,ordinal,frame)) + return orders + def test_empty_before_one_shot_plain_seed(self): + self.assertEqual(self.model(range(20)),[]);orders=self.model(range(20),observed=2);self.assertEqual(len(orders),3);self.assertTrue(all(x[2:4]==(1,0)and x[-1]==3 for x in orders));self.assertNotEqual(sum(x[2]for x in self.model(range(20),2,32)),3) + def test_actual_source_gate_and_passive_transition(self): + text=(ROOT/'barc_stock_queue_fixture.lua').read_text();self.assertIn('frame > observed.observedFrame',text);self.assertLess(text.index('phase = "passive"'),text.index('Spring.GiveOrderToUnit'));self.assertEqual(text.count('Spring.GiveOrderToUnit('),1);self.assertIn('VFS.CalculateHash(bytes, 1)',text) + def test_observer_hook_preserves_wire_source(self): + baseline=pathlib.Path(__file__).with_name('fixtures')/'barc_stock_queue_reader.baseline.lua';self.assertEqual(hashlib.sha256(baseline.read_bytes()).hexdigest(),'78f6be637bf9f062a4e9591c34038acb20bf3b726b63d29f18a3e1880a1c6cb8');old=baseline.read_text();new=(ROOT/'barc_stock_queue_reader.lua').read_text();a=new.index(' if result then\n');z=new.index(' return unavailable(message, request, "oversize")',a);self.assertEqual(new[:a]+' if result then return result end\n'+new[z:],old) + hook=new[a:z];self.assertNotIn('GiveOrder',hook);self.assertIn('#rows == 0',hook);self.assertIn('observed.observedFrame == nil',hook) + def test_unsynced_only_configuration(self): + text=(ROOT/'barc_stock_queue_fixture.lua').read_text();start=text.index('if not gadgetHandler:IsSyncedCode() then');end=text.index('local OPT_ENABLE',start);self.assertIn('Spring.GetAIInfo(team)',text[start:end]);self.assertNotIn('Spring.GetAIInfo(',text[end:]);self.assertNotIn('Spring.GetConfigInt(',text[end:]);self.assertNotIn('SYNCED_NOSHORTNAME',text[end:]);self.assertNotEqual(hashlib.sha512(b'controlled').hexdigest(),hashlib.sha256(b'controlled').hexdigest()) +if __name__=='__main__':unittest.main() diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/buffered_writer.py b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/buffered_writer.py new file mode 100644 index 0000000..1188879 --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/buffered_writer.py @@ -0,0 +1,51 @@ +"""Public Recoil2639 FileSink-style libc full-buffered ordinary-file writer. +FileSink.cpp:123-130,214-223: fopen(w), setvbuf(_IOFBF,8192), fprintf newline. +No severity-triggered fflush, engine, content, or private inputs. +""" +import base64,ctypes,os,sys +ATLAS=(b'CTextureRenderAtlas::CreateAtlasTexture()[0] atlas=IconsAtlas_0 FBO::ready=1',b'CTextureRenderAtlas::CreateAtlasTexture()[1] atlas=IconsAtlas_0 atlasRendered=0') +ROOTS=(b'[DataDirLocater::Check] Using read-write data directory: /public/write/',b'[DataDirLocater::Check] Using read-only data directory: /public/data/',b'[DataDirLocater::Check] Isolation Mode!',b'[SpringApp::Init] Spring writeable configuration directory: /public/write/') +class BufferedWriter: + def __init__(self,path): + self.libc=ctypes.CDLL(None) + self.libc.fopen.argtypes=(ctypes.c_char_p,ctypes.c_char_p);self.libc.fopen.restype=ctypes.c_void_p + self.libc.setvbuf.argtypes=(ctypes.c_void_p,ctypes.c_void_p,ctypes.c_int,ctypes.c_size_t) + self.libc.fprintf.argtypes=(ctypes.c_void_p,ctypes.c_char_p) + self.libc.fflush.argtypes=(ctypes.c_void_p,);self.libc.fclose.argtypes=(ctypes.c_void_p,) + self.stream=self.libc.fopen(os.fsencode(path),b'w');assert self.stream + os.chmod(path,0o600) + assert self.libc.setvbuf(self.stream,None,0,8192)==0 # Linux _IOFBF + self.path=path;self.records=0 + def record(self,text): + assert b'\x00' not in text and b'\n' not in text + assert self.libc.fprintf(self.stream,b'%s\n',ctypes.c_char_p(text))>0 + self.records+=1 + def partial_eof(self): + previous=os.stat(self.path).st_size + for index in range(512): # <=64KiB public records per command + self.record(ATLAS[self.records%2]);size=os.stat(self.path).st_size + if size>previous: + with open(self.path,'rb') as reader:reader.seek(-1,2);last=reader.read(1) + if last!=b'\n':return size + previous=size + raise RuntimeError('no partial EOF in bounded public replay') + def close(self):self.libc.fclose(self.stream) +def main(): + os.umask(0o077);writer=BufferedWriter(sys.argv[1]) + try: + records=ROOTS if len(sys.argv)==2 else base64.b64decode(sys.argv[2]).splitlines() + for record in records:writer.record(record) + writer.partial_eof() + if len(sys.argv)>2:assert writer.libc.fflush(writer.stream)==0 + print('ready',flush=True) + for command in sys.stdin: + command=command.strip() + if command=='grow':writer.partial_eof() + elif command=='growflush': + writer.partial_eof();assert writer.libc.fflush(writer.stream)==0 + elif command=='flush':assert writer.libc.fflush(writer.stream)==0 + elif command=='close':break + else:raise RuntimeError('closed writer command') + print('done',flush=True) + finally:writer.close() +if __name__=='__main__':main() diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/capture_custody.py b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/capture_custody.py new file mode 100644 index 0000000..eb87da6 --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/capture_custody.py @@ -0,0 +1,77 @@ +"""Exact immutable capture inputs copied to the fresh owned execution capsule.""" +import copy,hashlib,math,os,stat,time +from pathlib import Path +from private_io import SHA,atomic_bytes_new,atomic_new,canonical,components,hash_artifact,need,read_bytes,read_json +from settings_custody import inventory +from growing_log import _source_set +MAX_FILES=8192 +MAX_TOTAL=128*1024*1024 +MAX_FILE=64*1024*1024 +SPEC='tests/Broker.NativeProof/tactical-native-journey.spec.js' + +def _names(root): + names=set() + for base,dirs,files in os.walk(root,followlinks=False): + for name in dirs:need(not (Path(base)/name).is_symlink(),'capture symlink directory') + for name in files: + p=Path(base)/name;need(not p.is_symlink(),'capture symlink file');names.add(str(p.relative_to(root)));need(len(names)<=MAX_FILES,'capture file census cap') + return names + +def _target(config,attempt): + attempt=components(str(attempt));need(str(attempt)==config['roots']['attemptRoot'],'capture attempt escape') + need(attempt.stat().st_uid==os.geteuid() and stat.S_IMODE(attempt.stat().st_mode)==0o700,'capture attempt custody') + browser=components(str(attempt/'browser'));need(browser.stat().st_uid==os.geteuid() and stat.S_IMODE(browser.stat().st_mode)==0o700,'capture browser root custody') + return browser/'execution-capsule' + +def prepare_capture_capsule(config,attempt,config_sha256,deadline): + need(type(deadline) in (int,float) and math.isfinite(deadline) and deadline<=time.monotonic()+180,'capture fixed operation deadline') + def checkpoint():need(time.monotonic()0,'host-dynamic-page-size') + ordinary=[];rows=[];objects=set();last_end=0 + for line in lines: + fields=line.split(None,5);name=fields[5] if len(fields)==6 else '' + if name!=NAME: + need(not name.startswith('/memfd:') and not name.endswith(' (deleted)'),'host-dynamic-unidentified-deleted-map') + ordinary.append(line);continue + match=ROW.fullmatch(line);need(match is not None,'host-dynamic-malformed-row') + begin,end,perms,offset,major,minor,inode,name=match.groups() + begin,end,offset,major,minor,inode=(int(begin,16),int(end,16),int(offset,16),int(major,16),int(minor,16),int(inode)) + need(perms in PROFILE['permissions'],'host-dynamic-permissions') + need(0=last_end and begin%page_size==end%page_size==0,'host-dynamic-range') + need(0<=offset1 and held['uid']==os.geteuid(),'host-dynamic-owned-process') + pid=held['pid'];need(os.stat(f'/proc/{pid}').st_uid==held['uid'] and int(start_ticks(pid))==int(held['startTicks']),'host-dynamic-process-reuse') + need(os.readlink(f'/proc/{pid}/exe')==executable,'host-dynamic-executable') + actual=os.stat(f'/proc/{pid}/exe');expected=os.stat(executable,follow_symlinks=False) + need(stat.S_ISREG(expected.st_mode) and (actual.st_dev,actual.st_ino)==(expected.st_dev,expected.st_ino),'host-dynamic-executable-inode') + return {'pid':pid,'startTicks':int(held['startTicks']),'uid':held['uid'],'executable':executable,'executableDevice':actual.st_dev,'executableInode':actual.st_ino} + +def _fd_stat(info): + return {'device':info.st_dev,'deviceMajor':os.major(info.st_dev),'deviceMinor':os.minor(info.st_dev),'inode':info.st_ino,'size':info.st_size,'mode':info.st_mode,'uid':info.st_uid,'nlink':info.st_nlink} + +def owner_metadata(held,executable,rows): + before=held_process(held,executable);pid=held['pid'];matches=[] + # Iterate boundedly rather than allocating an unbounded list of FD names. + with os.scandir(f'/proc/{pid}/fd') as entries: + for count,entry in enumerate(entries,1): + need(count<=PROFILE['fds'],'host-dynamic-fd-bound');need(entry.name.isascii() and entry.name.isdecimal(),'host-dynamic-fd-name') + path=f'/proc/{pid}/fd/{entry.name}' + try:link=os.readlink(path) + except FileNotFoundError:continue + need(len(os.fsencode(link))<=PROFILE['linkBytes'],'host-dynamic-link-bound') + if link!=NAME:continue + # Follow only the exact matched proc FD, for stat metadata, never data. + first=_fd_stat(os.stat(path));fdinfo=proc_bytes(pid,f'fdinfo/{entry.name}',PROFILE['fdinfoBytes']).decode('ascii') + need(os.readlink(path)==NAME,'host-dynamic-fd-link-race');last=_fd_stat(os.stat(path)) + need(first==last,'host-dynamic-fd-stat-race') + matches.append({'fd':int(entry.name),'link':link,'stat':first,'fdinfo':fdinfo}) + after=held_process(held,executable);need(before==after,'host-dynamic-process-race') + # Recheck every captured descriptor after census and process check. + for item in matches: + path=f"/proc/{pid}/fd/{item['fd']}" + need(os.readlink(path)==NAME and _fd_stat(os.stat(path))==item['stat'],'host-dynamic-owner-race') + need(held_process(held,executable)==before,'host-dynamic-final-process-race') + return validate_owner(rows,matches,before,after) + +def validate_owner(rows,matches,before,after): + """Pure metadata join also used by explicitly mocked source controls.""" + need(before==after,'host-dynamic-process-race');need(len(matches)<=PROFILE['fds'],'host-dynamic-fd-bound') + objects=set() + for item in matches: + info=item['stat'];need(type(item['fd']) is int and item['fd']>=0 and item['link']==NAME,'host-dynamic-fd-identity') + need(stat.S_ISREG(info['mode']) and info['uid']==before['uid'] and info['nlink']==0 and type(info['size']) is int and 0<=info['size']=0 and value['consumedRevision']<=value['validatedRevision']<=value['observedRevision'],'coherent growing log counters') + phases={'accepted':'validated','pending':'sampled','refused':'invalid','unknown':'unknown'} + need(status in phases and value['phase']==phases[status],'growing log status/phase mismatch') + previous=prior or {'validatedRevision':0,'validatedBoundary':None,'consumedRevision':0,'consumedBoundary':None,'consumedPrefix':None,'consumption':None} + need(value['consumedRevision']==previous['consumedRevision'] and value['consumedBoundary']==previous['consumedBoundary'] and value['consumedPrefix']==previous['consumedPrefix'] and value['consumption']==previous['consumption'],'candidate cannot manufacture consumption') + if status in ('accepted','pending'): + prefix=value['prefix'];cut=raw.rfind(b'\n')+1 + need(isinstance(prefix,dict) and set(prefix)=={'rawBytes','rawSha256','completeBytes','completeSha256','tailBytes','tailSha256','completeRecords','tailClass'},'closed raw/P/T descriptor') + expected={'rawBytes':len(raw),'rawSha256':hashlib.sha256(raw).hexdigest(),'completeBytes':cut,'completeSha256':hashlib.sha256(raw[:cut]).hexdigest(),'tailBytes':len(raw)-cut,'tailSha256':hashlib.sha256(raw[cut:]).hexdigest(),'completeRecords':raw.count(b'\n')} + need(all(type(prefix[key]) is type(item) and prefix[key]==item for key,item in expected.items()),'exact raw/P/T byte join') + need(type(prefix['tailClass']) is str and prefix['tailClass'] in ('empty','atlas','pending','pending-utf8','refused-nul','refused-utf8'),'closed F# tail class') + if status=='accepted':need(value['candidatePrefix']==prefix and prefix['completeRecords']>0 and prefix['tailClass'] in ('empty','atlas'),'safe F# tail/root candidate') + if status=='accepted': + need(value['observedRevision']==value['validatedRevision']==revision and value['validatedBoundary']==boundary and value['bytes']==sample_bytes and value['sha256']==sample_sha256 and value['stickyInvalid'] is False and value['reason'] is None,'fresh growing log decision') + elif status=='pending': + previous=prior or {'validatedRevision':0,'validatedBoundary':None,'consumedRevision':0,'consumedBoundary':None} + need(value['observedRevision']==revision and value['bytes']==sample_bytes and value['sha256']==sample_sha256 and value['stickyInvalid'] is False and type(value['reason']) is str and len(value['reason'])>0,'fresh pending growing log decision') + need(value['validatedRevision']==previous['validatedRevision'] and value['validatedBoundary']==previous['validatedBoundary'] and value['consumedRevision']==previous['consumedRevision'] and value['consumedBoundary']==previous['consumedBoundary'],'pending growing log history mismatch') + else: + need(value['stickyInvalid'] is True and type(value['reason']) is str and len(value['reason'])>0,'terminal growing log state mismatch') + return value + +class GrowingLog: + def __init__(self,path,process,fd,opened,writer): + self.path=str(path);self.process=dict(process);self.fd=fd;self.device=opened.st_dev;self.inode=opened.st_ino + self.previous=b'';self.revision=0;self.state=None;self.revoked=False;self.writer=writer;self.closure_sha256=None;self.failure_checkpoint='policy-scope-join';self.last_policy_observation=None;self.settlement_transcript=[];self.settlement_effects=[];self._reset_counters(None) + def _reset_counters(self,boundary): + self.counter_started=time.monotonic();self.counter_cause=None;self.counter_deadline=None + self.counters={'boundary':boundary,'probesBegun':0,'probesCompleted':0,'readOperations':0,'readCalls':0,'readCallsInOperation':0,'requestedReadBytes':0,'bytesReadInOperation':0,'evaluationsBegun':0,'evaluationsCompleted':0,'growthAfterEvaluation':0,'maximumObservedBytes':0,'sampleBytes':0,'completePrefixBytes':0,'tailBytes':0} + prefix=self.previous.rfind(b'\n')+1 + self.counters.update(sampleBytes=len(self.previous),completePrefixBytes=prefix,tailBytes=len(self.previous)-prefix) + def _counter_observation(self,check): + elapsed=max(0,int((time.monotonic()-self.counter_started)*1000000)) + return {'schema':'fsbar.barc-infolog-mechanical-observation/v1',**self.counters,'elapsedMicroseconds':elapsed,'settlementBudgetMicroseconds':None if self.counter_deadline is None else max(0,int((self.counter_deadline-self.counter_started)*1000000)),'terminalCheck':check,'terminalCause':self.counter_cause or ('settlement-deadline' if check=='infolog-record-settlement-deadline' else 'other'),'sampleSha256':hashlib.sha256(self.previous).hexdigest(),'nativeAcceptance':False} + def _retain_counter_observation(self,error,config): + check=getattr(error,'_barc_failure_observation',{}).get('check',self.failure_checkpoint) + observation=self._counter_observation(check) + error._barc_infolog_mechanical_observation=observation + # Separate mechanical evidence: never alters the typed failure projection + # or acceptance. Missing publication is explicitly visible on the error. + try: + need(self.counters['boundary'] in BOUNDARIES,'closed counter boundary') + name='infolog-mechanical-'+self.counters['boundary']+'.json' + root=components(config['roots']['attemptRoot']) + encoded=(json.dumps(observation,separators=(',',':'),allow_nan=False)+'\n').encode() + atomic_bytes_new(str(root/name),encoded,4096) + atomic_new(str(root/(name+'.receipt.json')),{'schema':'fsbar.barc-infolog-mechanical-receipt/v1','file':name,'bytes':len(encoded),'sha256':hashlib.sha256(encoded).hexdigest(),'sourceSetSha256':_source_set(config),'nativeAcceptance':False}) + error._barc_infolog_mechanical_retained=True + except Exception:error._barc_infolog_mechanical_retained=False + @classmethod + def acquire(cls,path,process): + check='infolog-path';fd=None + try: + path=components(str(canonical(str(path))));before=path.lstat() + check='infolog-parent';parent=path.parent.stat() + need(parent.st_uid==os.geteuid() and stat.S_ISDIR(parent.st_mode) and stat.S_IMODE(parent.st_mode)==0o700,'growing log private parent') + check='infolog-file-custody';need(stat.S_ISREG(before.st_mode) and before.st_uid==os.geteuid() and stat.S_IMODE(before.st_mode)==0o600 and before.st_nlink==1 and before.st_size<=MAX_LOG,'growing log custody/bound') + check='infolog-process';need(set(process)=={'pid','startTicks','uid'} and process['uid']==os.geteuid() and start_ticks(process['pid'])==process['startTicks'],'growing log producer identity') + check='infolog-open';fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_CLOEXEC|os.O_NONBLOCK) + check='infolog-descriptor-identity' + opened=os.fstat(fd);need((opened.st_dev,opened.st_ino,opened.st_uid,stat.S_IMODE(opened.st_mode),opened.st_nlink)==(before.st_dev,before.st_ino,before.st_uid,0o600,1),'growing log identity changed') + writer=cls._writer(process,path,opened) + check='infolog-final-generation' + need(start_ticks(process['pid'])==process['startTicks'],'growing log producer changed') + return cls(path,process,fd,opened,writer) + except BaseException as error: + failure=mark_failure(error,check) + if fd is not None: + try:os.close(fd) + except OSError:setattr(failure,'_barc_cleanup_unknown',True) + raise failure + @staticmethod + def _writer(process,path,opened,deadline=None): + def check_deadline(): + if deadline is not None:GrowingLog._deadline(deadline) + try: + check_deadline();pid=process['pid'];names=os.listdir(f'/proc/{pid}/fd');check_deadline();need(len(names)<=MAX_FDS,'growing log writer fd bound');matches=[] + for name in names: + check_deadline() + if not name.isdigit():continue + try: + current=os.stat(f'/proc/{pid}/fd/{name}') + check_deadline() + if (current.st_dev,current.st_ino)!=(opened.st_dev,opened.st_ino):continue + value=proc_bytes(pid,f'fdinfo/{name}',4096).decode('ascii') + check_deadline() + flags=re.search(r'^flags:\s+([0-7]+)$',value,re.MULTILINE);position=re.search(r'^pos:\s+([0-9]+)$',value,re.MULTILINE) + if flags and position: + bits=int(flags[1],8) + if bits&os.O_ACCMODE in (os.O_WRONLY,os.O_RDWR):matches.append({'fd':int(name),'flags':bits,'position':int(position[1])}) + except FileNotFoundError:continue + except BaseException as error:raise mark_failure(error,'infolog-writer-census') + try: + check_deadline() + need(len(matches)==1,'growing log writer unavailable or ambiguous') + need(start_ticks(pid)==process['startTicks'],'growing log writer generation changed') + check_deadline() + return matches[0] + except BaseException as error:raise mark_failure(error,'infolog-writer-match') + def close(self): + if self.fd is not None: + os.close(self.fd);self.fd=None + @staticmethod + def _deadline(deadline): + if time.monotonic()>=deadline: + raise mark_failure(Refused('growing log record settlement deadline'),'infolog-record-settlement-deadline','deadline',None) + def _read_exact(self,length,deadline=None): + chunks=[];offset=0;calls=0 + self.counters['readOperations']+=1;self.counters['readCallsInOperation']=0;self.counters['requestedReadBytes']=length;self.counters['bytesReadInOperation']=0 + while offset=MAX_PREAD_CALLS: + self.counter_cause='exact-read-call-cap' + raise mark_failure(Refused('growing log record exact-read exhausted'),'infolog-record-settlement-exhausted','refused',None) + self.counters['readCalls']+=1;self.counters['readCallsInOperation']+=1 + block=os.pread(self.fd,min(65536,length-offset),offset);need(block,'growing log truncated');chunks.append(block);offset+=len(block) + calls+=1;self.counters['bytesReadInOperation']=offset + if deadline is not None:self._deadline(deadline) + return b''.join(chunks) + def _sample(self,deadline): + try:return self._sample_impl(deadline) + except BaseException: + self.revoked=True;raise + def _custody(self, expected=None, exact_size=False, final=False, deadline=None): + def checkpoint(name): + if final:self.failure_checkpoint='infolog-final-'+name + if deadline is not None:self._deadline(deadline) + checkpoint('scope') + need(not self.revoked and self.fd is not None,'growing log scope revoked or closed') + pid=self.process['pid'] + checkpoint('process') + need(os.stat(f'/proc/{pid}').st_uid==self.process['uid']==os.geteuid() and start_ticks(pid)==self.process['startTicks'],'growing log producer changed') + checkpoint('path');path=components(self.path) + checkpoint('parent');parent=path.parent.stat() + need(parent.st_uid==os.geteuid() and stat.S_ISDIR(parent.st_mode) and stat.S_IMODE(parent.st_mode)==0o700,'growing log private parent changed') + checkpoint('named-identity');named=path.lstat() + need((named.st_dev,named.st_ino)==(self.device,self.inode),'growing log named identity changed') + checkpoint('descriptor-identity');opened=os.fstat(self.fd) + need((opened.st_dev,opened.st_ino)==(self.device,self.inode),'growing log descriptor identity changed') + checkpoint('file-custody') + need(stat.S_ISREG(named.st_mode) and named.st_uid==os.geteuid() and stat.S_IMODE(named.st_mode)==0o600 and named.st_nlink==1,'growing log custody') + self.counters['maximumObservedBytes']=max(self.counters['maximumObservedBytes'],opened.st_size) + checkpoint('size-cap');need(opened.st_size<=MAX_LOG,'growing log bound') + writer=self._writer(self.process,path,opened,deadline) + if final:self.final_read_started=time.monotonic() + if expected is not None: + checkpoint('size-regression');need(opened.st_size>=len(expected) and (not exact_size or opened.st_size==len(expected)),'growing log changed after policy') + checkpoint('prefix-read');actual=self._read_exact(len(expected),deadline) + checkpoint('prefix-drift');need(actual==expected,'growing log prefix changed after read') + if final: + # The size at L must be observed after the complete prefix reread. + # Never return the earlier pre-read fstat as a final horizon. + read_end=time.monotonic() + # Recheck permanent changes during the reread, then observe size last. + self._custody(deadline=deadline) + opened=os.fstat(self.fd);at=time.monotonic() + need((opened.st_dev,opened.st_ino)==(self.device,self.inode) and opened.st_size>=len(expected),'growing log final identity/size regression') + need(opened.st_size<=MAX_LOG,'growing log bound') + self.final_observation={'readStartMicroseconds':max(0,int((self.final_read_started-self.counter_started)*1000000)),'readEndMicroseconds':max(0,int((read_end-self.counter_started)*1000000)),'linearizedMicroseconds':max(0,int((at-self.counter_started)*1000000)),'rawBytes':opened.st_size} + return opened,writer + def _sample_impl(self,deadline): + self._deadline(deadline) + opened,_=self._custody(deadline=deadline) + need(len(self.previous)<=opened.st_size,'growing log truncated') + length=opened.st_size;self.counters['maximumObservedBytes']=max(self.counters['maximumObservedBytes'],length);raw=self._read_exact(length,deadline) + # This second retained-FD read closes the permanent overwrite window + # after the first read. The custody check also reopens no authority: + # any failure makes this object sticky-revoked in _sample. + after,writer=self._custody(raw,False,deadline=deadline) + need(after.st_size>=length,'growing log truncated during sample') + intact=raw.startswith(self.previous) + need(intact,'growing log prefix changed') + self._deadline(deadline) + return raw,intact,writer + def _settle(self,deadline,probes,raw_mode=False,pending_raw=None): + while probes0,'growing log policy stdin stalled');offset+=written + except BrokenPipeError: + offset=len(encoded) + if offset==len(encoded): + selector.unregister(input_fd);process.stdin.close();stdin_open=False + elif key.data=='stdout' and stdout_open: + block=os.read(output_fd,min(65536,131073-transport_count)) + if block: + transport_count+=len(block);output.extend(block);need(transport_count<=131072,'growing log policy output bound') + if not ready and b'\n' in output: + line,tail=bytes(output).split(b'\n',1) + value=json.loads(line.decode('utf-8'),object_pairs_hook=_unique) + need(set(value)=={'schema','invocationId','closureSha256','pid','startTicks','uid','phase'} and value['schema']=='fsbar.barc-runtime-evidence-policy-ready/v2' and value['phase']=='ready' and value['invocationId']==invocation and value['closureSha256']==closure_sha and value['pid']==process.pid and value['startTicks']==start_ticks(process.pid) and value['uid']==os.geteuid(),'policy ready join') + ready=True;ready_line=line;ready_value=value;output=bytearray(tail);selector.register(input_fd,selectors.EVENT_WRITE,'stdin');input_registered=True + else: + selector.unregister(output_fd);process.stdout.close();stdout_open=False + if not ready and stdin_open: + process.stdin.close();stdin_open=False + elif key.data=='stderr' and stderr_open: + block=os.read(error_fd,min(65536,131073-transport_count)) + if block: + transport_count+=len(block);diagnostic.extend(block);need(transport_count<=131072,'growing log policy combined output bound') + else: + selector.unregister(error_fd);process.stderr.close();stderr_open=False + if process.poll() is not None and stdin_open: + if input_registered:selector.unregister(input_fd) + process.stdin.close();stdin_open=False + remaining=policy_deadline-time.monotonic() + if remaining<=0:raise subprocess.TimeoutExpired([path],5) + code=process.wait(timeout=remaining);need(code in (0,2),'growing log policy execution') + need(ready and ready_line is not None,'policy readiness unavailable') + check='policy-completion' + try:final=json.loads(bytes(output).decode('utf-8'),object_pairs_hook=_unique) + except (UnicodeError,json.JSONDecodeError) as error:raise Refused('invalid policy completion') from error + need(set(final)=={'schema','invocationId','closureSha256','pid','startTicks','uid','phase','result'} and final['schema']=='fsbar.barc-runtime-evidence-policy-completed/v2' and final['invocationId']==invocation and final['closureSha256']==closure_sha and final['pid']==process.pid and final['startTicks']==ready_value['startTicks'] and final['uid']==os.geteuid() and final['phase']=='completed','policy completion join') + self.last_policy_observation=_policy_observation(bytes(diagnostic)) if diagnostic else None + self.last_invocation={'invocationId':invocation,'closureSha256':closure_sha,'pid':process.pid,'startTicks':ready_value['startTicks'],'uid':os.geteuid(),'readyPhase':ready_value['phase'],'completedPhase':final['phase']} + return (json.dumps(final['result'],separators=(',',':'))+'\n').encode() + except BaseException as error: + observation=_policy_observation(bytes(diagnostic)) if diagnostic else None + if observation is not None:check=POLICY_TO_MECHANICAL[observation['checkpoint']] + outcome='deadline' if isinstance(error,subprocess.TimeoutExpired) else None + if process is not None and process.poll() is None: + process.kill() + try:process.wait(timeout=max(0,policy_deadline-time.monotonic())) + except subprocess.TimeoutExpired as settlement:raise mark_failure(Refused('growing log policy child settlement unknown'),check,'unexpected',observation) from settlement + raise mark_failure(error,check,outcome,observation) + finally: + if selector is not None:selector.close() + if process is not None and process.stdin is not None and not process.stdin.closed:process.stdin.close() + if process is not None and process.stdout is not None and not process.stdout.closed:process.stdout.close() + if process is not None and process.stderr is not None and not process.stderr.closed:process.stderr.close() + def consume(self,boundary,config,deadline): + self._reset_counters(boundary) + try:return self._consume_impl(boundary,config,deadline) + except BaseException as error: + self.revoked=True + error=mark_failure(error,self.failure_checkpoint) + if self.state is not None and not self.state['stickyInvalid']: + before=self.state;fact=getattr(error,'_barc_failure_observation',{}) + unavailable=fact.get('outcome')=='os-unavailable' + self.state={**before,'phase':'unknown' if unavailable else 'invalid','stickyInvalid':True,'candidatePrefix':None,'reason':fact.get('check',self.failure_checkpoint)} + self.settlement_effects.append({'kind':'revoke','unavailable':unavailable,'reason':self.state['reason'],'beforeState':before,'afterState':self.state}) + self._retain_counter_observation(error,config) + raise error + def _consume_impl(self,boundary,config,deadline): + need(boundary in BOUNDARIES and isinstance(deadline,(int,float)),'closed growing log boundary/deadline') + policy_deadline=min(deadline,self.counter_started+5);self.counter_deadline=policy_deadline + attempt_id=1 if self.state is None else self.state['attemptId']+1 + frozen_deadline=min(5000000,max(0,int((policy_deadline-self.counter_started)*1000000))) + self.settlement_effects.append({'kind':'boundary','boundary':boundary,'attemptId':attempt_id,'deadlineMicroseconds':frozen_deadline,'beforeState':self.state}) + policy=config['artifacts']['runtimeEvidencePolicy'];closure=config['artifacts']['runtimeEvidencePolicyClosure'] + self.failure_checkpoint='policy-artifact';hash_artifact(policy['path'],policy['sha256'],268435456) + self.failure_checkpoint='policy-closure-precheck';closure_value=self._closure(closure['path'],closure['sha256'],policy_deadline) + apphost=next(row for row in closure_value['managed'] if row['role']=='apphost') + self.failure_checkpoint='policy-apphost-join' + need((policy['path'],policy['sha256'])==(apphost['path'],apphost['sha256']),'launched apphost/closure join') + self.failure_checkpoint='policy-scope-join' + if self.closure_sha256 is None:self.closure_sha256=closure['sha256'] + need(self.closure_sha256==closure['sha256'],'policy closure changed within growing-log scope') + expected={'runId':config['runId'],'sourceSetSha256':_source_set(config),'apphostSha256':policy['sha256'],'closureSha256':closure['sha256'],'writeRoot':config['commands']['engine'][5],'dataRoot':config['commands']['engine'][3]} + last=None;probes=0 + for _ in range(MAX_SAMPLES): + need(time.monotonic()0,'growing log policy deadline') + self.failure_checkpoint='policy-transport';self.last_policy_observation=None + self.counters['evaluationsBegun']+=1 + output=self._run_policy(policy['path'],closure['path'],closure['sha256'],encoded,policy_deadline) + self.counters['evaluationsCompleted']+=1 + try:value=json.loads(output.decode('utf-8'),object_pairs_hook=_unique,parse_constant=lambda _:(_ for _ in ()).throw(Refused('nonfinite policy JSON'))) + except (UnicodeError,json.JSONDecodeError) as error:raise Refused('invalid growing log policy result') from error + self.failure_checkpoint='policy-result-join' + need(set(value)=={'schema','status','state'} and value['schema']=='fsbar.barc-growing-log-policy-result/v3' and value['status'] in ('accepted','pending','refused','unknown') and isinstance(value['state'],dict),'closed growing log policy result') + identity=dict(expected);identity.update({'pid':self.process['pid'],'startTicks':self.process['startTicks'],'uid':self.process['uid'],'device':str(self.device),'inode':str(self.inode),'path':self.path}) + sample_sha256=hashlib.sha256(raw).hexdigest() + next_state=_closed_state(value['state'],value['status'],boundary,identity,self.revision,len(raw),sample_sha256,self.state,raw);last=value['status'] + need(next_state['attemptId']==attempt_id and next_state['probeCount']==probes and next_state['evaluationCount']==self.counters['evaluationsCompleted'] and next_state['deadlineMicroseconds']==frozen_deadline,'frozen boundary budget/result join') + before_state=self.state + self.state=next_state + # Qualification consumes this bounded in-memory trace to prove the + # actual policy transition and its position among settlement probes. + self.settlement_effects.append({'kind':'policy','boundary':boundary,'status':last,'expected':expected,'observation':{key:request['observation'][key] for key in ('pid','startTicks','uid','device','inode','path','revision','bytes','sha256','attemptId','probeCount','evaluationCount','deadlineMicroseconds')},'prefix':next_state['prefix'],'rawBase64':request['observation']['logBase64'],'invocation':self.last_invocation,'beforeState':before_state,'afterState':next_state}) + # This is only a scoped candidate. Growth + # observed after policy evaluation receives a new revision instead. + if last in ('refused','unknown'): + self.failure_checkpoint='policy-result-join';self.revoked=True + raise mark_failure(Refused('growing log policy refused or unknown'),self.failure_checkpoint,'policy-nonaccepted',self.last_policy_observation) + self.failure_checkpoint='infolog-final-refresh';current,_=self._custody(raw,False,True,policy_deadline) + self.settlement_effects.append({'kind':'final-observation','boundary':boundary,'revision':self.revision,**self.final_observation,'rawSha256':sample_sha256}) + self.failure_checkpoint='policy-final-precheck' + hash_artifact(policy['path'],policy['sha256'],268435456) + self._closure(closure['path'],closure['sha256'],policy_deadline) + need(time.monotonic()0,'proc positive byte bound') + chunks=[];length=0 + with open(f'/proc/{pid}/{name}','rb',buffering=0) as stream: + while length<=maximum: + block=stream.read(min(65536,maximum+1-length)) + if not block:break + chunks.append(block);length+=len(block) + need(length<=maximum,'proc byte bound');return b''.join(chunks) + +def start_ticks(pid): + value=proc_bytes(pid,'stat',16384).decode();return value.rsplit(') ',1)[1].split()[19] +def verify_process(process,engine,plugin,producer_path=None): + need(set(process)=={'pid','startTicks','uid'},'closed process identity') + pid=process['pid'];need(type(pid) is int and pid>1 and process['uid']==os.geteuid(),'owned selected process') + need(os.stat(f'/proc/{pid}').st_uid==os.geteuid() and start_ticks(pid)==process['startTicks'],'PID/start mismatch') + need(os.readlink(f'/proc/{pid}/exe')==engine['path'],'runtime executable mismatch') + actual_exe=os.stat(f'/proc/{pid}/exe');expected_exe=os.stat(engine['path'],follow_symlinks=False) + need((actual_exe.st_dev,actual_exe.st_ino)==(expected_exe.st_dev,expected_exe.st_ino),'runtime executable inode mismatch') + hash_artifact(engine['path'],engine['sha256']);info=hash_artifact(plugin['path'],plugin['sha256']) + lines=proc_bytes(pid,'maps',1048576).decode().splitlines();matches=[] + for line in lines: + fields=line.split(None,5) + if len(fields)==6 and fields[5]==plugin['path']: + matches.append(fields) + need(matches and all(int(fields[4])==info.st_ino and fields[3]==f'{os.major(info.st_dev):02x}:{os.minor(info.st_dev):02x}' for fields in matches),'mapped module inode mismatch') + if producer_path is not None: + target=os.stat(producer_path,follow_symlinks=False);names=os.listdir(f'/proc/{pid}/fd');need(len(names)<=256,'producer fd bound') + found=False + for name in names: + try: + current=os.stat(f'/proc/{pid}/fd/{name}') + if (current.st_dev,current.st_ino)==(target.st_dev,target.st_ino): + info=proc_bytes(pid,f"fdinfo/{name}",4096).decode() + flags=re.search(r"^flags:\s+([0-7]+)$",info,re.MULTILINE) + if flags: + value=int(flags[1],8) + if value&os.O_ACCMODE==os.O_WRONLY and value&os.O_APPEND: found=True + except FileNotFoundError: continue + need(found,'actual producer open fd unavailable') + need(start_ticks(pid)==process['startTicks'],'process changed during validation') + return {'pid':pid,'startTicks':process['startTicks'],'engineSha256':engine['sha256'],'pluginSha256':plugin['sha256']} + +def verify_capture_writer(expected,writer,node,source): + need(set(expected)=={'pid','startTicks','uid'} and set(writer)=={'pid','startTicks','uid','executable','sourcePath','sourceSha256'},'closed capture writer identity') + need({key:writer[key] for key in expected}==expected,'root-selected capture writer mismatch') + pid=expected['pid'];need(type(pid) is int and pid>1 and expected['uid']==os.geteuid(),'owned capture writer') + need(os.stat(f'/proc/{pid}').st_uid==os.geteuid() and start_ticks(pid)==expected['startTicks'],'capture PID/start mismatch') + need(os.readlink(f'/proc/{pid}/exe')==node['path'] and writer['executable']==node['path'],'capture executable mismatch') + hash_artifact(node['path'],node['sha256']);hash_artifact(source['path'],source['sha256']) + need(writer['sourcePath']==source['path'] and writer['sourceSha256']==source['sha256'],'capture source mismatch') + need(start_ticks(pid)==expected['startTicks'],'capture process changed during validation') + return {'pid':pid,'startTicks':expected['startTicks'],'sourceSha256':source['sha256']} + +def _observed_file(path,maximum=268435456): + try: + before=os.stat(path,follow_symlinks=False) + if not stat.S_ISREG(before.st_mode) or before.st_size>maximum:return {'available':False,'reason':'nonregular-or-oversized'} + fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK|os.O_CLOEXEC) + try: + first=os.fstat(fd);h=hashlib.sha256();size=0 + while True: + block=os.read(fd,65536) + if not block:break + size+=len(block) + if size>maximum:return {'available':False,'reason':'oversized'} + h.update(block) + last=os.fstat(fd) + if (first.st_dev,first.st_ino,first.st_size,first.st_mtime_ns,first.st_ctime_ns)!=(last.st_dev,last.st_ino,last.st_size,last.st_mtime_ns,last.st_ctime_ns):return {'available':False,'reason':'changed-during-read'} + return {'available':True,'device':first.st_dev,'inode':first.st_ino,'bytes':size,'sha256':h.hexdigest()} + finally:os.close(fd) + except OSError:return {'available':False,'reason':'unavailable'} + +def _closure_refuse(code,diagnostic_path,fields,allowed_count,executable_count): + document={'schema':'fsbar.barc-stock-runtime-map-diagnostic/v1','status':'refused','failureCode':code,'allowedCount':allowed_count,'executableMapCount':executable_count,'candidate':fields} + if diagnostic_path is not None: + try:atomic_new(str(diagnostic_path),document) + except Exception:pass + raise RuntimeClosureRefused(code) + +def verify_executable_map_lines(lines,allowed,diagnostic_path=None): + """Require every file-backed executable map to match one pinned path/inode.""" + seen=set();executable=[] + for line in lines: + fields=line.split(None,5) + if len(fields)!=6 or 'x' not in fields[1] or not fields[5].startswith('/'):continue + executable.append(fields) + need(len(executable)<=512,'executable mapping count bound') + for fields in executable: + raw=fields[5];deleted=raw.endswith(' (deleted)');candidate_raw=raw[:-10] if deleted else raw + path=os.path.realpath(candidate_raw);need(len(path.encode())<=4096,'executable mapping path bound') + admitted=allowed.get(path);candidate={'path':path,'mappedDevice':fields[3],'mappedInode':fields[4],'deleted':deleted,'admitted':admitted is not None,'expectedSha256':None if admitted is None else admitted['sha256'],'observed':_observed_file(path)} + if deleted:_closure_refuse('runtime-map-deleted',diagnostic_path,candidate,len(allowed),len(executable)) + if admitted is None:_closure_refuse('runtime-map-unadmitted',diagnostic_path,candidate,len(allowed),len(executable)) + try:info=os.stat(path,follow_symlinks=False) + except OSError:_closure_refuse('runtime-map-missing',diagnostic_path,candidate,len(allowed),len(executable)) + if int(fields[4])!=info.st_ino or fields[3]!=f'{os.major(info.st_dev):02x}:{os.minor(info.st_dev):02x}':_closure_refuse('runtime-map-identity-drift',diagnostic_path,candidate,len(allowed),len(executable)) + try:hash_artifact(path,admitted['sha256'],268435456) + except (Refused,OSError):_closure_refuse('runtime-map-content-drift',diagnostic_path,candidate,len(allowed),len(executable)) + seen.add(path) + if not seen:_closure_refuse('runtime-map-none',diagnostic_path,{},len(allowed),len(executable)) + return seen + +def verify_runtime_closure(process,libraries_artifact,diagnostic_path=None): + try: + need(set(process)=={'pid','startTicks','uid'},'closed runtime process identity') + pid=process['pid'];need(os.stat(f'/proc/{pid}').st_uid==os.geteuid() and start_ticks(pid)==process['startTicks'],'runtime closure PID/start mismatch') + except BaseException as error:raise mark_failure(error,'runtime-closure-process') + try: + from private_io import read_json + value=read_json(libraries_artifact['path'],libraries_artifact['sha256'],4*1024*1024) + except BaseException as error:raise mark_failure(error,'runtime-closure-input') + try: + allowed={} + for item in value['resolvedElfClosure']: + resolved=os.path.realpath(item['resolvedPath']);need(resolved==item['resolvedPath'],'closure resolved path drift');allowed[resolved]=item + for item in value['binaries']: + resolved=os.path.realpath(item['path']);allowed[resolved]={'sha256':item['sha256']} + except BaseException as error:raise mark_failure(error,'runtime-closure-resolve') + try:lines=proc_bytes(pid,'maps',4*1024*1024).decode().splitlines() + except BaseException as error:raise mark_failure(error,'runtime-map-census') + try:seen=verify_executable_map_lines(lines,allowed,diagnostic_path) + except BaseException as error:raise mark_failure(error,'runtime-map-validation') + try: + if os.path.realpath(os.readlink(f'/proc/{pid}/exe')) not in seen:_closure_refuse('runtime-engine-map-missing',diagnostic_path,{},len(allowed),len(seen)) + except BaseException as error:raise mark_failure(error,'runtime-executable-map') + try: + if start_ticks(pid)!=process['startTicks']:_closure_refuse('runtime-process-identity-drift',diagnostic_path,{},len(allowed),len(seen)) + except BaseException as error:raise mark_failure(error,'runtime-final-generation') + return seen + +def acquire_data_root_evidence(log_path,process): + """Acquire the held log descriptor; typed F# owns all root parsing and decisions.""" + from growing_log import GrowingLog + try: + need(set(process)=={'pid','startTicks','uid'},'closed runtime process identity') + pid=process['pid'];need(os.stat(f'/proc/{pid}').st_uid==os.geteuid() and process['uid']==os.geteuid() and start_ticks(pid)==process['startTicks'],'data-root PID/start mismatch') + except BaseException as error:raise mark_failure(error,'infolog-process') + try:return GrowingLog.acquire(log_path,process) + except BaseException as error:raise mark_failure(error,getattr(error,'_barc_failure_observation',{}).get('check','infolog-path')) + + +def verify_host_dynamic_map_lines(lines,allowed,held,executable,profile): + """Only startup's explicit host profile may classify generated mappings.""" + from dynamic_mapping import classify,owner_metadata + ordinary,rows=classify(lines,'host',profile,os.sysconf('SC_PAGE_SIZE')) + try:metadata=owner_metadata(held,executable,rows) + except OSError as error:raise RuntimeClosureRefused('host-dynamic-owner-unavailable') from error + seen=verify_executable_map_lines(ordinary,allowed) + return seen,metadata diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/seed_policy.py b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/seed_policy.py new file mode 100644 index 0000000..ae9c933 --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/seed_policy.py @@ -0,0 +1,109 @@ +"""Literal owned direct-apphost transport; inherited canonical infolog semantics. +Inert without the selected startup runner's concrete live custody context. +""" +import os,time,json,hashlib,selectors,signal,subprocess,pathlib +from growing_log import GrowingLog,_unique,_policy_observation,POLICY_TO_MECHANICAL,MAX_POLICY_INPUT +from private_io import need,Refused +from runtime_identity import mark_failure +class SeedPolicyContext: + def __init__(self,profile,admission,owner,children,actor_selector,streams,counts,deadline): + self.profile=profile;self.admission=admission;self.owner=owner;self.children=children;self.actor_selector=actor_selector;self.streams=streams;self.counts=counts;self.deadline=deadline;self.evaluations=0;self.active=None;self.reaped=[] + def pump(self,deadline): + # This is the real actor selector/owner/quota, not an injected success callback. + import startup_runner as runner + need(time.monotonic()0,'policy settlement Unknown: deadline') + code=child.wait(timeout=remaining);self.owner.leaders.discard(child.pid);self.reaped.append({'identity':ident,'exitCode':code,'reaped':True});self.active=None;return code +class SeedGrowingLog(GrowingLog): + def bind(self,context): + need(type(context)is SeedPolicyContext and not hasattr(self,'context'),'closed concrete policy context');self.context=context;return self + def _run_policy(self,path,closure_path,closure_sha,encoded,deadline): + ctx=self.context;p=ctx.profile['policy'];need((path,closure_path,closure_sha)==(p['apphost'],p['closurePath'],p['closureSha256']),'literal policy input mismatch');need(p['environment']=={'PATH':'/usr/bin:/bin'} and len(encoded)<=MAX_POLICY_INPUT,'literal policy environment/input cap') + invocation=hashlib.sha256(os.urandom(32)).hexdigest();child=None;sel=None;diagnostic=bytearray();all_stdout=bytearray();output=bytearray();ready=None;offset=0;count=0;check='policy-child-start';stdin_open=True;input_registered=False + try: + child=ctx.launch(invocation,deadline);ident=ctx.owner.records[child.pid] + inp,out,err=child.stdin.fileno(),child.stdout.fileno(),child.stderr.fileno() + for fd in [inp,out,err]:os.set_blocking(fd,False) + sel=selectors.DefaultSelector();sel.register(out,selectors.EVENT_READ,'stdout');sel.register(err,selectors.EVENT_READ,'stderr') + while sel.get_map()or stdin_open: + ctx.pump(deadline);left=deadline-time.monotonic();need(left>0,'policy five-second deadline');check='policy-ready'if ready is None else 'policy-transport' + for key,_ in sel.select(min(.02,left)): + if key.data=='stdin': + try:written=os.write(inp,encoded[offset:offset+65536]) + except BrokenPipeError as e:raise Refused('policy request broken pipe')from e + need(written>0,'policy stdin stalled');offset+=written + if offset==len(encoded):sel.unregister(inp);child.stdin.close();stdin_open=False + else: + block=os.read(key.fd,min(65536,131073-count));count+=len(block);need(count<=131072,'combined policy output cap') + if not block: + sel.unregister(key.fd) + if key.data=='stdout': + need(ready is not None,'policy ready unavailable') + need(not stdin_open,'policy output ended before complete request') + continue + if key.data=='stderr':diagnostic.extend(block);continue + all_stdout.extend(block);output.extend(block) + if ready is None and b'\n'in output: + line,tail=bytes(output).split(b'\n',1);ready=json.loads(line.decode(),object_pairs_hook=_unique) + expected={'schema':'fsbar.barc-runtime-evidence-policy-ready/v2','invocationId':invocation,'closureSha256':closure_sha,'pid':ident['pid'],'startTicks':ident['startTicks'],'uid':ident['uid'],'phase':'ready'} + need(ready==expected,'policy exact ready/current-process join');output=bytearray(tail);sel.register(inp,selectors.EVENT_WRITE,'stdin');input_registered=True + if child.poll()is not None and stdin_open:raise Refused('policy exited before full ready/request') + need(ready is not None and offset==len(encoded),'ready before exact request');code=ctx.settle(child,deadline);need(code in [0,2],'policy natural exit') + check='policy-completion';final=json.loads(bytes(output).decode(),object_pairs_hook=_unique) + need(set(final)==set(ready)|{'result'} and all(final[k]==ready[k]for k in ready if k not in ['schema','phase']) and final['schema']=='fsbar.barc-runtime-evidence-policy-completed/v2' and final['phase']=='completed','one completed/final-closure frame') + self.last_policy_observation=_policy_observation(bytes(diagnostic))if diagnostic else None + self.last_invocation={k:ready[k]for k in ['invocationId','closureSha256','pid','startTicks','uid']};self.last_invocation.update(readyPhase='ready',completedPhase='completed') + from snapshot_publication import bytes_new,json_new + import startup_runner as runner + root=pathlib.Path(runner.ROOT)/'evidence';n=ctx.evaluations + frames=bytes_new(root/('policy-'+str(n)+'.stdout.raw'),bytes(all_stdout),131072) + stderr=bytes_new(root/('policy-'+str(n)+'.stderr.raw'),bytes(diagnostic),131072) + json_new(root/('policy-'+str(n)+'.transport.json'),{'requestSha256':hashlib.sha256(encoded).hexdigest(),'requestBytes':len(encoded),'ready':ready,'completed':final,'stdout':frames,'stderr':stderr,'ownedSettlement':ctx.reaped[-1],'deadlineMonotonic':deadline}) + return (json.dumps(final['result'],separators=(',',':'))+'\n').encode() + except BaseException as error: + from snapshot_publication import bytes_new,json_new + import startup_runner as runner + root=pathlib.Path(runner.ROOT)/'evidence';n=ctx.evaluations + try: + stdout_ack=bytes_new(root/('policy-'+str(n)+'.failure.stdout.raw'),bytes(all_stdout),131072) + stderr_ack=bytes_new(root/('policy-'+str(n)+'.failure.stderr.raw'),bytes(diagnostic),131072) + json_new(root/('policy-'+str(n)+'.failure.json'),{'primaryError':str(error),'checkpoint':check,'requestSha256':hashlib.sha256(encoded).hexdigest(),'requestBytes':len(encoded),'stdout':stdout_ack,'stderr':stderr_ack,'nativeAcceptance':False}) + except BaseException as publication:error._barc_secondary_publication_failure=str(publication) + observation=_policy_observation(bytes(diagnostic))if diagnostic else None + if observation is not None:check=POLICY_TO_MECHANICAL[observation['checkpoint']] + if child is not None and ctx.active is child: + try:ctx.settle(child,deadline,force=True) + except BaseException as settlement:raise mark_failure(Refused('policy held child settlement Unknown'),check,'unexpected',observation)from settlement + raise mark_failure(error,check,None,observation) + finally: + if sel is not None:sel.close() + if child is not None and ctx.active is None: + for stream in [child.stdin,child.stdout,child.stderr]: + if stream is not None and not stream.closed:stream.close() diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/settings_custody.py b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/settings_custody.py new file mode 100644 index 0000000..e240e1c --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/settings_custody.py @@ -0,0 +1,59 @@ +"""Root-selected immutable settings seed and attempt-scoped engine output.""" +import hashlib,os,stat +from pathlib import Path +from private_io import Refused,atomic_bytes_new,atomic_new,canonical,components,hash_artifact,need,read_bytes,read_json +MAX_SETTINGS=65536 +MAX_INVENTORY=16*1024*1024 +MAX_FILES=65536 + +def inventory(config): + root=components(config['roots']['packetRoot']);need(root.stat().st_uid==os.geteuid() and stat.S_IMODE(root.stat().st_mode)==0o700,'private packet root') + value=read_json(str(root/'packet-inventory.json'),config['packetSha256'],MAX_INVENTORY) + need(value['root']==str(root) and isinstance(value['files'],list) and len(value['files'])<=MAX_FILES,'bounded packet census') + need(value['fileCount']==len(value['files']),'packet census count') + names=set() + for row in value['files']: + path=row['path'];need(isinstance(path,str) and not path.startswith('/') and all(part not in ('','.','..') for part in path.split('/')),'packet census relative path') + need(path not in names,'duplicate packet census path');names.add(path) + return root,value + +def validate_engine_argv(original,actual,attempt): + need(isinstance(original,list) and isinstance(actual,list) and len(original)==len(actual)==9,'closed engine argv') + need(all(actual[i]==original[i] for i in range(9) if i!=7),'unexpected engine argv change') + need(actual[7]==str(canonical(str(attempt))/'engine'/'springsettings.cfg'),'derived config path mismatch') + need(actual[7]!=original[7],'engine settings must be isolated from seed') + return actual + +def prepare_engine_settings(config,attempt): + attempt=components(str(attempt));need(str(attempt)==config['roots']['attemptRoot'],'attempt output escape') + need(attempt.stat().st_uid==os.geteuid() and stat.S_IMODE(attempt.stat().st_mode)==0o700,'private attempt root') + root,value=inventory(config);original=list(config['commands']['engine']);need(len(original)==9,'closed seed engine argv') + seed=components(original[7]);need(seed.is_relative_to(root),'seed escapes packet') + rel=str(seed.relative_to(root));row=next((x for x in value['files'] if x['path']==rel),None);need(row is not None,'seed absent from admitted census') + before=hash_artifact(str(seed),row['sha256'],MAX_SETTINGS);need(before.st_nlink==1 and before.st_uid==os.geteuid() and stat.S_IMODE(before.st_mode)==0o400,'immutable single-linked seed') + data=read_bytes(str(seed),MAX_SETTINGS,private=False);digest=hashlib.sha256(data).hexdigest();need(digest==row['sha256'] and len(data)==row['bytes'],'seed bytes mismatch') + output=canonical(str(attempt/'engine'/'springsettings.cfg'));parent=components(str(output.parent));need(parent.is_relative_to(attempt) and parent.stat().st_uid==os.geteuid() and stat.S_IMODE(parent.stat().st_mode)==0o700,'private engine output parent') + # Exclusive publication refuses an existing file or symlink without following it. + copied=atomic_bytes_new(str(output),data,MAX_SETTINGS);after=output.lstat();need(after.st_nlink==1 and stat.S_IMODE(after.st_mode)==0o600 and after.st_uid==os.geteuid() and (after.st_dev,after.st_ino)!=(before.st_dev,before.st_ino),'fresh single-linked writable output') + hash_artifact(str(seed),digest,MAX_SETTINGS);need(copied==digest,'exact seed copy') + actual=list(original);actual[7]=str(output);validate_engine_argv(original,actual,attempt) + receipt={'schema':'fsbar.barc-stock-engine-settings-copy/v1','seedPath':str(seed),'seedSha256':digest,'outputPath':str(output),'copiedSha256':copied,'actualEngineArgv':actual,'originalEngineArgv':original,'packetSha256':config['packetSha256'],'seedDevice':before.st_dev,'seedInode':before.st_ino,'outputDevice':after.st_dev,'outputInode':after.st_ino,'nativeAcceptance':False} + atomic_new(str(attempt/'engine-settings-copy.json'),receipt);return actual,receipt + +def verify_packet_unchanged(config): + root,value=inventory(config);expected={row['path']:row for row in value['files']};actual=set() + for base,dirs,files in os.walk(root,followlinks=False): + for name in dirs:need(not (Path(base)/name).is_symlink(),'packet symlink directory') + for name in files: + p=Path(base)/name;need(not p.is_symlink(),'packet symlink file');rel=str(p.relative_to(root)) + if rel=='packet-inventory.json':continue + need(len(actual)0) +def _unique(pairs): + out={} + for key,value in pairs:need(key not in out,'duplicate JSON key');out[key]=value + return out +def _decode(data,label):return json.loads(data.decode('utf-8'),object_pairs_hook=_unique,parse_constant=lambda _:(_ for _ in ()).throw(Refused(f'nonfinite {label} JSON'))) +def canonical_b64(value,size=None): + try:raw=base64.b64decode(value,validate=True) + except Exception:return False + return bool(raw) and (size is None or len(raw)==size) and base64.b64encode(raw).decode()==value +def _writer(value):exact(value,{'pid','startTicks','uid'},'writer');need(type(value['pid']) is int and value['pid']>1 and u64(value['startTicks']) and value['uid']==os.geteuid(),'writer identity') +def _writable_append_fd(pid,path): + target=os.stat(path,follow_symlinks=False);names=os.listdir(f'/proc/{pid}/fd');need(len(names)<=256,'writer fd bound');found=False + for name in names: + try: + current=os.stat(f'/proc/{pid}/fd/{name}') + if (current.st_dev,current.st_ino)!=(target.st_dev,target.st_ino):continue + info=proc_bytes(pid,f'fdinfo/{name}',16384).decode();match=re.search(r'^flags:\s+([0-7]+)$',info,re.MULTILINE) + if match: + flags=int(match.group(1),8);found=found or flags&os.O_ACCMODE==os.O_WRONLY and bool(flags&os.O_APPEND) + except FileNotFoundError:continue + need(found,'actual writable append FD unavailable') +def _same_process(writer,child): + _writer(writer);need(child.poll() is None and writer['pid']==child.pid,'selected writer process');need(os.stat(f'/proc/{child.pid}').st_uid==os.geteuid() and start_ticks(child.pid)==writer['startTicks'],'writer PID/start mismatch') +def _loopback_url(value,schemes): + parsed=urllib.parse.urlsplit(value);need(parsed.scheme in schemes and parsed.hostname=='127.0.0.1' and parsed.port and not parsed.username and not parsed.password and not parsed.fragment,'closed loopback URL') +def _validated_result(value,raw,path,return_snapshot): + if not return_snapshot:return value + return value,{'raw':raw,'sha256':digest(raw),'bytes':len(raw),'sourcePath':str(path),'writer':dict(value['writer']),'authentication':'Observed'} + +def persist_authenticated_snapshots(directory,snapshots): + """Publish only exact bytes already validated; never reread producer files.""" + from snapshot_publication import bytes_new,json_new + receipts={} + for label,(value,snapshot) in snapshots.items(): + need(label in ('ready','metadata','setup') and snapshot['authentication']=='Observed' and digest(snapshot['raw'])==snapshot['sha256'],'validated snapshot binding') + raw_path=str(Path(directory)/('authenticated-'+label+'.raw')) + need(bool(snapshot['raw']),'authenticated nonempty snapshot') + raw_ack=bytes_new(raw_path,snapshot['raw'],1048576) + parsed_ack=json_new(str(Path(directory)/('authenticated-'+label+'.json')),value,1048576) + receipts[label]={k:v for k,v in snapshot.items() if k!='raw'} + receipts[label].update(rawPath=raw_path,sha256=raw_ack['sha256'],rawPublication=raw_ack,parsedPublication=parsed_ack) + json_new(str(Path(directory)/'ready-producer-authentication.json'),{'readyProducerAuthenticated':True,'startupAccepted':False,'snapshots':receipts}) + return receipts + +def authenticate_ready(path,host,config,return_snapshot=False): + raw=read_bytes(str(path),65536);ready=_decode(raw,'ready');exact(ready,{'schema','runId','writer','source','profile','tacticalRevision','queueEvidenceScheme','grpcAddress','gatewayUrl','allowedOrigin','receiverUrl','sessionId','credential','metadataPath','setupPath','hostTracePath'},'ready') + need(ready['schema']==READY_SCHEMA and ready['runId']==config['runId'] and ready['source']==config['source'] and ready['profile']==PROFILE and ready['tacticalRevision']==2 and ready['queueEvidenceScheme']==2 and ready['receiverUrl']==config['bootstrap']['receiverUrl'],'ready binding');_same_process(ready['writer'],host) + need(re.fullmatch(r'127\.0\.0\.1:[1-9][0-9]{0,4}',ready['grpcAddress']),'grpc address');_loopback_url(ready['gatewayUrl'],{'ws','wss'});_loopback_url(ready['allowedOrigin'],{'http','https'});_loopback_url(ready['receiverUrl'],{'http','https'}) + try:need(str(uuid.UUID(ready['sessionId']))==ready['sessionId'],'session UUID') + except (ValueError,AttributeError):raise Refused('session UUID') + need(HEX64.fullmatch(ready['credential']),'credential shape') + for key in ('metadataPath','setupPath','hostTracePath'):beneath(ready[key],config['roots']['attemptRoot']) + need(ready['hostTracePath']==config['paths']['hostTrace'],'host trace path mismatch');_writable_append_fd(host.pid,ready['hostTracePath']);need(start_ticks(host.pid)==ready['writer']['startTicks'],'host changed during ready authentication');return _validated_result(ready,raw,path,return_snapshot) +def _stable_private_json(path,maximum=1048576):return _decode(read_bytes(path,maximum),'private') +def authenticate_metadata(path,ready,return_snapshot=False): + raw=read_bytes(str(path),1048576);value=_decode(raw,'private');exact(value,{'schema','runId','writer','basis','catalogue','actors'},'metadata');need(value['schema']==METADATA_SCHEMA and value['runId']==ready['runId'] and value['writer']==ready['writer'],'metadata identity') + basis=value['basis'];exact(basis,{'token','stateSequence','nativeFrame','matchId','processIncarnation','stateChannelIncarnation'},'basis');need(canonical_b64(basis['token']) and u64(basis['stateSequence'],True) and type(basis['nativeFrame']) is int and 0<=basis['nativeFrame']<2**32 and canonical_b64(basis['matchId'],16),'basis values');need(all(isinstance(basis[k],str) and 0=first.st_size,'live prefix truncated or replaced') + finally:os.close(fd) + raw=b''.join(chunks);need(raw.endswith(b'\n') and b'\x00' not in raw,'incomplete or NUL live prefix');lines=raw.splitlines(keepends=True);need(01 and os.getpgid(pid)==browser.pid and os.getsid(pid)==browser.pid and browser.poll() is None,'capture writer outside retained browser group');selected={'pid':pid,'startTicks':start_ticks(pid),'uid':os.stat(f'/proc/{pid}').st_uid};need(selected['startTicks']==writer['startTicks'],'capture writer changed') + verify_capture_writer(selected,writer,config['artifacts']['captureRuntime'],config['artifacts']['captureSource']);_writable_append_fd(pid,path);_validate_journal(lines,CAPTURE_SCHEMA,config['runId'],writer,config['source']);return selected,writer +def authenticate_host_writer(path,host,ready,config): + _same_process(ready['writer'],host);_writable_append_fd(host.pid,path);_,lines=_bounded_raw(path);_validate_journal(lines,HOST_JOURNAL_SCHEMA,config['runId'],ready['writer'],config['source']);return {'pid':host.pid,'startTicks':ready['writer']['startTicks'],'uid':ready['writer']['uid']} +def _freeze_prefix(path,target,verifier,schema,run_id,writer,source): + before=verifier();raw,lines=_bounded_raw(path);rows=_validate_journal(lines,schema,run_id,writer,source);st=components(path).stat();need(stat.S_ISREG(st.st_mode) and st.st_uid==os.geteuid() and stat.S_IMODE(st.st_mode)==0o600 and st.st_nlink==1,'journal custody');after=verifier();need(before==after,'writer changed during freeze');sha=atomic_bytes_new(target,raw,16*1024*1024);return {'path':target,'sha256':sha,'bytes':len(raw),'records':len(rows),'firstSequence':'1','lastSequence':str(len(rows)),'sourcePath':path,'sourceDevice':st.st_dev,'sourceInode':st.st_ino,'sourceSize':st.st_size,'sourceMtimeNs':st.st_mtime_ns,'writer':after} +def freeze_host_prefix(path,target,host,ready,config):return _freeze_prefix(path,target,lambda:authenticate_host_writer(path,host,ready,config),HOST_JOURNAL_SCHEMA,config['runId'],ready['writer'],config['source']) +def freeze_capture_prefix(path,target,browser,config): + _,writer=authenticate_capture_writer(path,browser,config);return _freeze_prefix(path,target,lambda:authenticate_capture_writer(path,browser,config)[0],CAPTURE_SCHEMA,config['runId'],writer,config['source']) +def freeze_trace_prefix(process,engine,plugin,trace_path,target,run_id): + need(RUN.fullmatch(run_id),'run id');before=verify_process(process,engine,plugin,trace_path);raw,lines,st=_bounded_live_prefix(trace_path);sequence=0 + for line in lines: + row=_decode(line,'trace');exact(row,{'schema','runId','sequence','phase','readFrame','perspectiveTeamId','nativeBasis','context','reader','queue','dispatch'},'trace record');need(row['schema']==TRACE_SCHEMA and row['runId']==run_id and u64(row['sequence'],True) and int(row['sequence'])==sequence+1 and row['phase'] in ('sample','final-read'),'trace sequence');sequence+=1 + after=verify_process(process,engine,plugin,trace_path);need(before==after,'process changed during trace freeze');sha=atomic_bytes_new(target,raw,16*1024*1024);return {'path':target,'sha256':sha,'bytes':len(raw),'records':len(lines),'firstSequence':'1','lastSequence':str(sequence),'sourcePath':trace_path,'sourceDevice':st.st_dev,'sourceInode':st.st_ino,'sourceSize':len(raw),'sourceMtimeNs':st.st_mtime_ns,'process':after} +def revalidate_growing_prefix(receipt,verifier): + need(type(receipt.get('bytes')) is int and 0=first.st_size and after.st_size<=maximum,'prefix append identity') + finally:os.close(fd) + body=b''.join(raw);rows=records(body,role) + for row in rows: + if role=='host': + exact(row,{'schema','runId','sequence','kind','writer','source','value'},'prearm host row');need(row['schema']==HOST_JOURNAL_SCHEMA and row['kind']!='complete','live host cannot claim completion') + else: + exact(row,{'schema','runId','sequence','kind','writer','source','acceptedGeneration','stateSequence','nativeFrame','disposition','value'},'prearm raw row');need(row['schema']=='fsbar.barc-one-unit-raw-state-journal/v1','raw schema') + need(row['runId']==config['runId'] and row['writer']==ready['writer'] and row['source']==config['source'],'prefix exact producer origin') + if role=='host':need(rows[0]['kind']=='header' and rows[0]['value']=={'selectedCase':'stock-smoke-count1'},'host real header') + else:need(len({row['acceptedGeneration']for row in rows})==1 and rows[0]['acceptedGeneration'],'raw owning generation') + live={'path':str(p),'sha256':digest(body),'bytes':len(body),'device':first.st_dev,'inode':first.st_ino,'uid':first.st_uid,'mode':stat.S_IMODE(first.st_mode),'nlink':first.st_nlink} + need(prefix_bytes(live,maximum,deadline)==body,'prefix revalidation');_same_process(ready['writer'],host);_writable_append_fd(host.pid,path) + ack=bytes_new(target,body,maximum);retained={k:ack[k]for k in ['path','sha256','bytes','device','inode','uid','mode','nlink']} + measured=p.stat().st_size + return {'schema':'bar.prearm-live-prefix-observation/v1','role':role,'livePin':live,'retainedPin':retained,'writer':dict(ready['writer']),'terminal':False,'observedBytes':measured,'suffixBytes':measured-len(body)} + +def collect_prearm(p,ready,metadata,setup,host,engine,policy,deadline,started_ns): + """Actual concrete producer joins; called once predicates are observed. + No Node, browser, commands or approval records are generated here. + """ + import time,copy + import mechanics as m + from snapshot_publication import json_new + from loaded_custody import persist_loaded_custody + from qualify_prearm import derive,prefix_bytes,records + A=Path(p['attemptRoot']);root=A/'evidence';cfg=copy.deepcopy(p['seed']['configuration']);need(cfg['source']==p['source'] and cfg['runId']==p['runId'],'seed configuration source') + cfg['commands']['engine']=__import__('startup_runner').commands(p)['engine'] + config_path=root/'seed-effective-launch-config.json';json_new(config_path,cfg);config_pin=m.physical(config_path) + process={'pid':engine.pid,'startTicks':start_ticks(engine.pid),'uid':os.geteuid()} + persist_loaded_custody(process,cfg,A,config_pin['sha256']) + result=policy.consume('browser',cfg,deadline) + json_new(root/'infolog-policy-result.json',result);json_new(root/'infolog-policy-effects.json',policy.settlement_effects) + infolog={'result':m.physical(root/'infolog-policy-result.json'),'effects':m.physical(root/'infolog-policy-effects.json'),'config':cfg,'configPin':config_pin,'producer':m.physical(Path(__file__).parent/'growing_log.py'),'transports':[m.physical(root/('policy-'+str(n)+'.transport.json'))for n in range(1,policy.context.evaluations+1)]} + prefixes={} + for role in ['raw','host']: + source=A/'host'/('raw.jsonl'if role=='raw'else 'host.jsonl');receipt=_freeze_prearm_owned(str(source),str(root/(role+'-prefix.raw')),role,host,ready,cfg,deadline) + json_new(root/(role+'-prefix.json'),receipt);prefixes[role]=receipt + stock=freeze_trace_prefix(process,cfg['artifacts']['engine'],cfg['artifacts']['plugin'],str(A/'engine/stock.jsonl'),str(root/'stock-prefix.raw'),cfg['runId']) + st=Path(stock['sourcePath']).stat();live={k:stock[k]for k in ['sha256','bytes']};live.update(path=stock['sourcePath'],device=stock['sourceDevice'],inode=stock['sourceInode'],uid=st.st_uid,mode=stat.S_IMODE(st.st_mode),nlink=st.st_nlink) + prefixes['stock']={'schema':'bar.prearm-live-prefix-observation/v1','role':'stock','livePin':live,'retainedPin':m.physical(stock['path']),'writer':process,'terminal':False,'observedBytes':st.st_size,'suffixBytes':st.st_size-stock['bytes']};json_new(root/'stock-prefix.json',prefixes['stock']) + horizons={'schema':'bar.prearm-prefix-observations/v2','prefixes':{k:m.physical(root/(k+'-prefix.json'))for k in prefixes},'infolog':infolog};json_new(root/'prefix-evidence.json',horizons) + inputs={k:prefixes[k]['livePin']for k in prefixes} + for label in ['metadata','setup']:inputs[label]=m.physical(root/('authenticated-'+label+'.raw')) + evidence=copy.deepcopy(p['seed']['configurationInputs']);evidence.update(loaded=m.physical(A/'runtime-closure-custody.json'),settings=m.physical(A/'engine-settings-copy.json'),configPin=config_pin,infolog=infolog);evidence['roles']['settings']=m.physical(A/'engine/springsettings.cfg');json_new(root/'native-evidence-inputs.json',evidence);inputs['configuration']=m.physical(root/'native-evidence-inputs.json') + rows=records(prefix_bytes(inputs['raw'],4*1024*1024,deadline),'raw') + contract={'schema':'bar.prearm-sealed-input-contract/v1','runId':cfg['runId'],'source':p['source'],'writer':ready['writer'],'acceptedGeneration':rows[0]['acceptedGeneration'],'factory':metadata['actors']['factory'],'setup':{'seedCount':3,'seedDefinitionId':setup['existingProductionDefinitionId'],'productDefinitionId':setup['productDefinitionId']},'inputs':inputs,'completeRecordEvidence':m.physical(root/'prefix-evidence.json'),'lifecycle':{'startedMonotonicNs':str(started_ns),'transitionDeadlineMonotonicNs':str(int(deadline*1e9)),'totalDeadlineMonotonicNs':str(started_ns+180_000_000_000)},'outputPath':str(root/'seed-qualification.json')} + json_new(root/'seed-input-contract.json',contract);qualification=derive(contract,min(deadline,time.monotonic()+10));ack=json_new(root/'seed-qualification.json',qualification,65536) + for role in prefixes:need(prefix_bytes(inputs[role],4*1024*1024 if role=='raw'else 16*1024*1024,deadline),'final live prefix') + return {'qualification':qualification,'publication':ack,'canonicalPolicyConsumed':True,'contract':m.physical(root/'seed-input-contract.json'),'seedQualificationAccepted':True} diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/stock_config.py b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/stock_config.py new file mode 100644 index 0000000..44e49f5 --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/stock_config.py @@ -0,0 +1,114 @@ +"""Closed stock-smoke config. Templates retain null pins and must fail preflight.""" +import os,re,stat,urllib.parse +from pathlib import Path +from private_io import Refused,SHA,canonical,components,hash_artifact,need,read_bytes,read_json + +SCHEMA='fsbar.barc-stock-native-smoke-supervisor-config/v2' +PROFILE='barc-live-tactical-stock-v1';BRIDGE='barc-stock-queue-reader-v1' +EXPECTED_SOURCE={'fsbarCommit':'fb00d0f77b6c7d0998f85a2039931c0c5eed2472','highbarCommit':'b57f11f290dc3239d894b0285ae272b6b21bc25d'} +ARTIFACT_ROLES=('engine','plugin','stockOverlay','baseGame','map','passiveFixture','nativeHost','receiverArchive','client','guest','codecManifest','helperManifest','fsbarNormalizer','runtimeClosure','normalizerRuntime','captureRuntime','captureSource','runtimeEvidencePolicy','runtimeEvidencePolicyClosure') +NULL_AI_SHA256='cf72efb79a43b8242f0f505915dff5d900cffc721cc8667bb8da0e4aec4eb5ed' +HANDOFF_ROLES=ARTIFACT_ROLES[:12] +COMMAND_ROLES=('host','engine','receiver','browser') +TOP={'schema','runId','packetSha256','source','artifacts','roots','paths','commands','bootstrap','limits','expected'} +RUN=re.compile(r'^[A-Za-z0-9._-]{1,64}$');GIT=re.compile(r'^[0-9a-f]{40}$') + +def exact(value,keys,label):need(isinstance(value,dict) and set(value)==set(keys),f'closed {label}') +def beneath(path,root): + path=canonical(path);root=canonical(root) + try:path.relative_to(root) + except ValueError:raise Refused('path outside admitted root') + return path + +def loopback_endpoint(value,schemes,label): + if schemes is None: + need(isinstance(value,str) and re.fullmatch(r'127\.0\.0\.1:[1-9][0-9]{0,4}',value),f'fixed loopback {label}') + port=int(value.rsplit(':',1)[1]);need(port<=65535,f'fixed loopback {label}');return (None,'127.0.0.1',port) + try: + parsed=urllib.parse.urlsplit(value or '');port=parsed.port + except ValueError as error:raise Refused(f'fixed loopback {label}') from error + need(parsed.scheme in schemes and parsed.hostname=='127.0.0.1' and port and port<=65535 and not parsed.username and not parsed.password and not parsed.fragment,f'fixed loopback {label}') + return (parsed.scheme,parsed.hostname,port) + +def coordinator_endpoint(value): + _,host,port=loopback_endpoint(value['commands']['host'][2],None,'coordinator');return f'{host}:{port}' + +def validate_template(value): + exact(value,TOP,'config');need(value['schema']==SCHEMA and (value['runId'] is None or RUN.fullmatch(value['runId'])),'config identity') + exact(value['source'],{'fsbarCommit','highbarCommit'},'source') + exact(value['artifacts'],ARTIFACT_ROLES,'artifact roles') + for role,item in value['artifacts'].items(): + exact(item,{'path','sha256'},f'artifact {role}');need(item['path'] is None or isinstance(item['path'],str),'artifact path');need(item['sha256'] is None or SHA.fullmatch(item['sha256']),'artifact hash') + exact(value['roots'],{'packetRoot','attemptRoot'},'roots');exact(value['paths'],{'codecDirectory','receiverRoot','hostTrace','stockTrace','capture','output'},'paths') + exact(value['commands'],COMMAND_ROLES,'commands') + for argv in value['commands'].values():need(argv is None or isinstance(argv,list) and 1<=len(argv)<=32 and all(isinstance(x,str) and 0=0) + current=p.read_bytes();self.assertTrue(current.startswith(previous));self.assertTrue(current.endswith(b'\n'));self.assertGreater(len(current),len(previous));previous=current + finally:libc.fclose(stream) + def test_error_default_does_not_flush_info_but_error_flushes(self): + with tempfile.TemporaryDirectory() as td: + p=pathlib.Path(td)/'infolog.txt';stream=libc.fopen(str(p).encode(),b'wb');buffer=ctypes.create_string_buffer(8192) + try: + self.assertEqual(libc.setvbuf(stream,buffer,0,8192),0) + self.write(stream,b'info',30>=50);self.assertEqual(p.read_bytes(),b'') + self.write(stream,b'error',50>=50);self.assertTrue(p.read_bytes().endswith(b'error\n')) + finally:libc.fclose(stream) + +if __name__=='__main__':unittest.main() diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/test_growing_log.py b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/test_growing_log.py new file mode 100644 index 0000000..7c2e39e --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/helper/test_growing_log.py @@ -0,0 +1,1016 @@ +import base64,fnmatch,hashlib,json,os,pathlib,shutil,stat,subprocess,sys,tempfile,threading,time,unittest +from unittest import mock +import growing_log +from growing_log import GrowingLog +from private_io import Refused,digest +from runtime_identity import acquire_data_root_evidence,start_ticks + +POLICY_BUILD=pathlib.Path(os.environ['BAR_RUNTIME_EVIDENCE_BUILD']) +POLICY_SOURCE=pathlib.Path(os.environ['BAR_RUNTIME_EVIDENCE_SOURCE']) +WRITER="""import os,sys +os.umask(0o077) +p=sys.argv[1] +f=open(p,'w+b',buffering=0) +f.write(base64.b64decode(sys.argv[2])) +for command in sys.stdin.buffer: + command=command.rstrip(b'\\n') + if command.startswith(b'append:'):f.seek(0,2);f.write(command[7:]+b'\\n') + elif command.startswith(b'appendraw:'):f.seek(0,2);f.write(base64.b64decode(command[10:])) + elif command==b'rewrite':f.seek(0);f.write(b'X');f.flush() + elif command==b'truncate':f.truncate(1);f.flush() + elif command==b'close':f.close();break +""" + +def _rp2_canonical_trace(source,timeline,name,directory): + """Concrete action parameters from actual framed F# and held-FD effects. + The expected core states are independently checked by the F# reader. The + canonical Quint actions check every projected field at every ordered step. + This fixture code grants no runtime authority and is never imported by the + production helper. + """ + empty_hash=hashlib.sha256(b'').hexdigest() + empty_prefix={'rawBytes':0,'rawSha256':empty_hash,'completeBytes':0,'completeSha256':empty_hash,'tailBytes':0,'tailSha256':empty_hash,'completeRecords':0,'tailClass':'empty'} + no_consumption={'boundary':'none','revision':0,'rawBytes':0,'rawSha256':'','readStartMicroseconds':0,'readEndMicroseconds':0,'linearizedMicroseconds':0,'releasedMicroseconds':0,'deadlineMicroseconds':0} + core={'phase':'empty','identity':None,'observedRevision':0,'validatedRevision':0,'validatedBoundary':None,'consumedRevision':0,'consumedBoundary':None,'bytes':0,'sha256':'','stickyInvalid':False,'reason':None,'prefix':empty_prefix,'candidatePrefix':None,'consumedPrefix':None,'consumption':None,'attemptId':0,'probeCount':0,'evaluationCount':0,'deadlineMicroseconds':0} + mech={'rootsValid':False,'prefixIntact':False,'writerPresent':False,'policyPhase':'none','policyInvocation':'none','policyClosure':'none','settlement':{'phase':'idle','boundary':'none','remaining':32,'deadlineAvailable':True},'producerBytes':0,'producerContradiction':False,'finalPhase':'none','finalSize':0,'readStart':0,'readEnd':0,'linearized':0} + steps=[] + def literal(value): + if type(value) is bool:return 'true' if value else 'false' + if type(value) is int:return str(value) + if type(value) is str:return json.dumps(value) + if type(value) is dict:return '{ '+', '.join(key+': '+literal(item) for key,item in value.items())+' }' + raise AssertionError('closed Quint literal') + def project(): + present=core['identity'] is not None + return {'phase':core['phase'],'generation':1 if present else 0,'observedRevision':core['observedRevision'],'validatedRevision':core['validatedRevision'],'intendedBoundary':core['validatedBoundary'] or 'none','consumedRevision':core['consumedRevision'],'consumedBoundary':core['consumedBoundary'] or 'none','producerMatches':present,'logMatches':present,'sourceMatches':present,'rootsValid':mech['rootsValid'],'prefixIntact':mech['prefixIntact'],'writerPresent':mech['writerPresent'],'authorityActive':False,'stickyInvalid':core['stickyInvalid'],'policyPhase':mech['policyPhase'],'policyInvocation':mech['policyInvocation'],'policyClosure':mech['policyClosure'],'settlement':dict(mech['settlement']),'prefix':core['prefix'],'consumedPrefix':core['consumedPrefix'] or empty_prefix,'consumption':core['consumption'] or no_consumption,'producerBytes':mech['producerBytes'],'producerContradiction':mech['producerContradiction'],'candidateBytes':(core['candidatePrefix'] or {}).get('rawBytes',0),'candidateSha256':(core['candidatePrefix'] or {}).get('rawSha256',''),'finalPhase':mech['finalPhase'],'finalSize':mech['finalSize'],'readStart':mech['readStart'],'readEnd':mech['readEnd'],'linearized':mech['linearized'],'evaluations':core['evaluationCount'],'attemptId':core['attemptId'],'probeCount':core['probeCount'],'deadlineMicroseconds':core['deadlineMicroseconds']} + def emit(action):steps.append({'action':action,'state':json.loads(json.dumps(project()))}) + first=next(row for row in timeline if row['kind']=='policy') + identity={key:first['expected'][key] for key in ('runId','sourceSetSha256','apphostSha256','closureSha256')} + identity.update({key:first['observation'][key] for key in ('pid','startTicks','uid','device','inode','path')}) + core={**core,'phase':'acquired','identity':identity};mech['writerPresent']=True;emit('acquire') + for row in timeline: + kind=row['kind'] + if kind=='boundary': + if mech['settlement']['phase']!='idle':raise AssertionError('boundary without terminal') + mech['settlement']={'phase':'probing','boundary':row['boundary'],'remaining':32,'deadlineAvailable':True};mech.update(policyPhase='none',policyInvocation='none',policyClosure='none',finalPhase='none');emit('beginObservedBoundary('+literal(row['boundary'])+')') + elif kind=='probe': + if mech['settlement']['phase']=='candidate': + mech['settlement']['phase']='probing';mech.update(policyPhase='none',policyInvocation='none',policyClosure='none',finalPhase='none') + core={**core,'phase':'sampled','candidatePrefix':None};emit('retryObserved('+literal(mech['settlement']['boundary'])+')') + mech['settlement']['remaining']=row['remaining'] + if row['event']=='raw':mech['settlement']['phase']='candidate';emit('completeRecordCandidate') + else:emit('incompleteRecordProbe') + elif kind=='policy': + inv=row['invocation'];args=literal(inv['invocationId'])+','+literal(inv['closureSha256']) + mech.update(policyPhase='started',policyInvocation=inv['invocationId'],policyClosure=inv['closureSha256']);emit('beginPolicy('+args+')') + mech['policyPhase']='ready';emit('readyPolicy('+args+')') + after=row['afterState'];before=core;core=json.loads(json.dumps(after)) + if row['status']=='accepted':core.update(phase='sampled',validatedRevision=before['validatedRevision'],validatedBoundary=before['validatedBoundary'],candidatePrefix=None) + mech.update(rootsValid=row['status']=='accepted',prefixIntact=True,producerBytes=max(mech['producerBytes'],after['bytes'])) + extras=str(after['attemptId'])+','+str(after['probeCount'])+','+str(after['evaluationCount'])+','+str(after['deadlineMicroseconds']) + params=literal(after['prefix'])+','+extras + emit(('rejectRaw('+params+')') if row['status']=='refused' else 'observeRaw('+literal(after['prefix'])+','+literal(row['status']=='accepted')+','+extras+')') + if row['status']=='accepted': + core=after;emit('validate('+literal(row['boundary'])+')') + mech['policyPhase']='evaluated';emit('evaluatedPolicy('+args+')') + mech['policyPhase']='completed';emit('completePolicy('+args+')') + elif kind=='final-observation': + mech.update(finalPhase='reading',readStart=row['readStartMicroseconds']);emit('beginFinalRead('+str(mech['readStart'])+')') + mech.update(finalPhase='read',readEnd=row['readEndMicroseconds']);emit('endFinalRead('+str(mech['readEnd'])+')') + if row['rawBytes']>mech['producerBytes']: + mech['producerBytes']=row['rawBytes'];emit('producerSize('+str(row['rawBytes'])+',false)') + mech.update(finalSize=row['rawBytes'],linearized=row['linearizedMicroseconds'],finalPhase='unchanged' if row['rawBytes']==core['bytes'] else 'grown') + emit('observeFinalSize('+str(row['rawBytes'])+','+str(row['linearizedMicroseconds'])+')') + elif kind=='growth': + core=row['afterState'];mech.update(rootsValid=False,policyPhase='none',policyInvocation='none',policyClosure='none',finalPhase='none');emit('observedGrowth') + elif kind=='producer-after-L': + mech.update(producerBytes=row['rawBytes'],producerContradiction=row['contradictory']);emit('producerSize('+str(row['rawBytes'])+','+literal(row['contradictory'])+')') + elif kind=='consume': + core=row['afterState'];mech['finalPhase']='released';obs=row['observation'];emit('consumeAt('+literal(obs['boundary'])+','+str(obs['releasedMicroseconds'])+','+str(obs['deadlineMicroseconds'])+')') + elif kind=='revoke': + core=row['afterState'] + if row['unavailable']:mech['writerPresent']=False + emit('revokeObserved('+literal(row['unavailable'])+')') + elif kind=='terminal': + if row['outcome']=='accepted': + mech['settlement'].update(phase='idle',boundary='none');emit('completeBoundary('+literal(core['consumedBoundary'])+')') + elif row['check']=='infolog-record-settlement-exhausted': + mech['settlement']['phase']='refused';emit('recordSettlementExhausted') + else: + # Policy/root refusal and evaluation exhaustion have already + # produced the concrete sticky revocation edge. + if not core['stickyInvalid']:raise AssertionError('unrevoked terminal') + else:raise AssertionError('closed RP2 effect') + # Dynamic constants contain public fixture values only. Import exactly the + # production canonical model; no alternate transition definitions. + directory.mkdir(parents=True,exist_ok=True) + shutil.copyfile(source/'GrowingLogEvidence.qnt',directory/'GrowingLogEvidence.qnt') + qnt=directory/(name+'.qnt') + body='module RP2_actual_test {\n import GrowingLogEvidence.* from "./GrowingLogEvidence"\n run actual = init' + for step in steps:body+='\n .then('+step['action']+').expect(evidence == '+literal(step['state'])+')' + qnt.write_text(body+'\n}\n') + trace=directory/(name+'.itf.json') + run=subprocess.run(['quint','test',str(qnt),'--backend=typescript','--main','RP2_actual_test','--match','^actual$','--out-itf',str(directory/(name+'_{test}_{seq}.itf.json')),'--seed','424242','--max-samples','1'],stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True,timeout=30) + (directory/(name+'.log')).write_text(run.stdout) + if run.returncode:raise AssertionError('actual full-state canonical correspondence: '+name+'\n'+run.stdout) + generated=directory/(name+'_actual_0.itf.json') + itf=json.loads(generated.read_text()) + # Apply the same volatile-metadata normalization as the owning predecessor + # gate. Preserve every generated variable and ordered state unchanged. + itf['#meta'].pop('description',None);itf['#meta'].pop('timestamp',None) + itf['#meta']['status']='ok' + trace.write_text(json.dumps(itf,separators=(',',':'),sort_keys=True)+'\n') + return {'steps':steps,'trace':trace.name} + +class GrowingLogTests(unittest.TestCase): + def test_policy_diagnostic_decoder_is_total_closed_and_never_echoes(self): + valid=b'{"schema":"fsbar.barc-runtime-evidence-failure-observation/v1","checkpoint":"request-evaluation","kind":"refused"}\n' + self.assertEqual(growing_log._policy_observation(valid)['checkpoint'],'request-evaluation') + invalid=(valid.replace(b'"refused"',b'"PRIVATE_SENTINEL"'),valid+valid,valid.replace(b'"checkpoint"',b'"checkpoint":[],"other"'),b'['+b'x'*2049+b']') + for raw in invalid:self.assertIsNone(growing_log._policy_observation(raw)) + def test_policy_transport_retains_only_one_closed_diagnostic_frame(self): + policy=self.root/'diagnostic-policy' + policy.write_text('''#!/usr/bin/python3 +import hashlib,json,os,sys +args=dict(zip(sys.argv[1::2],sys.argv[2::2]));inv=args["--invocation-id"];closure=args["--closure-sha256"] +ticks=open(f"/proc/{os.getpid()}/stat").read().split()[21] +print(json.dumps({"schema":"fsbar.barc-runtime-evidence-policy-ready/v2","invocationId":inv,"closureSha256":closure,"pid":os.getpid(),"startTicks":ticks,"uid":os.geteuid(),"phase":"ready"},separators=(",",":")),flush=True) +sys.stdin.buffer.read() +print(json.dumps({"schema":"fsbar.barc-runtime-evidence-policy-completed/v2","invocationId":inv,"closureSha256":closure,"pid":os.getpid(),"startTicks":ticks,"uid":os.geteuid(),"phase":"completed","result":{}},separators=(",",":")),flush=True) +sys.stderr.write('{"schema":"fsbar.barc-runtime-evidence-failure-observation/v1","checkpoint":"request-evaluation","kind":"refused"}\\n');sys.stderr.flush() +''');os.chmod(policy,0o700) + runner=object.__new__(GrowingLog);runner.last_policy_observation=None + self.assertEqual(runner._run_policy(str(policy),'/unused','a'*64,b'{}',time.monotonic()+2),b'{}\n') + self.assertEqual(runner.last_policy_observation['checkpoint'],'request-evaluation') + policy.write_text(policy.read_text().replace("sys.stderr.write('{\"schema\"", "sys.stderr.write('PRIVATE_SENTINEL\\n'+ '{\"schema\""));os.chmod(policy,0o700) + runner.last_policy_observation=None + self.assertEqual(runner._run_policy(str(policy),'/unused','a'*64,b'{}',time.monotonic()+2),b'{}\n') + self.assertIsNone(runner.last_policy_observation) + def test_actual_compiled_failure_projection_uses_production_transport(self): + config=self.config();expected={'configSha256':'d'*64,'sourceSetSha256':growing_log._source_set(config),'policySha256':config['artifacts']['runtimeEvidencePolicy']['sha256'],'closureSha256':self.closure_sha} + observation={'schema':'fsbar.barc-stock-failure-observation/v1',**expected,'scope':'post-handoff-pre-browser','check':'policy-result-join','outcome':'policy-nonaccepted','policyObservation':{'schema':'fsbar.barc-runtime-evidence-failure-observation/v1','checkpoint':'request-evaluation','kind':'refused'}} + observation_raw=(json.dumps(observation,separators=(',',':'))+'\n').encode() + operation_raw=b'{"schema":"fsbar.barc-stock-operation-result/v1","status":"failed","category":"refused"}\n' + request={'schema':'fsbar.barc-stock-failure-projection/v1','expected':expected,'observation':observation,'observationSha256':hashlib.sha256(observation_raw).hexdigest(),'operationResultBase64':base64.b64encode(operation_raw).decode(),'operationResultSha256':hashlib.sha256(operation_raw).hexdigest()} + runner=object.__new__(GrowingLog);runner.last_policy_observation=None + encoded=json.dumps(request,separators=(',',':')).encode();projected=json.loads(runner._run_policy(str(self.policy),str(self.closure),self.closure_sha,encoded,time.monotonic()+5)) + self.assertEqual((projected['status'],projected['check'],projected['nativeAcceptance']),('observed-failure','policy-result-join',False)) + request['expected']['sourceSetSha256']='f'*64 + unavailable=json.loads(runner._run_policy(str(self.policy),str(self.closure),self.closure_sha,json.dumps(request,separators=(',',':')).encode(),time.monotonic()+5)) + self.assertEqual(unavailable['status'],'diagnostic-unavailable');self.assertNotIn('PRIVATE_SENTINEL',json.dumps(unavailable)) + request['expected']['sourceSetSha256']=expected['sourceSetSha256'];request['operationResultSha256']='0'*64 + false_digest=json.loads(runner._run_policy(str(self.policy),str(self.closure),self.closure_sha,json.dumps(request,separators=(',',':')).encode(),time.monotonic()+5)) + self.assertEqual(false_digest['status'],'diagnostic-unavailable') + def test_later_final_refresh_failure_drops_noncausal_pending_observation(self): + self.spawn(self.text().replace(b'[DataDirLocater::Check] Isolation Mode!\n',b''));original_run=self.handle._run_policy;original_custody=self.handle._custody;after=[False] + def policy(*args): + output=original_run(*args);after[0]=True;return output + def custody(*args,**kwargs): + if after[0]:raise OSError('PRIVATE_SENTINEL_FINAL_REFRESH') + return original_custody(*args,**kwargs) + self.handle._run_policy=policy;self.handle._custody=custody + with self.assertRaises(OSError) as caught:self.consume('browser') + fact=caught.exception._barc_failure_observation + self.assertEqual((fact['check'],fact['outcome'],fact['policyObservation']),('infolog-final-refresh','os-unavailable',None)) + def test_acquisition_close_failure_preserves_writer_refusal(self): + self.spawn();real_close=os.close;closed=[] + def close_failure(fd):closed.append(fd);raise OSError('PRIVATE_SENTINEL_CLOSE') + with mock.patch.object(GrowingLog,'_writer',side_effect=growing_log.mark_failure(Refused('PRIVATE_SENTINEL_WRITER'),'infolog-writer-match')),mock.patch.object(growing_log.os,'close',side_effect=close_failure): + with self.assertRaises(Refused) as caught:GrowingLog.acquire(self.log,self.handle.process) + for fd in closed:real_close(fd) + fact=caught.exception._barc_failure_observation + self.assertEqual((fact['check'],fact['outcome']),('infolog-writer-match','refused'));self.assertTrue(caught.exception._barc_cleanup_unknown) + def test_three_accepted_growth_samples_report_mechanical_exhaustion(self): + self.spawn();original=self.handle._run_policy;calls=[0] + def grow(*args): + output=original(*args);calls[0]+=1;self.command('append:benign complete record');return output + self.handle._run_policy=grow + with self.assertRaises(Refused) as caught:self.consume('browser') + fact=caught.exception._barc_failure_observation + self.assertEqual(calls[0],3);self.assertEqual((fact['check'],fact['outcome'],fact['policyObservation']),('policy-sample-exhausted','refused',None)) + counters=caught.exception._barc_infolog_mechanical_observation + self.assertEqual((counters['terminalCause'],counters['evaluationsBegun'],counters['evaluationsCompleted'],counters['growthAfterEvaluation']),('policy-evaluation-growth-cap',3,3,3)) + self.assertEqual(counters['probesCompleted'],3) + def test_full_buffered_writer_growth_after_real_policy_exhausts_three_evaluations(self): + self.child=subprocess.Popen([sys.executable,str(pathlib.Path(__file__).with_name('buffered_writer.py')),str(self.log),base64.b64encode(self.text()).decode()],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True,start_new_session=True) + def ack(expected): + need=__import__('select').select([self.child.stdout],[],[],2)[0] + self.assertTrue(need,'public buffered writer ack deadline') + self.assertEqual(self.child.stdout.readline().strip(),expected) + ack('ready') + self.handle=acquire_data_root_evidence(self.log,{'pid':self.child.pid,'startTicks':start_ticks(self.child.pid),'uid':os.geteuid()}) + original=self.handle._run_policy + def grow(*args): + result=original(*args) + self.child.stdin.write('growflush\n');self.child.stdin.flush();ack('done') + return result + self.handle._run_policy=grow + try: + with self.assertRaises(Refused) as caught:self.consume('browser') + counters=caught.exception._barc_infolog_mechanical_observation + self.assertEqual((counters['terminalCause'],counters['evaluationsBegun'],counters['evaluationsCompleted'],counters['growthAfterEvaluation']),('policy-evaluation-growth-cap',3,3,3)) + self.assertEqual(counters['probesCompleted'],3) + self.assertEqual(counters['terminalCheck'],'policy-sample-exhausted') + self.assertEqual(counters['tailBytes'],0) + self.assertTrue(caught.exception._barc_infolog_mechanical_retained) + finally: + self.child.stdin.write('close\n');self.child.stdin.flush();self.child.communicate(timeout=2) + def test_genuine_pending_exhaustion_retains_policy_observation(self): + self.spawn(self.text().replace(b'[DataDirLocater::Check] Isolation Mode!\n',b'')) + with self.assertRaises(Refused) as caught:self.consume('browser') + fact=caught.exception._barc_failure_observation + self.assertEqual((fact['check'],fact['outcome']),('policy-sample-exhausted','policy-nonaccepted'));self.assertEqual(fact['policyObservation']['kind'],'pending') + def test_partial_record_settles_before_first_policy_revision(self): + initial=self.text()[:-1];self.spawn(initial);requests=[];actual=self.handle._run_policy;actual_sample=self.handle._sample;probes=[0] + def inspect(*args): + requests.append(json.loads(args[3])['observation']['revision']);return actual(*args) + self.handle._run_policy=inspect + def sample(deadline): + value=actual_sample(deadline);probes[0]+=1 + if probes[0]==1:self.command('appendraw:'+base64.b64encode(b'\n').decode()) + return value + self.handle._sample=sample;result=self.consume('browser') + self.assertEqual((requests,result['revision']),([1],1)) + self.assertGreaterEqual(len(self.handle.settlement_transcript),2) + self.assertEqual(self.handle.settlement_transcript[-1]['event'],'complete') + output=os.environ.get('BAR_SETTLEMENT_TRANSCRIPT') + if output:pathlib.Path(output).write_text(json.dumps(self.handle.settlement_transcript,separators=(',',':'))+'\n') + def test_continuous_partial_tail_exhausts_without_policy_evaluation(self): + self.spawn(self.text()[:-1]);calls=[0] + self.handle._run_policy=lambda *args:(calls.__setitem__(0,calls[0]+1),b'')[1] + with mock.patch.object(growing_log,'MAX_SETTLEMENT_PROBES',3): + with self.assertRaises(Refused) as caught:self.consume('browser') + fact=caught.exception._barc_failure_observation + self.assertEqual((calls[0],self.handle.revision,fact['check'],fact['outcome'],fact['policyObservation']),(0,0,'infolog-record-settlement-exhausted','refused',None)) + def test_settlement_deadline_prevents_policy_launch(self): + self.spawn(self.text()[:-1]);calls=[0];self.handle._run_policy=lambda *args:calls.__setitem__(0,calls[0]+1) + with self.assertRaises(Refused) as caught:self.handle._settle(time.monotonic(),0) + fact=caught.exception._barc_failure_observation + self.assertEqual((calls[0],self.handle.revision,fact['check'],fact['outcome'],fact['policyObservation']),(0,0,'infolog-record-settlement-deadline','deadline',None)) + def test_partial_prefix_rewrite_is_sticky_across_probes(self): + self.spawn(self.text()[:-1]);real_sleep=time.sleep;changed=[False] + def rewrite(_): + if not changed[0]: + changed[0]=True;self.child.stdin.write(b'rewrite\n');self.child.stdin.flush();real_sleep(.03) + else:real_sleep(0) + with mock.patch.object(growing_log.time,'sleep',side_effect=rewrite): + with self.assertRaises(Refused):self.consume('browser') + self.assertTrue(self.handle.revoked);self.assertEqual(self.handle.revision,0) + def test_exact_read_refuses_after_finite_short_read_budget(self): + self.spawn(self.text());actual=os.pread + def short(fd,count,offset):return actual(fd,min(count,1),offset) + with mock.patch.object(growing_log,'MAX_PREAD_CALLS',2),mock.patch.object(growing_log.os,'pread',side_effect=short): + with self.assertRaises(Refused) as caught:self.consume('browser') + self.assertEqual(caught.exception._barc_failure_observation['check'],'infolog-record-settlement-exhausted') + def test_deadline_expiry_inside_writer_census_stops_before_fd_walk_and_policy(self): + self.spawn(self.text());base=time.monotonic();expired=[False];fd_stats=[0];actual_listdir=os.listdir;actual_stat=os.stat;policy=[0] + def listdir(path): + value=actual_listdir(path) + if path==f'/proc/{self.child.pid}/fd':expired[0]=True + return value + def guarded_stat(path,*args,**kwargs): + if expired[0] and str(path).startswith(f'/proc/{self.child.pid}/fd/'): + fd_stats[0]+=1 + return actual_stat(path,*args,**kwargs) + self.handle._run_policy=lambda *args:policy.__setitem__(0,policy[0]+1) + with mock.patch.object(growing_log.time,'monotonic',side_effect=lambda:base+2 if expired[0] else base),mock.patch.object(growing_log.os,'listdir',side_effect=listdir),mock.patch.object(growing_log.os,'stat',side_effect=guarded_stat): + with self.assertRaises(Refused) as caught:self.handle._settle(base+1,0) + self.assertEqual((fd_stats[0],policy[0],self.handle.revision,caught.exception._barc_failure_observation['check']),(0,0,0,'infolog-record-settlement-deadline')) + def test_deadline_at_candidate_promotion_does_not_advance_revision_or_launch_policy(self): + self.spawn(self.text());base=time.monotonic();expired=[False];actual=self.handle._sample;policy=[0] + def expire_after_sample(deadline): + value=actual(deadline);expired[0]=True;return value + self.handle._sample=expire_after_sample;self.handle._run_policy=lambda *args:policy.__setitem__(0,policy[0]+1) + with mock.patch.object(growing_log.time,'monotonic',side_effect=lambda:base+2 if expired[0] else base): + with self.assertRaises(Refused) as caught:self.handle._settle(base+1,0) + self.assertEqual((policy[0],self.handle.revision,caught.exception._barc_failure_observation['check']),(0,0,'infolog-record-settlement-deadline')) + def test_settlement_failure_after_pending_drops_policy_observation(self): + self.spawn(self.text().replace(b'[DataDirLocater::Check] Isolation Mode!\n',b''));actual=self.handle._run_policy;calls=[0] + def append_partial(*args): + output=actual(*args);calls[0]+=1 + if calls[0]==1:self.command('appendraw:'+base64.b64encode(b'partial').decode()) + return output + self.handle._run_policy=append_partial + with mock.patch.object(growing_log,'MAX_SETTLEMENT_PROBES',4): + with self.assertRaises(Refused) as caught:self.consume('browser') + fact=caught.exception._barc_failure_observation + self.assertEqual((calls[0],fact['check'],fact['outcome'],fact['policyObservation']),(1,'infolog-record-settlement-exhausted','refused',None)) + @classmethod + def setUpClass(cls): + cls.fixture=tempfile.TemporaryDirectory(prefix='bar-policy-closure-fixture-');root=pathlib.Path(cls.fixture.name) + managed=root/'managed';provenance=root/'provenance';managed.mkdir();provenance.mkdir() + for name in ('RuntimeEvidence','RuntimeEvidence.dll','RuntimeEvidence.deps.json','RuntimeEvidence.runtimeconfig.json','FSharp.Core.dll'): + shutil.copyfile(POLICY_BUILD/name,managed/name);os.chmod(managed/name,0o555 if name=='RuntimeEvidence' else 0o444) + shutil.copyfile(POLICY_BUILD/'RuntimeEvidence.pdb',provenance/'RuntimeEvidence.pdb') + system_dotnet=pathlib.Path(os.environ.get('DOTNET_ROOT','/usr/share/dotnet')).resolve() + version=subprocess.check_output(['dotnet','--list-runtimes'],text=True).splitlines()[-1].split()[1] + system_framework=system_dotnet/'shared/Microsoft.NETCore.App'/version + system_fxr=max((system_dotnet/'host/fxr').iterdir(),key=lambda p:tuple(map(int,p.name.split('.')))) + cls.dotnet_root=root/'dotnet';framework=cls.dotnet_root/'shared/Microsoft.NETCore.App'/version;fxr=cls.dotnet_root/'host/fxr'/system_fxr.name + framework.parent.mkdir(parents=True);fxr.parent.mkdir(parents=True) + shutil.copytree(system_framework,framework);shutil.copytree(system_fxr,fxr) + shutil.copyfile(system_dotnet/'dotnet',cls.dotnet_root/'dotnet') + for base,directories,files in os.walk(cls.dotnet_root): + for name in directories:os.chmod(pathlib.Path(base)/name,0o555) + for name in files:os.chmod(pathlib.Path(base)/name,0o555 if '.so' in name else 0o444) + os.chmod(cls.dotnet_root/'dotnet',0o555);os.chmod(cls.dotnet_root,0o555) + if any(path.is_symlink() for path in cls.dotnet_root.rglob('*')):raise AssertionError('private runtime staging retained a link') + def row(path,role=None): + info=path.stat();value={'path':str(path),'bytes':info.st_size,'sha256':hashlib.sha256(path.read_bytes()).hexdigest(),'device':f'{os.major(info.st_dev):x}:{os.minor(info.st_dev):x}','inode':info.st_ino,'ownerUid':info.st_uid,'mode':stat.S_IMODE(info.st_mode),'links':info.st_nlink} + if role:value={'role':role,**value} + return value + runtime_paths={path.resolve() for directory in (framework,fxr) for path in directory.rglob('*') if path.is_file()} + candidates=[managed/'RuntimeEvidence',framework/'libcoreclr.so',framework/'libhostpolicy.so',fxr/'libhostfxr.so',*framework.glob('*.so')] + for candidate in candidates: + output=subprocess.run(['ldd',str(candidate)],text=True,stdout=subprocess.PIPE,check=True).stdout + for line in output.splitlines(): + fields=line.replace('=>',' ').split() + runtime_paths.update(pathlib.Path(x).resolve() for x in fields if x.startswith('/') and pathlib.Path(x).is_file()) + dynamic_patterns={'icu':'libicu*.so*','crypto':'libcrypto.so*','ssl':'libssl.so*','brotli':'libbrotli*.so*','zlib':'libz.so*','zstd':'libzstd.so*'} + ldconfig=subprocess.run(['ldconfig','-p'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=True) + if len(ldconfig.stdout)>1024*1024 or len(ldconfig.stderr)>4096:raise AssertionError('bounded loader cache census') + dynamic={name:set() for name in dynamic_patterns} + for line in ldconfig.stdout.decode('utf-8','strict').splitlines(): + if '=>' not in line:continue + raw=line.rsplit('=>',1)[1].strip();path=pathlib.Path(raw) + for name,pattern in dynamic_patterns.items(): + if fnmatch.fnmatchcase(path.name,pattern): + resolved=path.resolve(strict=True) + if resolved.is_file():dynamic[name].add(resolved) + if sum(map(len,dynamic.values()))>128:raise AssertionError('bounded dynamic runtime census') + cls.dynamic_libraries={name:sorted(paths) for name,paths in dynamic.items()} + runtime_paths.update(path for paths in cls.dynamic_libraries.values() for path in paths) + runtime_paths.add(pathlib.Path('/etc/ld.so.cache')) + runtime=[row(path) for path in sorted(runtime_paths)] + roles={'hostfxr':str(fxr/'libhostfxr.so'),'hostpolicy':str(framework/'libhostpolicy.so'),'coreLib':str(framework/'System.Private.CoreLib.dll'),'coreClr':str(framework/'libcoreclr.so'),'jit':str(framework/'libclrjit.so')} + policy_commit=subprocess.check_output(['git','-C',str(POLICY_SOURCE),'rev-parse','HEAD'],text=True).strip();policy_tree=subprocess.check_output(['git','-C',str(POLICY_SOURCE),'rev-parse','HEAD^{tree}'],text=True).strip() + product_commit='2cd9f47dd120d43b6781b6edc7dbb1d571cc51a1';product_tree='4fee295bf14f6b2fe2369085c1e743610709297e' + highbar_commit='54e17084c35b90584f8f84efc468bcc8943e4ec0' + product_roles={'fsbarCommit':product_commit,'highbarCommit':highbar_commit} + product_source_set=hashlib.sha256(json.dumps(product_roles,sort_keys=True,separators=(',',':')).encode()).hexdigest() + source_link=json.dumps({'documents':{'/_/*':f'https://raw.githubusercontent.com/FS-GG/FSBarV2/{policy_commit}/*'}},separators=(',',':')).encode() + (provenance/'RuntimeEvidence.sourcelink.json').write_bytes(source_link) + (provenance/'policy-source.json').write_text(json.dumps({'schema':'fsbar.barc-source-identity/v1','role':'policySourceManifest','commit':policy_commit,'tree':policy_tree},separators=(',',':'))) + (provenance/'product-source.json').write_text(json.dumps({'schema':'fsbar.barc-source-identity/v1','role':'productSourceManifest','commit':product_commit,'tree':product_tree},separators=(',',':'))) + (provenance/'product-role-graph.json').write_text(json.dumps({'schema':'fsbar.barc-product-source-role-graph/v1','roles':product_roles},separators=(',',':'))) + (provenance/'locked-build-inputs.json').write_text(json.dumps({'schema':'fsbar.barc-locked-build-inputs/v1','policyProjectSha256':hashlib.sha256((POLICY_SOURCE/'tests/Broker.NativeProof/RuntimeEvidence/RuntimeEvidence.fsproj').read_bytes()).hexdigest(),'policyLockSha256':hashlib.sha256((POLICY_SOURCE/'tests/Broker.NativeProof/RuntimeEvidence/RuntimeEvidence.packages.lock.json').read_bytes()).hexdigest(),'policyTree':policy_tree},separators=(',',':'))) + (provenance/'toolchain-receipt.json').write_text(json.dumps({'schema':'fsbar.barc-toolchain-receipt/v1','dotnetSdk':subprocess.check_output(['dotnet','--version'],text=True).strip(),'targetFramework':'net10.0'},separators=(',',':'))) + shutil.copyfile(pathlib.Path(__file__).with_name('source-manifest.json'),provenance/'helper-source-manifest.json') + helper_identity=json.loads((provenance/'helper-source-manifest.json').read_bytes()) + managed_rows=[row(managed/name,role) for role,name in [('apphost','RuntimeEvidence'),('managedDll','RuntimeEvidence.dll'),('depsJson','RuntimeEvidence.deps.json'),('runtimeConfigJson','RuntimeEvidence.runtimeconfig.json'),('fsharpCore','FSharp.Core.dll')]] + managed_hashes={item['role']:item['sha256'] for item in managed_rows} + receipt={'schema':'fsbar.barc-runtime-evidence-build-receipt/v2','policyCommit':policy_commit,'policyTree':policy_tree,'productCommit':product_commit,'productTree':product_tree,'helperCommit':helper_identity['publicHead'],'helperTree':helper_identity['publicTree'],'productSourceSetSha256':product_source_set,'productRoleGraphSha256':hashlib.sha256((provenance/'product-role-graph.json').read_bytes()).hexdigest(),'lockedBuildInputsSha256':hashlib.sha256((provenance/'locked-build-inputs.json').read_bytes()).hexdigest(),'toolchainReceiptSha256':hashlib.sha256((provenance/'toolchain-receipt.json').read_bytes()).hexdigest(),'managed':managed_hashes,'pdbSha256':hashlib.sha256((provenance/'RuntimeEvidence.pdb').read_bytes()).hexdigest(),'sourceLinkSha256':hashlib.sha256(source_link).hexdigest(),'helperManifestSha256':hashlib.sha256((provenance/'helper-source-manifest.json').read_bytes()).hexdigest()} + (provenance/'build-receipt.json').write_text(json.dumps(receipt,separators=(',',':'))) + for path in provenance.iterdir():os.chmod(path,0o444) + provenance_rows=[row(provenance/name,role) for role,name in [('pdb','RuntimeEvidence.pdb'),('sourceLink','RuntimeEvidence.sourcelink.json'),('buildReceipt','build-receipt.json'),('helperManifest','helper-source-manifest.json'),('policySourceManifest','policy-source.json'),('productSourceManifest','product-source.json'),('productRoleGraph','product-role-graph.json'),('lockedBuildInputs','locked-build-inputs.json'),('toolchainReceipt','toolchain-receipt.json')]] + source={'policyCommit':policy_commit,'policyTree':policy_tree,'productCommit':product_commit,'productTree':product_tree,'productSourceSetSha256':product_source_set,**{role+'Sha256':next(item['sha256'] for item in provenance_rows if item['role']==role) for role in ('policySourceManifest','productSourceManifest','productRoleGraph','lockedBuildInputs','toolchainReceipt','buildReceipt','pdb','sourceLink','helperManifest')}} + def directory(path): + info=path.stat();names=sorted(p.name for p in path.iterdir());return {'path':str(path),'ownerUid':info.st_uid,'mode':stat.S_IMODE(info.st_mode),'entriesSha256':hashlib.sha256('\n'.join(names).encode()).hexdigest()} + os.chmod(managed,0o555);os.chmod(provenance,0o555) + search_paths={managed,provenance,framework,fxr,framework.parent,fxr.parent,*[path.parent for path in runtime_paths]} + search=[directory(path) for path in sorted(search_paths)] + value={'schema':'fsbar.barc-runtime-evidence-policy-closure/v3','custodyRoot':str(root),'managedRoot':str(managed),'provenanceRoot':str(provenance),'runtimeRoots':[str(framework),str(fxr)],'searchLayout':search,'managed':managed_rows,'provenance':provenance_rows,'runtime':runtime,'runtimeRoles':roles,'source':source,'custody':{'ownerUid':os.geteuid(),'executablePath':str(managed/'RuntimeEvidence'),'fixedArgv':[str(managed/'RuntimeEvidence'),'--closure-manifest','--closure-sha256','--invocation-id']}} + cls.closure=root/'closure.json';cls.closure.write_text(json.dumps(value,separators=(',',':')));os.chmod(cls.closure,0o600) + cls.policy=managed/'RuntimeEvidence';cls.closure_sha=hashlib.sha256(cls.closure.read_bytes()).hexdigest() + @classmethod + def tearDownClass(cls): + for path in pathlib.Path(cls.fixture.name).rglob('*'): + try:os.chmod(path,0o700 if path.is_dir() else 0o600) + except FileNotFoundError:pass + cls.fixture.cleanup() + def setUp(self): + self.temp=tempfile.TemporaryDirectory(prefix='bar-growing-log-');self.root=pathlib.Path(self.temp.name);os.chmod(self.root,0o700) + self.write=self.root/'engine';self.data=self.root/'runtime-data';self.write.mkdir(mode=0o700);self.data.mkdir(mode=0o700);self.log=self.write/'infolog.txt';self.child=None;self.handle=None + real_popen=subprocess.Popen + def private_runtime_popen(argv,*args,**kwargs): + if argv and pathlib.Path(argv[0])==self.policy: + env=dict(kwargs.get('env') or {});env['DOTNET_ROOT']=str(self.dotnet_root);env['DOTNET_ROOT_X64']=str(self.dotnet_root);env['DOTNET_MULTILEVEL_LOOKUP']='0';kwargs['env']=env + return real_popen(argv,*args,**kwargs) + self.popen_patch=mock.patch.object(growing_log.subprocess,'Popen',side_effect=private_runtime_popen);self.popen_patch.start() + def tearDown(self): + self.popen_patch.stop() + if self.handle: + try:self.handle.close() + except OSError:pass + if self.child and self.child.poll() is None: + self.child.kill();self.child.wait() + if self.child and self.child.stdin: + self.child.stdin.close() + self.temp.cleanup() + def text(self): + return (f'[DataDirLocater::Check] Isolation Mode!\n[DataDirLocater::FindWriteableDataDir] using writeable data-directory "{self.write}"\n[DataDirLocater::FilterUsableDataDirs] using read-write data directory: {self.write}/\n[DataDirLocater::FilterUsableDataDirs] using read-only data directory: {self.data}/\n').encode() + def spawn(self,initial=None): + initial=self.text() if initial is None else initial + self.child=subprocess.Popen([sys.executable,'-c','import base64\n'+WRITER,str(self.log),base64.b64encode(initial).decode()],stdin=subprocess.PIPE,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,start_new_session=True) + end=time.time()+3 + while not self.log.exists() and time.time()=phase_end:break + sleep(min(.02,max(0,phase_end-now()))) + if not held_live(owner,errors):break + # Discovery errors survive even if every held PID settled. Never signal unseen IDs. + remaining_held=held_live(owner,errors) + if remaining_held:errors.add('held processes remain after finite cleanup') + return {'status':'Unknown'if errors else 'settled','signals':counts,'ownedLive':len(remaining_held),'remainingHeld':remaining_held,'errors':sorted(errors),'discoveryComplete':not bool(errors),'heldIdentities':list(owner.records.values())} + +def custody_observation(path,root,reason,phase,known_owned,live_owned,lstat=None,error=None): + p=pathlib.Path(path);observation={'schema':'bar.stage-custody-refusal/v1','reason':reason,'absolutePath':str(p),'relativePath':str(p.relative_to(root)),'phase':phase,'observerIdentity':identity(os.getpid()),'knownOwnedProcessIdentities':known_owned,'observedLiveOwnedProcessIdentities':live_owned,'processCausedObjectClaim':False,'observedMonotonicSeconds':clock()} + if lstat is None:observation['lstat']={'status':'Unknown','errorType':type(error).__name__,'errno':getattr(error,'errno',None)} + else: + kind=stat.S_IFMT(lstat.st_mode);types={stat.S_IFREG:'regular',stat.S_IFDIR:'directory',stat.S_IFLNK:'symlink',stat.S_IFSOCK:'socket',stat.S_IFIFO:'fifo',stat.S_IFCHR:'character-device',stat.S_IFBLK:'block-device'} + observation['lstat']={'status':'Observed','type':types.get(kind,'unknown'),'typeBits':kind,'mode':stat.S_IMODE(lstat.st_mode),'rawMode':lstat.st_mode,'device':lstat.st_dev,'inode':lstat.st_ino,'uid':lstat.st_uid,'gid':lstat.st_gid,'nlink':lstat.st_nlink,'bytes':lstat.st_size,'mtimeNs':lstat.st_mtime_ns,'ctimeNs':lstat.st_ctime_ns} + return observation + +def quota(root,maximum,phase=None,known_owned=(),live_owned=(),runtime_socket=None): + total=0;files=0 + for base,dirs,names in os.walk(root,followlinks=False): + for name in dirs+names: + p=pathlib.Path(base)/name + try:st=p.lstat() + except OSError as e:raise CustodyRefused('stage lstat unavailable',custody_observation(p,root,'stage lstat unavailable',phase,known_owned,live_owned,error=e)) from e + reason=None + if name in dirs: + if stat.S_ISLNK(st.st_mode):reason='stage symlink directory' + elif stat.S_ISSOCK(st.st_mode)and runtime_socket is not None: + runtime_socket.inspect_for_quota(p) + elif not (stat.S_ISREG(st.st_mode) and st.st_uid==os.geteuid() and st.st_nlink==1):reason='stage file custody' + if reason:raise CustodyRefused(reason,custody_observation(p,root,reason,phase,known_owned,live_owned,lstat=st)) + if name in names:files+=1;total+=st.st_size;need(files<=1024 and total<=maximum,'stage output census bound') + return {'files':files,'bytes':total} +def census(root,runtime_socket=None): + result=[] + if pathlib.Path(root).exists(): + for p in sorted(pathlib.Path(root).rglob('*')): + s=p.lstat() + if stat.S_ISSOCK(s.st_mode):need(runtime_socket is not None,'unknown census socket');result.append(runtime_socket.inspect_for_quota(p)) + elif p.is_file():need(not p.is_symlink(),'census symlink');result.append(physical(p)) + return result + diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/native_evidence.py b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/native_evidence.py new file mode 100644 index 0000000..cb362e6 --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/native_evidence.py @@ -0,0 +1,135 @@ +"""Read-only joins over actual custody outputs. No authority or command producer. +New tagged Lua observations are parsed from the policy-authenticated Recoil infolog. +Source tests may supply controlled records; those are never native acceptance. +""" +import hashlib,json,re,base64,os,stat +from qualify_prearm import need,exact,held_bytes,prefix_bytes,parse,sha,dec,live_process,mapped_roles +PREFIX=b'BARC_PREARM_V1 ' +FIELDS={ + 'content':{'path','bytes','sha512'}, + 'archives':{'map','game','map_sha512','game_sha512'}, + 'ai':{'team','id','name','host','short','version','options'}, + 'config':{'key','type','value'}, + 'vfs':{'path','archive','absolute'}, + 'actors':{'team','enemy','factory','builder','target','seed','product'}, + 'empty':{'factory','observed_frame'}, + 'order':{'factory','ordinal','definition','count','options'}, + 'queue':{'factory','ordinal','definition','tag','options'}, + 'complete':{'factory','count','empty_frame'}, + 'refused':{'reason'}, +} +def text(value): + if value=='-':return '' + need(type(value)is str and re.fullmatch(r'(?:[0-9a-f]{2}){1,4096}',value),'lua-hex-text');return bytes.fromhex(value).decode('utf8') +def observations(raw,nonce): + need(type(raw)is bytes and len(raw)<=10*1024*1024,'infolog-bound');out=[] + for line in raw.splitlines(): + at=line.find(PREFIX) + if at<0:continue + line=line[at+len(PREFIX):];need(len(line)<=16384 and b'\0'not in line,'lua-line-bound') + fields={} + for item in line.decode('ascii').split(' '): + need(item.count('=')==1,'lua-token');key,value=item.split('=');need(key not in fields and value,'lua-duplicate-or-empty');fields[key]=value + kind=fields.get('kind');need(kind in FIELDS,'lua-kind');exact(fields,{'kind','nonce','frame'}|FIELDS[kind],'lua-'+kind) + need(fields['nonce']==nonce and re.fullmatch('[A-Za-z0-9._-]{1,64}',nonce),'lua-nonce');need(dec(fields['frame'])<=2**32-1,'lua-frame') + need(kind!='refused','lua-runtime-refusal');out.append(fields) + need(0=len(raw) and receipt['suffixBytes']==receipt['observedBytes']-len(raw),'prefix-byte-observation') + # Authenticate an actual writable O_APPEND FD in the exact live writer generation. + live_process(receipt['writer']);writable=False + names=os.listdir('/proc/'+str(receipt['writer']['pid'])+'/fd');need(len(names)<=256,'writer-fd-bound') + for name in names: + fdpath='/proc/'+str(receipt['writer']['pid'])+'/fd/'+name + try: + st=os.stat(fdpath);info=open('/proc/'+str(receipt['writer']['pid'])+'/fdinfo/'+name).read(4096);m=re.search(r'^flags:\s+([0-7]+)$',info,re.M) + except FileNotFoundError:continue + if (st.st_dev,st.st_ino)==(receipt['livePin']['device'],receipt['livePin']['inode']) and m: + flags=int(m[1],8);writable=writable or bool(flags&os.O_APPEND and flags&os.O_ACCMODE in [os.O_WRONLY,os.O_RDWR]) + need(writable,'actual-prefix-writable-append-fd');live_process(receipt['writer']) + infolog(value['infolog'],contract,deadline) + +def configuration(value,contract,deadline): + exact(value,['schema','source','roles','loaded','settings','configPin','infolog','review','archiveInventory','producerSource'],'native-evidence') + need(value['schema']=='bar.prearm-native-evidence-inputs/v2' and value['source']==contract['source'],'native-evidence-source') + roles=value['roles'];exact(roles,['engine','plugin','passivePlugin','fixture','observer','gameInventory','mapInventory','settings','setupScript','nullAiSource'],'native-roles') + for pin in roles.values():held_bytes(pin,128*1024*1024,deadline) + raw,config=infolog(value['infolog'],contract,deadline);need(value['configPin']==value['infolog']['configPin'],'configuration-policy-join') + loaded=parse(held_bytes(value['loaded'],1024*1024,deadline));exact(loaded,['schema','status','nativeAcceptance','beforeBrowserEffect','configSha256','sourceSetSha256','process','runtimeClosureSha256','rawMapsSha256','mapsBytes','executableMapCount','uniqueExecutableFiles','required','mappings','observationBeginMonotonicNs','observationEndMonotonicNs'],'loaded') + need(loaded['schema']=='fsbar.barc-stock-loaded-custody/v1' and loaded['status']=='verified-snapshot' and loaded['nativeAcceptance']is False and loaded['beforeBrowserEffect']is True and loaded['configSha256']==value['configPin']['sha256'] and loaded['sourceSetSha256']==sha(json.dumps(contract['source'],sort_keys=True,separators=(',',':')).encode()) and loaded['runtimeClosureSha256']==config['artifacts']['runtimeClosure']['sha256'],'actual-loaded-inputs') + policy_result=parse(held_bytes(value['infolog']['result'],65536,deadline));need(all(policy_result['state']['identity'][k]==loaded['process'][k]for k in ['pid','startTicks','uid']),'actual-infolog-engine-generation') + for role in ['engine','plugin','passivePlugin']: + need(any(x['path']==roles[role]['path'] and x['sha256']==roles[role]['sha256'] and 'x'in x['permissions'] and int(x['mappedInode'])==roles[role]['inode'] and os.makedev(*[int(part,16) for part in x['mappedDevice'].split(':')])==roles[role]['device'] for x in loaded['mappings']),'actual-mapped-'+role) + if role!='passivePlugin':need(loaded['required'][role]=={k:roles[role][k]for k in ['path','sha256']},'required-map-join') + settings=parse(held_bytes(value['settings'],65536,deadline));exact(settings,['schema','seedPath','seedSha256','outputPath','copiedSha256','actualEngineArgv','originalEngineArgv','packetSha256','seedDevice','seedInode','outputDevice','outputInode','nativeAcceptance'],'settings') + need(settings['schema']=='fsbar.barc-stock-engine-settings-copy/v1' and settings['nativeAcceptance']is False and settings['actualEngineArgv']==config['commands']['engine'] and settings['outputPath']==roles['settings']['path'] and settings['copiedSha256']==settings['seedSha256']==roles['settings']['sha256'] and (settings['outputDevice'],settings['outputInode'])==(roles['settings']['device'],roles['settings']['inode']),'actual-settings-copy') + review=parse(held_bytes(value['review'],65536,deadline));exact(review,['schema','source','rolesSha256','setupOnlyOrderSites','postSetupOrderSites','competingControllers','aiSlots','effectiveConfig','vfs','archives','sourceReceiptPins'],'static-source-review') + need(review['schema']=='bar.prearm-static-source-review/v2' and review['source']==contract['source'] and review['rolesSha256']==sha(json.dumps({role:{k:pin[k] for k in ['path','sha256']} for role,pin in roles.items()},sort_keys=True,separators=(',',':')).encode()) and review['postSetupOrderSites']==[] and review['competingControllers']==[],'static-order-closure') + need(review['setupOnlyOrderSites']==[{'fixtureSha256':roles['fixture']['sha256'],'count':contract['setup']['seedCount'],'definition':contract['setup']['seedDefinitionId'],'options':0}],'static-finite-order-site') + need(type(review['sourceReceiptPins'])is list and 1<=len(review['sourceReceiptPins'])<=32,'source-receipt-closure') + for pin in review['sourceReceiptPins']:held_bytes(pin,16*1024*1024,deadline) + sources=value['producerSource'];exact(sources,['CircuitAI.h','CircuitAI.cpp'],'source');h=held_bytes(sources['CircuitAI.h'],2*1024*1024,deadline);cpp=held_bytes(sources['CircuitAI.cpp'],2*1024*1024,deadline);need(b'bool enableBuiltin = false;'in h and b'if (enableBuiltin)'in cpp and b'modules.push_back(grpcGateway);'in cpp and b'built-in Circuit decision modules cannot be re-enabled'in cpp,'external-control-source') + null=held_bytes(roles['nullAiSource'],2*1024*1024,deadline);need(re.search(rb'EXPORT\(int\) handleEvent\([^)]*\)\s*\{\s*// TODO: do something\s*// signal: ok\s*return 0;\s*\}',null) is not None,'passive-null-source') + obs=observations(raw,contract['runId']);contents=[r for r in obs if r['kind']=='content'];need(len(contents)==2,'two-vfs-contents') + for role,path in [('fixture','LuaRules/Gadgets/barc_stock_queue_fixture.lua'),('observer','LuaRules/Gadgets/barc_stock_queue_reader.lua')]: + matches=[r for r in contents if text(r['path'])==path];need(len(matches)==1,'vfs-path');body=held_bytes(roles[role],131072,deadline);need(dec(matches[0]['bytes'])==len(body) and matches[0]['sha512']==hashlib.sha512(body).hexdigest(),'vfs-actual-content-sha512') + actors=[r for r in obs if r['kind']=='actors'];need(len(actors)==1 and actors[0]['factory']==contract['factory']['id'] and dec(actors[0]['seed'])==contract['setup']['seedDefinitionId'] and dec(actors[0]['product'])==contract['setup']['productDefinitionId'],'actual-setup-actors') + actual_ai=[r for r in obs if r['kind']=='ai'];need(len(actual_ai)==len(review['aiSlots'])==2,'all-two-ai-slots') + for row in actual_ai: + expected=next((x for x in review['aiSlots'] if x['team']==int(row['team'])),None);need(expected is not None and text(row['short'])==expected['shortName'] and text(row['version'])==expected['version'] and text(row['options'])==expected['options'] and expected['mode']in ['external-control','passive-null'],'actual-ai-identity') + need({x['shortName']for x in review['aiSlots']}=={'highBar','NullAI'} and len({x['team']for x in review['aiSlots']})==2,'known-ai-slots-only') + actual_config={text(r['key']):{'type':r['type'],'value':text(r['value'])}for r in obs if r['kind']=='config'};need(len([r for r in obs if r['kind']=='config'])==len(actual_config)==len(review['effectiveConfig']) and actual_config==review['effectiveConfig'],'actual-effective-settings') + actual_vfs=[{k:text(r[k])for k in ['path','archive','absolute']}for r in obs if r['kind']=='vfs'];need(actual_vfs==review['vfs'],'actual-vfs-search-selection') + archives=[r for r in obs if r['kind']=='archives'];need(len(archives)==1,'archive-observation');archive={k:(archives[0][k] if k.endswith('sha512')else text(archives[0][k]))for k in ['map','game','map_sha512','game_sha512']};need(archive==review['archives'] and all(re.fullmatch('[0-9a-f]{128}',archive[k])for k in ['map_sha512','game_sha512']),'native-complete-archive-sha512') + inventory=parse(held_bytes(value['archiveInventory'],16*1024*1024,deadline));exact(inventory,['schema','archives','gameInventory','mapInventory','searchClosure'],'archive-inventory');need(inventory['schema']=='bar.prearm-selected-archive-inventory/v1' and inventory['archives']==archive and inventory['gameInventory']==roles['gameInventory'] and inventory['mapInventory']==roles['mapInventory'],'complete-package-inventory');need(type(inventory['searchClosure'])is list and inventory['searchClosure'],'archive-search-closure') + for pin in inventory['searchClosure']:held_bytes(pin,128*1024*1024,deadline) + complete=[r for r in obs if r['kind']=='complete'];empty=[r for r in obs if r['kind']=='empty'];orders=[r for r in obs if r['kind']=='order'];queues=[r for r in obs if r['kind']=='queue'];need(len(complete)==len(empty)==1 and len(orders)==len(queues)==contract['setup']['seedCount'],'finite-observed-setup') + c=complete[0];need(c['factory']==contract['factory']['id'] and dec(c['count'])==contract['setup']['seedCount'] and dec(c['empty_frame'])==dec(empty[0]['observed_frame']) and dec(empty[0]['frame'])','-p:DisableImplicitLibraryPacksFolder=true','-m:1','-nr:false','-p:NuGetAudit=false'],'buildArgs':['build','tests/Broker.NativeProof/RuntimeEvidence/RuntimeEvidence.Tests.fsproj','-c','Release','--no-restore','-p:DisableImplicitLibraryPacksFolder=true','-m:1','-nr:false','-p:NuGetAudit=false','-p:UseSharedCompilation=false','-p:SourceLink='],'environment':{'DOTNET_PROCESSOR_COUNT':'1'},'actualOutputs':None,'actualPDBSourceProof':None,'componentReuse':'Retained v5/v6 compile/proof outputs require explicit unchanged input-equivalence joins to candidate361. No prior receipt rewritten.'},'requiredProducerSemantics':['genuine policy/helper/product source identities','locked existing SDK/project/lock inputs','PE/PDB embedded SourceLink and original output correspondence','exact final product graph','complete runtime/dependency census and loaded map/assembly custody'],'receiptRewritePermitted':False,'effectsPerformed':False} diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/qualify_prearm.py b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/qualify_prearm.py new file mode 100644 index 0000000..27b81a9 --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/qualify_prearm.py @@ -0,0 +1,182 @@ +"""Bounded SOURCE pre-Arm reducer. Controlled tests are not native evidence. +No process/tool/network/command effects. Genuine originals and canonical policy +receipts are supplied by a separately admitted owning supervisor, never made here. +""" +import os, stat, json, hashlib, pathlib, re, time, sys, base64, struct, math +MAX_RAW=4*1024*1024; MAX_ROWS=8192; MAX_ROW=32768; MAX_OUTPUT=65536 +class Refused(Exception): pass +def need(ok,code): + if not ok:raise Refused(code) +def exact(v,keys,label):need(type(v)is dict and set(v)==set(keys),label+'-fields') +def unique(pairs): + d={} + for k,v in pairs:need(k not in d,'duplicate-json-key');d[k]=v + return d +def finite(value): + result=float(value);need(math.isfinite(result),'nonfinite-json');return result +def parse(body): + need(b'\0'not in body,'nul');return json.loads(body.decode('utf8'),object_pairs_hook=unique,parse_float=finite,parse_constant=lambda _:(_ for _ in()).throw(Refused('nonfinite-json'))) +def sha(body):return hashlib.sha256(body).hexdigest() +def dec(v):need(type(v)is str and re.fullmatch(r'0|[1-9][0-9]*',v)and int(v)<=2**64-1,'decimal');return int(v) +def pin_keys(p):exact(p,['path','sha256','bytes','device','inode','uid','mode','nlink'],'pin');need(type(p['path'])is str and pathlib.Path(p['path']).is_absolute()and str(pathlib.Path(p['path']))==p['path']and re.fullmatch('[0-9a-f]{64}',p['sha256']),'pin-format') +def held_bytes(pin,limit,deadline): + pin_keys(pin);need(type(pin['bytes'])is int and 0=pin['bytes'] and s.st_size<=limit and s.st_uid==pin['uid'] and stat.S_IMODE(s.st_mode)==pin['mode'] and (s.st_dev,s.st_ino)==(pin['device'],pin['inode']) and (named.st_dev,named.st_ino)==(s.st_dev,s.st_ino),'prefix-custody-drift');return s + check();parts=[];offset=0 + while offset0 and dec(v['lifetime'])>0,'reference-positive');return v +def b64(v,lo,hi): + need(type(v)is str,'base64-type') + try:raw=base64.b64decode(v,validate=True) + except Exception:raise Refused('base64') + need(lo<=len(raw)<=hi and base64.b64encode(raw).decode()==v,'base64-canonical');return raw +def basis(v): + exact(v,['token','stateSequence','frame','matchIncarnation','processIncarnation','stateChannelIncarnation','snapshotSendMonotonicNs','effectiveCadenceFrames'],'native-basis') + b64(v['token'],1,64);b64(v['matchIncarnation'],16,16);need(dec(v['stateSequence'])>0 and dec(v['snapshotSendMonotonicNs'])>0,'basis-positive') + need(type(v['frame'])is int and 0<=v['frame']<=2**32-1 and type(v['effectiveCadenceFrames'])is int and 019 and tail[0] not in ['Z','X']and m and tail[19]==process['startTicks']and int(m[1])==process['uid'],'live-pid-generation') + read();read() + finally:os.close(fd) +def mapped_roles(processes,roles): + engines=[p for p in processes if p['role']=='engine'];need(len(engines)==1,'engine-process-custody') + with open('/proc/'+str(engines[0]['pid'])+'/maps','rb')as stream:body=stream.read(1024*1024+1) + need(len(body)<=1024*1024,'loaded-maps-bound');lines=body.decode().splitlines() + for role in ['engine','plugin']: + pin=roles[role];found=False + for line in lines: + fields=line.split(None,5) + if len(fields)==6 and fields[5]==pin['path']: + dev=fields[3].split(':');found=found or (int(fields[4])==pin['inode']and os.makedev(int(dev[0],16),int(dev[1],16))==pin['device']) + need(found,'current-engine-plugin-mapping') +def framed(value,magic): + need(type(value)is str and value.isascii()and '\0'not in value and len(value)<=8192 and value.endswith('\n'),'stock-frame') + lines=value[:-1].split('\n');need(len(lines)>=3 and lines[0]==magic and lines[-1]=='end'and all(len(x)<=512 for x in lines),'stock-framing');need(re.fullmatch(r'length=[0-9]{8}',lines[1])and int(lines[1][7:])==len(value),'stock-frame-length');return lines +def field(value,name):need(value.startswith(name+'='),'stock-field');return value[len(name)+1:] +def row_text(row):return row['domain']+'|'+str(row['id'])+'|'+str(row['codedOptions'])+'|'+str(row['tag'])+'|'+','.join(row['float32params']) +def stock_revision(c,rows): + parts=[b'barc-stock-queue-revision/1\0'] + def add(tag,v):parts.append(bytes([tag])+struct.pack('>I',len(v))+v) + u32=lambda v:struct.pack('>I',v);u64=lambda v:struct.pack('>Q',int(v)) + values=[u32(2),c['profile'].encode(),u32(2),b64(c['catalogueId'],16,16),u64(c['catalogueRevision']),c['engineVersion'].encode(),c['gameName'].encode(),c['gameVersion'].encode(),bytes.fromhex(c['contentSha256']),u32(int(c['actor']['id'])),u64(c['actor']['lifetime']),c['domain'].encode(),u32(len(rows))] + for n,v in enumerate(values,1):add(n,v) + for row in rows:add(14,row_text(row).encode('ascii')) + return str(int.from_bytes(hashlib.sha256(b''.join(parts)).digest()[:8],'big')or 1) +def stock_record(r): + exact(r,['schema','runId','sequence','phase','readFrame','perspectiveTeamId','nativeBasis','context','reader','queue','dispatch'],'stock-row');basis(r['nativeBasis']);need(type(r['readFrame'])is int and 0<=r['readFrame']<=2**32-1 and type(r['perspectiveTeamId'])is int and 0<=r['perspectiveTeamId']<=255,'stock-envelope') + c=r['context'];exact(c,['profile','tacticalRevision','queueEvidenceScheme','actor','catalogueId','catalogueRevision','engineVersion','gameName','gameVersion','contentSha256','domain'],'stock-context');reference(c['actor']);b64(c['catalogueId'],16,16) + need(c['profile']=='barc-live-tactical-stock-v1'and c['tacticalRevision']==2 and c['queueEvidenceScheme']==2 and c['engineVersion']=='2026.07.04'and c['domain']in ['production','rally']and int(c['actor']['id'])<=31999 and dec(c['catalogueRevision'])>0 and re.fullmatch('[0-9a-f]{64}',c['contentSha256'])and all(type(c[k])is str and 0=9 and response[2]==req[2]and field(response[3],'request-sha256')==sha(reader['request'].encode())and field(response[4],'status')=='ok'and response[5]=='domain='+c['domain']and response[6]=='unit='+c['actor']['id'],'stock-response-echo') + count=dec(field(response[7],'count'));need(count<=64 and len(response)==count+9,'stock-response-count');q=r['queue'];exact(q,['revision','entries'],'stock-queue');need(type(q['entries'])is list and len(q['entries'])==count and dec(q['revision'])>0,'stock-queue-count');expanded=[];params=0 + for row,line in zip(q['entries'],response[8:-1]): + exact(row,['id','codedOptions','tag','float32params'],'stock-entry');need(all(type(row[k])is int and -2**31<=row[k]<2**31 for k in ['id','tag'])and type(row['codedOptions'])is int and 0<=row['codedOptions']<=65535 and type(row['float32params'])is list and len(row['float32params'])<=16,'stock-entry-bound') + for bits in row['float32params']:need(type(bits)is str and re.fullmatch('[0-9a-f]{8}',bits)and ((int(bits,16)>>23)&255)!=255,'stock-float32') + params+=len(row['float32params']);x={'domain':c['domain'],**row};need(field(line,'row')==row_text(x),'stock-response-row');expanded.append(x) + need(params<=256 and q['revision']==stock_revision(c,expanded),'stock-revision') + +def configuration_evidence(cfg,contract,deadline): + from native_evidence import configuration + return configuration(cfg,contract,deadline) + +def derive(contract,deadline): + exact(contract,['schema','runId','source','writer','acceptedGeneration','factory','setup','inputs','completeRecordEvidence','lifecycle','outputPath'],'contract') + need(contract['schema']=='bar.prearm-sealed-input-contract/v1','contract-schema');lifecycle=contract['lifecycle'];exact(lifecycle,['startedMonotonicNs','transitionDeadlineMonotonicNs','totalDeadlineMonotonicNs'],'lifecycle');start,transition,total=(dec(lifecycle[k])for k in ['startedMonotonicNs','transitionDeadlineMonotonicNs','totalDeadlineMonotonicNs']);need(start<=time.monotonic_ns()0 and type(setup['productDefinitionId'])is int and setup['productDefinitionId']>0 and setup['seedDefinitionId']!=setup['productDefinitionId'],'finite-distinct-seeds') + exact(contract['inputs'],['raw','host','stock','metadata','setup','configuration'],'inputs');limits={'raw':MAX_RAW,'host':16*1024*1024,'stock':16*1024*1024,'metadata':65536,'setup':65536,'configuration':65536};bodies={k:(prefix_bytes(v,limits[k],deadline) if k in ['raw','host','stock'] else held_bytes(v,limits[k],deadline)) for k,v in contract['inputs'].items()} + from native_evidence import horizons + horizons(parse(held_bytes(contract['completeRecordEvidence'],65536,deadline)),contract,bodies,deadline) + raw=records(bodies['raw'],'raw');host=records(bodies['host'],'host');stock=records(bodies['stock'],'stock');metadata=parse(bodies['metadata']);setup_record=parse(bodies['setup']);loaded=configuration_evidence(parse(bodies['configuration']),contract,deadline) + need(metadata.get('schema')=='fsbar.barc-stock-native-smoke-metadata/v1'and metadata.get('runId')==contract['runId']and metadata.get('writer')==contract['writer']and metadata.get('actors',{}).get('factory')==contract['factory'],'actual-metadata-origin') + need(setup_record.get('schema')=='fsbar.barc-stock-native-smoke-setup/v1'and setup_record.get('runId')==contract['runId']and setup_record.get('writer')==contract['writer']and setup_record.get('existingProductionDefinitionId')==setup['seedDefinitionId']and setup_record.get('productDefinitionId')==setup['productDefinitionId'],'actual-setup-origin') + origin={k:contract[k]for k in ['runId','source','writer','acceptedGeneration']};previous_state=0;previous_frame=0 + for row in raw: + exact(row,['schema','runId','sequence','kind','writer','source','acceptedGeneration','stateSequence','nativeFrame','disposition','value'],'raw-row');need(row['schema']=='fsbar.barc-one-unit-raw-state-journal/v1'and all(row[k]==v for k,v in origin.items()),'raw-origin');seq=dec(row['stateSequence']);need(seq>=previous_state and type(row['nativeFrame'])is int and row['nativeFrame']>=previous_frame,'raw-state-frame-order');previous_state=seq;previous_frame=row['nativeFrame'];need(row['disposition']in ['materialized','invalidated','liveness-only']and row['kind']in ['snapshot','unit-created','unit-finished','command-dispatch'],'gap-or-raw-kind');need(row['kind']!='command-dispatch','prearm-dispatch') + for row in host: + exact(row,['schema','runId','sequence','kind','writer','source','value'],'host-row');need(row.get('schema')=='fsbar.barc-stock-host-journal/v1'and all(row.get(k)==contract[k]for k in ['runId','writer','source']),'host-origin');need(row.get('kind')!='result','prearm-broker-result');need(row.get('kind') not in ['gap','detach','identity-conflict'],'host-generation-refusal');need(not (row.get('kind')=='stale' and any(x in str(row['value'].get('detail','')).lower() for x in ['gap','detach','identity','incarnation'])),'host-generation-refusal') + process=metadata['basis']['processIncarnation'];channel=metadata['basis']['stateChannelIncarnation'];match=metadata['basis']['matchId'];need(process and channel and match,'metadata-incarnations') + snapshots={};metadata_rows=[] + for row in host: + if row['kind']=='live-metadata'and type(row['value'].get('tactical'))is dict: + t=row['value']['tactical'];b=basis(t['basis']);need(b['processIncarnation']==process and b['stateChannelIncarnation']==channel and b['matchIncarnation']==match,'metadata-generation-basis');need(type(t.get('actors'))is list and len(t['actors'])<=4096,'metadata-actor-bound');refs=[reference(a['actor'])for a in t['actors']if a.get('actor')];need(len({a['id']for a in refs})==len(refs),'metadata-actor-lifetime-ambiguity');metadata_rows.append(t) + def joined(row): + found=[t for t in metadata_rows if t['basis']['stateSequence']==row['stateSequence']and t['basis']['frame']==row['nativeFrame']];need(len(found)>=1,'same-basis-native-metadata-missing');need(all(t==found[0]for t in found),'contradictory-metadata');return found[0] + for row in raw: + if row['kind']=='snapshot': + need(row['disposition']=='materialized','snapshot-not-materialized');t=joined(row);units=row['value']['units'];need(type(units)is list and len(units)<=4096 and len({str(u['unitId'])for u in units})==len(units),'snapshot-unit-set');snapshots[dec(row['sequence'])]=(row,t) + seed_created=[r for r in raw if r['kind']=='unit-created'and str(r['value'].get('builderId'))==contract['factory']['id']];need(len(seed_created)==setup['seedCount'],'seed-creation-count');ids=[str(r['value']['unitId'])for r in seed_created];need(len(set(ids))==len(ids)and '0'not in ids,'seed-creation-identities') + seed_finished=[r for r in raw if r['kind']=='unit-finished'and str(r['value'].get('unitId'))in ids];need(len(seed_finished)==len(ids)and len({str(r['value']['unitId'])for r in seed_finished})==len(ids),'seed-finish-count') + completion=loaded['setupCompletion'];need(completion['completionNativeFrame']last_finish];need(len(terminals)>=1,'later-complete-replacement-missing');terminal,t=terminals[-1];need(dec(terminal['stateSequence'])>max(dec(r['stateSequence'])for r in seed_finished)and terminal['nativeFrame']>=max(r['nativeFrame']for r in seed_finished),'replacement-after-invalidation') + units=terminal['value']['units'];need(all(u.get('underConstruction')is False and u.get('buildProgress')==1 for u in units),'unfinished-terminal');need(known.issubset({str(u['unitId'])for u in units}),'setup-actor-disappeared');fresh={str(u['unitId'])for u in units}-known;need(fresh==set(ids),'unexplained-extra-products');actors={a['actor']['id']:reference(a['actor'])for a in t['actors']if a.get('actor')};products=[] + for created in seed_created: + ident=str(created['value']['unitId']);finished=next(r for r in seed_finished if str(r['value']['unitId'])==ident);need(created['disposition']=='invalidated'and finished['disposition']=='invalidated'and dec(finished['sequence'])>dec(created['sequence'])and finished['nativeFrame']>=created['nativeFrame'],'seed-lifecycle-invalidation-order') + for event in [created,finished]: + need(any(h['kind']=='stale'and str(h['value'].get('receivedSequence'))==event['stateSequence']for h in host),'actual-stale-evidence-missing') + need(any(h['kind']=='observation'and str(h['value'].get('stateSequence'))==terminal['stateSequence']for h in host),'actual-replacement-projection-missing') + u=next(u for u in units if str(u['unitId'])==ident);need(u.get('definitionId')==setup['seedDefinitionId']and ident in actors,'seed-definition-lifetime-missing');need(all(a['actor']==actors[ident]for mt in metadata_rows for a in mt['actors']if a.get('actor',{}).get('id')==ident),'seed-lifetime-changed');products.append({'reference':actors[ident],'createdSequence':created['sequence'],'finishedSequence':finished['sequence']}) + for row in raw: + if dec(row['sequence'])=dec(initial['sequence']) and row['kind']=='unit-created':need(str(row['value'].get('unitId'))in ids and str(row['value'].get('builderId'))==contract['factory']['id'],'extra-or-direct-spawn') + if dec(row['sequence'])>=first_created and row['kind']=='unit-finished':need(str(row['value'].get('unitId'))in ids,'extra-finish') + samples=[] + for r in stock: + stock_record(r);need(r.get('schema')=='highbar.barc-stock-queue-trace/v1'and r.get('runId')==contract['runId'],'stock-origin');b=basis(r['nativeBasis']);need(b['processIncarnation']==process and b['stateChannelIncarnation']==channel and b['matchIncarnation']==match,'stock-basis-incarnation');need(r['phase']=='sample'and r['dispatch']is None,'prearm-stock-dispatch');need(r['context']['actor']==contract['factory'],'stock-factory'); + if r['context']['domain']=='production'and r['reader']['status']=='complete': + fake={'stateSequence':b['stateSequence'],'nativeFrame':b['frame']};tm=joined(fake);actor=next((a for a in tm['actors']if a.get('actor')==contract['factory']),None);need(actor,'stock-host-actor');queues=[z for z in actor.get('queue',[])if z.get('domain')in ['NATIVE_QUEUE_DOMAIN_FACTORY_PRODUCTION','FactoryProduction',2]];need(len(queues)==1,'production-projection');queue=queues[0];need(all(str(z.get('definitionId'))==str(-e['id'])and z.get('nativeTag',0)==e['tag'] for z,e in zip(queue.get('entries',[]),r['queue']['entries'])),'stock-host-entry-projection');need(queue.get('complete')is True and queue.get('repeat')is False and queue.get('evidenceScheme')in [2,'NATIVE_QUEUE_EVIDENCE_SCHEME_STOCK_LUA_SUPPORTED_FIELDS_V1']and str(queue.get('revision'))==r['queue']['revision']and len(queue.get('entries',[]))==len(r['queue']['entries']),'same-basis-complete-stock-queue');samples.append(r) + nonempty=[r for r in samples if len(r['queue']['entries'])==setup['seedCount']and all(e['id']==-setup['seedDefinitionId']and e['codedOptions']==0 and e['float32params']==[] for e in r['queue']['entries'])];need(nonempty,'actual-finite-nonempty-seed-queue-missing') + empty=[r for r in samples if not r['queue']['entries']and dec(r['nativeBasis']['stateSequence'])>=dec(terminal['stateSequence'])and r['readFrame']>=terminal['nativeFrame']];need(empty,'actual-terminal-empty-queue-missing');need(empty[-1]['readFrame']>nonempty[0]['readFrame']and dec(empty[-1]['sequence'])>dec(nonempty[0]['sequence'])and dec(nonempty[0]['nativeBasis']['stateSequence'])0 and fields[4]=='0001','literal Unix stream kernel row');matches.append(fields) + need(len(matches)==1,'one exact pathname net/unix row');kernel=matches[0][6] + names=os.listdir(f"/proc/{ident['pid']}/fd");need(len(names)<=256,'engine FD bound');fds=[] + for name in names: + need(name.isdigit(),'numeric engine FD') + try:target=os.readlink(f"/proc/{ident['pid']}/fd/{name}") + except FileNotFoundError:continue + if target=='socket:['+kernel+']':fds.append(int(name)) + need(bool(fds),'actual engine held socket FD');need(m.same(ident,m.identity(ident['pid'])),'engine generation drift during socket join');self.socket_stat() + self.owner=dict(schema='bar.typed-stock-runtime-socket/v1',filesystem=row,kernelSocketInode=kernel,engine=ident,engineFDs=sorted(fds),netUnixRawSHA256=m.hashlib.sha256(raw).hexdigest(),directory=self.pin,ownership='Observed',inodeDomains='kernel socket inode and filesystem dentry inode are distinct; never equated');self.pending=None + return dict(row,ownership='Observed'if self.owner else 'Pending',firstObservedMonotonic=self.first['firstObservedMonotonic'],lastObservedMonotonic=self.last['lastObservedMonotonic']) + def inspect_for_quota(self,path): + need(pathlib.Path(path)==self.path,'only literal admitted socket path');row=self.observe(False) + if not self.owner: + try:self.observe(True) + except BaseException as e:self.pending=type(e).__name__+':'+str(e) + return dict(row,ownership='Observed'if self.owner else 'Pending',ownerJoinUnknown=self.pending) + def cleanup(self,settled): + self.directory_check() + try:self.socket_stat() + except FileNotFoundError:return dict(status='ObservedAbsent',removed=False,owner=self.owner,first=self.first,last=self.last) + need(settled and self.owner is not None,'socket cleanup requires observed owner and settled engine generations');engine=self.owner['engine'] + try:current=m.identity(engine['pid']) + except (FileNotFoundError,ProcessLookupError):current=None + need(current is None,'engine numeric PID must be absent before leftover unlink');self.socket_stat();os.unlink('highbar-0.sock',dir_fd=self.fd) + try:os.stat('highbar-0.sock',dir_fd=self.fd,follow_symlinks=False) + except FileNotFoundError:return dict(status='ObservedAbsent',removed=True,owner=self.owner,first=self.first,last=self.last) + raise m.CleanupUnknown('socket remains after exact held-FD unlink') + def close(self):os.close(self.fd) diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/startup_runner.py b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/startup_runner.py new file mode 100644 index 0000000..83c907f --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/startup_runner.py @@ -0,0 +1,416 @@ +"""Literal host+engine seed-only startup and one sequential direct policy child. Inert without fresh exact root admission.""" +import sys +sys.dont_write_bytecode=True +import os,json,pathlib,time,datetime,argparse,subprocess,selectors,ctypes,hashlib,stat,re +HERE=pathlib.Path(__file__).resolve().parent;sys.path[:0]=[str(HERE),str(HERE/'helper')] +import mechanics as m,outer_custody as outer +from runtime_socket import RuntimeSocket +from stock_bootstrap import authenticate_ready,authenticate_metadata,authenticate_setup,persist_authenticated_snapshots +from runtime_identity import proc_bytes,start_ticks,verify_executable_map_lines,verify_host_dynamic_map_lines +ROOT='/tmp/bar-stock-seed-attribution-20261004';PYTHON='/usr/bin/python3.14' +need=m.need;clock=m.clock +RUNTIME_SOCKET=None + +def profile(): + p=m.load(HERE/'startup-profile.json');need(p['schema']=='bar.seed-attribution-profile/v2' and p['preparationReady'] is True and p['attemptRoot']==ROOT and p['roles']==['host','engine'],'closed startup profile') + need(p['source']=={'fsbarCommit':'361f5060aea0e340fb8a33da96a91fd234e7fc39','highbarCommit':'b57f11f290dc3239d894b0285ae272b6b21bc25d'} and p['engineSource']=='de69361239d8c8b1012dba3f5aa3122954ea4da3','literal source roles') + need(p['limits']==dict(wholeSeconds=180,hostReadinessSeconds=60,cleanupReserve=8,termSeconds=3,killSeconds=3,streamBytes=33554432,attemptBytes=134217728,attemptFiles=1024,mapsBytes=4194304,ownedIdentities=128),'literal diagnostic caps') + need(p['runtimeEnvironment']['DOTNET_EnableDiagnostics']=='0' and p['diagnosticConfigRootSourceDecision']=='Root approved SOURCE profile only; runtime diagnostic admission must bind this exact environment','explicit preparation-independent runtime configuration') + need(p['runtimeObjects']==dict(directory=ROOT+'/engine/run',socket=ROOT+'/engine/run/highbar-0.sock',role='engine',aiId=0,mode=448)and p['engineRuntimeEnvironment']=={'XDG_RUNTIME_DIR':ROOT+'/engine/run'},'literal reviewed runtime object/environment') + need(len(os.fsencode(p['runtimeObjects']['socket']))+1<108,'no UDS fallback') + from dynamic_mapping import PROFILE + need(p['dynamicMappingProfile']==PROFILE,'closed host-only dynamic metadata profile') + required=[p['nativeHost']]+[path for path in p['allowedExecutableMappings'] if pathlib.Path(path).name in ('libcoreclr.so','libclrjit.so')] + from mapping_profile import validate_schema + validate_schema(p) + need(p['requiredHostMappings']==required and len(required)==3,'literal pinned apphost/coreclr/clrjit') + need(p['policy']['environment']=={'PATH':'/usr/bin:/bin'} and p['policy']['maximumSimultaneousChildren']==1 and p['policy']['maximumEvaluations']==3 and p['policy']['deadlineSeconds']==5 and p['policy']['inputBytes']==16777216 and p['policy']['outputBytes']==131072,'literal closed policy caps') + need(set(p['sourceHelperPins'])=={'helper/runtime_identity.py','helper/stock_bootstrap.py','helper/dynamic_mapping.py','helper/snapshot_publication.py','helper/mapping_profile.py','helper/seed_policy.py','helper/growing_log.py','helper/loaded_custody.py','helper/settings_custody.py','qualify_prearm.py','native_evidence.py'},'closed successor source helpers') + for path,sha in p['sourceHelperPins'].items():need(m.digest(HERE/path)==sha,'exact profile helper byte pin') + return p + +def reviewed_source(): + s=m.load(HERE/'review-packet.json');need(s['schema']=='bar.seed-attribution-source-review/v2','source schema') + need(len(s['files'])==len({x['path']for x in s['files']}),'unique sealed source rows') + need(set(x['path']for x in s['files'])=={str(f.relative_to(HERE))for f in HERE.rglob('*')if f.is_file()and f.name!='review-packet.json'},'complete source leaf roster') + for x in s['files']: + need(set(x)=={'path','bytes','sha256','mode'}and not pathlib.PurePosixPath(x['path']).is_absolute()and '..'not in pathlib.PurePosixPath(x['path']).parts,'closed safe source row') + f=HERE/x['path'];need(f.is_file()and not f.is_symlink()and f.stat().st_size==x['bytes']and stat.S_IMODE(f.stat().st_mode)==x['mode']and m.digest(f)==x['sha256'],'all source leaf pins') + return m.digest(HERE/'review-packet.json') + +def admission(a,p,source,operator,now,root_absent,affinity): + need(set(a)=={'schema','operation','sourceSHA256','profileSHA256','operator','cpu','expiresUTC','root','oneUse','runtimeEnvironment','engineRuntimeEnvironment','runtimeObjects','source','ports','policy','capacity','seedInputsSHA256'},'closed seed admission') + need(a['schema']=='bar.seed-attribution-admission/v2'and a['operation']=='seed-attribution-startup-only'and a['root']==ROOT and a['oneUse']is True,'startup-only one-use operation') + need(a['sourceSHA256']==source and a['profileSHA256']==m.digest(HERE/'startup-profile.json'),'exact admitted source/profile') + need(m.same(a['operator'],operator)and a['operator']['pid']==operator['pid'],'actual admitted operator') + need(type(a['cpu'])is int and a['cpu']in affinity and root_absent,'fresh absent root and one allowed CPU') + expires=datetime.datetime.fromisoformat(a['expiresUTC']);need(expires.tzinfo is not None and 0<(expires-now).total_seconds()<=600,'fresh finite admission') + need(a['engineRuntimeEnvironment']==p['engineRuntimeEnvironment']and a['runtimeObjects']==p['runtimeObjects'],'exact UDS and engine environment admission') + need(a['runtimeEnvironment']==p['runtimeEnvironment']and a['source']==p['source']and a['ports']==p['proposedPorts'],'closed semantic role/environment/port pins') + need(a['policy']==p['policy'] and a['seedInputsSHA256']==hashlib.sha256(json.dumps(p['seed'],sort_keys=True,separators=(',',':')).encode()).hexdigest(),'admitted closed seed/policy inputs') + need(a['capacity']['maximumTaskCLR']==2 and a['capacity']['maximumInfrastructureCLR']==2 and a['capacity']['cpu']==a['cpu'] and a['capacity']['reservationSource'] and a['capacity']['currentCensusPin'],'actual capacity reservation binding required') + m.checked_pin(a['capacity']['currentCensusPin']) + return a + +def originals(p,deadline): + rows=p['originalDataPins']+p['hostPins']+p['clrPins']+p['elfPins']+p['pythonPins']+p['authorityPins'] + for x in rows:m.remaining(deadline,1);m.checked_pin(x) + from mapping_profile import revalidate + mapping=revalidate(p,lambda:m.remaining(deadline,1)) + return {'mappingIdentities':mapping,'observedPins':len(rows),'originalDataFiles':len(p['originalDataPins']),'acceptedHostMembers':len(p['hostPins'])} + +def environment(p,role): + if role=='policy':return {'PATH':'/usr/bin:/bin'} + A=pathlib.Path(ROOT);env={'PATH':'/usr/bin:/bin','HOME':str(A/role),'TMPDIR':str(A/role/'tmp'),'LANG':'C.UTF-8','LC_ALL':'C.UTF-8','PYTHONDONTWRITEBYTECODE':'1'} + if role=='host': + env.update(p['runtimeEnvironment']);env.update(BARC_ONE_UNIT_RAW_EVENTS=str(A/'host/raw.jsonl'),BARC_STOCK_SMOKE_RUN_ID=p['runId'],BARC_STOCK_SMOKE_READY=str(A/'host/ready.json'),BARC_STOCK_SMOKE_METADATA=str(A/'host/metadata.json'),BARC_STOCK_SMOKE_SETUP=str(A/'host/setup.json'),BARC_STOCK_SMOKE_HOST_TRACE=str(A/'host/host.jsonl'),BARC_STOCK_SMOKE_SOURCE_JSON=json.dumps(p['source'],sort_keys=True,separators=(',',':')),BARC_STOCK_SMOKE_RECEIVER_URL=f"http://127.0.0.1:{p['proposedPorts']['unusedReceiverOrigin']}/barc/?barc-profile=barc-live-tactical-stock-v1") + if role=='engine':env.update(p['engineRuntimeEnvironment']);env.update(HIGHBAR_COORDINATOR=f"127.0.0.1:{p['proposedPorts']['grpc']}",HIGHBAR_STOCK_QUEUE_TRACE=str(A/'engine/stock.jsonl'),HIGHBAR_STOCK_QUEUE_TRACE_RUN_ID=p['runId']) + return env + +def commands(p,invocation=None): + ports=p['proposedPorts'];A=pathlib.Path(ROOT) + result={'host':[p['nativeHost'],'--stock-tactical-live-host',f"127.0.0.1:{ports['grpc']}",f"http://127.0.0.1:{ports['gateway']}",f"http://127.0.0.1:{ports['unusedReceiverOrigin']}",str(A/'host'),p['source']['fsbarCommit']], 'engine':[p['engine'],'--isolation','--isolation-dir',p['dataRoot'],'--write-dir',str(A/'engine'),'--config',str(A/'engine/springsettings.cfg'),p['startscript']]} + if invocation is not None: + need(re.fullmatch('[0-9a-f]{64}',invocation),'exact policy invocation');result['policy']=[p['policy']['apphost'],'--closure-manifest',p['policy']['closurePath'],'--closure-sha256',p['policy']['closureSha256'],'--invocation-id',invocation] + return result + +def runtime_socket(): + global RUNTIME_SOCKET + if RUNTIME_SOCKET is None: + p=profile();pin=m.load(pathlib.Path(ROOT)/'evidence/runtime-directory.json') + def engine_identity(): + f=pathlib.Path(ROOT)/'evidence/engine.process.json' + if not f.exists():return None + row=m.load(f);need(row['argv']==commands(p)['engine']and row['environment']==environment(p,'engine'),'source-bound observed engine receipt');return row['identity'] + RUNTIME_SOCKET=RuntimeSocket(p['runtimeObjects'],pin,engine_identity) + return RUNTIME_SOCKET + +def validate_startscript(raw): + text=raw.decode('ascii');slots=re.findall(r'\[ai([0-9]+)\]\s*\{([^{}]*)\}',text,re.I);highbar=[int(i)for i,body in slots if re.search(r'\bshortname\s*=\s*highBar\s*;',body,re.I)] + need(highbar==[0]and len(slots)==2 and any(re.search(r'\bshortname\s*=\s*NullAI\s*;',body,re.I)for _,body in slots)and len({i for i,body in slots})==len(slots),'exact single HighBar AI ID0 startscript') + +def quota(owner=None): + q=m.quota(pathlib.Path(ROOT),134217728,phase='startup',known_owned=list(owner.records.values())if owner else[],live_owned=owner.observe()if owner else[],runtime_socket=runtime_socket());need(q['files']<=1024,'startup leaf bound');return q + +def ready_config(p):return {'runId':p['runId'],'source':p['source'],'bootstrap':{'receiverUrl':environment(p,'host')['BARC_STOCK_SMOKE_RECEIVER_URL']},'roots':{'attemptRoot':ROOT},'paths':{'hostTrace':ROOT+'/host/host.jsonl'}} + +def maps(role,child,p,phase,require_complete=False): + held=m.load(pathlib.Path(ROOT)/'evidence'/(role+'.process.json'))['identity'];ident=m.identity(child.pid) + need(child.poll() is None and child.pid==held['pid'] and m.same(held,ident),'held map snapshot process drift') + from dynamic_mapping import held_process + executable=p['nativeHost'] if role=='host' else p['engine'];before=held_process(held,executable) + raw=proc_bytes(child.pid,'maps',4194304);target=pathlib.Path(ROOT)/'evidence'/(role+'.'+phase+'.maps.raw') + from snapshot_publication import bytes_new + raw_ack=bytes_new(str(target),raw,4194304) + receipt={'role':role,'phase':phase,'process':ident,'rawSHA256':hashlib.sha256(raw).hexdigest(),'rawBytes':len(raw),'rawPublication':raw_ack,'completeMapsReadThroughEOF':True,'notExhaustiveManagedModuleOrIOProof':True,'status':'Unknown','dynamicMappingMetadata':'Unknown' if role=='host' else 'NotApplicable','dynamicContentSHA256':None,'dynamicContentProvenance':'Unknown','creatorAttribution':'Inference','exhaustiveMemoryOrIOProof':False} + try: + allowed={path:{'sha256':sha}for path,sha in p['allowedExecutableMappings'].items()};lines=raw.decode().splitlines() + if role=='host': + seen,dynamic=verify_host_dynamic_map_lines(lines,allowed,held,executable,p['dynamicMappingProfile']);receipt['dynamicMapping']=dynamic;receipt['dynamicMappingMetadata']=dynamic['dynamicMappingMetadata'] + else: + for line in lines: + fields=line.split(None,5);name=fields[5] if len(fields)==6 else '';need(not name.startswith('/memfd:') and not name.endswith(' (deleted)'),'engine-dynamic-map-refused') + seen=verify_executable_map_lines(lines,allowed) + required=[x['path']for x in p['requiredEngineMappings'].values()]if role=='engine'else p['requiredHostMappings'];missing=[x for x in required if x not in seen] + need(m.same(held,m.identity(child.pid)) and before==held_process(held,executable),'map snapshot process drift');receipt.update(executableFiles=sorted(seen),missingRequired=missing,fileBackedExecutableIdentity='Observed',status='Pending'if missing else 'Observed') + if require_complete:need(not missing,'actual required loaded role identities') + except BaseException as e:receipt.update(status='Unknown',error=str(e));m.write_new(target.with_suffix('.json'),receipt);raise + m.write_new(target.with_suffix('.json'),receipt);return receipt + +def content_checkpoint(path): + path=pathlib.Path(path) + if not path.is_file()or path.is_symlink():return {'status':'Unknown','reason':'no emitted engine log'} + need(path.stat().st_size<=33554432,'content log bound');raw=path.read_bytes();text=raw.decode('utf-8') + patterns={'map':r'\[PreGame::AddMapArchivesToVFS\]\[server=[^\]\r\n]+\] using map "Avalanche 3\.4" \(loaded=1 cached=[01]\)', 'game':r'\[PreGame::AddModArchivesToVFS\]\[server=[^\]\r\n]+\] using game "HighBar BARC Stock Queue Fixture 2" \(loaded=[01] cached=[01]\)'} + matches={k:re.search(v,text)for k,v in patterns.items()} + if not all(matches.values()):return {'status':'Unknown','reason':'literal selected map/game emitted joins absent','logSHA256':hashlib.sha256(raw).hexdigest()} + return {'status':'Observed','scope':'loaded-map emission plus selected-game emission only; game Lua/fixture and authenticated native readiness are separate joins','map':'Avalanche 3.4','game':'HighBar BARC Stock Queue Fixture 2','logSHA256':hashlib.sha256(raw).hexdigest(),'matchedRecords':{k:{'record':v.group(0),'byteOffset':len(text[:v.start()].encode('utf8'))}for k,v in matches.items()}} + +def checkpoint_report(observed): + order=['launch','engine-loader','content/map','game-Lua/stock-reader','fixture-ready','local-service-bind','native-channel','catalogue/basis','stock-selection','authenticated-ready'];known=[x for x in order if x in observed] + return {'order':order,'observed':known,'lastPositive':known[-1]if known else None,'firstUnmet':next((x for x in order if x not in observed),None),'silenceMeans':'Unknown, not guessed map failure','nativeUsefulPlay':'0/6','ArmAuthority':False} + +def record_tail(path,label): + path=pathlib.Path(path) + if not path.is_file()or path.is_symlink():return {'label':label,'status':'Unknown','reason':'absent'} + s=path.stat();need(stat.S_ISREG(s.st_mode)and s.st_uid==os.geteuid()and s.st_nlink==1,'failure log custody');need(s.st_size<=33554432,'individual runtime log bound') + with path.open('rb')as f:f.seek(max(0,s.st_size-65536));raw=f.read(65536) + target=pathlib.Path(ROOT)/'evidence'/(label+'.tail.raw');target.write_bytes(raw);target.chmod(0o600) + return {'label':label,'status':'Observed','sourceBytes':s.st_size,'offset':max(0,s.st_size-len(raw)),'bytes':len(raw),'sha256':hashlib.sha256(raw).hexdigest()} + +def launch(role,p,a_path,owner,deadline,registered,invocation=None,evaluation=None,cleanup_reserve=8,policy_context=None): + need(role in ['host','engine','policy'],'literal launch role') + need((role!='policy' and invocation is None and evaluation is None) or (role=='policy' and re.fullmatch('[0-9a-f]{64}',invocation or '') and evaluation in [1,2,3]),'literal invocation/evaluation') + name=role if role!='policy'else 'policy-'+str(evaluation) + end=min(deadline-cleanup_reserve,clock()+5);rd,rw=os.pipe();ar,aw=os.pipe();child=None + try: + runtime_socket().directory_check() + shim_argv=[PYTHON,'-I','-B',str(HERE/'startup_runner.py'),'--shim',role,'--admission',str(a_path),'--ready',str(rw),'--ack',str(ar)] + if role=='policy':shim_argv+=['--invocation',invocation] + child=subprocess.Popen(shim_argv,env=environment(p,role),pass_fds=(rw,ar),start_new_session=True,stdin=subprocess.PIPE if role=='policy'else None,stdout=subprocess.PIPE,stderr=subprocess.PIPE,umask=0o077) + registered[name]=child;owner.leaders.add(child.pid);os.close(rw);rw=-1;os.close(ar);ar=-1 + with selectors.DefaultSelector()as sel: + sel.register(rd,selectors.EVENT_READ) + while True: + if role=='policy': + from seed_policy import SeedPolicyContext + need(type(policy_context)is SeedPolicyContext,'concrete policy preexec supervision context');policy_context.pump(end) + if sel.select(m.remaining(end,.02 if role=='policy'else 5)):break + need(os.read(rd,64)==b'BAR-STARTUP-READY\n','actor preexec handshake') + ident=m.identity(child.pid);need(ident['pgid']==ident['session']==child.pid and ident['ppid']==os.getpid(),'actual owned actor identity');owner.acquire(ident);need(os.sched_getaffinity(child.pid)=={m.load(a_path)['cpu']},'actor serial affinity') + m.write_new(pathlib.Path(ROOT)/'evidence'/(name+'.process.json'),dict(identity=ident,argv=commands(p,invocation)[role],environment=environment(p,role)));os.write(aw,b'GO\n');return child + finally: + for fd in [rd,rw,ar,aw]: + if fd>=0: + try:os.close(fd) + except OSError:pass + +def diagnostic_checkpoints(A,observed): + reports=[] + for label,path in [('infolog',A/'engine/infolog.txt'),('stderr',A/'logs/engine.stderr.raw')]: + if not path.is_file():continue + need(path.stat().st_size<=33554432,'checkpoint log bound');raw=path.read_bytes();text=raw.decode('utf8') + patterns={'game-Lua/stock-reader':r'^.*Loaded synced gadget: BARC Stock Queue Reader .*$', 'fixture-ready':r'^.*highbar_barc_stock_fixture kind=ready frame=([0-9]+) team=0 enemy_team=1 .*$', 'local-service-fault':r'^.*(?:Failed to add port to server:.*unix:|\[hb-gateway\] fault subsystem=callback reason=rpc_internal detail="frame_tick_threw").*$'} + for key,pattern in patterns.items(): + found=re.search(pattern,text,re.MULTILINE) + if found: + record=dict(status='Observed',log=label,logSHA256=hashlib.sha256(raw).hexdigest(),byteOffset=len(text[:found.start()].encode('utf8')),matchedRecord=found.group(0),frame=(re.search(r'(?:frame[=: ]+)([0-9]+)',found.group(0),re.I).group(1)if re.search(r'(?:frame[=: ]+)([0-9]+)',found.group(0),re.I)else None)) + observed.setdefault(key,record);reports.append(record) + content=content_checkpoint(A/'engine/infolog.txt') + if content['status']=='Observed':observed.setdefault('content/map',content) + return reports + +def seed_predicates(A,ready): + from qualify_prearm import records + import native_evidence + raw_path=A/'host/raw.jsonl';stock_path=A/'engine/stock.jsonl';log_path=A/'engine/infolog.txt' + if not all(x.is_file() for x in [raw_path,stock_path,log_path]):return False + for path,cap in [(raw_path,4194304),(stock_path,16777216),(log_path,10485760)]:need(path.stat().st_size<=cap,'seed predicate input cap') + raw=records(raw_path.read_bytes(),'raw');stock=records(stock_path.read_bytes(),'stock') + completed=[r for r in raw if r['kind']=='unit-finished'];snapshots=[r for r in raw if r['kind']=='snapshot'] + if len(completed)<3 or not snapshots:return False + later=snapshots[-1] + if int(later['sequence'])<=max(int(r['sequence']) for r in completed):return False + observed=native_evidence.observations(log_path.read_bytes(),ready['runId']) + return any(r['kind']=='complete'for r in observed) and any(r.get('queue') is not None and r.get('reader',{}).get('status')=='complete' and r.get('context',{}).get('domain')=='production' and not r['queue']['entries'] and int(r['nativeBasis']['stateSequence'])>=int(later['stateSequence'])for r in stock) + +def supervise(p,a_path,deadline): + owner=m.LinuxOwner();children={};streams={};counts={};observed={};failure=None;secondary=[];result={'readyProducerAuthenticated':False,'startupAccepted':False,'seedQualificationAccepted':False,'canonicalPolicyConsumed':False};first_join=None;A=pathlib.Path(ROOT);captured=set();socket=runtime_socket();next_scan=0 + def capture_maps(role,phase,required=False): + key=(role,phase) + if key in captured:return None + captured.add(key) + try: + receipt=maps(role,children[role],p,phase,required) + if role=='engine'and receipt['status']=='Observed':observed.setdefault('engine-loader',{'basis':'actual full engine/plugin/NullAI/interface executable mappings','phase':phase,'rawSHA256':receipt['rawSHA256']}) + return receipt + except BaseException as e: + secondary.append(dict(kind='maps',role=role,phase=phase,error=str(e))) + if required:raise + return {'status':'Unknown','error':str(e)} + def drain(sel,seconds): + end=min(deadline-8,clock()+seconds) + while sel.get_map()and clock()=next_scan: + diagnostic_checkpoints(A,observed);next_scan=clock()+.2 + if 'game-Lua/stock-reader'in observed or 'fixture-ready'in observed or 'local-service-fault'in observed: + for role in ['host','engine']:capture_maps(role,'early') + if (A/'host/ready.json').is_file(): + first_join='ready-producer-authentication' + ready,ready_snapshot=authenticate_ready(A/'host/ready.json',children['host'],ready_config(p),return_snapshot=True);metadata,metadata_snapshot=authenticate_metadata(ready['metadataPath'],ready,return_snapshot=True);setup,setup_snapshot=authenticate_setup(ready['setupPath'],ready,return_snapshot=True);need(ready['grpcAddress']==f"127.0.0.1:{p['proposedPorts']['grpc']}"and ready['allowedOrigin']==f"http://127.0.0.1:{p['proposedPorts']['unusedReceiverOrigin']}",'actual ready ports') + result.update(readyProducerAuthenticated=True,readySnapshotsPublicationAcknowledged=False) + first_join='authenticated-snapshot-publication' + authenticated=persist_authenticated_snapshots(A/'evidence',{'ready':(ready,ready_snapshot),'metadata':(metadata,metadata_snapshot),'setup':(setup,setup_snapshot)});result.update(readyProducerAuthenticated=True,readySnapshotsPublicationAcknowledged=True,authenticatedSnapshots=authenticated);observed['authenticated-ready']={'basis':'authenticated producer exact retained snapshots','readySHA256':ready_snapshot['sha256']} + first_join='owned-socket' + owned_socket=socket.observe(True);need(owned_socket['ownership']=='Observed','actual owned stock UDS');m.write_new(A/'evidence/runtime-socket-owner.json',socket.owner);observed['local-service-bind']={'basis':'actual owned UDS kernel/fd receipt plus authenticated stock readiness','socket':socket.owner} + joined=[] + for role in ['engine','host']: + first_join=role+'-maps' + receipt=capture_maps(role,'terminal',True);need(receipt and receipt['status']=='Observed','complete actual loaded role joins at ready');joined.append(receipt) + for label in ['engine-loader','Lua/AI-init','native-channel','catalogue/basis','stock-selection','authenticated-ready']:observed[label]={'basis':'authenticated producer + full actual role maps + owned UDS','readySHA256':ready_snapshot['sha256']} + first_join='stock-reader-and-fixture' + diagnostic_checkpoints(A,observed);need('game-Lua/stock-reader'in observed and 'fixture-ready'in observed,'actual emitted stock reader and fixture readiness checkpoints');content=content_checkpoint(A/'engine/infolog.txt') + first_join='selected-content' + result.update(authenticatedReady=True,mapLoadCheckpoint=content,hostSourceUnchanged=p['source'],maps=joined,ownedSocket=socket.owner);need(content['status']=='Observed','authenticated ready but loaded selected map/game log join Unknown') + first_join='finite-seed-observation' + # Ready is a prerequisite, never the success predicate. Continue draining + # the actual actor selector until real retained lifecycle predicates exist. + from seed_policy import SeedGrowingLog,SeedPolicyContext + from stock_bootstrap import collect_prearm + import native_evidence,qualify_prearm + policy_context=SeedPolicyContext(p,a_path,owner,children,sel,streams,counts,deadline) + while not seed_predicates(A,ready): + m.remaining(deadline,1,8);policy_context.pump(deadline-8);time.sleep(min(.02,m.remaining(deadline,.02,8))) + first_join='canonical-infolog-and-native-seed-qualification' + policy=SeedGrowingLog.acquire(str(A/'engine/infolog.txt'),{'pid':children['engine'].pid,'startTicks':start_ticks(children['engine'].pid),'uid':os.geteuid()}).bind(policy_context) + try:seed=collect_prearm(p,ready,metadata,setup,children['host'],children['engine'],policy,deadline-8,int((deadline-180)*1e9)) + finally:policy.close() + need(seed['seedQualificationAccepted'] and seed['canonicalPolicyConsumed'] and seed['publication']['fullWriteFsyncAcknowledged'],'real seed/policy/publication joins') + result.update(seedQualificationAccepted=True,canonicalPolicyConsumed=True,qualificationPublicationAcknowledged=True,seedQualification=seed) + break + for role,child in children.items():need(child.poll()is None,role+' exited before authenticated readiness') + finally: + try:drain(sel,.1) + except BaseException as e:secondary.append(dict(kind='pipe-drain',error=str(e))) + except BaseException as e: + failure=e;result['firstFailedJoin']=str(e) if str(e).startswith('host-dynamic-') else first_join + finally: + snapshots={} + for role,child in children.items(): + try:snapshots[role]={'identity':m.identity(child.pid),'state':'Observed'} + except BaseException as e:snapshots[role]={'state':'Unknown','reason':type(e).__name__} + if role in ['host','engine']:capture_maps(role,'terminal') + try:diagnostic_checkpoints(A,observed) + except BaseException as e:secondary.append(dict(kind='checkpoint',error=str(e))) + try: + if socket.path.exists():socket.observe(True) + except BaseException as e:secondary.append(dict(kind='socket-owner',error=str(e))) + try: + tails=[record_tail(A/'engine/infolog.txt','engine-infolog')] + for key in streams:tails.append(record_tail(A/'logs'/(key+'.raw'),key)) + m.write_new(A/'evidence/precleanup-observation.json',{'processes':snapshots,'tails':tails,'checkpoint':checkpoint_report(observed),'firstFailure':str(failure)if failure else None,'secondaryFailures':secondary}) + except BaseException as e:secondary.append(dict(kind='precleanup-capture',error=str(e))) + try:result['cleanup']=m.settle(owner,deadline);need(result['cleanup']['status']=='settled','discovery/cleanup Unknown') + except BaseException as e:secondary.append(dict(kind='cleanup',error=str(e)));result.setdefault('cleanup',{'status':'Unknown','reason':str(e)}) + for role,child in children.items(): + try:child.wait(timeout=max(.01,min(1,deadline-clock()))) + except BaseException as e:secondary.append(dict(kind='actor-reap',error=str(e))) + owner.leaders.discard(child.pid) + for stream in [child.stdin,child.stdout,child.stderr]: + if stream is not None and not stream.closed:stream.close() + try:result['socketLifecycle']=socket.cleanup(result.get('cleanup',{}).get('status')=='settled') + except BaseException as e:secondary.append(dict(kind='socket-cleanup',error=str(e)));result['socketLifecycle']={'status':'Unknown','error':str(e)} + for stream in streams.values():stream.close() + result['precleanupSeedQualificationAccepted']=result.get('seedQualificationAccepted',False) + result['seedQualificationAccepted']=bool(result.get('seedQualificationAccepted') and failure is None and not secondary and result.get('cleanup',{}).get('status')=='settled') + result.update(actorEligible=False,nativeUsefulPlay='0/6',dynamicContentProvenance='Unknown',creatorAttribution='Inference',exhaustiveMemoryOrIOProof=False,seedProofScope='Bounded through last captured complete snapshot; post-snapshot disconnect coverage unavailable',failure=str(failure)if failure else None,firstObservedNativeFault=observed.get('local-service-fault'),secondaryFailures=secondary,checkpoint=checkpoint_report(observed),checkpointRecords=observed,ownedIdentities=list(owner.records.values()),browserLoaded=False,Arm=False,commandsSubmitted=0);owner.close();m.write_new(A/'evidence/diagnostic-result.json',result);socket.close() + return 3 if failure or secondary else 0 + +def source_binding(a,p): + need(a['sourceSHA256']==reviewed_source()and a['profileSHA256']==m.digest(HERE/'startup-profile.json'),'shim/worker exact source') + +def shim(args): + p=profile();a=m.load(args.admission);source_binding(a,p);need(args.shim in ['host','engine','policy'],'literal roles');need((args.shim=='policy' and re.fullmatch('[0-9a-f]{64}',args.invocation or ''))or(args.shim!='policy' and args.invocation is None),'shim literal invocation');need(os.sched_getaffinity(0)=={a['cpu']},'shim affinity') + parent=m.load(pathlib.Path(ROOT)/'evidence/worker-start.json');need(m.same(parent['worker'],m.identity(os.getppid()))and parent['admissionSHA256']==m.digest(args.admission),'owned source-bound parent') + os.write(args.ready,b'BAR-STARTUP-READY\n');os.close(args.ready);need(os.read(args.ack,16)==b'GO\n','actual preexec observer release');os.close(args.ack);runtime_socket().directory_check();os.chdir(ROOT);c=commands(p,args.invocation)[args.shim];os.execve(c[0],c,environment(p,args.shim)) + +def worker(args): + p=profile();a=m.load(args.admission);source_binding(a,p);os.sched_setaffinity(0,{a['cpu']});start=m.load(pathlib.Path(ROOT)/'evidence/outer-start.json');need(m.same(start['outer'],m.identity(os.getppid()))and start['admissionSHA256']==m.digest(args.admission),'independent actual outer') + os.write(args.ready,b'BAR-STARTUP-WORKER\n');os.close(args.ready);need(os.read(args.ack,16)==b'GO\n','independent release');os.close(args.ack);m.write_new(pathlib.Path(ROOT)/'evidence/worker-start.json',dict(worker=m.identity(os.getpid()),admissionSHA256=m.digest(args.admission))) + return supervise(p,pathlib.Path(args.admission),start['deadline']) + +def finish_outer(A,result,buffers,started,failure): + """Retain an exclusive failure receipt even if final census/quota refuses.""" + from snapshot_publication import bytes_new,json_new + def failed(check,error): + nonlocal failure + value={'check':check,'errorType':type(error).__name__,'error':str(error),'custodyObservation':getattr(error,'observation',None)} + if failure is None:failure=error;result['firstFailure']=value + else:result.setdefault('secondaryFailures',[]).append(value) + for name,raw in buffers.items(): + try:bytes_new(A/'logs'/('outer-'+name+'.raw'),bytes(raw),524288) + except BaseException as e:failed('outer-output-publication',e) + try:json_new(A/'evidence/full-output-census.json',{'files':m.census(A,runtime_socket=runtime_socket()),'scope':'stage leaves before census and outer-result; independent root final census required'}) + except BaseException as e:result['finalCensus']={'status':'Unknown','reason':str(e)};failed('final-census',e) + try:result['finalQuota']=quota() + except BaseException as e:result['finalQuota']={'status':'Unknown','reason':str(e)};failed('final-quota',e) + try:runtime_socket().close() + except BaseException as e:failed('runtime-socket-close',e) + result.update(failure=str(failure)if failure else None,elapsedSeconds=clock()-started,operationCount=1,noRetry=True,actorEligible=False,dynamicContentProvenance='Unknown',creatorAttribution='Inference',exhaustiveMemoryOrIOProof=False,seedProofScope='Bounded through last captured complete snapshot; post-snapshot disconnect coverage unavailable',rootVerdict='Pending independent root full actual receipt review',nativeUsefulPlay='0/6',noBrowserGuestArmOrCommands=True) + settled=bool(result.get('independentCustody',{}).get('leaderReaped') and not result.get('independentCustody',{}).get('remaining') and not result.get('independentCustody',{}).get('errors') and result.get('innerCleanup',{}).get('status')=='settled') + result['cleanupStatus']='settled' if settled else 'Unknown' + result['seedQualificationAccepted']=bool(failure is None and result.get('innerSeedAccepted') and settled and not result.get('secondaryFailures')) + result['startupAccepted']=bool(result['seedQualificationAccepted'] and failure is None and settled and result.get('innerJoinsAccepted') and not result.get('secondaryFailures')) + # Reserve exactly this final receipt's bytes/leaf against the unchanged quota. + encoded=(json.dumps(result,sort_keys=True,separators=(',',':'),allow_nan=False)+'\n').encode();q=result.get('finalQuota',{}) + if 'files' in q and (q['files']+1>1024 or q['bytes']+len(encoded)>134217728): + failed('outer-receipt-reserve',m.Refused('stage output census bound'));result.update(startupAccepted=False,seedQualificationAccepted=False,failure=str(failure)) + # Failure recording deliberately follows the failing checks, without replacing them. + json_new(A/'evidence/outer-result.json',result,1048576) + return failure + +def execute(a_path): + started=clock();deadline=started+180;p=profile();source=reviewed_source();a=m.load(a_path);admission(a,p,source,m.identity(os.getppid()),datetime.datetime.now(datetime.timezone.utc),not os.path.lexists(ROOT),os.sched_getaffinity(0));os.sched_setaffinity(0,{a['cpu']});pre=originals(p,deadline-8) + A=pathlib.Path(ROOT);A.mkdir(mode=0o700) + for n in ['host','engine','logs','evidence']:(A/n).mkdir(mode=0o700) + for role in ['host','engine']:(A/role/'tmp').mkdir(mode=0o700) + validate_startscript(pathlib.Path(p['startscript']).read_bytes());run=A/'engine/run';run.mkdir(mode=0o700);need(not os.path.lexists(p['runtimeObjects']['socket']),'new socket must be absent');ds=run.lstat();m.write_new(A/'evidence/runtime-directory.json',dict(path=str(run),device=ds.st_dev,inode=ds.st_ino,uid=ds.st_uid,mode=stat.S_IMODE(ds.st_mode)));runtime_socket().directory_check() + from settings_custody import prepare_engine_settings + seed_argv,settings_receipt=prepare_engine_settings(p['seed']['configuration'],A);need(seed_argv==commands(p)['engine'],'actual isolated settings argv') + m.write_new(A/'evidence/outer-start.json',dict(outer=m.identity(os.getpid()),admissionSHA256=m.digest(a_path),deadline=deadline,preOriginals=pre));need(ctypes.CDLL(None,use_errno=True).prctl(36,1,0,0,0)==0,'outer subreaper') + rd,rw=os.pipe();ar,aw=os.pipe();proc=None;owned=None;failure=None;result={'startupAccepted':False,'firstFailure':None,'secondaryFailures':[]};buffers={'stdout':bytearray(),'stderr':bytearray()} + def fail(check,error): + nonlocal failure + observation={'check':check,'errorType':type(error).__name__,'error':str(error),'custodyObservation':getattr(error,'observation',None)} + if failure is None:failure=error;result['firstFailure']=observation + else:result['secondaryFailures'].append(observation) + try: + env={'PATH':'/usr/bin:/bin','HOME':str(A/'host'),'PYTHONDONTWRITEBYTECODE':'1'} + proc=subprocess.Popen([PYTHON,'-I','-B',str(HERE/'startup_runner.py'),'--worker','--admission',str(a_path),'--ready',str(rw),'--ack',str(ar)],env=env,pass_fds=(rw,ar),start_new_session=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE,umask=0o077);os.close(rw);rw=-1;os.close(ar);ar=-1;owned=outer.OwnedChild(proc,adopted=True) + with selectors.DefaultSelector()as sel:sel.register(rd,selectors.EVENT_READ);need(sel.select(m.remaining(deadline,5,8))and os.read(rd,64)==b'BAR-STARTUP-WORKER\n','independent actual worker handshake') + need(os.sched_getaffinity(proc.pid)=={a['cpu']},'independent actual affinity');m.write_new(A/'evidence/worker-independent-identity.json',dict(leader=owned.leader,cpu=a['cpu']));os.write(aw,b'GO\n');os.close(aw);aw=-1 + with selectors.DefaultSelector()as sel: + for name in buffers:sel.register(getattr(proc,name),selectors.EVENT_READ,name) + while sel.get_map()or owned.exited()is None: + m.remaining(deadline,1,8);owned.observe();quota() + for key,_ in sel.select(.02): + raw=os.read(key.fileobj.fileno(),65536) + if not raw:sel.unregister(key.fileobj);continue + buffers[key.data].extend(raw);need(len(buffers[key.data])<=524288,'outer output bound') + status=owned.exited();result['workerExitCode']=status.si_status if status.si_code==os.CLD_EXITED else-status.si_status + except BaseException as e:fail('outer-supervision',e) + finally: + for fd in [rd,rw,ar,aw]: + if fd>=0: + try:os.close(fd) + except OSError:pass + if owned: + try: + if runtime_socket().path.exists():runtime_socket().observe(True) + except BaseException as e:fail('outer-socket-owner',e) + try:result['independentCustody']=owned.settle(deadline);need(result['independentCustody']['leaderReaped']and not result['independentCustody']['remaining']and not result['independentCustody']['errors'],'independent settled cleanup') + except BaseException as e:result.setdefault('independentCustody',{'status':'Unknown','reason':str(e)});fail('independent-cleanup',e) + else:fail('independent-cleanup',m.CleanupUnknown('independent observer absent'));result['independentCustody']={'status':'Unknown','reason':'independent observer absent'} + try:result['socketLifecycle']=runtime_socket().cleanup(bool(result.get('independentCustody',{}).get('leaderReaped')and not result.get('independentCustody',{}).get('remaining')and not result.get('independentCustody',{}).get('errors'))) + except BaseException as e:result['socketLifecycle']={'status':'Unknown','reason':str(e)};fail('outer-socket-cleanup',e) + try:result['postOriginals']=originals(p,deadline) + except BaseException as e:fail('post-originals',e) + try: + inner=m.load(A/'evidence/diagnostic-result.json');result['readyProducerAuthenticated']=inner.get('readyProducerAuthenticated',False);result['firstFailedJoin']=inner.get('firstFailedJoin');result['innerCleanup']=inner.get('cleanup',{'status':'Unknown'});result['innerSecondaryFailures']=inner.get('secondaryFailures',[]) + result['innerSeedAccepted']=bool(inner.get('seedQualificationAccepted') and inner.get('canonicalPolicyConsumed') and inner.get('qualificationPublicationAcknowledged')) + result['innerJoinsAccepted']=bool(result['innerSeedAccepted'] and result.get('workerExitCode')==0 and inner.get('authenticatedReady') and inner.get('readySnapshotsPublicationAcknowledged') and inner.get('cleanup',{}).get('status')=='settled' and not inner.get('secondaryFailures')) + if inner.get('failure'):fail(inner.get('firstFailedJoin')or 'inner-operation',m.Refused(inner['failure'])) + elif result.get('workerExitCode')!=0:fail('inner-operation',m.Refused('worker failed; retained secondary failures' if inner.get('secondaryFailures') else 'worker failed')) + except BaseException as e:result.update(innerCleanup={'status':'Unknown','reason':'inner result unavailable'},innerJoinsAccepted=False,readyProducerAuthenticated=False,readyProducerAuthentication='Unknown');fail('inner-result',e) + failure=finish_outer(A,result,buffers,started,failure) + + return 0 if failure is None and result.get('workerExitCode')==0 else 3 + +def main(): + parser=argparse.ArgumentParser();parser.add_argument('--admission',required=True);parser.add_argument('--worker',action='store_true');parser.add_argument('--shim',choices=['host','engine','policy']);parser.add_argument('--ready',type=int);parser.add_argument('--ack',type=int);parser.add_argument('--invocation');a=parser.parse_args() + if a.shim:return shim(a) + if a.worker:return worker(a) + return execute(pathlib.Path(a.admission)) +if __name__=='__main__':raise SystemExit(main()) diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/test_native_evidence.py b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/test_native_evidence.py new file mode 100644 index 0000000..8713610 --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/test_native_evidence.py @@ -0,0 +1,28 @@ +"""Controlled bytes/process mocks only. Not native evidence.""" +import unittest,time,os,pathlib,tempfile,json +import native_evidence as n +import qualify_prearm as q +class Evidence(unittest.TestCase): + def test_observation_closed_and_unsynced_identity(self): + line=b'prefix BARC_PREARM_V1 kind=ai nonce=controlled frame=1 team=0 id=0 name=78 host=0 short=68696768426172 version=31 options=-\n' + row=n.observations(line,'controlled')[0];self.assertEqual(n.text(row['short']),'highBar');self.assertEqual(n.text(row['options']),'') + for body in [line.replace(b'nonce=controlled',b'nonce=foreign'),line.replace(b' id=0',b' id=0 id=0'),line+b'BARC_PREARM_V1 kind=refused nonce=controlled frame=1 reason=vfs\n',line.replace(b' options=-',b' options=- passed=true')]: + with self.assertRaises(q.Refused):n.observations(body,'controlled') + def test_prefix_append_is_not_completion(self): + with tempfile.TemporaryDirectory()as d: + p=pathlib.Path(d)/'live';body=b'{"sequence":"1"}\n';p.write_bytes(body);p.chmod(0o600);s=p.stat();pin={'path':str(p),'sha256':q.sha(body),'bytes':len(body),'device':s.st_dev,'inode':s.st_ino,'uid':s.st_uid,'mode':0o600,'nlink':1} + with p.open('ab')as f:f.write(b'{"sequence":"2"}\n') + self.assertEqual(q.prefix_bytes(pin,1024,time.monotonic()+1),body) + with self.assertRaises(q.Refused):q.held_bytes(pin,1024,time.monotonic()+1) + p.write_bytes(body[:-1]); + with self.assertRaises(q.Refused):q.prefix_bytes(pin,1024,time.monotonic()+1) + p.write_bytes(body.replace(b'1',b'3')) + with self.assertRaises(q.Refused):q.prefix_bytes(pin,1024,time.monotonic()+1) + p.unlink();p.write_bytes(body) + with self.assertRaises(q.Refused):q.prefix_bytes(pin,1024,time.monotonic()+1) + def test_policy_is_distinct_and_mandatory(self): + with self.assertRaises(q.Refused):n.infolog({'canonicalReceipt':'fabricated'}, {},time.monotonic()+1) + with self.assertRaises(q.Refused):n.horizons({'schema':'bar.root-accepted-complete-record-horizons/v1'}, {},{},time.monotonic()+1) + def test_schema_does_not_assert_open_lua_files(self): + with self.assertRaises(q.Refused):n.configuration({'schema':'bar.root-accepted-loaded-configuration-custody/v1'}, {},time.monotonic()+1) +if __name__=='__main__':unittest.main() diff --git a/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/test_qualify_prearm.py b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/test_qualify_prearm.py new file mode 100644 index 0000000..fd885c7 --- /dev/null +++ b/tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/test_qualify_prearm.py @@ -0,0 +1,96 @@ +"""Controlled source fixtures only; never actual/native qualification evidence.""" +import unittest,tempfile,pathlib,json,os,time,copy +from unittest.mock import patch +import qualify_prearm as q +import prepare_policy_closure as policy +class Controlled(unittest.TestCase): + def setUp(self):self.tmp=tempfile.TemporaryDirectory();self.root=pathlib.Path(self.tmp.name);self.count=0 + def tearDown(self):self.tmp.cleanup() + def pin(self,v,raw=False): + self.count+=1;p=self.root/str(self.count);b=v if raw else json.dumps(v,separators=(',',':')).encode();p.write_bytes(b);p.chmod(0o600);s=p.stat();return {'path':str(p),'sha256':q.sha(b),'bytes':len(b),'device':s.st_dev,'inode':s.st_ino,'uid':s.st_uid,'mode':0o600,'nlink':1} + def framed(self,magic,rows): + text='\n'.join([magic,'length=00000000',*rows,'end'])+'\n';return text.replace('length=00000000','length='+str(len(text)).zfill(8)) + def fixture(self): + source={'fsbarCommit':'a'*40,'highbarCommit':'b'*40};writer={'pid':99,'startTicks':'123','uid':os.getuid()};factory={'id':'10','lifetime':'5'};products=[{'id':'11','lifetime':'6'}];base={'token':'eA==','matchIncarnation':'AAAAAAAAAAAAAAAAAAAAAA==','processIncarnation':'P','stateChannelIncarnation':'C','snapshotSendMonotonicNs':'1','effectiveCadenceFrames':1} + def basis(n):return {**base,'stateSequence':str(n*2),'frame':n*10} + context={'profile':'barc-live-tactical-stock-v1','tacticalRevision':2,'queueEvidenceScheme':2,'actor':factory,'catalogueId':base['matchIncarnation'],'catalogueRevision':'1','engineVersion':'2026.07.04','gameName':'controlled','gameVersion':'1','contentSha256':'c'*64,'domain':'production'} + raw=[];host=[];stock=[] + def hr(kind,value):host.append({'schema':'fsbar.barc-stock-host-journal/v1','runId':'controlled','source':source,'writer':writer,'sequence':str(len(host)+1),'kind':kind,'value':value}) + for n,kind,value in [(1,'snapshot',{'units':[{'unitId':10,'definitionId':1,'underConstruction':False,'buildProgress':1}]}),(2,'unit-created',{'unitId':11,'builderId':10}),(3,'snapshot',{'units':[{'unitId':10,'definitionId':1,'underConstruction':False,'buildProgress':1},{'unitId':11,'definitionId':2,'underConstruction':True,'buildProgress':0.5}]}),(4,'unit-finished',{'unitId':11}),(5,'snapshot',{'units':[{'unitId':10,'definitionId':1,'underConstruction':False,'buildProgress':1},{'unitId':11,'definitionId':2,'underConstruction':False,'buildProgress':1}]})]: + entries=[{'id':-2,'codedOptions':0,'tag':8,'float32params':[]}]if n==3 else [] + revision=q.stock_revision(context,[{'domain':'production',**x}for x in entries]);actors=[{'actor':factory,'queue':[{'domain':2,'revision':revision,'entries':[{'definitionId':2,'nativeTag':8}]if entries else [],'repeat':False,'complete':True,'evidenceScheme':2}]}]+([{'actor':products[0],'queue':[]}]if n in [3,5] else []) + if n in [1,3,5]:hr('live-metadata',{'tactical':{'basis':basis(n),'actors':actors}}) + raw.append({'schema':'fsbar.barc-one-unit-raw-state-journal/v1','runId':'controlled','source':source,'writer':writer,'acceptedGeneration':'G','sequence':str(n),'kind':kind,'stateSequence':str(n*2),'nativeFrame':n*10,'disposition':'materialized'if kind=='snapshot'else 'invalidated','value':value}) + if n in [2,4]:hr('stale',{'receivedSequence':str(n*2)}) + if n==5:hr('observation',{'stateSequence':'10'}) + if n in [1,3,5]: + req=self.framed('BARC_QUEUE_REQUEST/1',['bridge=barc-stock-queue-reader-v1','domain=production','unit=10']);response=self.framed('BARC_QUEUE_RESPONSE/1',['bridge=barc-stock-queue-reader-v1','request-sha256='+q.sha(req.encode()),'status=ok','domain=production','unit=10','count='+str(len(entries)),*['row='+q.row_text({'domain':'production',**x})for x in entries]]) + stock.append({'schema':'highbar.barc-stock-queue-trace/v1','runId':'controlled','sequence':str(len(stock)+1),'phase':'sample','readFrame':n*10,'perspectiveTeamId':0,'nativeBasis':basis(n),'context':context,'reader':{'kind':'CallRules','status':'complete','request':req,'response':response},'queue':{'revision':revision,'entries':entries},'dispatch':None}) + inputs={k:self.pin(b''.join(json.dumps(r,separators=(',',':')).encode()+b'\n'for r in rows),True)for k,rows in [('raw',raw),('host',host),('stock',stock)]} + inputs['metadata']=self.pin({'schema':'fsbar.barc-stock-native-smoke-metadata/v1','runId':'controlled','writer':writer,'actors':{'factory':factory},'basis':{'processIncarnation':'P','stateChannelIncarnation':'C','matchId':base['matchIncarnation']}});inputs['setup']=self.pin({'schema':'fsbar.barc-stock-native-smoke-setup/v1','runId':'controlled','writer':writer,'existingProductionDefinitionId':2,'productDefinitionId':3}) + inputs['configuration']=self.pin({'CONTROLLED':'native adapter tested separately'}) + record=self.pin({'CONTROLLED':'prefix custody tested separately'});now=time.monotonic_ns() + return {'schema':'bar.prearm-sealed-input-contract/v1','runId':'controlled','source':source,'writer':writer,'acceptedGeneration':'G','factory':factory,'setup':{'seedCount':1,'seedDefinitionId':2,'productDefinitionId':3},'inputs':inputs,'completeRecordEvidence':record,'lifecycle':{'startedMonotonicNs':str(now),'transitionDeadlineMonotonicNs':str(now+60_000_000_000),'totalDeadlineMonotonicNs':str(now+120_000_000_000)},'outputPath':str(self.root/'qualification')} + def derive(self,c): + with patch.object(q,'configuration_evidence',return_value={'processes':[{**c['writer'],'role':'host'}],'setupCompletion':{'completionNativeFrame':15}}),patch('native_evidence.horizons'):return q.derive(c,time.monotonic()+10) + def mutate_raw(self,c,change): + rows=[json.loads(x)for x in pathlib.Path(c['inputs']['raw']['path']).read_bytes().splitlines()];change(rows);pin=self.pin(b''.join(json.dumps(x,separators=(',',':')).encode()+b'\n'for x in rows),True);c['inputs']['raw']=pin + def test_controlled_positive_nonconsecutive_native_sequences(self): + c=self.fixture();r=self.derive(c);self.assertEqual(r['seedEvidence']['seedCount'],1);q.publish(r,c['outputPath']);self.assertEqual(pathlib.Path(c['outputPath']).stat().st_mode&0o777,0o600) + with self.assertRaises(FileExistsError):q.publish(r,c['outputPath']) + def test_lifecycle_mutants_refuse(self): + changes=[lambda r:r[1]['value'].update(builderId=0),lambda r:r[3].update(kind='unit-created'),lambda r:r[3].update(disposition='materialized'),lambda r:r[2].update(acceptedGeneration='foreign'),lambda r:r[2].update(kind='command-dispatch'),lambda r:r[4]['value']['units'][1].update(underConstruction=True),lambda r:r[4]['value']['units'].append({'unitId':12,'definitionId':2,'underConstruction':False,'buildProgress':1}),lambda r:r[1].update(sequence='3'),lambda r:r[1].update(disposition='gap'),lambda r:r[3].update(stateSequence='6')] + for change in changes: + with self.subTest(change=change):c=self.fixture();self.mutate_raw(c,change) + with self.assertRaises((q.Refused,KeyError,StopIteration)):self.derive(c) + def test_json_and_record_bounds(self): + for body in [b'{"a":1,"a":2}',b'{"a":NaN}',b'{"a":1}\0']: + with self.assertRaises(q.Refused):q.parse(body) + for body in [b'{"sequence":"1"}',b'{"sequence":"2"}\n',b'{"sequence":"1","x":"'+b'x'*32768+b'"}\n']: + with self.assertRaises(q.Refused):q.records(body,'controlled') + def test_physical_mutants(self): + p=self.pin(b'controlled',True);path=pathlib.Path(p['path']);path.write_bytes(b'tampering') + with self.assertRaises(q.Refused):q.held_bytes(p,100,time.monotonic()+1) + p=self.pin(b'controlled',True);os.link(p['path'],self.root/'link') + with self.assertRaises(q.Refused):q.held_bytes(p,100,time.monotonic()+1) + p=self.pin(b'controlled',True);path=pathlib.Path(p['path']);path.unlink();path.symlink_to(self.root/'link') + with self.assertRaises(OSError):q.held_bytes(p,100,time.monotonic()+1) + def test_expiry_and_missing_config(self): + c=self.fixture();c['lifecycle']['transitionDeadlineMonotonicNs']='1' + with self.assertRaises(q.Refused):self.derive(c) + c=self.fixture();del c['inputs']['configuration'] + with self.assertRaises(q.Refused):self.derive(c) + def test_stock_response_revision_not_boolean(self): + c=self.fixture();rows=[json.loads(x)for x in pathlib.Path(c['inputs']['stock']['path']).read_bytes().splitlines()];rows[1]['queue']['revision']='1' + with self.assertRaises(q.Refused):q.stock_record(rows[1]) + def test_actual_pid_generation_inspector(self): + text=pathlib.Path('/proc/self/stat').read_text();ticks=text[text.rfind(')')+2:].split()[19];q.live_process({'pid':os.getpid(),'startTicks':ticks,'uid':os.getuid()}) + with self.assertRaises(q.Refused):q.live_process({'pid':os.getpid(),'startTicks':str(int(ticks)+1),'uid':os.getuid()}) + def test_exact_record_limits_and_partial_tail(self): + base=b'{"sequence":"1","padding":""}';padding=q.MAX_ROW-len(base)-1;exact=base.replace(b'"padding":""',b'"padding":"'+b'x'*padding+b'"')+b'\n';self.assertEqual(len(exact),q.MAX_ROW);q.records(exact,'raw') + with self.assertRaises(q.Refused):q.records(exact[:-2]+b'x"\n','raw') + with self.assertRaises(q.Refused):q.records(exact[:-1],'raw') + body=b''.join(json.dumps({'sequence':str(i+1)}).encode()+b'\n'for i in range(q.MAX_ROWS));q.records(body,'raw') + with self.assertRaises(q.Refused):q.records(body+b'{"sequence":"8193"}\n','raw') + def test_snapshot_join_and_generation_refusal(self): + c=self.fixture();self.mutate_raw(c,lambda rows:rows[0].update(nativeFrame=11)) + with self.assertRaises(q.Refused):self.derive(c) + c=self.fixture();self.mutate_raw(c,lambda rows:rows[4].update(stateSequence='8')) + with self.assertRaises(q.Refused):self.derive(c) + def test_reported_host_gap_and_lifetime_refusals(self): + for mutant in ['gap','detach','lifetime']: + c=self.fixture();rows=[json.loads(x)for x in pathlib.Path(c['inputs']['host']['path']).read_bytes().splitlines()] + if mutant in ['gap','detach']:next(x for x in rows if x['kind']=='stale')['value']['detail']='state sequence gap'if mutant=='gap'else 'coordinator detached' + else: + tactical=[x['value']['tactical']for x in rows if x['kind']=='live-metadata'];tactical[1]['actors'][1]['actor']['lifetime']='99' + c['inputs']['host']=self.pin(b''.join(json.dumps(x,separators=(',',':')).encode()+b'\n'for x in rows),True) + with self.assertRaises(q.Refused):self.derive(c) + def test_no_fabricated_boolean_contract(self): + with self.assertRaises(q.Refused):q.derive({'actualPassed':True,'noAutonomousOrders':True},time.monotonic()+1) + c=self.fixture();pin=c['inputs']['raw'];body=pathlib.Path(pin['path']).read_bytes();pathlib.Path(pin['path']).write_bytes(body+b'{') + self.derive(c) # Later append is legal; the captured complete extent stays unchanged. + def test_policy_preparation_is_obstructed(self): + roles=lambda names:[{'role':x,'path':'/controlled/'+x,'sha256':'a'*64}for x in names] + original={'schema':'fsbar.barc-runtime-evidence-policy-closure/v3','custodyRoot':'/controlled','managedRoot':'/controlled/m','provenanceRoot':'/controlled/p','runtimeRoots':['/controlled/r'],'searchLayout':[],'managed':roles(policy.MANAGED),'provenance':roles(policy.PROVENANCE),'runtime':[{'path':'/controlled/lib'}],'runtimeRoles':{x:'/controlled/'+x for x in policy.RUNTIME},'source':{'productCommit':'a'*40,'productTree':'b'*40},'custody':{}} + result=policy.prepare(self.pin(original),{'productCommit':'c'*40,'productTree':'d'*40,'highbarCommit':'e'*40},str(self.root/'fresh'),time.monotonic()+5);self.assertFalse(result['ready']);self.assertIn('productCommit-mismatch',result['obstructions']);self.assertFalse((self.root/'fresh').exists()) +if __name__=='__main__':unittest.main() diff --git a/tests/Broker.NativeProof/one-unit-prearm.test.mjs b/tests/Broker.NativeProof/one-unit-prearm.test.mjs new file mode 100644 index 0000000..7c17eac --- /dev/null +++ b/tests/Broker.NativeProof/one-unit-prearm.test.mjs @@ -0,0 +1,48 @@ +// Controlled source checks only. No native handoff, browser, Arm or production effect. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import vm from "node:vm"; +const source=readFileSync(new URL("./tactical-native-journey.spec.js",import.meta.url),"utf8"); +const start=source.indexOf("// ONE_UNIT_SOURCE_BEGIN"),end=source.indexOf("if(oneUnitEnabled)oneUnitHandoff=",start); +assert.ok(start>=0&&end>start); +const context=vm.createContext({Buffer,Set,BigInt,Number,Object,JSON,Error,Array,String, + key:r=>`${r.id}:${r.lifetime}`,sameRef:(a,b)=>a?.id===b?.id&&a?.lifetime===b?.lifetime, + queue:(o,f)=>o?.productionQueue}); +vm.runInContext(source.slice(start,end).replace("export function assertOneProducedUnit","function assertOneProducedUnit")+"\n globalThis.api={validateOneQualification,assertOnePreArm,assertOneProducedUnit};",context); +const {api}=context; +function fixture(){ + const factory={id:"10",lifetime:"5"},seed={id:"11",lifetime:"6"},writer={pid:99,startTicks:"123",uid:1000},source={fsbarCommit:"a".repeat(40),highbarCommit:"b".repeat(40)}; + const h={runId:"controlled",source,actors:{factory},setup:{seedDefinitionId:2,productDefinitionId:3},paths:{nativeEvents:"/controlled/raw",stockTrace:"/controlled/stock"},attribution:{writer,acceptedGeneration:"G",processIncarnation:"P",stateChannelIncarnation:"C"}}; + const pin=path=>({path,sha256:"c".repeat(64),bytes:1,device:1,inode:1,uid:1000,mode:0o600,nlink:1}); + const q={schema:"bar.one-unit-attribution-source-qualification/v1",actualPassed:true,noAutonomousOrders:true,runId:h.runId,source,writer,acceptedGeneration:"G",processIncarnation:"P",stateChannelIncarnation:"C",matchIncarnation:"M",noAutonomousConfigurationEvidenceSha256:"c".repeat(64),seedEvidence:{factory,seedDefinitionId:2,productDefinitionId:3,seedCount:1,initialRawSequence:"1",nonemptyStockSequence:"1",terminalRawSequence:"4",terminalStateSequence:"8",terminalNativeFrame:40,emptyStockSequence:"2",products:[{reference:seed,createdSequence:"2",finishedSequence:"3"}]},inputCustody:{raw:pin("/controlled/raw"),host:pin("/controlled/host"),stock:pin("/controlled/stock"),metadata:pin("/controlled/metadata"),setup:pin("/controlled/setup"),configuration:pin("/controlled/config")},liveProcesses:[{...writer,role:"host"}],completeRecordEvidence:pin("/controlled/policy"),lifecycle:{startedMonotonicNs:"1",transitionDeadlineMonotonicNs:"1000000000",totalDeadlineMonotonicNs:"2000000000"}}; + const before={basis:{processIncarnation:"P",stateChannelIncarnation:"C",matchId:"M",stateSequence:"10",nativeFrame:50},units:[{reference:factory,definitionId:1},{reference:seed,definitionId:2}],productionQueue:{complete:true,repeat:false,entries:[]}}; + const native=[{kind:"baseline",stateSequence:"10",value:{units:[{unitId:10,underConstruction:false,buildProgress:1},{unitId:11,underConstruction:false,buildProgress:1}]}}]; + return {h,q,before,native,rows:[]}; +} +test("retained earlier nonempty history permits a fresh current empty baseline",()=>{ + const f=fixture();assert.equal(api.assertOnePreArm(f.h,f.q,f.rows,f.before,f.native),f.before); +}); +test("qualification tampering and foreign owning identity refuse before Arm",()=>{ + const changes=[f=>f.q.actualPassed=false,f=>f.q.noAutonomousOrders=false,f=>f.q.runId="foreign",f=>f.q.acceptedGeneration="foreign",f=>f.q.stateChannelIncarnation="foreign",f=>f.q.source={...f.q.source,fsbarCommit:"d".repeat(40)},f=>f.q.seedEvidence.finishedSequence="0",f=>f.q.seedEvidence.products[0].finishedSequence="2",f=>f.q.seedEvidence.seedCount=2,f=>f.q.seedEvidence.emptyStockSequence="1",f=>f.q.inputCustody.raw.path="/other",f=>f.q.liveProcesses[0].startTicks="999",f=>f.q.extra=true]; + for(const change of changes){const f=fixture();change(f);let armCount=0;assert.throws(()=>{api.assertOnePreArm(f.h,f.q,f.rows,f.before,f.native);armCount++},/one-unit refused/);assert.equal(armCount,0)} +}); +test("current stale/nonempty/unfinished baseline and later dispatch refuse",()=>{ + const changes=[f=>f.before.basis.stateSequence="6",f=>f.before.basis.stateChannelIncarnation="other",f=>f.before.productionQueue.entries=[{definitionId:2}],f=>f.before.productionQueue.repeat=true,f=>f.before.productionQueue.complete=false,f=>f.native[0].value.units[1].underConstruction=true,f=>f.before.units.pop(),f=>f.rows.push({kind:"submit"}),f=>f.native.push({kind:"command-dispatch"}),f=>f.native.push({kind:"unit-created"}),f=>f.native.push({kind:"unit-finished"}),f=>f.native.push({kind:"snapshot",disposition:"gap"}),f=>f.native[0].stateSequence="9"]; + for(const change of changes){const f=fixture();change(f);assert.throws(()=>api.assertOnePreArm(f.h,f.q,f.rows,f.before,f.native),/one-unit refused/)} +}); +test("production source checks late extra creation and dispatch before freezing baseline",()=>{ + assert.match(source,/production or gap after seed qualification before baseline/); + assert.match(source,/\["unit-created","unit-finished","command-dispatch"\]\.includes\(r.kind\)/); + assert.match(source,/assertOnePreArm\(h,qualified,rows,before,nativeOneEvents/); + assert.ok(source.indexOf("recheckOneQualification(h);return h")>0); +}); +test("single-submit assertion rejects a retry before lifecycle evaluation",()=>{ + assert.throws(()=>api.assertOneProducedUnit([{kind:"submit",value:{}},{kind:"submit",value:{}}],{},[],{}),/exactly one submission required, no retry/); +}); + +test("handoff qualification initializes after lexical validators and before registration",()=>{ + assert.ok(source.indexOf("let oneUnitHandoff=null")1)throw new Error("native proof routes are mutually exclusive"); const handoff=enabled?await loadTacticalHandoff(process.env.BARC_TACTICAL_HANDOFF):null,stockHandoff=stockEnabled?await loadStockSmokeHandoff(process.env.BARC_STOCK_SMOKE_HANDOFF):null; let stage,codec; -test.beforeAll(async()=>{if(!enabled&&!stockEnabled)return;stage=await mkdtemp(resolve(import.meta.dirname,"node_modules/.barc-tactical-codec-"));await cp(enabled?handoff.codecDirectory:stockHandoff.paths.codecDirectory,stage,{recursive:true});codec=await import(pathToFileURL(resolve(stage,"codec.js")).href)}); +let oneUnitHandoff=null; // Initialized after the closed one-unit validators, before browser hooks run. +test.beforeAll(async()=>{if(!enabled&&!stockEnabled&&!oneUnitEnabled)return;stage=await mkdtemp(resolve(import.meta.dirname,"node_modules/.barc-tactical-codec-"));await cp(enabled?handoff.codecDirectory:(oneUnitEnabled?oneUnitHandoff.paths.codecDirectory:stockHandoff.paths.codecDirectory),stage,{recursive:true});codec=await import(pathToFileURL(resolve(stage,"codec.js")).href)}); test.afterAll(async()=>{if(stage)await rm(stage,{recursive:true,force:true})}); const key=ref=>`${ref.id}:${ref.lifetime}`, sameRef=(a,b)=>String(a?.id??0)===b.id&&String(a?.lifetime??0)===b.lifetime; @@ -22,10 +25,10 @@ const queue=(o,ref,domain)=>actor(o,ref)?.queue?.find(x=>x.domain===domain); const near=(a,b,t=64)=>a&&Math.hypot(a.x-b.x,a.z-b.z)<=t; const startTicks=()=>{const value=readFileSync(`/proc/${process.pid}/stat`,"utf8"),close=value.lastIndexOf(") "),fields=value.slice(close+2).trim().split(/ +/);if(close<0||fields.length<=19)throw new Error("browser writer start identity unavailable");return fields[19]}; -function captureJournal(path,release,runId,source){ +function captureJournal(path,release,runId,source,selectedCase="stock-smoke-count1"){ if(!path||!release||existsSync(path)||existsSync(release))throw new Error("new stock capture/release paths required");const sourcePath=import.meta.filename,writer={pid:process.pid,startTicks:startTicks(),uid:process.geteuid(),executable:process.execPath,sourcePath,sourceSha256:createHash("sha256").update(readFileSync(sourcePath)).digest("hex")},fd=openSync(path,constants.O_WRONLY|constants.O_CREAT|constants.O_EXCL|constants.O_APPEND|constants.O_CLOEXEC,0o600);let sequence=0,closed=false; - const append=(kind,value)=>{if(closed)throw new Error("stock capture already completed");sequence++;writeSync(fd,`${JSON.stringify({schema:"fsbar.barc-stock-browser-capture/v1",runId,sequence:String(sequence),kind,writer,source,value})}\n`)};append("header",{selectedCase:"stock-smoke-count1"}); - return{append,complete(){append("complete",{selectedCase:"stock-smoke-count1"});fsyncSync(fd);closed=true},async hold(){await expect.poll(()=>existsSync(release),{timeout:30000,intervals:[25,50,100,250]}).toBe(true)},close(){closeSync(fd)}}; + const append=(kind,value)=>{if(closed)throw new Error("stock capture already completed");sequence++;writeSync(fd,`${JSON.stringify({schema:"fsbar.barc-stock-browser-capture/v1",runId,sequence:String(sequence),kind,writer,source,value})}\n`)};append("header",{selectedCase}); + return{append,complete(){append("complete",{selectedCase});fsyncSync(fd);closed=true},async hold(){await expect.poll(()=>existsSync(release),{timeout:30000,intervals:[25,50,100,250]}).toBe(true)},close(){closeSync(fd)}}; } // Readiness controls execute this exact source block without loading native handoffs. const pairingState=()=>({observed:false,authSentCount:0,bootstrapDecodedCount:0,decodeFailureCount:0,clientCloseCount:0,closed:false,error:false,overflow:false}); @@ -100,6 +103,7 @@ async function pair(page,j,guest="Manual guest",rows){ await page.goto(j.receiverUrl);await live.getByLabel("Gateway").fill(j.gatewayUrl);await live.getByLabel("Session UUID").fill(j.expectedSessionId);await live.getByLabel("One-time credential").fill(j.credential);await live.getByRole("button",{name:"Pair"}).click(); await expect(live.locator(".catalogue")).not.toContainText("unavailable");await expect(live.locator(".economy")).toContainText("Economy"); await loadSelectedGuest(page,live,j,guest,state); + if(j.deferArm)return live; state.phase="arm-clicked";await live.getByRole("button",{name:"Arm live"}).click(); await expect.poll(()=>state.armSha256).toBe(state.moduleSha256);await expect(live.locator(".authority")).toContainText("arm native confirmed");state.phase="native-confirmed";return live; }catch(error){pairingFailed=true;state.failureCode??=state.phase==="arm-clicked"?"authority-not-confirmed":"pair-or-module-not-ready";await writeArmFailure(live,j,state);throw error} @@ -188,3 +192,135 @@ test.describe("selected stock Count1 smoke",()=>{ }finally{journal.close()} }); }); + + +// ONE_UNIT_SOURCE_BEGIN: dedicated proof, never a smoke or full-six alias. +const oneUnitCase="selected stock one-unit"; +const insistOne=(condition,message)=>{if(!condition)throw new Error(`one-unit refused: ${message}`)}; +const closedOne=(value,keys,label)=>insistOne(value&&typeof value==="object"&&!Array.isArray(value)&&JSON.stringify(Object.keys(value).sort())===JSON.stringify([...keys].sort()),`${label} keys changed`); +const qualificationOneKeys=["schema","actualPassed","noAutonomousOrders","runId","source","writer","acceptedGeneration","processIncarnation","stateChannelIncarnation","matchIncarnation","noAutonomousConfigurationEvidenceSha256","seedEvidence","inputCustody","liveProcesses","completeRecordEvidence","lifecycle"]; +function validateOneQualification(h,q){ + closedOne(q,qualificationOneKeys,"qualification"); + insistOne(q.schema==="bar.one-unit-attribution-source-qualification/v1"&&q.actualPassed===true&&q.noAutonomousOrders===true&&q.runId===h.runId&&JSON.stringify(q.source)===JSON.stringify(h.source)&&JSON.stringify(q.writer)===JSON.stringify(h.attribution.writer)&&q.acceptedGeneration===h.attribution.acceptedGeneration&&q.processIncarnation===h.attribution.processIncarnation&&q.stateChannelIncarnation===h.attribution.stateChannelIncarnation&&typeof q.matchIncarnation==="string"&&q.matchIncarnation.length>0,"qualification owning identity changed"); + closedOne(q.seedEvidence,["factory","seedDefinitionId","productDefinitionId","seedCount","initialRawSequence","nonemptyStockSequence","terminalRawSequence","terminalStateSequence","terminalNativeFrame","emptyStockSequence","products"],"seed evidence");const e=q.seedEvidence; + insistOne(sameRef(e.factory,h.actors.factory)&&e.seedDefinitionId===h.setup.seedDefinitionId&&e.productDefinitionId===h.setup.productDefinitionId&&Number.isInteger(e.seedCount)&&e.seedCount>0&&e.seedCount<=16&&Array.isArray(e.products)&&e.products.length===e.seedCount,"reviewed distinct finite seed history missing"); + const decimal=x=>typeof x==="string"&&/^[1-9][0-9]*$/.test(x)&&BigInt(x)<=18446744073709551615n; + insistOne([e.initialRawSequence,e.nonemptyStockSequence,e.terminalRawSequence,e.terminalStateSequence,e.emptyStockSequence].every(decimal)&&BigInt(e.terminalRawSequence)>BigInt(e.initialRawSequence)&&BigInt(e.emptyStockSequence)>BigInt(e.nonemptyStockSequence)&&Number.isInteger(e.terminalNativeFrame)&&e.terminalNativeFrame>=0,"seed history causality changed"); + for(const product of e.products){closedOne(product,["reference","createdSequence","finishedSequence"],"seed product");closedOne(product.reference,["id","lifetime"],"seed reference");insistOne(decimal(product.reference.id)&&decimal(product.reference.lifetime)&&decimal(product.createdSequence)&&decimal(product.finishedSequence)&&BigInt(product.createdSequence)>BigInt(e.initialRawSequence)&&BigInt(product.finishedSequence)>BigInt(product.createdSequence)&&BigInt(product.finishedSequence)key(p.reference))).size===e.seedCount&&new Set(e.products.map(p=>p.reference.id)).size===e.seedCount,"duplicate seed lifetime"); + closedOne(q.inputCustody,["raw","host","stock","metadata","setup","configuration"],"qualified inputs");insistOne(q.inputCustody.raw.path===h.paths.nativeEvents&&q.inputCustody.stock.path===h.paths.stockTrace&&q.inputCustody.configuration.sha256===q.noAutonomousConfigurationEvidenceSha256,"retained feed/configuration join changed"); + closedOne(q.lifecycle,["startedMonotonicNs","transitionDeadlineMonotonicNs","totalDeadlineMonotonicNs"],"owned lifecycle");const l=q.lifecycle;insistOne(Object.values(l).every(decimal)&&BigInt(l.startedMonotonicNs)0&&q.liveProcesses.length<=4,"owned live process census missing"); + for(const process of q.liveProcesses){closedOne(process,["pid","startTicks","uid","role"],"live process");insistOne(Number.isInteger(process.pid)&&process.pid>0&&decimal(process.startTicks)&&Number.isInteger(process.uid)&&process.uid>=0&&["host","engine"].includes(process.role),"live process identity changed")} + insistOne(q.liveProcesses.some(p=>p.role==="host"&&p.pid===q.writer.pid&&p.startTicks===q.writer.startTicks&&p.uid===q.writer.uid),"live host writer join missing");return q; +} +function readPinnedOne(pin,limit,growing=false){ + closedOne(pin,["path","sha256","bytes","device","inode","uid","mode","nlink"],"physical pin");insistOne(typeof pin.path==="string"&&pin.path.startsWith("/")&&resolve(pin.path)===pin.path&&/^[a-f0-9]{64}$/.test(pin.sha256)&&Number.isSafeInteger(pin.bytes)&&pin.bytes>0&&pin.bytes<=limit,"physical pin format changed"); + for(let parent=dirname(pin.path);;parent=dirname(parent)){const st=lstatSync(parent);insistOne(st.isDirectory()&&!st.isSymbolicLink(),"input ancestor link");if(parent==="/")break} + const fd=openSync(pin.path,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_CLOEXEC); + try{const st=fstatSync(fd),check=()=>{const now=fstatSync(fd),named=lstatSync(pin.path);insistOne(now.isFile()&&!named.isSymbolicLink()&&now.nlink===1&&now.dev===pin.device&&now.ino===pin.inode&&now.uid===pin.uid&&(now.mode&0o777)===pin.mode&&named.dev===now.dev&&named.ino===now.ino&&now.size<=limit&&(growing?now.size>=pin.bytes:now.size===pin.bytes),"retained physical custody changed")};check();const body=Buffer.alloc(pin.bytes);let offset=0;while(offset0,"retained input short read");offset+=count}insistOne(createHash("sha256").update(body).digest("hex")===pin.sha256,"retained input prefix changed");check();return body}finally{closeSync(fd)} +} +function readQualifiedOne(h){ + const st=lstatSync(h.paths.eventQualification);insistOne(st.isFile()&&!st.isSymbolicLink()&&st.nlink===1&&(st.mode&0o777)===0o600&&st.size>0&&st.size<=65536,"qualification physical custody changed"); + return readPinnedOne({path:h.paths.eventQualification,sha256:h.attribution.qualificationSha256,bytes:st.size,device:st.dev,inode:st.ino,uid:st.uid,mode:0o600,nlink:1},65536); +} +function mappedOneConfiguration(q,cfg){ + const engines=q.liveProcesses.filter(p=>p.role==="engine");insistOne(engines.length===1,"current engine custody missing");const fd=openSync(`/proc/${engines[0].pid}/maps`,constants.O_RDONLY|constants.O_CLOEXEC),bytes=Buffer.alloc(1024*1024+1);let offset=0; + try{while(offset{const m=/^\S+\s+\S+\s+\S+\s+([0-9a-f]+):([0-9a-f]+)\s+(\d+)\s+(.+)$/.exec(line);if(!m||m[4]!==pin.path)return false;const major=BigInt(`0x${m[1]}`),minor=BigInt(`0x${m[2]}`),dev=((major&4095n)<<8n)|(minor&255n)|((minor&~255n)<<12n)|((major&~4095n)<<32n);return dev===BigInt(pin.device)&&BigInt(m[3])===BigInt(pin.inode)}),"engine/plugin mapped custody changed before Arm")} +} +function recheckOneQualification(h){ + const bytes=readQualifiedOne(h);insistOne(bytes.length<=65536&&createHash("sha256").update(bytes).digest("hex")===h.attribution.qualificationSha256,"qualification changed before Arm");const q=validateOneQualification(h,parseClosedJson(bytes.toString("utf8"))); + // process.hrtime and Python monotonic_ns share CLOCK_MONOTONIC on admitted Linux. + const now=process.hrtime.bigint();insistOne(now>=BigInt(q.lifecycle.startedMonotonicNs)&&now{insistOne(depth<=16&&++count<=1024,"configuration pin traversal bound");if(value&&typeof value==="object"){if(typeof value.path==="string"&&typeof value.sha256==="string"&&"inode"in value){const body=readPinnedOne(value,128*1024*1024);if(["loaded","configuration","review","setupCompletion","aiSlots","buildReceipt","canonicalReceipt","completeRecordEvidence"].includes(name))walk(parseClosedJson(body.toString("utf8")),depth+1,name)}else for(const [key,child]of Object.entries(value))walk(child,depth+1,key)}};const cfg=parseClosedJson(readPinnedOne(q.inputCustody.configuration,65536).toString("utf8"));walk(cfg);mappedOneConfiguration(q,cfg);walk(q.completeRecordEvidence,0,"completeRecordEvidence"); + for(const p of q.liveProcesses){const text=readFileSync(`/proc/${p.pid}/stat`,"utf8"),tail=text.slice(text.lastIndexOf(")")+2).trim().split(/\s+/),uid=/^Uid:\s+(\d+)\s+/m.exec(readFileSync(`/proc/${p.pid}/status`,"utf8"));insistOne(!["Z","X"].includes(tail[0])&&tail[19]===p.startTicks&&uid&&Number(uid[1])===p.uid,"live PID generation changed before Arm")} + return q; +} +function assertOnePreArm(h,q,rows,before,native){ + validateOneQualification(h,q);const e=q.seedEvidence,f=h.actors.factory,b=before?.basis; + insistOne(b&&b.processIncarnation===q.processIncarnation&&b.stateChannelIncarnation===q.stateChannelIncarnation&&b.matchId===q.matchIncarnation&&BigInt(b.stateSequence)>=BigInt(e.terminalStateSequence)&&Number(b.nativeFrame)>=e.terminalNativeFrame,"fresh same-generation baseline missing"); + const current=queue(before,f,"QUEUE_DOMAIN_FACTORY_PRODUCTION");insistOne(current?.complete===true&¤t.repeat===false&¤t.entries?.length===0,"fresh empty repeat-off baseline missing"); + const rawUnits=native[0]?.value?.units;insistOne(Array.isArray(before.units)&&Array.isArray(rawUnits)&&rawUnits.length>0&&rawUnits.every(u=>u.underConstruction===false&&u.buildProgress===1),"fresh baseline contains unfinished units"); + insistOne(e.products.every(p=>before.units.some(u=>sameRef(u.reference,p.reference)&&Number(u.definitionId)===h.setup.seedDefinitionId)),"qualified seed lifetime missing from fresh baseline"); + insistOne(rows.every(r=>!["submit","result"].includes(r.kind))&&native[0]?.kind==="baseline"&&native[0].stateSequence===b.stateSequence&&native.every(r=>!["unit-created","unit-finished","command-dispatch"].includes(r.kind)&&r.disposition!=="gap"),"pre-Arm dispatch or missing native baseline"); + return before; +} +function loadOneUnitHandoff(path,sha256){ + insistOne(typeof path==="string"&&path.startsWith("/")&&/^[a-f0-9]{64}$/.test(sha256??""),"pinned handoff required"); + const st=lstatSync(path),bytes=readFileSync(path);insistOne(st.isFile()&&!st.isSymbolicLink()&&st.nlink===1&&(st.mode&0o777)===0o600&&bytes.length<=65536&&createHash("sha256").update(bytes).digest("hex")===sha256,"handoff physical/body pin changed"); + const h=parseClosedJson(bytes.toString("utf8"));closedOne(h,["schema","runId","selectedCase","source","paths","connection","artifacts","actors","setup","attribution"],"handoff"); + insistOne(h.schema==="fsbar.barc-one-produced-unit-handoff/v1"&&h.selectedCase===oneUnitCase,"closed selected case required"); + closedOne(h.source,["fsbarCommit","highbarCommit"],"source");insistOne(Object.values(h.source).every(x=>/^[a-f0-9]{40}$/.test(x)),"exact source commits required"); + closedOne(h.connection,["receiverUrl","gatewayUrl","allowedOrigin","sessionId","credential"],"connection");const receiver=new URL(h.connection.receiverUrl),gateway=new URL(h.connection.gatewayUrl),origin=new URL(h.connection.allowedOrigin);insistOne(receiver.protocol==="http:"&&origin.protocol==="http:"&&gateway.protocol==="ws:"&&[receiver,origin,gateway].every(u=>u.hostname==="127.0.0.1"&&!u.username&&!u.password&&!u.hash)&&receiver.origin===origin.origin&&/^[a-f0-9]{64}$/.test(h.connection.credential),"exact private loopback connection required"); + closedOne(h.artifacts,["guest"],"artifacts");closedOne(h.artifacts.guest,["path","sha256"],"guest");insistOne(/^[a-f0-9]{64}$/.test(h.artifacts.guest.sha256),"actual produced guest digest required"); + closedOne(h.paths,["codecDirectory","capture","release","nativeEvents","stockTrace","eventQualification"],"paths"); + insistOne(Object.values(h.paths).every(p=>typeof p==="string"&&p.startsWith("/")),"absolute capsule paths required"); + closedOne(h.attribution,["qualified","qualificationSha256","writer","acceptedGeneration","processIncarnation","stateChannelIncarnation"],"attribution"); + insistOne(h.attribution.qualified===true&&/^[a-f0-9]{64}$/.test(h.attribution.qualificationSha256),"actual event-source qualification missing"); + const qualification=readQualifiedOne(h);insistOne(qualification.length<=65536&&createHash("sha256").update(qualification).digest("hex")===h.attribution.qualificationSha256,"event-source qualification body changed"); + const qualified=parseClosedJson(qualification.toString("utf8")); + closedOne(h.actors,["factory"],"actors");closedOne(h.setup,["productDefinitionId","seedDefinitionId","maximumSeconds"],"setup"); + insistOne(h.setup.maximumSeconds===180&&Number.isInteger(h.setup.productDefinitionId)&&h.setup.productDefinitionId>0&&Number.isInteger(h.setup.seedDefinitionId)&&h.setup.seedDefinitionId>0&&h.setup.seedDefinitionId!==h.setup.productDefinitionId,"finite distinct seed/product setup required"); + // Missing capture-source or native feed refuses before browser pairing/Arm. + insistOne(existsSync(h.paths.nativeEvents),"genuine native event feed missing");validateOneQualification(h,qualified);recheckOneQualification(h);return h; +} +function nativeOneEvents(h,rows,before){ + const st=lstatSync(h.paths.nativeEvents);insistOne(st.isFile()&&!st.isSymbolicLink()&&st.nlink===1&&st.size<=4*1024*1024,"native event stream physical bound changed"); + const text=readFileSync(h.paths.nativeEvents,"utf8");insistOne(text.endsWith("\n"),"native partial record refused");const lines=text.split("\n");lines.pop();insistOne(lines.length>0&&lines.length<=8192,"native event row bound changed"); + const raw=lines.map((line,index)=>{insistOne(Buffer.byteLength(line)+1<=32768,"native event record oversized");const r=parseClosedJson(line);closedOne(r,["schema","runId","sequence","kind","writer","source","acceptedGeneration","stateSequence","nativeFrame","disposition","value"],"native raw row");insistOne(r.schema==="fsbar.barc-one-unit-raw-state-journal/v1"&&r.runId===h.runId&&r.sequence===String(index+1)&&JSON.stringify(r.writer)===JSON.stringify(h.attribution.writer)&&JSON.stringify(r.source)===JSON.stringify(h.source)&&r.acceptedGeneration===h.attribution.acceptedGeneration,"native raw origin/generation/sequence changed");insistOne(["snapshot","unit-created","unit-finished","command-dispatch"].includes(r.kind)&&["materialized","invalidated","gap","liveness-only"].includes(r.disposition)&&/^[0-9]+$/.test(r.stateSequence)&&Number.isSafeInteger(r.nativeFrame)&&r.nativeFrame>=0,"unknown native event or basis");return r}); + // Freeze the pre-Arm baseline by its actual browser/native state position. + const snapshot=raw.find(r=>r.kind==="snapshot"&&r.disposition==="materialized"&&r.stateSequence===before.basis.stateSequence&&r.nativeFrame===Number(before.basis.nativeFrame));insistOne(snapshot,"actual same-basis baseline snapshot missing"); + insistOne(snapshot.value.units.length>0&&snapshot.value.units.every(u=>u.underConstruction===false&&u.buildProgress===1),"native baseline contains unfinished units"); + const q=validateOneQualification(h,parseClosedJson(readQualifiedOne(h).toString("utf8")));for(const r of raw.filter(r=>BigInt(r.sequence)>BigInt(q.seedEvidence.terminalRawSequence)&&BigInt(r.sequence)<=BigInt(snapshot.sequence)))insistOne(!["unit-created","unit-finished","command-dispatch"].includes(r.kind)&&r.disposition!=="gap","production or gap after seed qualification before baseline");const result=raw.filter(r=>BigInt(r.sequence)>BigInt(snapshot.sequence));result.unshift({...snapshot,kind:"baseline"}); + const stockStat=lstatSync(h.paths.stockTrace);insistOne(stockStat.isFile()&&!stockStat.isSymbolicLink()&&stockStat.nlink===1&&stockStat.size<=16*1024*1024,"stock reader physical bound changed");let stockText=readFileSync(h.paths.stockTrace,"utf8");insistOne(stockText.endsWith("\n"),"stock partial record refused");const stock=parseStockTraceJsonl(stockText); + for(const r of stock){if(r.reader.status!=="complete")continue;insistOne(r.runId===h.runId&&sameRef(r.context.actor,h.actors.factory)&&r.nativeBasis.processIncarnation===h.attribution.processIncarnation&&r.nativeBasis.stateChannelIncarnation===h.attribution.stateChannelIncarnation,"stock queue origin/basis changed"); + const o=observations(rows).find(o=>o.basis.stateSequence===r.nativeBasis.stateSequence&&o.basis.processIncarnation===r.nativeBasis.processIncarnation&&o.basis.stateChannelIncarnation===r.nativeBasis.stateChannelIncarnation&&o.basis.matchId===r.nativeBasis.matchIncarnation&&Number(o.basis.nativeFrame)===r.nativeBasis.frame),q=queue(o,h.actors.factory,"QUEUE_DOMAIN_FACTORY_PRODUCTION"); + if(r.context.domain!=="production"||q?.complete!==true||q.repeat!==false||String(q.revision)!==r.queue.revision||q.entries?.length!==r.queue.entries.length)continue; + result.push({...snapshot,kind:r.phase==="final-read"?"final-read":"queue-observation",sequence:r.sequence,stateSequence:r.nativeBasis.stateSequence,nativeFrame:r.readFrame,value:{actor:r.context.actor,nativeBasis:r.nativeBasis,catalogueId:r.context.catalogueId,catalogueRevision:r.context.catalogueRevision,queueRevision:r.queue.revision,complete:true,repeat:false,entries:r.queue.entries.map(e=>({definitionId:-e.id,codedOptions:e.codedOptions,tag:e.tag,float32params:e.float32params})),dispatch:r.dispatch}}); + }return result; +} +export function assertOneProducedUnit(rows,before,native,h){ + const submits=rows.filter(r=>r.kind==="submit").map(r=>r.value);insistOne(submits.length===1,"exactly one submission required, no retry");const s=submits[0],f=h.actors.factory; + insistOne(s.intent?.action==="factoryProduce"&&s.intent.actors?.length===1&&sameRef(s.intent.actors[0],f)&&s.intent.factoryProduce?.count===1&&s.intent.factoryProduce.queuePolicy==="TACTICAL_QUEUE_POLICY_APPEND"&&Number(s.intent.factoryProduce.definitionId)===h.setup.productDefinitionId,"one actor Append Count1 definition changed"); + insistOne(plannedChildCount(s.intent)===1&&s.parentId&&s.inputId&&s.module?.sha256&&s.controller&&s.basis,"parent/input/module/controller/basis missing");insistOne(Buffer.from(s.module.sha256,"base64").toString("hex")===h.artifacts.guest.sha256,"submitted module differs from actual produced guest"); + const result=rows.filter(r=>r.kind==="result").map(r=>r.value);insistOne(result.every(r=>r.parentId===s.parentId&&r.childCount===1&&(r.childIndex??0)===0&&sameRef(r.actor,f)&&r.inputId===s.inputId&&JSON.stringify(r.module)===JSON.stringify(s.module)&&JSON.stringify(r.controller)===JSON.stringify(s.controller)&&JSON.stringify(r.basis)===JSON.stringify(s.basis)),"foreign parent/input/module/controller/basis/child lifecycle"); + for(const [stage,status]of[["LIVE_RESULT_STAGE_BROKER_ADMISSION","LIVE_RESULT_STATUS_ACCEPTED"],["LIVE_RESULT_STAGE_NATIVE_ADMISSION","LIVE_RESULT_STATUS_ACCEPTED"],["LIVE_RESULT_STAGE_NATIVE_DISPATCH","LIVE_RESULT_STATUS_APPLIED"]])insistOne(result.filter(r=>r.stage===stage&&r.status===status).length===1,`unique ${stage} result missing`); + insistOne(result.length===3,"extra lifecycle result");const dispatch=result.find(r=>r.stage==="LIVE_RESULT_STAGE_NATIVE_DISPATCH");insistOne(Number.isInteger(dispatch.nativeFrame),"actual dispatch frame missing"); + insistOne(before.basis.processIncarnation===h.attribution.processIncarnation&&before.basis.stateChannelIncarnation===h.attribution.stateChannelIncarnation&&before.basis.matchId===s.basis.matchId,"baseline/session incarnation changed"); + const baseline=native[0];insistOne(baseline?.kind==="baseline"&&baseline.stateSequence===before.basis.stateSequence,"frozen native baseline missing"); + const initial=queue(before,f,"QUEUE_DOMAIN_FACTORY_PRODUCTION");insistOne(initial?.complete&&initial.repeat===false&&initial.entries?.length===0,"empty complete repeat-off baseline missing"); + const observed=observations(rows).filter(o=>BigInt(o.basis.stateSequence)>BigInt(s.basis.stateSequence)&&Number(o.basis.nativeFrame)>dispatch.nativeFrame&&o.basis.processIncarnation===s.basis.processIncarnation&&o.basis.stateChannelIncarnation===s.basis.stateChannelIncarnation&&o.basis.matchId===s.basis.matchId); + const known=new Set(before.units.map(u=>key(u.reference))),created=native.filter(r=>r.kind==="unit-created"&&r.nativeFrame>=dispatch.nativeFrame&&BigInt(r.stateSequence)>BigInt(s.basis.stateSequence)),finished=native.filter(r=>r.kind==="unit-finished"&&r.nativeFrame>=dispatch.nativeFrame&&BigInt(r.stateSequence)>BigInt(s.basis.stateSequence)); + insistOne(created.every(r=>r.disposition==="invalidated")&&finished.every(r=>r.disposition==="invalidated"),"unit lifecycle delta did not retain unsupported-event invalidation"); + const nativeDispatch=native.filter(r=>r.kind==="command-dispatch");insistOne(nativeDispatch.length===1&&String(nativeDispatch[0].value.batchSequence)===String(dispatch.batchSequence)&&String(nativeDispatch[0].value.correlationId)===String(dispatch.correlationId)&&String(nativeDispatch[0].value.unitId)===f.id&&nativeDispatch[0].value.commandChannelIncarnation===dispatch.commandChannelIncarnation&&nativeDispatch[0].value.status==="CommandDispatchApplied"&&nativeDispatch[0].value.dispatchFrame===dispatch.nativeFrame,"unique genuine dispatch event missing or extra native dispatch"); + insistOne(created.length===1&&finished.length===1&&String(created[0].value.builderId)===f.id&&String(created[0].value.unitId)===String(finished[0].value.unitId)&&BigInt(finished[0].sequence)>BigInt(created[0].sequence)&&finished[0].nativeFrame>=created[0].nativeFrame,"unique genuine builder creation and finished events missing"); + const queued=native.filter(r=>r.kind==="queue-observation"&&r.nativeFrame>=dispatch.nativeFrame&&sameRef(r.value.actor,f)&&r.value.complete===true&&r.value.repeat===false&&r.value.entries?.length===1&&Number(r.value.entries[0].definitionId)===h.setup.productDefinitionId&&r.value.entries[0].codedOptions===0&&r.value.entries[0].float32params?.length===0); + insistOne(queued.length>0,"exact native queue entry with options 0 missing"); + const later=observed.filter(o=>Number(o.basis.nativeFrame)>finished[0].nativeFrame&&queue(o,f,"QUEUE_DOMAIN_FACTORY_PRODUCTION")?.complete===true&&queue(o,f,"QUEUE_DOMAIN_FACTORY_PRODUCTION")?.repeat===false&&queue(o,f,"QUEUE_DOMAIN_FACTORY_PRODUCTION")?.entries?.length===0); + insistOne(later.length>=2&&BigInt(later.at(-1).basis.stateSequence)>BigInt(later.at(-2).basis.stateSequence),"two later empty repeat-off snapshots missing"); + for(const o of later){insistOne(native.some(r=>r.kind==="snapshot"&&r.disposition==="materialized"&&r.stateSequence===o.basis.stateSequence&&r.nativeFrame===Number(o.basis.nativeFrame)&&BigInt(r.sequence)>BigInt(finished[0].sequence)),"later browser state lacks complete native replacement after invalidation");const fresh=o.units.filter(u=>!known.has(key(u.reference)));insistOne(fresh.length===1&&String(fresh[0].reference.id)===String(created[0].value.unitId)&&Number(unit(o,fresh[0].reference)?.definitionId)===h.setup.productDefinitionId,"exactly one new product lifetime missing or extra production");} + const final=native.filter(r=>r.kind==="final-read"&&r.nativeFrame<=dispatch.nativeFrame&&sameRef(r.value.actor,f)&&r.value.complete===true&&r.value.repeat===false&&r.value.entries?.length===0); + insistOne(final.length===1,"one genuine predispatch final native fence missing");const d=final[0].value.dispatch;insistOne(d?.matched===true&&String(d.batchSequence)===String(dispatch.batchSequence)&&String(d.correlationId)===String(dispatch.correlationId)&&d.moduleSha256===h.artifacts.guest.sha256&&String(d.moduleGeneration)===String(s.module.generation)&&d.brokerSessionId===s.controller.sessionId&&d.commandChannelIncarnation===dispatch.commandChannelIncarnation&&d.commandIndex===nativeDispatch[0].value.commandIndex&&String(d.expectedQueueRevision)===String(s.intent.actorTacticalBindings?.find(b=>sameRef(b.actor,f))?.queueRevisions?.find(q=>q.domain==="QUEUE_DOMAIN_FACTORY_PRODUCTION")?.revision)&&final[0].value.nativeBasis.stateSequence===s.basis.stateSequence&&final[0].value.nativeBasis.token===s.basis.token&&final[0].value.nativeBasis.matchIncarnation===s.basis.matchId&&final[0].value.catalogueId===s.intent.factoryProduce.catalogueId&&String(final[0].value.catalogueRevision)===String(s.intent.factoryProduce.catalogueRevision)&&String(d.authorityEpoch)===String(s.controller.authorityEpoch),"final native dispatch/module/authority/catalogue/queue-basis join missing"); + const terminal=native.filter(r=>r.kind==="queue-observation"&&r.nativeFrame>=Number(later.at(-1).basis.nativeFrame)&&sameRef(r.value.actor,f)&&r.value.complete===true&&r.value.repeat===false&&r.value.entries?.length===0);insistOne(terminal.length>0,"later terminal native queue read missing");return{schema:"fsbar.barc-one-produced-unit-evidence/v1",runId:h.runId,parentId:s.parentId,product:later.at(-1).units.find(u=>!known.has(key(u.reference))).reference,builder:f,createdSequence:created[0].sequence,finishedSequence:finished[0].sequence,finalReadSequence:final[0].sequence,terminalReadSequence:terminal.at(-1).sequence,unitProduced:true,fullSix:false,nativeUsefulPlay:"0/6"}; +} +if(oneUnitEnabled)oneUnitHandoff=loadOneUnitHandoff(process.env.BARC_ONE_UNIT_HANDOFF,process.env.BARC_ONE_UNIT_HANDOFF_SHA256); +test.describe(oneUnitCase,()=>{ + test.skip(!oneUnitEnabled,"requires root-admitted checked one-unit capsule and genuine native attribution feed"); + test("one Append Count1 completes exactly one factory-attributed unit",async({page})=>{ + test.setTimeout(180000);const h=oneUnitHandoff,j={receiverUrl:h.connection.receiverUrl,gatewayUrl:h.connection.gatewayUrl,allowedOrigin:h.connection.allowedOrigin,expectedSessionId:h.connection.sessionId,credential:h.connection.credential,bundledGuestSha256:h.artifacts.guest.sha256,outputPath:h.paths.capture,armFailurePath:`${h.paths.capture}.arm-failure.json`,deferArm:true},journal=captureJournal(h.paths.capture,h.paths.release,h.runId,h.source,oneUnitCase); + try{const rows=await capture(page,journal),live=await pair(page,j,"Manual guest",rows),f=h.actors.factory;await select(page,live,"pointer",[f]); + // Qualification retains the genuine earlier nonempty queue and its natural drain. + // Browser readiness is a fresh empty baseline, so no seed/browser race is needed. + const before=structuredClone(latest(rows)),qualified=recheckOneQualification(h); + assertOnePreArm(h,qualified,rows,before,nativeOneEvents(h,rows,before)); + journal.append("one-unit-baseline",before);rows.readiness.phase="arm-clicked";await live.getByRole("button",{name:"Arm live"}).click();await expect.poll(()=>rows.readiness.armSha256).toBe(rows.readiness.moduleSha256);await expect(live.locator(".authority")).toContainText("arm native confirmed");insistOne(rows.readiness.armCount===1,"exactly one Arm required"); + await live.getByLabel("Action").selectOption("factoryProduce");await live.getByLabel("Definition").selectOption(String(h.setup.productDefinitionId));await live.getByLabel("Count").fill("1");await live.getByLabel("Queue policy").selectOption("TACTICAL_QUEUE_POLICY_APPEND");await physicalSubmit(page,live,"pointer");await expect.poll(()=>rows.filter(r=>r.kind==="submit").length,{timeout:5000}).toBe(1);const submit=rows.find(r=>r.kind==="submit").value;await settleSubmit(rows,submit,"applied"); + // No stale-basis replacement: this route submits once or refuses. + let proof;await expect.poll(()=>{try{proof=assertOneProducedUnit(rows,before,nativeOneEvents(h,rows,before),h);return true}catch{return false}},{timeout:90000,intervals:[25,50,100,250]}).toBe(true);journal.append("one-unit-proof",proof);journal.complete();await journal.hold(); + }finally{journal.close()} + }); +}); +// ONE_UNIT_SOURCE_END