apsimo 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apsimo/__init__.py +38 -0
- apsimo/__main__.py +6 -0
- apsimo/agent/__init__.py +6 -0
- apsimo/agent/client.py +276 -0
- apsimo/agent/models.py +46 -0
- apsimo/agents/__init__.py +20 -0
- apsimo/agents/models.py +264 -0
- apsimo/agents/store.py +861 -0
- apsimo/agents/websocket.py +522 -0
- apsimo/api/__init__.py +1 -0
- apsimo/api/auth_telemetry.py +287 -0
- apsimo/api/authority.py +1203 -0
- apsimo/api/contact_grants.py +347 -0
- apsimo/api/middleware.py +483 -0
- apsimo/api/routers/__init__.py +1 -0
- apsimo/api/routers/commitment_work.py +265 -0
- apsimo/api/routers/context_gate.py +123 -0
- apsimo/api/routers/executions.py +140 -0
- apsimo/api/routers/followup_plans.py +147 -0
- apsimo/api/routers/governed_actions.py +162 -0
- apsimo/api/routers/host.py +14473 -0
- apsimo/api/routers/initiative_work.py +115 -0
- apsimo/api/routers/mining.py +104 -0
- apsimo/api/routers/observations.py +110 -0
- apsimo/api/routers/social_state.py +225 -0
- apsimo/api/routers/task_queue.py +2715 -0
- apsimo/api/routers/temporal_followups.py +251 -0
- apsimo/api/routers/transport.py +110 -0
- apsimo/api/routers/transport_ingress_api.py +240 -0
- apsimo/api/schemas/__init__.py +1 -0
- apsimo/api/schemas/host.py +1949 -0
- apsimo/autonomy/cli.py +110 -0
- apsimo/autonomy/condition_worker.py +437 -0
- apsimo/autonomy/config.py +424 -0
- apsimo/autonomy/loop.py +4316 -0
- apsimo/autonomy/registry.py +339 -0
- apsimo/autonomy/scheduler.py +1822 -0
- apsimo/autonomy/synthesis.py +449 -0
- apsimo/backup.py +962 -0
- apsimo/beliefs/__init__.py +23 -0
- apsimo/beliefs/contradictions.py +109 -0
- apsimo/beliefs/decay.py +61 -0
- apsimo/beliefs/engine.py +479 -0
- apsimo/beliefs/models.py +67 -0
- apsimo/beliefs/promotion.py +41 -0
- apsimo/beliefs/resolve.py +58 -0
- apsimo/beliefs/source_claims.py +690 -0
- apsimo/beliefs/source_projection.py +883 -0
- apsimo/beliefs/source_time.py +208 -0
- apsimo/beliefs/store.py +133 -0
- apsimo/briefings/aggregators.py +824 -0
- apsimo/briefings/composer.py +420 -0
- apsimo/briefings/config.py +55 -0
- apsimo/briefings/delivery.py +439 -0
- apsimo/briefings/engagement.py +97 -0
- apsimo/briefings/engine.py +274 -0
- apsimo/briefings/enhancer.py +99 -0
- apsimo/briefings/models.py +183 -0
- apsimo/briefings/scheduler.py +382 -0
- apsimo/briefings/store.py +435 -0
- apsimo/chain/__init__.py +48 -0
- apsimo/chain/block.py +100 -0
- apsimo/chain/cli.py +704 -0
- apsimo/chain/genesis.py +443 -0
- apsimo/chain/identity.py +416 -0
- apsimo/chain/keys.py +1025 -0
- apsimo/chain/local_keys.py +187 -0
- apsimo/chain/manager.py +290 -0
- apsimo/chain/node.py +163 -0
- apsimo/chain/plugin_transactions.py +371 -0
- apsimo/chain/protocol.py +220 -0
- apsimo/chain/state_machine.py +676 -0
- apsimo/chain/storage.py +503 -0
- apsimo/chain/transactions.py +250 -0
- apsimo/chain/validation.py +397 -0
- apsimo/channels/__init__.py +1 -0
- apsimo/channels/manifest.py +31 -0
- apsimo/channels/migrations/001_channels_schema.sql +12 -0
- apsimo/channels/phone_gateways.py +42 -0
- apsimo/channels/presence.py +188 -0
- apsimo/channels/router.py +235 -0
- apsimo/channels/store.py +231 -0
- apsimo/cli.py +2688 -0
- apsimo/cognition/__init__.py +11 -0
- apsimo/cognition/charter.py +398 -0
- apsimo/cognition/drive_governance.py +3530 -0
- apsimo/cognition/evidence_pipeline.py +1627 -0
- apsimo/cognition/external_events.py +932 -0
- apsimo/cognition/goal_spine.py +3488 -0
- apsimo/cognition/introspection.py +214 -0
- apsimo/cognition/prompt.py +150 -0
- apsimo/cognition/runtime.py +108 -0
- apsimo/cognition/trigger.py +154 -0
- apsimo/commitments/__init__.py +18 -0
- apsimo/commitments/local_work.py +355 -0
- apsimo/commitments/store.py +1052 -0
- apsimo/commitments/work.py +91 -0
- apsimo/compat.py +53 -0
- apsimo/compression/__init__.py +467 -0
- apsimo/connectors/__init__.py +21 -0
- apsimo/connectors/base.py +152 -0
- apsimo/connectors/caldav_calendar.py +125 -0
- apsimo/connectors/fs_documents.py +85 -0
- apsimo/connectors/imap_email.py +138 -0
- apsimo/connectors/manager.py +218 -0
- apsimo/connectors/webhook_pull.py +88 -0
- apsimo/contacts/__init__.py +33 -0
- apsimo/contacts/comms.py +357 -0
- apsimo/contacts/config.py +79 -0
- apsimo/contacts/exporters/__init__.py +1 -0
- apsimo/contacts/exporters/vcard.py +71 -0
- apsimo/contacts/identity_links.py +251 -0
- apsimo/contacts/importer.py +280 -0
- apsimo/contacts/importers/__init__.py +1 -0
- apsimo/contacts/importers/batch.py +43 -0
- apsimo/contacts/importers/macos_contacts.py +101 -0
- apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
- apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
- apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
- apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
- apsimo/contacts/migrations/005_identity_links.sql +27 -0
- apsimo/contacts/models.py +308 -0
- apsimo/contacts/scoring.py +16 -0
- apsimo/contacts/store.py +1623 -0
- apsimo/contacts/transport_ingress.py +252 -0
- apsimo/contacts/world_bridge.py +314 -0
- apsimo/contextgate/__init__.py +69 -0
- apsimo/contextgate/chunker.py +169 -0
- apsimo/contextgate/estimate.py +54 -0
- apsimo/contextgate/gate.py +313 -0
- apsimo/contextgate/retrieve.py +115 -0
- apsimo/delivery/__init__.py +16 -0
- apsimo/delivery/bridge.py +1260 -0
- apsimo/delivery/channels.py +526 -0
- apsimo/delivery/classification.py +50 -0
- apsimo/delivery/rate_limiter.py +268 -0
- apsimo/delivery/reachout_policy.py +206 -0
- apsimo/directed/__init__.py +22 -0
- apsimo/directed/audit.py +167 -0
- apsimo/directed/intake.py +95 -0
- apsimo/directed/models.py +191 -0
- apsimo/directed/service.py +509 -0
- apsimo/directives/__init__.py +25 -0
- apsimo/directives/evidence.py +87 -0
- apsimo/directives/extractor.py +188 -0
- apsimo/directives/guard.py +364 -0
- apsimo/directives/models.py +206 -0
- apsimo/directives/service.py +372 -0
- apsimo/directives/store.py +167 -0
- apsimo/doctor.py +2173 -0
- apsimo/environment.py +43 -0
- apsimo/events/__init__.py +33 -0
- apsimo/events/broadcaster.py +98 -0
- apsimo/events/bus.py +217 -0
- apsimo/events/journal.py +863 -0
- apsimo/events/stream.py +131 -0
- apsimo/events/types.py +150 -0
- apsimo/execution_results.py +357 -0
- apsimo/feedback/__init__.py +5 -0
- apsimo/feedback/store.py +76 -0
- apsimo/feeds/__init__.py +19 -0
- apsimo/feeds/cli.py +84 -0
- apsimo/feeds/engine.py +437 -0
- apsimo/feeds/example-feed.yaml +77 -0
- apsimo/feeds/hermes_cron.py +126 -0
- apsimo/feeds/manager.py +235 -0
- apsimo/feeds/spec.py +250 -0
- apsimo/feeds/template.py +202 -0
- apsimo/gate/__init__.py +18 -0
- apsimo/gate/audit.py +61 -0
- apsimo/gate/communication_policy.py +166 -0
- apsimo/gate/config.py +72 -0
- apsimo/gate/context_provenance.py +170 -0
- apsimo/gate/env_risk.py +226 -0
- apsimo/gate/guard_audit.py +353 -0
- apsimo/gate/layers/__init__.py +1 -0
- apsimo/gate/layers/base.py +15 -0
- apsimo/gate/layers/l1_recipient.py +66 -0
- apsimo/gate/layers/l2_pii.py +134 -0
- apsimo/gate/layers/l3_cross_context.py +50 -0
- apsimo/gate/layers/l4_trust_tier.py +78 -0
- apsimo/gate/layers/l5_injection.py +199 -0
- apsimo/gate/layers/l6_review.py +86 -0
- apsimo/gate/layers/l7_delay.py +100 -0
- apsimo/gate/layers/tom2_epistemic.py +185 -0
- apsimo/gate/models.py +64 -0
- apsimo/gate/pending_dispatch.py +5 -0
- apsimo/gate/pipeline.py +206 -0
- apsimo/gate/rejection.py +259 -0
- apsimo/gate/response_guard.py +700 -0
- apsimo/gate/rulesets/injection_v1.yaml +51 -0
- apsimo/gate/surface_policy.py +189 -0
- apsimo/gate/taint.py +226 -0
- apsimo/genesis.json +9 -0
- apsimo/goals/__init__.py +100 -0
- apsimo/goals/config.py +38 -0
- apsimo/goals/decomposer.py +421 -0
- apsimo/goals/engine.py +617 -0
- apsimo/goals/inference.py +354 -0
- apsimo/goals/models.py +302 -0
- apsimo/goals/priority.py +270 -0
- apsimo/goals/queue_bridge.py +149 -0
- apsimo/goals/replan.py +450 -0
- apsimo/goals/schema.sql +89 -0
- apsimo/goals/store.py +692 -0
- apsimo/governed_actions.py +1708 -0
- apsimo/harness_integration/__init__.py +45 -0
- apsimo/harness_integration/context.py +41 -0
- apsimo/harness_integration/skills.py +231 -0
- apsimo/identity/__init__.py +26 -0
- apsimo/identity/participants.py +181 -0
- apsimo/identity/resolver.py +329 -0
- apsimo/identity_bootstrap/__init__.py +5 -0
- apsimo/identity_bootstrap/builder.py +208 -0
- apsimo/identity_bootstrap/corpus.py +443 -0
- apsimo/identity_bootstrap/models.py +54 -0
- apsimo/identity_bootstrap/runner.py +353 -0
- apsimo/identity_bootstrap/seeders/__init__.py +25 -0
- apsimo/identity_bootstrap/seeders/briefings.py +109 -0
- apsimo/identity_bootstrap/seeders/chain.py +57 -0
- apsimo/identity_bootstrap/seeders/goals.py +128 -0
- apsimo/identity_bootstrap/seeders/memory.py +191 -0
- apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
- apsimo/identity_bootstrap/seeders/relationship.py +152 -0
- apsimo/identity_bootstrap/seeders/sessions.py +67 -0
- apsimo/identity_bootstrap/seeders/skills.py +92 -0
- apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
- apsimo/identity_bootstrap/seeders/world_model.py +143 -0
- apsimo/identity_bootstrap/self_query.py +92 -0
- apsimo/identity_bootstrap/self_reflection.py +155 -0
- apsimo/identity_bootstrap/skill.py +37 -0
- apsimo/identity_bootstrap/verifier.py +436 -0
- apsimo/initiatives/__init__.py +20 -0
- apsimo/initiatives/action_registry.py +454 -0
- apsimo/initiatives/approval_authority.py +2105 -0
- apsimo/initiatives/approval_policy.py +123 -0
- apsimo/initiatives/assignment.py +263 -0
- apsimo/initiatives/backup_evidence.py +100 -0
- apsimo/initiatives/context_freshness.py +103 -0
- apsimo/initiatives/models.py +318 -0
- apsimo/initiatives/native_work.py +270 -0
- apsimo/initiatives/standing_approvals.py +232 -0
- apsimo/initiatives/store.py +1081 -0
- apsimo/initiatives/temporal_followup.py +410 -0
- apsimo/intelligence/__init__.py +1 -0
- apsimo/intelligence/cognition/__init__.py +24 -0
- apsimo/intelligence/cognition/gap_detector.py +148 -0
- apsimo/intelligence/cognition/metalearner.py +547 -0
- apsimo/intelligence/cognition/metrics_collector.py +217 -0
- apsimo/intelligence/cognition/performance_index.py +299 -0
- apsimo/intelligence/cognition/registry.py +192 -0
- apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
- apsimo/intelligence/cognition/types.py +16 -0
- apsimo/intelligence/components/__init__.py +66 -0
- apsimo/intelligence/components/anomaly_detector.py +413 -0
- apsimo/intelligence/components/initiative_engine.py +2643 -0
- apsimo/intelligence/components/preference_learner.py +521 -0
- apsimo/intelligence/components/research_orchestrator.py +358 -0
- apsimo/intelligence/components/self_directed_thinker.py +221 -0
- apsimo/intelligence/components/self_reflector.py +252 -0
- apsimo/intelligence/components/session_continuity.py +154 -0
- apsimo/intelligence/components/task_planner.py +320 -0
- apsimo/intelligence/components/tool_learner.py +217 -0
- apsimo/intelligence/graph/__init__.py +79 -0
- apsimo/intelligence/graph/client.py +2483 -0
- apsimo/intelligence/graph/consolidator.py +405 -0
- apsimo/intelligence/graph/distiller.py +312 -0
- apsimo/intelligence/graph/migrations.py +129 -0
- apsimo/intelligence/graph/queries.py +248 -0
- apsimo/intelligence/graph/recall.py +281 -0
- apsimo/intelligence/graph/reconciler.py +144 -0
- apsimo/intelligence/graph/schema.py +337 -0
- apsimo/intelligence/graph/selection.py +252 -0
- apsimo/intelligence/learning/__init__.py +17 -0
- apsimo/intelligence/learning/continuous_learner.py +245 -0
- apsimo/intelligence/learning/feedback_store.py +321 -0
- apsimo/intelligence/mind_model/__init__.py +1 -0
- apsimo/intelligence/mind_model/graph_baseline.py +136 -0
- apsimo/intelligence/mind_model/signal_collector.py +361 -0
- apsimo/intelligence/relationships/__init__.py +11 -0
- apsimo/intelligence/relationships/profiler.py +389 -0
- apsimo/intelligence/relationships/scorer.py +560 -0
- apsimo/intelligence/relationships/signal_floor.py +66 -0
- apsimo/intelligence/relationships/trust_tiers.py +300 -0
- apsimo/intelligence/synthesis/__init__.py +40 -0
- apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
- apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
- apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
- apsimo/intelligence/synthesis/insight_store.py +79 -0
- apsimo/intelligence/synthesis/insight_validator.py +183 -0
- apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
- apsimo/intelligence/turn_middleware/__init__.py +15 -0
- apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
- apsimo/mcp/__init__.py +41 -0
- apsimo/mcp/__main__.py +6 -0
- apsimo/mcp/config.py +287 -0
- apsimo/mcp/server.py +501 -0
- apsimo/migrations.py +187 -0
- apsimo/mining/__init__.py +27 -0
- apsimo/mining/corpus.py +239 -0
- apsimo/mining/escalations.py +289 -0
- apsimo/mining/models.py +169 -0
- apsimo/mining/store.py +210 -0
- apsimo/models/__init__.py +30 -0
- apsimo/models/memory.py +80 -0
- apsimo/models/mesh.py +72 -0
- apsimo/models/person.py +104 -0
- apsimo/models/signal.py +108 -0
- apsimo/observations/__init__.py +15 -0
- apsimo/observations/store.py +277 -0
- apsimo/patterns/__init__.py +6 -0
- apsimo/patterns/extract.py +187 -0
- apsimo/patterns/store.py +227 -0
- apsimo/persona/__init__.py +1 -0
- apsimo/persona/engine.py +611 -0
- apsimo/persona/manifest.py +140 -0
- apsimo/projects/__init__.py +28 -0
- apsimo/projects/engine.py +1681 -0
- apsimo/projects/event_outbox.py +188 -0
- apsimo/projects/models.py +216 -0
- apsimo/projects/planner.py +181 -0
- apsimo/projects/store.py +1446 -0
- apsimo/proposals/__init__.py +12 -0
- apsimo/proposals/engine.py +114 -0
- apsimo/proposals/models.py +207 -0
- apsimo/qualification/__init__.py +1 -0
- apsimo/qualification/cases.py +75 -0
- apsimo/qualification/cli.py +51 -0
- apsimo/qualification/memory_cases.py +209 -0
- apsimo/qualification/records.py +92 -0
- apsimo/qualification/report.py +87 -0
- apsimo/qualification/runner.py +311 -0
- apsimo/qualification/structured_cases.py +131 -0
- apsimo/reasoning/__init__.py +13 -0
- apsimo/reasoning/executor.py +506 -0
- apsimo/reasoning/loop.py +373 -0
- apsimo/reasoning/native_tools/__init__.py +16 -0
- apsimo/reasoning/native_tools/calculate.py +141 -0
- apsimo/reasoning/native_tools/file_ops.py +150 -0
- apsimo/reasoning/native_tools/web_search.py +49 -0
- apsimo/reasoning/tool_policy.py +182 -0
- apsimo/redact/__init__.py +176 -0
- apsimo/repos/__init__.py +5 -0
- apsimo/repos/mirrors.py +204 -0
- apsimo/research/__init__.py +41 -0
- apsimo/research/artifact.py +482 -0
- apsimo/research/gatherer.py +387 -0
- apsimo/research/pipeline.py +513 -0
- apsimo/research/search/__init__.py +7 -0
- apsimo/research/search/base.py +41 -0
- apsimo/research/search/brave.py +59 -0
- apsimo/research/search/cache.py +51 -0
- apsimo/research/search/duckduckgo.py +103 -0
- apsimo/research/search/orchestrator.py +119 -0
- apsimo/research/search/serpapi.py +59 -0
- apsimo/research/search/tavily.py +59 -0
- apsimo/research/synthesizer.py +309 -0
- apsimo/router/__init__.py +30 -0
- apsimo/router/complexity_scorer.py +148 -0
- apsimo/router/endpoints.py +153 -0
- apsimo/router/fallback.py +58 -0
- apsimo/router/functions.py +243 -0
- apsimo/router/native_policy.py +52 -0
- apsimo/router/router.py +762 -0
- apsimo/router/self_learning.py +174 -0
- apsimo/router/tiers.py +677 -0
- apsimo/sandbox/__init__.py +21 -0
- apsimo/sandbox/backend.py +195 -0
- apsimo/sandbox/manager.py +173 -0
- apsimo/scope_bounds.py +7 -0
- apsimo/secrets/__init__.py +6 -0
- apsimo/secrets/backends/__init__.py +8 -0
- apsimo/secrets/backends/base.py +42 -0
- apsimo/secrets/backends/env.py +110 -0
- apsimo/secrets/backends/keyring.py +72 -0
- apsimo/secrets/backends/onepassword.py +232 -0
- apsimo/secrets/cli.py +191 -0
- apsimo/secrets/manager.py +160 -0
- apsimo/secrets/migration.py +101 -0
- apsimo/secrets/types.py +98 -0
- apsimo/seed.py +41 -0
- apsimo/self_model/__init__.py +37 -0
- apsimo/self_model/appraisals.py +673 -0
- apsimo/self_model/benchmark.py +1314 -0
- apsimo/self_model/brief.py +40 -0
- apsimo/self_model/event_concerns.py +1128 -0
- apsimo/self_model/execution_forecasts.py +353 -0
- apsimo/self_model/expectations.py +1595 -0
- apsimo/self_model/experiments.py +1150 -0
- apsimo/self_model/journal.py +148 -0
- apsimo/self_model/judgments.py +705 -0
- apsimo/self_model/native_outcomes.py +55 -0
- apsimo/self_model/params.py +220 -0
- apsimo/self_model/perspective.py +246 -0
- apsimo/self_model/reconcile.py +183 -0
- apsimo/self_model/reply_forecasts.py +381 -0
- apsimo/self_model/runtime_forecasts.py +296 -0
- apsimo/self_model/runtime_models.py +67 -0
- apsimo/self_model/settlement.py +207 -0
- apsimo/self_model/situation.py +1731 -0
- apsimo/self_model/store.py +883 -0
- apsimo/self_model/supervised.py +137 -0
- apsimo/self_model/thinker.py +99 -0
- apsimo/self_model/trust.py +388 -0
- apsimo/self_model/workspace.py +2388 -0
- apsimo/server.py +4197 -0
- apsimo/services/__init__.py +1 -0
- apsimo/services/agent_bridge.py +474 -0
- apsimo/services/initiative_executor.py +914 -0
- apsimo/services/instance.py +297 -0
- apsimo/sessions/__init__.py +22 -0
- apsimo/sessions/config.py +13 -0
- apsimo/sessions/context_loader.py +88 -0
- apsimo/sessions/federation_session.py +75 -0
- apsimo/sessions/isolated_session.py +98 -0
- apsimo/sessions/reports.py +84 -0
- apsimo/sessions/store.py +148 -0
- apsimo/setup.py +2818 -0
- apsimo/setup_hermes.py +879 -0
- apsimo/setup_local_work.py +218 -0
- apsimo/setup_native_goals.py +134 -0
- apsimo/setup_native_reviews.py +115 -0
- apsimo/skills/__init__.py +10 -0
- apsimo/skills/base.py +108 -0
- apsimo/skills/budget.py +28 -0
- apsimo/skills/executor.py +493 -0
- apsimo/skills/executors/__init__.py +1 -0
- apsimo/skills/executors/behavioral_correction.py +75 -0
- apsimo/skills/executors/capability_gap.py +38 -0
- apsimo/skills/executors/data_quality.py +163 -0
- apsimo/skills/executors/knowledge_acquisition.py +41 -0
- apsimo/skills/executors/operational_hygiene.py +185 -0
- apsimo/skills/executors/subsystem_health.py +169 -0
- apsimo/skills/hermes_export.py +431 -0
- apsimo/skills/index.py +123 -0
- apsimo/skills/learning/__init__.py +21 -0
- apsimo/skills/learning/novelty_detector.py +206 -0
- apsimo/skills/learning/pattern_extractor.py +199 -0
- apsimo/skills/learning/triggers.py +159 -0
- apsimo/skills/loader.py +246 -0
- apsimo/skills/migrations/002_progressive_loading.sql +6 -0
- apsimo/skills/migrations/backfill_triggers.py +20 -0
- apsimo/skills/models.py +202 -0
- apsimo/skills/packager.py +128 -0
- apsimo/skills/protocols.py +70 -0
- apsimo/skills/registry.py +191 -0
- apsimo/skills/runtime.py +58 -0
- apsimo/skills/sandbox_runner.py +229 -0
- apsimo/skills/scheduler.py +129 -0
- apsimo/skills/schema.py +79 -0
- apsimo/skills/security/__init__.py +12 -0
- apsimo/skills/security/guards.py +53 -0
- apsimo/skills/security/scanner.py +223 -0
- apsimo/skills_memory/__init__.py +26 -0
- apsimo/skills_memory/distill.py +159 -0
- apsimo/skills_memory/models.py +85 -0
- apsimo/skills_memory/retrieve.py +62 -0
- apsimo/skills_memory/store.py +172 -0
- apsimo/surprise/__init__.py +6 -0
- apsimo/surprise/accumulation.py +57 -0
- apsimo/surprise/scorer.py +102 -0
- apsimo/surprise/store.py +203 -0
- apsimo/task_queue/__init__.py +69 -0
- apsimo/task_queue/action_receipts.py +148 -0
- apsimo/task_queue/approval_relay_canary.py +108 -0
- apsimo/task_queue/config.py +85 -0
- apsimo/task_queue/contract.py +361 -0
- apsimo/task_queue/events.py +130 -0
- apsimo/task_queue/governor.py +1031 -0
- apsimo/task_queue/handlers/__init__.py +16 -0
- apsimo/task_queue/handlers/base.py +37 -0
- apsimo/task_queue/handlers/inference.py +640 -0
- apsimo/task_queue/handlers/monitoring.py +116 -0
- apsimo/task_queue/handlers/registry.py +75 -0
- apsimo/task_queue/handlers/subtask_handler.py +173 -0
- apsimo/task_queue/handlers/system_maintenance.py +147 -0
- apsimo/task_queue/mesh_integration.py +111 -0
- apsimo/task_queue/models.py +317 -0
- apsimo/task_queue/queue_manager.py +8286 -0
- apsimo/task_queue/routing.py +287 -0
- apsimo/task_queue/scheduler.py +252 -0
- apsimo/task_queue/schema.sql +197 -0
- apsimo/task_queue/work_control.py +342 -0
- apsimo/task_queue/worker.py +993 -0
- apsimo/telemetry.py +145 -0
- apsimo/tom/__init__.py +6 -0
- apsimo/tom/affect.py +387 -0
- apsimo/tom/approvals.py +171 -0
- apsimo/tom/arcs.py +896 -0
- apsimo/tom/asymmetry.py +131 -0
- apsimo/tom/eligibility.py +248 -0
- apsimo/tom/engagement.py +214 -0
- apsimo/tom/exposure.py +214 -0
- apsimo/tom/extractor.py +306 -0
- apsimo/tom/fact_adapters.py +144 -0
- apsimo/tom/facts.py +326 -0
- apsimo/tom/integration.py +592 -0
- apsimo/tom/leveled.py +118 -0
- apsimo/tom/levels.py +247 -0
- apsimo/tom/recipient_audit.py +995 -0
- apsimo/tom/recipient_simulator.py +593 -0
- apsimo/tom/source_lineage.py +93 -0
- apsimo/tom/tom2.py +277 -0
- apsimo/tom/visibility.py +559 -0
- apsimo/tom/visibility_store.py +414 -0
- apsimo/tools/__init__.py +0 -0
- apsimo/tools/definitions.py +740 -0
- apsimo/tools/handlers.py +943 -0
- apsimo/toolsmith/__init__.py +26 -0
- apsimo/toolsmith/authority.py +166 -0
- apsimo/toolsmith/engine.py +559 -0
- apsimo/toolsmith/integrity.py +100 -0
- apsimo/toolsmith/miner.py +145 -0
- apsimo/toolsmith/policy.py +110 -0
- apsimo/toolsmith/registry.py +635 -0
- apsimo/turns/__init__.py +17 -0
- apsimo/turns/audio.py +134 -0
- apsimo/turns/documents.py +235 -0
- apsimo/turns/executions.py +486 -0
- apsimo/turns/hermes_history.py +245 -0
- apsimo/turns/hermes_kanban.py +268 -0
- apsimo/turns/hermes_work.py +96 -0
- apsimo/turns/idempotency.py +752 -0
- apsimo/turns/local_work.py +115 -0
- apsimo/turns/media.py +581 -0
- apsimo/turns/reported_workers.py +196 -0
- apsimo/turns/source_annotations.py +283 -0
- apsimo/turns/source_attribution.py +154 -0
- apsimo/turns/source_read.py +351 -0
- apsimo/turns/source_vectors.py +263 -0
- apsimo/turns/video.py +210 -0
- apsimo/util/autonomy_preset.py +220 -0
- apsimo/util/instance.py +92 -0
- apsimo/util/model_output.py +25 -0
- apsimo/util/quiet_hours.py +27 -0
- apsimo/util/session_safety.py +37 -0
- apsimo/util/temporal.py +343 -0
- apsimo/vector/__init__.py +75 -0
- apsimo/vector/backfill.py +171 -0
- apsimo/vector/caption.py +114 -0
- apsimo/vector/collections.py +51 -0
- apsimo/vector/config.py +102 -0
- apsimo/vector/embedder.py +670 -0
- apsimo/vector/image_preprocess.py +406 -0
- apsimo/vector/image_store.py +296 -0
- apsimo/vector/indexes.py +162 -0
- apsimo/vector/migrate.py +334 -0
- apsimo/vector/multimodal_provider.py +417 -0
- apsimo/vector/multimodal_types.py +87 -0
- apsimo/vector/openai_provider.py +119 -0
- apsimo/vector/query.py +49 -0
- apsimo/vector/reranker.py +565 -0
- apsimo/vector/safety_image.py +159 -0
- apsimo/vector/scanner.py +197 -0
- apsimo/vector/setup.py +289 -0
- apsimo/vector/store.py +533 -0
- apsimo/vector/tiers.py +263 -0
- apsimo/work_orders.py +925 -0
- apsimo/workers/__init__.py +21 -0
- apsimo/workers/agent_bridge.py +640 -0
- apsimo/workers/colony_worker.py +382 -0
- apsimo/workers/queue_worker.py +441 -0
- apsimo/workers/skills_sync.py +152 -0
- apsimo/world_model/__init__.py +71 -0
- apsimo/world_model/causal_maintenance.py +131 -0
- apsimo/world_model/causal_policy.py +43 -0
- apsimo/world_model/causal_query.py +125 -0
- apsimo/world_model/confidence.py +54 -0
- apsimo/world_model/config.py +64 -0
- apsimo/world_model/constants.py +97 -0
- apsimo/world_model/entities.py +145 -0
- apsimo/world_model/expectation_resolvers.py +177 -0
- apsimo/world_model/extraction/__init__.py +7 -0
- apsimo/world_model/extraction/base.py +62 -0
- apsimo/world_model/extraction/conversation_extractor.py +262 -0
- apsimo/world_model/extraction/detector.py +74 -0
- apsimo/world_model/extraction/document_extractor.py +78 -0
- apsimo/world_model/extraction/formats/__init__.py +24 -0
- apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
- apsimo/world_model/extraction/formats/html_fmt.py +72 -0
- apsimo/world_model/extraction/formats/json_fmt.py +68 -0
- apsimo/world_model/extraction/formats/pdf.py +43 -0
- apsimo/world_model/extraction/formats/text.py +27 -0
- apsimo/world_model/extraction/llm_extractor.py +164 -0
- apsimo/world_model/extraction/pipeline.py +73 -0
- apsimo/world_model/integrations/__init__.py +5 -0
- apsimo/world_model/integrations/mind_model_bridge.py +115 -0
- apsimo/world_model/integrations/social_intel_bridge.py +120 -0
- apsimo/world_model/jobs/__init__.py +4 -0
- apsimo/world_model/jobs/extraction_job.py +168 -0
- apsimo/world_model/llm_extract.py +572 -0
- apsimo/world_model/neo4j/__init__.py +5 -0
- apsimo/world_model/neo4j/backend.py +654 -0
- apsimo/world_model/observations.py +155 -0
- apsimo/world_model/populator.py +307 -0
- apsimo/world_model/postgres/__init__.py +1 -0
- apsimo/world_model/postgres/backend.py +683 -0
- apsimo/world_model/relationships.py +25 -0
- apsimo/world_model/resolution/__init__.py +13 -0
- apsimo/world_model/resolution/entity_resolver.py +232 -0
- apsimo/world_model/resolution/merge_audit.py +16 -0
- apsimo/world_model/resolution/merge_workflow.py +117 -0
- apsimo/world_model/source_reports.py +121 -0
- apsimo/world_model/sqlite/__init__.py +4 -0
- apsimo/world_model/sqlite/backend.py +855 -0
- apsimo/world_model/sqlite/schema.sql +132 -0
- apsimo/world_model/store.py +545 -0
- apsimo-1.3.0.dist-info/METADATA +78 -0
- apsimo-1.3.0.dist-info/RECORD +614 -0
- apsimo-1.3.0.dist-info/WHEEL +5 -0
- apsimo-1.3.0.dist-info/entry_points.txt +11 -0
- apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
- apsimo-1.3.0.dist-info/top_level.txt +2 -0
- colony_sidecar/__init__.py +4 -0
apsimo/work_orders.py
ADDED
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
"""Canonical WorkOrderV1 bridge from durable projects to external executors.
|
|
2
|
+
|
|
3
|
+
Colony owns the goal/work ledger; it does not execute external effects. A
|
|
4
|
+
project step becomes one deterministic ``agent_action`` queue job containing
|
|
5
|
+
bounded references and success criteria. The host-side Action Plane decides
|
|
6
|
+
authorization and records real effect receipts.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import importlib
|
|
13
|
+
import inspect
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import asdict, dataclass
|
|
19
|
+
from datetime import datetime, timedelta, timezone
|
|
20
|
+
from typing import Any, Dict, Iterable, Mapping, Optional, Tuple
|
|
21
|
+
|
|
22
|
+
from apsimo.execution_results import (
|
|
23
|
+
EXTERNAL_EFFECT_CLASSES,
|
|
24
|
+
ExecutionResultError,
|
|
25
|
+
ExecutionResultV1,
|
|
26
|
+
ReceiptVerificationV1,
|
|
27
|
+
)
|
|
28
|
+
from apsimo.projects.store import ProjectStore
|
|
29
|
+
from apsimo.scope_bounds import VIEWER_SCOPE_MAX_CHARS
|
|
30
|
+
from apsimo.task_queue.models import Job, JobPriority, JobStatus, JobType
|
|
31
|
+
from apsimo.task_queue.models import JobCapabilityRequirement
|
|
32
|
+
from apsimo.task_queue.routing import (
|
|
33
|
+
ACTION_PLANE_ROUTE,
|
|
34
|
+
WORK_ORDER_ROUTE,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
WORK_ORDER_VERSION = 1
|
|
39
|
+
WORK_ORDER_EXECUTOR_CAPABILITY = WORK_ORDER_ROUTE
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
_SECRET_ASSIGNMENT = re.compile(
|
|
42
|
+
r"(?i)\b(api[_-]?key|authorization|bearer|credential|password|secret|token)"
|
|
43
|
+
r"\s*[:=]\s*(?:bearer\s+)?[^\s,;]+"
|
|
44
|
+
)
|
|
45
|
+
_IMPORT_PATH = re.compile(
|
|
46
|
+
r"^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*:[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*$"
|
|
47
|
+
)
|
|
48
|
+
_ACTION_KIND = {
|
|
49
|
+
"analyze": ("agent_project_analyze", ("memory:read", "reasoning"), "internal"),
|
|
50
|
+
"research": ("agent_project_research", ("memory:read", "web:read", "reasoning"), "read_only"),
|
|
51
|
+
"internal": ("agent_project_internal", ("memory:read", "reasoning"), "internal"),
|
|
52
|
+
"directed": (
|
|
53
|
+
"agent_project_directed",
|
|
54
|
+
(
|
|
55
|
+
"code:execute",
|
|
56
|
+
"filesystem:read",
|
|
57
|
+
"filesystem:write",
|
|
58
|
+
"git:write",
|
|
59
|
+
"memory:read",
|
|
60
|
+
"reasoning",
|
|
61
|
+
"terminal:execute",
|
|
62
|
+
"web:read",
|
|
63
|
+
),
|
|
64
|
+
"mutation",
|
|
65
|
+
),
|
|
66
|
+
"deliver": ("agent_project_deliver", ("messaging:send",), "disclosure"),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def action_authority(action_kind: str) -> Tuple[str, Tuple[str, ...], str]:
|
|
71
|
+
"""Return the canonical WorkOrder action/capability/risk contract."""
|
|
72
|
+
|
|
73
|
+
action = _ACTION_KIND.get(str(action_kind or ""))
|
|
74
|
+
if action is None:
|
|
75
|
+
raise WorkOrderError("unsupported project action kind")
|
|
76
|
+
return action
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class WorkOrderError(ValueError):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ReceiptVerifierConfigurationError(RuntimeError):
|
|
84
|
+
"""A configured external-effect verifier could not be built safely."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_receipt_verifier_from_env(environ: Optional[Mapping[str, str]] = None) -> Any:
|
|
88
|
+
"""Construct the configured receipt verifier without host coupling.
|
|
89
|
+
|
|
90
|
+
The verifier remains an operator-selected plugin. Colony knows only a
|
|
91
|
+
``module:attribute`` factory and a bounded JSON object of keyword
|
|
92
|
+
arguments; it does not import any particular host deployment by default. An
|
|
93
|
+
explicitly configured verifier fails closed on every import, parse,
|
|
94
|
+
construction, or interface error.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
env = os.environ if environ is None else environ
|
|
98
|
+
spec = str(env.get("COLONY_WORK_ORDER_RECEIPT_VERIFIER", "") or "").strip()
|
|
99
|
+
if not spec:
|
|
100
|
+
return None
|
|
101
|
+
if len(spec) > 512 or not _IMPORT_PATH.fullmatch(spec):
|
|
102
|
+
raise ReceiptVerifierConfigurationError(
|
|
103
|
+
"COLONY_WORK_ORDER_RECEIPT_VERIFIER must be module:attribute"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
raw_config = str(
|
|
107
|
+
env.get("COLONY_WORK_ORDER_RECEIPT_VERIFIER_CONFIG", "{}") or "{}"
|
|
108
|
+
)
|
|
109
|
+
if len(raw_config.encode("utf-8")) > 16 * 1024:
|
|
110
|
+
raise ReceiptVerifierConfigurationError("receipt verifier config is too large")
|
|
111
|
+
try:
|
|
112
|
+
config = json.loads(raw_config)
|
|
113
|
+
except (TypeError, ValueError) as exc:
|
|
114
|
+
raise ReceiptVerifierConfigurationError(
|
|
115
|
+
"receipt verifier config must be valid JSON"
|
|
116
|
+
) from exc
|
|
117
|
+
if not isinstance(config, dict) or len(config) > 64:
|
|
118
|
+
raise ReceiptVerifierConfigurationError(
|
|
119
|
+
"receipt verifier config must be a JSON object with at most 64 fields"
|
|
120
|
+
)
|
|
121
|
+
if not all(isinstance(key, str) and key for key in config):
|
|
122
|
+
raise ReceiptVerifierConfigurationError(
|
|
123
|
+
"receipt verifier config keys must be non-empty strings"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
module_name, attribute_path = spec.split(":", 1)
|
|
127
|
+
try:
|
|
128
|
+
target = importlib.import_module(module_name)
|
|
129
|
+
for name in attribute_path.split("."):
|
|
130
|
+
target = getattr(target, name)
|
|
131
|
+
except (ImportError, AttributeError) as exc:
|
|
132
|
+
raise ReceiptVerifierConfigurationError(
|
|
133
|
+
"configured receipt verifier could not be imported"
|
|
134
|
+
) from exc
|
|
135
|
+
if not callable(target):
|
|
136
|
+
raise ReceiptVerifierConfigurationError(
|
|
137
|
+
"configured receipt verifier factory is not callable"
|
|
138
|
+
)
|
|
139
|
+
try:
|
|
140
|
+
verifier = target(**config)
|
|
141
|
+
except Exception as exc:
|
|
142
|
+
raise ReceiptVerifierConfigurationError(
|
|
143
|
+
"configured receipt verifier factory failed"
|
|
144
|
+
) from exc
|
|
145
|
+
if not callable(getattr(verifier, "verify", None)) and not callable(verifier):
|
|
146
|
+
raise ReceiptVerifierConfigurationError(
|
|
147
|
+
"configured receipt verifier has no callable verify interface"
|
|
148
|
+
)
|
|
149
|
+
return verifier
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _bounded_text(value: object, name: str, maximum: int) -> str:
|
|
153
|
+
result = str(value or "").strip()
|
|
154
|
+
if not result or len(result) > maximum:
|
|
155
|
+
raise WorkOrderError(f"{name} must be 1..{maximum} characters")
|
|
156
|
+
return _SECRET_ASSIGNMENT.sub(lambda match: f"{match.group(1)}=[REDACTED]", result)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _canonical(value: object) -> str:
|
|
160
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True)
|
|
164
|
+
class WorkOrderV1:
|
|
165
|
+
work_order_id: str
|
|
166
|
+
idempotency_key: str
|
|
167
|
+
project_id: str
|
|
168
|
+
step_id: str
|
|
169
|
+
step_ordinal: int
|
|
170
|
+
objective: str
|
|
171
|
+
success_criteria: Tuple[str, ...]
|
|
172
|
+
context_refs: Tuple[str, ...]
|
|
173
|
+
capability_allowlist: Tuple[str, ...]
|
|
174
|
+
risk_class: str
|
|
175
|
+
recipient_scope: str
|
|
176
|
+
max_runtime_seconds: int
|
|
177
|
+
max_attempts: int
|
|
178
|
+
issued_at: str
|
|
179
|
+
deadline: str
|
|
180
|
+
action_hint: str
|
|
181
|
+
work_order_digest: str
|
|
182
|
+
source: str = "project_engine"
|
|
183
|
+
schema: str = "WorkOrderV1"
|
|
184
|
+
version: int = WORK_ORDER_VERSION
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def from_payload(cls, payload: Mapping[str, Any]) -> "WorkOrderV1":
|
|
188
|
+
"""Reconstruct and verify every authority-bearing queue field.
|
|
189
|
+
|
|
190
|
+
A WorkOrder digest is useful only when the queue validates the object
|
|
191
|
+
that produced it. Treating ``schema=WorkOrderV1`` as a routing hint
|
|
192
|
+
would let a malformed row acquire the privileged Action Plane route.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
if not isinstance(payload, Mapping):
|
|
196
|
+
raise WorkOrderError("WorkOrder payload must be an object")
|
|
197
|
+
if payload.get("schema") != "WorkOrderV1":
|
|
198
|
+
raise WorkOrderError("WorkOrder schema is invalid")
|
|
199
|
+
if payload.get("version") != WORK_ORDER_VERSION:
|
|
200
|
+
raise WorkOrderError("WorkOrder version is stale")
|
|
201
|
+
try:
|
|
202
|
+
order = cls(
|
|
203
|
+
work_order_id=str(payload["work_order_id"]),
|
|
204
|
+
idempotency_key=str(payload["idempotency_key"]),
|
|
205
|
+
project_id=str(payload["project_id"]),
|
|
206
|
+
step_id=str(payload["step_id"]),
|
|
207
|
+
step_ordinal=int(payload["step_ordinal"]),
|
|
208
|
+
objective=str(payload["objective"]),
|
|
209
|
+
success_criteria=tuple(payload["success_criteria"]),
|
|
210
|
+
context_refs=tuple(payload["context_refs"]),
|
|
211
|
+
capability_allowlist=tuple(payload["capability_allowlist"]),
|
|
212
|
+
risk_class=str(payload["risk_class"]),
|
|
213
|
+
recipient_scope=str(payload["recipient_scope"]),
|
|
214
|
+
max_runtime_seconds=int(payload["max_runtime_seconds"]),
|
|
215
|
+
max_attempts=int(payload["max_attempts"]),
|
|
216
|
+
issued_at=str(payload["issued_at"]),
|
|
217
|
+
deadline=str(payload["deadline"]),
|
|
218
|
+
action_hint=str(payload["action_hint"]),
|
|
219
|
+
work_order_digest=str(payload["work_order_digest"]),
|
|
220
|
+
source=str(payload["source"]),
|
|
221
|
+
schema=str(payload["schema"]),
|
|
222
|
+
version=int(payload["version"]),
|
|
223
|
+
)
|
|
224
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
225
|
+
missing = exc.args[0] if isinstance(exc, KeyError) else str(exc)
|
|
226
|
+
raise WorkOrderError(
|
|
227
|
+
f"invalid WorkOrder authority field: {missing}"
|
|
228
|
+
) from exc
|
|
229
|
+
order.validate()
|
|
230
|
+
order.validate_payload(payload)
|
|
231
|
+
return order
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def for_project_step(
|
|
235
|
+
cls,
|
|
236
|
+
project,
|
|
237
|
+
step,
|
|
238
|
+
*,
|
|
239
|
+
context_refs: Iterable[str] = (),
|
|
240
|
+
now: Optional[datetime] = None,
|
|
241
|
+
) -> "WorkOrderV1":
|
|
242
|
+
action = action_authority(str(step.action_kind or ""))
|
|
243
|
+
action_hint, capabilities, risk_class = action
|
|
244
|
+
project_id = _bounded_text(project.id, "project_id", 128)
|
|
245
|
+
step_id = _bounded_text(step.id, "step_id", 128)
|
|
246
|
+
project_source = _bounded_text(project.source, "project_source", 64)
|
|
247
|
+
boundary_subjects = " ".join(
|
|
248
|
+
item for item in (
|
|
249
|
+
str(getattr(step, "boundary_subject", "") or "").strip(),
|
|
250
|
+
str(getattr(project, "subject_person_id", "") or "").strip(),
|
|
251
|
+
" ".join(
|
|
252
|
+
str(item) for item in (
|
|
253
|
+
getattr(project, "entity_ids", None) or ()
|
|
254
|
+
) if str(item).strip()
|
|
255
|
+
),
|
|
256
|
+
) if item
|
|
257
|
+
)
|
|
258
|
+
objective = _bounded_text(
|
|
259
|
+
f"Project: {project.title}\nObjective: {project.objective}\n"
|
|
260
|
+
f"Current step: {step.description}\n"
|
|
261
|
+
f"Boundary subject: {boundary_subjects or 'project subject'}",
|
|
262
|
+
"objective",
|
|
263
|
+
4000,
|
|
264
|
+
)
|
|
265
|
+
criteria = (
|
|
266
|
+
"Complete only the named project step",
|
|
267
|
+
"Return structured status and evidence receipts",
|
|
268
|
+
"Do not claim an external effect without independent evidence",
|
|
269
|
+
)
|
|
270
|
+
refs = tuple(sorted({
|
|
271
|
+
_bounded_text(item, "context_ref", 256) for item in context_refs if str(item).strip()
|
|
272
|
+
}))
|
|
273
|
+
# The deadline is authority-relevant, so its source must be stable
|
|
274
|
+
# across polling/restarts. A caller may override it for deterministic
|
|
275
|
+
# construction; otherwise the durable step creation time is used.
|
|
276
|
+
observed = now or datetime.fromtimestamp(
|
|
277
|
+
float(
|
|
278
|
+
getattr(step, "work_order_issued_at", 0.0)
|
|
279
|
+
or getattr(step, "created_at", 0.0)
|
|
280
|
+
or getattr(project, "created_at", 0.0)
|
|
281
|
+
),
|
|
282
|
+
tz=timezone.utc,
|
|
283
|
+
)
|
|
284
|
+
if observed.tzinfo is None:
|
|
285
|
+
observed = observed.replace(tzinfo=timezone.utc)
|
|
286
|
+
runtime = 900 if step.action_kind == "research" else 600
|
|
287
|
+
if project_source in {"cognition_spine", "governed_action"}:
|
|
288
|
+
recipient_scope = _bounded_text(
|
|
289
|
+
getattr(project, "viewer_scope", "") or "owner",
|
|
290
|
+
"recipient_scope", VIEWER_SCOPE_MAX_CHARS,
|
|
291
|
+
)
|
|
292
|
+
else:
|
|
293
|
+
recipient_scope = (
|
|
294
|
+
"owner" if project.source == "owner" else "project:%s" % project_id
|
|
295
|
+
)
|
|
296
|
+
authority = {
|
|
297
|
+
"schema": "WorkOrderV1",
|
|
298
|
+
"version": WORK_ORDER_VERSION,
|
|
299
|
+
"source": project_source,
|
|
300
|
+
"project_id": project_id,
|
|
301
|
+
"step_id": step_id,
|
|
302
|
+
"step_ordinal": int(step.ordinal),
|
|
303
|
+
"objective": objective,
|
|
304
|
+
"success_criteria": list(criteria),
|
|
305
|
+
"context_refs": list(refs),
|
|
306
|
+
"capability_allowlist": list(capabilities),
|
|
307
|
+
"risk_class": risk_class,
|
|
308
|
+
"recipient_scope": recipient_scope,
|
|
309
|
+
"max_runtime_seconds": runtime,
|
|
310
|
+
"max_attempts": 2,
|
|
311
|
+
"issued_at": observed.isoformat(),
|
|
312
|
+
"deadline": (observed + timedelta(hours=24)).isoformat(),
|
|
313
|
+
"action_hint": action_hint,
|
|
314
|
+
}
|
|
315
|
+
digest = cls.authority_digest_from_payload(authority)
|
|
316
|
+
return cls(
|
|
317
|
+
work_order_id="work-%s" % digest[:24],
|
|
318
|
+
idempotency_key="project-step:%s" % digest,
|
|
319
|
+
project_id=project_id,
|
|
320
|
+
step_id=step_id,
|
|
321
|
+
step_ordinal=int(step.ordinal),
|
|
322
|
+
objective=objective,
|
|
323
|
+
success_criteria=criteria,
|
|
324
|
+
context_refs=refs,
|
|
325
|
+
capability_allowlist=tuple(capabilities),
|
|
326
|
+
risk_class=risk_class,
|
|
327
|
+
recipient_scope=str(authority["recipient_scope"]),
|
|
328
|
+
max_runtime_seconds=runtime,
|
|
329
|
+
max_attempts=2,
|
|
330
|
+
issued_at=str(authority["issued_at"]),
|
|
331
|
+
deadline=str(authority["deadline"]),
|
|
332
|
+
action_hint=action_hint,
|
|
333
|
+
work_order_digest=digest,
|
|
334
|
+
source=project_source,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def effect_class(self) -> str:
|
|
339
|
+
"""Normalize authority risk into the effect class used by receipts."""
|
|
340
|
+
|
|
341
|
+
return self.risk_class if self.risk_class in (
|
|
342
|
+
"read_only", "mutation", "disclosure"
|
|
343
|
+
) else "none"
|
|
344
|
+
|
|
345
|
+
def authority_payload(self) -> Dict[str, Any]:
|
|
346
|
+
"""Return every field that may change execution authority or budget."""
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
"schema": self.schema,
|
|
350
|
+
"version": self.version,
|
|
351
|
+
"source": self.source,
|
|
352
|
+
"project_id": self.project_id,
|
|
353
|
+
"step_id": self.step_id,
|
|
354
|
+
"step_ordinal": self.step_ordinal,
|
|
355
|
+
"objective": self.objective,
|
|
356
|
+
"success_criteria": list(self.success_criteria),
|
|
357
|
+
"context_refs": list(self.context_refs),
|
|
358
|
+
"capability_allowlist": list(self.capability_allowlist),
|
|
359
|
+
"risk_class": self.risk_class,
|
|
360
|
+
"recipient_scope": self.recipient_scope,
|
|
361
|
+
"max_runtime_seconds": self.max_runtime_seconds,
|
|
362
|
+
"max_attempts": self.max_attempts,
|
|
363
|
+
"issued_at": self.issued_at,
|
|
364
|
+
"deadline": self.deadline,
|
|
365
|
+
"action_hint": self.action_hint,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
@classmethod
|
|
369
|
+
def authority_digest_from_payload(cls, payload: Mapping[str, Any]) -> str:
|
|
370
|
+
fields = (
|
|
371
|
+
"schema", "version", "source", "project_id", "step_id",
|
|
372
|
+
"step_ordinal", "objective", "success_criteria", "context_refs",
|
|
373
|
+
"capability_allowlist", "risk_class", "recipient_scope",
|
|
374
|
+
"max_runtime_seconds", "max_attempts", "issued_at", "deadline",
|
|
375
|
+
"action_hint",
|
|
376
|
+
)
|
|
377
|
+
try:
|
|
378
|
+
authority = {name: payload[name] for name in fields}
|
|
379
|
+
except KeyError as exc:
|
|
380
|
+
raise WorkOrderError(f"missing authority field: {exc.args[0]}") from exc
|
|
381
|
+
return hashlib.sha256(_canonical(authority).encode("utf-8")).hexdigest()
|
|
382
|
+
|
|
383
|
+
def validate(self) -> None:
|
|
384
|
+
if self.schema != "WorkOrderV1" or self.version != WORK_ORDER_VERSION:
|
|
385
|
+
raise WorkOrderError("WorkOrder schema/version is invalid")
|
|
386
|
+
_bounded_text(self.project_id, "project_id", 128)
|
|
387
|
+
_bounded_text(self.step_id, "step_id", 128)
|
|
388
|
+
_bounded_text(self.source, "source", 64)
|
|
389
|
+
_bounded_text(self.objective, "objective", 4000)
|
|
390
|
+
_bounded_text(
|
|
391
|
+
self.recipient_scope, "recipient_scope", VIEWER_SCOPE_MAX_CHARS,
|
|
392
|
+
)
|
|
393
|
+
if self.step_ordinal < 0:
|
|
394
|
+
raise WorkOrderError("step_ordinal must be non-negative")
|
|
395
|
+
if not self.success_criteria or len(self.success_criteria) > 32:
|
|
396
|
+
raise WorkOrderError("success_criteria must contain 1..32 entries")
|
|
397
|
+
if len(self.context_refs) > 128:
|
|
398
|
+
raise WorkOrderError("context_refs exceeds 128 entries")
|
|
399
|
+
if self.max_runtime_seconds <= 0 or self.max_runtime_seconds > 86400:
|
|
400
|
+
raise WorkOrderError("max_runtime_seconds is outside 1..86400")
|
|
401
|
+
if self.max_attempts <= 0 or self.max_attempts > 10:
|
|
402
|
+
raise WorkOrderError("max_attempts is outside 1..10")
|
|
403
|
+
if not re.fullmatch(r"[0-9a-f]{64}", self.work_order_digest):
|
|
404
|
+
raise WorkOrderError("WorkOrder digest is invalid")
|
|
405
|
+
matching = [
|
|
406
|
+
authority for authority in _ACTION_KIND.values()
|
|
407
|
+
if authority[0] == self.action_hint
|
|
408
|
+
]
|
|
409
|
+
if len(matching) != 1:
|
|
410
|
+
raise WorkOrderError("WorkOrder action hint is unsupported")
|
|
411
|
+
_hint, expected_capabilities, expected_risk = matching[0]
|
|
412
|
+
if tuple(self.capability_allowlist) != tuple(expected_capabilities):
|
|
413
|
+
raise WorkOrderError(
|
|
414
|
+
"WorkOrder capability allowlist does not match action authority"
|
|
415
|
+
)
|
|
416
|
+
if self.risk_class != expected_risk:
|
|
417
|
+
raise WorkOrderError(
|
|
418
|
+
"WorkOrder risk class does not match action authority"
|
|
419
|
+
)
|
|
420
|
+
for name, values, maximum in (
|
|
421
|
+
("success_criteria", self.success_criteria, 1000),
|
|
422
|
+
("context_refs", self.context_refs, 256),
|
|
423
|
+
("capability_allowlist", self.capability_allowlist, 128),
|
|
424
|
+
):
|
|
425
|
+
for value in values:
|
|
426
|
+
_bounded_text(value, name, maximum)
|
|
427
|
+
try:
|
|
428
|
+
issued = datetime.fromisoformat(self.issued_at.replace("Z", "+00:00"))
|
|
429
|
+
deadline = datetime.fromisoformat(self.deadline.replace("Z", "+00:00"))
|
|
430
|
+
except ValueError as exc:
|
|
431
|
+
raise WorkOrderError("WorkOrder timestamps must be ISO-8601") from exc
|
|
432
|
+
if issued.tzinfo is None or deadline.tzinfo is None or deadline <= issued:
|
|
433
|
+
raise WorkOrderError("WorkOrder deadline must follow issued_at")
|
|
434
|
+
digest = self.authority_digest_from_payload(self.authority_payload())
|
|
435
|
+
if digest != self.work_order_digest:
|
|
436
|
+
raise WorkOrderError("WorkOrder authority digest mismatch")
|
|
437
|
+
if self.work_order_id != "work-%s" % digest[:24]:
|
|
438
|
+
raise WorkOrderError("WorkOrder ID does not match authority digest")
|
|
439
|
+
if self.idempotency_key != "project-step:%s" % digest:
|
|
440
|
+
raise WorkOrderError("WorkOrder idempotency key does not match authority digest")
|
|
441
|
+
|
|
442
|
+
def legacy_work_order_id(self) -> str:
|
|
443
|
+
"""Return the pre-P1 transport ID for migration collision detection.
|
|
444
|
+
|
|
445
|
+
It is never authorized for execution by the P1 adapter. Looking it up
|
|
446
|
+
before posting prevents an in-flight pre-P1 job from being duplicated
|
|
447
|
+
under the stronger digest.
|
|
448
|
+
"""
|
|
449
|
+
|
|
450
|
+
legacy_identity = {
|
|
451
|
+
"version": self.version,
|
|
452
|
+
"project_id": self.project_id,
|
|
453
|
+
"step_id": self.step_id,
|
|
454
|
+
"step_ordinal": self.step_ordinal,
|
|
455
|
+
"objective": self.objective,
|
|
456
|
+
"context_refs": self.context_refs,
|
|
457
|
+
"action_hint": self.action_hint,
|
|
458
|
+
}
|
|
459
|
+
digest = hashlib.sha256(_canonical(legacy_identity).encode("utf-8")).hexdigest()
|
|
460
|
+
return "work-%s" % digest[:24]
|
|
461
|
+
|
|
462
|
+
def validate_payload(self, payload: Mapping[str, Any]) -> None:
|
|
463
|
+
digest = self.authority_digest_from_payload(payload)
|
|
464
|
+
if digest != self.work_order_digest:
|
|
465
|
+
raise WorkOrderError("queued WorkOrder authority digest mismatch")
|
|
466
|
+
if str(payload.get("work_order_digest") or "") != digest:
|
|
467
|
+
raise WorkOrderError("queued WorkOrder digest field mismatch")
|
|
468
|
+
if str(payload.get("work_order_id") or "") != self.work_order_id:
|
|
469
|
+
raise WorkOrderError("queued WorkOrder ID mismatch")
|
|
470
|
+
if int(payload.get("version") or 0) != self.version:
|
|
471
|
+
raise WorkOrderError("queued WorkOrder version is stale")
|
|
472
|
+
|
|
473
|
+
def payload(self) -> Dict[str, Any]:
|
|
474
|
+
self.validate()
|
|
475
|
+
result = asdict(self)
|
|
476
|
+
result["success_criteria"] = list(self.success_criteria)
|
|
477
|
+
result["context_refs"] = list(self.context_refs)
|
|
478
|
+
result["capability_allowlist"] = list(self.capability_allowlist)
|
|
479
|
+
# Compatibility fields used by the current host action adapter. They
|
|
480
|
+
# repeat bounded public metadata, never credentials or copied context.
|
|
481
|
+
result["description"] = self.objective
|
|
482
|
+
result["context"] = {
|
|
483
|
+
"WORK_ORDER_ID": self.work_order_id,
|
|
484
|
+
"PROJECT_ID": self.project_id,
|
|
485
|
+
"STEP_ID": self.step_id,
|
|
486
|
+
}
|
|
487
|
+
return result
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class QueueWorkOrderAdapter:
|
|
491
|
+
"""Idempotently post/poll WorkOrderV1 jobs on Colony's durable queue."""
|
|
492
|
+
|
|
493
|
+
def __init__(
|
|
494
|
+
self,
|
|
495
|
+
task_queue_manager,
|
|
496
|
+
*,
|
|
497
|
+
project_store: Optional[ProjectStore] = None,
|
|
498
|
+
receipt_verifier: Any = None,
|
|
499
|
+
approval_authority: Any = None,
|
|
500
|
+
posted_by: str = "project-engine",
|
|
501
|
+
) -> None:
|
|
502
|
+
self.manager = task_queue_manager
|
|
503
|
+
# Production passes the durable ProjectEngine store. The in-memory
|
|
504
|
+
# default keeps direct/library construction backward compatible while
|
|
505
|
+
# preserving the same ledger invariants in tests.
|
|
506
|
+
self.project_store = project_store or ProjectStore()
|
|
507
|
+
self.receipt_verifier = receipt_verifier
|
|
508
|
+
self.approval_authority = approval_authority
|
|
509
|
+
self.posted_by = posted_by
|
|
510
|
+
|
|
511
|
+
def _approval_store(self):
|
|
512
|
+
if self.approval_authority is not None:
|
|
513
|
+
return self.approval_authority
|
|
514
|
+
from apsimo.initiatives.approval_authority import (
|
|
515
|
+
ApprovalAuthorityStore,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
self.approval_authority = ApprovalAuthorityStore()
|
|
519
|
+
return self.approval_authority
|
|
520
|
+
|
|
521
|
+
async def _prepare_effect_authority(
|
|
522
|
+
self,
|
|
523
|
+
queue,
|
|
524
|
+
job: Job,
|
|
525
|
+
order: WorkOrderV1,
|
|
526
|
+
) -> Job:
|
|
527
|
+
"""Materialize or consume canonical authority while the job is blocked.
|
|
528
|
+
|
|
529
|
+
The queue row is born blocked, so a bounded grant can be consumed
|
|
530
|
+
without a claim race. Every retry reuses the exact grant-use or request
|
|
531
|
+
record and repairs an interrupted cross-database status transition.
|
|
532
|
+
"""
|
|
533
|
+
|
|
534
|
+
from apsimo.initiatives.approval_authority import (
|
|
535
|
+
prepare_action_approval,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
authority = prepare_action_approval(
|
|
539
|
+
self._approval_store(),
|
|
540
|
+
job_id=job.job_id,
|
|
541
|
+
job_type=job.job_type.value,
|
|
542
|
+
payload=job.payload,
|
|
543
|
+
deadline=job.deadline or order.deadline,
|
|
544
|
+
approval_started_at=job.posted_at,
|
|
545
|
+
)
|
|
546
|
+
state = authority["state"]
|
|
547
|
+
tags = authority["tags"]
|
|
548
|
+
target = None
|
|
549
|
+
reason = "approval_request_materialized"
|
|
550
|
+
if state in {"authorized_grant", "authorized_direct"}:
|
|
551
|
+
target = JobStatus.QUEUED
|
|
552
|
+
reason = (
|
|
553
|
+
"bounded_grant"
|
|
554
|
+
if state == "authorized_grant"
|
|
555
|
+
else "approval_decision_reconciled"
|
|
556
|
+
)
|
|
557
|
+
elif state == "rejected":
|
|
558
|
+
target = JobStatus.CANCELLED
|
|
559
|
+
reason = "approval_rejection_reconciled"
|
|
560
|
+
elif state in {"expired", "superseded"}:
|
|
561
|
+
target = JobStatus.FAILED
|
|
562
|
+
reason = "canonical_approval_%s" % state
|
|
563
|
+
|
|
564
|
+
if target is None:
|
|
565
|
+
await queue.merge_job_tags(job.job_id, tags)
|
|
566
|
+
else:
|
|
567
|
+
changed = await queue.update_job_status(
|
|
568
|
+
job.job_id,
|
|
569
|
+
target,
|
|
570
|
+
reason=reason,
|
|
571
|
+
tags=tags,
|
|
572
|
+
remove_tags=(
|
|
573
|
+
"hold_kind", "blocked_reason", "awaiting_owner_approval",
|
|
574
|
+
),
|
|
575
|
+
)
|
|
576
|
+
if not changed:
|
|
577
|
+
raise WorkOrderError(
|
|
578
|
+
"canonical approval resolved but queue transition must reconcile"
|
|
579
|
+
)
|
|
580
|
+
refreshed = await queue.get_job(job.job_id)
|
|
581
|
+
return refreshed or job
|
|
582
|
+
|
|
583
|
+
async def execute(self, project, step, *, context_refs: Iterable[str] = ()) -> Tuple[Optional[bool], str]:
|
|
584
|
+
if not step.work_order_issued_at:
|
|
585
|
+
step.work_order_issued_at = datetime.now(timezone.utc).timestamp()
|
|
586
|
+
issued_at = datetime.fromtimestamp(step.work_order_issued_at, tz=timezone.utc)
|
|
587
|
+
order = WorkOrderV1.for_project_step(
|
|
588
|
+
project, step, context_refs=context_refs, now=issued_at
|
|
589
|
+
)
|
|
590
|
+
try:
|
|
591
|
+
if step.work_order_digest and step.work_order_digest != order.work_order_digest:
|
|
592
|
+
raise WorkOrderError(
|
|
593
|
+
"step authority changed after WorkOrder issue; explicit reissue required"
|
|
594
|
+
)
|
|
595
|
+
# Atomically insert missing parents or validate existing ones.
|
|
596
|
+
# Never let a stale polling object upsert a terminal lifecycle
|
|
597
|
+
# back to active before posting new work.
|
|
598
|
+
work_order_ref = self.project_store.prepare_work_order(
|
|
599
|
+
project, step, order,
|
|
600
|
+
)
|
|
601
|
+
step.work_order_ref = work_order_ref
|
|
602
|
+
step.work_order_digest = order.work_order_digest
|
|
603
|
+
except (ValueError, WorkOrderError) as exc:
|
|
604
|
+
return False, f"work order rejected: {exc}"
|
|
605
|
+
|
|
606
|
+
queue = self.manager.queue
|
|
607
|
+
job = await queue.get_job(order.work_order_id)
|
|
608
|
+
if job is None:
|
|
609
|
+
legacy_id = order.legacy_work_order_id()
|
|
610
|
+
legacy_job = await queue.get_job(legacy_id)
|
|
611
|
+
if legacy_job is not None:
|
|
612
|
+
return None, (
|
|
613
|
+
f"work_order:{legacy_id}:legacy_migration_hold; "
|
|
614
|
+
"cancel or reconcile it before canonical reissue"
|
|
615
|
+
)
|
|
616
|
+
job = Job(
|
|
617
|
+
job_id=order.work_order_id,
|
|
618
|
+
job_type=JobType.AGENT_ACTION,
|
|
619
|
+
payload=order.payload(),
|
|
620
|
+
priority=(JobPriority.HIGH if project.source == "owner" else JobPriority.NORMAL),
|
|
621
|
+
deadline=datetime.fromisoformat(order.deadline),
|
|
622
|
+
max_retries=max(0, order.max_attempts - 1),
|
|
623
|
+
timeout_secs=float(order.max_runtime_seconds),
|
|
624
|
+
posted_by=self.posted_by,
|
|
625
|
+
capabilities=[
|
|
626
|
+
JobCapabilityRequirement(name=capability)
|
|
627
|
+
for capability in (
|
|
628
|
+
WORK_ORDER_EXECUTOR_CAPABILITY,
|
|
629
|
+
ACTION_PLANE_ROUTE,
|
|
630
|
+
*order.capability_allowlist,
|
|
631
|
+
)
|
|
632
|
+
],
|
|
633
|
+
tags={
|
|
634
|
+
"schema": order.schema,
|
|
635
|
+
"project_id": order.project_id,
|
|
636
|
+
"step_id": order.step_id,
|
|
637
|
+
"risk_class": order.risk_class,
|
|
638
|
+
"idempotency_key": order.idempotency_key,
|
|
639
|
+
"work_order_digest": order.work_order_digest,
|
|
640
|
+
"work_order_version": str(order.version),
|
|
641
|
+
"executor_protocol": WORK_ORDER_EXECUTOR_CAPABILITY,
|
|
642
|
+
},
|
|
643
|
+
)
|
|
644
|
+
if order.effect_class in EXTERNAL_EFFECT_CLASSES:
|
|
645
|
+
job.status = JobStatus.BLOCKED
|
|
646
|
+
job.tags.update({
|
|
647
|
+
"hold_kind": "approval",
|
|
648
|
+
"blocked_reason": "awaiting_owner_approval",
|
|
649
|
+
"awaiting_owner_approval": "true",
|
|
650
|
+
"approval_requested_at": datetime.now(
|
|
651
|
+
timezone.utc
|
|
652
|
+
).isoformat(),
|
|
653
|
+
})
|
|
654
|
+
await queue.post(job)
|
|
655
|
+
if (
|
|
656
|
+
order.effect_class in EXTERNAL_EFFECT_CLASSES
|
|
657
|
+
and job.status is JobStatus.BLOCKED
|
|
658
|
+
and job.tags.get("blocked_reason") == "awaiting_owner_approval"
|
|
659
|
+
and not job.tags.get("approval_request_id")
|
|
660
|
+
):
|
|
661
|
+
job = await self._prepare_effect_authority(
|
|
662
|
+
queue, job, order,
|
|
663
|
+
)
|
|
664
|
+
return None, (
|
|
665
|
+
f"work_order:{order.work_order_id}:"
|
|
666
|
+
f"{job.status.value}"
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
if (
|
|
670
|
+
order.effect_class in EXTERNAL_EFFECT_CLASSES
|
|
671
|
+
and job.status is JobStatus.BLOCKED
|
|
672
|
+
and job.tags.get("blocked_reason") == "awaiting_owner_approval"
|
|
673
|
+
):
|
|
674
|
+
job = await self._prepare_effect_authority(queue, job, order)
|
|
675
|
+
|
|
676
|
+
try:
|
|
677
|
+
order.validate_payload(job.payload)
|
|
678
|
+
except WorkOrderError as exc:
|
|
679
|
+
return False, f"work order rejected: {exc}"
|
|
680
|
+
|
|
681
|
+
reconciliation = str(
|
|
682
|
+
job.tags.get("effect_reconciliation_finding") or ""
|
|
683
|
+
).lower()
|
|
684
|
+
if reconciliation == "applied":
|
|
685
|
+
return None, (
|
|
686
|
+
f"work_order:{order.work_order_id}:"
|
|
687
|
+
"independently_verified_applied_no_retry"
|
|
688
|
+
)
|
|
689
|
+
if reconciliation == "not_applied":
|
|
690
|
+
return None, (
|
|
691
|
+
f"work_order:{order.work_order_id}:"
|
|
692
|
+
"independently_verified_not_applied_retry_available"
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
if job.status in (
|
|
696
|
+
JobStatus.QUEUED,
|
|
697
|
+
JobStatus.CLAIMED,
|
|
698
|
+
JobStatus.RUNNING,
|
|
699
|
+
JobStatus.BLOCKED,
|
|
700
|
+
JobStatus.ABANDONED,
|
|
701
|
+
):
|
|
702
|
+
return None, f"work_order:{order.work_order_id}:{job.status.value}"
|
|
703
|
+
if job.status not in {JobStatus.COMPLETED, JobStatus.NEUTRAL}:
|
|
704
|
+
if job.status not in (JobStatus.CANCELLED, JobStatus.FAILED):
|
|
705
|
+
return False, f"work order {order.work_order_id} has unknown state {job.status.value}"
|
|
706
|
+
|
|
707
|
+
return await self._project_terminal_result(order, job, step=step)
|
|
708
|
+
|
|
709
|
+
async def reconcile_terminal_results(
|
|
710
|
+
self, *, limit: int = 25,
|
|
711
|
+
) -> Dict[str, Any]:
|
|
712
|
+
"""Project already-terminal queue truth without pursuing Projects.
|
|
713
|
+
|
|
714
|
+
This path never posts, claims, retries, or authorizes work. It only
|
|
715
|
+
polls immutable WorkOrders that already exist in the Project ledger
|
|
716
|
+
and materializes a terminal queue result through the same validation,
|
|
717
|
+
verification, and idempotent storage path used by ``execute``.
|
|
718
|
+
"""
|
|
719
|
+
|
|
720
|
+
report: Dict[str, Any] = {
|
|
721
|
+
"checked": 0,
|
|
722
|
+
"terminal": 0,
|
|
723
|
+
"projected": 0,
|
|
724
|
+
"errors": 0,
|
|
725
|
+
}
|
|
726
|
+
queue = self.manager.queue
|
|
727
|
+
rows = self.project_store.work_orders_awaiting_reconciliation(
|
|
728
|
+
limit=limit,
|
|
729
|
+
)
|
|
730
|
+
terminal_states = {
|
|
731
|
+
JobStatus.COMPLETED,
|
|
732
|
+
JobStatus.NEUTRAL,
|
|
733
|
+
JobStatus.CANCELLED,
|
|
734
|
+
JobStatus.FAILED,
|
|
735
|
+
}
|
|
736
|
+
for row in rows:
|
|
737
|
+
report["checked"] += 1
|
|
738
|
+
work_order_id = str(row.get("work_order_id") or "")
|
|
739
|
+
try:
|
|
740
|
+
order = WorkOrderV1.from_payload(row["payload"])
|
|
741
|
+
if (
|
|
742
|
+
order.work_order_id != work_order_id
|
|
743
|
+
or order.work_order_digest != row["work_order_digest"]
|
|
744
|
+
or order.project_id != row["project_id"]
|
|
745
|
+
or order.step_id != row["step_id"]
|
|
746
|
+
):
|
|
747
|
+
raise WorkOrderError(
|
|
748
|
+
"persisted WorkOrder ledger identity mismatch"
|
|
749
|
+
)
|
|
750
|
+
job = await queue.get_job(order.work_order_id)
|
|
751
|
+
if job is None or job.status not in terminal_states:
|
|
752
|
+
continue
|
|
753
|
+
report["terminal"] += 1
|
|
754
|
+
order.validate_payload(job.payload)
|
|
755
|
+
attempts_before = len(
|
|
756
|
+
self.project_store.execution_attempts_for(
|
|
757
|
+
order.work_order_id,
|
|
758
|
+
)
|
|
759
|
+
)
|
|
760
|
+
await self._project_terminal_result(order, job)
|
|
761
|
+
attempts_after = len(
|
|
762
|
+
self.project_store.execution_attempts_for(
|
|
763
|
+
order.work_order_id,
|
|
764
|
+
)
|
|
765
|
+
)
|
|
766
|
+
if attempts_after > attempts_before:
|
|
767
|
+
report["projected"] += attempts_after - attempts_before
|
|
768
|
+
except Exception as exc:
|
|
769
|
+
report["errors"] += 1
|
|
770
|
+
logger.warning(
|
|
771
|
+
"terminal WorkOrder reconciliation failed for %s: %s",
|
|
772
|
+
work_order_id[:64], exc,
|
|
773
|
+
)
|
|
774
|
+
return report
|
|
775
|
+
|
|
776
|
+
async def _project_terminal_result(
|
|
777
|
+
self, order: WorkOrderV1, job: Job, *, step: Any = None,
|
|
778
|
+
) -> Tuple[Optional[bool], str]:
|
|
779
|
+
"""Validate and persist one terminal queue outcome idempotently."""
|
|
780
|
+
|
|
781
|
+
queue = self.manager.queue
|
|
782
|
+
reconciliation = str(
|
|
783
|
+
job.tags.get("effect_reconciliation_finding") or ""
|
|
784
|
+
).lower()
|
|
785
|
+
if reconciliation == "applied":
|
|
786
|
+
return None, (
|
|
787
|
+
f"work_order:{order.work_order_id}:"
|
|
788
|
+
"independently_verified_applied_no_retry"
|
|
789
|
+
)
|
|
790
|
+
if reconciliation == "not_applied":
|
|
791
|
+
return None, (
|
|
792
|
+
f"work_order:{order.work_order_id}:"
|
|
793
|
+
"independently_verified_not_applied_retry_available"
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
# A started mutation/disclosure that stopped without independent
|
|
797
|
+
# not-applied evidence is neither success nor failure. Keep the
|
|
798
|
+
# durable queue attempt/audit as reconciliation evidence, but never
|
|
799
|
+
# turn ambiguity into a false failed ExecutionResult or a replan that
|
|
800
|
+
# could duplicate the external effect.
|
|
801
|
+
if (
|
|
802
|
+
job.status is JobStatus.NEUTRAL
|
|
803
|
+
and str(job.tags.get("ambiguous_prior_effects") or "").lower()
|
|
804
|
+
== "true"
|
|
805
|
+
and str(job.tags.get("verification_pending") or "").lower()
|
|
806
|
+
== "true"
|
|
807
|
+
):
|
|
808
|
+
return None, (
|
|
809
|
+
f"work_order:{order.work_order_id}:"
|
|
810
|
+
"awaiting_independent_effect_reconciliation"
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
try:
|
|
814
|
+
result = ExecutionResultV1.from_job(order, job)
|
|
815
|
+
result = await self._verify(order, result, job)
|
|
816
|
+
if (
|
|
817
|
+
result.terminal_outcome == "succeeded"
|
|
818
|
+
and result.verification_result == "verified"
|
|
819
|
+
):
|
|
820
|
+
attested = await queue.attest_job_success(
|
|
821
|
+
order.work_order_id,
|
|
822
|
+
report={
|
|
823
|
+
"status": "verified",
|
|
824
|
+
"execution_result": result.payload(),
|
|
825
|
+
},
|
|
826
|
+
verifier_identity=result.verifier_identity,
|
|
827
|
+
verifier_type=str(
|
|
828
|
+
getattr(
|
|
829
|
+
self.receipt_verifier,
|
|
830
|
+
"verifier_type",
|
|
831
|
+
"receipt_verifier",
|
|
832
|
+
)
|
|
833
|
+
),
|
|
834
|
+
)
|
|
835
|
+
if not attested:
|
|
836
|
+
return False, (
|
|
837
|
+
"independent execution result could not attest queue job"
|
|
838
|
+
)
|
|
839
|
+
transport_status = job.status.value
|
|
840
|
+
payload = result.payload()
|
|
841
|
+
prior_attempts = self.project_store.execution_attempts_for(
|
|
842
|
+
order.work_order_id,
|
|
843
|
+
)
|
|
844
|
+
exact_replay = any(
|
|
845
|
+
attempt["run_id"] == result.run_id
|
|
846
|
+
and int(attempt["attempt_number"]) == result.attempt_number
|
|
847
|
+
and attempt["transport_status"] == transport_status
|
|
848
|
+
and attempt["result"] == payload
|
|
849
|
+
for attempt in prior_attempts
|
|
850
|
+
)
|
|
851
|
+
if exact_replay:
|
|
852
|
+
# Avoid rewriting Step timestamps/outbox state every autonomy
|
|
853
|
+
# tick while retaining strict collision checks below for a
|
|
854
|
+
# reused run/attempt with different content.
|
|
855
|
+
result_ref = result.result_ref
|
|
856
|
+
else:
|
|
857
|
+
result_ref = self.project_store.save_execution_result(
|
|
858
|
+
order,
|
|
859
|
+
result,
|
|
860
|
+
transport_status=transport_status,
|
|
861
|
+
)
|
|
862
|
+
if step is not None:
|
|
863
|
+
# Preserve the existing execute() contract for its caller's
|
|
864
|
+
# in-memory Step; the store already bound the durable row.
|
|
865
|
+
step.result_ref = result_ref
|
|
866
|
+
except (ExecutionResultError, ValueError) as exc:
|
|
867
|
+
return False, f"execution result rejected: {exc}"
|
|
868
|
+
|
|
869
|
+
if result.terminal_outcome in ("skipped", "cancelled"):
|
|
870
|
+
reason = result.error or result.summary or result.terminal_outcome
|
|
871
|
+
return True, f"SKIPPED: {reason[:1200]}"
|
|
872
|
+
if result.terminal_outcome == "failed":
|
|
873
|
+
error = result.error or result.summary or "worker failed"
|
|
874
|
+
return False, f"work order {order.work_order_id} failed: {error[:1200]}"
|
|
875
|
+
if result.verification_result != "verified":
|
|
876
|
+
return None, (
|
|
877
|
+
f"work order {order.work_order_id} completed without verified evidence "
|
|
878
|
+
f"(result_ref={result.result_ref})"
|
|
879
|
+
)
|
|
880
|
+
summary = result.summary or "receipt-backed completion"
|
|
881
|
+
return True, f"work order {order.work_order_id} completed: {summary[:1200]}"
|
|
882
|
+
|
|
883
|
+
async def _verify(self, order: WorkOrderV1, result: ExecutionResultV1, job: Job) -> ExecutionResultV1:
|
|
884
|
+
if result.terminal_outcome != "succeeded":
|
|
885
|
+
return result.with_verification("not_applicable")
|
|
886
|
+
|
|
887
|
+
if self.receipt_verifier is None:
|
|
888
|
+
return result.with_verification(
|
|
889
|
+
"unverified",
|
|
890
|
+
error=result.error or (
|
|
891
|
+
"completion has no independent evidence verifier"
|
|
892
|
+
),
|
|
893
|
+
)
|
|
894
|
+
verify = getattr(self.receipt_verifier, "verify", self.receipt_verifier)
|
|
895
|
+
decision = verify(
|
|
896
|
+
order=order,
|
|
897
|
+
result=result,
|
|
898
|
+
job=job,
|
|
899
|
+
output=dict(job.result.output or {}) if job.result else {},
|
|
900
|
+
)
|
|
901
|
+
if inspect.isawaitable(decision):
|
|
902
|
+
decision = await decision
|
|
903
|
+
checked = ReceiptVerificationV1.from_value(decision)
|
|
904
|
+
if (
|
|
905
|
+
checked.verified
|
|
906
|
+
and checked.verifier_identity == result.executor_identity
|
|
907
|
+
):
|
|
908
|
+
return result.with_verification(
|
|
909
|
+
"unverified",
|
|
910
|
+
receipt_refs=checked.receipt_refs or result.receipt_refs,
|
|
911
|
+
verifier_identity=checked.verifier_identity,
|
|
912
|
+
error="executor cannot independently verify its own result",
|
|
913
|
+
)
|
|
914
|
+
if not checked.verified:
|
|
915
|
+
return result.with_verification(
|
|
916
|
+
"unverified",
|
|
917
|
+
receipt_refs=checked.receipt_refs or result.receipt_refs,
|
|
918
|
+
verifier_identity=checked.verifier_identity,
|
|
919
|
+
error=checked.detail or result.error or "external receipt verification failed",
|
|
920
|
+
)
|
|
921
|
+
return result.with_verification(
|
|
922
|
+
"verified",
|
|
923
|
+
receipt_refs=checked.receipt_refs,
|
|
924
|
+
verifier_identity=checked.verifier_identity,
|
|
925
|
+
)
|