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,42 @@
|
|
|
1
|
+
"""Shared phone gateway resolution.
|
|
2
|
+
|
|
3
|
+
Replaces the three hardcoded _PHONE_GATEWAYS tuples scattered across
|
|
4
|
+
contacts/store.py, identity/resolver.py, and contacts/world_bridge.py.
|
|
5
|
+
|
|
6
|
+
Reads from the channel store when available; falls back to a built-in
|
|
7
|
+
set so the system works before any channels are registered.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_BUILTIN_PHONE_GATEWAYS = frozenset({
|
|
18
|
+
"imessage", "sms", "signal", "whatsapp",
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
_channel_store_ref: Optional[object] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def set_channel_store_ref(store) -> None:
|
|
25
|
+
global _channel_store_ref
|
|
26
|
+
_channel_store_ref = store
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_phone_gateways() -> frozenset[str]:
|
|
30
|
+
"""Return the set of gateways that use phone-number identity.
|
|
31
|
+
|
|
32
|
+
When a channel store is available, returns the union of registered
|
|
33
|
+
channels with phone_identity_unification=True and the built-in set.
|
|
34
|
+
Without a channel store, returns only the built-in set.
|
|
35
|
+
"""
|
|
36
|
+
if _channel_store_ref is not None:
|
|
37
|
+
try:
|
|
38
|
+
registered = _channel_store_ref.get_phone_gateways()
|
|
39
|
+
return frozenset(registered | _BUILTIN_PHONE_GATEWAYS)
|
|
40
|
+
except Exception:
|
|
41
|
+
logger.debug("Channel store query failed, using built-in set", exc_info=True)
|
|
42
|
+
return _BUILTIN_PHONE_GATEWAYS
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""ConversationPresenceStore — who has been seen in which conversation (L1.1).
|
|
2
|
+
|
|
3
|
+
The conversation participant registry the leveled cross-contact tom2 system
|
|
4
|
+
(docs-level: TOM2 levels) is built on. Every attributed turn records
|
|
5
|
+
``(conversation_key, contact_id, resolution method, group_id)``; downstream
|
|
6
|
+
consumers (the environment-risk classifier, subject-presence exclusion) read a
|
|
7
|
+
windowed census of a conversation.
|
|
8
|
+
|
|
9
|
+
Properties:
|
|
10
|
+
|
|
11
|
+
* **Passive.** Recording is fed from the turns/sync attribution chokepoint
|
|
12
|
+
(after the ParticipantResolver has decided WHO the turn came from) and
|
|
13
|
+
changes nothing about turn processing. ``COLONY_CONV_PRESENCE`` (default
|
|
14
|
+
on) can disable recording entirely.
|
|
15
|
+
* **Identity only.** Rows carry contact ids, resolution methods and opaque
|
|
16
|
+
conversation keys — never message content.
|
|
17
|
+
* **System excluded.** The reserved machine sentinel (``system``) is never
|
|
18
|
+
recorded: machine turns must not shape a conversation's human census.
|
|
19
|
+
* **Reads fail closed.** Read methods PROPAGATE storage errors instead of
|
|
20
|
+
returning an empty (i.e. "nobody here") census — an empty answer from a
|
|
21
|
+
broken store would read as a SAFE signal to the risk classifier. Callers
|
|
22
|
+
must catch and treat any error as "unknown, assume hostile".
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import os
|
|
29
|
+
import sqlite3
|
|
30
|
+
import threading
|
|
31
|
+
from datetime import datetime, timedelta, timezone
|
|
32
|
+
from typing import Any, Dict, List, Optional
|
|
33
|
+
|
|
34
|
+
from apsimo.identity.participants import SYSTEM_CONTACT_ID
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
#: Resolution methods considered STRONG identity evidence: the contact was
|
|
39
|
+
#: matched by a verified handle or arrived pre-resolved as a canonical contact
|
|
40
|
+
#: id. Scoped-name matches (unverified link proposals), shadow contacts and
|
|
41
|
+
#: client-claimed ids are NOT strong.
|
|
42
|
+
STRONG_METHODS = frozenset({"handle", "contact_id"})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def conv_presence_enabled() -> bool:
|
|
46
|
+
"""COLONY_CONV_PRESENCE (default on): passive presence recording."""
|
|
47
|
+
return os.environ.get("COLONY_CONV_PRESENCE", "on").strip().lower() not in (
|
|
48
|
+
"off", "0", "false", "no")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _now() -> datetime:
|
|
52
|
+
return datetime.now(tz=timezone.utc)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ConversationPresenceStore:
|
|
56
|
+
"""SQLite-backed registry of who was seen in which conversation."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, db_path: Optional[str] = None) -> None:
|
|
59
|
+
self._lock = threading.RLock()
|
|
60
|
+
self._conn = sqlite3.connect(db_path or ":memory:",
|
|
61
|
+
check_same_thread=False)
|
|
62
|
+
self._conn.row_factory = sqlite3.Row
|
|
63
|
+
with self._lock:
|
|
64
|
+
self._conn.execute("""
|
|
65
|
+
CREATE TABLE IF NOT EXISTS conversation_presence (
|
|
66
|
+
conversation_key TEXT NOT NULL,
|
|
67
|
+
contact_id TEXT NOT NULL,
|
|
68
|
+
method TEXT NOT NULL DEFAULT '',
|
|
69
|
+
group_id TEXT NOT NULL DEFAULT '',
|
|
70
|
+
first_seen_at TEXT NOT NULL,
|
|
71
|
+
last_seen_at TEXT NOT NULL,
|
|
72
|
+
turns INTEGER NOT NULL DEFAULT 0,
|
|
73
|
+
PRIMARY KEY (conversation_key, contact_id)
|
|
74
|
+
)""")
|
|
75
|
+
self._conn.execute(
|
|
76
|
+
"CREATE INDEX IF NOT EXISTS idx_conv_presence_contact "
|
|
77
|
+
"ON conversation_presence(contact_id)")
|
|
78
|
+
self._conn.commit()
|
|
79
|
+
|
|
80
|
+
# -- writes -----------------------------------------------------------
|
|
81
|
+
def record(self, conversation_key: str, contact_id: str, *,
|
|
82
|
+
method: str = "", group_id: str = "") -> bool:
|
|
83
|
+
"""Record one sighting of ``contact_id`` in ``conversation_key``.
|
|
84
|
+
|
|
85
|
+
Returns True when a row was written/refreshed. The system sentinel
|
|
86
|
+
and empty ids are silently skipped (never an error — the chokepoint
|
|
87
|
+
must not care); the COLONY_CONV_PRESENCE gate turns the whole write
|
|
88
|
+
into a no-op. ``method`` stores the LATEST resolution method for the
|
|
89
|
+
pair: a weak latest sighting correctly downgrades the row, and a row
|
|
90
|
+
can only read as strong when the most recent turn actually resolved
|
|
91
|
+
strongly (by verified handle / canonical contact id).
|
|
92
|
+
"""
|
|
93
|
+
if not conv_presence_enabled():
|
|
94
|
+
return False
|
|
95
|
+
conversation_key = str(conversation_key or "").strip()
|
|
96
|
+
contact_id = str(contact_id or "").strip()
|
|
97
|
+
if not conversation_key or not contact_id:
|
|
98
|
+
return False
|
|
99
|
+
if contact_id == SYSTEM_CONTACT_ID:
|
|
100
|
+
return False
|
|
101
|
+
now = _now().isoformat()
|
|
102
|
+
method = str(method or "").strip().lower()
|
|
103
|
+
group_id = str(group_id or "").strip()
|
|
104
|
+
with self._lock:
|
|
105
|
+
self._conn.execute(
|
|
106
|
+
"""INSERT INTO conversation_presence
|
|
107
|
+
(conversation_key, contact_id, method, group_id,
|
|
108
|
+
first_seen_at, last_seen_at, turns)
|
|
109
|
+
VALUES (?,?,?,?,?,?,1)
|
|
110
|
+
ON CONFLICT(conversation_key, contact_id) DO UPDATE SET
|
|
111
|
+
method=excluded.method,
|
|
112
|
+
group_id=CASE WHEN excluded.group_id != ''
|
|
113
|
+
THEN excluded.group_id ELSE group_id END,
|
|
114
|
+
last_seen_at=excluded.last_seen_at,
|
|
115
|
+
turns=turns+1""",
|
|
116
|
+
(conversation_key, contact_id, method, group_id, now, now))
|
|
117
|
+
self._conn.commit()
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
# -- reads (fail closed: errors PROPAGATE) ------------------------------
|
|
121
|
+
def census(self, conversation_key: str,
|
|
122
|
+
window_hours: float = 48.0) -> List[Dict[str, Any]]:
|
|
123
|
+
"""Participants seen in this conversation within the window.
|
|
124
|
+
|
|
125
|
+
Raises on any storage error — callers MUST treat an exception as
|
|
126
|
+
"census unknown" (hostile), never as "empty room".
|
|
127
|
+
"""
|
|
128
|
+
cutoff = (_now() - timedelta(hours=float(window_hours))).isoformat()
|
|
129
|
+
with self._lock:
|
|
130
|
+
rows = self._conn.execute(
|
|
131
|
+
"SELECT * FROM conversation_presence "
|
|
132
|
+
"WHERE conversation_key=? AND last_seen_at >= ? "
|
|
133
|
+
"ORDER BY last_seen_at DESC",
|
|
134
|
+
(str(conversation_key or ""), cutoff)).fetchall()
|
|
135
|
+
return [dict(r) for r in rows]
|
|
136
|
+
|
|
137
|
+
def is_present(self, conversation_key: str, contact_id: str,
|
|
138
|
+
window_hours: float = 48.0) -> bool:
|
|
139
|
+
"""True when the contact was seen in the conversation within the
|
|
140
|
+
window. Raises on storage errors (see census)."""
|
|
141
|
+
cutoff = (_now() - timedelta(hours=float(window_hours))).isoformat()
|
|
142
|
+
with self._lock:
|
|
143
|
+
row = self._conn.execute(
|
|
144
|
+
"SELECT 1 FROM conversation_presence "
|
|
145
|
+
"WHERE conversation_key=? AND contact_id=? AND last_seen_at >= ?",
|
|
146
|
+
(str(conversation_key or ""), str(contact_id or ""),
|
|
147
|
+
cutoff)).fetchone()
|
|
148
|
+
return row is not None
|
|
149
|
+
|
|
150
|
+
def cooccurred(self, contact_a: str, contact_b: str,
|
|
151
|
+
within_days: float = 30.0) -> bool:
|
|
152
|
+
"""True when both contacts were seen in at least ONE common
|
|
153
|
+
conversation within the window (mutual-knowledge evidence: they
|
|
154
|
+
demonstrably know of each other). Raises on storage errors."""
|
|
155
|
+
a = str(contact_a or "").strip()
|
|
156
|
+
b = str(contact_b or "").strip()
|
|
157
|
+
if not a or not b or a == b:
|
|
158
|
+
return False
|
|
159
|
+
cutoff = (_now() - timedelta(days=float(within_days))).isoformat()
|
|
160
|
+
with self._lock:
|
|
161
|
+
row = self._conn.execute(
|
|
162
|
+
"""SELECT 1 FROM conversation_presence pa
|
|
163
|
+
JOIN conversation_presence pb
|
|
164
|
+
ON pa.conversation_key = pb.conversation_key
|
|
165
|
+
WHERE pa.contact_id=? AND pb.contact_id=?
|
|
166
|
+
AND pa.last_seen_at >= ? AND pb.last_seen_at >= ?
|
|
167
|
+
LIMIT 1""", (a, b, cutoff, cutoff)).fetchone()
|
|
168
|
+
return row is not None
|
|
169
|
+
|
|
170
|
+
def counts(self) -> Dict[str, Any]:
|
|
171
|
+
"""Aggregate observability (numbers only)."""
|
|
172
|
+
with self._lock:
|
|
173
|
+
convs = self._conn.execute(
|
|
174
|
+
"SELECT COUNT(DISTINCT conversation_key) n "
|
|
175
|
+
"FROM conversation_presence").fetchone()
|
|
176
|
+
contacts = self._conn.execute(
|
|
177
|
+
"SELECT COUNT(DISTINCT contact_id) n "
|
|
178
|
+
"FROM conversation_presence").fetchone()
|
|
179
|
+
rows = self._conn.execute(
|
|
180
|
+
"SELECT COUNT(*) n FROM conversation_presence").fetchone()
|
|
181
|
+
return {"rows": int(rows["n"]), "conversations": int(convs["n"]),
|
|
182
|
+
"contacts": int(contacts["n"])}
|
|
183
|
+
|
|
184
|
+
def close(self) -> None:
|
|
185
|
+
try:
|
|
186
|
+
self._conn.close()
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Channel registration API -- /v1/channels/ endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ipaddress
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
from fastapi import APIRouter, Header, HTTPException, status
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from apsimo.channels.manifest import ChannelManifest
|
|
15
|
+
from apsimo.channels.store import ChannelStore
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
router = APIRouter(prefix="/v1/channels", tags=["channels"])
|
|
20
|
+
|
|
21
|
+
_channel_store: Optional[ChannelStore] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def set_channel_store(store: ChannelStore) -> None:
|
|
25
|
+
global _channel_store
|
|
26
|
+
_channel_store = store
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_channel_store() -> Optional[ChannelStore]:
|
|
30
|
+
return _channel_store
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _require_store() -> ChannelStore:
|
|
34
|
+
if _channel_store is None:
|
|
35
|
+
raise HTTPException(
|
|
36
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
37
|
+
detail="Channel store not initialized",
|
|
38
|
+
)
|
|
39
|
+
return _channel_store
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── Request / Response models ────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RegisterResponse(BaseModel):
|
|
46
|
+
channel_key: str
|
|
47
|
+
registered_at: str
|
|
48
|
+
channel_token: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ChannelInfo(BaseModel):
|
|
52
|
+
channel_key: str
|
|
53
|
+
display_name: str
|
|
54
|
+
gateway_family: str
|
|
55
|
+
status: str
|
|
56
|
+
registered_at: str
|
|
57
|
+
last_seen_at: Optional[str] = None
|
|
58
|
+
supports_media: bool = False
|
|
59
|
+
supports_reactions: bool = False
|
|
60
|
+
supports_voice: bool = False
|
|
61
|
+
supports_rich_text: bool = False
|
|
62
|
+
max_message_length: Optional[int] = None
|
|
63
|
+
phone_identity_unification: bool = False
|
|
64
|
+
provides_channel_id: bool = False
|
|
65
|
+
delivery_protocol: str = "hermes"
|
|
66
|
+
delivery_aliases: list[str] = []
|
|
67
|
+
home_chat_id: Optional[str] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── Webhook validation ───────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_PRIVATE_NETWORKS = [
|
|
74
|
+
ipaddress.ip_network("10.0.0.0/8"),
|
|
75
|
+
ipaddress.ip_network("172.16.0.0/12"),
|
|
76
|
+
ipaddress.ip_network("192.168.0.0/16"),
|
|
77
|
+
ipaddress.ip_network("169.254.0.0/16"),
|
|
78
|
+
ipaddress.ip_network("127.0.0.0/8"),
|
|
79
|
+
ipaddress.ip_network("::1/128"),
|
|
80
|
+
ipaddress.ip_network("fd00::/8"),
|
|
81
|
+
ipaddress.ip_network("fe80::/10"),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _validate_webhook(url: str) -> None:
|
|
86
|
+
parsed = urlparse(url)
|
|
87
|
+
if parsed.scheme not in ("http", "https"):
|
|
88
|
+
raise HTTPException(
|
|
89
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
90
|
+
detail=f"Webhook URL must be http or https, got '{parsed.scheme}'",
|
|
91
|
+
)
|
|
92
|
+
if not parsed.hostname:
|
|
93
|
+
raise HTTPException(
|
|
94
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
95
|
+
detail="Webhook URL has no hostname",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
allow_private = os.environ.get(
|
|
99
|
+
"COLONY_ALLOW_PRIVATE_WEBHOOKS", ""
|
|
100
|
+
).lower() in ("true", "1", "yes")
|
|
101
|
+
|
|
102
|
+
if not allow_private:
|
|
103
|
+
try:
|
|
104
|
+
addr = ipaddress.ip_address(parsed.hostname)
|
|
105
|
+
if any(addr in net for net in _PRIVATE_NETWORKS):
|
|
106
|
+
raise HTTPException(
|
|
107
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
108
|
+
detail=(
|
|
109
|
+
f"Webhook URL resolves to private address {addr}. "
|
|
110
|
+
"Set COLONY_ALLOW_PRIVATE_WEBHOOKS=true to allow."
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
except ValueError:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ── Endpoints ────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@router.post("/register", status_code=status.HTTP_201_CREATED)
|
|
121
|
+
async def register_channel(
|
|
122
|
+
manifest: ChannelManifest,
|
|
123
|
+
x_channel_token: Optional[str] = Header(None),
|
|
124
|
+
) -> RegisterResponse:
|
|
125
|
+
store = _require_store()
|
|
126
|
+
|
|
127
|
+
if manifest.delivery_webhook:
|
|
128
|
+
_validate_webhook(manifest.delivery_webhook)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
registered = store.register(manifest, channel_token=x_channel_token)
|
|
132
|
+
except ValueError as exc:
|
|
133
|
+
raise HTTPException(
|
|
134
|
+
status_code=status.HTTP_409_CONFLICT, detail=str(exc)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return RegisterResponse(
|
|
138
|
+
channel_key=registered.channel_key,
|
|
139
|
+
registered_at=registered.registered_at,
|
|
140
|
+
channel_token=registered.channel_token,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@router.get("", response_model=list[ChannelInfo])
|
|
145
|
+
async def list_channels() -> list[ChannelInfo]:
|
|
146
|
+
store = _require_store()
|
|
147
|
+
return [_to_info(ch) for ch in store.list_all()]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@router.get("/{channel_key}", response_model=ChannelInfo)
|
|
151
|
+
async def get_channel(channel_key: str) -> ChannelInfo:
|
|
152
|
+
store = _require_store()
|
|
153
|
+
ch = store.get(channel_key)
|
|
154
|
+
if ch is None:
|
|
155
|
+
raise HTTPException(
|
|
156
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
157
|
+
detail=f"Channel '{channel_key}' not found",
|
|
158
|
+
)
|
|
159
|
+
return _to_info(ch)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@router.put("/{channel_key}", response_model=ChannelInfo)
|
|
163
|
+
async def update_channel(
|
|
164
|
+
channel_key: str,
|
|
165
|
+
manifest: ChannelManifest,
|
|
166
|
+
x_channel_token: str = Header(...),
|
|
167
|
+
) -> ChannelInfo:
|
|
168
|
+
store = _require_store()
|
|
169
|
+
|
|
170
|
+
if manifest.channel_key != channel_key:
|
|
171
|
+
raise HTTPException(
|
|
172
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
173
|
+
detail="channel_key in body must match URL path",
|
|
174
|
+
)
|
|
175
|
+
if manifest.delivery_webhook:
|
|
176
|
+
_validate_webhook(manifest.delivery_webhook)
|
|
177
|
+
|
|
178
|
+
if not store.verify_token(channel_key, x_channel_token):
|
|
179
|
+
raise HTTPException(
|
|
180
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
181
|
+
detail="Invalid channel token",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
registered = store.register(manifest, channel_token=x_channel_token)
|
|
186
|
+
except ValueError as exc:
|
|
187
|
+
raise HTTPException(
|
|
188
|
+
status_code=status.HTTP_409_CONFLICT, detail=str(exc)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return _to_info(registered)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@router.delete("/{channel_key}", status_code=status.HTTP_204_NO_CONTENT)
|
|
195
|
+
async def delete_channel(
|
|
196
|
+
channel_key: str,
|
|
197
|
+
x_channel_token: str = Header(...),
|
|
198
|
+
) -> None:
|
|
199
|
+
store = _require_store()
|
|
200
|
+
|
|
201
|
+
if not store.verify_token(channel_key, x_channel_token):
|
|
202
|
+
raise HTTPException(
|
|
203
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
204
|
+
detail="Invalid channel token",
|
|
205
|
+
)
|
|
206
|
+
if not store.revoke(channel_key):
|
|
207
|
+
raise HTTPException(
|
|
208
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
209
|
+
detail=f"Channel '{channel_key}' not found",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ── Helpers ──────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _to_info(ch) -> ChannelInfo:
|
|
217
|
+
m = ch.manifest
|
|
218
|
+
return ChannelInfo(
|
|
219
|
+
channel_key=ch.channel_key,
|
|
220
|
+
display_name=ch.display_name,
|
|
221
|
+
gateway_family=ch.gateway_family,
|
|
222
|
+
status=ch.status,
|
|
223
|
+
registered_at=ch.registered_at,
|
|
224
|
+
last_seen_at=ch.last_seen_at,
|
|
225
|
+
supports_media=m.supports_media,
|
|
226
|
+
supports_reactions=m.supports_reactions,
|
|
227
|
+
supports_voice=m.supports_voice,
|
|
228
|
+
supports_rich_text=m.supports_rich_text,
|
|
229
|
+
max_message_length=m.max_message_length,
|
|
230
|
+
phone_identity_unification=m.phone_identity_unification,
|
|
231
|
+
provides_channel_id=m.provides_channel_id,
|
|
232
|
+
delivery_protocol=m.delivery_protocol,
|
|
233
|
+
delivery_aliases=m.delivery_aliases,
|
|
234
|
+
home_chat_id=m.home_chat_id,
|
|
235
|
+
)
|
apsimo/channels/store.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Channel registration store -- backed by colony-channels.db.
|
|
2
|
+
|
|
3
|
+
Tracks which channels are registered with this Colony instance, their
|
|
4
|
+
capabilities, and authentication tokens.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import hmac
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import secrets
|
|
14
|
+
import sqlite3
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from apsimo.channels.manifest import ChannelManifest
|
|
21
|
+
from apsimo.migrations import run_migrations_sync
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
_MIGRATIONS_DIR = Path(__file__).parent / "migrations"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class RegisteredChannel:
|
|
30
|
+
channel_key: str
|
|
31
|
+
display_name: str
|
|
32
|
+
gateway_family: str
|
|
33
|
+
manifest: ChannelManifest
|
|
34
|
+
channel_token: str
|
|
35
|
+
registered_at: str
|
|
36
|
+
last_seen_at: Optional[str]
|
|
37
|
+
status: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ChannelStore:
|
|
41
|
+
"""SQLite-backed channel registration store."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, db_path: str = ":memory:") -> None:
|
|
44
|
+
self._db_path = db_path
|
|
45
|
+
self._conn: Optional[sqlite3.Connection] = None
|
|
46
|
+
|
|
47
|
+
def connect(self) -> None:
|
|
48
|
+
if self._db_path != ":memory:":
|
|
49
|
+
Path(self._db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
|
|
51
|
+
self._conn.row_factory = sqlite3.Row
|
|
52
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
53
|
+
self._conn.execute("PRAGMA foreign_keys=ON")
|
|
54
|
+
run_migrations_sync(self._conn, _MIGRATIONS_DIR)
|
|
55
|
+
|
|
56
|
+
def close(self) -> None:
|
|
57
|
+
if self._conn:
|
|
58
|
+
self._conn.close()
|
|
59
|
+
self._conn = None
|
|
60
|
+
|
|
61
|
+
def _require_conn(self) -> sqlite3.Connection:
|
|
62
|
+
if not self._conn:
|
|
63
|
+
raise RuntimeError("ChannelStore not connected")
|
|
64
|
+
return self._conn
|
|
65
|
+
|
|
66
|
+
# ── Registration ─────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
def register(
|
|
69
|
+
self,
|
|
70
|
+
manifest: ChannelManifest,
|
|
71
|
+
*,
|
|
72
|
+
channel_token: Optional[str] = None,
|
|
73
|
+
) -> RegisteredChannel:
|
|
74
|
+
"""Register a new channel or update an existing one.
|
|
75
|
+
|
|
76
|
+
If the channel_key already exists and the provided token matches,
|
|
77
|
+
the registration is updated (upsert). If no token is provided for
|
|
78
|
+
a new registration, one is generated.
|
|
79
|
+
|
|
80
|
+
Raises ValueError on token mismatch (409 at the API layer).
|
|
81
|
+
"""
|
|
82
|
+
conn = self._require_conn()
|
|
83
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
84
|
+
|
|
85
|
+
existing = self.get(manifest.channel_key)
|
|
86
|
+
|
|
87
|
+
if existing is not None:
|
|
88
|
+
if not channel_token or not self.verify_token(
|
|
89
|
+
manifest.channel_key, channel_token
|
|
90
|
+
):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Channel '{manifest.channel_key}' already registered; "
|
|
93
|
+
"provide the correct channel_token to update"
|
|
94
|
+
)
|
|
95
|
+
conn.execute(
|
|
96
|
+
"""UPDATE channels
|
|
97
|
+
SET display_name = ?, gateway_family = ?,
|
|
98
|
+
manifest_json = ?, last_seen_at = ?
|
|
99
|
+
WHERE channel_key = ?""",
|
|
100
|
+
(
|
|
101
|
+
manifest.display_name,
|
|
102
|
+
manifest.gateway_family,
|
|
103
|
+
manifest.model_dump_json(),
|
|
104
|
+
now,
|
|
105
|
+
manifest.channel_key,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
conn.commit()
|
|
109
|
+
return self.get(manifest.channel_key) # type: ignore[return-value]
|
|
110
|
+
|
|
111
|
+
token = channel_token or _generate_token()
|
|
112
|
+
token_hash = _hash_token(token)
|
|
113
|
+
|
|
114
|
+
conn.execute(
|
|
115
|
+
"""INSERT INTO channels
|
|
116
|
+
(channel_key, display_name, gateway_family,
|
|
117
|
+
manifest_json, channel_token, registered_at, last_seen_at, status)
|
|
118
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')""",
|
|
119
|
+
(
|
|
120
|
+
manifest.channel_key,
|
|
121
|
+
manifest.display_name,
|
|
122
|
+
manifest.gateway_family,
|
|
123
|
+
manifest.model_dump_json(),
|
|
124
|
+
token_hash,
|
|
125
|
+
now,
|
|
126
|
+
now,
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
conn.commit()
|
|
130
|
+
|
|
131
|
+
registered = self.get(manifest.channel_key)
|
|
132
|
+
assert registered is not None
|
|
133
|
+
registered.channel_token = token
|
|
134
|
+
return registered
|
|
135
|
+
|
|
136
|
+
# ── Queries ──────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
def get(self, channel_key: str) -> Optional[RegisteredChannel]:
|
|
139
|
+
conn = self._require_conn()
|
|
140
|
+
cur = conn.execute(
|
|
141
|
+
"SELECT * FROM channels WHERE channel_key = ?", (channel_key,)
|
|
142
|
+
)
|
|
143
|
+
row = cur.fetchone()
|
|
144
|
+
return _row_to_channel(row) if row else None
|
|
145
|
+
|
|
146
|
+
def list_active(self) -> list[RegisteredChannel]:
|
|
147
|
+
conn = self._require_conn()
|
|
148
|
+
cur = conn.execute(
|
|
149
|
+
"SELECT * FROM channels WHERE status = 'active' ORDER BY channel_key"
|
|
150
|
+
)
|
|
151
|
+
return [_row_to_channel(row) for row in cur.fetchall()]
|
|
152
|
+
|
|
153
|
+
def list_all(self) -> list[RegisteredChannel]:
|
|
154
|
+
conn = self._require_conn()
|
|
155
|
+
cur = conn.execute("SELECT * FROM channels ORDER BY channel_key")
|
|
156
|
+
return [_row_to_channel(row) for row in cur.fetchall()]
|
|
157
|
+
|
|
158
|
+
def get_phone_gateways(self) -> set[str]:
|
|
159
|
+
"""Return channel_keys with phone_identity_unification enabled."""
|
|
160
|
+
return {
|
|
161
|
+
ch.channel_key
|
|
162
|
+
for ch in self.list_active()
|
|
163
|
+
if ch.manifest.phone_identity_unification
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# ── Mutations ────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
def touch(self, channel_key: str) -> None:
|
|
169
|
+
"""Update last_seen_at for the given channel."""
|
|
170
|
+
conn = self._require_conn()
|
|
171
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
172
|
+
conn.execute(
|
|
173
|
+
"UPDATE channels SET last_seen_at = ? WHERE channel_key = ?",
|
|
174
|
+
(now, channel_key),
|
|
175
|
+
)
|
|
176
|
+
conn.commit()
|
|
177
|
+
|
|
178
|
+
def revoke(self, channel_key: str) -> bool:
|
|
179
|
+
conn = self._require_conn()
|
|
180
|
+
cur = conn.execute(
|
|
181
|
+
"UPDATE channels SET status = 'revoked' WHERE channel_key = ?",
|
|
182
|
+
(channel_key,),
|
|
183
|
+
)
|
|
184
|
+
conn.commit()
|
|
185
|
+
return cur.rowcount > 0
|
|
186
|
+
|
|
187
|
+
def delete(self, channel_key: str) -> bool:
|
|
188
|
+
conn = self._require_conn()
|
|
189
|
+
cur = conn.execute(
|
|
190
|
+
"DELETE FROM channels WHERE channel_key = ?", (channel_key,)
|
|
191
|
+
)
|
|
192
|
+
conn.commit()
|
|
193
|
+
return cur.rowcount > 0
|
|
194
|
+
|
|
195
|
+
# ── Auth ─────────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
def verify_token(self, channel_key: str, token: str) -> bool:
|
|
198
|
+
conn = self._require_conn()
|
|
199
|
+
cur = conn.execute(
|
|
200
|
+
"SELECT channel_token FROM channels WHERE channel_key = ?",
|
|
201
|
+
(channel_key,),
|
|
202
|
+
)
|
|
203
|
+
row = cur.fetchone()
|
|
204
|
+
if not row:
|
|
205
|
+
return False
|
|
206
|
+
return hmac.compare_digest(row["channel_token"], _hash_token(token))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ── Helpers ──────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _generate_token() -> str:
|
|
213
|
+
return "ch_" + secrets.token_urlsafe(32)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _hash_token(token: str) -> str:
|
|
217
|
+
return hashlib.sha256(token.encode()).hexdigest()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _row_to_channel(row: sqlite3.Row) -> RegisteredChannel:
|
|
221
|
+
manifest = ChannelManifest.model_validate_json(row["manifest_json"])
|
|
222
|
+
return RegisteredChannel(
|
|
223
|
+
channel_key=row["channel_key"],
|
|
224
|
+
display_name=row["display_name"],
|
|
225
|
+
gateway_family=row["gateway_family"],
|
|
226
|
+
manifest=manifest,
|
|
227
|
+
channel_token=row["channel_token"],
|
|
228
|
+
registered_at=row["registered_at"],
|
|
229
|
+
last_seen_at=row["last_seen_at"],
|
|
230
|
+
status=row["status"],
|
|
231
|
+
)
|