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,170 @@
|
|
|
1
|
+
"""Context provenance — the data layer behind the cross-context leak check.
|
|
2
|
+
|
|
3
|
+
Records which (normalized) entities have appeared in which *conversation context*, so a
|
|
4
|
+
response about to go to one context can be flagged for surfacing an entity that is known
|
|
5
|
+
ONLY from a different, private context. This is the durable, populated replacement for the
|
|
6
|
+
old gate's dead in-memory per-session entity sets.
|
|
7
|
+
|
|
8
|
+
Generic by design: a ``conversation_key`` is any opaque string the embedding host uses to
|
|
9
|
+
identify a context (e.g. a platform conversation id). Colony attaches no meaning to it.
|
|
10
|
+
|
|
11
|
+
Privacy model:
|
|
12
|
+
- An entity recorded with ``is_public=True`` in any context is never treated as a leak
|
|
13
|
+
(public knowledge: place names, public figures, etc.).
|
|
14
|
+
- An entity present in the *current* conversation's provenance is fine (it belongs here).
|
|
15
|
+
- An entity present only in *other* private conversations, surfaced in a reply to this one,
|
|
16
|
+
is the leak we flag.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import sqlite3
|
|
22
|
+
import unicodedata
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from typing import List, Optional, Sequence
|
|
25
|
+
|
|
26
|
+
from apsimo.gate.communication_policy import CommunicationPolicyContextV1
|
|
27
|
+
from apsimo.gate.response_guard import CrossContextGuard, GuardFinding
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _now() -> str:
|
|
31
|
+
return datetime.now(tz=timezone.utc).isoformat()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def normalize_entity(name: str) -> str:
|
|
35
|
+
return unicodedata.normalize("NFKC", name or "").lower().strip()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ContextProvenanceStore:
|
|
39
|
+
"""SQLite-backed index of entity -> conversation-context provenance."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, db_path: str = ":memory:") -> None:
|
|
42
|
+
self._conn = sqlite3.connect(db_path)
|
|
43
|
+
self._conn.row_factory = sqlite3.Row
|
|
44
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
45
|
+
self._create_tables()
|
|
46
|
+
|
|
47
|
+
def _create_tables(self) -> None:
|
|
48
|
+
self._conn.executescript(
|
|
49
|
+
"""
|
|
50
|
+
CREATE TABLE IF NOT EXISTS context_entities (
|
|
51
|
+
entity_norm TEXT NOT NULL,
|
|
52
|
+
conversation_key TEXT NOT NULL,
|
|
53
|
+
contact_id TEXT,
|
|
54
|
+
is_public INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
first_seen TEXT NOT NULL,
|
|
56
|
+
last_seen TEXT NOT NULL,
|
|
57
|
+
count INTEGER NOT NULL DEFAULT 1,
|
|
58
|
+
PRIMARY KEY (entity_norm, conversation_key)
|
|
59
|
+
);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_ctx_entities_norm ON context_entities(entity_norm);
|
|
61
|
+
"""
|
|
62
|
+
)
|
|
63
|
+
self._conn.commit()
|
|
64
|
+
|
|
65
|
+
def record(
|
|
66
|
+
self,
|
|
67
|
+
conversation_key: str,
|
|
68
|
+
entities: Sequence[str],
|
|
69
|
+
*,
|
|
70
|
+
contact_id: Optional[str] = None,
|
|
71
|
+
is_public: bool = False,
|
|
72
|
+
min_len: int = 3,
|
|
73
|
+
at: Optional[str] = None,
|
|
74
|
+
) -> int:
|
|
75
|
+
"""Record that ``entities`` appeared in ``conversation_key``. Returns the count kept."""
|
|
76
|
+
if not conversation_key:
|
|
77
|
+
return 0
|
|
78
|
+
now = at or _now()
|
|
79
|
+
kept = 0
|
|
80
|
+
for raw in entities or ():
|
|
81
|
+
n = normalize_entity(raw)
|
|
82
|
+
if len(n) < min_len:
|
|
83
|
+
continue
|
|
84
|
+
self._conn.execute(
|
|
85
|
+
"INSERT INTO context_entities "
|
|
86
|
+
"(entity_norm, conversation_key, contact_id, is_public, first_seen, last_seen, count) "
|
|
87
|
+
"VALUES (?,?,?,?,?,?,1) "
|
|
88
|
+
"ON CONFLICT(entity_norm, conversation_key) DO UPDATE SET "
|
|
89
|
+
"last_seen=excluded.last_seen, count=count+1, "
|
|
90
|
+
"is_public=MAX(is_public, excluded.is_public)",
|
|
91
|
+
(n, conversation_key, contact_id, 1 if is_public else 0, now, now),
|
|
92
|
+
)
|
|
93
|
+
kept += 1
|
|
94
|
+
self._conn.commit()
|
|
95
|
+
return kept
|
|
96
|
+
|
|
97
|
+
def contexts_for(self, entity: str) -> List[dict]:
|
|
98
|
+
"""Contexts where this entity has appeared: [{conversation_key, is_public}]."""
|
|
99
|
+
n = normalize_entity(entity)
|
|
100
|
+
if not n:
|
|
101
|
+
return []
|
|
102
|
+
rows = self._conn.execute(
|
|
103
|
+
"SELECT conversation_key, is_public FROM context_entities WHERE entity_norm=?",
|
|
104
|
+
(n,),
|
|
105
|
+
).fetchall()
|
|
106
|
+
return [dict(r) for r in rows]
|
|
107
|
+
|
|
108
|
+
def close(self) -> None:
|
|
109
|
+
try:
|
|
110
|
+
self._conn.close()
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ProvenanceCrossContextGuard(CrossContextGuard):
|
|
116
|
+
"""Flags a reply that surfaces an entity known only from a different private context.
|
|
117
|
+
|
|
118
|
+
``extractor`` (optional ``ConversationExtractor``) pulls entities from the response text
|
|
119
|
+
itself, so the host need only pass the text; any ``mentioned_entities`` passed in are
|
|
120
|
+
merged on top. Entities introduced earlier in the *same* conversation (recorded under the
|
|
121
|
+
same ``conversation_key`` on inbound) are never flagged.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, store: ContextProvenanceStore, extractor=None,
|
|
125
|
+
public_entities: Optional[Sequence[str]] = None) -> None:
|
|
126
|
+
self._store = store
|
|
127
|
+
self._extractor = extractor
|
|
128
|
+
self._public = {normalize_entity(e) for e in (public_entities or ())}
|
|
129
|
+
|
|
130
|
+
async def _entities_in(self, response_text: str, mentioned_entities: Sequence[str]) -> set:
|
|
131
|
+
ents = {normalize_entity(e) for e in (mentioned_entities or ())}
|
|
132
|
+
if self._extractor is not None and response_text:
|
|
133
|
+
try:
|
|
134
|
+
res = await self._extractor.extract(response_text, "response_guard")
|
|
135
|
+
ents |= {normalize_entity(getattr(c, "text", None) or getattr(c, "name", ""))
|
|
136
|
+
for c in getattr(res, "entities", [])}
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
return {e for e in ents if e}
|
|
140
|
+
|
|
141
|
+
async def check(self, *, response_text: str, conversation_key: Optional[str],
|
|
142
|
+
mentioned_entities: Sequence[str],
|
|
143
|
+
communication_policy: Optional[
|
|
144
|
+
CommunicationPolicyContextV1
|
|
145
|
+
] = None) -> List[GuardFinding]:
|
|
146
|
+
# The policy is a narrowing input made available to every configured
|
|
147
|
+
# checker. Provenance rules intentionally do not use it as an
|
|
148
|
+
# exemption: even ``owner_explicit`` cannot confer authority here.
|
|
149
|
+
del communication_policy
|
|
150
|
+
if not conversation_key:
|
|
151
|
+
return []
|
|
152
|
+
findings: List[GuardFinding] = []
|
|
153
|
+
for ent in await self._entities_in(response_text, mentioned_entities):
|
|
154
|
+
if ent in self._public:
|
|
155
|
+
continue
|
|
156
|
+
contexts = self._store.contexts_for(ent)
|
|
157
|
+
if not contexts:
|
|
158
|
+
continue # never seen -> can't be a leak
|
|
159
|
+
if any(c["is_public"] for c in contexts):
|
|
160
|
+
continue # public knowledge
|
|
161
|
+
keys = {c["conversation_key"] for c in contexts}
|
|
162
|
+
if conversation_key in keys:
|
|
163
|
+
continue # belongs to this conversation
|
|
164
|
+
findings.append(GuardFinding(
|
|
165
|
+
check="cross_context",
|
|
166
|
+
severity="block",
|
|
167
|
+
reason=f"entity {ent!r} is known only from another private conversation",
|
|
168
|
+
excerpt=f"[{ent}]",
|
|
169
|
+
))
|
|
170
|
+
return findings
|
apsimo/gate/env_risk.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Environment-risk classifier (L1.2) — the auto-ratchet under leveled tom2.
|
|
2
|
+
|
|
3
|
+
``classify()`` grades ONE conversation, as experienced by ONE reader, into
|
|
4
|
+
R0..R3. It is the input that lets the effective-level resolver auto-degrade
|
|
5
|
+
cross-contact rendering the moment the room gets bigger, stranger, or less
|
|
6
|
+
certain — with no human in the loop.
|
|
7
|
+
|
|
8
|
+
R0 owner-private — census ⊆ {owner}, DM, private gateway.
|
|
9
|
+
R1 trusted-private — DM with the owner; reader tier >= trusted, strongly
|
|
10
|
+
resolved; no other non-owner participant.
|
|
11
|
+
R2 known-social — strongly-resolved reader tier >= regular in a DM;
|
|
12
|
+
OR a group where EVERY participant is strongly
|
|
13
|
+
resolved and tier >= trusted. Private gateway.
|
|
14
|
+
R3 open/hostile — the DEFAULT. Any unknown/shadow/peripheral/
|
|
15
|
+
group_guest participant, any unresolved group
|
|
16
|
+
member, a public/embodied/unclassified gateway, an
|
|
17
|
+
unresolved/system/shadow reader, a missing census,
|
|
18
|
+
a missing owner identity, or ANY classifier error.
|
|
19
|
+
|
|
20
|
+
Design pins:
|
|
21
|
+
|
|
22
|
+
* **Monotone.** Rules only RAISE risk: a lower class is granted solely on
|
|
23
|
+
positive, verified evidence; every missing signal leaves the conversation
|
|
24
|
+
at R3.
|
|
25
|
+
* **Fail-closed.** Any exception anywhere (stores, env parsing, lookups)
|
|
26
|
+
returns R3 with a ``classifier-error`` reason — never a lower class.
|
|
27
|
+
* **Deployment-configured gateways.** Colony ships NO gateway names.
|
|
28
|
+
``COLONY_ENV_RISK_GATEWAY_CLASS`` (default empty) maps gateway families to
|
|
29
|
+
``private`` / ``public`` / ``embodied``; an unclassified gateway is treated
|
|
30
|
+
as hostile, so a deployment must explicitly bless its private DM surfaces
|
|
31
|
+
before anything can grade below R3.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import logging
|
|
37
|
+
import os
|
|
38
|
+
from dataclasses import dataclass, field
|
|
39
|
+
from typing import Any, Dict, List, Optional
|
|
40
|
+
|
|
41
|
+
from apsimo.channels.presence import STRONG_METHODS
|
|
42
|
+
from apsimo.identity.participants import SYSTEM_CONTACT_ID
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
R0, R1, R2, R3 = 0, 1, 2, 3
|
|
47
|
+
|
|
48
|
+
_LABELS = {R0: "R0", R1: "R1", R2: "R2", R3: "R3"}
|
|
49
|
+
|
|
50
|
+
#: Trust-tier ordering for floor checks. Anything unlisted ranks 0 (hostile).
|
|
51
|
+
_TIER_RANK = {
|
|
52
|
+
"inner_circle": 5,
|
|
53
|
+
"trusted": 4,
|
|
54
|
+
"regular": 3,
|
|
55
|
+
"acquaintance": 2,
|
|
56
|
+
"group_guest": 2,
|
|
57
|
+
"peripheral": 1,
|
|
58
|
+
"unknown": 0,
|
|
59
|
+
"silenced": 0,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_GATEWAY_CLASSES = ("private", "public", "embodied")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class EnvRisk:
|
|
67
|
+
level: int
|
|
68
|
+
reasons: List[str] = field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def label(self) -> str:
|
|
72
|
+
return _LABELS.get(self.level, "R3")
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
75
|
+
return {"level": self.level, "label": self.label,
|
|
76
|
+
"reasons": list(self.reasons)}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def env_risk_window_hours() -> float:
|
|
80
|
+
"""COLONY_ENV_RISK_WINDOW_HOURS (default 48): census recency window."""
|
|
81
|
+
try:
|
|
82
|
+
v = float(os.environ.get("COLONY_ENV_RISK_WINDOW_HOURS", "48"))
|
|
83
|
+
return v if v > 0 else 48.0
|
|
84
|
+
except (TypeError, ValueError):
|
|
85
|
+
return 48.0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def gateway_class(gateway: str) -> str:
|
|
89
|
+
"""Deployment-declared class for a gateway family, or '' (unclassified).
|
|
90
|
+
|
|
91
|
+
COLONY_ENV_RISK_GATEWAY_CLASS is a comma list of ``gateway:class`` pairs
|
|
92
|
+
(class in private|public|embodied). Malformed entries are ignored — an
|
|
93
|
+
entry that fails to parse simply leaves its gateway unclassified, which
|
|
94
|
+
is the hostile default.
|
|
95
|
+
"""
|
|
96
|
+
g = str(gateway or "").strip().lower()
|
|
97
|
+
if not g:
|
|
98
|
+
return ""
|
|
99
|
+
raw = os.environ.get("COLONY_ENV_RISK_GATEWAY_CLASS", "")
|
|
100
|
+
for pair in raw.split(","):
|
|
101
|
+
if ":" not in pair:
|
|
102
|
+
continue
|
|
103
|
+
name, cls = pair.split(":", 1)
|
|
104
|
+
if name.strip().lower() == g:
|
|
105
|
+
cls = cls.strip().lower()
|
|
106
|
+
return cls if cls in _GATEWAY_CLASSES else ""
|
|
107
|
+
return ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _tier_rank(tier: Any) -> int:
|
|
111
|
+
return _TIER_RANK.get(str(tier or "").strip().lower(), 0)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def _contact_tier(contacts_store: Any, contact_id: str) -> Optional[str]:
|
|
115
|
+
"""The contact's trust tier, or None when the contact does not resolve."""
|
|
116
|
+
c = await contacts_store.get(contact_id)
|
|
117
|
+
if c is None:
|
|
118
|
+
return None
|
|
119
|
+
return str(getattr(c, "trust_tier", "") or "")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def classify(conversation_key: str, reader_contact_id: str, *,
|
|
123
|
+
presence_store: Any, contacts_store: Any,
|
|
124
|
+
owner_id: Optional[str] = None) -> EnvRisk:
|
|
125
|
+
"""Grade one (conversation, reader) pair into R0..R3. Never raises."""
|
|
126
|
+
try:
|
|
127
|
+
return await _classify(conversation_key, reader_contact_id,
|
|
128
|
+
presence_store=presence_store,
|
|
129
|
+
contacts_store=contacts_store,
|
|
130
|
+
owner_id=owner_id)
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
logger.debug("env-risk classification failed (=> R3): %s", exc,
|
|
133
|
+
exc_info=True)
|
|
134
|
+
return EnvRisk(R3, [f"classifier-error:{type(exc).__name__}"])
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def _classify(conversation_key: str, reader_contact_id: str, *,
|
|
138
|
+
presence_store: Any, contacts_store: Any,
|
|
139
|
+
owner_id: Optional[str]) -> EnvRisk:
|
|
140
|
+
reasons: List[str] = []
|
|
141
|
+
conversation_key = str(conversation_key or "").strip()
|
|
142
|
+
reader = str(reader_contact_id or "").strip()
|
|
143
|
+
|
|
144
|
+
# Hard preconditions: each missing signal is terminal (R3).
|
|
145
|
+
if not conversation_key:
|
|
146
|
+
return EnvRisk(R3, ["conversation-key-missing"])
|
|
147
|
+
if not reader or reader == SYSTEM_CONTACT_ID:
|
|
148
|
+
return EnvRisk(R3, ["reader-unresolved-or-system"])
|
|
149
|
+
if presence_store is None:
|
|
150
|
+
return EnvRisk(R3, ["presence-store-missing"])
|
|
151
|
+
if contacts_store is None:
|
|
152
|
+
return EnvRisk(R3, ["contacts-store-missing"])
|
|
153
|
+
|
|
154
|
+
if owner_id is None:
|
|
155
|
+
from apsimo.identity.resolver import get_owner_contact_id
|
|
156
|
+
owner_id = get_owner_contact_id()
|
|
157
|
+
owner = str(owner_id or "").strip()
|
|
158
|
+
if not owner:
|
|
159
|
+
return EnvRisk(R3, ["owner-identity-unset"])
|
|
160
|
+
|
|
161
|
+
gateway = conversation_key.split(":", 1)[0]
|
|
162
|
+
gclass = gateway_class(gateway)
|
|
163
|
+
if gclass != "private":
|
|
164
|
+
return EnvRisk(R3, [f"gateway-class:{gclass or 'unclassified'}"])
|
|
165
|
+
|
|
166
|
+
window = env_risk_window_hours()
|
|
167
|
+
census = presence_store.census(conversation_key, window_hours=window)
|
|
168
|
+
by_id = {str(r.get("contact_id") or ""): r for r in census}
|
|
169
|
+
if reader not in by_id:
|
|
170
|
+
# No verified sighting of the reader in this conversation's window —
|
|
171
|
+
# presence recording is either off or the reader never actually
|
|
172
|
+
# spoke here. Missing signal.
|
|
173
|
+
return EnvRisk(R3, ["reader-not-in-census"])
|
|
174
|
+
|
|
175
|
+
def _strong(cid: str) -> bool:
|
|
176
|
+
return str(by_id[cid].get("method") or "").lower() in STRONG_METHODS
|
|
177
|
+
|
|
178
|
+
if not _strong(reader):
|
|
179
|
+
return EnvRisk(R3, [
|
|
180
|
+
f"reader-resolution-weak:{by_id[reader].get('method') or 'none'}"])
|
|
181
|
+
|
|
182
|
+
non_owner = [cid for cid in by_id if cid != owner]
|
|
183
|
+
is_group = (len(non_owner) > 1
|
|
184
|
+
or any(str(r.get("group_id") or "").strip() for r in census))
|
|
185
|
+
|
|
186
|
+
# R0: an owner-private room — every participant in the window IS the
|
|
187
|
+
# owner, and the reader being rendered to IS the owner.
|
|
188
|
+
if reader == owner and not is_group and set(by_id) <= {owner}:
|
|
189
|
+
return EnvRisk(R0, ["owner-private"])
|
|
190
|
+
|
|
191
|
+
if reader == owner:
|
|
192
|
+
# Owner reading in a room with other people: grade by the room.
|
|
193
|
+
reasons.append("owner-with-company")
|
|
194
|
+
|
|
195
|
+
reader_tier = await _contact_tier(contacts_store, reader)
|
|
196
|
+
if reader_tier is None:
|
|
197
|
+
return EnvRisk(R3, ["reader-contact-unresolvable"])
|
|
198
|
+
|
|
199
|
+
if not is_group and set(non_owner) <= {reader}:
|
|
200
|
+
# A true DM between the owner and the (strongly-resolved) reader.
|
|
201
|
+
if _tier_rank(reader_tier) >= _TIER_RANK["trusted"]:
|
|
202
|
+
return EnvRisk(R1, ["trusted-private-dm"])
|
|
203
|
+
if _tier_rank(reader_tier) >= _TIER_RANK["regular"]:
|
|
204
|
+
return EnvRisk(R2, ["known-social-dm"])
|
|
205
|
+
return EnvRisk(R3, [f"reader-tier-below-regular:{reader_tier}"])
|
|
206
|
+
|
|
207
|
+
if is_group:
|
|
208
|
+
# R2 group: EVERY participant strongly resolved and tier >= trusted
|
|
209
|
+
# (the owner is exempt from the tier floor, not from resolution).
|
|
210
|
+
for cid in by_id:
|
|
211
|
+
if not _strong(cid):
|
|
212
|
+
return EnvRisk(R3, [f"group-member-unresolved:{cid}"])
|
|
213
|
+
if cid == owner:
|
|
214
|
+
continue
|
|
215
|
+
tier = await _contact_tier(contacts_store, cid)
|
|
216
|
+
if tier is None:
|
|
217
|
+
return EnvRisk(R3, [f"group-member-unresolvable:{cid}"])
|
|
218
|
+
if _tier_rank(tier) < _TIER_RANK["trusted"]:
|
|
219
|
+
return EnvRisk(R3, [f"group-member-below-trusted:{cid}"])
|
|
220
|
+
return EnvRisk(R2, ["known-social-group"])
|
|
221
|
+
|
|
222
|
+
# Not a group, but the non-owner set is not just the reader (e.g. a
|
|
223
|
+
# third party was sighted in the window): hostile.
|
|
224
|
+
others = sorted(set(non_owner) - {reader})
|
|
225
|
+
reasons.append("unexpected-co-presence:" + ",".join(others[:5]))
|
|
226
|
+
return EnvRisk(R3, reasons)
|