@agentplat/mesh-protocol 0.3.0-alpha.2 → 0.3.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { canonicalizeJsonValue } from './canonical-json.js';
2
+ import { normalizeMeshEvidenceAttestationV1, normalizeMeshEvidenceChallengeV1, normalizeMeshEvidenceClaimV1, normalizeMeshEvidenceRetractionV1, normalizeMeshTrustObservationV1, } from '@agentplat/trust/mesh-records';
2
3
  import { DEFAULT_MESH_PROTOCOL_LIMITS, MESH_AUDIENCE_TOPICS, MESH_MESSAGE_TYPES, MESH_PROTOCOL, MESH_SIGNATURE_ALGORITHM, MESH_WIRE_VERSION, } from './contracts.js';
3
4
  import { parseStrictJsonDocument, } from './strict-json.js';
4
5
  const identifierPattern = /^[A-Za-z0-9][A-Za-z0-9._:@-]*$/;
@@ -42,6 +43,13 @@ const maximumCapacityReservationUnits = 1_000_000;
42
43
  const maximumWorkBidWindowMs = 60 * 60 * 1_000;
43
44
  const maximumWorkDeadlineMs = 30 * 24 * 60 * 60 * 1_000;
44
45
  const maximumWorkExecutionLifetimeMs = 5 * 60 * 1_000;
46
+ const maximumEvidenceLifetimeMs = 5 * 60 * 1_000;
47
+ const maximumEvidenceReferences = 32;
48
+ const maximumEvidenceReferenceBytes = 4_096;
49
+ const maximumEvidenceContentBytes = 4_096;
50
+ const maximumEvidenceIdentifierBytes = 256;
51
+ const maximumEvidenceReasonCodes = 32;
52
+ const trustDigestPattern = /^[0-9a-f]{64}$/;
45
53
  const knownMessageTypes = new Set(MESH_MESSAGE_TYPES);
46
54
  const knownTopics = new Set(MESH_AUDIENCE_TOPICS);
47
55
  const utf8Encoder = new TextEncoder();
@@ -288,6 +296,7 @@ function validateEnvelope(input, limits) {
288
296
  fail('type_payload_mismatch', '$["payload"]["type"]');
289
297
  }
290
298
  validateMessageSpecificEnvelope(type, sender, audience, objectiveId, causationId, payload, sentAt, expiresAt);
