apsimo 1.3.0__py3-none-any.whl
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.
- apsimo/__init__.py +38 -0
- apsimo/__main__.py +6 -0
- apsimo/agent/__init__.py +6 -0
- apsimo/agent/client.py +276 -0
- apsimo/agent/models.py +46 -0
- apsimo/agents/__init__.py +20 -0
- apsimo/agents/models.py +264 -0
- apsimo/agents/store.py +861 -0
- apsimo/agents/websocket.py +522 -0
- apsimo/api/__init__.py +1 -0
- apsimo/api/auth_telemetry.py +287 -0
- apsimo/api/authority.py +1203 -0
- apsimo/api/contact_grants.py +347 -0
- apsimo/api/middleware.py +483 -0
- apsimo/api/routers/__init__.py +1 -0
- apsimo/api/routers/commitment_work.py +265 -0
- apsimo/api/routers/context_gate.py +123 -0
- apsimo/api/routers/executions.py +140 -0
- apsimo/api/routers/followup_plans.py +147 -0
- apsimo/api/routers/governed_actions.py +162 -0
- apsimo/api/routers/host.py +14473 -0
- apsimo/api/routers/initiative_work.py +115 -0
- apsimo/api/routers/mining.py +104 -0
- apsimo/api/routers/observations.py +110 -0
- apsimo/api/routers/social_state.py +225 -0
- apsimo/api/routers/task_queue.py +2715 -0
- apsimo/api/routers/temporal_followups.py +251 -0
- apsimo/api/routers/transport.py +110 -0
- apsimo/api/routers/transport_ingress_api.py +240 -0
- apsimo/api/schemas/__init__.py +1 -0
- apsimo/api/schemas/host.py +1949 -0
- apsimo/autonomy/cli.py +110 -0
- apsimo/autonomy/condition_worker.py +437 -0
- apsimo/autonomy/config.py +424 -0
- apsimo/autonomy/loop.py +4316 -0
- apsimo/autonomy/registry.py +339 -0
- apsimo/autonomy/scheduler.py +1822 -0
- apsimo/autonomy/synthesis.py +449 -0
- apsimo/backup.py +962 -0
- apsimo/beliefs/__init__.py +23 -0
- apsimo/beliefs/contradictions.py +109 -0
- apsimo/beliefs/decay.py +61 -0
- apsimo/beliefs/engine.py +479 -0
- apsimo/beliefs/models.py +67 -0
- apsimo/beliefs/promotion.py +41 -0
- apsimo/beliefs/resolve.py +58 -0
- apsimo/beliefs/source_claims.py +690 -0
- apsimo/beliefs/source_projection.py +883 -0
- apsimo/beliefs/source_time.py +208 -0
- apsimo/beliefs/store.py +133 -0
- apsimo/briefings/aggregators.py +824 -0
- apsimo/briefings/composer.py +420 -0
- apsimo/briefings/config.py +55 -0
- apsimo/briefings/delivery.py +439 -0
- apsimo/briefings/engagement.py +97 -0
- apsimo/briefings/engine.py +274 -0
- apsimo/briefings/enhancer.py +99 -0
- apsimo/briefings/models.py +183 -0
- apsimo/briefings/scheduler.py +382 -0
- apsimo/briefings/store.py +435 -0
- apsimo/chain/__init__.py +48 -0
- apsimo/chain/block.py +100 -0
- apsimo/chain/cli.py +704 -0
- apsimo/chain/genesis.py +443 -0
- apsimo/chain/identity.py +416 -0
- apsimo/chain/keys.py +1025 -0
- apsimo/chain/local_keys.py +187 -0
- apsimo/chain/manager.py +290 -0
- apsimo/chain/node.py +163 -0
- apsimo/chain/plugin_transactions.py +371 -0
- apsimo/chain/protocol.py +220 -0
- apsimo/chain/state_machine.py +676 -0
- apsimo/chain/storage.py +503 -0
- apsimo/chain/transactions.py +250 -0
- apsimo/chain/validation.py +397 -0
- apsimo/channels/__init__.py +1 -0
- apsimo/channels/manifest.py +31 -0
- apsimo/channels/migrations/001_channels_schema.sql +12 -0
- apsimo/channels/phone_gateways.py +42 -0
- apsimo/channels/presence.py +188 -0
- apsimo/channels/router.py +235 -0
- apsimo/channels/store.py +231 -0
- apsimo/cli.py +2688 -0
- apsimo/cognition/__init__.py +11 -0
- apsimo/cognition/charter.py +398 -0
- apsimo/cognition/drive_governance.py +3530 -0
- apsimo/cognition/evidence_pipeline.py +1627 -0
- apsimo/cognition/external_events.py +932 -0
- apsimo/cognition/goal_spine.py +3488 -0
- apsimo/cognition/introspection.py +214 -0
- apsimo/cognition/prompt.py +150 -0
- apsimo/cognition/runtime.py +108 -0
- apsimo/cognition/trigger.py +154 -0
- apsimo/commitments/__init__.py +18 -0
- apsimo/commitments/local_work.py +355 -0
- apsimo/commitments/store.py +1052 -0
- apsimo/commitments/work.py +91 -0
- apsimo/compat.py +53 -0
- apsimo/compression/__init__.py +467 -0
- apsimo/connectors/__init__.py +21 -0
- apsimo/connectors/base.py +152 -0
- apsimo/connectors/caldav_calendar.py +125 -0
- apsimo/connectors/fs_documents.py +85 -0
- apsimo/connectors/imap_email.py +138 -0
- apsimo/connectors/manager.py +218 -0
- apsimo/connectors/webhook_pull.py +88 -0
- apsimo/contacts/__init__.py +33 -0
- apsimo/contacts/comms.py +357 -0
- apsimo/contacts/config.py +79 -0
- apsimo/contacts/exporters/__init__.py +1 -0
- apsimo/contacts/exporters/vcard.py +71 -0
- apsimo/contacts/identity_links.py +251 -0
- apsimo/contacts/importer.py +280 -0
- apsimo/contacts/importers/__init__.py +1 -0
- apsimo/contacts/importers/batch.py +43 -0
- apsimo/contacts/importers/macos_contacts.py +101 -0
- apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
- apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
- apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
- apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
- apsimo/contacts/migrations/005_identity_links.sql +27 -0
- apsimo/contacts/models.py +308 -0
- apsimo/contacts/scoring.py +16 -0
- apsimo/contacts/store.py +1623 -0
- apsimo/contacts/transport_ingress.py +252 -0
- apsimo/contacts/world_bridge.py +314 -0
- apsimo/contextgate/__init__.py +69 -0
- apsimo/contextgate/chunker.py +169 -0
- apsimo/contextgate/estimate.py +54 -0
- apsimo/contextgate/gate.py +313 -0
- apsimo/contextgate/retrieve.py +115 -0
- apsimo/delivery/__init__.py +16 -0
- apsimo/delivery/bridge.py +1260 -0
- apsimo/delivery/channels.py +526 -0
- apsimo/delivery/classification.py +50 -0
- apsimo/delivery/rate_limiter.py +268 -0
- apsimo/delivery/reachout_policy.py +206 -0
- apsimo/directed/__init__.py +22 -0
- apsimo/directed/audit.py +167 -0
- apsimo/directed/intake.py +95 -0
- apsimo/directed/models.py +191 -0
- apsimo/directed/service.py +509 -0
- apsimo/directives/__init__.py +25 -0
- apsimo/directives/evidence.py +87 -0
- apsimo/directives/extractor.py +188 -0
- apsimo/directives/guard.py +364 -0
- apsimo/directives/models.py +206 -0
- apsimo/directives/service.py +372 -0
- apsimo/directives/store.py +167 -0
- apsimo/doctor.py +2173 -0
- apsimo/environment.py +43 -0
- apsimo/events/__init__.py +33 -0
- apsimo/events/broadcaster.py +98 -0
- apsimo/events/bus.py +217 -0
- apsimo/events/journal.py +863 -0
- apsimo/events/stream.py +131 -0
- apsimo/events/types.py +150 -0
- apsimo/execution_results.py +357 -0
- apsimo/feedback/__init__.py +5 -0
- apsimo/feedback/store.py +76 -0
- apsimo/feeds/__init__.py +19 -0
- apsimo/feeds/cli.py +84 -0
- apsimo/feeds/engine.py +437 -0
- apsimo/feeds/example-feed.yaml +77 -0
- apsimo/feeds/hermes_cron.py +126 -0
- apsimo/feeds/manager.py +235 -0
- apsimo/feeds/spec.py +250 -0
- apsimo/feeds/template.py +202 -0
- apsimo/gate/__init__.py +18 -0
- apsimo/gate/audit.py +61 -0
- apsimo/gate/communication_policy.py +166 -0
- apsimo/gate/config.py +72 -0
- apsimo/gate/context_provenance.py +170 -0
- apsimo/gate/env_risk.py +226 -0
- apsimo/gate/guard_audit.py +353 -0
- apsimo/gate/layers/__init__.py +1 -0
- apsimo/gate/layers/base.py +15 -0
- apsimo/gate/layers/l1_recipient.py +66 -0
- apsimo/gate/layers/l2_pii.py +134 -0
- apsimo/gate/layers/l3_cross_context.py +50 -0
- apsimo/gate/layers/l4_trust_tier.py +78 -0
- apsimo/gate/layers/l5_injection.py +199 -0
- apsimo/gate/layers/l6_review.py +86 -0
- apsimo/gate/layers/l7_delay.py +100 -0
- apsimo/gate/layers/tom2_epistemic.py +185 -0
- apsimo/gate/models.py +64 -0
- apsimo/gate/pending_dispatch.py +5 -0
- apsimo/gate/pipeline.py +206 -0
- apsimo/gate/rejection.py +259 -0
- apsimo/gate/response_guard.py +700 -0
- apsimo/gate/rulesets/injection_v1.yaml +51 -0
- apsimo/gate/surface_policy.py +189 -0
- apsimo/gate/taint.py +226 -0
- apsimo/genesis.json +9 -0
- apsimo/goals/__init__.py +100 -0
- apsimo/goals/config.py +38 -0
- apsimo/goals/decomposer.py +421 -0
- apsimo/goals/engine.py +617 -0
- apsimo/goals/inference.py +354 -0
- apsimo/goals/models.py +302 -0
- apsimo/goals/priority.py +270 -0
- apsimo/goals/queue_bridge.py +149 -0
- apsimo/goals/replan.py +450 -0
- apsimo/goals/schema.sql +89 -0
- apsimo/goals/store.py +692 -0
- apsimo/governed_actions.py +1708 -0
- apsimo/harness_integration/__init__.py +45 -0
- apsimo/harness_integration/context.py +41 -0
- apsimo/harness_integration/skills.py +231 -0
- apsimo/identity/__init__.py +26 -0
- apsimo/identity/participants.py +181 -0
- apsimo/identity/resolver.py +329 -0
- apsimo/identity_bootstrap/__init__.py +5 -0
- apsimo/identity_bootstrap/builder.py +208 -0
- apsimo/identity_bootstrap/corpus.py +443 -0
- apsimo/identity_bootstrap/models.py +54 -0
- apsimo/identity_bootstrap/runner.py +353 -0
- apsimo/identity_bootstrap/seeders/__init__.py +25 -0
- apsimo/identity_bootstrap/seeders/briefings.py +109 -0
- apsimo/identity_bootstrap/seeders/chain.py +57 -0
- apsimo/identity_bootstrap/seeders/goals.py +128 -0
- apsimo/identity_bootstrap/seeders/memory.py +191 -0
- apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
- apsimo/identity_bootstrap/seeders/relationship.py +152 -0
- apsimo/identity_bootstrap/seeders/sessions.py +67 -0
- apsimo/identity_bootstrap/seeders/skills.py +92 -0
- apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
- apsimo/identity_bootstrap/seeders/world_model.py +143 -0
- apsimo/identity_bootstrap/self_query.py +92 -0
- apsimo/identity_bootstrap/self_reflection.py +155 -0
- apsimo/identity_bootstrap/skill.py +37 -0
- apsimo/identity_bootstrap/verifier.py +436 -0
- apsimo/initiatives/__init__.py +20 -0
- apsimo/initiatives/action_registry.py +454 -0
- apsimo/initiatives/approval_authority.py +2105 -0
- apsimo/initiatives/approval_policy.py +123 -0
- apsimo/initiatives/assignment.py +263 -0
- apsimo/initiatives/backup_evidence.py +100 -0
- apsimo/initiatives/context_freshness.py +103 -0
- apsimo/initiatives/models.py +318 -0
- apsimo/initiatives/native_work.py +270 -0
- apsimo/initiatives/standing_approvals.py +232 -0
- apsimo/initiatives/store.py +1081 -0
- apsimo/initiatives/temporal_followup.py +410 -0
- apsimo/intelligence/__init__.py +1 -0
- apsimo/intelligence/cognition/__init__.py +24 -0
- apsimo/intelligence/cognition/gap_detector.py +148 -0
- apsimo/intelligence/cognition/metalearner.py +547 -0
- apsimo/intelligence/cognition/metrics_collector.py +217 -0
- apsimo/intelligence/cognition/performance_index.py +299 -0
- apsimo/intelligence/cognition/registry.py +192 -0
- apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
- apsimo/intelligence/cognition/types.py +16 -0
- apsimo/intelligence/components/__init__.py +66 -0
- apsimo/intelligence/components/anomaly_detector.py +413 -0
- apsimo/intelligence/components/initiative_engine.py +2643 -0
- apsimo/intelligence/components/preference_learner.py +521 -0
- apsimo/intelligence/components/research_orchestrator.py +358 -0
- apsimo/intelligence/components/self_directed_thinker.py +221 -0
- apsimo/intelligence/components/self_reflector.py +252 -0
- apsimo/intelligence/components/session_continuity.py +154 -0
- apsimo/intelligence/components/task_planner.py +320 -0
- apsimo/intelligence/components/tool_learner.py +217 -0
- apsimo/intelligence/graph/__init__.py +79 -0
- apsimo/intelligence/graph/client.py +2483 -0
- apsimo/intelligence/graph/consolidator.py +405 -0
- apsimo/intelligence/graph/distiller.py +312 -0
- apsimo/intelligence/graph/migrations.py +129 -0
- apsimo/intelligence/graph/queries.py +248 -0
- apsimo/intelligence/graph/recall.py +281 -0
- apsimo/intelligence/graph/reconciler.py +144 -0
- apsimo/intelligence/graph/schema.py +337 -0
- apsimo/intelligence/graph/selection.py +252 -0
- apsimo/intelligence/learning/__init__.py +17 -0
- apsimo/intelligence/learning/continuous_learner.py +245 -0
- apsimo/intelligence/learning/feedback_store.py +321 -0
- apsimo/intelligence/mind_model/__init__.py +1 -0
- apsimo/intelligence/mind_model/graph_baseline.py +136 -0
- apsimo/intelligence/mind_model/signal_collector.py +361 -0
- apsimo/intelligence/relationships/__init__.py +11 -0
- apsimo/intelligence/relationships/profiler.py +389 -0
- apsimo/intelligence/relationships/scorer.py +560 -0
- apsimo/intelligence/relationships/signal_floor.py +66 -0
- apsimo/intelligence/relationships/trust_tiers.py +300 -0
- apsimo/intelligence/synthesis/__init__.py +40 -0
- apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
- apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
- apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
- apsimo/intelligence/synthesis/insight_store.py +79 -0
- apsimo/intelligence/synthesis/insight_validator.py +183 -0
- apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
- apsimo/intelligence/turn_middleware/__init__.py +15 -0
- apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
- apsimo/mcp/__init__.py +41 -0
- apsimo/mcp/__main__.py +6 -0
- apsimo/mcp/config.py +287 -0
- apsimo/mcp/server.py +501 -0
- apsimo/migrations.py +187 -0
- apsimo/mining/__init__.py +27 -0
- apsimo/mining/corpus.py +239 -0
- apsimo/mining/escalations.py +289 -0
- apsimo/mining/models.py +169 -0
- apsimo/mining/store.py +210 -0
- apsimo/models/__init__.py +30 -0
- apsimo/models/memory.py +80 -0
- apsimo/models/mesh.py +72 -0
- apsimo/models/person.py +104 -0
- apsimo/models/signal.py +108 -0
- apsimo/observations/__init__.py +15 -0
- apsimo/observations/store.py +277 -0
- apsimo/patterns/__init__.py +6 -0
- apsimo/patterns/extract.py +187 -0
- apsimo/patterns/store.py +227 -0
- apsimo/persona/__init__.py +1 -0
- apsimo/persona/engine.py +611 -0
- apsimo/persona/manifest.py +140 -0
- apsimo/projects/__init__.py +28 -0
- apsimo/projects/engine.py +1681 -0
- apsimo/projects/event_outbox.py +188 -0
- apsimo/projects/models.py +216 -0
- apsimo/projects/planner.py +181 -0
- apsimo/projects/store.py +1446 -0
- apsimo/proposals/__init__.py +12 -0
- apsimo/proposals/engine.py +114 -0
- apsimo/proposals/models.py +207 -0
- apsimo/qualification/__init__.py +1 -0
- apsimo/qualification/cases.py +75 -0
- apsimo/qualification/cli.py +51 -0
- apsimo/qualification/memory_cases.py +209 -0
- apsimo/qualification/records.py +92 -0
- apsimo/qualification/report.py +87 -0
- apsimo/qualification/runner.py +311 -0
- apsimo/qualification/structured_cases.py +131 -0
- apsimo/reasoning/__init__.py +13 -0
- apsimo/reasoning/executor.py +506 -0
- apsimo/reasoning/loop.py +373 -0
- apsimo/reasoning/native_tools/__init__.py +16 -0
- apsimo/reasoning/native_tools/calculate.py +141 -0
- apsimo/reasoning/native_tools/file_ops.py +150 -0
- apsimo/reasoning/native_tools/web_search.py +49 -0
- apsimo/reasoning/tool_policy.py +182 -0
- apsimo/redact/__init__.py +176 -0
- apsimo/repos/__init__.py +5 -0
- apsimo/repos/mirrors.py +204 -0
- apsimo/research/__init__.py +41 -0
- apsimo/research/artifact.py +482 -0
- apsimo/research/gatherer.py +387 -0
- apsimo/research/pipeline.py +513 -0
- apsimo/research/search/__init__.py +7 -0
- apsimo/research/search/base.py +41 -0
- apsimo/research/search/brave.py +59 -0
- apsimo/research/search/cache.py +51 -0
- apsimo/research/search/duckduckgo.py +103 -0
- apsimo/research/search/orchestrator.py +119 -0
- apsimo/research/search/serpapi.py +59 -0
- apsimo/research/search/tavily.py +59 -0
- apsimo/research/synthesizer.py +309 -0
- apsimo/router/__init__.py +30 -0
- apsimo/router/complexity_scorer.py +148 -0
- apsimo/router/endpoints.py +153 -0
- apsimo/router/fallback.py +58 -0
- apsimo/router/functions.py +243 -0
- apsimo/router/native_policy.py +52 -0
- apsimo/router/router.py +762 -0
- apsimo/router/self_learning.py +174 -0
- apsimo/router/tiers.py +677 -0
- apsimo/sandbox/__init__.py +21 -0
- apsimo/sandbox/backend.py +195 -0
- apsimo/sandbox/manager.py +173 -0
- apsimo/scope_bounds.py +7 -0
- apsimo/secrets/__init__.py +6 -0
- apsimo/secrets/backends/__init__.py +8 -0
- apsimo/secrets/backends/base.py +42 -0
- apsimo/secrets/backends/env.py +110 -0
- apsimo/secrets/backends/keyring.py +72 -0
- apsimo/secrets/backends/onepassword.py +232 -0
- apsimo/secrets/cli.py +191 -0
- apsimo/secrets/manager.py +160 -0
- apsimo/secrets/migration.py +101 -0
- apsimo/secrets/types.py +98 -0
- apsimo/seed.py +41 -0
- apsimo/self_model/__init__.py +37 -0
- apsimo/self_model/appraisals.py +673 -0
- apsimo/self_model/benchmark.py +1314 -0
- apsimo/self_model/brief.py +40 -0
- apsimo/self_model/event_concerns.py +1128 -0
- apsimo/self_model/execution_forecasts.py +353 -0
- apsimo/self_model/expectations.py +1595 -0
- apsimo/self_model/experiments.py +1150 -0
- apsimo/self_model/journal.py +148 -0
- apsimo/self_model/judgments.py +705 -0
- apsimo/self_model/native_outcomes.py +55 -0
- apsimo/self_model/params.py +220 -0
- apsimo/self_model/perspective.py +246 -0
- apsimo/self_model/reconcile.py +183 -0
- apsimo/self_model/reply_forecasts.py +381 -0
- apsimo/self_model/runtime_forecasts.py +296 -0
- apsimo/self_model/runtime_models.py +67 -0
- apsimo/self_model/settlement.py +207 -0
- apsimo/self_model/situation.py +1731 -0
- apsimo/self_model/store.py +883 -0
- apsimo/self_model/supervised.py +137 -0
- apsimo/self_model/thinker.py +99 -0
- apsimo/self_model/trust.py +388 -0
- apsimo/self_model/workspace.py +2388 -0
- apsimo/server.py +4197 -0
- apsimo/services/__init__.py +1 -0
- apsimo/services/agent_bridge.py +474 -0
- apsimo/services/initiative_executor.py +914 -0
- apsimo/services/instance.py +297 -0
- apsimo/sessions/__init__.py +22 -0
- apsimo/sessions/config.py +13 -0
- apsimo/sessions/context_loader.py +88 -0
- apsimo/sessions/federation_session.py +75 -0
- apsimo/sessions/isolated_session.py +98 -0
- apsimo/sessions/reports.py +84 -0
- apsimo/sessions/store.py +148 -0
- apsimo/setup.py +2818 -0
- apsimo/setup_hermes.py +879 -0
- apsimo/setup_local_work.py +218 -0
- apsimo/setup_native_goals.py +134 -0
- apsimo/setup_native_reviews.py +115 -0
- apsimo/skills/__init__.py +10 -0
- apsimo/skills/base.py +108 -0
- apsimo/skills/budget.py +28 -0
- apsimo/skills/executor.py +493 -0
- apsimo/skills/executors/__init__.py +1 -0
- apsimo/skills/executors/behavioral_correction.py +75 -0
- apsimo/skills/executors/capability_gap.py +38 -0
- apsimo/skills/executors/data_quality.py +163 -0
- apsimo/skills/executors/knowledge_acquisition.py +41 -0
- apsimo/skills/executors/operational_hygiene.py +185 -0
- apsimo/skills/executors/subsystem_health.py +169 -0
- apsimo/skills/hermes_export.py +431 -0
- apsimo/skills/index.py +123 -0
- apsimo/skills/learning/__init__.py +21 -0
- apsimo/skills/learning/novelty_detector.py +206 -0
- apsimo/skills/learning/pattern_extractor.py +199 -0
- apsimo/skills/learning/triggers.py +159 -0
- apsimo/skills/loader.py +246 -0
- apsimo/skills/migrations/002_progressive_loading.sql +6 -0
- apsimo/skills/migrations/backfill_triggers.py +20 -0
- apsimo/skills/models.py +202 -0
- apsimo/skills/packager.py +128 -0
- apsimo/skills/protocols.py +70 -0
- apsimo/skills/registry.py +191 -0
- apsimo/skills/runtime.py +58 -0
- apsimo/skills/sandbox_runner.py +229 -0
- apsimo/skills/scheduler.py +129 -0
- apsimo/skills/schema.py +79 -0
- apsimo/skills/security/__init__.py +12 -0
- apsimo/skills/security/guards.py +53 -0
- apsimo/skills/security/scanner.py +223 -0
- apsimo/skills_memory/__init__.py +26 -0
- apsimo/skills_memory/distill.py +159 -0
- apsimo/skills_memory/models.py +85 -0
- apsimo/skills_memory/retrieve.py +62 -0
- apsimo/skills_memory/store.py +172 -0
- apsimo/surprise/__init__.py +6 -0
- apsimo/surprise/accumulation.py +57 -0
- apsimo/surprise/scorer.py +102 -0
- apsimo/surprise/store.py +203 -0
- apsimo/task_queue/__init__.py +69 -0
- apsimo/task_queue/action_receipts.py +148 -0
- apsimo/task_queue/approval_relay_canary.py +108 -0
- apsimo/task_queue/config.py +85 -0
- apsimo/task_queue/contract.py +361 -0
- apsimo/task_queue/events.py +130 -0
- apsimo/task_queue/governor.py +1031 -0
- apsimo/task_queue/handlers/__init__.py +16 -0
- apsimo/task_queue/handlers/base.py +37 -0
- apsimo/task_queue/handlers/inference.py +640 -0
- apsimo/task_queue/handlers/monitoring.py +116 -0
- apsimo/task_queue/handlers/registry.py +75 -0
- apsimo/task_queue/handlers/subtask_handler.py +173 -0
- apsimo/task_queue/handlers/system_maintenance.py +147 -0
- apsimo/task_queue/mesh_integration.py +111 -0
- apsimo/task_queue/models.py +317 -0
- apsimo/task_queue/queue_manager.py +8286 -0
- apsimo/task_queue/routing.py +287 -0
- apsimo/task_queue/scheduler.py +252 -0
- apsimo/task_queue/schema.sql +197 -0
- apsimo/task_queue/work_control.py +342 -0
- apsimo/task_queue/worker.py +993 -0
- apsimo/telemetry.py +145 -0
- apsimo/tom/__init__.py +6 -0
- apsimo/tom/affect.py +387 -0
- apsimo/tom/approvals.py +171 -0
- apsimo/tom/arcs.py +896 -0
- apsimo/tom/asymmetry.py +131 -0
- apsimo/tom/eligibility.py +248 -0
- apsimo/tom/engagement.py +214 -0
- apsimo/tom/exposure.py +214 -0
- apsimo/tom/extractor.py +306 -0
- apsimo/tom/fact_adapters.py +144 -0
- apsimo/tom/facts.py +326 -0
- apsimo/tom/integration.py +592 -0
- apsimo/tom/leveled.py +118 -0
- apsimo/tom/levels.py +247 -0
- apsimo/tom/recipient_audit.py +995 -0
- apsimo/tom/recipient_simulator.py +593 -0
- apsimo/tom/source_lineage.py +93 -0
- apsimo/tom/tom2.py +277 -0
- apsimo/tom/visibility.py +559 -0
- apsimo/tom/visibility_store.py +414 -0
- apsimo/tools/__init__.py +0 -0
- apsimo/tools/definitions.py +740 -0
- apsimo/tools/handlers.py +943 -0
- apsimo/toolsmith/__init__.py +26 -0
- apsimo/toolsmith/authority.py +166 -0
- apsimo/toolsmith/engine.py +559 -0
- apsimo/toolsmith/integrity.py +100 -0
- apsimo/toolsmith/miner.py +145 -0
- apsimo/toolsmith/policy.py +110 -0
- apsimo/toolsmith/registry.py +635 -0
- apsimo/turns/__init__.py +17 -0
- apsimo/turns/audio.py +134 -0
- apsimo/turns/documents.py +235 -0
- apsimo/turns/executions.py +486 -0
- apsimo/turns/hermes_history.py +245 -0
- apsimo/turns/hermes_kanban.py +268 -0
- apsimo/turns/hermes_work.py +96 -0
- apsimo/turns/idempotency.py +752 -0
- apsimo/turns/local_work.py +115 -0
- apsimo/turns/media.py +581 -0
- apsimo/turns/reported_workers.py +196 -0
- apsimo/turns/source_annotations.py +283 -0
- apsimo/turns/source_attribution.py +154 -0
- apsimo/turns/source_read.py +351 -0
- apsimo/turns/source_vectors.py +263 -0
- apsimo/turns/video.py +210 -0
- apsimo/util/autonomy_preset.py +220 -0
- apsimo/util/instance.py +92 -0
- apsimo/util/model_output.py +25 -0
- apsimo/util/quiet_hours.py +27 -0
- apsimo/util/session_safety.py +37 -0
- apsimo/util/temporal.py +343 -0
- apsimo/vector/__init__.py +75 -0
- apsimo/vector/backfill.py +171 -0
- apsimo/vector/caption.py +114 -0
- apsimo/vector/collections.py +51 -0
- apsimo/vector/config.py +102 -0
- apsimo/vector/embedder.py +670 -0
- apsimo/vector/image_preprocess.py +406 -0
- apsimo/vector/image_store.py +296 -0
- apsimo/vector/indexes.py +162 -0
- apsimo/vector/migrate.py +334 -0
- apsimo/vector/multimodal_provider.py +417 -0
- apsimo/vector/multimodal_types.py +87 -0
- apsimo/vector/openai_provider.py +119 -0
- apsimo/vector/query.py +49 -0
- apsimo/vector/reranker.py +565 -0
- apsimo/vector/safety_image.py +159 -0
- apsimo/vector/scanner.py +197 -0
- apsimo/vector/setup.py +289 -0
- apsimo/vector/store.py +533 -0
- apsimo/vector/tiers.py +263 -0
- apsimo/work_orders.py +925 -0
- apsimo/workers/__init__.py +21 -0
- apsimo/workers/agent_bridge.py +640 -0
- apsimo/workers/colony_worker.py +382 -0
- apsimo/workers/queue_worker.py +441 -0
- apsimo/workers/skills_sync.py +152 -0
- apsimo/world_model/__init__.py +71 -0
- apsimo/world_model/causal_maintenance.py +131 -0
- apsimo/world_model/causal_policy.py +43 -0
- apsimo/world_model/causal_query.py +125 -0
- apsimo/world_model/confidence.py +54 -0
- apsimo/world_model/config.py +64 -0
- apsimo/world_model/constants.py +97 -0
- apsimo/world_model/entities.py +145 -0
- apsimo/world_model/expectation_resolvers.py +177 -0
- apsimo/world_model/extraction/__init__.py +7 -0
- apsimo/world_model/extraction/base.py +62 -0
- apsimo/world_model/extraction/conversation_extractor.py +262 -0
- apsimo/world_model/extraction/detector.py +74 -0
- apsimo/world_model/extraction/document_extractor.py +78 -0
- apsimo/world_model/extraction/formats/__init__.py +24 -0
- apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
- apsimo/world_model/extraction/formats/html_fmt.py +72 -0
- apsimo/world_model/extraction/formats/json_fmt.py +68 -0
- apsimo/world_model/extraction/formats/pdf.py +43 -0
- apsimo/world_model/extraction/formats/text.py +27 -0
- apsimo/world_model/extraction/llm_extractor.py +164 -0
- apsimo/world_model/extraction/pipeline.py +73 -0
- apsimo/world_model/integrations/__init__.py +5 -0
- apsimo/world_model/integrations/mind_model_bridge.py +115 -0
- apsimo/world_model/integrations/social_intel_bridge.py +120 -0
- apsimo/world_model/jobs/__init__.py +4 -0
- apsimo/world_model/jobs/extraction_job.py +168 -0
- apsimo/world_model/llm_extract.py +572 -0
- apsimo/world_model/neo4j/__init__.py +5 -0
- apsimo/world_model/neo4j/backend.py +654 -0
- apsimo/world_model/observations.py +155 -0
- apsimo/world_model/populator.py +307 -0
- apsimo/world_model/postgres/__init__.py +1 -0
- apsimo/world_model/postgres/backend.py +683 -0
- apsimo/world_model/relationships.py +25 -0
- apsimo/world_model/resolution/__init__.py +13 -0
- apsimo/world_model/resolution/entity_resolver.py +232 -0
- apsimo/world_model/resolution/merge_audit.py +16 -0
- apsimo/world_model/resolution/merge_workflow.py +117 -0
- apsimo/world_model/source_reports.py +121 -0
- apsimo/world_model/sqlite/__init__.py +4 -0
- apsimo/world_model/sqlite/backend.py +855 -0
- apsimo/world_model/sqlite/schema.sql +132 -0
- apsimo/world_model/store.py +545 -0
- apsimo-1.3.0.dist-info/METADATA +78 -0
- apsimo-1.3.0.dist-info/RECORD +614 -0
- apsimo-1.3.0.dist-info/WHEEL +5 -0
- apsimo-1.3.0.dist-info/entry_points.txt +11 -0
- apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
- apsimo-1.3.0.dist-info/top_level.txt +2 -0
- colony_sidecar/__init__.py +4 -0
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
"""Reference-only persistence and coverage for P8 recipient simulation.
|
|
2
|
+
|
|
3
|
+
Coverage is event based. A selected outbound item is first registered as a
|
|
4
|
+
``sample``; a later ``evaluation`` event references the same outbound item.
|
|
5
|
+
That makes a missing simulation visible instead of deriving a misleading
|
|
6
|
+
success rate solely from results that happened to be written.
|
|
7
|
+
|
|
8
|
+
Rows retain stable references, action/risk codes, and SHA-256 digests only.
|
|
9
|
+
Draft/fact content, fact and arc references, dependency errors, credentials,
|
|
10
|
+
and host topology are intentionally not represented by this schema.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
import re
|
|
22
|
+
import sqlite3
|
|
23
|
+
import threading
|
|
24
|
+
from typing import Any, Iterable, Optional
|
|
25
|
+
|
|
26
|
+
from apsimo.tom.recipient_simulator import (
|
|
27
|
+
FAIL_BEHAVIOR_BY_RISK,
|
|
28
|
+
REALTIME_VOICE_SURFACES,
|
|
29
|
+
RecipientSimulationRequestV1,
|
|
30
|
+
RecipientSimulationResultV1,
|
|
31
|
+
is_realtime_voice_surface,
|
|
32
|
+
recipient_simulator_mode,
|
|
33
|
+
)
|
|
34
|
+
from apsimo.tom.visibility import ViewerContextV1, content_digest
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
SCHEMA_VERSION = 1
|
|
38
|
+
EVENT_KINDS = frozenset({"sample", "evaluation"})
|
|
39
|
+
MAX_AUDIT_CODES = 64
|
|
40
|
+
MAX_PROJECTED_EVENTS = 256
|
|
41
|
+
MAX_MISSING_REFS = 64
|
|
42
|
+
COVERAGE_FETCH_SIZE = 512
|
|
43
|
+
|
|
44
|
+
AUDIT_RISK_CODES = frozenset({
|
|
45
|
+
"arc_projection_incomplete",
|
|
46
|
+
"fact_ref_not_recipient_authorized",
|
|
47
|
+
"high_salience_provenance_unknown",
|
|
48
|
+
"recipient_identity_unattested",
|
|
49
|
+
"simulation_dependency_error",
|
|
50
|
+
"stress_topic_pressure",
|
|
51
|
+
})
|
|
52
|
+
AUDIT_REPAIR_CODES = frozenset({
|
|
53
|
+
"add_fact_provenance",
|
|
54
|
+
"review_arc_context",
|
|
55
|
+
"soften_pressure_language",
|
|
56
|
+
})
|
|
57
|
+
AUDIT_ACTIONS = frozenset({
|
|
58
|
+
"hold", "no_effect", "observe", "observe_async", "observe_only",
|
|
59
|
+
"repair", "review", "send",
|
|
60
|
+
})
|
|
61
|
+
AUDIT_EVALUATION_PATHS = frozenset({
|
|
62
|
+
"async_observation", "disabled", "pre_send_advisory",
|
|
63
|
+
"shadow_observation",
|
|
64
|
+
})
|
|
65
|
+
AUDIT_SURFACES = REALTIME_VOICE_SURFACES.union({
|
|
66
|
+
"api", "assistant", "chat", "deck", "discord", "email",
|
|
67
|
+
"facetime", "google_chat", "imessage", "matrix", "mms",
|
|
68
|
+
"operator_deck", "rcs", "signal", "slack", "sms", "teams",
|
|
69
|
+
"telegram", "text", "web", "whatsapp", "whatsapp_call",
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/+\-]{0,191}$")
|
|
73
|
+
_DIGEST_RE = re.compile(r"^[a-f0-9]{64}$")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class RecipientAuditConflictError(ValueError):
|
|
77
|
+
"""An audit identity/outbound reference changed immutable content."""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _canonical(value: Any) -> str:
|
|
81
|
+
return json.dumps(
|
|
82
|
+
value,
|
|
83
|
+
ensure_ascii=True,
|
|
84
|
+
separators=(",", ":"),
|
|
85
|
+
sort_keys=True,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _digest(value: Any) -> str:
|
|
90
|
+
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _ref(value: Any, *, field: str, allow_empty: bool = False) -> str:
|
|
94
|
+
normalized = str(value or "").strip()
|
|
95
|
+
if allow_empty and not normalized:
|
|
96
|
+
return ""
|
|
97
|
+
if not _REF_RE.fullmatch(normalized):
|
|
98
|
+
raise ValueError(f"{field} is not a bounded opaque reference")
|
|
99
|
+
return normalized
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _sha(value: Any, *, field: str, allow_empty: bool = False) -> str:
|
|
103
|
+
normalized = str(value or "").strip().lower()
|
|
104
|
+
if allow_empty and not normalized:
|
|
105
|
+
return ""
|
|
106
|
+
if not _DIGEST_RE.fullmatch(normalized):
|
|
107
|
+
raise ValueError(f"{field} must be a SHA-256 hex digest")
|
|
108
|
+
return normalized
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _refs(values: Iterable[Any], *, field: str) -> tuple[str, ...]:
|
|
112
|
+
normalized = tuple(sorted(dict.fromkeys(
|
|
113
|
+
_ref(value, field=field) for value in values
|
|
114
|
+
)))
|
|
115
|
+
if len(normalized) > MAX_AUDIT_CODES:
|
|
116
|
+
raise ValueError(f"{field} exceeds bounded reference limit")
|
|
117
|
+
return normalized
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _iso(value: datetime | str, *, field: str) -> str:
|
|
121
|
+
if isinstance(value, datetime):
|
|
122
|
+
parsed = value
|
|
123
|
+
else:
|
|
124
|
+
try:
|
|
125
|
+
parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
126
|
+
except (TypeError, ValueError) as exc:
|
|
127
|
+
raise ValueError(f"{field} must be an ISO-8601 timestamp") from exc
|
|
128
|
+
if parsed.tzinfo is None:
|
|
129
|
+
raise ValueError(f"{field} must include a timezone")
|
|
130
|
+
return parsed.astimezone(timezone.utc).isoformat()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True, slots=True)
|
|
134
|
+
class RecipientAuditEventV1:
|
|
135
|
+
event_id: str
|
|
136
|
+
idempotency_key: str
|
|
137
|
+
event_kind: str
|
|
138
|
+
outbound_item_ref: str
|
|
139
|
+
recipient_person_id: str
|
|
140
|
+
scope_revision: str
|
|
141
|
+
high_salience: bool
|
|
142
|
+
occurred_at: str
|
|
143
|
+
simulation_ref: str = ""
|
|
144
|
+
request_digest: str = ""
|
|
145
|
+
result_digest: str = ""
|
|
146
|
+
draft_digest: str = ""
|
|
147
|
+
surface: str = ""
|
|
148
|
+
risk_class: str = ""
|
|
149
|
+
evaluated: bool = False
|
|
150
|
+
would_action: str = ""
|
|
151
|
+
effective_action: str = ""
|
|
152
|
+
evaluation_path: str = ""
|
|
153
|
+
risk_codes: tuple[str, ...] = ()
|
|
154
|
+
repair_codes: tuple[str, ...] = ()
|
|
155
|
+
schema_version: int = SCHEMA_VERSION
|
|
156
|
+
|
|
157
|
+
def __post_init__(self) -> None:
|
|
158
|
+
if self.schema_version != SCHEMA_VERSION:
|
|
159
|
+
raise ValueError("unsupported recipient audit version")
|
|
160
|
+
event_id = _ref(self.event_id, field="event_id")
|
|
161
|
+
idempotency_key = _ref(
|
|
162
|
+
self.idempotency_key, field="idempotency_key")
|
|
163
|
+
event_kind = str(self.event_kind or "").strip().lower()
|
|
164
|
+
if event_kind not in EVENT_KINDS:
|
|
165
|
+
raise ValueError("unknown recipient audit event kind")
|
|
166
|
+
outbound = _ref(self.outbound_item_ref, field="outbound_item_ref")
|
|
167
|
+
recipient = _ref(
|
|
168
|
+
self.recipient_person_id, field="recipient_person_id")
|
|
169
|
+
revision = _ref(self.scope_revision, field="scope_revision")
|
|
170
|
+
occurred_at = _iso(self.occurred_at, field="occurred_at")
|
|
171
|
+
simulation = _ref(
|
|
172
|
+
self.simulation_ref, field="simulation_ref", allow_empty=True)
|
|
173
|
+
request_digest = _sha(
|
|
174
|
+
self.request_digest, field="request_digest", allow_empty=True)
|
|
175
|
+
result_digest = _sha(
|
|
176
|
+
self.result_digest, field="result_digest", allow_empty=True)
|
|
177
|
+
draft_digest = _sha(self.draft_digest, field="draft_digest")
|
|
178
|
+
surface = _ref(self.surface, field="surface", allow_empty=True).lower()
|
|
179
|
+
risk_class = _ref(
|
|
180
|
+
self.risk_class, field="risk_class", allow_empty=True).lower()
|
|
181
|
+
would = _ref(
|
|
182
|
+
self.would_action, field="would_action", allow_empty=True).lower()
|
|
183
|
+
effective = _ref(
|
|
184
|
+
self.effective_action,
|
|
185
|
+
field="effective_action",
|
|
186
|
+
allow_empty=True,
|
|
187
|
+
).lower()
|
|
188
|
+
path = _ref(
|
|
189
|
+
self.evaluation_path,
|
|
190
|
+
field="evaluation_path",
|
|
191
|
+
allow_empty=True,
|
|
192
|
+
).lower()
|
|
193
|
+
risk_codes = _refs(self.risk_codes, field="risk_code")
|
|
194
|
+
repair_codes = _refs(self.repair_codes, field="repair_code")
|
|
195
|
+
|
|
196
|
+
evaluation_values = (
|
|
197
|
+
simulation, request_digest, result_digest, surface,
|
|
198
|
+
risk_class, would, effective, path,
|
|
199
|
+
)
|
|
200
|
+
if event_kind == "sample":
|
|
201
|
+
if any(evaluation_values) or self.evaluated\
|
|
202
|
+
or risk_codes or repair_codes:
|
|
203
|
+
raise ValueError("sample events cannot contain evaluation data")
|
|
204
|
+
else:
|
|
205
|
+
if not all(evaluation_values):
|
|
206
|
+
raise ValueError(
|
|
207
|
+
"evaluation events require complete digest metadata")
|
|
208
|
+
if risk_class not in {"low", "medium", "high", "critical"}:
|
|
209
|
+
raise ValueError("evaluation risk class is not auditable")
|
|
210
|
+
if surface not in AUDIT_SURFACES:
|
|
211
|
+
raise ValueError("evaluation surface is not auditable")
|
|
212
|
+
if would not in AUDIT_ACTIONS or effective not in AUDIT_ACTIONS:
|
|
213
|
+
raise ValueError("evaluation action is not auditable")
|
|
214
|
+
if path not in AUDIT_EVALUATION_PATHS:
|
|
215
|
+
raise ValueError("evaluation path is not auditable")
|
|
216
|
+
for code in risk_codes:
|
|
217
|
+
severity, separator, name = code.partition(":")
|
|
218
|
+
if separator != ":"\
|
|
219
|
+
or severity not in {"low", "medium", "high", "critical"}\
|
|
220
|
+
or name not in AUDIT_RISK_CODES:
|
|
221
|
+
raise ValueError("evaluation risk code is not auditable")
|
|
222
|
+
for code in repair_codes:
|
|
223
|
+
priority, separator, name = code.partition(":")
|
|
224
|
+
if separator != ":"\
|
|
225
|
+
or priority not in {"low", "medium", "high", "critical"}\
|
|
226
|
+
or name not in AUDIT_REPAIR_CODES:
|
|
227
|
+
raise ValueError("evaluation repair code is not auditable")
|
|
228
|
+
|
|
229
|
+
object.__setattr__(self, "event_id", event_id)
|
|
230
|
+
object.__setattr__(self, "idempotency_key", idempotency_key)
|
|
231
|
+
object.__setattr__(self, "event_kind", event_kind)
|
|
232
|
+
object.__setattr__(self, "outbound_item_ref", outbound)
|
|
233
|
+
object.__setattr__(self, "recipient_person_id", recipient)
|
|
234
|
+
object.__setattr__(self, "scope_revision", revision)
|
|
235
|
+
object.__setattr__(self, "high_salience", bool(self.high_salience))
|
|
236
|
+
object.__setattr__(self, "occurred_at", occurred_at)
|
|
237
|
+
object.__setattr__(self, "simulation_ref", simulation)
|
|
238
|
+
object.__setattr__(self, "request_digest", request_digest)
|
|
239
|
+
object.__setattr__(self, "result_digest", result_digest)
|
|
240
|
+
object.__setattr__(self, "draft_digest", draft_digest)
|
|
241
|
+
object.__setattr__(self, "surface", surface)
|
|
242
|
+
object.__setattr__(self, "risk_class", risk_class)
|
|
243
|
+
object.__setattr__(self, "evaluated", bool(self.evaluated))
|
|
244
|
+
object.__setattr__(self, "would_action", would)
|
|
245
|
+
object.__setattr__(self, "effective_action", effective)
|
|
246
|
+
object.__setattr__(self, "evaluation_path", path)
|
|
247
|
+
object.__setattr__(self, "risk_codes", risk_codes)
|
|
248
|
+
object.__setattr__(self, "repair_codes", repair_codes)
|
|
249
|
+
|
|
250
|
+
def public(self) -> dict[str, Any]:
|
|
251
|
+
"""Return stable references/codes/digests and no message content."""
|
|
252
|
+
|
|
253
|
+
value: dict[str, Any] = {
|
|
254
|
+
"schema_version": self.schema_version,
|
|
255
|
+
"event_id": self.event_id,
|
|
256
|
+
"idempotency_key": self.idempotency_key,
|
|
257
|
+
"event_kind": self.event_kind,
|
|
258
|
+
"outbound_item_ref": self.outbound_item_ref,
|
|
259
|
+
"recipient_person_id": self.recipient_person_id,
|
|
260
|
+
"scope_revision": self.scope_revision,
|
|
261
|
+
"high_salience": self.high_salience,
|
|
262
|
+
"occurred_at": self.occurred_at,
|
|
263
|
+
"draft_digest": self.draft_digest,
|
|
264
|
+
"evaluated": self.evaluated,
|
|
265
|
+
}
|
|
266
|
+
if self.event_kind == "evaluation":
|
|
267
|
+
value.update({
|
|
268
|
+
"simulation_ref": self.simulation_ref,
|
|
269
|
+
"request_digest": self.request_digest,
|
|
270
|
+
"result_digest": self.result_digest,
|
|
271
|
+
"draft_digest": self.draft_digest,
|
|
272
|
+
"surface": self.surface,
|
|
273
|
+
"risk_class": self.risk_class,
|
|
274
|
+
"would_action": self.would_action,
|
|
275
|
+
"effective_action": self.effective_action,
|
|
276
|
+
"evaluation_path": self.evaluation_path,
|
|
277
|
+
"risk_codes": list(self.risk_codes),
|
|
278
|
+
"repair_codes": list(self.repair_codes),
|
|
279
|
+
})
|
|
280
|
+
return value
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def audit_digest(self) -> str:
|
|
284
|
+
return _digest(self.public())
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _attested_recipient(recipient: ViewerContextV1) -> tuple[str, str]:
|
|
288
|
+
if not isinstance(recipient, ViewerContextV1)\
|
|
289
|
+
or not recipient.attested\
|
|
290
|
+
or not recipient.viewer_person_id\
|
|
291
|
+
or not recipient.scope_revision:
|
|
292
|
+
raise ValueError("recipient audit requires an attested recipient")
|
|
293
|
+
return recipient.viewer_person_id, recipient.scope_revision
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def sample_event(
|
|
297
|
+
*,
|
|
298
|
+
event_id: str,
|
|
299
|
+
idempotency_key: str,
|
|
300
|
+
outbound_item_ref: str,
|
|
301
|
+
recipient: ViewerContextV1,
|
|
302
|
+
high_salience: bool,
|
|
303
|
+
draft_text: str,
|
|
304
|
+
sampled_at: datetime | str,
|
|
305
|
+
) -> RecipientAuditEventV1:
|
|
306
|
+
"""Build a reference-only selection marker before simulation runs."""
|
|
307
|
+
|
|
308
|
+
person, revision = _attested_recipient(recipient)
|
|
309
|
+
return RecipientAuditEventV1(
|
|
310
|
+
event_id=event_id,
|
|
311
|
+
idempotency_key=idempotency_key,
|
|
312
|
+
event_kind="sample",
|
|
313
|
+
outbound_item_ref=outbound_item_ref,
|
|
314
|
+
recipient_person_id=person,
|
|
315
|
+
scope_revision=revision,
|
|
316
|
+
high_salience=high_salience,
|
|
317
|
+
occurred_at=_iso(sampled_at, field="sampled_at"),
|
|
318
|
+
draft_digest=content_digest(draft_text),
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def evaluation_event_from_result(
|
|
323
|
+
*,
|
|
324
|
+
event_id: str,
|
|
325
|
+
idempotency_key: str,
|
|
326
|
+
outbound_item_ref: str,
|
|
327
|
+
request: RecipientSimulationRequestV1,
|
|
328
|
+
result: RecipientSimulationResultV1,
|
|
329
|
+
evaluated_at: datetime | str,
|
|
330
|
+
) -> RecipientAuditEventV1:
|
|
331
|
+
"""Reduce a simulation into digest/code evidence without private refs."""
|
|
332
|
+
|
|
333
|
+
if not isinstance(request, RecipientSimulationRequestV1):
|
|
334
|
+
raise ValueError("request must be RecipientSimulationRequestV1")
|
|
335
|
+
if not isinstance(result, RecipientSimulationResultV1):
|
|
336
|
+
raise ValueError("result must be RecipientSimulationResultV1")
|
|
337
|
+
result_payload = result.public()
|
|
338
|
+
result_payload.pop("audit_digest", None)
|
|
339
|
+
if result.audit_digest != _digest(result_payload):
|
|
340
|
+
raise ValueError("simulation result audit digest is invalid")
|
|
341
|
+
if result.request_digest != request.audit_digest:
|
|
342
|
+
raise ValueError("simulation result request digest does not match request")
|
|
343
|
+
if result.simulation_id != request.simulation_id:
|
|
344
|
+
raise ValueError("simulation result reference does not match request")
|
|
345
|
+
if result.risk_class != request.risk_class:
|
|
346
|
+
raise ValueError("simulation result risk class does not match request")
|
|
347
|
+
if result.external_effect or result.authority_granted\
|
|
348
|
+
or result.synchronous_gate:
|
|
349
|
+
raise ValueError("simulation audit cannot record granted authority/effect")
|
|
350
|
+
if request.surface not in AUDIT_SURFACES:
|
|
351
|
+
raise ValueError("simulation surface is not auditable")
|
|
352
|
+
if result.mode not in {"off", "shadow", "live"}:
|
|
353
|
+
raise ValueError("simulation result mode is not auditable")
|
|
354
|
+
if result.fail_behavior != FAIL_BEHAVIOR_BY_RISK[request.risk_class]:
|
|
355
|
+
raise ValueError("simulation fail behavior does not match risk class")
|
|
356
|
+
if result.would_recommend not in AUDIT_ACTIONS\
|
|
357
|
+
or result.recommended_action not in AUDIT_ACTIONS:
|
|
358
|
+
raise ValueError("simulation action is not auditable")
|
|
359
|
+
if result.evaluation_path not in AUDIT_EVALUATION_PATHS:
|
|
360
|
+
raise ValueError("simulation evaluation path is not auditable")
|
|
361
|
+
unknown_risks = sorted(
|
|
362
|
+
risk.code for risk in result.risks if risk.code not in AUDIT_RISK_CODES)
|
|
363
|
+
if unknown_risks:
|
|
364
|
+
raise ValueError(
|
|
365
|
+
f"simulation risk code is not auditable: {unknown_risks[0]}")
|
|
366
|
+
unknown_repairs = sorted(
|
|
367
|
+
repair.code for repair in result.repairs
|
|
368
|
+
if repair.code not in AUDIT_REPAIR_CODES)
|
|
369
|
+
if unknown_repairs:
|
|
370
|
+
raise ValueError(
|
|
371
|
+
f"simulation repair code is not auditable: {unknown_repairs[0]}")
|
|
372
|
+
severity_rank = {"low": 1, "medium": 2, "high": 3, "critical": 4}
|
|
373
|
+
if any(risk.code == "simulation_dependency_error" for risk in result.risks):
|
|
374
|
+
expected_would = result.fail_behavior
|
|
375
|
+
else:
|
|
376
|
+
rank = max(
|
|
377
|
+
(severity_rank[risk.severity] for risk in result.risks),
|
|
378
|
+
default=0,
|
|
379
|
+
)
|
|
380
|
+
if rank >= 3:
|
|
381
|
+
expected_would = "hold"
|
|
382
|
+
elif rank == 2:
|
|
383
|
+
expected_would = "repair" if result.repairs else "review"
|
|
384
|
+
elif rank == 1:
|
|
385
|
+
expected_would = "observe"
|
|
386
|
+
else:
|
|
387
|
+
expected_would = "send"
|
|
388
|
+
if not result.evaluated:
|
|
389
|
+
expected_would = "no_effect"
|
|
390
|
+
expected_action = "no_effect"
|
|
391
|
+
expected_path = "disabled"
|
|
392
|
+
if result.mode != "off" or result.risks or result.repairs\
|
|
393
|
+
or result.authorized_fact_refs or result.active_arc_refs\
|
|
394
|
+
or result.fact_projection_digest or result.arc_projection_digest:
|
|
395
|
+
raise ValueError("disabled simulation result has impossible state")
|
|
396
|
+
elif result.mode == "off":
|
|
397
|
+
raise ValueError("evaluated simulation result cannot use off mode")
|
|
398
|
+
elif is_realtime_voice_surface(request.surface):
|
|
399
|
+
expected_action = "observe_async"
|
|
400
|
+
expected_path = "async_observation"
|
|
401
|
+
elif result.mode == "shadow":
|
|
402
|
+
expected_action = "observe_only"
|
|
403
|
+
expected_path = "shadow_observation"
|
|
404
|
+
else:
|
|
405
|
+
expected_action = expected_would
|
|
406
|
+
expected_path = "pre_send_advisory"
|
|
407
|
+
if result.would_recommend != expected_would\
|
|
408
|
+
or result.recommended_action != expected_action\
|
|
409
|
+
or result.evaluation_path != expected_path:
|
|
410
|
+
raise ValueError("simulation result state matrix is inconsistent")
|
|
411
|
+
person, revision = _attested_recipient(request.recipient)
|
|
412
|
+
risks = tuple(
|
|
413
|
+
f"{risk.severity}:{risk.code}" for risk in result.risks)
|
|
414
|
+
repairs = tuple(
|
|
415
|
+
f"{repair.priority}:{repair.code}" for repair in result.repairs)
|
|
416
|
+
return RecipientAuditEventV1(
|
|
417
|
+
event_id=event_id,
|
|
418
|
+
idempotency_key=idempotency_key,
|
|
419
|
+
event_kind="evaluation",
|
|
420
|
+
outbound_item_ref=outbound_item_ref,
|
|
421
|
+
recipient_person_id=person,
|
|
422
|
+
scope_revision=revision,
|
|
423
|
+
high_salience=request.high_salience,
|
|
424
|
+
occurred_at=_iso(evaluated_at, field="evaluated_at"),
|
|
425
|
+
simulation_ref=request.simulation_id,
|
|
426
|
+
request_digest=request.audit_digest,
|
|
427
|
+
result_digest=result.audit_digest,
|
|
428
|
+
draft_digest=content_digest(request.draft_text),
|
|
429
|
+
surface=request.surface,
|
|
430
|
+
risk_class=request.risk_class,
|
|
431
|
+
evaluated=result.evaluated,
|
|
432
|
+
would_action=result.would_recommend,
|
|
433
|
+
effective_action=result.recommended_action,
|
|
434
|
+
evaluation_path=result.evaluation_path,
|
|
435
|
+
risk_codes=risks,
|
|
436
|
+
repair_codes=repairs,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@dataclass(frozen=True, slots=True)
|
|
441
|
+
class RecipientAuditAppendV1:
|
|
442
|
+
event: RecipientAuditEventV1
|
|
443
|
+
sequence: int
|
|
444
|
+
appended: bool
|
|
445
|
+
replayed: bool
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@dataclass(frozen=True, slots=True)
|
|
449
|
+
class RecipientAuditProjectionV1:
|
|
450
|
+
events: tuple[RecipientAuditEventV1, ...]
|
|
451
|
+
viewer_attested: bool
|
|
452
|
+
corrupt_count: int
|
|
453
|
+
truncated: bool
|
|
454
|
+
viewer_digest: str
|
|
455
|
+
audit_digest: str
|
|
456
|
+
schema_version: int = SCHEMA_VERSION
|
|
457
|
+
|
|
458
|
+
def public(self) -> dict[str, Any]:
|
|
459
|
+
return {
|
|
460
|
+
"schema_version": self.schema_version,
|
|
461
|
+
"events": [event.public() for event in self.events],
|
|
462
|
+
"viewer_attested": self.viewer_attested,
|
|
463
|
+
"corrupt_count": self.corrupt_count,
|
|
464
|
+
"truncated": self.truncated,
|
|
465
|
+
"viewer_digest": self.viewer_digest,
|
|
466
|
+
"audit_digest": self.audit_digest,
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@dataclass(frozen=True, slots=True)
|
|
471
|
+
class RecipientAuditCoverageV1:
|
|
472
|
+
sampled_high_salience: int
|
|
473
|
+
evaluated_high_salience: int
|
|
474
|
+
unevaluated_high_salience: int
|
|
475
|
+
missing_item_refs: tuple[str, ...]
|
|
476
|
+
missing_refs_truncated: bool
|
|
477
|
+
corrupt_count: int
|
|
478
|
+
scan_truncated: bool
|
|
479
|
+
status: str
|
|
480
|
+
coverage_complete: bool
|
|
481
|
+
viewer_digest: str
|
|
482
|
+
audit_digest: str
|
|
483
|
+
schema_version: int = SCHEMA_VERSION
|
|
484
|
+
|
|
485
|
+
def public(self) -> dict[str, Any]:
|
|
486
|
+
return {
|
|
487
|
+
"schema_version": self.schema_version,
|
|
488
|
+
"sampled_high_salience": self.sampled_high_salience,
|
|
489
|
+
"evaluated_high_salience": self.evaluated_high_salience,
|
|
490
|
+
"unevaluated_high_salience": self.unevaluated_high_salience,
|
|
491
|
+
"missing_item_refs": list(self.missing_item_refs),
|
|
492
|
+
"missing_refs_truncated": self.missing_refs_truncated,
|
|
493
|
+
"corrupt_count": self.corrupt_count,
|
|
494
|
+
"scan_truncated": self.scan_truncated,
|
|
495
|
+
"status": self.status,
|
|
496
|
+
"coverage_complete": self.coverage_complete,
|
|
497
|
+
"viewer_digest": self.viewer_digest,
|
|
498
|
+
"audit_digest": self.audit_digest,
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class RecipientSimulationAuditStore:
|
|
503
|
+
"""Append-only recipient-simulation evidence and coverage ledger."""
|
|
504
|
+
|
|
505
|
+
def __init__(self, db_path: str | os.PathLike[str] = ":memory:") -> None:
|
|
506
|
+
self.path = str(db_path)
|
|
507
|
+
if self.path != ":memory:":
|
|
508
|
+
path = Path(self.path)
|
|
509
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
510
|
+
if not path.exists():
|
|
511
|
+
path.touch(mode=0o600)
|
|
512
|
+
else:
|
|
513
|
+
os.chmod(path, 0o600)
|
|
514
|
+
self._lock = threading.RLock()
|
|
515
|
+
self._conn = sqlite3.connect(
|
|
516
|
+
self.path, check_same_thread=False, timeout=30.0)
|
|
517
|
+
self._conn.row_factory = sqlite3.Row
|
|
518
|
+
self._conn.execute("PRAGMA busy_timeout=30000")
|
|
519
|
+
self._conn.execute("PRAGMA recursive_triggers=ON")
|
|
520
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
521
|
+
self._conn.execute("PRAGMA synchronous=FULL")
|
|
522
|
+
self._conn.executescript(
|
|
523
|
+
"""
|
|
524
|
+
CREATE TABLE IF NOT EXISTS recipient_simulation_audit (
|
|
525
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
526
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
527
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
528
|
+
event_kind TEXT NOT NULL CHECK (
|
|
529
|
+
event_kind IN ('sample','evaluation')),
|
|
530
|
+
outbound_item_ref TEXT NOT NULL,
|
|
531
|
+
recipient_person_id TEXT NOT NULL,
|
|
532
|
+
scope_revision TEXT NOT NULL,
|
|
533
|
+
high_salience INTEGER NOT NULL CHECK (high_salience IN (0,1)),
|
|
534
|
+
occurred_at TEXT NOT NULL,
|
|
535
|
+
simulation_ref TEXT NOT NULL,
|
|
536
|
+
request_digest TEXT NOT NULL,
|
|
537
|
+
result_digest TEXT NOT NULL,
|
|
538
|
+
draft_digest TEXT NOT NULL,
|
|
539
|
+
surface TEXT NOT NULL,
|
|
540
|
+
risk_class TEXT NOT NULL,
|
|
541
|
+
evaluated INTEGER NOT NULL CHECK (evaluated IN (0,1)),
|
|
542
|
+
would_action TEXT NOT NULL,
|
|
543
|
+
effective_action TEXT NOT NULL,
|
|
544
|
+
evaluation_path TEXT NOT NULL,
|
|
545
|
+
risk_codes_json TEXT NOT NULL,
|
|
546
|
+
repair_codes_json TEXT NOT NULL,
|
|
547
|
+
event_digest TEXT NOT NULL UNIQUE,
|
|
548
|
+
payload_json TEXT NOT NULL,
|
|
549
|
+
stored_at TEXT NOT NULL,
|
|
550
|
+
UNIQUE(event_kind,outbound_item_ref,simulation_ref)
|
|
551
|
+
);
|
|
552
|
+
CREATE INDEX IF NOT EXISTS idx_recipient_audit_viewer_time
|
|
553
|
+
ON recipient_simulation_audit(
|
|
554
|
+
recipient_person_id,scope_revision,occurred_at,seq);
|
|
555
|
+
CREATE INDEX IF NOT EXISTS idx_recipient_audit_coverage
|
|
556
|
+
ON recipient_simulation_audit(
|
|
557
|
+
event_kind,high_salience,recipient_person_id,outbound_item_ref);
|
|
558
|
+
CREATE INDEX IF NOT EXISTS idx_recipient_audit_outbound
|
|
559
|
+
ON recipient_simulation_audit(outbound_item_ref,event_kind);
|
|
560
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_recipient_audit_simulation_once
|
|
561
|
+
ON recipient_simulation_audit(simulation_ref)
|
|
562
|
+
WHERE event_kind='evaluation';
|
|
563
|
+
CREATE TRIGGER IF NOT EXISTS recipient_audit_no_update
|
|
564
|
+
BEFORE UPDATE ON recipient_simulation_audit BEGIN
|
|
565
|
+
SELECT RAISE(ABORT, 'recipient audit is append-only');
|
|
566
|
+
END;
|
|
567
|
+
CREATE TRIGGER IF NOT EXISTS recipient_audit_no_delete
|
|
568
|
+
BEFORE DELETE ON recipient_simulation_audit BEGIN
|
|
569
|
+
SELECT RAISE(ABORT, 'recipient audit is append-only');
|
|
570
|
+
END;
|
|
571
|
+
CREATE TRIGGER IF NOT EXISTS recipient_audit_no_replace
|
|
572
|
+
BEFORE INSERT ON recipient_simulation_audit
|
|
573
|
+
WHEN EXISTS (
|
|
574
|
+
SELECT 1 FROM recipient_simulation_audit
|
|
575
|
+
WHERE seq=NEW.seq
|
|
576
|
+
OR event_id=NEW.event_id
|
|
577
|
+
OR idempotency_key=NEW.idempotency_key
|
|
578
|
+
OR event_digest=NEW.event_digest
|
|
579
|
+
OR (
|
|
580
|
+
event_kind=NEW.event_kind
|
|
581
|
+
AND outbound_item_ref=NEW.outbound_item_ref
|
|
582
|
+
AND simulation_ref=NEW.simulation_ref
|
|
583
|
+
)
|
|
584
|
+
OR (
|
|
585
|
+
NEW.event_kind='evaluation'
|
|
586
|
+
AND event_kind='evaluation'
|
|
587
|
+
AND simulation_ref=NEW.simulation_ref
|
|
588
|
+
)
|
|
589
|
+
) BEGIN
|
|
590
|
+
SELECT RAISE(ABORT, 'recipient audit is append-only');
|
|
591
|
+
END;
|
|
592
|
+
"""
|
|
593
|
+
)
|
|
594
|
+
self._conn.commit()
|
|
595
|
+
if self.path != ":memory:":
|
|
596
|
+
os.chmod(self.path, 0o600)
|
|
597
|
+
|
|
598
|
+
@staticmethod
|
|
599
|
+
def _decode(row: sqlite3.Row) -> RecipientAuditEventV1:
|
|
600
|
+
try:
|
|
601
|
+
payload = json.loads(row["payload_json"])
|
|
602
|
+
event = RecipientAuditEventV1(**payload)
|
|
603
|
+
risk_codes = json.loads(row["risk_codes_json"])
|
|
604
|
+
repair_codes = json.loads(row["repair_codes_json"])
|
|
605
|
+
except (TypeError, ValueError) as exc:
|
|
606
|
+
raise ValueError("recipient audit payload is unreadable") from exc
|
|
607
|
+
indexed = {
|
|
608
|
+
"event_id": str(row["event_id"]),
|
|
609
|
+
"idempotency_key": str(row["idempotency_key"]),
|
|
610
|
+
"event_kind": str(row["event_kind"]),
|
|
611
|
+
"outbound_item_ref": str(row["outbound_item_ref"]),
|
|
612
|
+
"recipient_person_id": str(row["recipient_person_id"]),
|
|
613
|
+
"scope_revision": str(row["scope_revision"]),
|
|
614
|
+
"high_salience": bool(row["high_salience"]),
|
|
615
|
+
"occurred_at": str(row["occurred_at"]),
|
|
616
|
+
"simulation_ref": str(row["simulation_ref"]),
|
|
617
|
+
"request_digest": str(row["request_digest"]),
|
|
618
|
+
"result_digest": str(row["result_digest"]),
|
|
619
|
+
"draft_digest": str(row["draft_digest"]),
|
|
620
|
+
"surface": str(row["surface"]),
|
|
621
|
+
"risk_class": str(row["risk_class"]),
|
|
622
|
+
"evaluated": bool(row["evaluated"]),
|
|
623
|
+
"would_action": str(row["would_action"]),
|
|
624
|
+
"effective_action": str(row["effective_action"]),
|
|
625
|
+
"evaluation_path": str(row["evaluation_path"]),
|
|
626
|
+
"risk_codes": tuple(risk_codes),
|
|
627
|
+
"repair_codes": tuple(repair_codes),
|
|
628
|
+
"event_digest": str(row["event_digest"]),
|
|
629
|
+
}
|
|
630
|
+
expected = {
|
|
631
|
+
"event_id": event.event_id,
|
|
632
|
+
"idempotency_key": event.idempotency_key,
|
|
633
|
+
"event_kind": event.event_kind,
|
|
634
|
+
"outbound_item_ref": event.outbound_item_ref,
|
|
635
|
+
"recipient_person_id": event.recipient_person_id,
|
|
636
|
+
"scope_revision": event.scope_revision,
|
|
637
|
+
"high_salience": event.high_salience,
|
|
638
|
+
"occurred_at": event.occurred_at,
|
|
639
|
+
"simulation_ref": event.simulation_ref,
|
|
640
|
+
"request_digest": event.request_digest,
|
|
641
|
+
"result_digest": event.result_digest,
|
|
642
|
+
"draft_digest": event.draft_digest,
|
|
643
|
+
"surface": event.surface,
|
|
644
|
+
"risk_class": event.risk_class,
|
|
645
|
+
"evaluated": event.evaluated,
|
|
646
|
+
"would_action": event.would_action,
|
|
647
|
+
"effective_action": event.effective_action,
|
|
648
|
+
"evaluation_path": event.evaluation_path,
|
|
649
|
+
"risk_codes": event.risk_codes,
|
|
650
|
+
"repair_codes": event.repair_codes,
|
|
651
|
+
"event_digest": event.audit_digest,
|
|
652
|
+
}
|
|
653
|
+
if indexed != expected:
|
|
654
|
+
raise ValueError("recipient audit digest/index mismatch")
|
|
655
|
+
return event
|
|
656
|
+
|
|
657
|
+
@staticmethod
|
|
658
|
+
def _same_outbound_authority(
|
|
659
|
+
first: RecipientAuditEventV1,
|
|
660
|
+
second: RecipientAuditEventV1,
|
|
661
|
+
) -> bool:
|
|
662
|
+
return (
|
|
663
|
+
first.outbound_item_ref == second.outbound_item_ref
|
|
664
|
+
and first.recipient_person_id == second.recipient_person_id
|
|
665
|
+
and first.scope_revision == second.scope_revision
|
|
666
|
+
and first.high_salience == second.high_salience
|
|
667
|
+
and first.draft_digest == second.draft_digest
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
def append(self, event: RecipientAuditEventV1) -> RecipientAuditAppendV1:
|
|
671
|
+
if not isinstance(event, RecipientAuditEventV1):
|
|
672
|
+
raise ValueError("audit store accepts RecipientAuditEventV1 only")
|
|
673
|
+
with self._lock:
|
|
674
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
675
|
+
try:
|
|
676
|
+
rows = self._conn.execute(
|
|
677
|
+
"SELECT * FROM recipient_simulation_audit "
|
|
678
|
+
"WHERE event_id=? OR idempotency_key=? "
|
|
679
|
+
"OR (event_kind=? AND outbound_item_ref=? "
|
|
680
|
+
"AND simulation_ref=?) "
|
|
681
|
+
"OR (event_kind='evaluation' AND ?='evaluation' "
|
|
682
|
+
"AND simulation_ref=?) ORDER BY seq",
|
|
683
|
+
(
|
|
684
|
+
event.event_id,
|
|
685
|
+
event.idempotency_key,
|
|
686
|
+
event.event_kind,
|
|
687
|
+
event.outbound_item_ref,
|
|
688
|
+
event.simulation_ref,
|
|
689
|
+
event.event_kind,
|
|
690
|
+
event.simulation_ref,
|
|
691
|
+
),
|
|
692
|
+
).fetchall()
|
|
693
|
+
if rows:
|
|
694
|
+
for row in rows:
|
|
695
|
+
saved = self._decode(row)
|
|
696
|
+
if (
|
|
697
|
+
saved.event_id == event.event_id
|
|
698
|
+
and saved.idempotency_key == event.idempotency_key
|
|
699
|
+
and saved.event_kind == event.event_kind
|
|
700
|
+
and saved.outbound_item_ref == event.outbound_item_ref
|
|
701
|
+
and saved.audit_digest == event.audit_digest
|
|
702
|
+
):
|
|
703
|
+
self._conn.commit()
|
|
704
|
+
return RecipientAuditAppendV1(
|
|
705
|
+
event=saved,
|
|
706
|
+
sequence=int(row["seq"]),
|
|
707
|
+
appended=False,
|
|
708
|
+
replayed=True,
|
|
709
|
+
)
|
|
710
|
+
raise RecipientAuditConflictError(
|
|
711
|
+
"recipient audit identity changed immutable content")
|
|
712
|
+
|
|
713
|
+
counterpart_kind = (
|
|
714
|
+
"evaluation" if event.event_kind == "sample" else "sample")
|
|
715
|
+
counterpart_rows = self._conn.execute(
|
|
716
|
+
"SELECT * FROM recipient_simulation_audit "
|
|
717
|
+
"WHERE event_kind=? AND outbound_item_ref=? ORDER BY seq",
|
|
718
|
+
(counterpart_kind, event.outbound_item_ref),
|
|
719
|
+
).fetchall()
|
|
720
|
+
if event.event_kind == "evaluation" and not counterpart_rows:
|
|
721
|
+
raise RecipientAuditConflictError(
|
|
722
|
+
"evaluation requires an immutable sample first")
|
|
723
|
+
for counterpart_row in counterpart_rows:
|
|
724
|
+
counterpart = self._decode(counterpart_row)
|
|
725
|
+
if not self._same_outbound_authority(event, counterpart):
|
|
726
|
+
raise RecipientAuditConflictError(
|
|
727
|
+
"evaluation does not match its immutable sample authority")
|
|
728
|
+
sample = event if event.event_kind == "sample" else counterpart
|
|
729
|
+
evaluation = (
|
|
730
|
+
event if event.event_kind == "evaluation" else counterpart)
|
|
731
|
+
if evaluation.occurred_at < sample.occurred_at:
|
|
732
|
+
raise RecipientAuditConflictError(
|
|
733
|
+
"evaluation predates its immutable sample")
|
|
734
|
+
|
|
735
|
+
cursor = self._conn.execute(
|
|
736
|
+
"INSERT INTO recipient_simulation_audit "
|
|
737
|
+
"(event_id,idempotency_key,event_kind,outbound_item_ref,"
|
|
738
|
+
"recipient_person_id,scope_revision,high_salience,occurred_at,"
|
|
739
|
+
"simulation_ref,request_digest,result_digest,draft_digest,"
|
|
740
|
+
"surface,risk_class,evaluated,would_action,effective_action,"
|
|
741
|
+
"evaluation_path,risk_codes_json,repair_codes_json,"
|
|
742
|
+
"event_digest,payload_json,stored_at) "
|
|
743
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
744
|
+
(
|
|
745
|
+
event.event_id,
|
|
746
|
+
event.idempotency_key,
|
|
747
|
+
event.event_kind,
|
|
748
|
+
event.outbound_item_ref,
|
|
749
|
+
event.recipient_person_id,
|
|
750
|
+
event.scope_revision,
|
|
751
|
+
int(event.high_salience),
|
|
752
|
+
event.occurred_at,
|
|
753
|
+
event.simulation_ref,
|
|
754
|
+
event.request_digest,
|
|
755
|
+
event.result_digest,
|
|
756
|
+
event.draft_digest,
|
|
757
|
+
event.surface,
|
|
758
|
+
event.risk_class,
|
|
759
|
+
int(event.evaluated),
|
|
760
|
+
event.would_action,
|
|
761
|
+
event.effective_action,
|
|
762
|
+
event.evaluation_path,
|
|
763
|
+
_canonical(event.risk_codes),
|
|
764
|
+
_canonical(event.repair_codes),
|
|
765
|
+
event.audit_digest,
|
|
766
|
+
_canonical(event.public()),
|
|
767
|
+
datetime.now(timezone.utc).isoformat(),
|
|
768
|
+
),
|
|
769
|
+
)
|
|
770
|
+
self._conn.commit()
|
|
771
|
+
return RecipientAuditAppendV1(
|
|
772
|
+
event=event,
|
|
773
|
+
sequence=int(cursor.lastrowid),
|
|
774
|
+
appended=True,
|
|
775
|
+
replayed=False,
|
|
776
|
+
)
|
|
777
|
+
except Exception:
|
|
778
|
+
self._conn.rollback()
|
|
779
|
+
raise
|
|
780
|
+
|
|
781
|
+
@staticmethod
|
|
782
|
+
def _authorized_where(
|
|
783
|
+
viewer: ViewerContextV1,
|
|
784
|
+
*,
|
|
785
|
+
alias: str = "",
|
|
786
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
787
|
+
if viewer.viewer_person_id == viewer.owner_person_id:
|
|
788
|
+
return "", ()
|
|
789
|
+
prefix = f"{alias}." if alias else ""
|
|
790
|
+
return (
|
|
791
|
+
f" AND {prefix}recipient_person_id=? "
|
|
792
|
+
f"AND {prefix}scope_revision=?",
|
|
793
|
+
(viewer.viewer_person_id, viewer.scope_revision),
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
def project(
|
|
797
|
+
self,
|
|
798
|
+
viewer: ViewerContextV1,
|
|
799
|
+
*,
|
|
800
|
+
max_events: int = 64,
|
|
801
|
+
) -> RecipientAuditProjectionV1:
|
|
802
|
+
if not isinstance(viewer, ViewerContextV1):
|
|
803
|
+
raise ValueError("viewer must be ViewerContextV1")
|
|
804
|
+
if not 1 <= int(max_events) <= MAX_PROJECTED_EVENTS:
|
|
805
|
+
raise ValueError("max_events is outside bounded projection range")
|
|
806
|
+
if not viewer.attested or not viewer.viewer_person_id:
|
|
807
|
+
payload = {
|
|
808
|
+
"schema_version": SCHEMA_VERSION,
|
|
809
|
+
"viewer_digest": viewer.audit_digest,
|
|
810
|
+
"viewer_attested": False,
|
|
811
|
+
"event_digests": (),
|
|
812
|
+
"corrupt_count": 0,
|
|
813
|
+
"truncated": False,
|
|
814
|
+
"max_events": int(max_events),
|
|
815
|
+
}
|
|
816
|
+
return RecipientAuditProjectionV1(
|
|
817
|
+
events=(),
|
|
818
|
+
viewer_attested=False,
|
|
819
|
+
corrupt_count=0,
|
|
820
|
+
truncated=False,
|
|
821
|
+
viewer_digest=viewer.audit_digest,
|
|
822
|
+
audit_digest=_digest(payload),
|
|
823
|
+
)
|
|
824
|
+
where, params = self._authorized_where(viewer)
|
|
825
|
+
with self._lock:
|
|
826
|
+
rows = self._conn.execute(
|
|
827
|
+
"SELECT * FROM recipient_simulation_audit WHERE 1=1"
|
|
828
|
+
+ where + " ORDER BY seq DESC LIMIT ?",
|
|
829
|
+
(*params, int(max_events) + 1),
|
|
830
|
+
).fetchall()
|
|
831
|
+
truncated = len(rows) > int(max_events)
|
|
832
|
+
events: list[RecipientAuditEventV1] = []
|
|
833
|
+
corrupt_count = 0
|
|
834
|
+
for row in rows[:int(max_events)]:
|
|
835
|
+
try:
|
|
836
|
+
events.append(self._decode(row))
|
|
837
|
+
except Exception:
|
|
838
|
+
corrupt_count += 1
|
|
839
|
+
payload = {
|
|
840
|
+
"schema_version": SCHEMA_VERSION,
|
|
841
|
+
"viewer_digest": viewer.audit_digest,
|
|
842
|
+
"viewer_attested": True,
|
|
843
|
+
"event_digests": tuple(event.audit_digest for event in events),
|
|
844
|
+
"corrupt_count": corrupt_count,
|
|
845
|
+
"truncated": truncated,
|
|
846
|
+
"max_events": int(max_events),
|
|
847
|
+
}
|
|
848
|
+
return RecipientAuditProjectionV1(
|
|
849
|
+
events=tuple(events),
|
|
850
|
+
viewer_attested=True,
|
|
851
|
+
corrupt_count=corrupt_count,
|
|
852
|
+
truncated=truncated,
|
|
853
|
+
viewer_digest=viewer.audit_digest,
|
|
854
|
+
audit_digest=_digest(payload),
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
def coverage(
|
|
858
|
+
self,
|
|
859
|
+
viewer: ViewerContextV1,
|
|
860
|
+
*,
|
|
861
|
+
max_missing_refs: int = 32,
|
|
862
|
+
) -> RecipientAuditCoverageV1:
|
|
863
|
+
"""Account for all authorized sampled high-salience outbound items."""
|
|
864
|
+
|
|
865
|
+
if not isinstance(viewer, ViewerContextV1):
|
|
866
|
+
raise ValueError("viewer must be ViewerContextV1")
|
|
867
|
+
if not 1 <= int(max_missing_refs) <= MAX_MISSING_REFS:
|
|
868
|
+
raise ValueError("max_missing_refs is outside bounded coverage range")
|
|
869
|
+
if not viewer.attested or not viewer.viewer_person_id:
|
|
870
|
+
payload = {
|
|
871
|
+
"schema_version": SCHEMA_VERSION,
|
|
872
|
+
"viewer_digest": viewer.audit_digest,
|
|
873
|
+
"status": "viewer_unattested",
|
|
874
|
+
"sampled": 0,
|
|
875
|
+
"evaluated": 0,
|
|
876
|
+
"missing": (),
|
|
877
|
+
"missing_refs_truncated": False,
|
|
878
|
+
}
|
|
879
|
+
return RecipientAuditCoverageV1(
|
|
880
|
+
sampled_high_salience=0,
|
|
881
|
+
evaluated_high_salience=0,
|
|
882
|
+
unevaluated_high_salience=0,
|
|
883
|
+
missing_item_refs=(),
|
|
884
|
+
missing_refs_truncated=False,
|
|
885
|
+
corrupt_count=0,
|
|
886
|
+
scan_truncated=False,
|
|
887
|
+
status="viewer_unattested",
|
|
888
|
+
coverage_complete=False,
|
|
889
|
+
viewer_digest=viewer.audit_digest,
|
|
890
|
+
audit_digest=_digest(payload),
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
where, params = self._authorized_where(viewer)
|
|
894
|
+
samples: dict[str, RecipientAuditEventV1] = {}
|
|
895
|
+
evaluated_items: set[str] = set()
|
|
896
|
+
corrupt_count = 0
|
|
897
|
+
with self._lock:
|
|
898
|
+
cursor = self._conn.execute(
|
|
899
|
+
"SELECT * FROM recipient_simulation_audit "
|
|
900
|
+
"WHERE event_kind IN ('sample','evaluation')" + where
|
|
901
|
+
+ " ORDER BY seq",
|
|
902
|
+
params,
|
|
903
|
+
)
|
|
904
|
+
while True:
|
|
905
|
+
rows = cursor.fetchmany(COVERAGE_FETCH_SIZE)
|
|
906
|
+
if not rows:
|
|
907
|
+
break
|
|
908
|
+
for row in rows:
|
|
909
|
+
try:
|
|
910
|
+
event = self._decode(row)
|
|
911
|
+
except Exception:
|
|
912
|
+
corrupt_count += 1
|
|
913
|
+
continue
|
|
914
|
+
if event.event_kind == "sample" and event.high_salience:
|
|
915
|
+
samples[event.outbound_item_ref] = event
|
|
916
|
+
elif event.event_kind == "evaluation" and event.evaluated:
|
|
917
|
+
evaluated_items.add(event.outbound_item_ref)
|
|
918
|
+
scan_truncated = False
|
|
919
|
+
sampled = len(samples)
|
|
920
|
+
evaluated = sum(
|
|
921
|
+
outbound in evaluated_items for outbound in samples)
|
|
922
|
+
unevaluated = sampled - evaluated
|
|
923
|
+
all_missing = tuple(sorted(
|
|
924
|
+
outbound for outbound in samples if outbound not in evaluated_items))
|
|
925
|
+
missing_truncated = len(all_missing) > int(max_missing_refs)
|
|
926
|
+
missing = all_missing[:int(max_missing_refs)]
|
|
927
|
+
if corrupt_count:
|
|
928
|
+
status = "indeterminate"
|
|
929
|
+
complete = False
|
|
930
|
+
elif sampled == 0:
|
|
931
|
+
status = "no_samples"
|
|
932
|
+
complete = False
|
|
933
|
+
elif unevaluated:
|
|
934
|
+
status = "incomplete"
|
|
935
|
+
complete = False
|
|
936
|
+
else:
|
|
937
|
+
status = "complete"
|
|
938
|
+
complete = True
|
|
939
|
+
payload = {
|
|
940
|
+
"schema_version": SCHEMA_VERSION,
|
|
941
|
+
"viewer_digest": viewer.audit_digest,
|
|
942
|
+
"status": status,
|
|
943
|
+
"sampled": sampled,
|
|
944
|
+
"evaluated": evaluated,
|
|
945
|
+
"missing": missing,
|
|
946
|
+
"missing_refs_truncated": missing_truncated,
|
|
947
|
+
"corrupt_count": corrupt_count,
|
|
948
|
+
"scan_truncated": scan_truncated,
|
|
949
|
+
}
|
|
950
|
+
return RecipientAuditCoverageV1(
|
|
951
|
+
sampled_high_salience=sampled,
|
|
952
|
+
evaluated_high_salience=evaluated,
|
|
953
|
+
unevaluated_high_salience=unevaluated,
|
|
954
|
+
missing_item_refs=missing,
|
|
955
|
+
missing_refs_truncated=missing_truncated,
|
|
956
|
+
corrupt_count=corrupt_count,
|
|
957
|
+
scan_truncated=scan_truncated,
|
|
958
|
+
status=status,
|
|
959
|
+
coverage_complete=complete,
|
|
960
|
+
viewer_digest=viewer.audit_digest,
|
|
961
|
+
audit_digest=_digest(payload),
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
def close(self) -> None:
|
|
965
|
+
with self._lock:
|
|
966
|
+
try:
|
|
967
|
+
self._conn.close()
|
|
968
|
+
except Exception:
|
|
969
|
+
pass
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def open_recipient_simulation_audit_store(
|
|
973
|
+
db_path: str | os.PathLike[str],
|
|
974
|
+
*,
|
|
975
|
+
mode: Optional[str] = None,
|
|
976
|
+
) -> Optional[RecipientSimulationAuditStore]:
|
|
977
|
+
"""Open only for explicit shadow/live mode; off/unknown creates no state."""
|
|
978
|
+
|
|
979
|
+
selected = recipient_simulator_mode() if mode is None else str(mode).strip().lower()
|
|
980
|
+
if selected not in {"shadow", "live"}:
|
|
981
|
+
return None
|
|
982
|
+
return RecipientSimulationAuditStore(db_path)
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
__all__ = [
|
|
986
|
+
"RecipientAuditAppendV1",
|
|
987
|
+
"RecipientAuditConflictError",
|
|
988
|
+
"RecipientAuditCoverageV1",
|
|
989
|
+
"RecipientAuditEventV1",
|
|
990
|
+
"RecipientAuditProjectionV1",
|
|
991
|
+
"RecipientSimulationAuditStore",
|
|
992
|
+
"evaluation_event_from_result",
|
|
993
|
+
"open_recipient_simulation_audit_store",
|
|
994
|
+
"sample_event",
|
|
995
|
+
]
|