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,932 @@
|
|
|
1
|
+
"""Strict Phase C intake for external text and system cognition evidence.
|
|
2
|
+
|
|
3
|
+
External producers may report bounded observations. They cannot assert person,
|
|
4
|
+
audience, boundary, or transport authority, and this module never accepts a
|
|
5
|
+
realtime audio/voice surface. The server-attested envelope is stored before it
|
|
6
|
+
is projected into Colony's existing durable host event journal.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import asdict, dataclass
|
|
12
|
+
from datetime import datetime, timedelta, timezone
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import sqlite3
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
from typing import Any, Callable, Dict, Mapping, Optional
|
|
22
|
+
|
|
23
|
+
from apsimo.scope_bounds import (
|
|
24
|
+
SUBJECT_PERSON_ID_MAX_CHARS,
|
|
25
|
+
VIEWER_SCOPE_MAX_CHARS,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_KINDS = frozenset({
|
|
30
|
+
"action_outcome", "delivery_outcome", "service_state", "approval_state",
|
|
31
|
+
"operator_reaction", "text_turn_observation",
|
|
32
|
+
})
|
|
33
|
+
EXTERNAL_JOURNAL_EVENT_ID_MAX_CHARS = 128
|
|
34
|
+
_SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/+\-]{7,191}$")
|
|
35
|
+
_SAFE_REF = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/+\-]{0,191}$")
|
|
36
|
+
_SAFE_JOURNAL_EVENT_ID = re.compile(
|
|
37
|
+
rf"^[A-Za-z0-9][A-Za-z0-9_.:@/+\-]"
|
|
38
|
+
rf"{{0,{EXTERNAL_JOURNAL_EVENT_ID_MAX_CHARS - 1}}}$"
|
|
39
|
+
)
|
|
40
|
+
_SAFE_ATTRIBUTE = re.compile(r"^[a-z][a-z0-9_.-]{0,63}$")
|
|
41
|
+
_SECRET_KEY = re.compile(
|
|
42
|
+
r"(?i)(?:^|[_.-])(api[_-]?key|authorization|bearer|cookie|credential|"
|
|
43
|
+
r"password|private[_-]?key|secret|session|token)(?:$|[_.-])"
|
|
44
|
+
)
|
|
45
|
+
_SECRET_VALUE = re.compile(
|
|
46
|
+
r"(?ix)(?:"
|
|
47
|
+
r"\bbearer\s+[A-Za-z0-9._~+/=-]{8,}|"
|
|
48
|
+
r"\bsk-[A-Za-z0-9_-]{8,}|"
|
|
49
|
+
r"\b(?:api[\s_-]?(?:key|token)|authorization|cookie|credential|password|"
|
|
50
|
+
r"private[\s_-]?key|secret|session|token)\s*[:=]\s*"
|
|
51
|
+
r"(?:\"[^\"\r\n]{8,}\"|'[^'\r\n]{8,}'|[A-Za-z0-9._~+/=-]{8,})"
|
|
52
|
+
r")"
|
|
53
|
+
)
|
|
54
|
+
_RESERVED_ATTRIBUTES = frozenset({
|
|
55
|
+
"approval", "audience", "audiences", "authority", "boundary_attested",
|
|
56
|
+
"credential_id", "evidence_status", "grant", "outcome_contract",
|
|
57
|
+
"person_id", "principal_id", "producer_revision", "receipt_ref", "scope",
|
|
58
|
+
"scope_digest", "shareability", "subject_person_id", "verified",
|
|
59
|
+
"viewer_person_id", "viewer_scope",
|
|
60
|
+
})
|
|
61
|
+
_AUTHORITY_ATTRIBUTE_ALIAS = re.compile(
|
|
62
|
+
r"(?:^|[_.-])(?:approved?|approval|authority|authorized|evidence[_-]?status|"
|
|
63
|
+
r"granted?|outcome[_-]?contract|receipt[_-]?ref|verification|verified)"
|
|
64
|
+
r"(?:$|[_.-])"
|
|
65
|
+
)
|
|
66
|
+
_KIND_FIELDS = {
|
|
67
|
+
"action_outcome": {
|
|
68
|
+
"required": {"action_id", "outcome"},
|
|
69
|
+
"allowed": {
|
|
70
|
+
"action_id", "outcome", "action_digest", "error_code",
|
|
71
|
+
"duration_ms",
|
|
72
|
+
},
|
|
73
|
+
"enum": {
|
|
74
|
+
"outcome": {
|
|
75
|
+
"succeeded", "failed", "blocked", "cancelled", "partial",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
"delivery_outcome": {
|
|
80
|
+
"required": {"outcome"},
|
|
81
|
+
"allowed": {
|
|
82
|
+
"delivery_ref", "message_ref", "outcome", "channel", "error_code",
|
|
83
|
+
},
|
|
84
|
+
"enum": {
|
|
85
|
+
"outcome": {
|
|
86
|
+
"delivered", "failed", "bounced", "read", "acknowledged",
|
|
87
|
+
},
|
|
88
|
+
"channel": {
|
|
89
|
+
"text", "sms", "rcs", "whatsapp", "chat", "email",
|
|
90
|
+
"webhook", "system",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
"service_state": {
|
|
95
|
+
"required": {"service", "state"},
|
|
96
|
+
"allowed": {
|
|
97
|
+
"service", "state", "detail", "latency_ms", "observed_samples",
|
|
98
|
+
},
|
|
99
|
+
"enum": {
|
|
100
|
+
"state": {"healthy", "degraded", "offline", "recovering"},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
"approval_state": {
|
|
104
|
+
"required": {"request_id", "state"},
|
|
105
|
+
"allowed": {"request_id", "state", "decision_ref", "reason_code"},
|
|
106
|
+
"enum": {
|
|
107
|
+
"state": {"pending", "approved", "rejected", "expired", "cancelled"},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
"operator_reaction": {
|
|
111
|
+
"required": {"target_ref", "reaction"},
|
|
112
|
+
"allowed": {"target_ref", "reaction", "intensity", "signal"},
|
|
113
|
+
"enum": {
|
|
114
|
+
"reaction": {
|
|
115
|
+
"positive", "negative", "neutral", "correction", "dismissed",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
"text_turn_observation": {
|
|
120
|
+
"required": {"turn_id", "channel", "observation"},
|
|
121
|
+
"allowed": {"turn_id", "channel", "observation"},
|
|
122
|
+
"enum": {
|
|
123
|
+
"channel": {"text", "sms", "rcs", "whatsapp", "chat", "email"},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_JOURNAL_PROJECTION_FIELDS = frozenset({
|
|
129
|
+
"schema", "version", "external_event_id", "external_event_digest",
|
|
130
|
+
"external_occurred_at", "kind", "summary", "attributes", "producer_principal_id",
|
|
131
|
+
"producer_revision", "subject_person_id", "viewer_person_id",
|
|
132
|
+
"viewer_scope", "shareability", "audience_scope", "scope_digest",
|
|
133
|
+
"boundary_attested", "evidence_status",
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ExternalEventValidationError(ValueError):
|
|
138
|
+
"""The external event is outside the strict Phase C evidence schema."""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ExternalEventConflict(ValueError):
|
|
142
|
+
"""An event ID was replayed with changed immutable content."""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ExternalEventProjectionError(RuntimeError):
|
|
146
|
+
"""The receipt is durable but journal projection is currently unavailable."""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def validate_external_journal_event_id(value: Any) -> str:
|
|
150
|
+
"""Return one exact, bounded host-journal identity for a V2 projection."""
|
|
151
|
+
|
|
152
|
+
if type(value) is not str or not _SAFE_JOURNAL_EVENT_ID.fullmatch(value):
|
|
153
|
+
raise ExternalEventValidationError(
|
|
154
|
+
"external cognition journal ID is not canonical"
|
|
155
|
+
)
|
|
156
|
+
return value
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _exact_projection_text(
|
|
160
|
+
value: Any,
|
|
161
|
+
*,
|
|
162
|
+
field: str,
|
|
163
|
+
maximum: int,
|
|
164
|
+
) -> str:
|
|
165
|
+
"""Accept only the exact JSON string form emitted by the server."""
|
|
166
|
+
|
|
167
|
+
if type(value) is not str:
|
|
168
|
+
raise ExternalEventValidationError(
|
|
169
|
+
f"external cognition journal {field} must be an exact string"
|
|
170
|
+
)
|
|
171
|
+
if not value or value != value.strip() or len(value) > maximum:
|
|
172
|
+
raise ExternalEventValidationError(
|
|
173
|
+
f"external cognition journal {field} is not canonical"
|
|
174
|
+
)
|
|
175
|
+
return value
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def validate_external_journal_projection(
|
|
179
|
+
event_type: Any,
|
|
180
|
+
value: Any,
|
|
181
|
+
) -> Dict[str, Any]:
|
|
182
|
+
"""Validate one server-produced host-journal projection exactly.
|
|
183
|
+
|
|
184
|
+
This is the shared trust boundary for downstream durable consumers. It
|
|
185
|
+
deliberately revalidates the typed attributes and recomputes the scope
|
|
186
|
+
digest instead of trusting fields copied from the journal payload.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
if not isinstance(value, Mapping):
|
|
190
|
+
raise ExternalEventValidationError(
|
|
191
|
+
"external cognition journal projection must be one object"
|
|
192
|
+
)
|
|
193
|
+
if set(value) != _JOURNAL_PROJECTION_FIELDS:
|
|
194
|
+
raise ExternalEventValidationError(
|
|
195
|
+
"external cognition journal projection fields are not exact"
|
|
196
|
+
)
|
|
197
|
+
if (
|
|
198
|
+
type(value.get("schema")) is not str
|
|
199
|
+
or value.get("schema") != "ExternalCognitionJournalProjectionV2"
|
|
200
|
+
or (
|
|
201
|
+
type(value.get("version")) is not int
|
|
202
|
+
or value.get("version") != 2
|
|
203
|
+
)
|
|
204
|
+
):
|
|
205
|
+
raise ExternalEventValidationError(
|
|
206
|
+
"external cognition journal projection schema is invalid"
|
|
207
|
+
)
|
|
208
|
+
kind = _exact_projection_text(
|
|
209
|
+
value.get("kind"), field="kind", maximum=64,
|
|
210
|
+
)
|
|
211
|
+
if kind not in _KINDS:
|
|
212
|
+
raise ExternalEventValidationError(
|
|
213
|
+
"external cognition journal projection kind is unsupported"
|
|
214
|
+
)
|
|
215
|
+
if (
|
|
216
|
+
type(event_type) is not str
|
|
217
|
+
or event_type != f"cognition.external.{kind}"
|
|
218
|
+
):
|
|
219
|
+
raise ExternalEventValidationError(
|
|
220
|
+
"external cognition journal event type differs from its kind"
|
|
221
|
+
)
|
|
222
|
+
external_id = _exact_projection_text(
|
|
223
|
+
value.get("external_event_id"), field="event ID", maximum=192,
|
|
224
|
+
)
|
|
225
|
+
if not _SAFE_ID.fullmatch(external_id):
|
|
226
|
+
raise ExternalEventValidationError(
|
|
227
|
+
"external cognition journal event ID is invalid"
|
|
228
|
+
)
|
|
229
|
+
external_digest = _exact_projection_text(
|
|
230
|
+
value.get("external_event_digest"), field="event digest", maximum=64,
|
|
231
|
+
)
|
|
232
|
+
scope_digest = _exact_projection_text(
|
|
233
|
+
value.get("scope_digest"), field="scope digest", maximum=64,
|
|
234
|
+
)
|
|
235
|
+
if not re.fullmatch(r"[a-f0-9]{64}", external_digest):
|
|
236
|
+
raise ExternalEventValidationError(
|
|
237
|
+
"external cognition journal event digest is invalid"
|
|
238
|
+
)
|
|
239
|
+
if not re.fullmatch(r"[a-f0-9]{64}", scope_digest):
|
|
240
|
+
raise ExternalEventValidationError(
|
|
241
|
+
"external cognition journal scope digest is invalid"
|
|
242
|
+
)
|
|
243
|
+
external_occurred_at = _exact_projection_text(
|
|
244
|
+
value.get("external_occurred_at"),
|
|
245
|
+
field="occurred time", maximum=64,
|
|
246
|
+
)
|
|
247
|
+
if _iso(_parse_time(
|
|
248
|
+
external_occurred_at, field="external_occurred_at",
|
|
249
|
+
)) != external_occurred_at:
|
|
250
|
+
raise ExternalEventValidationError(
|
|
251
|
+
"external cognition journal occurred time is not canonical"
|
|
252
|
+
)
|
|
253
|
+
producer = _exact_projection_text(
|
|
254
|
+
value.get("producer_principal_id"),
|
|
255
|
+
field="producer principal ID", maximum=128,
|
|
256
|
+
)
|
|
257
|
+
revision = _exact_projection_text(
|
|
258
|
+
value.get("producer_revision"), field="producer revision", maximum=64,
|
|
259
|
+
)
|
|
260
|
+
subject = _exact_projection_text(
|
|
261
|
+
value.get("subject_person_id"), field="subject person ID",
|
|
262
|
+
maximum=SUBJECT_PERSON_ID_MAX_CHARS,
|
|
263
|
+
)
|
|
264
|
+
viewer = _exact_projection_text(
|
|
265
|
+
value.get("viewer_person_id"), field="viewer person ID",
|
|
266
|
+
maximum=SUBJECT_PERSON_ID_MAX_CHARS,
|
|
267
|
+
)
|
|
268
|
+
if not _SAFE_REF.fullmatch(producer) or not re.fullmatch(
|
|
269
|
+
r"external-principal:[a-f0-9]{24}", revision,
|
|
270
|
+
):
|
|
271
|
+
raise ExternalEventValidationError(
|
|
272
|
+
"external cognition journal producer identity is invalid"
|
|
273
|
+
)
|
|
274
|
+
if (
|
|
275
|
+
not _SAFE_REF.fullmatch(subject)
|
|
276
|
+
or not _SAFE_REF.fullmatch(viewer)
|
|
277
|
+
or subject != viewer
|
|
278
|
+
):
|
|
279
|
+
raise ExternalEventValidationError(
|
|
280
|
+
"external cognition journal subject binding is invalid"
|
|
281
|
+
)
|
|
282
|
+
sharing = _exact_projection_text(
|
|
283
|
+
value.get("shareability"), field="shareability", maximum=32,
|
|
284
|
+
)
|
|
285
|
+
viewer_scope = _exact_projection_text(
|
|
286
|
+
value.get("viewer_scope"), field="viewer scope",
|
|
287
|
+
maximum=VIEWER_SCOPE_MAX_CHARS,
|
|
288
|
+
)
|
|
289
|
+
audience = value.get("audience_scope")
|
|
290
|
+
if (
|
|
291
|
+
type(audience) is not list
|
|
292
|
+
or any(
|
|
293
|
+
type(item) is not str or not item or item != item.strip()
|
|
294
|
+
for item in audience
|
|
295
|
+
)
|
|
296
|
+
):
|
|
297
|
+
raise ExternalEventValidationError(
|
|
298
|
+
"external cognition journal audience scope is not canonical"
|
|
299
|
+
)
|
|
300
|
+
configured_owner = (
|
|
301
|
+
os.environ.get("COLONY_OWNER_PERSON_ID", "")
|
|
302
|
+
or os.environ.get("COLONY_OWNER_CONTACT_ID", "")
|
|
303
|
+
or "owner"
|
|
304
|
+
)
|
|
305
|
+
owner = _authority_id(
|
|
306
|
+
configured_owner, field="configured owner_person_id", maximum=128,
|
|
307
|
+
)
|
|
308
|
+
if sharing == "owner_private":
|
|
309
|
+
if subject != owner:
|
|
310
|
+
raise ExternalEventValidationError(
|
|
311
|
+
"external cognition owner lane subject is not the owner"
|
|
312
|
+
)
|
|
313
|
+
expected_viewer_scope = "owner"
|
|
314
|
+
expected_audience = ["owner"]
|
|
315
|
+
elif sharing == "subject_private":
|
|
316
|
+
if subject == owner:
|
|
317
|
+
raise ExternalEventValidationError(
|
|
318
|
+
"external cognition owner cannot assert a subject-private lane"
|
|
319
|
+
)
|
|
320
|
+
expected_viewer_scope = f"person:{subject}"
|
|
321
|
+
expected_audience = []
|
|
322
|
+
else:
|
|
323
|
+
raise ExternalEventValidationError(
|
|
324
|
+
"external cognition journal sharing lane is invalid"
|
|
325
|
+
)
|
|
326
|
+
if viewer_scope != expected_viewer_scope or audience != expected_audience:
|
|
327
|
+
raise ExternalEventValidationError(
|
|
328
|
+
"external cognition journal viewer or audience scope is invalid"
|
|
329
|
+
)
|
|
330
|
+
scope = {
|
|
331
|
+
"schema": "ExternalCognitionScopeV1",
|
|
332
|
+
"version": 1,
|
|
333
|
+
"subject_person_id": subject,
|
|
334
|
+
"viewer_person_id": viewer,
|
|
335
|
+
"viewer_scope": viewer_scope,
|
|
336
|
+
"shareability": sharing,
|
|
337
|
+
"audience_scope": expected_audience,
|
|
338
|
+
}
|
|
339
|
+
if scope_digest != _digest(scope):
|
|
340
|
+
raise ExternalEventValidationError(
|
|
341
|
+
"external cognition journal scope digest mismatch"
|
|
342
|
+
)
|
|
343
|
+
evidence_status = _exact_projection_text(
|
|
344
|
+
value.get("evidence_status"), field="evidence status", maximum=32,
|
|
345
|
+
)
|
|
346
|
+
if value.get("boundary_attested") is not False or (
|
|
347
|
+
evidence_status != "reported/unverified"
|
|
348
|
+
):
|
|
349
|
+
raise ExternalEventValidationError(
|
|
350
|
+
"external cognition journal projection asserted authority"
|
|
351
|
+
)
|
|
352
|
+
if type(value.get("summary")) is not str:
|
|
353
|
+
raise ExternalEventValidationError(
|
|
354
|
+
"external cognition journal summary must be an exact string"
|
|
355
|
+
)
|
|
356
|
+
summary = _text(value.get("summary"), field="summary", maximum=1000)
|
|
357
|
+
attributes = _typed_attributes(kind, value.get("attributes"))
|
|
358
|
+
if summary != value.get("summary") or _canonical(attributes) != _canonical(
|
|
359
|
+
value.get("attributes")
|
|
360
|
+
):
|
|
361
|
+
raise ExternalEventValidationError(
|
|
362
|
+
"external cognition journal content is not canonical"
|
|
363
|
+
)
|
|
364
|
+
return dict(value)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _canonical(value: Any) -> str:
|
|
368
|
+
return json.dumps(
|
|
369
|
+
value, ensure_ascii=False, sort_keys=True, separators=(",", ":"),
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _digest(value: Any) -> str:
|
|
374
|
+
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _parse_time(value: Any, *, field: str) -> datetime:
|
|
378
|
+
text = str(value or "").strip()
|
|
379
|
+
if text.endswith("Z"):
|
|
380
|
+
text = text[:-1] + "+00:00"
|
|
381
|
+
try:
|
|
382
|
+
parsed = datetime.fromisoformat(text)
|
|
383
|
+
except ValueError as exc:
|
|
384
|
+
raise ExternalEventValidationError(
|
|
385
|
+
f"{field} must be an RFC3339 timestamp"
|
|
386
|
+
) from exc
|
|
387
|
+
if parsed.tzinfo is None:
|
|
388
|
+
raise ExternalEventValidationError(f"{field} requires a timezone")
|
|
389
|
+
return parsed.astimezone(timezone.utc)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _iso(value: datetime) -> str:
|
|
393
|
+
return value.astimezone(timezone.utc).isoformat()
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _text(value: Any, *, field: str, maximum: int) -> str:
|
|
397
|
+
if not isinstance(value, str):
|
|
398
|
+
raise ExternalEventValidationError(f"{field} must be text")
|
|
399
|
+
result = " ".join(value.split()).strip()
|
|
400
|
+
if not result or len(result) > maximum:
|
|
401
|
+
raise ExternalEventValidationError(
|
|
402
|
+
f"{field} must contain 1-{maximum} characters"
|
|
403
|
+
)
|
|
404
|
+
if _SECRET_VALUE.search(result):
|
|
405
|
+
raise ExternalEventValidationError(f"{field} contains secret-like data")
|
|
406
|
+
return result
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _authority_id(value: Any, *, field: str, maximum: int) -> str:
|
|
410
|
+
"""Accept one exact server authority identifier without collision-prone slicing."""
|
|
411
|
+
|
|
412
|
+
if (
|
|
413
|
+
type(value) is not str
|
|
414
|
+
or not value
|
|
415
|
+
or value != value.strip()
|
|
416
|
+
or len(value) > maximum
|
|
417
|
+
or not _SAFE_REF.fullmatch(value)
|
|
418
|
+
):
|
|
419
|
+
raise ExternalEventValidationError(
|
|
420
|
+
f"{field} is outside the exact authority identifier boundary"
|
|
421
|
+
)
|
|
422
|
+
return value
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _attribute_scalar(value: Any, *, field: str) -> Any:
|
|
426
|
+
if value is None or isinstance(value, (bool, int)):
|
|
427
|
+
return value
|
|
428
|
+
if isinstance(value, float):
|
|
429
|
+
if not math.isfinite(value):
|
|
430
|
+
raise ExternalEventValidationError(f"{field} must be finite")
|
|
431
|
+
return value
|
|
432
|
+
if isinstance(value, str):
|
|
433
|
+
result = " ".join(value.split()).strip()
|
|
434
|
+
if len(result) > 500:
|
|
435
|
+
raise ExternalEventValidationError(f"{field} exceeds 500 characters")
|
|
436
|
+
if _SECRET_VALUE.search(result):
|
|
437
|
+
raise ExternalEventValidationError(f"{field} contains secret-like data")
|
|
438
|
+
return result
|
|
439
|
+
raise ExternalEventValidationError(
|
|
440
|
+
f"{field} supports only JSON scalars or scalar arrays"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _attributes(value: Any) -> Dict[str, Any]:
|
|
445
|
+
if value is None:
|
|
446
|
+
return {}
|
|
447
|
+
if not isinstance(value, Mapping):
|
|
448
|
+
raise ExternalEventValidationError("attributes must be one object")
|
|
449
|
+
if len(value) > 32:
|
|
450
|
+
raise ExternalEventValidationError("attributes exceeds 32 fields")
|
|
451
|
+
result: Dict[str, Any] = {}
|
|
452
|
+
for raw_key, raw_value in value.items():
|
|
453
|
+
key = str(raw_key or "").strip().lower()
|
|
454
|
+
if not _SAFE_ATTRIBUTE.fullmatch(key):
|
|
455
|
+
raise ExternalEventValidationError("attributes contains an invalid key")
|
|
456
|
+
if key in _RESERVED_ATTRIBUTES or _AUTHORITY_ATTRIBUTE_ALIAS.search(key):
|
|
457
|
+
raise ExternalEventValidationError(
|
|
458
|
+
f"attributes.{key} attempts to assert server authority"
|
|
459
|
+
)
|
|
460
|
+
if _SECRET_KEY.search(key):
|
|
461
|
+
raise ExternalEventValidationError(
|
|
462
|
+
f"attributes.{key} is secret-bearing"
|
|
463
|
+
)
|
|
464
|
+
if isinstance(raw_value, list):
|
|
465
|
+
if len(raw_value) > 16:
|
|
466
|
+
raise ExternalEventValidationError(
|
|
467
|
+
f"attributes.{key} exceeds 16 list items"
|
|
468
|
+
)
|
|
469
|
+
result[key] = [
|
|
470
|
+
_attribute_scalar(item, field=f"attributes.{key}")
|
|
471
|
+
for item in raw_value
|
|
472
|
+
]
|
|
473
|
+
else:
|
|
474
|
+
result[key] = _attribute_scalar(
|
|
475
|
+
raw_value, field=f"attributes.{key}",
|
|
476
|
+
)
|
|
477
|
+
if len(_canonical(result).encode("utf-8")) > 8192:
|
|
478
|
+
raise ExternalEventValidationError("attributes exceeds 8192 bytes")
|
|
479
|
+
return result
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _typed_attributes(kind: str, value: Any) -> Dict[str, Any]:
|
|
483
|
+
result = _attributes(value)
|
|
484
|
+
contract = _KIND_FIELDS[kind]
|
|
485
|
+
missing = set(contract["required"]) - set(result)
|
|
486
|
+
unknown = set(result) - set(contract["allowed"])
|
|
487
|
+
if kind == "delivery_outcome" and not (
|
|
488
|
+
result.get("delivery_ref") or result.get("message_ref")
|
|
489
|
+
):
|
|
490
|
+
missing.add("delivery_ref_or_message_ref")
|
|
491
|
+
if missing:
|
|
492
|
+
raise ExternalEventValidationError(
|
|
493
|
+
f"{kind} attributes missing: {','.join(sorted(missing))}"
|
|
494
|
+
)
|
|
495
|
+
if unknown:
|
|
496
|
+
raise ExternalEventValidationError(
|
|
497
|
+
f"{kind} attributes unsupported: {','.join(sorted(unknown))}"
|
|
498
|
+
)
|
|
499
|
+
identifier_fields = {
|
|
500
|
+
"action_id", "action_digest", "delivery_ref", "message_ref", "service",
|
|
501
|
+
"request_id", "decision_ref", "target_ref", "turn_id",
|
|
502
|
+
}
|
|
503
|
+
for field in identifier_fields.intersection(result):
|
|
504
|
+
if not isinstance(result[field], str) or not _SAFE_REF.fullmatch(
|
|
505
|
+
result[field]
|
|
506
|
+
):
|
|
507
|
+
raise ExternalEventValidationError(
|
|
508
|
+
f"{kind} attributes.{field} is invalid"
|
|
509
|
+
)
|
|
510
|
+
if "action_digest" in result and not (
|
|
511
|
+
isinstance(result["action_digest"], str)
|
|
512
|
+
and re.fullmatch(r"[a-f0-9]{64}", result["action_digest"])
|
|
513
|
+
):
|
|
514
|
+
raise ExternalEventValidationError(
|
|
515
|
+
"action_outcome attributes.action_digest must be lowercase 64-hex"
|
|
516
|
+
)
|
|
517
|
+
for field in {
|
|
518
|
+
"channel", "detail", "error_code", "observation", "reason_code", "signal",
|
|
519
|
+
}.intersection(result):
|
|
520
|
+
if not isinstance(result[field], str):
|
|
521
|
+
raise ExternalEventValidationError(
|
|
522
|
+
f"{kind} attributes.{field} must be text"
|
|
523
|
+
)
|
|
524
|
+
if kind == "text_turn_observation":
|
|
525
|
+
result["observation"] = _text(
|
|
526
|
+
result["observation"],
|
|
527
|
+
field="text_turn_observation attributes.observation",
|
|
528
|
+
maximum=500,
|
|
529
|
+
)
|
|
530
|
+
numeric_bounds = {
|
|
531
|
+
"duration_ms": (0.0, 86_400_000.0, False),
|
|
532
|
+
"latency_ms": (0.0, 3_600_000.0, False),
|
|
533
|
+
"observed_samples": (1.0, 1_000_000.0, True),
|
|
534
|
+
"intensity": (0.0, 1.0, False),
|
|
535
|
+
}
|
|
536
|
+
for field, (minimum, maximum, integer_only) in numeric_bounds.items():
|
|
537
|
+
if field not in result:
|
|
538
|
+
continue
|
|
539
|
+
numeric = result[field]
|
|
540
|
+
if (
|
|
541
|
+
isinstance(numeric, bool)
|
|
542
|
+
or not isinstance(numeric, (int, float))
|
|
543
|
+
or not math.isfinite(float(numeric))
|
|
544
|
+
or not minimum <= float(numeric) <= maximum
|
|
545
|
+
or (integer_only and not isinstance(numeric, int))
|
|
546
|
+
):
|
|
547
|
+
raise ExternalEventValidationError(
|
|
548
|
+
f"{kind} attributes.{field} is outside bounds"
|
|
549
|
+
)
|
|
550
|
+
for field, choices in contract["enum"].items():
|
|
551
|
+
if field not in result:
|
|
552
|
+
continue
|
|
553
|
+
if not isinstance(result.get(field), str):
|
|
554
|
+
raise ExternalEventValidationError(
|
|
555
|
+
f"{kind} attributes.{field} must be text"
|
|
556
|
+
)
|
|
557
|
+
normalized = result[field].strip().lower()
|
|
558
|
+
if normalized not in choices:
|
|
559
|
+
raise ExternalEventValidationError(
|
|
560
|
+
f"{kind} attributes.{field} is unsupported"
|
|
561
|
+
)
|
|
562
|
+
result[field] = normalized
|
|
563
|
+
return result
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@dataclass(frozen=True)
|
|
567
|
+
class ExternalCognitionEventV1:
|
|
568
|
+
event_id: str
|
|
569
|
+
kind: str
|
|
570
|
+
occurred_at: str
|
|
571
|
+
summary: str
|
|
572
|
+
attributes: Mapping[str, Any]
|
|
573
|
+
producer_principal_id: str
|
|
574
|
+
producer_credential_id: str
|
|
575
|
+
producer_revision: str
|
|
576
|
+
subject_person_id: str
|
|
577
|
+
viewer_person_id: str
|
|
578
|
+
viewer_scope: str
|
|
579
|
+
shareability: str
|
|
580
|
+
audience_scope: tuple[str, ...]
|
|
581
|
+
scope_digest: str
|
|
582
|
+
boundary_attested: bool
|
|
583
|
+
event_digest: str
|
|
584
|
+
schema: str = "ExternalCognitionEventV1"
|
|
585
|
+
version: int = 1
|
|
586
|
+
|
|
587
|
+
@classmethod
|
|
588
|
+
def from_authority(
|
|
589
|
+
cls,
|
|
590
|
+
body: Mapping[str, Any],
|
|
591
|
+
*,
|
|
592
|
+
authority: Any,
|
|
593
|
+
now: Optional[datetime] = None,
|
|
594
|
+
) -> "ExternalCognitionEventV1":
|
|
595
|
+
if not isinstance(body, Mapping):
|
|
596
|
+
raise ExternalEventValidationError("event body must be one object")
|
|
597
|
+
allowed = {"event_id", "kind", "occurred_at", "summary", "attributes"}
|
|
598
|
+
unknown = set(body) - allowed
|
|
599
|
+
if unknown:
|
|
600
|
+
raise ExternalEventValidationError(
|
|
601
|
+
"event body contains unsupported fields: "
|
|
602
|
+
+ ",".join(sorted(str(item) for item in unknown))
|
|
603
|
+
)
|
|
604
|
+
if not (
|
|
605
|
+
getattr(authority, "authenticated", False)
|
|
606
|
+
and not getattr(authority, "legacy", False)
|
|
607
|
+
and not getattr(authority, "anonymous", False)
|
|
608
|
+
and authority.has_scope("cognition:events-ingest")
|
|
609
|
+
and getattr(authority, "principal_id", "")
|
|
610
|
+
and getattr(authority, "credential_id", "")
|
|
611
|
+
and getattr(authority, "viewer_person_id", "")
|
|
612
|
+
):
|
|
613
|
+
raise ExternalEventValidationError(
|
|
614
|
+
"scoped principal with an exact viewer binding is required"
|
|
615
|
+
)
|
|
616
|
+
event_id = str(body.get("event_id") or "").strip()
|
|
617
|
+
if not _SAFE_ID.fullmatch(event_id):
|
|
618
|
+
raise ExternalEventValidationError("event_id is invalid")
|
|
619
|
+
kind = str(body.get("kind") or "").strip().lower()
|
|
620
|
+
if kind not in _KINDS:
|
|
621
|
+
raise ExternalEventValidationError("external event kind is unsupported")
|
|
622
|
+
observed = now or datetime.now(timezone.utc)
|
|
623
|
+
if observed.tzinfo is None:
|
|
624
|
+
observed = observed.replace(tzinfo=timezone.utc)
|
|
625
|
+
occurred = _parse_time(body.get("occurred_at"), field="occurred_at")
|
|
626
|
+
if occurred > observed.astimezone(timezone.utc) + timedelta(minutes=5):
|
|
627
|
+
raise ExternalEventValidationError("occurred_at is too far in the future")
|
|
628
|
+
if occurred < observed.astimezone(timezone.utc) - timedelta(days=366):
|
|
629
|
+
raise ExternalEventValidationError("occurred_at is outside retention bounds")
|
|
630
|
+
summary = _text(body.get("summary"), field="summary", maximum=1000)
|
|
631
|
+
attributes = _typed_attributes(kind, body.get("attributes"))
|
|
632
|
+
|
|
633
|
+
principal = _authority_id(
|
|
634
|
+
authority.principal_id, field="producer principal_id", maximum=128,
|
|
635
|
+
)
|
|
636
|
+
credential = _authority_id(
|
|
637
|
+
authority.credential_id, field="producer credential_id", maximum=192,
|
|
638
|
+
)
|
|
639
|
+
viewer = _authority_id(
|
|
640
|
+
authority.viewer_person_id, field="viewer_person_id", maximum=128,
|
|
641
|
+
)
|
|
642
|
+
configured_owner = (
|
|
643
|
+
os.environ.get("COLONY_OWNER_PERSON_ID", "")
|
|
644
|
+
or os.environ.get("COLONY_OWNER_CONTACT_ID", "")
|
|
645
|
+
or "owner"
|
|
646
|
+
)
|
|
647
|
+
owner = _authority_id(
|
|
648
|
+
configured_owner, field="configured owner_person_id", maximum=128,
|
|
649
|
+
)
|
|
650
|
+
owner_private = "owner" in authority.audiences and viewer == owner
|
|
651
|
+
sharing = "owner_private" if owner_private else "subject_private"
|
|
652
|
+
viewer_scope = "owner" if owner_private else f"person:{viewer}"
|
|
653
|
+
audience_scope = ("owner",) if owner_private else ()
|
|
654
|
+
scope = {
|
|
655
|
+
"schema": "ExternalCognitionScopeV1",
|
|
656
|
+
"version": 1,
|
|
657
|
+
"subject_person_id": viewer,
|
|
658
|
+
"viewer_person_id": viewer,
|
|
659
|
+
"viewer_scope": viewer_scope,
|
|
660
|
+
"shareability": sharing,
|
|
661
|
+
"audience_scope": list(audience_scope),
|
|
662
|
+
}
|
|
663
|
+
producer_revision = "external-principal:" + _digest({
|
|
664
|
+
"principal_id": principal,
|
|
665
|
+
"credential_id": credential,
|
|
666
|
+
"scopes": sorted(authority.scopes),
|
|
667
|
+
})[:24]
|
|
668
|
+
payload = {
|
|
669
|
+
"schema": "ExternalCognitionEventV1",
|
|
670
|
+
"version": 1,
|
|
671
|
+
"event_id": event_id,
|
|
672
|
+
"kind": kind,
|
|
673
|
+
"occurred_at": _iso(occurred),
|
|
674
|
+
"summary": summary,
|
|
675
|
+
"attributes": attributes,
|
|
676
|
+
"producer_principal_id": principal,
|
|
677
|
+
"producer_credential_id": credential,
|
|
678
|
+
"producer_revision": producer_revision,
|
|
679
|
+
"subject_person_id": scope["subject_person_id"],
|
|
680
|
+
"viewer_person_id": scope["viewer_person_id"],
|
|
681
|
+
"viewer_scope": scope["viewer_scope"],
|
|
682
|
+
"shareability": scope["shareability"],
|
|
683
|
+
"audience_scope": scope["audience_scope"],
|
|
684
|
+
"scope_digest": _digest(scope),
|
|
685
|
+
"boundary_attested": False,
|
|
686
|
+
}
|
|
687
|
+
return cls(
|
|
688
|
+
event_id=event_id,
|
|
689
|
+
kind=kind,
|
|
690
|
+
occurred_at=payload["occurred_at"],
|
|
691
|
+
summary=summary,
|
|
692
|
+
attributes=attributes,
|
|
693
|
+
producer_principal_id=principal,
|
|
694
|
+
producer_credential_id=credential,
|
|
695
|
+
producer_revision=producer_revision,
|
|
696
|
+
subject_person_id=viewer,
|
|
697
|
+
viewer_person_id=viewer,
|
|
698
|
+
viewer_scope=viewer_scope,
|
|
699
|
+
shareability=sharing,
|
|
700
|
+
audience_scope=audience_scope,
|
|
701
|
+
scope_digest=payload["scope_digest"],
|
|
702
|
+
boundary_attested=False,
|
|
703
|
+
event_digest=_digest(payload),
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
def payload(self) -> Dict[str, Any]:
|
|
707
|
+
payload = asdict(self)
|
|
708
|
+
payload["audience_scope"] = list(self.audience_scope)
|
|
709
|
+
payload["attributes"] = dict(self.attributes)
|
|
710
|
+
return payload
|
|
711
|
+
|
|
712
|
+
def journal_payload(self) -> Dict[str, Any]:
|
|
713
|
+
return {
|
|
714
|
+
"schema": "ExternalCognitionJournalProjectionV2",
|
|
715
|
+
"version": 2,
|
|
716
|
+
"external_event_id": self.event_id,
|
|
717
|
+
"external_event_digest": self.event_digest,
|
|
718
|
+
"external_occurred_at": self.occurred_at,
|
|
719
|
+
"kind": self.kind,
|
|
720
|
+
"summary": self.summary,
|
|
721
|
+
"attributes": dict(self.attributes),
|
|
722
|
+
"producer_principal_id": self.producer_principal_id,
|
|
723
|
+
"producer_revision": self.producer_revision,
|
|
724
|
+
"subject_person_id": self.subject_person_id,
|
|
725
|
+
"viewer_person_id": self.viewer_person_id,
|
|
726
|
+
"viewer_scope": self.viewer_scope,
|
|
727
|
+
"shareability": self.shareability,
|
|
728
|
+
"audience_scope": list(self.audience_scope),
|
|
729
|
+
"scope_digest": self.scope_digest,
|
|
730
|
+
"boundary_attested": False,
|
|
731
|
+
"evidence_status": "reported/unverified",
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
class ExternalEventInboxStore:
|
|
736
|
+
"""Durable idempotency ledger for external cognition events."""
|
|
737
|
+
|
|
738
|
+
def __init__(self, db_path: str) -> None:
|
|
739
|
+
self._lock = threading.RLock()
|
|
740
|
+
self._conn = sqlite3.connect(str(db_path), check_same_thread=False)
|
|
741
|
+
self._conn.row_factory = sqlite3.Row
|
|
742
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
743
|
+
with self._lock:
|
|
744
|
+
self._conn.execute(
|
|
745
|
+
"""CREATE TABLE IF NOT EXISTS external_cognition_events (
|
|
746
|
+
event_id TEXT PRIMARY KEY,
|
|
747
|
+
event_digest TEXT NOT NULL,
|
|
748
|
+
event_json TEXT NOT NULL,
|
|
749
|
+
receipt_ref TEXT NOT NULL UNIQUE,
|
|
750
|
+
state TEXT NOT NULL,
|
|
751
|
+
receipt_json TEXT NOT NULL,
|
|
752
|
+
journal_seq INTEGER,
|
|
753
|
+
journal_event_id TEXT,
|
|
754
|
+
created_at REAL NOT NULL,
|
|
755
|
+
updated_at REAL NOT NULL
|
|
756
|
+
)"""
|
|
757
|
+
)
|
|
758
|
+
self._conn.commit()
|
|
759
|
+
|
|
760
|
+
def close(self) -> None:
|
|
761
|
+
with self._lock:
|
|
762
|
+
try:
|
|
763
|
+
self._conn.close()
|
|
764
|
+
except sqlite3.ProgrammingError:
|
|
765
|
+
pass
|
|
766
|
+
|
|
767
|
+
@staticmethod
|
|
768
|
+
def _row(row: sqlite3.Row) -> Dict[str, Any]:
|
|
769
|
+
result = dict(row)
|
|
770
|
+
result["event"] = json.loads(result.pop("event_json"))
|
|
771
|
+
result["receipt"] = json.loads(result.pop("receipt_json"))
|
|
772
|
+
return result
|
|
773
|
+
|
|
774
|
+
def reserve(
|
|
775
|
+
self,
|
|
776
|
+
event: ExternalCognitionEventV1,
|
|
777
|
+
*,
|
|
778
|
+
now: Optional[datetime] = None,
|
|
779
|
+
) -> tuple[Dict[str, Any], bool]:
|
|
780
|
+
observed = now or datetime.now(timezone.utc)
|
|
781
|
+
if observed.tzinfo is None:
|
|
782
|
+
observed = observed.replace(tzinfo=timezone.utc)
|
|
783
|
+
stamp = observed.astimezone(timezone.utc).timestamp()
|
|
784
|
+
receipt_ref = f"external-event-receipt:{event.event_digest[:24]}"
|
|
785
|
+
accepted = {
|
|
786
|
+
"schema": "ExternalEventIntakeReceiptV1",
|
|
787
|
+
"version": 1,
|
|
788
|
+
"receipt_ref": receipt_ref,
|
|
789
|
+
"event_id": event.event_id,
|
|
790
|
+
"event_digest": event.event_digest,
|
|
791
|
+
"status": "accepted",
|
|
792
|
+
"subject_person_id": event.subject_person_id,
|
|
793
|
+
"viewer_person_id": event.viewer_person_id,
|
|
794
|
+
"shareability": event.shareability,
|
|
795
|
+
"scope_digest": event.scope_digest,
|
|
796
|
+
"journal_seq": None,
|
|
797
|
+
"journal_event_id": None,
|
|
798
|
+
"accepted_at": _iso(observed),
|
|
799
|
+
}
|
|
800
|
+
encoded_event = _canonical(event.payload())
|
|
801
|
+
with self._lock:
|
|
802
|
+
row = self._conn.execute(
|
|
803
|
+
"SELECT * FROM external_cognition_events WHERE event_id=?",
|
|
804
|
+
(event.event_id,),
|
|
805
|
+
).fetchone()
|
|
806
|
+
if row is not None:
|
|
807
|
+
if (
|
|
808
|
+
row["event_digest"] != event.event_digest
|
|
809
|
+
or row["event_json"] != encoded_event
|
|
810
|
+
):
|
|
811
|
+
raise ExternalEventConflict(
|
|
812
|
+
"event_id replay changed immutable content"
|
|
813
|
+
)
|
|
814
|
+
return self._row(row), False
|
|
815
|
+
self._conn.execute(
|
|
816
|
+
"""INSERT INTO external_cognition_events
|
|
817
|
+
(event_id,event_digest,event_json,receipt_ref,state,
|
|
818
|
+
receipt_json,journal_seq,journal_event_id,created_at,updated_at)
|
|
819
|
+
VALUES (?,?,?,?, 'accepted',?,NULL,NULL,?,?)""",
|
|
820
|
+
(
|
|
821
|
+
event.event_id, event.event_digest, encoded_event,
|
|
822
|
+
receipt_ref, _canonical(accepted), stamp, stamp,
|
|
823
|
+
),
|
|
824
|
+
)
|
|
825
|
+
self._conn.commit()
|
|
826
|
+
row = self._conn.execute(
|
|
827
|
+
"SELECT * FROM external_cognition_events WHERE event_id=?",
|
|
828
|
+
(event.event_id,),
|
|
829
|
+
).fetchone()
|
|
830
|
+
return self._row(row), True
|
|
831
|
+
|
|
832
|
+
def complete_projection(
|
|
833
|
+
self,
|
|
834
|
+
event: ExternalCognitionEventV1,
|
|
835
|
+
journal_record: Mapping[str, Any],
|
|
836
|
+
) -> Dict[str, Any]:
|
|
837
|
+
with self._lock:
|
|
838
|
+
row = self._conn.execute(
|
|
839
|
+
"SELECT * FROM external_cognition_events WHERE event_id=?",
|
|
840
|
+
(event.event_id,),
|
|
841
|
+
).fetchone()
|
|
842
|
+
if row is None:
|
|
843
|
+
raise ValueError("external event receipt is unavailable")
|
|
844
|
+
if row["event_digest"] != event.event_digest:
|
|
845
|
+
raise ExternalEventConflict(
|
|
846
|
+
"event changed before journal projection"
|
|
847
|
+
)
|
|
848
|
+
if row["state"] == "projected":
|
|
849
|
+
return self._row(row)["receipt"]
|
|
850
|
+
receipt = json.loads(row["receipt_json"])
|
|
851
|
+
receipt.update({
|
|
852
|
+
"status": "projected",
|
|
853
|
+
"journal_seq": int(journal_record["seq"]),
|
|
854
|
+
"journal_event_id": str(journal_record["ulid"]),
|
|
855
|
+
"projected_at": str(journal_record["recordedAt"]),
|
|
856
|
+
"journal_retained": bool(journal_record.get("retained", True)),
|
|
857
|
+
})
|
|
858
|
+
self._conn.execute(
|
|
859
|
+
"""UPDATE external_cognition_events
|
|
860
|
+
SET state='projected',receipt_json=?,journal_seq=?,
|
|
861
|
+
journal_event_id=?,updated_at=? WHERE event_id=?
|
|
862
|
+
AND state='accepted'""",
|
|
863
|
+
(
|
|
864
|
+
_canonical(receipt), receipt["journal_seq"],
|
|
865
|
+
receipt["journal_event_id"], time.time(), event.event_id,
|
|
866
|
+
),
|
|
867
|
+
)
|
|
868
|
+
self._conn.commit()
|
|
869
|
+
return receipt
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
class ExternalEventIntake:
|
|
873
|
+
"""Reserve one external receipt, then project it to the host journal."""
|
|
874
|
+
|
|
875
|
+
def __init__(
|
|
876
|
+
self,
|
|
877
|
+
store: ExternalEventInboxStore,
|
|
878
|
+
*,
|
|
879
|
+
journal_projector: Optional[Callable[..., Optional[Mapping[str, Any]]]] = None,
|
|
880
|
+
journal_acknowledger: Optional[Callable[[str], bool]] = None,
|
|
881
|
+
) -> None:
|
|
882
|
+
self.store = store
|
|
883
|
+
if journal_projector is None:
|
|
884
|
+
from apsimo.events.journal import append_event_record
|
|
885
|
+
journal_projector = append_event_record
|
|
886
|
+
if journal_acknowledger is None:
|
|
887
|
+
from apsimo.events.journal import acknowledge_event_record
|
|
888
|
+
journal_acknowledger = acknowledge_event_record
|
|
889
|
+
self._project = journal_projector
|
|
890
|
+
self._acknowledge = journal_acknowledger
|
|
891
|
+
|
|
892
|
+
def close(self) -> None:
|
|
893
|
+
self.store.close()
|
|
894
|
+
|
|
895
|
+
def ingest(
|
|
896
|
+
self,
|
|
897
|
+
event: ExternalCognitionEventV1,
|
|
898
|
+
*,
|
|
899
|
+
now: Optional[datetime] = None,
|
|
900
|
+
) -> Dict[str, Any]:
|
|
901
|
+
row, _created = self.store.reserve(event, now=now)
|
|
902
|
+
event_key = f"external-cognition:{event.event_id}"
|
|
903
|
+
if row["state"] == "projected":
|
|
904
|
+
# A process may have committed the inbox transaction and died
|
|
905
|
+
# before releasing the journal's metadata-only handshake marker.
|
|
906
|
+
self._acknowledge(event_key)
|
|
907
|
+
return dict(row["receipt"])
|
|
908
|
+
record = self._project(
|
|
909
|
+
f"cognition.external.{event.kind}",
|
|
910
|
+
event.journal_payload(),
|
|
911
|
+
occurred_at=event.occurred_at,
|
|
912
|
+
event_key=event_key,
|
|
913
|
+
)
|
|
914
|
+
if record is None:
|
|
915
|
+
raise ExternalEventProjectionError(
|
|
916
|
+
"external event receipt is durable; journal projection failed"
|
|
917
|
+
)
|
|
918
|
+
receipt = self.store.complete_projection(event, record)
|
|
919
|
+
# Only the durable inbox commit can acknowledge the keyed journal
|
|
920
|
+
# projection. A failed cleanup is harmless and retried on exact replay.
|
|
921
|
+
self._acknowledge(event_key)
|
|
922
|
+
return receipt
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
__all__ = [
|
|
926
|
+
"EXTERNAL_JOURNAL_EVENT_ID_MAX_CHARS",
|
|
927
|
+
"ExternalCognitionEventV1", "ExternalEventConflict",
|
|
928
|
+
"ExternalEventInboxStore", "ExternalEventIntake",
|
|
929
|
+
"ExternalEventProjectionError", "ExternalEventValidationError",
|
|
930
|
+
"validate_external_journal_event_id",
|
|
931
|
+
"validate_external_journal_projection",
|
|
932
|
+
]
|