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,1031 @@
|
|
|
1
|
+
"""WorkerGovernor -- server-side enforcement for the distributed job queue
|
|
2
|
+
(cognition program, Phase B item 5).
|
|
3
|
+
|
|
4
|
+
The worker daemon is UNTRUSTED. Capability coverage, boundary compliance, and
|
|
5
|
+
the scope of what a worker actually did are all re-decided HERE, server-side,
|
|
6
|
+
never taken on the worker's word:
|
|
7
|
+
|
|
8
|
+
claim -> evaluate_claim(): re-verify (a) the worker's advertised
|
|
9
|
+
capabilities really cover the job's requirement and (b) the
|
|
10
|
+
job's subject is not under a standing owner boundary
|
|
11
|
+
(DirectiveGuard, capability-aware). A boundaried or
|
|
12
|
+
uncovered claim is refused server-side.
|
|
13
|
+
completion -> audit_report(): cross-check the worker's structured report
|
|
14
|
+
against what the job was authorized to do. A worker that
|
|
15
|
+
reports a mutation on a read-only job (or a force-push, or
|
|
16
|
+
out-of-scope deletes) is a VIOLATION, flagged loudly.
|
|
17
|
+
outcome -> record_outcome(): the audited verdict is the earned-autonomy
|
|
18
|
+
evidence. Each worker JOB TYPE is its own trust domain
|
|
19
|
+
("worker:<job_type>") feeding the self-model/trust engine
|
|
20
|
+
(item 4): clean real completions graduate it, a violation
|
|
21
|
+
trips its circuit breaker. Feedback + journal + skill
|
|
22
|
+
distillation ride the same chokepoint.
|
|
23
|
+
|
|
24
|
+
Mode (COLONY_WORKERS_MODE, default shadow):
|
|
25
|
+
off -> governor disabled; the queue behaves exactly as before.
|
|
26
|
+
shadow -> CALIBRATION. Every claim is evaluated and journaled but ALLOWED
|
|
27
|
+
regardless (would_refuse is recorded, not enforced); only
|
|
28
|
+
explicitly verified completions become shadow competence events.
|
|
29
|
+
Neutral skips/unknowns never graduate a job type. Non-blocking by
|
|
30
|
+
design, so turning it on never disturbs the agent_action path.
|
|
31
|
+
live -> ENFORCING. A refused claim is blocked server-side; outcomes count
|
|
32
|
+
as real evidence toward earned autonomy.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
import hashlib
|
|
39
|
+
import logging
|
|
40
|
+
import os
|
|
41
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
# Reported operations that imply the worker CHANGED something. A worker that
|
|
46
|
+
# reports any of these on a read-only job exceeded its grant.
|
|
47
|
+
_MUTATE_OPS = frozenset({
|
|
48
|
+
"modify_files", "write", "commit", "push", "push_branch", "open_pr",
|
|
49
|
+
"delete", "rm", "execute", "deploy", "send", "post", "create", "update",
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
# Risk tiers (from the action registry) that authorize a job to change state.
|
|
53
|
+
# A job at read_only risk that reports a mutation is a violation.
|
|
54
|
+
_MUTATING_RISK = frozenset({"low", "medium", "high", "outbound", "destructive"})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def job_declares_effect(job: Any) -> bool:
|
|
58
|
+
"""Classify effectful work without treating its label as permission."""
|
|
59
|
+
|
|
60
|
+
payload = getattr(job, "payload", {}) or {}
|
|
61
|
+
risk = str(
|
|
62
|
+
payload.get("risk_class") or payload.get("risk") or ""
|
|
63
|
+
).strip().lower()
|
|
64
|
+
action = str(
|
|
65
|
+
payload.get("action") or payload.get("command")
|
|
66
|
+
or payload.get("action_hint") or ""
|
|
67
|
+
).strip().lower()
|
|
68
|
+
registered_action_effect = False
|
|
69
|
+
action_hint = str(payload.get("action_hint") or "").strip()
|
|
70
|
+
if action_hint:
|
|
71
|
+
try:
|
|
72
|
+
from apsimo.initiatives.action_registry import (
|
|
73
|
+
RiskTier,
|
|
74
|
+
get_action,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
spec = get_action(action_hint)
|
|
78
|
+
registered_action_effect = bool(
|
|
79
|
+
spec is not None and spec.risk is not RiskTier.READ_ONLY
|
|
80
|
+
)
|
|
81
|
+
except Exception:
|
|
82
|
+
# Registry availability is not needed to preserve the other
|
|
83
|
+
# conservative effect signals below.
|
|
84
|
+
registered_action_effect = False
|
|
85
|
+
job_type = getattr(job, "job_type", "")
|
|
86
|
+
job_type = (
|
|
87
|
+
job_type.value if hasattr(job_type, "value") else str(job_type)
|
|
88
|
+
).strip().lower()
|
|
89
|
+
required: set[str] = set()
|
|
90
|
+
try:
|
|
91
|
+
required.update(str(item) for item in job.required_capabilities())
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
extra = str((getattr(job, "tags", {}) or {}).get(
|
|
95
|
+
"required_capability", ""
|
|
96
|
+
)).strip()
|
|
97
|
+
if extra:
|
|
98
|
+
required.add(extra)
|
|
99
|
+
effect_capability = any(
|
|
100
|
+
capability in {
|
|
101
|
+
"agent:tools", "code:execute", "filesystem:write",
|
|
102
|
+
"git:write", "messaging:send", "shell", "terminal:execute",
|
|
103
|
+
}
|
|
104
|
+
or capability.endswith(":write")
|
|
105
|
+
or capability.endswith(":send")
|
|
106
|
+
for capability in required
|
|
107
|
+
)
|
|
108
|
+
return bool(
|
|
109
|
+
payload.get("destructive") is True
|
|
110
|
+
or registered_action_effect
|
|
111
|
+
or effect_capability
|
|
112
|
+
or risk in (_MUTATING_RISK | {"mutation", "disclosure"})
|
|
113
|
+
or job_type == "system_maintenance"
|
|
114
|
+
or action in (_MUTATE_OPS | {
|
|
115
|
+
"disk_cleanup", "log_rotate", "db_vacuum",
|
|
116
|
+
"agent_project_directed", "agent_project_deliver",
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def workers_mode() -> str:
|
|
122
|
+
from apsimo.util.autonomy_preset import resolve
|
|
123
|
+
explicit = os.environ.get("COLONY_WORKERS_MODE")
|
|
124
|
+
if explicit is not None and explicit.strip():
|
|
125
|
+
value = explicit.strip().lower()
|
|
126
|
+
if value not in {"off", "shadow", "live"}:
|
|
127
|
+
raise RuntimeError(
|
|
128
|
+
"COLONY_WORKERS_MODE must be off, shadow, or live"
|
|
129
|
+
)
|
|
130
|
+
return resolve("COLONY_WORKERS_MODE", ("off", "shadow", "live"), "shadow")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class ClaimVerdict:
|
|
135
|
+
"""Immutable authority result consumed by the central queue claim path.
|
|
136
|
+
|
|
137
|
+
Compatibility mapping helpers keep the existing read-only tests and API
|
|
138
|
+
serialization ergonomic, but callers cannot fabricate or mutate a partial
|
|
139
|
+
dictionary. Construction rejects inconsistent mode/boolean combinations.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
mode: str
|
|
143
|
+
allowed: bool
|
|
144
|
+
enforced: bool
|
|
145
|
+
would_refuse: bool
|
|
146
|
+
reason: str
|
|
147
|
+
capability_ok: Optional[bool]
|
|
148
|
+
boundary_ok: Optional[bool]
|
|
149
|
+
boundary_reason: str
|
|
150
|
+
missing_capabilities: tuple[str, ...] = ()
|
|
151
|
+
shadow: bool = False
|
|
152
|
+
trust_ok: Optional[bool] = True
|
|
153
|
+
trust_reason: str = "trust_not_required"
|
|
154
|
+
|
|
155
|
+
def __post_init__(self) -> None:
|
|
156
|
+
if self.mode not in {"off", "shadow", "live"}:
|
|
157
|
+
raise ValueError("invalid worker-governor mode")
|
|
158
|
+
for field_name in ("allowed", "enforced", "would_refuse", "shadow"):
|
|
159
|
+
if type(getattr(self, field_name)) is not bool:
|
|
160
|
+
raise TypeError(f"{field_name} must be an exact boolean")
|
|
161
|
+
for field_name in ("capability_ok", "boundary_ok", "trust_ok"):
|
|
162
|
+
value = getattr(self, field_name)
|
|
163
|
+
if self.mode == "off":
|
|
164
|
+
# Mode off checks NOTHING: the verdict must say "unchecked"
|
|
165
|
+
# (None), never fabricate a pass — and never a bool at all,
|
|
166
|
+
# so an off verdict cannot be mistaken for an evaluated one.
|
|
167
|
+
if value is not None:
|
|
168
|
+
raise TypeError(
|
|
169
|
+
f"{field_name} must be None (unchecked) in mode 'off'"
|
|
170
|
+
)
|
|
171
|
+
elif type(value) is not bool:
|
|
172
|
+
raise TypeError(f"{field_name} must be an exact boolean")
|
|
173
|
+
if not isinstance(self.reason, str) or not isinstance(
|
|
174
|
+
self.boundary_reason, str
|
|
175
|
+
):
|
|
176
|
+
raise TypeError("claim verdict reasons must be strings")
|
|
177
|
+
if not isinstance(self.missing_capabilities, tuple) or any(
|
|
178
|
+
not isinstance(item, str) for item in self.missing_capabilities
|
|
179
|
+
):
|
|
180
|
+
raise TypeError("missing_capabilities must be a tuple of strings")
|
|
181
|
+
|
|
182
|
+
expected_refusal = not (
|
|
183
|
+
self.capability_ok and self.boundary_ok and self.trust_ok
|
|
184
|
+
)
|
|
185
|
+
if self.mode == "off":
|
|
186
|
+
# capability/boundary/trust are already required to be None
|
|
187
|
+
# ("unchecked") above.
|
|
188
|
+
valid = (
|
|
189
|
+
self.allowed and not self.enforced and not self.would_refuse
|
|
190
|
+
and not self.shadow
|
|
191
|
+
)
|
|
192
|
+
elif self.mode == "shadow":
|
|
193
|
+
valid = (
|
|
194
|
+
self.shadow
|
|
195
|
+
and self.enforced == (not self.allowed)
|
|
196
|
+
and self.would_refuse == expected_refusal
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
valid = (
|
|
200
|
+
self.enforced and not self.shadow
|
|
201
|
+
and self.would_refuse == expected_refusal
|
|
202
|
+
and self.allowed == (not expected_refusal)
|
|
203
|
+
)
|
|
204
|
+
if not valid:
|
|
205
|
+
raise ValueError("inconsistent worker claim verdict")
|
|
206
|
+
|
|
207
|
+
def as_dict(self) -> Dict[str, Any]:
|
|
208
|
+
return {
|
|
209
|
+
"mode": self.mode,
|
|
210
|
+
"allowed": self.allowed,
|
|
211
|
+
"enforced": self.enforced,
|
|
212
|
+
"would_refuse": self.would_refuse,
|
|
213
|
+
"reason": self.reason,
|
|
214
|
+
"capability_ok": self.capability_ok,
|
|
215
|
+
"boundary_ok": self.boundary_ok,
|
|
216
|
+
"boundary_reason": self.boundary_reason,
|
|
217
|
+
"missing_capabilities": list(self.missing_capabilities),
|
|
218
|
+
"shadow": self.shadow,
|
|
219
|
+
"trust_ok": self.trust_ok,
|
|
220
|
+
"trust_reason": self.trust_reason,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
def __getitem__(self, key: str) -> Any:
|
|
224
|
+
return self.as_dict()[key]
|
|
225
|
+
|
|
226
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
227
|
+
return self.as_dict().get(key, default)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class WorkerGovernor:
|
|
231
|
+
"""Server-authoritative claim gate + completion audit for queue workers."""
|
|
232
|
+
|
|
233
|
+
def __init__(
|
|
234
|
+
self,
|
|
235
|
+
*,
|
|
236
|
+
directive_manager: Any = None,
|
|
237
|
+
feedback_store: Any = None,
|
|
238
|
+
self_model: Any = None,
|
|
239
|
+
delivery_router: Any = None, # awaitable(payload)->bool, guarded reach-out
|
|
240
|
+
proposal_store: Any = None,
|
|
241
|
+
skill_store: Any = None,
|
|
242
|
+
llm_router: Any = None,
|
|
243
|
+
boundary_required: bool = False,
|
|
244
|
+
) -> None:
|
|
245
|
+
self._directives = directive_manager
|
|
246
|
+
self._feedback = feedback_store
|
|
247
|
+
self._self_model = self_model
|
|
248
|
+
self._deliver = delivery_router
|
|
249
|
+
self._proposal_store = proposal_store
|
|
250
|
+
self._skills = skill_store
|
|
251
|
+
self._llm = llm_router
|
|
252
|
+
self._boundary_required = bool(boundary_required)
|
|
253
|
+
|
|
254
|
+
def ready_for_live_claims(self) -> bool:
|
|
255
|
+
"""Whether this instance is valid live claim authority."""
|
|
256
|
+
|
|
257
|
+
trust = getattr(self._self_model, "trust", None)
|
|
258
|
+
return bool(
|
|
259
|
+
self._boundary_required
|
|
260
|
+
and self._directives is not None
|
|
261
|
+
and self._self_model is not None
|
|
262
|
+
and trust is not None
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# -- trust domain per job type ---------------------------------------
|
|
266
|
+
@staticmethod
|
|
267
|
+
def trust_domain(job_type: str) -> str:
|
|
268
|
+
return f"worker:{(job_type or 'custom').strip().lower()}"
|
|
269
|
+
|
|
270
|
+
# -- job classification (never trust the worker's own claim) ----------
|
|
271
|
+
@staticmethod
|
|
272
|
+
def _job_authorized_to_mutate(job: Any) -> bool:
|
|
273
|
+
"""Was THIS job authorized to change state? Read-only jobs were not.
|
|
274
|
+
|
|
275
|
+
A job carries its risk tier and approval provenance in payload/tags
|
|
276
|
+
(set at post time by the approval policy). read_only risk => not
|
|
277
|
+
authorized; an approved or mutating-risk job => authorized.
|
|
278
|
+
"""
|
|
279
|
+
payload = getattr(job, "payload", {}) or {}
|
|
280
|
+
tags = getattr(job, "tags", {}) or {}
|
|
281
|
+
# Risk/action fields are untrusted classification input. They may
|
|
282
|
+
# increase gating but can never grant mutation authority. Permission
|
|
283
|
+
# must be provenance stamped by the server-owned approval/policy
|
|
284
|
+
# plane and bound to the queued action.
|
|
285
|
+
return bool(
|
|
286
|
+
job_declares_effect(job)
|
|
287
|
+
and (
|
|
288
|
+
tags.get("approved_by")
|
|
289
|
+
or tags.get("auto_approved_by_policy")
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _job_action(self, job: Any):
|
|
294
|
+
"""Build the DirectiveGuard Action for this job's subject.
|
|
295
|
+
|
|
296
|
+
Worker jobs are active autonomous work -> ACT capability (an
|
|
297
|
+
ACT-level "leave X alone" boundary must stop a worker touching X;
|
|
298
|
+
awareness/reads survive). high_risk on any mutating/outbound job so
|
|
299
|
+
the guard fails closed on ambiguity.
|
|
300
|
+
"""
|
|
301
|
+
from apsimo.directives import Action
|
|
302
|
+
payload = getattr(job, "payload", {}) or {}
|
|
303
|
+
subject_keys = (
|
|
304
|
+
"description", "action_hint", "domain", "action", "command",
|
|
305
|
+
"script", "query", "endpoint", "url", "target_path", "path",
|
|
306
|
+
"target", "entity_id", "purpose",
|
|
307
|
+
)
|
|
308
|
+
# Only fields that name the proposed work belong in the boundary
|
|
309
|
+
# subject. Thought jobs also carry prompts, injected directives, and
|
|
310
|
+
# other control context. Passing the whole payload through ``args``
|
|
311
|
+
# made DirectiveGuard index that context as if the worker intended to
|
|
312
|
+
# act on it, so an injected boundary could match its own text and
|
|
313
|
+
# permanently hold an otherwise unrelated job.
|
|
314
|
+
subject_args = {
|
|
315
|
+
key: payload[key]
|
|
316
|
+
for key in subject_keys
|
|
317
|
+
if type(payload.get(key)) in (str, int, float)
|
|
318
|
+
and payload.get(key) not in (None, "")
|
|
319
|
+
}
|
|
320
|
+
text = " ".join(
|
|
321
|
+
str(subject_args[key]) for key in subject_keys
|
|
322
|
+
if key in subject_args
|
|
323
|
+
).strip()
|
|
324
|
+
target = str(
|
|
325
|
+
subject_args.get("target_path") or subject_args.get("path")
|
|
326
|
+
or subject_args.get("endpoint") or subject_args.get("url")
|
|
327
|
+
or subject_args.get("target") or subject_args.get("entity_id")
|
|
328
|
+
or subject_args.get("domain") or ""
|
|
329
|
+
)
|
|
330
|
+
return Action(
|
|
331
|
+
kind="execute_tool",
|
|
332
|
+
text=text or str(getattr(job, "job_type", "")),
|
|
333
|
+
target=target,
|
|
334
|
+
entity_id=str(subject_args.get("entity_id") or ""),
|
|
335
|
+
tool_name=str(
|
|
336
|
+
subject_args.get("action") or subject_args.get("command")
|
|
337
|
+
or getattr(getattr(job, "job_type", None), "value", "")
|
|
338
|
+
or getattr(job, "job_type", "")
|
|
339
|
+
),
|
|
340
|
+
args=subject_args,
|
|
341
|
+
high_risk=self._job_authorized_to_mutate(job),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
@staticmethod
|
|
345
|
+
def _required_caps(job: Any) -> List[str]:
|
|
346
|
+
req: List[str] = []
|
|
347
|
+
try:
|
|
348
|
+
req.extend(job.required_capabilities())
|
|
349
|
+
except Exception:
|
|
350
|
+
pass
|
|
351
|
+
tags = getattr(job, "tags", {}) or {}
|
|
352
|
+
rc = tags.get("required_capability")
|
|
353
|
+
if rc:
|
|
354
|
+
req.append(str(rc))
|
|
355
|
+
return [c for c in req if c]
|
|
356
|
+
|
|
357
|
+
def _read_only_live_trial_status(
|
|
358
|
+
self,
|
|
359
|
+
domain: str,
|
|
360
|
+
) -> tuple[bool, str]:
|
|
361
|
+
"""Authorize one bounded, evidence-backed read-only live trial.
|
|
362
|
+
|
|
363
|
+
``ask_first`` normally requires an owner decision. That creates a
|
|
364
|
+
bootstrap deadlock for no-effect worker domains: calibration can
|
|
365
|
+
prove they are safe enough to leave shadow, but they can never
|
|
366
|
+
produce the real, independently attested outcomes required to earn
|
|
367
|
+
``act_first``. This bridge is deliberately narrower than approval:
|
|
368
|
+
|
|
369
|
+
* it is considered only for jobs already classified as no-effect;
|
|
370
|
+
* the domain needs clean, server-attested no-effect shadow evidence;
|
|
371
|
+
* any real failure, timeout, violation, or evidence gap closes it;
|
|
372
|
+
* every real attempt consumes one of a small, bounded trial budget.
|
|
373
|
+
|
|
374
|
+
Capability, boundary, route, owner, and global kill-switch checks are
|
|
375
|
+
separate claim gates and remain mandatory.
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
store = getattr(self._self_model, "store", None)
|
|
379
|
+
if store is None or not hasattr(store, "events"):
|
|
380
|
+
return False, "read_only_live_trial_store_unavailable"
|
|
381
|
+
try:
|
|
382
|
+
if (
|
|
383
|
+
hasattr(store, "active_evidence_gaps")
|
|
384
|
+
and store.active_evidence_gaps(domain)
|
|
385
|
+
):
|
|
386
|
+
return False, "read_only_live_trial_evidence_gap"
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
ask_min = int(os.environ.get("COLONY_TRUST_ASK_MIN_N", "3"))
|
|
390
|
+
except (TypeError, ValueError):
|
|
391
|
+
ask_min = 3
|
|
392
|
+
ask_min = max(1, min(ask_min, 100))
|
|
393
|
+
try:
|
|
394
|
+
trial_max = int(os.environ.get(
|
|
395
|
+
"COLONY_TRUST_READ_ONLY_TRIAL_MAX",
|
|
396
|
+
os.environ.get("COLONY_TRUST_ACT_MIN_N", "5"),
|
|
397
|
+
))
|
|
398
|
+
except (TypeError, ValueError):
|
|
399
|
+
trial_max = 5
|
|
400
|
+
trial_max = max(1, min(trial_max, 20))
|
|
401
|
+
|
|
402
|
+
history = store.events(
|
|
403
|
+
domain,
|
|
404
|
+
include_shadow=True,
|
|
405
|
+
include_invalid=True,
|
|
406
|
+
include_unavailable=True,
|
|
407
|
+
)
|
|
408
|
+
shadow_history = [
|
|
409
|
+
event for event in history if bool(event.get("shadow"))
|
|
410
|
+
]
|
|
411
|
+
clean_calibration = []
|
|
412
|
+
for event in shadow_history:
|
|
413
|
+
evidence = event.get("evidence")
|
|
414
|
+
if not isinstance(evidence, dict):
|
|
415
|
+
continue
|
|
416
|
+
if (
|
|
417
|
+
event.get("source") == "task_queue.governor"
|
|
418
|
+
and event.get("outcome") == "success"
|
|
419
|
+
and not bool(event.get("violation"))
|
|
420
|
+
and event.get("evidence_status") == "verified"
|
|
421
|
+
and event.get("outcome_contract")
|
|
422
|
+
== "colony.worker-outcome/v1"
|
|
423
|
+
and evidence.get("schema") == "colony.worker-outcome/v1"
|
|
424
|
+
and evidence.get("success_attested") is True
|
|
425
|
+
and evidence.get("effectful") is False
|
|
426
|
+
and event.get("valid") is not False
|
|
427
|
+
and event.get("evidence_available") is not False
|
|
428
|
+
):
|
|
429
|
+
clean_calibration.append(event)
|
|
430
|
+
if len(clean_calibration) < ask_min:
|
|
431
|
+
return (
|
|
432
|
+
False,
|
|
433
|
+
"read_only_live_trial_calibration_insufficient_"
|
|
434
|
+
f"{len(clean_calibration)}_of_{ask_min}",
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
real_history = [
|
|
438
|
+
event for event in history if not bool(event.get("shadow"))
|
|
439
|
+
]
|
|
440
|
+
if any(
|
|
441
|
+
bool(event.get("violation"))
|
|
442
|
+
or event.get("outcome") in {"failure", "timeout"}
|
|
443
|
+
or event.get("valid") is False
|
|
444
|
+
or event.get("evidence_available") is False
|
|
445
|
+
for event in real_history
|
|
446
|
+
):
|
|
447
|
+
return False, "read_only_live_trial_negative_evidence"
|
|
448
|
+
trial_count = len(real_history)
|
|
449
|
+
if trial_count >= trial_max:
|
|
450
|
+
return (
|
|
451
|
+
False,
|
|
452
|
+
f"read_only_live_trial_budget_exhausted_{trial_max}",
|
|
453
|
+
)
|
|
454
|
+
return (
|
|
455
|
+
True,
|
|
456
|
+
f"bounded_read_only_live_trial_{trial_count + 1}_of_"
|
|
457
|
+
f"{trial_max}",
|
|
458
|
+
)
|
|
459
|
+
except Exception:
|
|
460
|
+
logger.warning(
|
|
461
|
+
"worker read-only live-trial evidence check failed closed",
|
|
462
|
+
exc_info=True,
|
|
463
|
+
)
|
|
464
|
+
return False, "read_only_live_trial_check_failed_closed"
|
|
465
|
+
|
|
466
|
+
# -- claim gate -------------------------------------------------------
|
|
467
|
+
def evaluate_claim(self, job: Any, worker_capabilities: Iterable[str],
|
|
468
|
+
worker_node_id: str = "") -> ClaimVerdict:
|
|
469
|
+
"""Re-decide, server-side, whether this worker may run this job.
|
|
470
|
+
|
|
471
|
+
Returns an immutable :class:`ClaimVerdict`. In shadow, ``allowed`` is
|
|
472
|
+
always true (calibration), while ``would_refuse`` records what live
|
|
473
|
+
mode would do.
|
|
474
|
+
"""
|
|
475
|
+
mode = workers_mode()
|
|
476
|
+
if mode == "off":
|
|
477
|
+
# Nothing is checked in mode "off": say so (None = unchecked)
|
|
478
|
+
# rather than fabricating capability/boundary/trust passes.
|
|
479
|
+
return ClaimVerdict(
|
|
480
|
+
mode="off", allowed=True, enforced=False,
|
|
481
|
+
would_refuse=False, reason="governor_off",
|
|
482
|
+
capability_ok=None, boundary_ok=None,
|
|
483
|
+
boundary_reason="governor_off_unchecked", shadow=False,
|
|
484
|
+
trust_ok=None, trust_reason="governor_off_unchecked",
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
caps = set(worker_capabilities or [])
|
|
488
|
+
required = self._required_caps(job)
|
|
489
|
+
missing = [c for c in required if c not in caps]
|
|
490
|
+
capability_ok = not missing
|
|
491
|
+
|
|
492
|
+
boundary_ok = not self._boundary_required
|
|
493
|
+
boundary_reason = (
|
|
494
|
+
"boundary_checker_not_required"
|
|
495
|
+
if boundary_ok else "boundary_checker_unavailable"
|
|
496
|
+
)
|
|
497
|
+
job_action = self._job_action(job)
|
|
498
|
+
if self._directives is not None:
|
|
499
|
+
try:
|
|
500
|
+
verdict = self._directives.check(job_action)
|
|
501
|
+
if type(getattr(verdict, "allowed", None)) is not bool:
|
|
502
|
+
raise TypeError("directive verdict allowed must be boolean")
|
|
503
|
+
boundary_ok = verdict.allowed
|
|
504
|
+
boundary_reason = str(getattr(verdict, "reason", "") or "ok")
|
|
505
|
+
except Exception:
|
|
506
|
+
# A configured boundary dependency is part of the live claim
|
|
507
|
+
# authority. Its failure cannot be interpreted as permission.
|
|
508
|
+
# Shadow remains observational, but records the exact claim it
|
|
509
|
+
# would refuse after graduation.
|
|
510
|
+
boundary_ok = False
|
|
511
|
+
boundary_reason = "boundary_check_failed_closed"
|
|
512
|
+
logger.warning(
|
|
513
|
+
"worker claim boundary check failed closed",
|
|
514
|
+
exc_info=True,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
jt = self._job_type_value(job)
|
|
518
|
+
domain = self.trust_domain(jt)
|
|
519
|
+
tags = getattr(job, "tags", {}) or {}
|
|
520
|
+
approved = bool(
|
|
521
|
+
tags.get("approved_by") or tags.get("auto_approved_by_policy")
|
|
522
|
+
)
|
|
523
|
+
effectful = job_declares_effect(job)
|
|
524
|
+
trust = getattr(self._self_model, "trust", None)
|
|
525
|
+
if trust is None:
|
|
526
|
+
trust_ok = False
|
|
527
|
+
trust_reason = "worker_trust_unavailable"
|
|
528
|
+
else:
|
|
529
|
+
try:
|
|
530
|
+
from apsimo.self_model.trust import floor_class
|
|
531
|
+
|
|
532
|
+
stage = str(trust.stage(domain) or "shadow")
|
|
533
|
+
confidence = float(trust.confidence(domain))
|
|
534
|
+
try:
|
|
535
|
+
threshold = float(os.environ.get(
|
|
536
|
+
"COLONY_TRUST_ACT_THRESHOLD", "0.8"
|
|
537
|
+
))
|
|
538
|
+
except ValueError:
|
|
539
|
+
threshold = 0.8
|
|
540
|
+
floor = floor_class(
|
|
541
|
+
f"{job_action.text} {job_action.target} "
|
|
542
|
+
f"{job_action.tool_name}"
|
|
543
|
+
)
|
|
544
|
+
if mode == "shadow" and effectful:
|
|
545
|
+
trust_ok = False
|
|
546
|
+
trust_reason = "shadow_effect_execution_disabled"
|
|
547
|
+
elif effectful and not approved:
|
|
548
|
+
trust_ok = False
|
|
549
|
+
trust_reason = "action_approval_required"
|
|
550
|
+
elif floor and not approved:
|
|
551
|
+
trust_ok = False
|
|
552
|
+
trust_reason = f"immutable_floor_{floor}"
|
|
553
|
+
elif (
|
|
554
|
+
mode == "live"
|
|
555
|
+
and stage == "ask_first"
|
|
556
|
+
and not effectful
|
|
557
|
+
and not approved
|
|
558
|
+
):
|
|
559
|
+
trust_ok, trust_reason = (
|
|
560
|
+
self._read_only_live_trial_status(domain)
|
|
561
|
+
)
|
|
562
|
+
else:
|
|
563
|
+
trust_ok = approved or (
|
|
564
|
+
stage == "act_first" and confidence >= threshold
|
|
565
|
+
)
|
|
566
|
+
trust_reason = (
|
|
567
|
+
f"worker_trust_{stage}_confidence_{confidence:.3f}"
|
|
568
|
+
+ ("_approved" if approved else "")
|
|
569
|
+
)
|
|
570
|
+
except Exception:
|
|
571
|
+
trust_ok = False
|
|
572
|
+
trust_reason = "worker_trust_check_failed_closed"
|
|
573
|
+
|
|
574
|
+
would_refuse = not (capability_ok and boundary_ok and trust_ok)
|
|
575
|
+
if would_refuse and not capability_ok:
|
|
576
|
+
reason = f"worker lacks required capabilities: {missing}"
|
|
577
|
+
elif would_refuse and boundary_reason in {
|
|
578
|
+
"boundary_checker_unavailable", "boundary_check_failed_closed",
|
|
579
|
+
}:
|
|
580
|
+
reason = f"job boundary unavailable: {boundary_reason}"
|
|
581
|
+
elif would_refuse:
|
|
582
|
+
if not boundary_ok:
|
|
583
|
+
reason = f"job subject under boundary: {boundary_reason}"
|
|
584
|
+
else:
|
|
585
|
+
reason = f"worker autonomy not earned: {trust_reason}"
|
|
586
|
+
else:
|
|
587
|
+
reason = "ok"
|
|
588
|
+
|
|
589
|
+
shadow = mode == "shadow"
|
|
590
|
+
allowed = (
|
|
591
|
+
not would_refuse
|
|
592
|
+
if mode == "live"
|
|
593
|
+
else (not effectful if mode == "shadow" else True)
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
# A refused (live) or would-be-refused (shadow) claim is journaled;
|
|
597
|
+
# a clean claim is not (it becomes an outcome at completion).
|
|
598
|
+
if would_refuse:
|
|
599
|
+
self._journal(
|
|
600
|
+
self.trust_domain(getattr(job, "job_type", None)
|
|
601
|
+
and job.job_type.value if hasattr(
|
|
602
|
+
getattr(job, "job_type", None), "value")
|
|
603
|
+
else getattr(job, "job_type", "custom")),
|
|
604
|
+
f"worker {worker_node_id} claim of {self._job_id(job)}",
|
|
605
|
+
reasoning=reason,
|
|
606
|
+
decision="blocked" if not allowed else "noted",
|
|
607
|
+
ref=self._job_id(job))
|
|
608
|
+
return ClaimVerdict(
|
|
609
|
+
mode=mode,
|
|
610
|
+
allowed=allowed,
|
|
611
|
+
enforced=(mode == "live" or (mode == "shadow" and not allowed)),
|
|
612
|
+
would_refuse=would_refuse,
|
|
613
|
+
reason=reason,
|
|
614
|
+
capability_ok=capability_ok,
|
|
615
|
+
boundary_ok=boundary_ok,
|
|
616
|
+
boundary_reason=boundary_reason,
|
|
617
|
+
missing_capabilities=tuple(missing),
|
|
618
|
+
shadow=shadow,
|
|
619
|
+
trust_ok=trust_ok,
|
|
620
|
+
trust_reason=trust_reason,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# -- completion audit (never trust the report) ------------------------
|
|
624
|
+
def audit_report(self, job: Any, report: Dict[str, Any]) -> Dict[str, Any]:
|
|
625
|
+
"""Cross-check the worker's structured report against its authority.
|
|
626
|
+
|
|
627
|
+
Verdict: 'clean' | 'violation' | 'unverified'. A violation from any
|
|
628
|
+
check wins (fail loud).
|
|
629
|
+
"""
|
|
630
|
+
report = report or {}
|
|
631
|
+
findings: List[str] = []
|
|
632
|
+
ok = True
|
|
633
|
+
|
|
634
|
+
authorized = self._job_authorized_to_mutate(job)
|
|
635
|
+
ops = [str(o).strip().lower() for o in (report.get("operations") or [])]
|
|
636
|
+
reported_mutation = (
|
|
637
|
+
bool(set(ops) & _MUTATE_OPS)
|
|
638
|
+
or int(report.get("commits") or 0) > 0
|
|
639
|
+
or bool(report.get("files_written"))
|
|
640
|
+
or bool(report.get("deletions"))
|
|
641
|
+
)
|
|
642
|
+
if reported_mutation and not authorized:
|
|
643
|
+
ok = False
|
|
644
|
+
findings.append(
|
|
645
|
+
"worker reported a mutation on a job not authorized to change "
|
|
646
|
+
f"state (ops={sorted(set(ops) & _MUTATE_OPS) or 'commits/files'})")
|
|
647
|
+
if report.get("force_push"):
|
|
648
|
+
ok = False
|
|
649
|
+
findings.append("force push reported (never allowed)")
|
|
650
|
+
|
|
651
|
+
# Capability escalation: a worker reporting it used a capability the
|
|
652
|
+
# job never required and the worker never advertised is suspicious.
|
|
653
|
+
# (Kept advisory unless it co-occurs with a mutation.)
|
|
654
|
+
if reported_mutation and not authorized and report.get("escalated"):
|
|
655
|
+
findings.append("worker reported escalating its own scope")
|
|
656
|
+
|
|
657
|
+
has_report = bool(report) and any(
|
|
658
|
+
k in report for k in (
|
|
659
|
+
"summary", "operations", "result", "output",
|
|
660
|
+
"execution_result",
|
|
661
|
+
)
|
|
662
|
+
)
|
|
663
|
+
if not ok:
|
|
664
|
+
verdict = "violation"
|
|
665
|
+
elif has_report:
|
|
666
|
+
verdict = "clean"
|
|
667
|
+
else:
|
|
668
|
+
verdict = "unverified"
|
|
669
|
+
|
|
670
|
+
result = {"verdict": verdict, "findings": findings,
|
|
671
|
+
"authorized_to_mutate": authorized,
|
|
672
|
+
"reported_mutation": reported_mutation}
|
|
673
|
+
if verdict == "violation":
|
|
674
|
+
logger.warning("WORKER SCOPE VIOLATION on %s: %s",
|
|
675
|
+
self._job_id(job), "; ".join(findings)[:400])
|
|
676
|
+
return result
|
|
677
|
+
|
|
678
|
+
# -- outcome recording (feedback + self-model + journal + skills) -----
|
|
679
|
+
@staticmethod
|
|
680
|
+
def classify_completion_outcome(
|
|
681
|
+
report: Dict[str, Any],
|
|
682
|
+
verdict: str,
|
|
683
|
+
) -> tuple[str, str]:
|
|
684
|
+
"""Classify semantic work outcome independently of HTTP completion.
|
|
685
|
+
|
|
686
|
+
``POST .../complete`` only says the worker stopped and returned a
|
|
687
|
+
payload. It is not evidence that useful work succeeded. Success needs
|
|
688
|
+
a clean audit plus an explicit verified completion marker. Policy
|
|
689
|
+
skips, cancellations, unknown/malformed terminal states, and ordinary
|
|
690
|
+
unverified reports are neutral: journalable, but never competence or
|
|
691
|
+
earned-autonomy evidence.
|
|
692
|
+
"""
|
|
693
|
+
|
|
694
|
+
payload = report if isinstance(report, dict) else {}
|
|
695
|
+
execution_result = (
|
|
696
|
+
payload.get("execution_result")
|
|
697
|
+
if isinstance(payload.get("execution_result"), dict)
|
|
698
|
+
else {}
|
|
699
|
+
)
|
|
700
|
+
semantic = str(
|
|
701
|
+
payload.get("status") or payload.get("outcome")
|
|
702
|
+
or execution_result.get("terminal_outcome") or ""
|
|
703
|
+
).strip().lower()
|
|
704
|
+
if semantic == "succeeded":
|
|
705
|
+
semantic = "completed"
|
|
706
|
+
action_plane = (
|
|
707
|
+
payload.get("action_plane")
|
|
708
|
+
if isinstance(payload.get("action_plane"), dict)
|
|
709
|
+
else {}
|
|
710
|
+
)
|
|
711
|
+
action_state = str(action_plane.get("state") or "").strip().lower()
|
|
712
|
+
verification = str(
|
|
713
|
+
payload.get("verification_status") or ""
|
|
714
|
+
).strip().lower()
|
|
715
|
+
|
|
716
|
+
if verdict == "violation":
|
|
717
|
+
return "failure", "scope_violation"
|
|
718
|
+
failure_markers = {"failed", "failure", "error"}
|
|
719
|
+
if semantic in failure_markers or action_state in failure_markers:
|
|
720
|
+
return "failure", "reported_failure"
|
|
721
|
+
neutral_markers = {
|
|
722
|
+
"skipped", "skip", "cancelled", "canceled", "unknown",
|
|
723
|
+
"pending", "proposed", "gated", "accepted",
|
|
724
|
+
}
|
|
725
|
+
if semantic in neutral_markers or action_state in neutral_markers:
|
|
726
|
+
return "neutral", f"terminal_{semantic or action_state}"
|
|
727
|
+
if verdict != "clean":
|
|
728
|
+
return "neutral", f"audit_{verdict or 'unverified'}"
|
|
729
|
+
|
|
730
|
+
verified_completion = (
|
|
731
|
+
(
|
|
732
|
+
execution_result.get("schema") == "ExecutionResultV1"
|
|
733
|
+
and str(
|
|
734
|
+
execution_result.get("terminal_outcome") or ""
|
|
735
|
+
).lower() == "succeeded"
|
|
736
|
+
)
|
|
737
|
+
or semantic == "verified"
|
|
738
|
+
or (
|
|
739
|
+
semantic == "completed"
|
|
740
|
+
and (
|
|
741
|
+
action_state in {"verified", "completed"}
|
|
742
|
+
or verification == "verified"
|
|
743
|
+
)
|
|
744
|
+
)
|
|
745
|
+
)
|
|
746
|
+
if verified_completion:
|
|
747
|
+
return "success", "verified_completion"
|
|
748
|
+
if semantic == "completed":
|
|
749
|
+
return "neutral", "completed_without_verification"
|
|
750
|
+
return "neutral", "unknown_semantic_outcome"
|
|
751
|
+
|
|
752
|
+
async def record_outcome(self, job: Any, report: Dict[str, Any],
|
|
753
|
+
verdict: str, *, outcome: Optional[str] = None,
|
|
754
|
+
latency: Optional[float] = None,
|
|
755
|
+
attempts: int = 0,
|
|
756
|
+
event_id: Optional[str] = None,
|
|
757
|
+
event_mode: Optional[str] = None,
|
|
758
|
+
success_attested: bool = False) -> Dict[str, Any]:
|
|
759
|
+
"""Fold an audited completion into the trust/accountability layer."""
|
|
760
|
+
report = report or {}
|
|
761
|
+
mode = (
|
|
762
|
+
str(event_mode).strip().lower()
|
|
763
|
+
if str(event_mode or "").strip().lower() in {"off", "shadow", "live"}
|
|
764
|
+
else workers_mode()
|
|
765
|
+
)
|
|
766
|
+
if mode == "off":
|
|
767
|
+
return {"recorded": False, "reason": "governor_off"}
|
|
768
|
+
jt = self._job_type_value(job)
|
|
769
|
+
domain = self.trust_domain(jt)
|
|
770
|
+
shadow = mode != "live"
|
|
771
|
+
|
|
772
|
+
if outcome is None:
|
|
773
|
+
outcome, outcome_reason = self.classify_completion_outcome(
|
|
774
|
+
report, verdict
|
|
775
|
+
)
|
|
776
|
+
else:
|
|
777
|
+
outcome = str(outcome).strip().lower()
|
|
778
|
+
if outcome not in {"success", "failure", "neutral"}:
|
|
779
|
+
outcome = "neutral"
|
|
780
|
+
outcome_reason = "explicit_outcome"
|
|
781
|
+
if outcome == "success":
|
|
782
|
+
derived, derived_reason = self.classify_completion_outcome(
|
|
783
|
+
report, verdict
|
|
784
|
+
)
|
|
785
|
+
if derived != "success":
|
|
786
|
+
outcome = derived
|
|
787
|
+
outcome_reason = f"rejected_explicit_success:{derived_reason}"
|
|
788
|
+
|
|
789
|
+
stated = report.get("confidence")
|
|
790
|
+
try:
|
|
791
|
+
stated = float(stated) if stated is not None else None
|
|
792
|
+
except (TypeError, ValueError):
|
|
793
|
+
stated = None
|
|
794
|
+
|
|
795
|
+
# Self-model / trust engine: the earned-autonomy evidence. Competence
|
|
796
|
+
# and its deterministic trust transition are separate durable stages:
|
|
797
|
+
# a replayed event re-runs trust reconciliation without duplicating
|
|
798
|
+
# the immutable competence row.
|
|
799
|
+
duplicate = False
|
|
800
|
+
if outcome != "neutral" and self._self_model is None and event_id:
|
|
801
|
+
raise RuntimeError("worker competence store is unavailable")
|
|
802
|
+
if outcome != "neutral" and self._self_model is not None:
|
|
803
|
+
try:
|
|
804
|
+
recorded = self._self_model.record(
|
|
805
|
+
domain, outcome, latency_secs=latency,
|
|
806
|
+
shadow=shadow, violation=(verdict == "violation"),
|
|
807
|
+
stated_confidence=stated,
|
|
808
|
+
source="task_queue.governor",
|
|
809
|
+
source_ref=self._job_id(job),
|
|
810
|
+
event_key=event_id,
|
|
811
|
+
# A worker report is observed evidence. It is never
|
|
812
|
+
# promoted to externally verified merely because the
|
|
813
|
+
# worker wrote status="verified" in its own payload.
|
|
814
|
+
evidence_status=(
|
|
815
|
+
"verified"
|
|
816
|
+
if outcome == "success" and success_attested
|
|
817
|
+
else "observed"
|
|
818
|
+
),
|
|
819
|
+
outcome_contract="colony.worker-outcome/v1",
|
|
820
|
+
evidence={
|
|
821
|
+
"schema": "colony.worker-outcome/v1",
|
|
822
|
+
"audit_verdict": verdict,
|
|
823
|
+
"classification": outcome_reason,
|
|
824
|
+
"attempts": max(0, int(attempts or 0)),
|
|
825
|
+
"event_id": event_id,
|
|
826
|
+
"success_attested": bool(success_attested),
|
|
827
|
+
"effectful": bool(job_declares_effect(job)),
|
|
828
|
+
},
|
|
829
|
+
defer_trust=True,
|
|
830
|
+
)
|
|
831
|
+
if event_id and recorded is False:
|
|
832
|
+
store = getattr(self._self_model, "store", None)
|
|
833
|
+
if (
|
|
834
|
+
store is not None
|
|
835
|
+
and hasattr(store, "has_event_key")
|
|
836
|
+
and store.has_event_key(
|
|
837
|
+
"task_queue.governor", event_id,
|
|
838
|
+
)
|
|
839
|
+
):
|
|
840
|
+
duplicate = True
|
|
841
|
+
else:
|
|
842
|
+
raise RuntimeError(
|
|
843
|
+
"worker competence event was not durably recorded"
|
|
844
|
+
)
|
|
845
|
+
if hasattr(self._self_model, "reconcile_trust"):
|
|
846
|
+
self._self_model.reconcile_trust(domain)
|
|
847
|
+
else:
|
|
848
|
+
trust = getattr(self._self_model, "trust", None)
|
|
849
|
+
if trust is not None:
|
|
850
|
+
trust.after_outcome(domain)
|
|
851
|
+
except Exception:
|
|
852
|
+
logger.warning(
|
|
853
|
+
"worker self-model record failed",
|
|
854
|
+
exc_info=True,
|
|
855
|
+
)
|
|
856
|
+
raise
|
|
857
|
+
|
|
858
|
+
# Worker self-reports are never owner feedback. TypeFeedbackStore is
|
|
859
|
+
# reserved for actual owner reactions and must not amplify a worker's
|
|
860
|
+
# own success claim into the trust confidence multiplier.
|
|
861
|
+
|
|
862
|
+
journal_id = self._journal(
|
|
863
|
+
domain,
|
|
864
|
+
f"job {self._job_id(job)} ({jt}) completed",
|
|
865
|
+
reasoning=(
|
|
866
|
+
f"audit verdict: {verdict}; semantic outcome: {outcome} "
|
|
867
|
+
f"({outcome_reason})"
|
|
868
|
+
),
|
|
869
|
+
confidence=stated,
|
|
870
|
+
reversibility="recoverable" if self._job_authorized_to_mutate(job)
|
|
871
|
+
else "reversible",
|
|
872
|
+
decision="noted", outcome=outcome, ref=self._job_id(job),
|
|
873
|
+
event_key=event_id,
|
|
874
|
+
)
|
|
875
|
+
if event_id and journal_id < 0:
|
|
876
|
+
raise RuntimeError("worker action journal is unavailable")
|
|
877
|
+
|
|
878
|
+
# Owner-facing safety notice precedes optional LLM distillation and
|
|
879
|
+
# carries a stable proposal ID so crash-window retries are deduplicable
|
|
880
|
+
# downstream. Its own durable receipt lets a failed first delivery be
|
|
881
|
+
# retried while suppressing routine outbox replays after success.
|
|
882
|
+
if verdict == "violation" and self._deliver is not None:
|
|
883
|
+
receipt_key = (
|
|
884
|
+
f"{event_id}:violation-notice-delivered"
|
|
885
|
+
if event_id else None
|
|
886
|
+
)
|
|
887
|
+
journal = getattr(self._self_model, "journal", None)
|
|
888
|
+
already_delivered = bool(
|
|
889
|
+
receipt_key
|
|
890
|
+
and journal is not None
|
|
891
|
+
and hasattr(journal, "has_event_key")
|
|
892
|
+
and journal.has_event_key(receipt_key)
|
|
893
|
+
)
|
|
894
|
+
if not already_delivered:
|
|
895
|
+
await self._notify_violation(job, report, event_id=event_id)
|
|
896
|
+
if receipt_key:
|
|
897
|
+
receipt_id = self._journal(
|
|
898
|
+
domain,
|
|
899
|
+
f"violation notice delivered for worker event "
|
|
900
|
+
f"{event_id}",
|
|
901
|
+
reasoning="durable worker-notice delivery receipt",
|
|
902
|
+
decision="noted",
|
|
903
|
+
outcome="delivered",
|
|
904
|
+
ref=self._job_id(job),
|
|
905
|
+
event_key=receipt_key,
|
|
906
|
+
)
|
|
907
|
+
if receipt_id < 0:
|
|
908
|
+
raise RuntimeError(
|
|
909
|
+
"worker violation delivery receipt unavailable"
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
# The durable queue outbox may replay an event after a timeout or
|
|
913
|
+
# restart. Trust reconciliation and the violation receipt above are
|
|
914
|
+
# intentionally repeatable; LLM skill distillation is not.
|
|
915
|
+
if duplicate:
|
|
916
|
+
return {
|
|
917
|
+
"recorded": False,
|
|
918
|
+
"duplicate": True,
|
|
919
|
+
"event_id": event_id,
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
# Skill distillation on genuine successes (retry-success / novel).
|
|
923
|
+
if outcome == "success" and verdict != "violation":
|
|
924
|
+
await self._maybe_distill(job, report, attempts)
|
|
925
|
+
return {
|
|
926
|
+
"recorded": not duplicate,
|
|
927
|
+
"duplicate": duplicate,
|
|
928
|
+
"event_id": event_id,
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
async def _maybe_distill(self, job: Any, report: Dict[str, Any],
|
|
932
|
+
attempts: int) -> None:
|
|
933
|
+
if self._skills is None or self._llm is None:
|
|
934
|
+
return
|
|
935
|
+
try:
|
|
936
|
+
from apsimo.skills_memory import (
|
|
937
|
+
should_distill, distill_from_completion,
|
|
938
|
+
)
|
|
939
|
+
result_text = str(report.get("summary")
|
|
940
|
+
or report.get("result") or "")
|
|
941
|
+
if not should_distill(attempts, result_text, self._skills):
|
|
942
|
+
return
|
|
943
|
+
payload = getattr(job, "payload", {}) or {}
|
|
944
|
+
task_text = str(payload.get("description")
|
|
945
|
+
or payload.get("action_hint") or "")
|
|
946
|
+
await distill_from_completion(
|
|
947
|
+
self._llm, self._skills,
|
|
948
|
+
domain=self.trust_domain(self._job_type_value(job)),
|
|
949
|
+
task_text=task_text, result_text=result_text,
|
|
950
|
+
source_ref=self._job_id(job))
|
|
951
|
+
except Exception:
|
|
952
|
+
logger.debug("worker skill distill failed", exc_info=True)
|
|
953
|
+
|
|
954
|
+
async def _notify_violation(
|
|
955
|
+
self,
|
|
956
|
+
job: Any,
|
|
957
|
+
report: Dict[str, Any],
|
|
958
|
+
*,
|
|
959
|
+
event_id: Optional[str] = None,
|
|
960
|
+
) -> None:
|
|
961
|
+
try:
|
|
962
|
+
from apsimo.proposals import Proposal, proposal_to_payload
|
|
963
|
+
findings = "; ".join(
|
|
964
|
+
self.audit_report(job, report).get("findings", [])[:5])
|
|
965
|
+
notice_key = str(
|
|
966
|
+
event_id or f"worker-violation:{self._job_id(job)}"
|
|
967
|
+
)
|
|
968
|
+
prop = Proposal(
|
|
969
|
+
id=("prop-worker-" + hashlib.sha256(
|
|
970
|
+
notice_key.encode()
|
|
971
|
+
).hexdigest()[:16]),
|
|
972
|
+
title=f"Worker scope violation on job {self._job_id(job)}",
|
|
973
|
+
finding=(f"A queue worker exceeded its authorized scope: "
|
|
974
|
+
f"{findings}. I flagged and recorded it; the job "
|
|
975
|
+
"type has been demoted from autonomous execution."),
|
|
976
|
+
why_it_helps="protects you from an over-reaching worker",
|
|
977
|
+
suggested_action="Review the flagged job and worker",
|
|
978
|
+
source=self._job_id(job), initiative_type="proposal",
|
|
979
|
+
confidence=0.95)
|
|
980
|
+
if self._proposal_store is not None:
|
|
981
|
+
insert_once = getattr(
|
|
982
|
+
self._proposal_store, "add_if_absent", None,
|
|
983
|
+
)
|
|
984
|
+
if callable(insert_once):
|
|
985
|
+
insert_once(prop)
|
|
986
|
+
else:
|
|
987
|
+
self._proposal_store.add(prop)
|
|
988
|
+
delivered = await self._deliver(proposal_to_payload(prop))
|
|
989
|
+
if delivered is not True:
|
|
990
|
+
raise RuntimeError("worker violation notice was not delivered")
|
|
991
|
+
except Exception:
|
|
992
|
+
logger.debug("worker violation notify failed", exc_info=True)
|
|
993
|
+
raise
|
|
994
|
+
|
|
995
|
+
# -- observability ----------------------------------------------------
|
|
996
|
+
def status(self) -> Dict[str, Any]:
|
|
997
|
+
out: Dict[str, Any] = {
|
|
998
|
+
"mode": workers_mode(),
|
|
999
|
+
"boundary_required": self._boundary_required,
|
|
1000
|
+
"directive_dependency_available": self._directives is not None,
|
|
1001
|
+
"ready_for_live_claims": self.ready_for_live_claims(),
|
|
1002
|
+
}
|
|
1003
|
+
trust = getattr(self._self_model, "trust", None)
|
|
1004
|
+
if trust is not None:
|
|
1005
|
+
try:
|
|
1006
|
+
out["worker_domains"] = [
|
|
1007
|
+
s for s in trust.snapshot()
|
|
1008
|
+
if str(s.get("domain", "")).startswith("worker:")]
|
|
1009
|
+
except Exception:
|
|
1010
|
+
pass
|
|
1011
|
+
return out
|
|
1012
|
+
|
|
1013
|
+
# -- helpers ----------------------------------------------------------
|
|
1014
|
+
def _journal(self, domain: str, description: str, **kw: Any) -> int:
|
|
1015
|
+
journal = getattr(self._self_model, "journal", None)
|
|
1016
|
+
if journal is None:
|
|
1017
|
+
return -1
|
|
1018
|
+
try:
|
|
1019
|
+
return int(journal.record(domain, description, **kw))
|
|
1020
|
+
except Exception:
|
|
1021
|
+
logger.debug("worker journal failed", exc_info=True)
|
|
1022
|
+
return -1
|
|
1023
|
+
|
|
1024
|
+
@staticmethod
|
|
1025
|
+
def _job_id(job: Any) -> str:
|
|
1026
|
+
return str(getattr(job, "job_id", "") or getattr(job, "id", "") or "?")
|
|
1027
|
+
|
|
1028
|
+
@staticmethod
|
|
1029
|
+
def _job_type_value(job: Any) -> str:
|
|
1030
|
+
jt = getattr(job, "job_type", "custom")
|
|
1031
|
+
return jt.value if hasattr(jt, "value") else str(jt)
|