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,509 @@
|
|
|
1
|
+
"""DirectedActionService -- owner directive -> gated, delegated, audited action.
|
|
2
|
+
|
|
3
|
+
Pipeline (option A: delegation only, Colony never mutates anything itself):
|
|
4
|
+
|
|
5
|
+
intake -> deterministic ScopedTask (intake.py)
|
|
6
|
+
gate 1 -> DirectiveGuard boundary check FIRST (a standing "leave X alone"
|
|
7
|
+
refuses intake with the citation)
|
|
8
|
+
gate 2 -> approval tiering: read-only scopes auto-approve; any mutating
|
|
9
|
+
scope requires owner approval (legacy repeat approvals are
|
|
10
|
+
expiring and use-capped during migration)
|
|
11
|
+
dispatch-> POST the ScopedTask contract to the EXISTING env-configured
|
|
12
|
+
delegate endpoint (COLONY_DIRECTED_TASK_URL, falling back to the
|
|
13
|
+
agent-bridge jobs webhook). Dry-run mode logs the exact would-be
|
|
14
|
+
dispatch and sends nothing (COLONY_DIRECTED_MODE, default dry_run).
|
|
15
|
+
report -> the delegate POSTs a structured report back; audit_completion
|
|
16
|
+
verifies it against the granted scope (mirror inspection when a
|
|
17
|
+
read-only mirror of the target exists).
|
|
18
|
+
outcome -> violations flagged loudly + recorded; success/violation/failure
|
|
19
|
+
feeds the TypeFeedbackStore; an owner-facing report artifact is
|
|
20
|
+
routed through the guarded reach-out path (held by delivery shadow).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import hashlib
|
|
26
|
+
import hmac
|
|
27
|
+
import logging
|
|
28
|
+
import os
|
|
29
|
+
import time
|
|
30
|
+
import uuid
|
|
31
|
+
from typing import Any, Dict, List, Optional
|
|
32
|
+
|
|
33
|
+
from apsimo.directed.models import ScopedTask, ScopedTaskStore
|
|
34
|
+
from apsimo.directed.intake import scope_from_directive
|
|
35
|
+
from apsimo.directed.audit import audit_completion
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def directed_mode() -> str:
|
|
41
|
+
from apsimo.util.autonomy_preset import resolve
|
|
42
|
+
return resolve("COLONY_DIRECTED_MODE", ("off", "dry_run", "live"),
|
|
43
|
+
"dry_run")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _dispatch_url() -> str:
|
|
47
|
+
return (os.environ.get("COLONY_DIRECTED_TASK_URL", "")
|
|
48
|
+
or os.environ.get("COLONY_BRIDGE_JOBS_WEBHOOK_URL", "")
|
|
49
|
+
or os.environ.get("COLONY_JOBS_WEBHOOK_URL", ""))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _strict_reports() -> bool:
|
|
53
|
+
"""Strict report-back (default): a completion report is accepted only for
|
|
54
|
+
a task that was actually dispatched. COLONY_DIRECTED_STRICT_REPORTS=0
|
|
55
|
+
restores the legacy accept-in-any-status behavior."""
|
|
56
|
+
return os.environ.get("COLONY_DIRECTED_STRICT_REPORTS", "1").strip() != "0"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _max_dispatch_per_day() -> int:
|
|
60
|
+
"""Daily dispatch cap (rolling 24h). <=0 disables the cap (legacy)."""
|
|
61
|
+
try:
|
|
62
|
+
return int(os.environ.get("COLONY_DIRECTED_MAX_DISPATCH_PER_DAY", "10"))
|
|
63
|
+
except (TypeError, ValueError):
|
|
64
|
+
return 10
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _hmac_key() -> bytes:
|
|
68
|
+
"""Optional shared secret for the dispatch envelope. Empty = unsigned."""
|
|
69
|
+
return os.environ.get("COLONY_DIRECTED_HMAC_KEY", "").encode("utf-8")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _sign(key: bytes, message: str) -> str:
|
|
73
|
+
return hmac.new(key, message.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def report_token_for(task_id: str) -> str:
|
|
77
|
+
"""Deterministic report-back token for a task (only when a key is set).
|
|
78
|
+
|
|
79
|
+
The delegate receives it inside the dispatch envelope and must echo it as
|
|
80
|
+
``report_token`` in the completion report; ``complete`` recomputes and
|
|
81
|
+
compares. Empty string when no key is configured (check disabled)."""
|
|
82
|
+
key = _hmac_key()
|
|
83
|
+
return _sign(key, f"report:{task_id}") if key else ""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DirectedActionService:
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
store: ScopedTaskStore,
|
|
90
|
+
directive_manager: Any = None,
|
|
91
|
+
mirrors: Any = None, # RepoMirrorManager (audit + target resolution)
|
|
92
|
+
feedback_store: Any = None,
|
|
93
|
+
delivery_router: Any = None, # awaitable (payload) -> bool, e.g. loop._route_reachout_delivery bound with delivery
|
|
94
|
+
self_model: Any = None, # SelfModel (trust engine + journal, Amendment 1)
|
|
95
|
+
) -> None:
|
|
96
|
+
self.store = store
|
|
97
|
+
self._directives = directive_manager
|
|
98
|
+
self._mirrors = mirrors
|
|
99
|
+
self._feedback = feedback_store
|
|
100
|
+
self._deliver = delivery_router
|
|
101
|
+
self._self_model = self_model
|
|
102
|
+
# Observability: boundary checks that raised (see intake gate 1).
|
|
103
|
+
self.boundary_check_errors = 0
|
|
104
|
+
|
|
105
|
+
# -- trust helpers (Amendment 1.7) -----------------------------------
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _trust_domain(task: ScopedTask) -> str:
|
|
108
|
+
"""Trust domain per scope class: all read-only scopes pool their
|
|
109
|
+
track record; mutating scopes earn trust per approval_key."""
|
|
110
|
+
return f"directed:{task.approval_key}" if task.mutating else "directed:read"
|
|
111
|
+
|
|
112
|
+
def _journal(self, task: ScopedTask, decision: str, why: str,
|
|
113
|
+
confidence: Any = None) -> None:
|
|
114
|
+
journal = getattr(self._self_model, "journal", None)
|
|
115
|
+
if journal is None:
|
|
116
|
+
return
|
|
117
|
+
try:
|
|
118
|
+
journal.record(
|
|
119
|
+
self._trust_domain(task), task.objective[:300],
|
|
120
|
+
reasoning=why, confidence=confidence,
|
|
121
|
+
reversibility="recoverable" if task.mutating else "reversible",
|
|
122
|
+
decision=decision, ref=task.id)
|
|
123
|
+
except Exception:
|
|
124
|
+
logger.debug("directed journal failed", exc_info=True)
|
|
125
|
+
|
|
126
|
+
# -- known targets --------------------------------------------------
|
|
127
|
+
def known_targets(self) -> List[Dict[str, str]]:
|
|
128
|
+
"""Owner-designated repos (mirrors) as intake-resolvable targets."""
|
|
129
|
+
out: List[Dict[str, str]] = []
|
|
130
|
+
if self._mirrors is not None:
|
|
131
|
+
try:
|
|
132
|
+
for name, info in self._mirrors.configured().items():
|
|
133
|
+
out.append({"kind": "repo", "name": name,
|
|
134
|
+
"aliases": info.get("aliases", ""),
|
|
135
|
+
"ref": info.get("url", "")})
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
return out
|
|
139
|
+
|
|
140
|
+
# -- intake + gates --------------------------------------------------
|
|
141
|
+
async def intake(self, directive_text: str,
|
|
142
|
+
source: str = "owner") -> ScopedTask:
|
|
143
|
+
"""Turn a directive into a gated ScopedTask (does NOT dispatch)."""
|
|
144
|
+
task = scope_from_directive(directive_text, self.known_targets())
|
|
145
|
+
|
|
146
|
+
# Gate 1: boundaries FIRST -- refuse with the citation.
|
|
147
|
+
if self._directives is not None:
|
|
148
|
+
try:
|
|
149
|
+
from apsimo.directives import Action
|
|
150
|
+
verdict = self._directives.check(Action(
|
|
151
|
+
kind="directed_action",
|
|
152
|
+
text=task.searchable_text(),
|
|
153
|
+
target=",".join(t.get("name", "") for t in task.targets),
|
|
154
|
+
high_risk=True,
|
|
155
|
+
))
|
|
156
|
+
if not verdict.allowed:
|
|
157
|
+
task.status = "refused"
|
|
158
|
+
task.refusal_reason = verdict.reason
|
|
159
|
+
self.store.save(task)
|
|
160
|
+
logger.warning("Directed intake REFUSED by boundary: %s", verdict.reason)
|
|
161
|
+
return task
|
|
162
|
+
except Exception:
|
|
163
|
+
# An owner boundary we cannot evaluate must not be assumed
|
|
164
|
+
# permissive: fail CLOSED by default and refuse the intake
|
|
165
|
+
# (the owner can simply re-issue the directive).
|
|
166
|
+
self.boundary_check_errors += 1
|
|
167
|
+
from apsimo.directives.guard import boundary_fail_closed
|
|
168
|
+
if boundary_fail_closed():
|
|
169
|
+
task.status = "refused"
|
|
170
|
+
task.refusal_reason = "boundary_check_error"
|
|
171
|
+
self.store.save(task)
|
|
172
|
+
logger.warning(
|
|
173
|
+
"Directed intake REFUSED: boundary_check_error "
|
|
174
|
+
"(boundary check raised; failing closed)",
|
|
175
|
+
exc_info=True,
|
|
176
|
+
)
|
|
177
|
+
return task
|
|
178
|
+
logger.debug("directed boundary check failed (allowing)", exc_info=True)
|
|
179
|
+
|
|
180
|
+
# Gate 2: approval tiering, now trust-graduated (Amendment 1.7).
|
|
181
|
+
# Read-only scopes act with journaling. Mutating scopes may consume a
|
|
182
|
+
# bounded compatibility approval; otherwise the trust engine decides: an
|
|
183
|
+
# earned act_first class self-approves (journaled + reported), an
|
|
184
|
+
# ask_first class asks the owner WITH the reasoning and confidence.
|
|
185
|
+
# The immutable floor always asks.
|
|
186
|
+
if not task.mutating:
|
|
187
|
+
task.approval = {"required": False, "reason": "read_only_auto"}
|
|
188
|
+
task.status = "approved"
|
|
189
|
+
self._journal(task, "acted", "read-only scope auto-approved")
|
|
190
|
+
else:
|
|
191
|
+
standing = False
|
|
192
|
+
try:
|
|
193
|
+
from apsimo.initiatives import standing_approvals
|
|
194
|
+
standing = standing_approvals.is_approved(task.approval_key)
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
if standing:
|
|
198
|
+
task.approval = {"required": True, "granted_by": "standing",
|
|
199
|
+
"standing": True, "key": task.approval_key}
|
|
200
|
+
task.status = "approved"
|
|
201
|
+
self._journal(task, "acted", "bounded repeat approval")
|
|
202
|
+
else:
|
|
203
|
+
gate = None
|
|
204
|
+
trust = getattr(self._self_model, "trust", None)
|
|
205
|
+
if trust is not None:
|
|
206
|
+
try:
|
|
207
|
+
gate = trust.gate(
|
|
208
|
+
self._trust_domain(task),
|
|
209
|
+
task.objective,
|
|
210
|
+
reasoning=f"owner directive: {task.directive_text[:200]}",
|
|
211
|
+
reversibility="recoverable",
|
|
212
|
+
default_stage="ask_first",
|
|
213
|
+
ref=task.id)
|
|
214
|
+
except Exception:
|
|
215
|
+
logger.debug("directed trust gate failed", exc_info=True)
|
|
216
|
+
if gate is not None and gate["decision"] == "act":
|
|
217
|
+
task.approval = {
|
|
218
|
+
"required": True, "granted_by": "trust_engine",
|
|
219
|
+
"standing": False, "key": task.approval_key,
|
|
220
|
+
"confidence": round(gate["confidence"], 3),
|
|
221
|
+
}
|
|
222
|
+
task.status = "approved"
|
|
223
|
+
else:
|
|
224
|
+
task.approval = {"required": True, "standing": False,
|
|
225
|
+
"key": task.approval_key}
|
|
226
|
+
if gate is not None:
|
|
227
|
+
task.approval["confidence"] = round(gate["confidence"], 3)
|
|
228
|
+
task.status = "awaiting_approval"
|
|
229
|
+
await self._request_approval(task, gate)
|
|
230
|
+
self.store.save(task)
|
|
231
|
+
return task
|
|
232
|
+
|
|
233
|
+
async def _request_approval(self, task: ScopedTask,
|
|
234
|
+
gate: Optional[Dict[str, Any]]) -> None:
|
|
235
|
+
"""Ask-first (Amendment 1.1): the approval request carries the
|
|
236
|
+
reasoning and the confidence, through the guarded delivery path."""
|
|
237
|
+
if self._deliver is None:
|
|
238
|
+
return
|
|
239
|
+
try:
|
|
240
|
+
from apsimo.proposals import Proposal, proposal_to_payload
|
|
241
|
+
conf = (gate or {}).get("confidence")
|
|
242
|
+
conf_txt = f" My confidence on this class of work is {conf:.2f}." if conf is not None else ""
|
|
243
|
+
prop = Proposal(
|
|
244
|
+
title=f"Approval needed: {task.objective[:60]}",
|
|
245
|
+
finding=(f"I want to run this directed task: {task.objective[:300]}. "
|
|
246
|
+
f"Scope: ops={','.join(task.allowed_ops)}, "
|
|
247
|
+
f"targets={[t.get('name') for t in task.targets]}, "
|
|
248
|
+
f"branch prefix {task.limits.branch_prefix!r}, "
|
|
249
|
+
f"max {task.limits.max_commits} commits.{conf_txt}"),
|
|
250
|
+
why_it_helps="it is mutating work, so you decide until I have "
|
|
251
|
+
"earned act-first trust on this scope",
|
|
252
|
+
suggested_action=f"Approve with: directed approve {task.id} "
|
|
253
|
+
"(or tell me to drop it)",
|
|
254
|
+
source=task.id, initiative_type="proposal",
|
|
255
|
+
confidence=0.8)
|
|
256
|
+
await self._deliver(proposal_to_payload(prop))
|
|
257
|
+
except Exception:
|
|
258
|
+
logger.debug("approval request delivery failed", exc_info=True)
|
|
259
|
+
|
|
260
|
+
def approve(
|
|
261
|
+
self,
|
|
262
|
+
task_id: str,
|
|
263
|
+
approved_by: str = "trusted-internal",
|
|
264
|
+
standing: bool = False,
|
|
265
|
+
grant_expires_in_seconds: int = 7 * 24 * 60 * 60,
|
|
266
|
+
grant_max_uses: int = 5,
|
|
267
|
+
) -> Optional[ScopedTask]:
|
|
268
|
+
task = self.store.get(task_id)
|
|
269
|
+
if task is None or task.status != "awaiting_approval":
|
|
270
|
+
return task
|
|
271
|
+
task.approval.update({"granted_by": approved_by, "standing": standing})
|
|
272
|
+
task.status = "approved"
|
|
273
|
+
if standing:
|
|
274
|
+
try:
|
|
275
|
+
from apsimo.initiatives import standing_approvals
|
|
276
|
+
standing_approvals.grant(
|
|
277
|
+
task.approval_key,
|
|
278
|
+
approved_by=approved_by,
|
|
279
|
+
expires_in_seconds=grant_expires_in_seconds,
|
|
280
|
+
max_uses=grant_max_uses,
|
|
281
|
+
)
|
|
282
|
+
except Exception:
|
|
283
|
+
pass
|
|
284
|
+
self.store.save(task)
|
|
285
|
+
return task
|
|
286
|
+
|
|
287
|
+
# -- dispatch ---------------------------------------------------------
|
|
288
|
+
async def dispatch(self, task_id: str) -> Dict[str, Any]:
|
|
289
|
+
"""Send an APPROVED ScopedTask to the delegate (or log it in dry-run)."""
|
|
290
|
+
task = self.store.get(task_id)
|
|
291
|
+
if task is None:
|
|
292
|
+
return {"dispatched": False, "reason": "not_found"}
|
|
293
|
+
if task.status != "approved":
|
|
294
|
+
return {"dispatched": False, "reason": f"not_approved (status={task.status})"}
|
|
295
|
+
if task.is_expired():
|
|
296
|
+
task.status = "expired"; self.store.save(task)
|
|
297
|
+
return {"dispatched": False, "reason": "expired"}
|
|
298
|
+
|
|
299
|
+
# Daily dispatch cap (rolling 24h, dry + live both count): bounds the
|
|
300
|
+
# blast radius of a runaway approval/dispatch loop. The task stays
|
|
301
|
+
# approved so the owner loses nothing but time.
|
|
302
|
+
cap = _max_dispatch_per_day()
|
|
303
|
+
if cap > 0:
|
|
304
|
+
recent = 0
|
|
305
|
+
try:
|
|
306
|
+
recent = self.store.dispatches_since(time.time() - 86400.0)
|
|
307
|
+
except Exception:
|
|
308
|
+
logger.debug("dispatch cap count failed (allowing)", exc_info=True)
|
|
309
|
+
if recent >= cap:
|
|
310
|
+
logger.warning(
|
|
311
|
+
"Directed dispatch %s REFUSED: daily cap reached (%d/%d "
|
|
312
|
+
"in 24h; COLONY_DIRECTED_MAX_DISPATCH_PER_DAY)",
|
|
313
|
+
task.id, recent, cap)
|
|
314
|
+
self._journal(task, "held",
|
|
315
|
+
f"daily dispatch cap reached ({recent}/{cap})")
|
|
316
|
+
return {"dispatched": False, "reason": "daily_dispatch_cap",
|
|
317
|
+
"cap": cap, "count": recent}
|
|
318
|
+
|
|
319
|
+
mode = directed_mode()
|
|
320
|
+
# Dispatch envelope: verifiable provenance for the contract. Signed
|
|
321
|
+
# only when COLONY_DIRECTED_HMAC_KEY is set (unset = legacy unsigned).
|
|
322
|
+
envelope: Dict[str, Any] = {
|
|
323
|
+
"task_id": task.id,
|
|
324
|
+
"issued_at": time.time(),
|
|
325
|
+
"nonce": uuid.uuid4().hex,
|
|
326
|
+
}
|
|
327
|
+
key = _hmac_key()
|
|
328
|
+
if key:
|
|
329
|
+
envelope["algo"] = "hmac-sha256"
|
|
330
|
+
envelope["signature"] = _sign(
|
|
331
|
+
key, f"{task.id}.{envelope['issued_at']}.{envelope['nonce']}")
|
|
332
|
+
envelope["report_token"] = report_token_for(task.id)
|
|
333
|
+
payload = {
|
|
334
|
+
"type": "directed_task",
|
|
335
|
+
"task": task.to_dict(),
|
|
336
|
+
"contract": {
|
|
337
|
+
"work_branch_prefix": task.limits.branch_prefix,
|
|
338
|
+
"instructions": (
|
|
339
|
+
"Work ONLY within the attached scope. Create a dedicated "
|
|
340
|
+
f"branch under '{task.limits.branch_prefix}'. Never force-push, "
|
|
341
|
+
"never delete outside scope, respect the commit cap. On "
|
|
342
|
+
"completion POST a structured report (summary, operations, "
|
|
343
|
+
"files_touched, commits, branch) to report_url."
|
|
344
|
+
),
|
|
345
|
+
},
|
|
346
|
+
"report_url": f"/v1/host/directed/tasks/{task.id}/report",
|
|
347
|
+
"envelope": envelope,
|
|
348
|
+
}
|
|
349
|
+
if mode != "live":
|
|
350
|
+
logger.info(
|
|
351
|
+
"DRY-RUN directed dispatch %s -> %s | ops=%s targets=%s "
|
|
352
|
+
"limits={branch_prefix:%s,max_commits:%d} mutating=%s",
|
|
353
|
+
task.id, _dispatch_url() or "(no endpoint configured)",
|
|
354
|
+
task.allowed_ops, [t.get("name") for t in task.targets],
|
|
355
|
+
task.limits.branch_prefix, task.limits.max_commits, task.mutating,
|
|
356
|
+
)
|
|
357
|
+
task.status = "dispatched_dry"
|
|
358
|
+
self.store.save(task)
|
|
359
|
+
try:
|
|
360
|
+
self.store.log_dispatch(task.id, mode)
|
|
361
|
+
except Exception:
|
|
362
|
+
logger.debug("dispatch log failed", exc_info=True)
|
|
363
|
+
return {"dispatched": False, "dry_run": True, "payload": payload}
|
|
364
|
+
|
|
365
|
+
url = _dispatch_url()
|
|
366
|
+
if not url:
|
|
367
|
+
return {"dispatched": False, "reason": "no_dispatch_endpoint"}
|
|
368
|
+
try:
|
|
369
|
+
import aiohttp
|
|
370
|
+
headers = {"Content-Type": "application/json"}
|
|
371
|
+
api_key = os.environ.get("COLONY_API_KEY", "")
|
|
372
|
+
if api_key:
|
|
373
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
374
|
+
if key:
|
|
375
|
+
headers["X-Colony-Directed-Signature"] = envelope["signature"]
|
|
376
|
+
async with aiohttp.ClientSession() as s:
|
|
377
|
+
async with s.post(url, json=payload, headers=headers,
|
|
378
|
+
timeout=aiohttp.ClientTimeout(total=10)) as resp:
|
|
379
|
+
ok = resp.status in (200, 202)
|
|
380
|
+
except Exception as exc:
|
|
381
|
+
logger.warning("directed dispatch failed: %s", exc)
|
|
382
|
+
ok = False
|
|
383
|
+
task.status = "dispatched" if ok else "failed"
|
|
384
|
+
self.store.save(task)
|
|
385
|
+
if ok:
|
|
386
|
+
try:
|
|
387
|
+
self.store.log_dispatch(task.id, mode)
|
|
388
|
+
except Exception:
|
|
389
|
+
logger.debug("dispatch log failed", exc_info=True)
|
|
390
|
+
return {"dispatched": ok, "url": url}
|
|
391
|
+
|
|
392
|
+
# -- report-back + audit ----------------------------------------------
|
|
393
|
+
def _reject_report(self, task: ScopedTask, reason: str) -> Dict[str, Any]:
|
|
394
|
+
"""Reject a report-back loudly: journaled WARNING, nothing recorded."""
|
|
395
|
+
logger.warning(
|
|
396
|
+
"Directed report for %s REJECTED (%s): task status=%s "
|
|
397
|
+
"(strict reports; COLONY_DIRECTED_STRICT_REPORTS=0 for legacy)",
|
|
398
|
+
task.id, reason, task.status)
|
|
399
|
+
self._journal(task, "blocked",
|
|
400
|
+
f"report-back rejected: {reason} (status={task.status})")
|
|
401
|
+
return {"ok": False, "reason": reason, "status": task.status}
|
|
402
|
+
|
|
403
|
+
async def complete(self, task_id: str, report: Dict[str, Any]) -> Dict[str, Any]:
|
|
404
|
+
"""Delegate report-back: audit vs scope, record outcome, notify owner.
|
|
405
|
+
|
|
406
|
+
Strict mode (default) closes a real hole — a report used to be
|
|
407
|
+
accepted for a task in ANY status, letting a stray or duplicate POST
|
|
408
|
+
fabricate audited outcomes (and trust evidence) for work that was
|
|
409
|
+
never dispatched. Now only ``dispatched`` tasks take a report; a
|
|
410
|
+
``dispatched_dry`` task takes a dry echo that records no trust
|
|
411
|
+
outcome; anything else is rejected with a journaled WARNING.
|
|
412
|
+
"""
|
|
413
|
+
task = self.store.get(task_id)
|
|
414
|
+
if task is None:
|
|
415
|
+
return {"ok": False, "reason": "not_found"}
|
|
416
|
+
|
|
417
|
+
report = report or {}
|
|
418
|
+
dry_echo = False
|
|
419
|
+
if _strict_reports():
|
|
420
|
+
# Envelope report token (only enforced when a key is configured).
|
|
421
|
+
expected = report_token_for(task.id)
|
|
422
|
+
if expected and not hmac.compare_digest(
|
|
423
|
+
str(report.get("report_token", "")), expected):
|
|
424
|
+
return self._reject_report(task, "bad_report_token")
|
|
425
|
+
if task.status in ("completed", "violated"):
|
|
426
|
+
return self._reject_report(task, "double_report")
|
|
427
|
+
if task.status == "dispatched_dry":
|
|
428
|
+
dry_echo = True
|
|
429
|
+
elif task.status != "dispatched":
|
|
430
|
+
return self._reject_report(task, "status_mismatch")
|
|
431
|
+
|
|
432
|
+
mirror_path = None
|
|
433
|
+
if self._mirrors is not None and task.targets:
|
|
434
|
+
try:
|
|
435
|
+
mirror_path = self._mirrors.path_for(task.targets[0].get("name", ""))
|
|
436
|
+
except Exception:
|
|
437
|
+
mirror_path = None
|
|
438
|
+
|
|
439
|
+
audit = audit_completion(task, report, mirror_path=mirror_path)
|
|
440
|
+
task.audit = audit
|
|
441
|
+
verdict = audit["verdict"]
|
|
442
|
+
task.status = "violated" if verdict == "violation" else "completed"
|
|
443
|
+
self.store.save(task)
|
|
444
|
+
|
|
445
|
+
# Outcome feedback.
|
|
446
|
+
if self._feedback is not None:
|
|
447
|
+
try:
|
|
448
|
+
self._feedback.record(
|
|
449
|
+
"directed_action",
|
|
450
|
+
"actioned" if verdict == "clean" else "dismissed",
|
|
451
|
+
)
|
|
452
|
+
except Exception:
|
|
453
|
+
pass
|
|
454
|
+
|
|
455
|
+
# Trust engine: audited outcomes are the earned-autonomy evidence
|
|
456
|
+
# (Amendment 1.7); a violation trips the circuit breaker. An
|
|
457
|
+
# unverified MUTATING completion is recorded as nothing: it must
|
|
458
|
+
# neither graduate a scope class nor trip its breaker. A dry echo
|
|
459
|
+
# (report against a dry-run dispatch) records NO trust outcome —
|
|
460
|
+
# nothing real happened, so nothing may graduate or demote.
|
|
461
|
+
if self._self_model is not None and not dry_echo:
|
|
462
|
+
try:
|
|
463
|
+
if verdict == "violation":
|
|
464
|
+
self._self_model.record(self._trust_domain(task),
|
|
465
|
+
"failure", violation=True)
|
|
466
|
+
elif verdict == "clean" or not task.mutating:
|
|
467
|
+
self._self_model.record(self._trust_domain(task),
|
|
468
|
+
"success")
|
|
469
|
+
except Exception:
|
|
470
|
+
pass
|
|
471
|
+
self._journal(task, "noted",
|
|
472
|
+
f"completion audited: {verdict}")
|
|
473
|
+
|
|
474
|
+
# Owner-facing report through the guarded reach-out path (still held
|
|
475
|
+
# by delivery shadow until go-live).
|
|
476
|
+
await self._notify_owner(task, report, audit)
|
|
477
|
+
out = {"ok": True, "verdict": verdict, "audit": audit}
|
|
478
|
+
if dry_echo:
|
|
479
|
+
out["dry_echo"] = True
|
|
480
|
+
return out
|
|
481
|
+
|
|
482
|
+
async def _notify_owner(self, task: ScopedTask, report: Dict[str, Any],
|
|
483
|
+
audit: Dict[str, Any]) -> None:
|
|
484
|
+
if self._deliver is None:
|
|
485
|
+
return
|
|
486
|
+
try:
|
|
487
|
+
from apsimo.proposals import Proposal, proposal_to_payload
|
|
488
|
+
verdict = audit.get("verdict", "unverified")
|
|
489
|
+
if verdict == "violation":
|
|
490
|
+
findings = "; ".join(
|
|
491
|
+
audit.get("report_audit", {}).get("findings", [])[:5])
|
|
492
|
+
title = f"SCOPE VIOLATION on directed task: {task.objective[:60]}"
|
|
493
|
+
finding = (f"The delegate exceeded the granted scope: {findings}. "
|
|
494
|
+
"I flagged and recorded it; nothing was accepted.")
|
|
495
|
+
urgency = 0.95
|
|
496
|
+
else:
|
|
497
|
+
title = f"Directed task {verdict}: {task.objective[:60]}"
|
|
498
|
+
finding = str(report.get("summary", ""))[:600] or "Task completed."
|
|
499
|
+
urgency = 0.7
|
|
500
|
+
prop = Proposal(
|
|
501
|
+
title=title, finding=finding,
|
|
502
|
+
why_it_helps="closes the loop on the work you directed",
|
|
503
|
+
suggested_action=("Review the flagged changes" if verdict == "violation"
|
|
504
|
+
else "Review the result; tell me any follow-ups"),
|
|
505
|
+
source=task.id, initiative_type="proposal", confidence=urgency,
|
|
506
|
+
)
|
|
507
|
+
await self._deliver(proposal_to_payload(prop))
|
|
508
|
+
except Exception:
|
|
509
|
+
logger.debug("directed owner-notify failed", exc_info=True)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Directive / boundary memory: durable owner boundaries + enforcement.
|
|
2
|
+
|
|
3
|
+
Public surface:
|
|
4
|
+
DirectiveManager -- capture + store + enforce (single entry point)
|
|
5
|
+
DirectiveStore -- SQLite persistence
|
|
6
|
+
DirectiveGuard -- boundary enforcement (check an Action -> Verdict)
|
|
7
|
+
Action, Verdict -- the enforcement contract
|
|
8
|
+
Directive, Polarity, DirectiveStatus -- the data model
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from apsimo.directives.models import (
|
|
12
|
+
Directive, Polarity, DirectiveStatus, normalize_terms,
|
|
13
|
+
)
|
|
14
|
+
from apsimo.directives.store import DirectiveStore
|
|
15
|
+
from apsimo.directives.guard import (
|
|
16
|
+
DirectiveGuard, DirectiveStoreUnavailable, Action, Verdict,
|
|
17
|
+
boundary_fail_closed,
|
|
18
|
+
)
|
|
19
|
+
from apsimo.directives.service import DirectiveManager
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Directive", "Polarity", "DirectiveStatus", "normalize_terms",
|
|
23
|
+
"DirectiveStore", "DirectiveGuard", "DirectiveStoreUnavailable", "Action",
|
|
24
|
+
"Verdict", "DirectiveManager", "boundary_fail_closed",
|
|
25
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Canonical source bindings for automatically learned standing directives.
|
|
2
|
+
|
|
3
|
+
Only references persist in the existing directive metadata column. Current
|
|
4
|
+
source bytes supply the exact rule; correction or erasure withdraws its effect.
|
|
5
|
+
"""
|
|
6
|
+
from contextlib import closing
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from apsimo.turns.idempotency import canonical_turn_digest, source_message_hash
|
|
11
|
+
from apsimo.turns.audio import source_text
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _message(ledger, reference):
|
|
15
|
+
if ledger is None:
|
|
16
|
+
return None
|
|
17
|
+
identifier = reference['source_turn_id']
|
|
18
|
+
if ledger.is_projection_erased(identifier):
|
|
19
|
+
return None
|
|
20
|
+
with closing(ledger._connect()) as db:
|
|
21
|
+
row = db.execute("SELECT * FROM turn_sources WHERE turn_id=? AND contact_id=? AND scope='person'",
|
|
22
|
+
(identifier, reference['contact_id'])).fetchone()
|
|
23
|
+
if row is None:
|
|
24
|
+
return None
|
|
25
|
+
from apsimo.turns.source_attribution import is_invalidated
|
|
26
|
+
if is_invalidated(db, identifier):
|
|
27
|
+
return None
|
|
28
|
+
messages = json.loads(row['messages_json'])
|
|
29
|
+
if canonical_turn_digest(messages) != reference['source_version']:
|
|
30
|
+
return None
|
|
31
|
+
message_hash = reference['message_hash']
|
|
32
|
+
# Existing correction annotations are tied to exact source messages.
|
|
33
|
+
if db.execute('SELECT 1 FROM source_annotations a,json_each(a.target_message_hashes_json) h '
|
|
34
|
+
'WHERE a.target_source_id=? AND h.value=? LIMIT 1', (identifier, message_hash)).fetchone():
|
|
35
|
+
return None
|
|
36
|
+
for message in messages:
|
|
37
|
+
if message.get('role') == 'user' and source_message_hash(row['session_id'], message) == message_hash:
|
|
38
|
+
return source_text(message.get('content'))
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def bind(ledger, *, source_id, contact_id, message, clause):
|
|
43
|
+
if ledger is None or not source_id or not contact_id:
|
|
44
|
+
return None
|
|
45
|
+
with closing(ledger._connect()) as db:
|
|
46
|
+
row = db.execute("SELECT * FROM turn_sources WHERE turn_id=? AND contact_id=? AND scope='person'",
|
|
47
|
+
(source_id, contact_id)).fetchone()
|
|
48
|
+
if row is None:
|
|
49
|
+
return None
|
|
50
|
+
messages = json.loads(row['messages_json'])
|
|
51
|
+
for item in messages:
|
|
52
|
+
text = source_text(item.get('content'))
|
|
53
|
+
if item.get('role') != 'user' or text.strip() != message.strip():
|
|
54
|
+
continue
|
|
55
|
+
start = text.find(clause)
|
|
56
|
+
if start < 0:
|
|
57
|
+
continue
|
|
58
|
+
reference = {'source_turn_id': source_id, 'contact_id': contact_id,
|
|
59
|
+
'source_version': canonical_turn_digest(messages),
|
|
60
|
+
'message_hash': source_message_hash(row['session_id'], item),
|
|
61
|
+
'start': start, 'end': start+len(clause),
|
|
62
|
+
'clause_sha256': hashlib.sha256(clause.encode()).hexdigest()}
|
|
63
|
+
if _message(ledger, reference) is not None:
|
|
64
|
+
return reference
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def hydrate(ledger, directive):
|
|
69
|
+
reference = directive.evidence
|
|
70
|
+
if reference is None:
|
|
71
|
+
return directive # Explicit manual/configuration and legacy rows.
|
|
72
|
+
text = _message(ledger, reference)
|
|
73
|
+
if text is None:
|
|
74
|
+
return None
|
|
75
|
+
clause = text[reference['start']:reference['end']]
|
|
76
|
+
if hashlib.sha256(clause.encode()).hexdigest() != reference['clause_sha256']:
|
|
77
|
+
return None
|
|
78
|
+
from .extractor import extract_directives, is_revocation
|
|
79
|
+
# Re-evaluate the full current source scope, not just a detached quotation.
|
|
80
|
+
rules = [item for item in extract_directives(text) if not is_revocation(item)
|
|
81
|
+
and item.raw_text == clause and item.polarity == directive.polarity]
|
|
82
|
+
if len(rules) != 1:
|
|
83
|
+
return None
|
|
84
|
+
current = rules[0]
|
|
85
|
+
directive.subject, directive.raw_text = current.subject, current.raw_text
|
|
86
|
+
directive.match_terms, directive.level = current.match_terms, current.level
|
|
87
|
+
return directive
|