diff --git a/contracts/epistemics/research.go b/contracts/epistemics/research.go index 6231110..fb6984b 100644 --- a/contracts/epistemics/research.go +++ b/contracts/epistemics/research.go @@ -17,23 +17,27 @@ type CoverageState string type ConflictState string type EvidenceKind string type ResearchFindingState string +type ReliabilityState string const ( - FreshnessCurrent FreshnessState = "CURRENT" - FreshnessStale FreshnessState = "STALE" - FreshnessUnknown FreshnessState = "UNKNOWN" - CoverageComplete CoverageState = "COMPLETE" - CoveragePartial CoverageState = "PARTIAL" - CoverageUnknown CoverageState = "UNKNOWN" - ConflictNone ConflictState = "NONE" - ConflictPresent ConflictState = "PRESENT" - ConflictUnknown ConflictState = "UNKNOWN" - EvidenceObserved EvidenceKind = "OBSERVED" - EvidenceAssumed EvidenceKind = "ASSUMED" - FindingSupported ResearchFindingState = "SUPPORTED" - FindingStale ResearchFindingState = "STALE" - FindingConflicted ResearchFindingState = "CONFLICTED" - FindingUnknown ResearchFindingState = "UNKNOWN" + FreshnessCurrent FreshnessState = "CURRENT" + FreshnessStale FreshnessState = "STALE" + FreshnessUnknown FreshnessState = "UNKNOWN" + CoverageComplete CoverageState = "COMPLETE" + CoveragePartial CoverageState = "PARTIAL" + CoverageUnknown CoverageState = "UNKNOWN" + ConflictNone ConflictState = "NONE" + ConflictPresent ConflictState = "PRESENT" + ConflictUnknown ConflictState = "UNKNOWN" + EvidenceObserved EvidenceKind = "OBSERVED" + EvidenceAssumed EvidenceKind = "ASSUMED" + FindingSupported ResearchFindingState = "SUPPORTED" + FindingStale ResearchFindingState = "STALE" + FindingConflicted ResearchFindingState = "CONFLICTED" + FindingUnknown ResearchFindingState = "UNKNOWN" + ReliabilityCurrent ReliabilityState = "CURRENT" + ReliabilityAging ReliabilityState = "AGING" + ReliabilityUnknown ReliabilityState = "UNKNOWN" ) type ResearchPlan struct { @@ -52,6 +56,21 @@ type Source struct { Publisher string } +type SourceReliabilityAssessment struct { + SourceRef string + DomainRef string + Freshness FreshnessState + HistoryScoreMillis int64 + ReliabilityScoreMillis int64 + State ReliabilityState + EvidenceRefs []string + AssessedAtUnixMillis int64 +} + +func (a SourceReliabilityAssessment) Valid() bool { + return a.SourceRef != "" && a.DomainRef != "" && a.Freshness != "" && a.HistoryScoreMillis >= 0 && a.HistoryScoreMillis <= 1000 && a.ReliabilityScoreMillis >= 0 && a.ReliabilityScoreMillis <= 1000 && a.State != "" && a.AssessedAtUnixMillis > 0 +} + type SourceSnapshot struct { ID SourceSnapshotID SourceID SourceID diff --git a/contracts/identity/types.go b/contracts/identity/types.go index 82da84e..c074ead 100644 --- a/contracts/identity/types.go +++ b/contracts/identity/types.go @@ -6,6 +6,30 @@ type ActorID ids.ID type PrincipalID ids.ID type IdentityID ids.ID +type DataRealmKind string + +const ( + DataRealmPersonal DataRealmKind = "PERSONAL" + DataRealmShared DataRealmKind = "SHARED" + DataRealmPublic DataRealmKind = "PUBLIC" +) + +// PrincipalDataRealm is an identity-owned access boundary. It does not make +// a speaker or World person referent an authenticated principal. +type PrincipalDataRealm struct { + ID string + OwnerPrincipalID PrincipalID + Kind DataRealmKind + MemberPrincipalIDs []PrincipalID + Shared bool + Revision int64 + RecordedAtUnixMillis int64 +} + +func (r PrincipalDataRealm) Valid() bool { + return r.ID != "" && r.OwnerPrincipalID != "" && (r.Kind == DataRealmPersonal || r.Kind == DataRealmShared || r.Kind == DataRealmPublic) && r.Revision > 0 && r.RecordedAtUnixMillis > 0 && (r.Kind != DataRealmShared || r.Shared) +} + // PrincipalBinding keeps the operational actor separate from the interest on // whose behalf it acts. A Creator is not exempt from this distinction. type PrincipalBinding struct { diff --git a/contracts/interaction/types.go b/contracts/interaction/types.go index 948393f..e743dc6 100644 --- a/contracts/interaction/types.go +++ b/contracts/interaction/types.go @@ -26,6 +26,13 @@ type SemanticCandidateSource string type SemanticCandidateSelectionState string type InformationRequestKind string type ConfigurationCommandKind string +type SpeakerAttribution struct { + SourceRef string + PrincipalCandidate string + ConfidenceMillis int64 + Authenticated bool + ObservationRef string +} const ( ChannelWebChat Channel = "WEB_CHAT" diff --git a/contracts/world/primitives.go b/contracts/world/primitives.go index 67fd161..7e3bedf 100644 --- a/contracts/world/primitives.go +++ b/contracts/world/primitives.go @@ -104,6 +104,16 @@ type ValueTypeSpec struct { type WorldObjectID ids.StableID type KindDefinitionID ids.StableID type WorldAliasID ids.StableID +type PersonReferent struct { + WorldObjectRef SemanticReference + DisplayName string + ConfidenceMillis int64 +} + +func (p PersonReferent) Valid() bool { + return p.WorldObjectRef.Valid() && p.DisplayName != "" && p.ConfidenceMillis >= 0 && p.ConfidenceMillis <= 1000 +} + type FacetDefinitionID ids.StableID type RelationDefinitionID ids.StableID type ResolutionDecisionID ids.StableID diff --git a/database/migrations/051_identity_principal_realms.sql b/database/migrations/051_identity_principal_realms.sql new file mode 100644 index 0000000..fc0855f --- /dev/null +++ b/database/migrations/051_identity_principal_realms.sql @@ -0,0 +1,11 @@ +BEGIN; +SET ROLE identity_role; +CREATE TABLE IF NOT EXISTS identity.principal_data_realms ( + realm_id TEXT PRIMARY KEY, owner_principal_id TEXT NOT NULL, realm_kind TEXT NOT NULL, + member_principal_ids TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], shared BOOLEAN NOT NULL DEFAULT FALSE, + revision BIGINT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL, + CHECK (realm_id <> ''), CHECK (owner_principal_id <> ''), CHECK (realm_kind IN ('PERSONAL','SHARED','PUBLIC')), + CHECK (realm_kind <> 'SHARED' OR shared), CHECK (revision > 0), CHECK (recorded_at_unix_millis > 0) +); +RESET ROLE; +COMMIT; diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index d704c5e..919a88f 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1 +1 @@ -{"task_file":"dev/tasks/GEN2-023-make-conversational-control-the-primary-configuration-and-operational-ux.json","task_id":"GEN2-023","task_revision":1} +{"task_file":"dev/tasks/GEN2-024-multi-person-realms-identity-boundaries-and-contextual-source-trust.json","task_id":"GEN2-024","task_revision":1} diff --git a/modules/epistemics/domain/reliability.go b/modules/epistemics/domain/reliability.go new file mode 100644 index 0000000..5f8f4d0 --- /dev/null +++ b/modules/epistemics/domain/reliability.go @@ -0,0 +1,14 @@ +package domain + +import "git.cin.su/m/Assistant/contracts/epistemics" + +func AssessSourceReliability(sourceRef, domainRef string, freshness epistemics.FreshnessState, historyScore, priorScore int64, evidence []string, at int64) epistemics.SourceReliabilityAssessment { + score := (historyScore + priorScore) / 2 + state := epistemics.ReliabilityCurrent + if freshness == epistemics.FreshnessUnknown { + state = epistemics.ReliabilityUnknown + } else if freshness == epistemics.FreshnessStale { + state = epistemics.ReliabilityAging + } + return epistemics.SourceReliabilityAssessment{SourceRef: sourceRef, DomainRef: domainRef, Freshness: freshness, HistoryScoreMillis: historyScore, ReliabilityScoreMillis: score, State: state, EvidenceRefs: append([]string(nil), evidence...), AssessedAtUnixMillis: at} +} diff --git a/modules/epistemics/domain/reliability_test.go b/modules/epistemics/domain/reliability_test.go new file mode 100644 index 0000000..6b06f17 --- /dev/null +++ b/modules/epistemics/domain/reliability_test.go @@ -0,0 +1,14 @@ +package domain_test + +import ( + "git.cin.su/m/Assistant/contracts/epistemics" + "git.cin.su/m/Assistant/modules/epistemics/domain" + "testing" +) + +func TestSourceReliabilityIncludesDomainFreshnessAndHistory(t *testing.T) { + a := domain.AssessSourceReliability("source", "weather", epistemics.FreshnessStale, 900, 700, []string{"history"}, 1) + if !a.Valid() || a.State != epistemics.ReliabilityAging || a.ReliabilityScoreMillis != 800 { + t.Fatalf("assessment=%+v", a) + } +} diff --git a/modules/identity/domain/realm.go b/modules/identity/domain/realm.go new file mode 100644 index 0000000..ce17168 --- /dev/null +++ b/modules/identity/domain/realm.go @@ -0,0 +1,36 @@ +package domain + +import ( + "errors" + "git.cin.su/m/Assistant/contracts/identity" +) + +var ErrInvalidRealm = errors.New("invalid principal data realm") + +func ValidatePrincipalDataRealm(value identity.PrincipalDataRealm) error { + if !value.Valid() { + return ErrInvalidRealm + } + if value.Kind == identity.DataRealmPersonal && len(value.MemberPrincipalIDs) != 0 { + return ErrInvalidRealm + } + return nil +} + +func CanReadRealm(value identity.PrincipalDataRealm, principalID identity.PrincipalID) bool { + if ValidatePrincipalDataRealm(value) != nil || principalID == "" { + return false + } + if value.Kind == identity.DataRealmPublic { + return true + } + if principalID == value.OwnerPrincipalID { + return true + } + for _, member := range value.MemberPrincipalIDs { + if member == principalID { + return value.Kind == identity.DataRealmShared && value.Shared + } + } + return false +} diff --git a/modules/identity/domain/realm_test.go b/modules/identity/domain/realm_test.go new file mode 100644 index 0000000..e5d8059 --- /dev/null +++ b/modules/identity/domain/realm_test.go @@ -0,0 +1,14 @@ +package domain_test + +import ( + "git.cin.su/m/Assistant/contracts/identity" + "git.cin.su/m/Assistant/modules/identity/domain" + "testing" +) + +func TestRealmSeparatesOwnerSharedAndPublicAccess(t *testing.T) { + r := identity.PrincipalDataRealm{ID: "realm", OwnerPrincipalID: "owner", Kind: identity.DataRealmShared, Shared: true, MemberPrincipalIDs: []identity.PrincipalID{"member"}, Revision: 1, RecordedAtUnixMillis: 1} + if !domain.CanReadRealm(r, "owner") || !domain.CanReadRealm(r, "member") || domain.CanReadRealm(r, "guest") { + t.Fatalf("realm access leaked: %+v", r) + } +} diff --git a/modules/interaction/domain/speaker.go b/modules/interaction/domain/speaker.go new file mode 100644 index 0000000..8d8a000 --- /dev/null +++ b/modules/interaction/domain/speaker.go @@ -0,0 +1,12 @@ +package domain + +import "git.cin.su/m/Assistant/contracts/interaction" + +// AttributeSpeaker is an observational hint only. It deliberately cannot +// manufacture an authenticated principal binding from voice recognition. +func AttributeSpeaker(sourceRef, principalCandidate, observationRef string, confidence int64) interaction.SpeakerAttribution { + if sourceRef == "" || observationRef == "" || confidence < 0 || confidence > 1000 { + return interaction.SpeakerAttribution{} + } + return interaction.SpeakerAttribution{SourceRef: sourceRef, PrincipalCandidate: principalCandidate, ConfidenceMillis: confidence, ObservationRef: observationRef, Authenticated: false} +} diff --git a/modules/interaction/domain/speaker_test.go b/modules/interaction/domain/speaker_test.go new file mode 100644 index 0000000..fba901d --- /dev/null +++ b/modules/interaction/domain/speaker_test.go @@ -0,0 +1,13 @@ +package domain_test + +import ( + "git.cin.su/m/Assistant/modules/interaction/domain" + "testing" +) + +func TestSpeakerRecognitionRemainsObservationNotAuthentication(t *testing.T) { + a := domain.AttributeSpeaker("mic-1", "principal-member", "observation-1", 700) + if a.Authenticated || a.PrincipalCandidate == "" || a.ObservationRef == "" { + t.Fatalf("attribution=%+v", a) + } +}