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,268 @@
|
|
|
1
|
+
"""DeliveryRateLimiter — per-person rate limiting for proactive messages.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
- Max 3 proactive messages per day per person
|
|
5
|
+
- No proactive messages between 22:00–08:00 local time (unless critical)
|
|
6
|
+
- At least 2 hours between proactive messages to the same person
|
|
7
|
+
- User can disable proactive messaging entirely (checked externally)
|
|
8
|
+
|
|
9
|
+
Counts can optionally be persisted to a small SQLite file so restarts don't
|
|
10
|
+
let a crashloop bypass the daily cap. Persistence is opt-in via ``db_path``;
|
|
11
|
+
when ``db_path`` is None the limiter stays purely in-memory (used in tests).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from contextlib import closing
|
|
17
|
+
import logging
|
|
18
|
+
import sqlite3
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
from datetime import datetime, timedelta, timezone
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, List, Optional
|
|
23
|
+
|
|
24
|
+
from apsimo.util.quiet_hours import in_quiet_window
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
_MAX_PER_DAY = 3
|
|
29
|
+
_COOLDOWN_HOURS = 2
|
|
30
|
+
_QUIET_START_HOUR = 22
|
|
31
|
+
_QUIET_END_HOUR = 8
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _resolve_tz():
|
|
35
|
+
"""Owner-local timezone for quiet hours: COLONY_TIMEZONE, else system, else UTC."""
|
|
36
|
+
import os
|
|
37
|
+
name = os.environ.get("COLONY_TIMEZONE", "").strip()
|
|
38
|
+
if name:
|
|
39
|
+
try:
|
|
40
|
+
from zoneinfo import ZoneInfo
|
|
41
|
+
return ZoneInfo(name)
|
|
42
|
+
except Exception:
|
|
43
|
+
logger.warning("rate_limiter: invalid COLONY_TIMEZONE %r, using system tz", name)
|
|
44
|
+
try:
|
|
45
|
+
local = datetime.now().astimezone().tzinfo
|
|
46
|
+
if local is not None:
|
|
47
|
+
return local
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
return timezone.utc
|
|
51
|
+
# Deliveries older than this many days are pruned at startup; nothing in the
|
|
52
|
+
# rate-limit logic needs more than ~48h of history.
|
|
53
|
+
_HISTORY_RETENTION_DAYS = 3
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DeliveryRateLimiter:
|
|
57
|
+
"""Per-person rate limiter for proactive message delivery.
|
|
58
|
+
|
|
59
|
+
Tracks delivery timestamps in memory for fast ``can_deliver`` checks, and
|
|
60
|
+
(optionally) mirrors them to a SQLite log so counts survive restarts.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
max_per_day: int = _MAX_PER_DAY,
|
|
66
|
+
cooldown_hours: int = _COOLDOWN_HOURS,
|
|
67
|
+
quiet_start_hour: int = _QUIET_START_HOUR,
|
|
68
|
+
quiet_end_hour: int = _QUIET_END_HOUR,
|
|
69
|
+
metrics: Optional[Any] = None,
|
|
70
|
+
db_path: Optional[Path] = None,
|
|
71
|
+
cap_provider: Optional[Any] = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
self._max_per_day = max_per_day
|
|
74
|
+
# Adaptive cap (Amendment 1.6): an optional callable(base:int)->int
|
|
75
|
+
# (the trust engine) that EARNS the daily cap upward with a proven
|
|
76
|
+
# delivery track record. Bounded by the provider; base is the floor
|
|
77
|
+
# semantics owners already know. Failure falls back to base.
|
|
78
|
+
self._cap_provider = cap_provider
|
|
79
|
+
self._cooldown = timedelta(hours=cooldown_hours)
|
|
80
|
+
self._quiet_start = quiet_start_hour
|
|
81
|
+
self._quiet_end = quiet_end_hour
|
|
82
|
+
self._metrics = metrics
|
|
83
|
+
# Quiet hours + the daily bucket are evaluated in the OWNER's local
|
|
84
|
+
# timezone (22:00-08:00 must mean their night, not a UTC window).
|
|
85
|
+
self._tz = _resolve_tz()
|
|
86
|
+
|
|
87
|
+
# person_id → list of delivery timestamps (UTC) today
|
|
88
|
+
self._daily_counts: Dict[str, List[datetime]] = defaultdict(list)
|
|
89
|
+
# person_id → last delivery timestamp (UTC)
|
|
90
|
+
self._last_delivery: Dict[str, Optional[datetime]] = defaultdict(lambda: None)
|
|
91
|
+
self._today = datetime.now(self._tz).date()
|
|
92
|
+
|
|
93
|
+
# Frustration back-off: person_id → probability (updated by autonomy loop)
|
|
94
|
+
self._frustration_cache: Dict[str, float] = {}
|
|
95
|
+
|
|
96
|
+
# Optional persistence. Failing to init the DB must not take the
|
|
97
|
+
# limiter offline — fall back to pure in-memory with a warning.
|
|
98
|
+
self._db_path: Optional[Path] = Path(db_path) if db_path else None
|
|
99
|
+
if self._db_path is not None:
|
|
100
|
+
try:
|
|
101
|
+
self._init_db()
|
|
102
|
+
self._reload_from_db()
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
logger.warning(
|
|
105
|
+
"rate_limiter: persistence disabled — %s: %s",
|
|
106
|
+
type(exc).__name__, exc,
|
|
107
|
+
)
|
|
108
|
+
self._db_path = None
|
|
109
|
+
|
|
110
|
+
def can_deliver(self, person_id: str, urgency: float = 0.5) -> tuple[bool, str]:
|
|
111
|
+
"""Check if a proactive message can be delivered to person_id.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
person_id: Recipient identifier
|
|
115
|
+
urgency: Message urgency (0-1). Messages with urgency >= 0.9 bypass quiet hours.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
(allowed: bool, reason: str)
|
|
119
|
+
"""
|
|
120
|
+
self._reset_if_new_day()
|
|
121
|
+
|
|
122
|
+
# Quiet hours check (bypassed for critical messages)
|
|
123
|
+
if urgency < 0.9 and self._in_quiet_hours():
|
|
124
|
+
return False, "quiet_hours"
|
|
125
|
+
|
|
126
|
+
# Frustration back-off (bypassed for critical messages)
|
|
127
|
+
if urgency < 0.9:
|
|
128
|
+
frust = self._frustration_cache.get(person_id, 0.0)
|
|
129
|
+
if frust > 0.5:
|
|
130
|
+
if self._metrics is not None:
|
|
131
|
+
try:
|
|
132
|
+
self._metrics.record_sentiment_suppression()
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
return False, f"person_frustrated (p={frust:.0%})"
|
|
136
|
+
|
|
137
|
+
# Daily limit (adaptive when a trust cap provider is wired)
|
|
138
|
+
effective_max = self._max_per_day
|
|
139
|
+
if self._cap_provider is not None:
|
|
140
|
+
try:
|
|
141
|
+
effective_max = max(1, int(self._cap_provider(self._max_per_day)))
|
|
142
|
+
except Exception:
|
|
143
|
+
effective_max = self._max_per_day
|
|
144
|
+
count = len(self._daily_counts[person_id])
|
|
145
|
+
if count >= effective_max:
|
|
146
|
+
return False, f"daily_limit_reached ({count}/{effective_max})"
|
|
147
|
+
|
|
148
|
+
# Cooldown
|
|
149
|
+
last = self._last_delivery[person_id]
|
|
150
|
+
if last is not None:
|
|
151
|
+
elapsed = datetime.now(timezone.utc) - last
|
|
152
|
+
if elapsed < self._cooldown:
|
|
153
|
+
remaining = int((self._cooldown - elapsed).total_seconds()) // 60
|
|
154
|
+
return False, f"cooldown_active ({remaining}m remaining)"
|
|
155
|
+
|
|
156
|
+
return True, "ok"
|
|
157
|
+
|
|
158
|
+
def record_delivery(self, person_id: str) -> None:
|
|
159
|
+
"""Mark that a proactive message was successfully delivered."""
|
|
160
|
+
self._reset_if_new_day()
|
|
161
|
+
now = datetime.now(timezone.utc)
|
|
162
|
+
self._daily_counts[person_id].append(now)
|
|
163
|
+
self._last_delivery[person_id] = now
|
|
164
|
+
if self._db_path is not None:
|
|
165
|
+
try:
|
|
166
|
+
with closing(sqlite3.connect(self._db_path)) as conn, conn:
|
|
167
|
+
conn.execute(
|
|
168
|
+
"INSERT INTO delivery_log (person_id, delivered_at) VALUES (?, ?)",
|
|
169
|
+
(person_id, now.isoformat()),
|
|
170
|
+
)
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
logger.warning(
|
|
173
|
+
"rate_limiter: failed to persist delivery for %s: %s",
|
|
174
|
+
person_id, exc,
|
|
175
|
+
)
|
|
176
|
+
logger.debug(
|
|
177
|
+
"Delivery recorded for %s (count today: %d)",
|
|
178
|
+
person_id,
|
|
179
|
+
len(self._daily_counts[person_id]),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def update_frustration(self, person_id: str, probability: float) -> None:
|
|
183
|
+
"""Update cached frustration probability for a person.
|
|
184
|
+
|
|
185
|
+
Called by the autonomy loop after running the state estimator.
|
|
186
|
+
Values above 0.5 will suppress non-critical deliveries.
|
|
187
|
+
"""
|
|
188
|
+
self._frustration_cache[person_id] = probability
|
|
189
|
+
|
|
190
|
+
def daily_count(self, person_id: str) -> int:
|
|
191
|
+
"""Return number of proactive deliveries today for a person."""
|
|
192
|
+
self._reset_if_new_day()
|
|
193
|
+
return len(self._daily_counts[person_id])
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------
|
|
196
|
+
# Helpers
|
|
197
|
+
# ------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def _reset_if_new_day(self) -> None:
|
|
200
|
+
"""Reset daily counters when the UTC date rolls over."""
|
|
201
|
+
today = datetime.now(self._tz).date()
|
|
202
|
+
if today != self._today:
|
|
203
|
+
self._daily_counts.clear()
|
|
204
|
+
self._today = today
|
|
205
|
+
|
|
206
|
+
def _in_quiet_hours(self) -> bool:
|
|
207
|
+
"""Return True if current time is within quiet hours (owner-local tz)."""
|
|
208
|
+
now = datetime.now(self._tz)
|
|
209
|
+
return in_quiet_window(
|
|
210
|
+
now.hour * 60 + now.minute,
|
|
211
|
+
self._quiet_start * 60,
|
|
212
|
+
self._quiet_end * 60,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
# Persistence
|
|
217
|
+
# ------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
def _init_db(self) -> None:
|
|
220
|
+
"""Create the delivery-log table on first use and prune stale rows."""
|
|
221
|
+
assert self._db_path is not None
|
|
222
|
+
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
223
|
+
with closing(sqlite3.connect(self._db_path)) as conn, conn:
|
|
224
|
+
conn.execute(
|
|
225
|
+
"CREATE TABLE IF NOT EXISTS delivery_log ("
|
|
226
|
+
" person_id TEXT NOT NULL,"
|
|
227
|
+
" delivered_at TEXT NOT NULL"
|
|
228
|
+
")"
|
|
229
|
+
)
|
|
230
|
+
conn.execute(
|
|
231
|
+
"CREATE INDEX IF NOT EXISTS idx_delivery_person_date "
|
|
232
|
+
"ON delivery_log(person_id, delivered_at)"
|
|
233
|
+
)
|
|
234
|
+
cutoff = (
|
|
235
|
+
datetime.now(timezone.utc) - timedelta(days=_HISTORY_RETENTION_DAYS)
|
|
236
|
+
).isoformat()
|
|
237
|
+
conn.execute(
|
|
238
|
+
"DELETE FROM delivery_log WHERE delivered_at < ?", (cutoff,),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def _reload_from_db(self) -> None:
|
|
242
|
+
"""Reload today's counts and the most-recent delivery per person."""
|
|
243
|
+
assert self._db_path is not None
|
|
244
|
+
# "Today" is the OWNER-LOCAL date (matching _reset_if_new_day), so
|
|
245
|
+
# its start is local midnight converted to UTC — the log stores UTC
|
|
246
|
+
# timestamps. Combining the local date with UTC midnight shifted the
|
|
247
|
+
# reload window by the UTC offset, over- or under-counting restarts.
|
|
248
|
+
today_start = datetime.combine(
|
|
249
|
+
self._today, datetime.min.time(), tzinfo=self._tz
|
|
250
|
+
).astimezone(timezone.utc).isoformat()
|
|
251
|
+
with closing(sqlite3.connect(self._db_path)) as conn, conn:
|
|
252
|
+
# Today's deliveries → daily count
|
|
253
|
+
cur = conn.execute(
|
|
254
|
+
"SELECT person_id, delivered_at FROM delivery_log "
|
|
255
|
+
"WHERE delivered_at >= ? ORDER BY delivered_at",
|
|
256
|
+
(today_start,),
|
|
257
|
+
)
|
|
258
|
+
for person_id, ts_str in cur:
|
|
259
|
+
self._daily_counts[person_id].append(datetime.fromisoformat(ts_str))
|
|
260
|
+
|
|
261
|
+
# Most recent delivery per person → cooldown state (spans day boundary)
|
|
262
|
+
cur = conn.execute(
|
|
263
|
+
"SELECT person_id, MAX(delivered_at) FROM delivery_log "
|
|
264
|
+
"GROUP BY person_id"
|
|
265
|
+
)
|
|
266
|
+
for person_id, ts_str in cur:
|
|
267
|
+
if ts_str:
|
|
268
|
+
self._last_delivery[person_id] = datetime.fromisoformat(ts_str)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Reach-out delivery policy: sanitisation, staleness, quiet-hours urgency.
|
|
2
|
+
|
|
3
|
+
These are the quality/safety guards applied to a user-facing reach-out
|
|
4
|
+
initiative *before* it is handed to the guarded Hermes delivery path. They are
|
|
5
|
+
generic and deployment-agnostic (patterns + env-tunable thresholds, no
|
|
6
|
+
hardcoded identities or messages).
|
|
7
|
+
|
|
8
|
+
* sanitise -- strip raw conversation / skill / control markup from the
|
|
9
|
+
outward-facing text so Hermes composes from clean intent.
|
|
10
|
+
* staleness -- drop reach-outs whose subject is older than a max age; a late
|
|
11
|
+
autonomous ping about a two-week-stale item is noise.
|
|
12
|
+
* quiet -- reach-out must respect quiet hours by default; a high
|
|
13
|
+
stale-age priority must NOT silently bypass the quiet window.
|
|
14
|
+
Only an explicit urgent signal may exceed the cap.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from typing import Any, Optional
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Sanitisation
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
# ``<<RCSCTX ...>>`` and any other ``<<...>>`` control block, terminated or not
|
|
29
|
+
# (titles are truncated so the closing ``>>`` is frequently missing).
|
|
30
|
+
_ANGLE_BLOCK = re.compile(r"<<.*?>>", re.DOTALL)
|
|
31
|
+
_ANGLE_UNTERMINATED = re.compile(r"<<[^>]*$", re.DOTALL)
|
|
32
|
+
|
|
33
|
+
# Bracketed system/skill directives, e.g.
|
|
34
|
+
# [IMPORTANT: The user has invoked the "colony-operations" skill ...]
|
|
35
|
+
# [System note: Your previous turn was interrupted ...]
|
|
36
|
+
_BRACKET_DIRECTIVE = re.compile(
|
|
37
|
+
r"\[\s*(?:IMPORTANT|SYSTEM\s*NOTE|SYSTEM|INTERNAL|CONTEXT|NOTE)\b[^\]]*\]",
|
|
38
|
+
re.IGNORECASE | re.DOTALL,
|
|
39
|
+
)
|
|
40
|
+
_BRACKET_UNTERMINATED = re.compile(
|
|
41
|
+
r"\[\s*(?:IMPORTANT|SYSTEM\s*NOTE|SYSTEM|INTERNAL|CONTEXT|NOTE)\b[^\]]*$",
|
|
42
|
+
re.IGNORECASE | re.DOTALL,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Non-printable / control characters (keep tab/newline out of outward text too).
|
|
46
|
+
_CONTROL = re.compile(r"[\x00-\x1f\x7f]")
|
|
47
|
+
_WS = re.compile(r"\s+")
|
|
48
|
+
|
|
49
|
+
_SANITIZE_FIELDS = ("title", "description", "rationale", "suggested_action")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def sanitize_text(text: Optional[str]) -> str:
|
|
53
|
+
"""Strip control/conversation/skill markup and collapse whitespace."""
|
|
54
|
+
if not text:
|
|
55
|
+
return ""
|
|
56
|
+
s = str(text)
|
|
57
|
+
s = _ANGLE_BLOCK.sub(" ", s)
|
|
58
|
+
s = _ANGLE_UNTERMINATED.sub(" ", s)
|
|
59
|
+
s = _BRACKET_DIRECTIVE.sub(" ", s)
|
|
60
|
+
s = _BRACKET_UNTERMINATED.sub(" ", s)
|
|
61
|
+
s = _CONTROL.sub(" ", s)
|
|
62
|
+
s = _WS.sub(" ", s).strip()
|
|
63
|
+
return s
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def sanitize_payload(payload: dict) -> dict:
|
|
67
|
+
"""Return a copy of the initiative payload with its text fields cleaned."""
|
|
68
|
+
out = dict(payload)
|
|
69
|
+
for field in _SANITIZE_FIELDS:
|
|
70
|
+
value = out.get(field)
|
|
71
|
+
if isinstance(value, str):
|
|
72
|
+
out[field] = sanitize_text(value)
|
|
73
|
+
if not out.get("title"):
|
|
74
|
+
out["title"] = (out.get("description") or "")[:80] or "(follow-up)"
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# Meaningful-content gate (applied at reach-out GENERATION time)
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
# Minimum real alphanumeric substance a reach-out source must retain after
|
|
83
|
+
# sanitisation to be worth generating an initiative from. Low enough to keep
|
|
84
|
+
# legitimately short subjects ("Call Bob"), high enough to drop fragments.
|
|
85
|
+
_MIN_MEANINGFUL_CHARS = 4
|
|
86
|
+
|
|
87
|
+
# Markers that a source turn is system / skill / non-conversational, i.e. not
|
|
88
|
+
# genuine conversation or a real commitment. These identify origins that
|
|
89
|
+
# sanitisation alone might leave partial residue from.
|
|
90
|
+
_SYSTEM_ORIGIN = re.compile(
|
|
91
|
+
r"\binvoked\b.{0,60}?\bskill\b" # "... invoked the <name> skill ..."
|
|
92
|
+
r"|previous turn was interrupted"
|
|
93
|
+
r"|\bsystem note\b"
|
|
94
|
+
r"|\bsystem[- ]?generated\b"
|
|
95
|
+
r"|conversation (?:was )?(?:interrupted|truncated|reset)"
|
|
96
|
+
r"|<\s*/?\s*system[\s>]", # <system> ... </system> style tags
|
|
97
|
+
re.IGNORECASE | re.DOTALL,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def is_system_origin(text: Optional[str]) -> bool:
|
|
102
|
+
"""True if the raw source text is a system/skill/non-conversational turn."""
|
|
103
|
+
return bool(text) and bool(_SYSTEM_ORIGIN.search(str(text)))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def meaningful_reachout_text(text: Optional[str]) -> str:
|
|
107
|
+
"""Clean reach-out source text, or "" if it is not worth surfacing.
|
|
108
|
+
|
|
109
|
+
Returns the sanitised text when the source is genuine, human-meaningful
|
|
110
|
+
content; returns "" when the source is empty, of system/skill origin, or
|
|
111
|
+
has no real substance after sanitisation. Use at reach-out GENERATION time
|
|
112
|
+
so near-empty / system-origin junk never becomes an initiative.
|
|
113
|
+
"""
|
|
114
|
+
if not text or is_system_origin(text):
|
|
115
|
+
return ""
|
|
116
|
+
cleaned = sanitize_text(text)
|
|
117
|
+
substance = re.sub(r"[^0-9a-z]+", "", cleaned.lower())
|
|
118
|
+
if len(substance) < _MIN_MEANINGFUL_CHARS:
|
|
119
|
+
return ""
|
|
120
|
+
return cleaned
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# Staleness
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
# Structured "how overdue is the subject" signals. Deliberately excludes
|
|
128
|
+
# contact-recency signals (e.g. days_since_contact) which are a *reason* to
|
|
129
|
+
# reach out, not a disqualifier.
|
|
130
|
+
_STALENESS_KEYS = (
|
|
131
|
+
"days_pending", "days_overdue", "days_stale", "stale_days", "age_days",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _scan_staleness(obj: Any, depth: int = 2) -> float:
|
|
136
|
+
best = 0.0
|
|
137
|
+
if isinstance(obj, dict):
|
|
138
|
+
for key, value in obj.items():
|
|
139
|
+
if key in _STALENESS_KEYS and isinstance(value, (int, float)):
|
|
140
|
+
best = max(best, float(value))
|
|
141
|
+
elif depth > 0 and isinstance(value, (dict, list)):
|
|
142
|
+
best = max(best, _scan_staleness(value, depth - 1))
|
|
143
|
+
elif isinstance(obj, list) and depth > 0:
|
|
144
|
+
for value in obj:
|
|
145
|
+
best = max(best, _scan_staleness(value, depth - 1))
|
|
146
|
+
return best
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def reachout_age_days(payload: dict) -> float:
|
|
150
|
+
"""Effective age of a reach-out: max of structured subject-staleness and
|
|
151
|
+
the initiative's own age since generation."""
|
|
152
|
+
age = _scan_staleness(payload.get("context") or {})
|
|
153
|
+
generated_at = payload.get("generated_at")
|
|
154
|
+
if generated_at:
|
|
155
|
+
try:
|
|
156
|
+
ts = datetime.fromisoformat(str(generated_at).replace("Z", "+00:00"))
|
|
157
|
+
if ts.tzinfo is None:
|
|
158
|
+
ts = ts.replace(tzinfo=timezone.utc)
|
|
159
|
+
init_age = (datetime.now(timezone.utc) - ts).total_seconds() / 86400.0
|
|
160
|
+
age = max(age, init_age)
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
return age
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def max_age_days() -> float:
|
|
167
|
+
try:
|
|
168
|
+
return float(os.environ.get("COLONY_REACHOUT_MAX_AGE_DAYS", "7"))
|
|
169
|
+
except (TypeError, ValueError):
|
|
170
|
+
return 7.0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def is_aged_out(payload: dict) -> bool:
|
|
174
|
+
"""True if this reach-out is too old to be worth surfacing."""
|
|
175
|
+
return reachout_age_days(payload) > max_age_days()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# Quiet-hours urgency
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
# The rate limiter bypasses quiet hours at urgency >= 0.9. Reach-out urgency is
|
|
183
|
+
# derived from priority, which for stale items pins to 1.0 -- an artifact, not a
|
|
184
|
+
# genuine "wake them up" signal. Cap it below the bypass threshold by default.
|
|
185
|
+
def _urgency_cap() -> float:
|
|
186
|
+
try:
|
|
187
|
+
return float(os.environ.get("COLONY_REACHOUT_URGENCY_CAP", "0.85"))
|
|
188
|
+
except (TypeError, ValueError):
|
|
189
|
+
return 0.85
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _explicit_urgent(payload: dict) -> bool:
|
|
193
|
+
if payload.get("urgent") is True or payload.get("bypass_quiet_hours") is True:
|
|
194
|
+
return True
|
|
195
|
+
ctx = payload.get("context") or {}
|
|
196
|
+
if isinstance(ctx, dict):
|
|
197
|
+
return ctx.get("urgent") is True or ctx.get("bypass_quiet_hours") is True
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def quiet_hours_urgency(payload: dict, raw_urgency: float) -> float:
|
|
202
|
+
"""Urgency to use for the delivery gate. Reach-out respects quiet hours
|
|
203
|
+
unless an explicit urgent flag is set on the initiative/context."""
|
|
204
|
+
if _explicit_urgent(payload):
|
|
205
|
+
return raw_urgency
|
|
206
|
+
return min(raw_urgency, _urgency_cap())
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Directed action: owner directive -> gated, delegated, audited execution.
|
|
2
|
+
|
|
3
|
+
Option A architecture: Colony never mutates external systems itself. It
|
|
4
|
+
scopes the directive deterministically, gates it (boundaries first, then
|
|
5
|
+
approval), dispatches the contract to an env-configured delegate, audits
|
|
6
|
+
what actually happened against the granted scope, and reports back through
|
|
7
|
+
the guarded reach-out path.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from apsimo.directed.models import (
|
|
11
|
+
ScopedTask, ScopedTaskStore, ScopeLimits, READ_OPS, MUTATE_OPS,
|
|
12
|
+
)
|
|
13
|
+
from apsimo.directed.intake import scope_from_directive, resolve_targets
|
|
14
|
+
from apsimo.directed.audit import audit_completion, audit_via_report
|
|
15
|
+
from apsimo.directed.service import DirectedActionService, directed_mode
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ScopedTask", "ScopedTaskStore", "ScopeLimits", "READ_OPS", "MUTATE_OPS",
|
|
19
|
+
"scope_from_directive", "resolve_targets",
|
|
20
|
+
"audit_completion", "audit_via_report",
|
|
21
|
+
"DirectedActionService", "directed_mode",
|
|
22
|
+
]
|
apsimo/directed/audit.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Post-action verification: what actually happened vs the granted scope.
|
|
2
|
+
|
|
3
|
+
Two evidence sources, best-first:
|
|
4
|
+
1. Repo inspection (when a read-only local mirror of the target exists):
|
|
5
|
+
fetch the work branch and diff it against its merge-base -- files touched,
|
|
6
|
+
commit count, branch name are verified MECHANICALLY.
|
|
7
|
+
2. The delegate's structured report, cross-checked field by field.
|
|
8
|
+
|
|
9
|
+
Any out-of-scope observation is a VIOLATION: flagged loudly, recorded on the
|
|
10
|
+
task, and fed to the outcome feedback store by the service.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import fnmatch
|
|
16
|
+
import logging
|
|
17
|
+
import subprocess
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
from apsimo.directed.models import ScopedTask, MUTATE_OPS
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _match_globs(path: str, globs: List[str]) -> bool:
|
|
26
|
+
p = (path or "").lstrip("./")
|
|
27
|
+
for g in globs or ["**"]:
|
|
28
|
+
if g in ("**", "*") or fnmatch.fnmatch(p, g) or fnmatch.fnmatch(p, g.rstrip("/") + "/**"):
|
|
29
|
+
return True
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _git(mirror_path: str, *args: str, timeout: int = 30) -> str:
|
|
34
|
+
out = subprocess.run(
|
|
35
|
+
["git", "-C", mirror_path, *args],
|
|
36
|
+
capture_output=True, text=True, timeout=timeout,
|
|
37
|
+
)
|
|
38
|
+
if out.returncode != 0:
|
|
39
|
+
raise RuntimeError(out.stderr.strip()[:200])
|
|
40
|
+
return out.stdout
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def audit_via_mirror(task: ScopedTask, mirror_path: str, branch: str,
|
|
44
|
+
base: str = "HEAD") -> Dict[str, Any]:
|
|
45
|
+
"""Mechanical audit from a read-only mirror: fetch the branch, diff vs base."""
|
|
46
|
+
findings: List[str] = []
|
|
47
|
+
ok = True
|
|
48
|
+
try:
|
|
49
|
+
_git(mirror_path, "fetch", "origin", branch, timeout=60)
|
|
50
|
+
merge_base = _git(mirror_path, "merge-base", base, "FETCH_HEAD").strip()
|
|
51
|
+
commits = _git(mirror_path, "rev-list", "--count", f"{merge_base}..FETCH_HEAD").strip()
|
|
52
|
+
n_commits = int(commits or 0)
|
|
53
|
+
files = [f for f in _git(
|
|
54
|
+
mirror_path, "diff", "--name-only", merge_base, "FETCH_HEAD",
|
|
55
|
+
).splitlines() if f.strip()]
|
|
56
|
+
except Exception as exc:
|
|
57
|
+
return {"method": "mirror", "ok": None,
|
|
58
|
+
"findings": [f"mirror inspection unavailable: {exc}"]}
|
|
59
|
+
|
|
60
|
+
if not branch.startswith(task.limits.branch_prefix):
|
|
61
|
+
ok = False
|
|
62
|
+
findings.append(f"branch {branch!r} outside required prefix {task.limits.branch_prefix!r}")
|
|
63
|
+
if n_commits > task.limits.max_commits:
|
|
64
|
+
ok = False
|
|
65
|
+
findings.append(f"{n_commits} commits exceeds cap {task.limits.max_commits}")
|
|
66
|
+
off = [f for f in files if not _match_globs(f, task.limits.path_globs)]
|
|
67
|
+
if off:
|
|
68
|
+
ok = False
|
|
69
|
+
findings.append(f"files outside allowed paths: {off[:10]}")
|
|
70
|
+
return {"method": "mirror", "ok": ok, "branch": branch,
|
|
71
|
+
"commits": n_commits, "files_touched": files[:100], "findings": findings}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def audit_via_report(task: ScopedTask, report: Dict[str, Any]) -> Dict[str, Any]:
|
|
75
|
+
"""Cross-check the delegate's structured report against the scope."""
|
|
76
|
+
findings: List[str] = []
|
|
77
|
+
ok = True
|
|
78
|
+
report = report or {}
|
|
79
|
+
|
|
80
|
+
ops = [str(o).lower() for o in (report.get("operations") or [])]
|
|
81
|
+
illegal = [o for o in ops if o not in set(task.allowed_ops)]
|
|
82
|
+
if illegal:
|
|
83
|
+
ok = False
|
|
84
|
+
findings.append(f"operations outside scope: {illegal}")
|
|
85
|
+
|
|
86
|
+
branch = str(report.get("branch") or "")
|
|
87
|
+
did_mutate = bool(set(ops) & MUTATE_OPS) or bool(report.get("commits"))
|
|
88
|
+
if did_mutate and not task.mutating:
|
|
89
|
+
ok = False
|
|
90
|
+
findings.append("delegate mutated on a read-only scope")
|
|
91
|
+
if did_mutate and branch and not branch.startswith(task.limits.branch_prefix):
|
|
92
|
+
ok = False
|
|
93
|
+
findings.append(f"branch {branch!r} outside required prefix {task.limits.branch_prefix!r}")
|
|
94
|
+
|
|
95
|
+
n_commits = int(report.get("commits") or 0)
|
|
96
|
+
if n_commits > task.limits.max_commits:
|
|
97
|
+
ok = False
|
|
98
|
+
findings.append(f"{n_commits} commits exceeds cap {task.limits.max_commits}")
|
|
99
|
+
|
|
100
|
+
if report.get("force_push"):
|
|
101
|
+
ok = False
|
|
102
|
+
findings.append("force push reported (never allowed)")
|
|
103
|
+
if report.get("deletions") and not task.limits.delete_allowed:
|
|
104
|
+
deld = report.get("deletions")
|
|
105
|
+
off = [f for f in deld if not _match_globs(str(f), task.limits.path_globs)] if isinstance(deld, list) else [str(deld)]
|
|
106
|
+
if off:
|
|
107
|
+
ok = False
|
|
108
|
+
findings.append(f"deletes outside scope: {off[:10]}")
|
|
109
|
+
|
|
110
|
+
files = [str(f) for f in (report.get("files_touched") or [])]
|
|
111
|
+
off = [f for f in files if not _match_globs(f, task.limits.path_globs)]
|
|
112
|
+
if off:
|
|
113
|
+
ok = False
|
|
114
|
+
findings.append(f"files outside allowed paths: {off[:10]}")
|
|
115
|
+
|
|
116
|
+
missing = [k for k in task.reporting if k not in report]
|
|
117
|
+
if missing:
|
|
118
|
+
findings.append(f"report missing expected fields: {missing}")
|
|
119
|
+
if ok:
|
|
120
|
+
# What was not reported cannot be verified: an empty/partial
|
|
121
|
+
# report is "unverified" (ok=None), never "clean" (ok=True).
|
|
122
|
+
ok = None
|
|
123
|
+
|
|
124
|
+
return {"method": "report", "ok": ok, "branch": branch, "commits": n_commits,
|
|
125
|
+
"files_touched": files[:100], "findings": findings}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def audit_completion(task: ScopedTask, report: Dict[str, Any],
|
|
129
|
+
mirror_path: Optional[str] = None) -> Dict[str, Any]:
|
|
130
|
+
"""Full audit: mirror inspection when reachable, report cross-check always.
|
|
131
|
+
|
|
132
|
+
Verdict: 'clean' | 'violation' | 'unverified'. A violation from EITHER
|
|
133
|
+
source wins (fail loud).
|
|
134
|
+
"""
|
|
135
|
+
report_audit = audit_via_report(task, report)
|
|
136
|
+
mirror_audit = None
|
|
137
|
+
branch = report_audit.get("branch") or ""
|
|
138
|
+
if mirror_path and branch:
|
|
139
|
+
mirror_audit = audit_via_mirror(task, mirror_path, branch)
|
|
140
|
+
elif mirror_path and not branch and task.mutating:
|
|
141
|
+
# A mirror was available but the mutating delegate reported no
|
|
142
|
+
# branch, so the mirror inspection silently could not run: the
|
|
143
|
+
# report side may not verdict "clean" on its own say-so.
|
|
144
|
+
report_audit["findings"].append(
|
|
145
|
+
"mirror audit skipped: no branch reported for mutating task")
|
|
146
|
+
if report_audit.get("ok") is True:
|
|
147
|
+
report_audit["ok"] = None
|
|
148
|
+
|
|
149
|
+
oks = [a["ok"] for a in (report_audit, mirror_audit) if a is not None]
|
|
150
|
+
if any(o is False for o in oks):
|
|
151
|
+
verdict = "violation"
|
|
152
|
+
elif any(o is True for o in oks):
|
|
153
|
+
verdict = "clean"
|
|
154
|
+
else:
|
|
155
|
+
verdict = "unverified"
|
|
156
|
+
|
|
157
|
+
result = {
|
|
158
|
+
"verdict": verdict,
|
|
159
|
+
"report_audit": report_audit,
|
|
160
|
+
"mirror_audit": mirror_audit,
|
|
161
|
+
}
|
|
162
|
+
if verdict == "violation":
|
|
163
|
+
all_findings = report_audit.get("findings", []) + (
|
|
164
|
+
(mirror_audit or {}).get("findings", []) if mirror_audit else [])
|
|
165
|
+
logger.warning("DIRECTED-ACTION SCOPE VIOLATION on %s: %s",
|
|
166
|
+
task.id, "; ".join(all_findings)[:400])
|
|
167
|
+
return result
|