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
apsimo/gate/models.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Gate data models — GatePayload, GateDecision, DispatchResult."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from apsimo.gate.communication_policy import CommunicationPolicyContextV1
|
|
11
|
+
from apsimo.intelligence.relationships.trust_tiers import TrustTier
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GateResultCode(str, Enum):
|
|
15
|
+
PASS = "pass"
|
|
16
|
+
BLOCK_RECIPIENT = "block_recipient"
|
|
17
|
+
BLOCK_PII = "block_pii"
|
|
18
|
+
BLOCK_CROSS_CONTEXT = "block_cross_context"
|
|
19
|
+
BLOCK_TRUST_TIER = "block_trust_tier"
|
|
20
|
+
BLOCK_INJECTION = "block_injection"
|
|
21
|
+
BLOCK_REVIEW = "block_review"
|
|
22
|
+
PENDING_DELAY = "pending_delay"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class GatePayload:
|
|
27
|
+
"""The only view of a response that the gate pipeline sees.
|
|
28
|
+
|
|
29
|
+
MUST NOT contain any workspace or reasoning content.
|
|
30
|
+
Fields are immutable after construction.
|
|
31
|
+
"""
|
|
32
|
+
response_text: str
|
|
33
|
+
target_contact_id: str
|
|
34
|
+
target_gateway: str
|
|
35
|
+
session_id: str
|
|
36
|
+
trust_tier: TrustTier
|
|
37
|
+
mentioned_entities: frozenset # entities from THIS session's history
|
|
38
|
+
turn_id: str
|
|
39
|
+
incoming_message_text: str # original incoming message (for Layer 5)
|
|
40
|
+
# Immutable host-owned purpose/disclosure constraints. Checkers and any
|
|
41
|
+
# restricted review model see the exact same object whose digest is bound
|
|
42
|
+
# into the ResponseGuard result. None preserves the legacy contract.
|
|
43
|
+
communication_policy: Optional[CommunicationPolicyContextV1] = None
|
|
44
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(tz=timezone.utc))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class GateDecision:
|
|
49
|
+
"""Result of gate pipeline evaluation."""
|
|
50
|
+
payload_turn_id: str
|
|
51
|
+
result_code: GateResultCode
|
|
52
|
+
blocked: bool
|
|
53
|
+
blocking_layer: Optional[int] # 1–7, None if PASS
|
|
54
|
+
block_reason: Optional[str] # human-readable, used in rejection
|
|
55
|
+
flagged_excerpt: Optional[str] # redacted excerpt for rejection feedback
|
|
56
|
+
layer_results: dict # per-layer detail for audit log
|
|
57
|
+
evaluated_at: datetime = field(default_factory=lambda: datetime.now(tz=timezone.utc))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class DispatchResult:
|
|
62
|
+
blocked: bool
|
|
63
|
+
reason: Optional[str] = None
|
|
64
|
+
gate_decision: Optional[GateDecision] = None
|
apsimo/gate/pipeline.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""ResponseGate — seven-layer response gate pipeline orchestrator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
# Dedicated audit logger for security-relevant gate events (SEC-13-L-03)
|
|
11
|
+
_audit_log = logging.getLogger("colony.gate.audit")
|
|
12
|
+
|
|
13
|
+
from apsimo.gate.models import GateDecision, GatePayload, GateResultCode, DispatchResult
|
|
14
|
+
from apsimo.gate.layers.l1_recipient import RecipientVerifier
|
|
15
|
+
from apsimo.gate.layers.l2_pii import PIIScanner
|
|
16
|
+
from apsimo.gate.layers.l3_cross_context import CrossContextDetector
|
|
17
|
+
from apsimo.gate.layers.l4_trust_tier import TrustTierChecker
|
|
18
|
+
from apsimo.gate.layers.l5_injection import InjectionDetector
|
|
19
|
+
from apsimo.gate.layers.l6_review import SecondaryReviewer
|
|
20
|
+
from apsimo.gate.layers.l7_delay import SendDelayGate, PendingDispatchStore
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ResponseGate:
|
|
26
|
+
"""Seven-layer response gate pipeline.
|
|
27
|
+
|
|
28
|
+
Usage::
|
|
29
|
+
|
|
30
|
+
gate = ResponseGate(config, session_store, audit_log)
|
|
31
|
+
result = await gate.evaluate(payload)
|
|
32
|
+
if not result.blocked:
|
|
33
|
+
await dispatch(payload)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
config: Any,
|
|
39
|
+
session_store: Any,
|
|
40
|
+
audit_log: logging.Logger,
|
|
41
|
+
secondary_reviewer: Optional[SecondaryReviewer] = None,
|
|
42
|
+
dispatch_store: Optional[PendingDispatchStore] = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Initialize the ResponseGate pipeline.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
config: Gate configuration object (GateConfig or compatible).
|
|
48
|
+
session_store: Session store used by L1 and L3 layers.
|
|
49
|
+
audit_log: Logger to record gate decisions for the audit trail.
|
|
50
|
+
secondary_reviewer: Optional override for L6; defaults to SecondaryReviewer(config).
|
|
51
|
+
dispatch_store: Optional persistent store for L7 delayed dispatches.
|
|
52
|
+
"""
|
|
53
|
+
self._config = config
|
|
54
|
+
self._l1 = RecipientVerifier(session_store)
|
|
55
|
+
self._l2 = PIIScanner(config)
|
|
56
|
+
self._l3 = CrossContextDetector(session_store, config)
|
|
57
|
+
self._l4 = TrustTierChecker(config)
|
|
58
|
+
self._l5 = InjectionDetector(config)
|
|
59
|
+
self._l6 = secondary_reviewer or SecondaryReviewer(config)
|
|
60
|
+
self._l7 = SendDelayGate(config, dispatch_store)
|
|
61
|
+
self._audit = audit_log
|
|
62
|
+
# Only explicitly enabled review needs a client. Do not report a
|
|
63
|
+
# completed review when that optional dependency is unavailable.
|
|
64
|
+
if (getattr(config, "enable_secondary_review", False)
|
|
65
|
+
and getattr(self._l6, "_llm_client", None) is None):
|
|
66
|
+
logger.warning(
|
|
67
|
+
"Gate L6 secondary review is ENABLED but no review LLM is "
|
|
68
|
+
"configured; L6 reports review_unavailable. Inject a client or disable "
|
|
69
|
+
"enable_secondary_review.")
|
|
70
|
+
|
|
71
|
+
async def evaluate(self, payload: GatePayload) -> GateDecision:
|
|
72
|
+
"""Run all gate layers. Returns decision; does NOT dispatch."""
|
|
73
|
+
response_hash = hashlib.sha256(payload.response_text.encode()).hexdigest()
|
|
74
|
+
|
|
75
|
+
layer_results: dict = {
|
|
76
|
+
"_session_id": payload.session_id,
|
|
77
|
+
"_contact_id": payload.target_contact_id,
|
|
78
|
+
"_sensitivity": self._config.sensitivity,
|
|
79
|
+
"_overrides_applied": [],
|
|
80
|
+
"_response_hash": response_hash,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Check per-contact overrides
|
|
84
|
+
override = self._config.get_contact_override(payload.target_contact_id)
|
|
85
|
+
bypass_layers: list[int] = []
|
|
86
|
+
if override:
|
|
87
|
+
raw_bypass = override.get("bypass_layers", [])
|
|
88
|
+
layer_results["_overrides_applied"] = [f"contact:{payload.target_contact_id}"]
|
|
89
|
+
# Layer 1 can NEVER be bypassed
|
|
90
|
+
bypass_layers = [layer_id for layer_id in raw_bypass if layer_id != 1]
|
|
91
|
+
|
|
92
|
+
# Emit structured audit entry for each active bypass (SEC-13-L-03)
|
|
93
|
+
for layer_id in bypass_layers:
|
|
94
|
+
_audit_log.warning(
|
|
95
|
+
"gate_layer_bypassed",
|
|
96
|
+
extra={
|
|
97
|
+
"contact_id": payload.target_contact_id,
|
|
98
|
+
"layer_id": layer_id,
|
|
99
|
+
"override_source": override.get("source", "unknown"),
|
|
100
|
+
"message_id": payload.turn_id,
|
|
101
|
+
"timestamp": time.time(),
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Layers 1–5: deterministic, short-circuit on first block
|
|
106
|
+
deterministic_layers = [
|
|
107
|
+
(1, self._l1),
|
|
108
|
+
(2, self._l2),
|
|
109
|
+
(3, self._l3),
|
|
110
|
+
(4, self._l4),
|
|
111
|
+
(5, self._l5),
|
|
112
|
+
]
|
|
113
|
+
for layer_num, checker in deterministic_layers:
|
|
114
|
+
if layer_num in bypass_layers:
|
|
115
|
+
# GATE-01: audit log every operator bypass exercise
|
|
116
|
+
logger.warning(
|
|
117
|
+
"GATE_OPERATOR_BYPASS: layer=%d session_id=%s contact_id=%s",
|
|
118
|
+
layer_num,
|
|
119
|
+
payload.session_id,
|
|
120
|
+
payload.target_contact_id,
|
|
121
|
+
)
|
|
122
|
+
layer_results[f"layer_{layer_num}"] = {"bypassed": True}
|
|
123
|
+
continue
|
|
124
|
+
result = await checker.check(payload)
|
|
125
|
+
layer_results[f"layer_{layer_num}"] = {
|
|
126
|
+
"blocked": result.blocked,
|
|
127
|
+
"code": result.code,
|
|
128
|
+
"reason": result.reason,
|
|
129
|
+
"suspicious": result.suspicious,
|
|
130
|
+
}
|
|
131
|
+
if result.blocked:
|
|
132
|
+
decision = GateDecision(
|
|
133
|
+
payload_turn_id=payload.turn_id,
|
|
134
|
+
result_code=GateResultCode(result.code),
|
|
135
|
+
blocked=True,
|
|
136
|
+
blocking_layer=layer_num,
|
|
137
|
+
block_reason=result.reason,
|
|
138
|
+
flagged_excerpt=result.flagged_excerpt,
|
|
139
|
+
layer_results=layer_results,
|
|
140
|
+
)
|
|
141
|
+
await self._audit.record(decision)
|
|
142
|
+
return decision
|
|
143
|
+
|
|
144
|
+
# Layer 6: explicitly enabled secondary review.
|
|
145
|
+
l5_result = layer_results.get("layer_5", {})
|
|
146
|
+
injection_suspicious = l5_result.get("suspicious", False) if isinstance(l5_result, dict) else False
|
|
147
|
+
|
|
148
|
+
if self._config.enable_secondary_review and 6 not in bypass_layers:
|
|
149
|
+
l6_result = await self._l6.review(
|
|
150
|
+
payload, injection_suspicious=injection_suspicious
|
|
151
|
+
)
|
|
152
|
+
layer_results["layer_6"] = {
|
|
153
|
+
"flagged": l6_result.flagged,
|
|
154
|
+
"category": l6_result.category,
|
|
155
|
+
"status": l6_result.status,
|
|
156
|
+
}
|
|
157
|
+
if l6_result.flagged:
|
|
158
|
+
decision = GateDecision(
|
|
159
|
+
payload_turn_id=payload.turn_id,
|
|
160
|
+
result_code=GateResultCode.BLOCK_REVIEW,
|
|
161
|
+
blocked=True,
|
|
162
|
+
blocking_layer=6,
|
|
163
|
+
block_reason=("secondary_review_flagged" if l6_result.status == "reviewed"
|
|
164
|
+
else "secondary_review_" + l6_result.status),
|
|
165
|
+
flagged_excerpt=None,
|
|
166
|
+
layer_results=layer_results,
|
|
167
|
+
)
|
|
168
|
+
await self._audit.record(decision)
|
|
169
|
+
return decision
|
|
170
|
+
else:
|
|
171
|
+
layer_results["layer_6"] = {"skipped": True}
|
|
172
|
+
|
|
173
|
+
# Layer 7: send delay
|
|
174
|
+
if 7 not in bypass_layers:
|
|
175
|
+
l7_result = await self._l7.hold(payload)
|
|
176
|
+
layer_results["layer_7"] = {
|
|
177
|
+
"cancelled": l7_result.cancelled,
|
|
178
|
+
"pending_id": l7_result.pending_id,
|
|
179
|
+
}
|
|
180
|
+
if l7_result.cancelled:
|
|
181
|
+
decision = GateDecision(
|
|
182
|
+
payload_turn_id=payload.turn_id,
|
|
183
|
+
result_code=GateResultCode.PENDING_DELAY,
|
|
184
|
+
blocked=True,
|
|
185
|
+
blocking_layer=7,
|
|
186
|
+
block_reason=l7_result.cancel_reason or "async_cancellation_during_delay",
|
|
187
|
+
flagged_excerpt=None,
|
|
188
|
+
layer_results=layer_results,
|
|
189
|
+
)
|
|
190
|
+
await self._audit.record(decision)
|
|
191
|
+
return decision
|
|
192
|
+
else:
|
|
193
|
+
layer_results["layer_7"] = {"bypassed": True}
|
|
194
|
+
|
|
195
|
+
# All layers passed
|
|
196
|
+
decision = GateDecision(
|
|
197
|
+
payload_turn_id=payload.turn_id,
|
|
198
|
+
result_code=GateResultCode.PASS,
|
|
199
|
+
blocked=False,
|
|
200
|
+
blocking_layer=None,
|
|
201
|
+
block_reason=None,
|
|
202
|
+
flagged_excerpt=None,
|
|
203
|
+
layer_results=layer_results,
|
|
204
|
+
)
|
|
205
|
+
await self._audit.record(decision)
|
|
206
|
+
return decision
|
apsimo/gate/rejection.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Rejection feedback loop — regenerate once when the ResponseGuard blocks.
|
|
2
|
+
|
|
3
|
+
Rebuilt ResponseGuard-native: the original loop was an orphan of the retired
|
|
4
|
+
7-layer ResponseGate and depended on subsystems that no longer exist
|
|
5
|
+
(GatePayload sessions, MetaLearner.record_gate_rejection, an escalation
|
|
6
|
+
manager). The dataclasses are kept as the stable contract; the loop now runs
|
|
7
|
+
against ``ResponseGuard.evaluate`` and a caller-supplied ``regenerate`` hook,
|
|
8
|
+
and rejections persist in a small ``RejectionStore`` instead of MetaLearner.
|
|
9
|
+
|
|
10
|
+
Error contract (mirrors the guard's own):
|
|
11
|
+
* A guard BLOCK is never overridden by a loop-internal error — if anything
|
|
12
|
+
inside the loop raises after a block, the result stays blocked (fail
|
|
13
|
+
CLOSED on the block side).
|
|
14
|
+
* If the loop errors while the latest guard verdict is an allow, the allow
|
|
15
|
+
stands (fail OPEN on the allow side) — a loop fault must never silence a
|
|
16
|
+
reply the guard already cleared.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import sqlite3
|
|
23
|
+
import threading
|
|
24
|
+
import uuid
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from datetime import datetime, timedelta, timezone
|
|
27
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class RejectionNotice:
|
|
34
|
+
"""Structured rejection fed back to the LLM for regeneration."""
|
|
35
|
+
notice_id: str
|
|
36
|
+
turn_id: str
|
|
37
|
+
session_id: str
|
|
38
|
+
contact_display_name: str
|
|
39
|
+
block_reason_type: str
|
|
40
|
+
blocking_layer: int
|
|
41
|
+
redacted_excerpt: Optional[str]
|
|
42
|
+
trust_tier: str
|
|
43
|
+
retry_number: int
|
|
44
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(tz=timezone.utc))
|
|
45
|
+
|
|
46
|
+
def to_llm_prompt_fragment(self) -> str:
|
|
47
|
+
excerpt_line = ""
|
|
48
|
+
if self.redacted_excerpt:
|
|
49
|
+
excerpt_line = f'\nFlagged content: "{self.redacted_excerpt}"'
|
|
50
|
+
return (
|
|
51
|
+
f"Your response to {self.contact_display_name} was blocked by the "
|
|
52
|
+
f"response gate.\n\n"
|
|
53
|
+
f"Reason: {self.block_reason_type}{excerpt_line}\n\n"
|
|
54
|
+
f"Please generate a new response that does not contain this content. "
|
|
55
|
+
f"The recipient's trust tier is {self.trust_tier}. Adjust accordingly."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class GateRejectionEvent:
|
|
61
|
+
"""Learning signal for a gate rejection (persisted in RejectionStore)."""
|
|
62
|
+
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
63
|
+
session_id: str = ""
|
|
64
|
+
contact_id: str = ""
|
|
65
|
+
trust_tier: str = ""
|
|
66
|
+
blocking_layer: int = 0
|
|
67
|
+
block_reason: str = ""
|
|
68
|
+
retry_number: int = 0
|
|
69
|
+
eventually_succeeded: bool = False
|
|
70
|
+
turn_id: str = ""
|
|
71
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(tz=timezone.utc))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class FeedbackLoopResult:
|
|
76
|
+
passed: bool
|
|
77
|
+
payload: Optional[object] # the (possibly regenerated) response text
|
|
78
|
+
attempts: int
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class RejectionStore:
|
|
82
|
+
"""SQLite persistence for gate rejections (replaces the MetaLearner
|
|
83
|
+
dependency of the retired loop). Also the counting surface for the
|
|
84
|
+
enforce circuit breaker."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, db_path: Optional[str] = None) -> None:
|
|
87
|
+
self._lock = threading.RLock()
|
|
88
|
+
self._conn = sqlite3.connect(db_path or ":memory:",
|
|
89
|
+
check_same_thread=False)
|
|
90
|
+
self._conn.row_factory = sqlite3.Row
|
|
91
|
+
with self._lock:
|
|
92
|
+
self._conn.execute(
|
|
93
|
+
"""CREATE TABLE IF NOT EXISTS gate_rejections (
|
|
94
|
+
event_id TEXT PRIMARY KEY, session_id TEXT,
|
|
95
|
+
contact_id TEXT, trust_tier TEXT, blocking_layer INTEGER,
|
|
96
|
+
block_reason TEXT, retry_number INTEGER,
|
|
97
|
+
eventually_succeeded INTEGER, turn_id TEXT, ts TEXT
|
|
98
|
+
)""")
|
|
99
|
+
self._conn.commit()
|
|
100
|
+
|
|
101
|
+
def record(self, event: GateRejectionEvent) -> None:
|
|
102
|
+
with self._lock:
|
|
103
|
+
self._conn.execute(
|
|
104
|
+
"""INSERT INTO gate_rejections
|
|
105
|
+
(event_id, session_id, contact_id, trust_tier,
|
|
106
|
+
blocking_layer, block_reason, retry_number,
|
|
107
|
+
eventually_succeeded, turn_id, ts)
|
|
108
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)
|
|
109
|
+
ON CONFLICT(event_id) DO UPDATE SET
|
|
110
|
+
eventually_succeeded=excluded.eventually_succeeded,
|
|
111
|
+
retry_number=excluded.retry_number""",
|
|
112
|
+
(event.event_id, event.session_id, event.contact_id,
|
|
113
|
+
event.trust_tier, event.blocking_layer, event.block_reason,
|
|
114
|
+
event.retry_number, 1 if event.eventually_succeeded else 0,
|
|
115
|
+
event.turn_id, event.timestamp.isoformat()))
|
|
116
|
+
self._conn.commit()
|
|
117
|
+
|
|
118
|
+
def recent(self, limit: int = 50) -> List[Dict[str, Any]]:
|
|
119
|
+
with self._lock:
|
|
120
|
+
rows = self._conn.execute(
|
|
121
|
+
"SELECT * FROM gate_rejections ORDER BY ts DESC LIMIT ?",
|
|
122
|
+
(int(limit),)).fetchall()
|
|
123
|
+
return [dict(r) for r in rows]
|
|
124
|
+
|
|
125
|
+
def count_since(self, hours: float = 24.0) -> int:
|
|
126
|
+
cutoff = (datetime.now(tz=timezone.utc)
|
|
127
|
+
- timedelta(hours=hours)).isoformat()
|
|
128
|
+
with self._lock:
|
|
129
|
+
r = self._conn.execute(
|
|
130
|
+
"SELECT COUNT(*) FROM gate_rejections WHERE ts >= ?",
|
|
131
|
+
(cutoff,)).fetchone()
|
|
132
|
+
return int(r[0]) if r else 0
|
|
133
|
+
|
|
134
|
+
def close(self) -> None:
|
|
135
|
+
try:
|
|
136
|
+
self._conn.close()
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# regenerate(prompt_fragment, blocked_text) -> new text (or None to give up)
|
|
142
|
+
RegenerateFn = Callable[[str, str], Awaitable[Optional[str]]]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class RejectionFeedbackLoop:
|
|
146
|
+
"""One regenerate-and-re-check cycle after a ResponseGuard block.
|
|
147
|
+
|
|
148
|
+
Usage::
|
|
149
|
+
|
|
150
|
+
loop = RejectionFeedbackLoop(guard, store=store, regenerate=regen)
|
|
151
|
+
result = await loop.run(text, initial_result=guard_result,
|
|
152
|
+
target_contact_id=..., ...)
|
|
153
|
+
|
|
154
|
+
``regenerate`` is a deployment-supplied async hook; without one the loop
|
|
155
|
+
cannot revise, so a block simply stands.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(self, guard: Any, store: Optional[RejectionStore] = None,
|
|
159
|
+
regenerate: Optional[RegenerateFn] = None,
|
|
160
|
+
max_retries: int = 1) -> None:
|
|
161
|
+
self._guard = guard
|
|
162
|
+
self._store = store
|
|
163
|
+
self._regenerate = regenerate
|
|
164
|
+
self._max_retries = max(0, int(max_retries))
|
|
165
|
+
|
|
166
|
+
def _record(self, event: Optional[GateRejectionEvent]) -> None:
|
|
167
|
+
if event is None or self._store is None:
|
|
168
|
+
return
|
|
169
|
+
try:
|
|
170
|
+
self._store.record(event)
|
|
171
|
+
except Exception:
|
|
172
|
+
logger.debug("rejection store record failed", exc_info=True)
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def _block_reason(result: Any) -> tuple[str, Optional[str]]:
|
|
176
|
+
for f in getattr(result, "findings", None) or []:
|
|
177
|
+
if getattr(f, "severity", "") == "block":
|
|
178
|
+
return (getattr(f, "check", "blocked"),
|
|
179
|
+
getattr(f, "excerpt", None))
|
|
180
|
+
return ("blocked", None)
|
|
181
|
+
|
|
182
|
+
async def run(self, response_text: str, *,
|
|
183
|
+
initial_result: Any = None,
|
|
184
|
+
contact_display_name: str = "the recipient",
|
|
185
|
+
**eval_kwargs: Any) -> FeedbackLoopResult:
|
|
186
|
+
"""Evaluate (or take ``initial_result``), regenerate on block, re-check.
|
|
187
|
+
|
|
188
|
+
``eval_kwargs`` are forwarded verbatim to ``guard.evaluate`` so the
|
|
189
|
+
re-check runs under exactly the same identity/context as the original.
|
|
190
|
+
"""
|
|
191
|
+
text = response_text
|
|
192
|
+
result = initial_result
|
|
193
|
+
attempts = 0
|
|
194
|
+
event: Optional[GateRejectionEvent] = None
|
|
195
|
+
# Tracks the LAST KNOWN guard verdict so the exception handler can
|
|
196
|
+
# honor the error contract (closed on block, open on allow).
|
|
197
|
+
blocked = bool(getattr(result, "blocked", True))
|
|
198
|
+
try:
|
|
199
|
+
if result is None:
|
|
200
|
+
result = await self._guard.evaluate(
|
|
201
|
+
response_text=text, **eval_kwargs)
|
|
202
|
+
attempts = 1
|
|
203
|
+
blocked = result.blocked
|
|
204
|
+
|
|
205
|
+
for retry in range(self._max_retries + 1):
|
|
206
|
+
if not result.blocked:
|
|
207
|
+
if event is not None:
|
|
208
|
+
event.eventually_succeeded = True
|
|
209
|
+
self._record(event)
|
|
210
|
+
return FeedbackLoopResult(passed=True, payload=text,
|
|
211
|
+
attempts=max(attempts, 1))
|
|
212
|
+
|
|
213
|
+
reason, excerpt = self._block_reason(result)
|
|
214
|
+
event = GateRejectionEvent(
|
|
215
|
+
session_id=str(eval_kwargs.get("session_id", "")),
|
|
216
|
+
contact_id=str(eval_kwargs.get("target_contact_id", "")),
|
|
217
|
+
trust_tier=str(eval_kwargs.get("trust_tier", "")),
|
|
218
|
+
blocking_layer=0,
|
|
219
|
+
block_reason=reason,
|
|
220
|
+
retry_number=retry,
|
|
221
|
+
turn_id=str(eval_kwargs.get("turn_id", "")),
|
|
222
|
+
)
|
|
223
|
+
self._record(event)
|
|
224
|
+
|
|
225
|
+
if retry >= self._max_retries or self._regenerate is None:
|
|
226
|
+
break
|
|
227
|
+
|
|
228
|
+
notice = RejectionNotice(
|
|
229
|
+
notice_id=str(uuid.uuid4()),
|
|
230
|
+
turn_id=str(eval_kwargs.get("turn_id", "")),
|
|
231
|
+
session_id=str(eval_kwargs.get("session_id", "")),
|
|
232
|
+
contact_display_name=contact_display_name,
|
|
233
|
+
block_reason_type=reason,
|
|
234
|
+
blocking_layer=0,
|
|
235
|
+
redacted_excerpt=excerpt,
|
|
236
|
+
trust_tier=str(eval_kwargs.get("trust_tier", "")),
|
|
237
|
+
retry_number=retry + 1,
|
|
238
|
+
)
|
|
239
|
+
new_text = await self._regenerate(
|
|
240
|
+
notice.to_llm_prompt_fragment(), text)
|
|
241
|
+
if not new_text or not str(new_text).strip():
|
|
242
|
+
break
|
|
243
|
+
text = str(new_text).strip()
|
|
244
|
+
result = await self._guard.evaluate(
|
|
245
|
+
response_text=text, **eval_kwargs)
|
|
246
|
+
attempts += 1
|
|
247
|
+
blocked = result.blocked
|
|
248
|
+
|
|
249
|
+
return FeedbackLoopResult(passed=False, payload=None,
|
|
250
|
+
attempts=max(attempts, 1))
|
|
251
|
+
except Exception:
|
|
252
|
+
logger.warning("RejectionFeedbackLoop internal error", exc_info=True)
|
|
253
|
+
if not blocked:
|
|
254
|
+
# allow verdict stands: fail open on the allow side
|
|
255
|
+
return FeedbackLoopResult(passed=True, payload=text,
|
|
256
|
+
attempts=max(attempts, 1))
|
|
257
|
+
# a guard BLOCK is never overridden by a loop error
|
|
258
|
+
return FeedbackLoopResult(passed=False, payload=None,
|
|
259
|
+
attempts=max(attempts, 1))
|