diff --git a/src/GameCult.Networking/Contracts/cultnet.witness-artifact-bundle.schema.json b/src/GameCult.Networking/Contracts/cultnet.witness-artifact-bundle.schema.json
new file mode 100644
index 00000000..a9cfbcef
--- /dev/null
+++ b/src/GameCult.Networking/Contracts/cultnet.witness-artifact-bundle.schema.json
@@ -0,0 +1,120 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.witness-artifact-bundle.schema.json",
+ "title": "CultNet Witness Artifact Bundle",
+ "type": "object",
+ "required": [
+ "bundleId",
+ "witnessKind",
+ "capturedAt",
+ "subject",
+ "contracts",
+ "artifacts",
+ "timingWitnesses",
+ "provenance"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "bundleId": { "type": "string", "minLength": 1 },
+ "witnessKind": { "type": "string", "minLength": 1 },
+ "capturedAt": { "type": "string", "minLength": 1 },
+ "subject": { "$ref": "#/$defs/subject_pin" },
+ "contracts": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$ref": "#/$defs/contract_pin" }
+ },
+ "artifacts": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$ref": "#/$defs/artifact_entry" }
+ },
+ "timingWitnesses": {
+ "type": "array",
+ "items": { "$ref": "#/$defs/timing_entry" }
+ },
+ "provenance": { "$ref": "#/$defs/provenance" }
+ },
+ "$defs": {
+ "subject_pin": {
+ "type": "object",
+ "required": [
+ "documentType",
+ "subjectId"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "documentType": { "type": "string", "minLength": 1 },
+ "subjectId": { "type": "string", "minLength": 1 },
+ "schemaVersion": { "type": "string", "minLength": 1 },
+ "schemaId": { "type": "string", "minLength": 1 },
+ "contentHash": { "type": "string", "minLength": 1 }
+ }
+ },
+ "contract_pin": {
+ "type": "object",
+ "required": [
+ "role",
+ "schemaId"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "role": { "type": "string", "minLength": 1 },
+ "schemaId": { "type": "string", "minLength": 1 },
+ "schemaVersion": { "type": "string", "minLength": 1 },
+ "contentHash": { "type": "string", "minLength": 1 }
+ }
+ },
+ "artifact_entry": {
+ "type": "object",
+ "required": [
+ "role",
+ "uri",
+ "mediaType"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "role": { "type": "string", "minLength": 1 },
+ "uri": { "type": "string", "minLength": 1 },
+ "mediaType": { "type": "string", "minLength": 1 },
+ "contentHash": { "type": "string", "minLength": 1 },
+ "byteLength": { "type": "integer", "minimum": 0 },
+ "producedAt": { "type": "string", "minLength": 1 }
+ }
+ },
+ "timing_entry": {
+ "type": "object",
+ "required": [
+ "stage",
+ "startedAt",
+ "completedAt",
+ "latencyMs"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "stage": { "type": "string", "minLength": 1 },
+ "startedAt": { "type": "string", "minLength": 1 },
+ "completedAt": { "type": "string", "minLength": 1 },
+ "latencyMs": { "type": "number", "minimum": 0 },
+ "witnessArtifactUri": { "type": "string", "minLength": 1 }
+ }
+ },
+ "provenance": {
+ "type": "object",
+ "required": [
+ "pipelineId",
+ "runId",
+ "runtimeId"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "pipelineId": { "type": "string", "minLength": 1 },
+ "runId": { "type": "string", "minLength": 1 },
+ "runtimeId": { "type": "string", "minLength": 1 },
+ "agentId": { "type": "string", "minLength": 1 },
+ "agentRole": { "type": "string", "minLength": 1 },
+ "toolVersion": { "type": "string", "minLength": 1 }
+ }
+ }
+ }
+}
diff --git a/src/GameCult.Networking/CultNetSchemaMessages.cs b/src/GameCult.Networking/CultNetSchemaMessages.cs
index fc21f5ee..18b6f9ef 100644
--- a/src/GameCult.Networking/CultNetSchemaMessages.cs
+++ b/src/GameCult.Networking/CultNetSchemaMessages.cs
@@ -116,6 +116,10 @@ namespace GameCult.Networking
///
public const string SimulationConsensusCandidate = "cultnet.simulation_consensus_candidate.v0";
///
+ /// witness artifact bundle document contract identifier.
+ ///
+ public const string WitnessArtifactBundle = "cultnet.witness_artifact_bundle.v0";
+ ///
/// CultMesh Verse catalog request contract identifier.
///
public const string VerseCatalogRequest = "cultmesh.verse_catalog_request.v0";
diff --git a/src/GameCult.Networking/CultNetSchemaRegistry.cs b/src/GameCult.Networking/CultNetSchemaRegistry.cs
index 025afa3f..10f216ec 100644
--- a/src/GameCult.Networking/CultNetSchemaRegistry.cs
+++ b/src/GameCult.Networking/CultNetSchemaRegistry.cs
@@ -375,6 +375,14 @@ namespace GameCult.Networking
SchemaVersion = "ghostlight.agent_state.v0",
DocumentType = "ghostlight.agent-state",
WireContracts = new[] { CultNetWireContracts.SchemaV0 }
+ },
+ new SchemaResourceSpec
+ {
+ FileName = "cultnet.witness-artifact-bundle.schema.json",
+ Kind = "document_payload",
+ SchemaVersion = CultNetSchemaVersions.WitnessArtifactBundle,
+ DocumentType = "cultnet.witness-artifact-bundle",
+ WireContracts = new[] { CultNetWireContracts.SchemaV0 }
}
};
}
diff --git a/src/GameCult.Networking/CultWitnessArtifactBundle.cs b/src/GameCult.Networking/CultWitnessArtifactBundle.cs
new file mode 100644
index 00000000..045ebfbb
--- /dev/null
+++ b/src/GameCult.Networking/CultWitnessArtifactBundle.cs
@@ -0,0 +1,254 @@
+using System;
+using GameCult.Caching;
+using MessagePack;
+
+namespace GameCult.Networking
+{
+ ///
+ /// Portable manifest for one inspectable pipeline witness bundle.
+ ///
+ [MessagePackObject]
+ [CultDocument("cultnet.witness_artifact_bundle", CultNetSchemaVersions.WitnessArtifactBundle)]
+ public sealed class CultWitnessArtifactBundle
+ {
+ ///
+ /// Gets or sets the stable bundle identifier.
+ ///
+ [Key(0)]
+ [CultName]
+ public string BundleId { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the witness family or pipeline kind.
+ ///
+ [Key(1)]
+ public string WitnessKind { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets when the manifest was captured.
+ ///
+ [Key(2)]
+ public string CapturedAt { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the pinned subject this witness bundle describes.
+ ///
+ [Key(3)]
+ public CultWitnessSubjectPin Subject { get; set; } = new CultWitnessSubjectPin();
+
+ ///
+ /// Gets or sets the schema and contract pins required to interpret the witness.
+ ///
+ [Key(4)]
+ public CultWitnessContractPin[] Contracts { get; set; } = Array.Empty();
+
+ ///
+ /// Gets or sets the inspectable artifact pointers carried by the bundle.
+ ///
+ [Key(5)]
+ public CultWitnessArtifactEntry[] Artifacts { get; set; } = Array.Empty();
+
+ ///
+ /// Gets or sets stage timing witnesses associated with the bundle.
+ ///
+ [Key(6)]
+ public CultWitnessTimingEntry[] TimingWitnesses { get; set; } = Array.Empty();
+
+ ///
+ /// Gets or sets provenance metadata for the bundle producer.
+ ///
+ [Key(7)]
+ public CultWitnessProvenance Provenance { get; set; } = new CultWitnessProvenance();
+ }
+
+ ///
+ /// Pins the document or payload instance the witness is about.
+ ///
+ [MessagePackObject]
+ public sealed class CultWitnessSubjectPin
+ {
+ ///
+ /// Gets or sets the subject document type or domain identifier.
+ ///
+ [Key(0)]
+ public string DocumentType { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the persisted record key or equivalent stable subject id.
+ ///
+ [Key(1)]
+ public string SubjectId { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the pinned subject schema version.
+ ///
+ [Key(2)]
+ public string? SchemaVersion { get; set; }
+
+ ///
+ /// Gets or sets the pinned subject schema identifier when one exists.
+ ///
+ [Key(3)]
+ public string? SchemaId { get; set; }
+
+ ///
+ /// Gets or sets the pinned subject content hash when one exists.
+ ///
+ [Key(4)]
+ public string? ContentHash { get; set; }
+ }
+
+ ///
+ /// Pins one schema or contract dependency for the witness bundle.
+ ///
+ [MessagePackObject]
+ public sealed class CultWitnessContractPin
+ {
+ ///
+ /// Gets or sets the contract role inside the bundle.
+ ///
+ [Key(0)]
+ public string Role { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the stable schema identifier.
+ ///
+ [Key(1)]
+ public string SchemaId { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the pinned schema version.
+ ///
+ [Key(2)]
+ public string? SchemaVersion { get; set; }
+
+ ///
+ /// Gets or sets the canonical schema content hash when one exists.
+ ///
+ [Key(3)]
+ public string? ContentHash { get; set; }
+ }
+
+ ///
+ /// Points at one inspectable artifact emitted by the witnessed pipeline.
+ ///
+ [MessagePackObject]
+ public sealed class CultWitnessArtifactEntry
+ {
+ ///
+ /// Gets or sets the artifact role inside the witness bundle.
+ ///
+ [Key(0)]
+ public string Role { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the portable URI for the artifact.
+ ///
+ [Key(1)]
+ public string Uri { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the artifact media type.
+ ///
+ [Key(2)]
+ public string MediaType { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the artifact content hash when one exists.
+ ///
+ [Key(3)]
+ public string? ContentHash { get; set; }
+
+ ///
+ /// Gets or sets the artifact size in bytes when known.
+ ///
+ [Key(4)]
+ public long? ByteLength { get; set; }
+
+ ///
+ /// Gets or sets when the artifact was produced.
+ ///
+ [Key(5)]
+ public string? ProducedAt { get; set; }
+ }
+
+ ///
+ /// Records timestamp and latency evidence for one stage in the witnessed pipeline.
+ ///
+ [MessagePackObject]
+ public sealed class CultWitnessTimingEntry
+ {
+ ///
+ /// Gets or sets the pipeline stage name.
+ ///
+ [Key(0)]
+ public string Stage { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets when the stage started.
+ ///
+ [Key(1)]
+ public string StartedAt { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets when the stage completed.
+ ///
+ [Key(2)]
+ public string CompletedAt { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the observed stage latency in milliseconds.
+ ///
+ [Key(3)]
+ public double LatencyMs { get; set; }
+
+ ///
+ /// Gets or sets the URI for the timing witness artifact when one exists.
+ ///
+ [Key(4)]
+ public string? WitnessArtifactUri { get; set; }
+ }
+
+ ///
+ /// Carries provenance metadata for the witness bundle producer.
+ ///
+ [MessagePackObject]
+ public sealed class CultWitnessProvenance
+ {
+ ///
+ /// Gets or sets the stable pipeline identifier.
+ ///
+ [Key(0)]
+ public string PipelineId { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the pipeline run identifier.
+ ///
+ [Key(1)]
+ public string RunId { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the runtime identifier that produced the bundle.
+ ///
+ [Key(2)]
+ public string RuntimeId { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the producing agent identifier when one exists.
+ ///
+ [Key(3)]
+ public string? AgentId { get; set; }
+
+ ///
+ /// Gets or sets the producing agent role when one exists.
+ ///
+ [Key(4)]
+ public string? AgentRole { get; set; }
+
+ ///
+ /// Gets or sets the pipeline or tool version when one exists.
+ ///
+ [Key(5)]
+ public string? ToolVersion { get; set; }
+ }
+}
diff --git a/tests/GameCult.Networking.Tests/NetworkingTests.cs b/tests/GameCult.Networking.Tests/NetworkingTests.cs
index 381ffaf0..25a5d4b6 100644
--- a/tests/GameCult.Networking.Tests/NetworkingTests.cs
+++ b/tests/GameCult.Networking.Tests/NetworkingTests.cs
@@ -2286,6 +2286,39 @@ namespace GameCult.Networking.Tests
Assert.That(ghostlight.Kind, Is.EqualTo("document_payload"));
}
+ [Test]
+ public void CultWitnessArtifactBundle_Has_Stable_CacheContract()
+ {
+ var descriptor = new CultCache().Registry.GetRequired();
+
+ Assert.That(descriptor.SchemaName, Is.EqualTo("cultnet.witness_artifact_bundle"));
+ Assert.That(descriptor.SchemaVersion, Is.EqualTo(CultNetSchemaVersions.WitnessArtifactBundle));
+ Assert.That(descriptor.NameMember, Is.EqualTo(nameof(CultWitnessArtifactBundle.BundleId)));
+ Assert.That(descriptor.CanonicalSchemaJson, Is.EqualTo(
+ "{\"schemaName\":\"cultnet.witness_artifact_bundle\",\"schemaVersion\":\"cultnet.witness_artifact_bundle.v0\",\"members\":[{\"slot\":0,\"name\":\"BundleId\",\"type\":\"System.String\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":true},{\"slot\":1,\"name\":\"WitnessKind\",\"type\":\"System.String\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":2,\"name\":\"CapturedAt\",\"type\":\"System.String\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":3,\"name\":\"Subject\",\"type\":\"GameCult.Networking.CultWitnessSubjectPin\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":4,\"name\":\"Contracts\",\"type\":\"GameCult.Networking.CultWitnessContractPin[]\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":5,\"name\":\"Artifacts\",\"type\":\"GameCult.Networking.CultWitnessArtifactEntry[]\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":6,\"name\":\"TimingWitnesses\",\"type\":\"GameCult.Networking.CultWitnessTimingEntry[]\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":7,\"name\":\"Provenance\",\"type\":\"GameCult.Networking.CultWitnessProvenance\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false}]}"));
+ }
+
+ [Test]
+ public void CultNetSchemaRegistry_BuiltInCatalog_Advertises_WitnessArtifactBundle()
+ {
+ var response = CultNetSchemaRegistry.BuiltIn.CreateCatalogResponse(
+ new CultNetSchemaCatalogRequestMessage
+ {
+ MessageId = "catalog-witness",
+ IncludeSchemaJson = true
+ });
+ var witnessBundle = Array.Find(response.Schemas,
+ schema => schema.SchemaVersion == CultNetSchemaVersions.WitnessArtifactBundle);
+
+ Assert.That(witnessBundle, Is.Not.Null);
+ Assert.That(witnessBundle!.DocumentType, Is.EqualTo("cultnet.witness-artifact-bundle"));
+ Assert.That(witnessBundle.Kind, Is.EqualTo("document_payload"));
+ Assert.That(witnessBundle.WireContracts, Is.EqualTo([CultNetWireContracts.SchemaV0]));
+ Assert.That(witnessBundle.SchemaJson, Does.Contain("\"bundleId\""));
+ Assert.That(witnessBundle.SchemaJson, Does.Contain("\"timingWitnesses\""));
+ Assert.That(witnessBundle.SchemaJson, Does.Contain("\"witnessArtifactUri\""));
+ }
+
[Test]
public void Server_Separates_Connection_And_Auth_RateLimits()
{