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,700 @@
|
|
|
1
|
+
"""ResponseGuard — the rebuilt outbound response gate for the messaging path.
|
|
2
|
+
|
|
3
|
+
A focused replacement for the dormant 7-layer ``ResponseGate`` (which had a dead
|
|
4
|
+
cross-context layer, a recipient layer that blocks anything without a pre-registered
|
|
5
|
+
session, and an LLM review + send delay that don't belong in a live chat hot path).
|
|
6
|
+
|
|
7
|
+
ResponseGuard runs a small set of FAST, deterministic checks plus a provenance-based
|
|
8
|
+
cross-context leak check, in one of two modes on an explicit outbound surface:
|
|
9
|
+
|
|
10
|
+
* ``shadow`` — evaluate and report findings, but never change the outcome (ALLOW).
|
|
11
|
+
Used to observe real traffic and tune before enforcing.
|
|
12
|
+
* ``enforce`` — a blocking finding yields REVISE (caller regenerates once, then
|
|
13
|
+
suppresses on repeat).
|
|
14
|
+
|
|
15
|
+
Contract guarantees:
|
|
16
|
+
* shadow evaluation remains fail-open and observational;
|
|
17
|
+
* enforce evaluation on a guarded text/artifact surface fails closed when a
|
|
18
|
+
configured check is unavailable;
|
|
19
|
+
* real-time speech is excluded by exact surface type, never by a caller-chosen
|
|
20
|
+
gateway label; and
|
|
21
|
+
* a request may strengthen shadow to enforce but cannot weaken configured enforce.
|
|
22
|
+
|
|
23
|
+
The deterministic checks reuse the existing layer implementations (PII, trust tier,
|
|
24
|
+
injection); the cross-context check is an injected, provenance-backed dependency
|
|
25
|
+
(``cross_context``) so this module stays decoupled from the memory/provenance layer.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import logging
|
|
31
|
+
import os
|
|
32
|
+
import time
|
|
33
|
+
from collections import deque
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
from enum import Enum
|
|
37
|
+
from hashlib import sha256
|
|
38
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence
|
|
39
|
+
|
|
40
|
+
from apsimo.gate.config import GateConfig
|
|
41
|
+
from apsimo.gate.communication_policy import CommunicationPolicyContextV1
|
|
42
|
+
from apsimo.gate.layers.l2_pii import PIIScanner
|
|
43
|
+
from apsimo.gate.layers.l4_trust_tier import TrustTierChecker
|
|
44
|
+
from apsimo.gate.layers.l5_injection import InjectionDetector
|
|
45
|
+
from apsimo.gate.models import GatePayload
|
|
46
|
+
from apsimo.gate.surface_policy import (
|
|
47
|
+
POLICY_DIGEST,
|
|
48
|
+
ResponseGuardSurfacePolicyV1,
|
|
49
|
+
SurfacePolicyDecisionV1,
|
|
50
|
+
)
|
|
51
|
+
from apsimo.intelligence.relationships.trust_tiers import TrustTier
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
_guard_audit = logging.getLogger("colony.gate.guard")
|
|
55
|
+
|
|
56
|
+
# Contact-layer tiers that the 5-value gate enum does not carry map to the nearest
|
|
57
|
+
# gate tier so disclosure gating still applies (acquaintance/unknown -> peripheral).
|
|
58
|
+
_CONTACT_TO_GATE_TIER = {
|
|
59
|
+
"acquaintance": TrustTier.PERIPHERAL,
|
|
60
|
+
"unknown": TrustTier.PERIPHERAL,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def to_gate_tier(value: Any) -> TrustTier:
|
|
65
|
+
"""Coerce a tier (TrustTier or contact-layer string) to a gate TrustTier."""
|
|
66
|
+
if isinstance(value, TrustTier):
|
|
67
|
+
return value
|
|
68
|
+
try:
|
|
69
|
+
return TrustTier(value)
|
|
70
|
+
except (ValueError, TypeError):
|
|
71
|
+
return _CONTACT_TO_GATE_TIER.get(str(value), TrustTier.REGULAR)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class GuardMode(str, Enum):
|
|
75
|
+
SHADOW = "shadow"
|
|
76
|
+
ENFORCE = "enforce"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def enforce_allowlist() -> Optional[frozenset]:
|
|
80
|
+
"""Checks allowed to BLOCK in enforce mode (per-check enforce ramp).
|
|
81
|
+
|
|
82
|
+
COLONY_GUARD_ENFORCE_CHECKS, default ``secret_leak,tom2_epistemic`` —
|
|
83
|
+
the lowest false-positive checks go first; everything else keeps shadow
|
|
84
|
+
semantics (observed, audited, never suppressing) until explicitly
|
|
85
|
+
added. ``tom2_epistemic`` ships allowlisted because it is INERT until a
|
|
86
|
+
level-2 injection registers a taint (zero findings, zero false
|
|
87
|
+
positives, on all other traffic). It is one level-2 prerequisite, but is
|
|
88
|
+
not sufficient without a receipt-backed applied-output evidence probe.
|
|
89
|
+
``all`` / ``*`` restores full enforcement across every check (legacy).
|
|
90
|
+
Returns None for "all checks", else a frozenset of check names.
|
|
91
|
+
"""
|
|
92
|
+
raw = os.environ.get("COLONY_GUARD_ENFORCE_CHECKS",
|
|
93
|
+
"secret_leak,tom2_epistemic").strip()
|
|
94
|
+
if raw.lower() in ("all", "*"):
|
|
95
|
+
return None
|
|
96
|
+
return frozenset(c.strip() for c in raw.split(",") if c.strip())
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def breaker_enabled() -> bool:
|
|
100
|
+
"""COLONY_GUARD_BREAKER, default on. off/0/false disables the breaker."""
|
|
101
|
+
return os.environ.get("COLONY_GUARD_BREAKER", "on").strip().lower() not in (
|
|
102
|
+
"off", "0", "false", "no")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def breaker_trip_blocks() -> int:
|
|
106
|
+
"""Enforce blocks within 24h that trip the breaker (default 10).
|
|
107
|
+
<=0 disables tripping."""
|
|
108
|
+
try:
|
|
109
|
+
return int(os.environ.get("COLONY_GUARD_TRIP_BLOCKS", "10"))
|
|
110
|
+
except (TypeError, ValueError):
|
|
111
|
+
return 10
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class GuardDecision(str, Enum):
|
|
115
|
+
ALLOW = "allow"
|
|
116
|
+
REVISE = "revise"
|
|
117
|
+
BLOCK = "block"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class GuardFinding:
|
|
122
|
+
check: str # secret_leak | disclosure_tier | injection | cross_context
|
|
123
|
+
severity: str # "block" (would suppress in enforce) | "warn" (advisory)
|
|
124
|
+
reason: str
|
|
125
|
+
excerpt: Optional[str] = None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class GuardResult:
|
|
130
|
+
decision: str
|
|
131
|
+
mode: str
|
|
132
|
+
findings: List[GuardFinding] = field(default_factory=list)
|
|
133
|
+
evaluated_at: datetime = field(default_factory=lambda: datetime.now(tz=timezone.utc))
|
|
134
|
+
surface: str = "text_chat"
|
|
135
|
+
surface_family: str = "text"
|
|
136
|
+
applicability: str = "guarded"
|
|
137
|
+
guard_status: str = "evaluated"
|
|
138
|
+
policy_id: str = "response-guard-surface-policy-v1"
|
|
139
|
+
policy_digest: str = POLICY_DIGEST
|
|
140
|
+
candidate_digest: str = ""
|
|
141
|
+
communication_policy_digest: Optional[str] = None
|
|
142
|
+
communication_context_digest: Optional[str] = None
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def blocked(self) -> bool:
|
|
146
|
+
return self.decision in (GuardDecision.REVISE.value, GuardDecision.BLOCK.value)
|
|
147
|
+
|
|
148
|
+
def to_dict(self) -> dict:
|
|
149
|
+
result = {
|
|
150
|
+
"decision": self.decision,
|
|
151
|
+
"mode": self.mode,
|
|
152
|
+
"surface": self.surface,
|
|
153
|
+
"surface_family": self.surface_family,
|
|
154
|
+
"applicability": self.applicability,
|
|
155
|
+
"guard_status": self.guard_status,
|
|
156
|
+
"policy_id": self.policy_id,
|
|
157
|
+
"policy_digest": self.policy_digest,
|
|
158
|
+
"candidate_digest": self.candidate_digest,
|
|
159
|
+
"findings": [
|
|
160
|
+
{"check": f.check, "severity": f.severity, "reason": f.reason,
|
|
161
|
+
"excerpt": f.excerpt}
|
|
162
|
+
for f in self.findings
|
|
163
|
+
],
|
|
164
|
+
}
|
|
165
|
+
# Preserve the legacy exact result shape when no communication policy
|
|
166
|
+
# was supplied. New clients opt into exactly two binding fields.
|
|
167
|
+
if self.communication_policy_digest is not None:
|
|
168
|
+
result["communication_policy_digest"] = self.communication_policy_digest
|
|
169
|
+
result["communication_context_digest"] = self.communication_context_digest
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _result_for_surface(
|
|
174
|
+
surface_decision: SurfacePolicyDecisionV1,
|
|
175
|
+
*,
|
|
176
|
+
decision: str,
|
|
177
|
+
findings: List[GuardFinding],
|
|
178
|
+
guard_status: str,
|
|
179
|
+
candidate_digest: str,
|
|
180
|
+
communication_policy: Optional[CommunicationPolicyContextV1] = None,
|
|
181
|
+
) -> GuardResult:
|
|
182
|
+
if decision not in {item.value for item in GuardDecision}:
|
|
183
|
+
raise ValueError("invalid guard result decision")
|
|
184
|
+
return GuardResult(
|
|
185
|
+
decision=decision,
|
|
186
|
+
mode=surface_decision.effective_mode,
|
|
187
|
+
findings=findings,
|
|
188
|
+
surface=surface_decision.surface,
|
|
189
|
+
surface_family=surface_decision.family,
|
|
190
|
+
applicability=surface_decision.disposition,
|
|
191
|
+
guard_status=guard_status,
|
|
192
|
+
policy_id=surface_decision.policy_id,
|
|
193
|
+
policy_digest=surface_decision.policy_digest,
|
|
194
|
+
candidate_digest=candidate_digest,
|
|
195
|
+
communication_policy_digest=(
|
|
196
|
+
communication_policy.policy_digest
|
|
197
|
+
if communication_policy is not None
|
|
198
|
+
else None
|
|
199
|
+
),
|
|
200
|
+
communication_context_digest=(
|
|
201
|
+
communication_policy.context_digest
|
|
202
|
+
if communication_policy is not None
|
|
203
|
+
else None
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def response_text_digest(response_text: Any) -> str:
|
|
209
|
+
"""Stable identity of the exact Unicode candidate evaluated/released."""
|
|
210
|
+
|
|
211
|
+
return sha256(str(response_text or "").encode("utf-8")).hexdigest()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def unavailable_guard_result(
|
|
215
|
+
*,
|
|
216
|
+
surface: str,
|
|
217
|
+
configured_mode: Any,
|
|
218
|
+
requested_mode: Optional[Any] = None,
|
|
219
|
+
response_text: str = "",
|
|
220
|
+
surface_policy: Optional[ResponseGuardSurfacePolicyV1] = None,
|
|
221
|
+
communication_policy: Optional[CommunicationPolicyContextV1] = None,
|
|
222
|
+
) -> GuardResult:
|
|
223
|
+
"""Return the deterministic result for a configured guard outage.
|
|
224
|
+
|
|
225
|
+
Shadow text/artifact checks remain observational and allow. Enforce
|
|
226
|
+
text/artifact checks block. Speech remains excluded and is never routed
|
|
227
|
+
through guard evaluation.
|
|
228
|
+
"""
|
|
229
|
+
policy = surface_policy or ResponseGuardSurfacePolicyV1()
|
|
230
|
+
surface_decision = policy.resolve(
|
|
231
|
+
surface,
|
|
232
|
+
configured_mode=configured_mode,
|
|
233
|
+
requested_mode=requested_mode,
|
|
234
|
+
)
|
|
235
|
+
if not surface_decision.guarded:
|
|
236
|
+
return _result_for_surface(
|
|
237
|
+
surface_decision,
|
|
238
|
+
decision=GuardDecision.ALLOW.value,
|
|
239
|
+
findings=[],
|
|
240
|
+
guard_status="bypassed",
|
|
241
|
+
candidate_digest=response_text_digest(response_text),
|
|
242
|
+
communication_policy=communication_policy,
|
|
243
|
+
)
|
|
244
|
+
enforce = surface_decision.effective_mode == GuardMode.ENFORCE.value
|
|
245
|
+
return _result_for_surface(
|
|
246
|
+
surface_decision,
|
|
247
|
+
decision=(
|
|
248
|
+
GuardDecision.BLOCK.value
|
|
249
|
+
if enforce else GuardDecision.ALLOW.value
|
|
250
|
+
),
|
|
251
|
+
findings=[GuardFinding(
|
|
252
|
+
check="guard_unavailable",
|
|
253
|
+
severity="block" if enforce else "warn",
|
|
254
|
+
reason="response guard unavailable",
|
|
255
|
+
)],
|
|
256
|
+
guard_status="degraded",
|
|
257
|
+
candidate_digest=response_text_digest(response_text),
|
|
258
|
+
communication_policy=communication_policy,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class CrossContextGuard:
|
|
263
|
+
"""Interface for the provenance-based cross-context leak check (filled in by the
|
|
264
|
+
memory/provenance layer). A no-op default so ResponseGuard works before it lands."""
|
|
265
|
+
|
|
266
|
+
async def check(self, *, response_text: str, conversation_key: Optional[str],
|
|
267
|
+
mentioned_entities: Sequence[str],
|
|
268
|
+
communication_policy: Optional[
|
|
269
|
+
CommunicationPolicyContextV1
|
|
270
|
+
] = None) -> List[GuardFinding]:
|
|
271
|
+
return []
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class ResponseGuard:
|
|
275
|
+
def __init__(self, config: Optional[GateConfig] = None,
|
|
276
|
+
cross_context: Optional[CrossContextGuard] = None,
|
|
277
|
+
default_mode: GuardMode = GuardMode.SHADOW,
|
|
278
|
+
excluded_gateways: Optional[Iterable[str]] = None,
|
|
279
|
+
audit_store: Optional[Any] = None,
|
|
280
|
+
tom2_epistemic: Optional[Any] = None,
|
|
281
|
+
surface_policy: Optional[ResponseGuardSurfacePolicyV1] = None) -> None:
|
|
282
|
+
self._config = config or GateConfig()
|
|
283
|
+
self._audit = audit_store
|
|
284
|
+
# Retained only as migration telemetry. Gateway names are not authority
|
|
285
|
+
# to bypass a content guard; exact SurfacePolicyV1 classification is.
|
|
286
|
+
self._legacy_excluded_gateways = frozenset(
|
|
287
|
+
(g or "").lower() for g in (excluded_gateways or ())
|
|
288
|
+
)
|
|
289
|
+
if self._legacy_excluded_gateways:
|
|
290
|
+
logger.warning(
|
|
291
|
+
"ResponseGuard ignored legacy excluded_gateways=%s; "
|
|
292
|
+
"use an exact outbound surface",
|
|
293
|
+
sorted(self._legacy_excluded_gateways),
|
|
294
|
+
)
|
|
295
|
+
self._surface_policy = surface_policy or ResponseGuardSurfacePolicyV1()
|
|
296
|
+
self._pii = PIIScanner(self._config)
|
|
297
|
+
self._tier = TrustTierChecker(self._config)
|
|
298
|
+
try:
|
|
299
|
+
self._injection: Optional[InjectionDetector] = InjectionDetector(self._config)
|
|
300
|
+
except Exception as exc: # ruleset load failure must not break the guard
|
|
301
|
+
logger.warning("ResponseGuard: injection detector unavailable: %s", exc)
|
|
302
|
+
self._injection = None
|
|
303
|
+
self._cross = cross_context
|
|
304
|
+
# tom2 epistemic egress net (L3.2): injected like cross_context so
|
|
305
|
+
# this module stays decoupled from the taint/tom layer. Inert (zero
|
|
306
|
+
# findings) whenever no level-2 injection taint is live.
|
|
307
|
+
self._tom2_epistemic = tom2_epistemic
|
|
308
|
+
self._default_mode = default_mode
|
|
309
|
+
# Circuit breaker state: timestamps of enforce-mode blocks (24h
|
|
310
|
+
# rolling window). The breaker only ever WEAKENS enforcement (open =
|
|
311
|
+
# fall back to shadow semantics); it can never latch INTO enforce.
|
|
312
|
+
self._block_times: deque = deque()
|
|
313
|
+
|
|
314
|
+
async def evaluate(
|
|
315
|
+
self,
|
|
316
|
+
*,
|
|
317
|
+
surface: str,
|
|
318
|
+
response_text: str,
|
|
319
|
+
incoming_message_text: str = "",
|
|
320
|
+
trust_tier: Any = TrustTier.REGULAR,
|
|
321
|
+
target_contact_id: str = "",
|
|
322
|
+
target_gateway: str = "",
|
|
323
|
+
session_id: str = "",
|
|
324
|
+
turn_id: str = "",
|
|
325
|
+
mentioned_entities: Optional[Sequence[str]] = None,
|
|
326
|
+
conversation_key: Optional[str] = None,
|
|
327
|
+
mode: Optional[GuardMode] = None,
|
|
328
|
+
authorized: bool = False,
|
|
329
|
+
communication_policy: Optional[CommunicationPolicyContextV1] = None,
|
|
330
|
+
) -> GuardResult:
|
|
331
|
+
if communication_policy is not None:
|
|
332
|
+
if not isinstance(communication_policy, CommunicationPolicyContextV1):
|
|
333
|
+
raise TypeError(
|
|
334
|
+
"communication_policy must be CommunicationPolicyContextV1"
|
|
335
|
+
)
|
|
336
|
+
# Pydantic's trusted ``model_construct``/``model_copy(update=...)``
|
|
337
|
+
# APIs can create an instance without field validation. Re-parse
|
|
338
|
+
# the canonical wire mapping at the effect boundary rather than
|
|
339
|
+
# treating isinstance() as sufficient attestation.
|
|
340
|
+
communication_policy = CommunicationPolicyContextV1.model_validate(
|
|
341
|
+
communication_policy.canonical_dict()
|
|
342
|
+
)
|
|
343
|
+
if target_contact_id != communication_policy.target_contact_id:
|
|
344
|
+
raise ValueError(
|
|
345
|
+
"communication policy target must match target_contact_id"
|
|
346
|
+
)
|
|
347
|
+
surface_decision = self._surface_policy.resolve(
|
|
348
|
+
surface,
|
|
349
|
+
configured_mode=self._default_mode,
|
|
350
|
+
requested_mode=mode,
|
|
351
|
+
)
|
|
352
|
+
if not surface_decision.guarded:
|
|
353
|
+
result = self._surface_result(
|
|
354
|
+
surface_decision,
|
|
355
|
+
decision=GuardDecision.ALLOW.value,
|
|
356
|
+
findings=[],
|
|
357
|
+
guard_status="bypassed",
|
|
358
|
+
response_text=response_text,
|
|
359
|
+
communication_policy=communication_policy,
|
|
360
|
+
)
|
|
361
|
+
self._audit_communication_policy(
|
|
362
|
+
result,
|
|
363
|
+
communication_policy=communication_policy,
|
|
364
|
+
conversation_key=conversation_key,
|
|
365
|
+
target_contact_id=target_contact_id,
|
|
366
|
+
target_gateway=target_gateway,
|
|
367
|
+
authorized=authorized,
|
|
368
|
+
response_text=response_text,
|
|
369
|
+
)
|
|
370
|
+
return result
|
|
371
|
+
effective_mode = GuardMode(surface_decision.effective_mode)
|
|
372
|
+
try:
|
|
373
|
+
tier = to_gate_tier(trust_tier)
|
|
374
|
+
payload = GatePayload(
|
|
375
|
+
response_text=response_text or "",
|
|
376
|
+
target_contact_id=target_contact_id,
|
|
377
|
+
target_gateway=target_gateway,
|
|
378
|
+
session_id=session_id,
|
|
379
|
+
trust_tier=tier,
|
|
380
|
+
mentioned_entities=frozenset(mentioned_entities or []),
|
|
381
|
+
turn_id=turn_id,
|
|
382
|
+
incoming_message_text=incoming_message_text or "",
|
|
383
|
+
communication_policy=communication_policy,
|
|
384
|
+
)
|
|
385
|
+
findings: List[GuardFinding] = []
|
|
386
|
+
unavailable: List[str] = []
|
|
387
|
+
|
|
388
|
+
observed, failed = await self._run_check(
|
|
389
|
+
"secret_leak", "block", self._pii, payload,
|
|
390
|
+
)
|
|
391
|
+
findings += observed
|
|
392
|
+
if failed:
|
|
393
|
+
unavailable.append("secret_leak")
|
|
394
|
+
observed, failed = await self._run_check(
|
|
395
|
+
"disclosure_tier", "block", self._tier, payload,
|
|
396
|
+
)
|
|
397
|
+
findings += observed
|
|
398
|
+
if failed:
|
|
399
|
+
unavailable.append("disclosure_tier")
|
|
400
|
+
if self._injection is not None:
|
|
401
|
+
observed, failed = await self._run_check(
|
|
402
|
+
"injection", "warn", self._injection, payload,
|
|
403
|
+
)
|
|
404
|
+
findings += observed
|
|
405
|
+
if failed:
|
|
406
|
+
unavailable.append("injection")
|
|
407
|
+
else:
|
|
408
|
+
# Configured-but-absent: the guard always configures the
|
|
409
|
+
# injection detector in __init__; None means its ruleset
|
|
410
|
+
# failed to load. Enforce must fail closed on that gap,
|
|
411
|
+
# shadow observes it (warn finding), never a silent skip.
|
|
412
|
+
unavailable.append("injection")
|
|
413
|
+
if self._cross is not None:
|
|
414
|
+
try:
|
|
415
|
+
cross_kwargs = {
|
|
416
|
+
"response_text": response_text or "",
|
|
417
|
+
"conversation_key": conversation_key,
|
|
418
|
+
"mentioned_entities": list(mentioned_entities or []),
|
|
419
|
+
}
|
|
420
|
+
# Legacy check implementations retain their exact call
|
|
421
|
+
# shape for legacy evaluations. A policy-bound evaluation
|
|
422
|
+
# requires a checker to accept the immutable policy input;
|
|
423
|
+
# refusal becomes configured-check unavailability.
|
|
424
|
+
if communication_policy is not None:
|
|
425
|
+
cross_kwargs["communication_policy"] = communication_policy
|
|
426
|
+
findings += list(await self._cross.check(**cross_kwargs))
|
|
427
|
+
except Exception as exc:
|
|
428
|
+
logger.warning("ResponseGuard: cross_context check failed: %s", exc)
|
|
429
|
+
unavailable.append("cross_context")
|
|
430
|
+
if self._tom2_epistemic is not None:
|
|
431
|
+
try:
|
|
432
|
+
findings += list(await self._tom2_epistemic.check(
|
|
433
|
+
response_text=response_text or "",
|
|
434
|
+
conversation_key=conversation_key))
|
|
435
|
+
except Exception as exc:
|
|
436
|
+
logger.warning("ResponseGuard: tom2_epistemic check failed: %s", exc)
|
|
437
|
+
unavailable.append("tom2_epistemic")
|
|
438
|
+
|
|
439
|
+
if unavailable:
|
|
440
|
+
findings.append(GuardFinding(
|
|
441
|
+
check="guard_unavailable",
|
|
442
|
+
severity=(
|
|
443
|
+
"block"
|
|
444
|
+
if effective_mode == GuardMode.ENFORCE
|
|
445
|
+
else "warn"
|
|
446
|
+
),
|
|
447
|
+
reason="configured checks unavailable: " + ",".join(unavailable),
|
|
448
|
+
))
|
|
449
|
+
|
|
450
|
+
# Owner-directed (authorized) cross-context transfers are legitimate, not leaks:
|
|
451
|
+
# downgrade them so enforce never blocks them, while still keeping the audit row.
|
|
452
|
+
cross = [f for f in findings if f.check == "cross_context"]
|
|
453
|
+
if authorized:
|
|
454
|
+
for f in cross:
|
|
455
|
+
f.severity = "info"
|
|
456
|
+
f.reason = "authorized owner-directed transfer: " + f.reason
|
|
457
|
+
|
|
458
|
+
fatal_unavailable = bool(
|
|
459
|
+
unavailable and effective_mode == GuardMode.ENFORCE
|
|
460
|
+
)
|
|
461
|
+
decision = (
|
|
462
|
+
GuardDecision.BLOCK.value
|
|
463
|
+
if fatal_unavailable
|
|
464
|
+
else self._decide(findings, effective_mode)
|
|
465
|
+
)
|
|
466
|
+
# Circuit breaker (fails open, never INTO enforce): repeated
|
|
467
|
+
# enforce blocks in a 24h window suspend suppression — the guard
|
|
468
|
+
# keeps evaluating and auditing, but allows, until the window
|
|
469
|
+
# slides. A breaker fault leaves the decision unchanged.
|
|
470
|
+
if decision != GuardDecision.ALLOW.value and not fatal_unavailable:
|
|
471
|
+
try:
|
|
472
|
+
if self._breaker_open():
|
|
473
|
+
logger.warning(
|
|
474
|
+
"ResponseGuard breaker OPEN (%d blocks/24h >= %d)"
|
|
475
|
+
" — enforce suspended, allowing (would be %s)",
|
|
476
|
+
len(self._block_times), breaker_trip_blocks(),
|
|
477
|
+
decision)
|
|
478
|
+
decision = GuardDecision.ALLOW.value
|
|
479
|
+
else:
|
|
480
|
+
self._block_times.append(time.time())
|
|
481
|
+
except Exception:
|
|
482
|
+
logger.debug("guard breaker check failed (decision "
|
|
483
|
+
"unchanged)", exc_info=True)
|
|
484
|
+
result = self._surface_result(
|
|
485
|
+
surface_decision,
|
|
486
|
+
decision=decision,
|
|
487
|
+
findings=findings,
|
|
488
|
+
guard_status="degraded" if unavailable else "evaluated",
|
|
489
|
+
response_text=response_text,
|
|
490
|
+
communication_policy=communication_policy,
|
|
491
|
+
)
|
|
492
|
+
# Audit trail: count EVERY evaluation (the rate denominator), and record a
|
|
493
|
+
# row for any evaluation with findings or a non-allow decision — all checks,
|
|
494
|
+
# not just cross_context — so per-check rates and the would-block rate can
|
|
495
|
+
# be measured in shadow before enforcement is turned on.
|
|
496
|
+
if self._audit is not None:
|
|
497
|
+
try:
|
|
498
|
+
self._audit.count_evaluation()
|
|
499
|
+
if findings or decision != GuardDecision.ALLOW.value:
|
|
500
|
+
self._audit.record(
|
|
501
|
+
conversation_key=conversation_key, mode=result.mode, decision=decision,
|
|
502
|
+
authorized=authorized, checks=[f.check for f in findings],
|
|
503
|
+
entities=[f.excerpt or "" for f in findings],
|
|
504
|
+
response_text=response_text or "",
|
|
505
|
+
would_block=any(f.severity == "block" for f in findings),
|
|
506
|
+
gateway=target_gateway or None,
|
|
507
|
+
surface=surface_decision.surface,
|
|
508
|
+
policy_id=surface_decision.policy_id,
|
|
509
|
+
policy_digest=surface_decision.policy_digest,
|
|
510
|
+
guard_status=result.guard_status)
|
|
511
|
+
except Exception:
|
|
512
|
+
logger.debug("guard audit record failed", exc_info=True)
|
|
513
|
+
self._audit_communication_policy(
|
|
514
|
+
result,
|
|
515
|
+
communication_policy=communication_policy,
|
|
516
|
+
conversation_key=conversation_key,
|
|
517
|
+
target_contact_id=target_contact_id,
|
|
518
|
+
target_gateway=target_gateway,
|
|
519
|
+
authorized=authorized,
|
|
520
|
+
response_text=response_text,
|
|
521
|
+
)
|
|
522
|
+
if findings:
|
|
523
|
+
_guard_audit.info(
|
|
524
|
+
"guard mode=%s decision=%s contact=%s turn=%s findings=%s",
|
|
525
|
+
result.mode, decision, target_contact_id, turn_id,
|
|
526
|
+
[f"{f.check}:{f.severity}" for f in findings],
|
|
527
|
+
)
|
|
528
|
+
return result
|
|
529
|
+
except Exception as exc:
|
|
530
|
+
logger.warning("ResponseGuard evaluation error: %s", exc)
|
|
531
|
+
enforce = effective_mode == GuardMode.ENFORCE
|
|
532
|
+
result = self._surface_result(
|
|
533
|
+
surface_decision,
|
|
534
|
+
decision=(
|
|
535
|
+
GuardDecision.BLOCK.value
|
|
536
|
+
if enforce else GuardDecision.ALLOW.value
|
|
537
|
+
),
|
|
538
|
+
findings=[GuardFinding(
|
|
539
|
+
check="guard_unavailable",
|
|
540
|
+
severity="block" if enforce else "warn",
|
|
541
|
+
reason="response guard evaluation unavailable",
|
|
542
|
+
)],
|
|
543
|
+
guard_status="degraded",
|
|
544
|
+
response_text=response_text,
|
|
545
|
+
communication_policy=communication_policy,
|
|
546
|
+
)
|
|
547
|
+
self._audit_communication_policy(
|
|
548
|
+
result,
|
|
549
|
+
communication_policy=communication_policy,
|
|
550
|
+
conversation_key=conversation_key,
|
|
551
|
+
target_contact_id=target_contact_id,
|
|
552
|
+
target_gateway=target_gateway,
|
|
553
|
+
authorized=authorized,
|
|
554
|
+
response_text=response_text,
|
|
555
|
+
)
|
|
556
|
+
return result
|
|
557
|
+
|
|
558
|
+
@staticmethod
|
|
559
|
+
def _surface_result(
|
|
560
|
+
surface_decision: SurfacePolicyDecisionV1,
|
|
561
|
+
*,
|
|
562
|
+
decision: str,
|
|
563
|
+
findings: List[GuardFinding],
|
|
564
|
+
guard_status: str,
|
|
565
|
+
response_text: str,
|
|
566
|
+
communication_policy: Optional[CommunicationPolicyContextV1] = None,
|
|
567
|
+
) -> GuardResult:
|
|
568
|
+
return _result_for_surface(
|
|
569
|
+
surface_decision,
|
|
570
|
+
decision=decision,
|
|
571
|
+
findings=findings,
|
|
572
|
+
guard_status=guard_status,
|
|
573
|
+
candidate_digest=response_text_digest(response_text),
|
|
574
|
+
communication_policy=communication_policy,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
def _audit_communication_policy(
|
|
578
|
+
self,
|
|
579
|
+
result: GuardResult,
|
|
580
|
+
*,
|
|
581
|
+
communication_policy: Optional[CommunicationPolicyContextV1],
|
|
582
|
+
conversation_key: Optional[str],
|
|
583
|
+
target_contact_id: str,
|
|
584
|
+
target_gateway: str,
|
|
585
|
+
authorized: bool,
|
|
586
|
+
response_text: str,
|
|
587
|
+
) -> None:
|
|
588
|
+
"""Persist a digest-bound row for every policy-bound evaluation.
|
|
589
|
+
|
|
590
|
+
This is separate from finding rows so clean constrained messages do
|
|
591
|
+
not inflate ResponseGuard's historical false-positive denominator.
|
|
592
|
+
"""
|
|
593
|
+
|
|
594
|
+
if communication_policy is None or self._audit is None:
|
|
595
|
+
return
|
|
596
|
+
recorder = getattr(self._audit, "record_communication_policy", None)
|
|
597
|
+
if not callable(recorder):
|
|
598
|
+
return
|
|
599
|
+
try:
|
|
600
|
+
recorder(
|
|
601
|
+
conversation_key=conversation_key,
|
|
602
|
+
mode=result.mode,
|
|
603
|
+
decision=result.decision,
|
|
604
|
+
authorized=authorized,
|
|
605
|
+
gateway=target_gateway or None,
|
|
606
|
+
surface=result.surface,
|
|
607
|
+
surface_policy_id=result.policy_id,
|
|
608
|
+
surface_policy_digest=result.policy_digest,
|
|
609
|
+
response_text=response_text or "",
|
|
610
|
+
guard_status=result.guard_status,
|
|
611
|
+
target_contact_id=target_contact_id,
|
|
612
|
+
communication_policy=communication_policy,
|
|
613
|
+
)
|
|
614
|
+
except Exception:
|
|
615
|
+
logger.debug(
|
|
616
|
+
"communication policy audit record failed", exc_info=True
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
async def _run_check(self, name: str, severity: str, checker: Any,
|
|
620
|
+
payload: GatePayload) -> tuple[List[GuardFinding], bool]:
|
|
621
|
+
try:
|
|
622
|
+
r = await checker.check(payload)
|
|
623
|
+
except Exception as exc:
|
|
624
|
+
logger.warning("ResponseGuard: check %s unavailable: %s", name, exc)
|
|
625
|
+
return [], True
|
|
626
|
+
if getattr(r, "blocked", False):
|
|
627
|
+
return ([GuardFinding(check=name, severity=severity,
|
|
628
|
+
reason=getattr(r, "reason", name),
|
|
629
|
+
excerpt=getattr(r, "flagged_excerpt", None))], False)
|
|
630
|
+
return [], False
|
|
631
|
+
|
|
632
|
+
@staticmethod
|
|
633
|
+
def _decide(findings: List[GuardFinding], mode: GuardMode) -> str:
|
|
634
|
+
if getattr(mode, "value", mode) == GuardMode.SHADOW.value:
|
|
635
|
+
return GuardDecision.ALLOW.value # shadow never changes the outcome
|
|
636
|
+
# Per-check enforce allowlist: only allowlisted checks may suppress;
|
|
637
|
+
# everything else keeps shadow semantics inside enforce mode.
|
|
638
|
+
allowed = enforce_allowlist()
|
|
639
|
+
for f in findings:
|
|
640
|
+
if f.severity != "block":
|
|
641
|
+
continue
|
|
642
|
+
if allowed is None or f.check in allowed:
|
|
643
|
+
return GuardDecision.REVISE.value # caller regenerates once, then suppresses
|
|
644
|
+
return GuardDecision.ALLOW.value
|
|
645
|
+
|
|
646
|
+
def _breaker_open(self) -> bool:
|
|
647
|
+
"""True when the enforce circuit breaker is tripped (24h window)."""
|
|
648
|
+
if not breaker_enabled():
|
|
649
|
+
return False
|
|
650
|
+
threshold = breaker_trip_blocks()
|
|
651
|
+
if threshold <= 0:
|
|
652
|
+
return False
|
|
653
|
+
cutoff = time.time() - 86400.0
|
|
654
|
+
while self._block_times and self._block_times[0] < cutoff:
|
|
655
|
+
self._block_times.popleft()
|
|
656
|
+
return len(self._block_times) >= threshold
|
|
657
|
+
|
|
658
|
+
@property
|
|
659
|
+
def configured_mode(self) -> GuardMode:
|
|
660
|
+
"""The server-owned default mode used by direct in-process callers."""
|
|
661
|
+
|
|
662
|
+
return self._default_mode
|
|
663
|
+
|
|
664
|
+
def evidence_probe(self, audit_store: Optional[Any] = None,
|
|
665
|
+
check: str = "tom2_epistemic",
|
|
666
|
+
hours: float = 24.0,
|
|
667
|
+
surface: str = "text_chat"):
|
|
668
|
+
"""Return a conservative probe until applied egress receipts exist.
|
|
669
|
+
|
|
670
|
+
A guard *evaluation* is not proof that a caller actually withheld or
|
|
671
|
+
revised the evaluated bytes. The current audit store records verdicts,
|
|
672
|
+
not digest-bound application receipts, so it must never unlock Tom2
|
|
673
|
+
level 2. The compatibility arguments remain accepted for callers that
|
|
674
|
+
construct this probe, but every result is False until a real egress
|
|
675
|
+
mediator can attest the applied output.
|
|
676
|
+
"""
|
|
677
|
+
|
|
678
|
+
del audit_store, check, hours, surface
|
|
679
|
+
|
|
680
|
+
def probe(gateway: str) -> bool:
|
|
681
|
+
del gateway
|
|
682
|
+
return False
|
|
683
|
+
|
|
684
|
+
return probe
|
|
685
|
+
|
|
686
|
+
def breaker_status(self) -> Dict[str, Any]:
|
|
687
|
+
"""Observability surface for the enforce breaker + allowlist."""
|
|
688
|
+
allowed = enforce_allowlist()
|
|
689
|
+
cutoff = time.time() - 86400.0
|
|
690
|
+
while self._block_times and self._block_times[0] < cutoff:
|
|
691
|
+
self._block_times.popleft()
|
|
692
|
+
return {
|
|
693
|
+
"enabled": breaker_enabled(),
|
|
694
|
+
"trip_blocks": breaker_trip_blocks(),
|
|
695
|
+
"blocks_24h": len(self._block_times),
|
|
696
|
+
"tripped": self._breaker_open(),
|
|
697
|
+
"enforce_checks": (sorted(allowed) if allowed is not None
|
|
698
|
+
else "all"),
|
|
699
|
+
"surface_policy": self._surface_policy.public(),
|
|
700
|
+
}
|