299
+ validateEvidenceTrustBinding(tenantId, meshId, objectiveId, sender, causationId, payload);
291
300
  return {
292
301
  protocol: MESH_PROTOCOL,
293
302
  wireVersion: MESH_WIRE_VERSION,
@@ -573,8 +582,367 @@ function validatePayload(type, input, limits) {
573
582
  if (type === 'lease.certificate') {
574
583
  return validateLeaseCertificate(input, limits);
575
584
  }
585
+ if (type === 'evidence.claim') {
586
+ return validateEvidenceClaim(input, limits);
587
+ }
588
+ if (type === 'evidence.attest') {
589
+ return validateEvidenceAttestation(input, limits);
590
+ }
591
+ if (type === 'evidence.challenge') {
592
+ return validateEvidenceChallenge(input, limits);
593
+ }
594
+ if (type === 'evidence.retract') {
595
+ return validateEvidenceRetraction(input, limits);
596
+ }
597
+ if (type === 'trust.observation') {
598
+ return validateTrustObservation(input, limits);
599
+ }
576
600
  return fail('unsupported_message_type', '$["type"]');
577
601
  }
602
+ function validateEvidenceClaim(input, limits) {
603
+ const payload = assertClosedRecord(input, [
604
+ 'assertionDigest',
605
+ 'basisReferences',
606
+ 'claimId',
607
+ 'content',
608
+ 'criterionId',
609
+ 'observedAt',
610
+ 'outcome',
611
+ 'scope',
612
+ 'subject',
613
+ 'type',
614
+ ], [], '$["payload"]', 'invalid_payload');
615
+ const content = validateEvidenceContent(payload.content, '$["payload"]["content"]', limits);
616
+ return {
617
+ type: assertPayloadType(payload.type, 'evidence.claim'),
618
+ claimId: assertTrustRecordId(payload.claimId, 'claim', '$["payload"]["claimId"]'),
619
+ subject: validateEvidenceSubject(payload.subject, '$["payload"]["subject"]', limits),
620
+ scope: validateEvidenceScope(payload.scope, '$["payload"]["scope"]', limits),
621
+ criterionId: assertBoundedString(payload.criterionId, '$["payload"]["criterionId"]', maximumDomainStringBytes),
622
+ outcome: assertEnum(payload.outcome, ['satisfied', 'violated', 'inconclusive'], '$["payload"]["outcome"]'),
623
+ assertionDigest: assertTrustDigest(payload.assertionDigest, '$["payload"]["assertionDigest"]'),
624
+ content,
625
+ basisReferences: validateEvidenceReferences(payload.basisReferences, '$["payload"]["basisReferences"]', limits, { allowControlRecords: false, minimum: 0 }),
626
+ observedAt: validateOptionalEvidenceTimestamp(payload.observedAt, '$["payload"]["observedAt"]'),
627
+ };
628
+ }
629
+ function validateEvidenceAttestation(input, limits) {
630
+ const payload = assertClosedRecord(input, [
631
+ 'attestationId',
632
+ 'basisReferences',
633
+ 'claimDigest',
634
+ 'claimId',
635
+ 'confidenceBasisPoints',
636
+ 'disposition',
637
+ 'observedAt',
638
+ 'scope',
639
+ 'type',
640
+ ], [], '$["payload"]', 'invalid_payload');
641
+ return {
642
+ type: assertPayloadType(payload.type, 'evidence.attest'),
643
+ attestationId: assertTrustRecordId(payload.attestationId, 'attestation', '$["payload"]["attestationId"]'),
644
+ scope: validateEvidenceScope(payload.scope, '$["payload"]["scope"]', limits),
645
+ claimId: assertTrustRecordId(payload.claimId, 'claim', '$["payload"]["claimId"]'),
646
+ claimDigest: assertTrustDigest(payload.claimDigest, '$["payload"]["claimDigest"]'),
647
+ disposition: assertEnum(payload.disposition, ['support', 'contradict', 'inconclusive'], '$["payload"]["disposition"]'),
648
+ confidenceBasisPoints: assertBoundedSafeInteger(payload.confidenceBasisPoints, '$["payload"]["confidenceBasisPoints"]', 0, 10_000),
649
+ basisReferences: validateEvidenceReferences(payload.basisReferences, '$["payload"]["basisReferences"]', limits, { allowControlRecords: false, minimum: 0 }),
650
+ observedAt: validateOptionalEvidenceTimestamp(payload.observedAt, '$["payload"]["observedAt"]'),
651
+ };
652
+ }
653
+ function validateEvidenceChallenge(input, limits) {
654
+ const payload = assertClosedRecord(input, [
655
+ 'basisReferences',
656
+ 'challengeId',
657
+ 'observedAt',
658
+ 'reasonCode',
659
+ 'scope',
660
+ 'targetDigest',
661
+ 'targetId',
662
+ 'targetKind',
663
+ 'type',
664
+ ], [], '$["payload"]', 'invalid_payload');
665
+ const targetKind = assertEnum(payload.targetKind, ['claim', 'attestation'], '$["payload"]["targetKind"]');
666
+ return {
667
+ type: assertPayloadType(payload.type, 'evidence.challenge'),
668
+ challengeId: assertTrustRecordId(payload.challengeId, 'challenge', '$["payload"]["challengeId"]'),
669
+ scope: validateEvidenceScope(payload.scope, '$["payload"]["scope"]', limits),
670
+ targetKind,
671
+ targetId: assertTrustRecordId(payload.targetId, targetKind, '$["payload"]["targetId"]'),
672
+ targetDigest: assertTrustDigest(payload.targetDigest, '$["payload"]["targetDigest"]'),
673
+ reasonCode: assertBoundedString(payload.reasonCode, '$["payload"]["reasonCode"]', maximumDomainStringBytes),
674
+ basisReferences: validateEvidenceReferences(payload.basisReferences, '$["payload"]["basisReferences"]', limits, { allowControlRecords: false, minimum: 1 }),
675
+ observedAt: validateOptionalEvidenceTimestamp(payload.observedAt, '$["payload"]["observedAt"]'),
676
+ };
677
+ }
678
+ function validateEvidenceRetraction(input, limits) {
679
+ const payload = assertClosedRecord(input, [
680
+ 'observedAt',
681
+ 'reasonCode',
682
+ 'retractionId',
683
+ 'scope',
684
+ 'targetDigest',
685
+ 'targetId',
686
+ 'targetKind',
687
+ 'type',
688
+ ], [], '$["payload"]', 'invalid_payload');
689
+ const targetKind = assertEnum(payload.targetKind, ['claim', 'attestation'], '$["payload"]["targetKind"]');
690
+ return {
691
+ type: assertPayloadType(payload.type, 'evidence.retract'),
692
+ retractionId: assertTrustRecordId(payload.retractionId, 'retraction', '$["payload"]["retractionId"]'),
693
+ scope: validateEvidenceScope(payload.scope, '$["payload"]["scope"]', limits),
694
+ targetKind,
695
+ targetId: assertTrustRecordId(payload.targetId, targetKind, '$["payload"]["targetId"]'),
696
+ targetDigest: assertTrustDigest(payload.targetDigest, '$["payload"]["targetDigest"]'),
697
+ reasonCode: assertBoundedString(payload.reasonCode, '$["payload"]["reasonCode"]', maximumDomainStringBytes),
698
+ observedAt: validateOptionalEvidenceTimestamp(payload.observedAt, '$["payload"]["observedAt"]'),
699
+ };
700
+ }
701
+ function validateTrustObservation(input, limits) {
702
+ const payload = assertClosedRecord(input, [
703
+ 'dimensionId',
704
+ 'disposition',
705
+ 'evidenceIds',
706
+ 'fusionDecisionDigest',
707
+ 'observationId',
708
+ 'observedAt',
709
+ 'policyDigest',
710
+ 'policyId',
711
+ 'policyVersion',
712
+ 'profileDigest',
713
+ 'reasonCodes',
714
+ 'scope',
715
+ 'scoreBand',
716
+ 'subject',
717
+ 'type',
718
+ 'uncertaintyBand',
719
+ 'validUntil',
720
+ ], [], '$["payload"]', 'invalid_payload');
721
+ const observedAt = assertRfc3339PayloadTimestamp(payload.observedAt, '$["payload"]["observedAt"]');
722
+ const validUntil = assertRfc3339PayloadTimestamp(payload.validUntil, '$["payload"]["validUntil"]');
723
+ if (validUntil.instant <= observedAt.instant ||
724
+ validUntil.instant - observedAt.instant > 24n * 60n * 60n * 1000000000n) {
725
+ fail('invalid_payload', '$["payload"]["validUntil"]');
726
+ }
727
+ return {
728
+ type: assertPayloadType(payload.type, 'trust.observation'),
729
+ observationId: assertTrustRecordId(payload.observationId, 'observation', '$["payload"]["observationId"]'),
730
+ subject: validateEvidenceSubject(payload.subject, '$["payload"]["subject"]', limits),
731
+ scope: validateEvidenceScope(payload.scope, '$["payload"]["scope"]', limits),
732
+ policyId: assertBoundedString(payload.policyId, '$["payload"]["policyId"]', maximumDomainStringBytes),
733
+ policyVersion: assertPositiveSafeInteger(payload.policyVersion, '$["payload"]["policyVersion"]', 'invalid_payload'),
734
+ policyDigest: assertTrustDigest(payload.policyDigest, '$["payload"]["policyDigest"]'),
735
+ profileDigest: assertTrustDigest(payload.profileDigest, '$["payload"]["profileDigest"]'),
736
+ fusionDecisionDigest: assertTrustDigest(payload.fusionDecisionDigest, '$["payload"]["fusionDecisionDigest"]'),
737
+ dimensionId: assertBoundedString(payload.dimensionId, '$["payload"]["dimensionId"]', maximumDomainStringBytes),
738
+ scoreBand: assertEnum(payload.scoreBand, ['unknown', 'low', 'medium', 'high'], '$["payload"]["scoreBand"]'),
739
+ uncertaintyBand: assertEnum(payload.uncertaintyBand, ['low', 'medium', 'high'], '$["payload"]["uncertaintyBand"]'),
740
+ disposition: assertEnum(payload.disposition, ['eligible', 'restricted', 'quarantined', 'unavailable'], '$["payload"]["disposition"]'),
741
+ evidenceIds: validateTrustRecordIdArray(payload.evidenceIds, '$["payload"]["evidenceIds"]'),
742
+ observedAt: observedAt.text,
743
+ validUntil: validUntil.text,
744
+ reasonCodes: validateBoundedStringArray(payload.reasonCodes, '$["payload"]["reasonCodes"]', maximumEvidenceReasonCodes, maximumDomainStringBytes, undefined, true),
745
+ };
746
+ }
747
+ function validateEvidenceSubject(input, path, limits) {
748
+ const subject = assertRecord(input, path, 'invalid_payload');
749
+ if (subject.kind === 'peer') {
750
+ assertExactKeys(subject, ['kind', 'peerId'], path, 'invalid_payload');
751
+ return {
752
+ kind: 'peer',
753
+ peerId: assertIdentifier(subject.peerId, `${path}["peerId"]`, limits),
754
+ };
755
+ }
756
+ if (subject.kind === 'peer_capability') {
757
+ assertExactKeys(subject, [
758
+ 'capabilityKey',
759
+ 'capabilityRevision',
760
+ 'capabilityVersion',
761
+ 'kind',
762
+ 'peerId',
763
+ ], path, 'invalid_payload');
764
+ return {
765
+ kind: 'peer_capability',
766
+ peerId: assertIdentifier(subject.peerId, `${path}["peerId"]`, limits),
767
+ capabilityKey: assertBoundedString(subject.capabilityKey, `${path}["capabilityKey"]`, maximumDomainStringBytes),
768
+ capabilityVersion: assertBoundedString(subject.capabilityVersion, `${path}["capabilityVersion"]`, maximumVersionBytes),
769
+ capabilityRevision: assertPositiveSafeInteger(subject.capabilityRevision, `${path}["capabilityRevision"]`, 'invalid_payload'),
770
+ };
771
+ }
772
+ return fail('invalid_payload', `${path}["kind"]`);
773
+ }
774
+ function validateEvidenceScope(input, path, limits) {
775
+ const scope = assertRecord(input, path, 'invalid_payload');
776
+ if (scope.kind === 'mesh') {
777
+ assertExactKeys(scope, ['kind'], path, 'invalid_payload');
778
+ return { kind: 'mesh' };
779
+ }
780
+ if (scope.kind === 'objective') {
781
+ assertExactKeys(scope, ['kind', 'objectiveRevision'], path, 'invalid_payload');
782
+ return {
783
+ kind: 'objective',
784
+ objectiveRevision: assertPositiveSafeInteger(scope.objectiveRevision, `${path}["objectiveRevision"]`, 'invalid_payload'),
785
+ };
786
+ }
787
+ if (scope.kind === 'work') {
788
+ assertExactKeys(scope, [
789
+ 'assignmentAuthorityId',
790
+ 'assignmentEpoch',
791
+ 'fencingToken',
792
+ 'kind',
793
+ 'objectiveRevision',
794
+ 'workItemId',
795
+ 'workItemRevision',
796
+ ], path, 'invalid_payload');
797
+ const assignmentAuthorityId = assertIdentifier(scope.assignmentAuthorityId, `${path}["assignmentAuthorityId"]`, limits);
798
+ const fencingToken = assertIdentifier(scope.fencingToken, `${path}["fencingToken"]`, limits);
799
+ return {
800
+ kind: 'work',
801
+ objectiveRevision: assertPositiveSafeInteger(scope.objectiveRevision, `${path}["objectiveRevision"]`, 'invalid_payload'),
802
+ workItemId: assertIdentifier(scope.workItemId, `${path}["workItemId"]`, limits),
803
+ workItemRevision: assertPositiveSafeInteger(scope.workItemRevision, `${path}["workItemRevision"]`, 'invalid_payload'),
804
+ assignmentEpoch: assertPositiveSafeInteger(scope.assignmentEpoch, `${path}["assignmentEpoch"]`, 'invalid_payload'),
805
+ assignmentAuthorityId,
806
+ fencingToken,
807
+ };
808
+ }
809
+ return fail('invalid_payload', `${path}["kind"]`);
810
+ }
811
+ function validateEvidenceReferences(input, path, limits, { allowControlRecords, minimum, }) {
812
+ if (!Array.isArray(input) ||
813
+ input.length < minimum ||
814
+ input.length > maximumEvidenceReferences) {
815
+ fail('invalid_payload', path);
816
+ }
817
+ const references = input.map((item, index) => validateEvidenceReference(item, `${path}[${index}]`, limits, allowControlRecords));
818
+ let previous = '';
819
+ for (const reference of references) {
820
+ const order = JSON.stringify([
821
+ reference.kind,
822
+ reference.referenceType,
823
+ reference.referenceId,
824
+ reference.referenceDigest,
825
+ ]);
826
+ if (order <= previous)
827
+ fail('invalid_payload', path);
828
+ previous = order;
829
+ }
830
+ return Object.freeze(references);
831
+ }
832
+ function validateEvidenceReference(input, path, limits, allowControlRecords) {
833
+ const reference = assertClosedRecord(input, [
834
+ 'kind',
835
+ 'referenceDigest',
836
+ 'referenceId',
837
+ 'referenceType',
838
+ 'schemaVersion',
839
+ ], [], path, 'invalid_payload');
840
+ if (reference.schemaVersion !== 1)
841
+ fail('invalid_payload', `${path}["schemaVersion"]`);
842
+ const kind = assertEnum(reference.kind, ['evidence', 'mesh_record', 'control_record', 'external'], `${path}["kind"]`);
843
+ if (!allowControlRecords && kind === 'control_record') {
844
+ fail('invalid_payload', `${path}["kind"]`);
845
+ }
846
+ const referenceDigest = assertEvidenceReferenceDigest(kind, reference.referenceDigest, `${path}["referenceDigest"]`);
847
+ return {
848
+ schemaVersion: 1,
849
+ kind,
850
+ referenceType: assertBoundedString(reference.referenceType, `${path}["referenceType"]`, maximumEvidenceReferenceBytes),
851
+ referenceId: assertBoundedString(reference.referenceId, `${path}["referenceId"]`, maximumEvidenceReferenceBytes),
852
+ referenceDigest,
853
+ };
854
+ }
855
+ function validateEvidenceContent(input, path, limits) {
856
+ if (input === null)
857
+ return null;
858
+ const content = assertRecord(input, path, 'invalid_payload');
859
+ if (content.kind === 'inline_summary') {
860
+ assertExactKeys(content, ['contentDigest', 'encodedBytes', 'kind', 'mediaType', 'summary'], path, 'invalid_payload');
861
+ const summary = assertBoundedString(content.summary, `${path}["summary"]`, maximumEvidenceContentBytes);
862
+ const encodedBytes = assertBoundedSafeInteger(content.encodedBytes, `${path}["encodedBytes"]`, 0, maximumEvidenceContentBytes);
863
+ if (utf8Encoder.encode(summary).byteLength !== encodedBytes) {
864
+ fail('invalid_payload', `${path}["encodedBytes"]`);
865
+ }
866
+ return {
867
+ kind: 'inline_summary',
868
+ mediaType: assertBoundedString(content.mediaType, `${path}["mediaType"]`, maximumMediaTypeBytes),
869
+ summary,
870
+ contentDigest: assertTrustDigest(content.contentDigest, `${path}["contentDigest"]`),
871
+ encodedBytes,
872
+ };
873
+ }
874
+ if (content.kind === 'reference') {
875
+ assertExactKeys(content, ['contentDigest', 'encodedBytes', 'kind', 'mediaType', 'reference'], path, 'invalid_payload');
876
+ const reference = validateEvidenceReference(content.reference, `${path}["reference"]`, limits, false);
877
+ if (reference.kind !== 'mesh_record' && reference.kind !== 'external') {
878
+ fail('invalid_payload', `${path}["reference"]["kind"]`);
879
+ }
880
+ return {
881
+ kind: 'reference',
882
+ mediaType: assertBoundedString(content.mediaType, `${path}["mediaType"]`, maximumMediaTypeBytes),
883
+ reference,
884
+ contentDigest: assertTrustDigest(content.contentDigest, `${path}["contentDigest"]`),
885
+ encodedBytes: assertBoundedSafeInteger(content.encodedBytes, `${path}["encodedBytes"]`, 0, maximumEvidenceContentBytes),
886
+ };
887
+ }
888
+ return fail('invalid_payload', `${path}["kind"]`);
889
+ }
890
+ function validateTrustRecordIdArray(input, path) {
891
+ if (!Array.isArray(input) || input.length > maximumEvidenceReferences) {
892
+ fail('invalid_payload', path);
893
+ }
894
+ const ids = input.map((value, index) => assertBoundedString(value, `${path}[${index}]`, maximumEvidenceIdentifierBytes));
895
+ let previous = '';
896
+ for (const id of ids) {
897
+ if (id <= previous)
898
+ fail('invalid_payload', path);
899
+ previous = id;
900
+ }
901
+ return Object.freeze(ids);
902
+ }
903
+ function validateOptionalEvidenceTimestamp(input, path) {
904
+ if (input === null)
905
+ return null;
906
+ return assertRfc3339PayloadTimestamp(input, path).text;
907
+ }
908
+ function assertTrustRecordId(input, prefix, path) {
909
+ const value = assertBoundedString(input, path, maximumEvidenceIdentifierBytes);
910
+ if (!new RegExp(`^${prefix}:[0-9a-f]{64}$`).test(value)) {
911
+ fail('invalid_payload', path);
912
+ }
913
+ return value;
914
+ }
915
+ function assertTrustDigest(input, path) {
916
+ const value = assertString(input, path, 'invalid_payload');
917
+ if (!trustDigestPattern.test(value))
918
+ fail('invalid_payload', path);
919
+ return value;
920
+ }
921
+ function assertEvidenceReferenceDigest(kind, input, path) {
922
+ if (kind === 'mesh_record')
923
+ return assertPayloadHash(input, path);
924
+ if (kind === 'control_record') {
925
+ const value = assertString(input, path, 'invalid_payload');
926
+ if (!/^sha256:[0-9a-f]{64}$/.test(value))
927
+ fail('invalid_payload', path);
928
+ return value;
929
+ }
930
+ return assertTrustDigest(input, path);
931
+ }
932
+ function assertBoundedSafeInteger(input, path, minimum, maximum) {
933
+ if (!Number.isSafeInteger(input) ||
934
+ input < minimum ||
935
+ input > maximum) {
936
+ fail('invalid_payload', path);
937
+ }
938
+ return input;
939
+ }
940
+ function assertEnum(input, values, path) {
941
+ if (typeof input !== 'string' || !values.includes(input)) {
942
+ fail('invalid_payload', path);
943
+ }
944
+ return input;
945
+ }
578
946
  function validateWorkOffer(input, limits) {
579
947
  const payload = assertClosedRecord(input, [
580
948
  'bidDeadline',
@@ -1578,6 +1946,19 @@ function validateMessageSpecificEnvelope(type, sender, audience, objectiveId, ca
1578
1946
  fail('invalid_audience', '$["audience"]["peerId"]');
1579
1947
  }
1580
1948
  }
1949
+ else if (type === 'evidence.claim' ||
1950
+ type === 'evidence.attest' ||
1951
+ type === 'evidence.challenge' ||
1952
+ type === 'evidence.retract') {
1953
+ if (audience.kind === 'mesh' && audience.topic !== 'evidence') {
1954
+ fail('invalid_audience', '$["audience"]["topic"]');
1955
+ }
1956
+ }
1957
+ else if (type === 'trust.observation') {
1958
+ if (audience.kind !== 'peer') {
1959
+ fail('invalid_audience', '$["audience"]');
1960
+ }
1961
+ }
1581
1962
  else if (type === 'work.award' ||
1582
1963
  type === 'work.accept' ||
1583
1964
  type === 'work.decline' ||
@@ -1658,6 +2039,30 @@ function validateMessageSpecificEnvelope(type, sender, audience, objectiveId, ca
1658
2039
  causationId === undefined) {
1659
2040
  fail('invalid_payload', '$["causationId"]');
1660
2041
  }
2042
+ if (type === 'evidence.claim' ||
2043
+ type === 'evidence.attest' ||
2044
+ type === 'evidence.challenge' ||
2045
+ type === 'evidence.retract') {
2046
+ const evidencePayload = payload;
2047
+ if (evidencePayload.scope.kind === 'mesh') {
2048
+ if (objectiveId !== undefined) {
2049
+ fail('invalid_payload', '$["objectiveId"]');
2050
+ }
2051
+ }
2052
+ else if (objectiveId === undefined) {
2053
+ fail('invalid_payload', '$["objectiveId"]');
2054
+ }
2055
+ const references = evidencePayload.type === 'evidence.claim' ||
2056
+ evidencePayload.type === 'evidence.attest' ||
2057
+ evidencePayload.type === 'evidence.challenge'
2058
+ ? evidencePayload.basisReferences
2059
+ : [];
2060
+ if ((evidencePayload.scope.kind === 'work' ||
2061
+ references.some((reference) => reference.kind === 'mesh_record')) &&
2062
+ causationId === undefined) {
2063
+ fail('invalid_payload', '$["causationId"]');
2064
+ }
2065
+ }
1661
2066
  if (type === 'objective.announce' ||
1662
2067
  type === 'objective.revise' ||
1663
2068
  type === 'objective.cancel' ||
@@ -1705,6 +2110,117 @@ function validateMessageSpecificEnvelope(type, sender, audience, objectiveId, ca
1705
2110
  validateWorkExecutionTimes(payload, sentAt, expiresAt);
1706
2111
  }
1707
2112
  }
2113
+ /**
2114
+ * The shared Trust normalizer owns every content-bound Evidence digest. Mesh
2115
+ * only supplies the signed envelope material, then compares the projected ID
2116
+ * (and Claim assertion digest) with the closed wire payload.
2117
+ */
2118
+ function validateEvidenceTrustBinding(tenantId, meshId, objectiveId, sender, causationId, payload) {
2119
+ if (payload.type !== 'evidence.claim' &&
2120
+ payload.type !== 'evidence.attest' &&
2121
+ payload.type !== 'evidence.challenge' &&
2122
+ payload.type !== 'evidence.retract' &&
2123
+ payload.type !== 'trust.observation') {
2124
+ return;
2125
+ }
2126
+ const envelope = {
2127
+ schemaVersion: 1,
2128
+ tenantId,
2129
+ meshId,
2130
+ objectiveId: objectiveId ?? null,
2131
+ senderPeerId: sender.peerId,
2132
+ causationId: causationId ?? null,
2133
+ };
2134
+ try {
2135
+ if (payload.type === 'evidence.claim') {
2136
+ const normalized = normalizeMeshEvidenceClaimV1(envelope, {
2137
+ subject: payload.subject,
2138
+ scope: payload.scope,
2139
+ criterionId: payload.criterionId,
2140
+ outcome: payload.outcome,
2141
+ content: payload.content,
2142
+ basisReferences: payload.basisReferences,
2143
+ observedAt: payload.observedAt,
2144
+ });
2145
+ if (normalized.claimId !== payload.claimId) {
2146
+ fail('invalid_payload', '$["payload"]["claimId"]');
2147
+ }
2148
+ if (normalized.assertionDigest !== payload.assertionDigest) {
2149
+ fail('invalid_payload', '$["payload"]["assertionDigest"]');
2150
+ }
2151
+ return;
2152
+ }
2153
+ if (payload.type === 'evidence.attest') {
2154
+ const normalized = normalizeMeshEvidenceAttestationV1(envelope, {
2155
+ scope: payload.scope,
2156
+ claimId: payload.claimId,
2157
+ claimDigest: payload.claimDigest,
2158
+ disposition: payload.disposition,
2159
+ confidenceBasisPoints: payload.confidenceBasisPoints,
2160
+ basisReferences: payload.basisReferences,
2161
+ observedAt: payload.observedAt,
2162
+ });
2163
+ if (normalized.attestationId !== payload.attestationId) {
2164
+ fail('invalid_payload', '$["payload"]["attestationId"]');
2165
+ }
2166
+ return;
2167
+ }
2168
+ if (payload.type === 'evidence.challenge') {
2169
+ const normalized = normalizeMeshEvidenceChallengeV1(envelope, {
2170
+ scope: payload.scope,
2171
+ targetKind: payload.targetKind,
2172
+ targetId: payload.targetId,
2173
+ targetDigest: payload.targetDigest,
2174
+ reasonCode: payload.reasonCode,
2175
+ basisReferences: payload.basisReferences,
2176
+ observedAt: payload.observedAt,
2177
+ });
2178
+ if (normalized.challengeId !== payload.challengeId) {
2179
+ fail('invalid_payload', '$["payload"]["challengeId"]');
2180
+ }
2181
+ return;
2182
+ }
2183
+ if (payload.type === 'trust.observation') {
2184
+ const normalized = normalizeMeshTrustObservationV1(envelope, {
2185
+ subject: payload.subject,
2186
+ scope: payload.scope,
2187
+ policyId: payload.policyId,
2188
+ policyVersion: payload.policyVersion,
2189
+ policyDigest: payload.policyDigest,
2190
+ profileDigest: payload.profileDigest,
2191
+ fusionDecisionDigest: payload.fusionDecisionDigest,
2192
+ dimensionId: payload.dimensionId,
2193
+ scoreBand: payload.scoreBand,
2194
+ uncertaintyBand: payload.uncertaintyBand,
2195
+ disposition: payload.disposition,
2196
+ evidenceIds: payload.evidenceIds,
2197
+ observedAt: payload.observedAt,
2198
+ validUntil: payload.validUntil,
2199
+ reasonCodes: payload.reasonCodes,
2200
+ });
2201
+ if (normalized.observationId !== payload.observationId) {
2202
+ fail('invalid_payload', '$["payload"]["observationId"]');
2203
+ }
2204
+ return;
2205
+ }
2206
+ const normalized = normalizeMeshEvidenceRetractionV1(envelope, {
2207
+ scope: payload.scope,
2208
+ targetKind: payload.targetKind,
2209
+ targetId: payload.targetId,
2210
+ targetDigest: payload.targetDigest,
2211
+ reasonCode: payload.reasonCode,
2212
+ observedAt: payload.observedAt,
2213
+ });
2214
+ if (normalized.retractionId !== payload.retractionId) {
2215
+ fail('invalid_payload', '$["payload"]["retractionId"]');
2216
+ }
2217
+ }
2218
+ catch (error) {
2219
+ if (error instanceof ValidationFailure)
2220
+ throw error;
2221
+ fail('invalid_payload', '$["payload"]');
2222
+ }
2223
+ }
1708
2224
  function validateWorkOfferTimes(payload, sentAt, expiresAt) {
1709
2225
  const bidDeadline = parseRfc3339(payload.bidDeadline, '$["payload"]["bidDeadline"]');
1710
2226
  const workDeadline = parseRfc3339(payload.workDeadline, '$["payload"]["workDeadline"]');
@@ -1973,20 +2489,26 @@ function validateLifetime(type, sentAt, expiresAt, limits) {
1973
2489
  }
1974
2490
  const messageMaximum = type === 'peer.ping' || type === 'peer.ping_ack'
1975
2491
  ? 30_000
1976
- : type === 'peer.goodbye' ||
1977
- type === 'lease.takeover_proposal' ||
1978
- type === 'lease.vote' ||
1979
- type === 'lease.certificate'
1980
- ? 60_000
1981
- : type === 'objective.announce' || type === 'objective.revise'
1982
- ? 5 * 60_000
1983
- : type === 'work.progress' ||
1984
- type === 'work.checkpoint' ||
1985
- type === 'work.result'
1986
- ? maximumWorkExecutionLifetimeMs
1987
- : type === 'lease.renew'
1988
- ? 30_000
1989
- : 120_000;
2492
+ : type === 'evidence.claim' ||
2493
+ type === 'evidence.attest' ||
2494
+ type === 'evidence.challenge' ||
2495
+ type === 'evidence.retract' ||
2496
+ type === 'trust.observation'
2497
+ ? maximumEvidenceLifetimeMs
2498
+ : type === 'peer.goodbye' ||
2499
+ type === 'lease.takeover_proposal' ||
2500
+ type === 'lease.vote' ||
2501
+ type === 'lease.certificate'
2502
+ ? 60_000
2503
+ : type === 'objective.announce' || type === 'objective.revise'
2504
+ ? 5 * 60_000
2505
+ : type === 'work.progress' ||
2506
+ type === 'work.checkpoint' ||
2507
+ type === 'work.result'
2508
+ ? maximumWorkExecutionLifetimeMs
2509
+ : type === 'lease.renew'
2510
+ ? 30_000
2511
+ : 120_000;
1990
2512
  const maximum = Math.min(limits.maximumLifetimeMs, messageMaximum);
1991
2513
  if (expiresAt - sentAt > BigInt(maximum) * 1000000n) {
1992
2514
  fail('invalid_lifetime', '$["expiresAt"]');
@@ -2174,7 +2696,12 @@ function isImplementedMessageType(value) {
2174
2696
  value === 'lease.renew' ||
2175
2697
  value === 'lease.takeover_proposal' ||
2176
2698
  value === 'lease.vote' ||
2177
- value === 'lease.certificate');
2699
+ value === 'lease.certificate' ||
2700
+ value === 'evidence.claim' ||
2701
+ value === 'evidence.attest' ||
2702
+ value === 'evidence.challenge' ||
2703
+ value === 'evidence.retract' ||
2704
+ value === 'trust.observation');
2178
2705
  }
2179
2706
  function canonicalizeMeshJsonInternal(input, options) {
2180
2707
  const limits = resolveLimits(options.limits);