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,1708 @@
|
|
|
1
|
+
"""Durable, generic execution boundary for owner-governed Colony actions.
|
|
2
|
+
|
|
3
|
+
This module deliberately knows nothing about conversational context. One
|
|
4
|
+
dedicated API principal supplies the owner/participant binding, while the
|
|
5
|
+
request supplies only an immutable, already-approved action description.
|
|
6
|
+
|
|
7
|
+
The ledger uses a conservative one-mutation-at-most protocol:
|
|
8
|
+
|
|
9
|
+
``prepared -> executing -> completed``
|
|
10
|
+
|
|
11
|
+
Any exception after ``executing`` is durable ``ambiguous`` and can never be
|
|
12
|
+
retried as a mutation. An ``executing`` row found at process startup is also
|
|
13
|
+
recovered as ``ambiguous``. This intentionally prefers an honest uncertain
|
|
14
|
+
result over accidentally applying an effect twice.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import hashlib
|
|
21
|
+
import inspect
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import math
|
|
25
|
+
import os
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
import re
|
|
28
|
+
import sqlite3
|
|
29
|
+
import stat
|
|
30
|
+
import threading
|
|
31
|
+
import time
|
|
32
|
+
from typing import Any, Mapping
|
|
33
|
+
import uuid
|
|
34
|
+
|
|
35
|
+
from apsimo.api.authority import RequestAuthority
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
GOVERNED_ACTION_PRINCIPAL = "host-action-worker"
|
|
41
|
+
GOVERNED_ACTION_SCOPES = frozenset({"actions:execute", "actions:verify"})
|
|
42
|
+
ACTION_TOOL_NAMES = frozenset(
|
|
43
|
+
{
|
|
44
|
+
"colony_autonomy_disable",
|
|
45
|
+
"colony_autonomy_enable",
|
|
46
|
+
"colony_create_commitment",
|
|
47
|
+
"colony_initiative_feedback",
|
|
48
|
+
"colony_record_insight",
|
|
49
|
+
"colony_research",
|
|
50
|
+
"colony_resolve_commitment",
|
|
51
|
+
"colony_task_complete",
|
|
52
|
+
"colony_task_dismiss",
|
|
53
|
+
"colony_task_snooze",
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
GOVERNED_ACTION_REQUEST_MAX_BYTES = 32 * 1024
|
|
58
|
+
_REQUEST_MAX = GOVERNED_ACTION_REQUEST_MAX_BYTES
|
|
59
|
+
_REQUEST_FIELDS = frozenset(
|
|
60
|
+
{
|
|
61
|
+
"schema",
|
|
62
|
+
"version",
|
|
63
|
+
"action_id",
|
|
64
|
+
"action_digest",
|
|
65
|
+
"intent_id",
|
|
66
|
+
"intent_digest",
|
|
67
|
+
"tool_name",
|
|
68
|
+
"args",
|
|
69
|
+
"args_sha256",
|
|
70
|
+
"approval",
|
|
71
|
+
"execution_digest",
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
_APPROVAL_FIELDS = frozenset(
|
|
75
|
+
{
|
|
76
|
+
"schema",
|
|
77
|
+
"version",
|
|
78
|
+
"approval_id",
|
|
79
|
+
"decision_id",
|
|
80
|
+
"revision",
|
|
81
|
+
"authorization_receipt_sha256",
|
|
82
|
+
"decided_at",
|
|
83
|
+
"expires_at",
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
_EFFECT_FIELDS = frozenset(
|
|
87
|
+
{"schema", "version", "effect_id", "outcome", "verification"}
|
|
88
|
+
)
|
|
89
|
+
_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
90
|
+
_INTENT_ID_RE = re.compile(r"^hti_[0-9a-f]{32}$")
|
|
91
|
+
_APPROVAL_ID_RE = re.compile(r"^APR-[A-Z0-9]{12}$")
|
|
92
|
+
_SAFE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,255}$")
|
|
93
|
+
GOVERNED_IDENTIFIER_PATTERN = (
|
|
94
|
+
r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}(?![\s\S])"
|
|
95
|
+
)
|
|
96
|
+
_REQUEST_ID_RE = re.compile(GOVERNED_IDENTIFIER_PATTERN)
|
|
97
|
+
_STATES = frozenset(
|
|
98
|
+
{"prepared", "executing", "completed", "failed", "ambiguous"}
|
|
99
|
+
)
|
|
100
|
+
GOVERNED_RESEARCH_TOPIC_MAX_CHARS = 1400
|
|
101
|
+
GOVERNED_COMMITMENT_PRIORITY_DEFAULT = 60
|
|
102
|
+
GOVERNED_COMMITMENT_DESCRIPTION_MAX_CHARS = 8000
|
|
103
|
+
GOVERNED_COMMITMENT_DUE_AT_MAX_CHARS = 256
|
|
104
|
+
GOVERNED_INSIGHT_CONTENT_MAX_CHARS = 16000
|
|
105
|
+
GOVERNED_FREEFORM_REASON_MAX_CHARS = 8000
|
|
106
|
+
GOVERNED_IDENTIFIER_MAX_CHARS = 256
|
|
107
|
+
GOVERNED_DETAILS_MAX_NODES = 512
|
|
108
|
+
GOVERNED_DETAILS_MAX_DEPTH = 8
|
|
109
|
+
GOVERNED_DETAILS_STRING_MAX_CHARS = 4096
|
|
110
|
+
GOVERNED_DETAILS_KEY_MAX_CHARS = 128
|
|
111
|
+
GOVERNED_DETAILS_INTEGER_MAX = (1 << 63) - 1
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class GovernedActionValidationError(ValueError):
|
|
115
|
+
"""The external execution document is not the exact bounded contract."""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class GovernedActionConflict(ValueError):
|
|
119
|
+
"""An action identifier is already bound to a different request."""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class GovernedActionNotFound(LookupError):
|
|
123
|
+
"""No durable execution record exists for the requested action."""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class PrivateGovernedActionLedgerError(OSError):
|
|
127
|
+
"""The governed-action ledger path is not owner-private and stable."""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def canonical_json(value: Any) -> str:
|
|
131
|
+
try:
|
|
132
|
+
return json.dumps(
|
|
133
|
+
value,
|
|
134
|
+
sort_keys=True,
|
|
135
|
+
separators=(",", ":"),
|
|
136
|
+
ensure_ascii=True,
|
|
137
|
+
allow_nan=False,
|
|
138
|
+
)
|
|
139
|
+
except (TypeError, ValueError, OverflowError, RecursionError) as exc:
|
|
140
|
+
raise GovernedActionValidationError("value is not canonical JSON") from exc
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def sha256_json(value: Any) -> str:
|
|
144
|
+
return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _wire_sha256_json(value: Any) -> str:
|
|
148
|
+
"""Hash the host Action Plane wire form without changing local storage.
|
|
149
|
+
|
|
150
|
+
The host's args/intent digests intentionally use the historical ASCII-escaped
|
|
151
|
+
canonical form above. Only its outer execution envelope and returned
|
|
152
|
+
effect digest use canonical UTF-8 JSON, so those two fields call this
|
|
153
|
+
narrowly scoped helper.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
encoded = json.dumps(
|
|
158
|
+
value,
|
|
159
|
+
sort_keys=True,
|
|
160
|
+
separators=(",", ":"),
|
|
161
|
+
ensure_ascii=False,
|
|
162
|
+
allow_nan=False,
|
|
163
|
+
).encode("utf-8")
|
|
164
|
+
except (TypeError, ValueError, OverflowError, RecursionError) as exc:
|
|
165
|
+
raise GovernedActionValidationError("value is not canonical JSON") from exc
|
|
166
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class _FrozenJSONDict(dict):
|
|
170
|
+
"""JSON-serializable mapping that refuses in-place authority changes."""
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _immutable(*_args, **_kwargs):
|
|
174
|
+
raise TypeError("governed execution context is immutable")
|
|
175
|
+
|
|
176
|
+
__setitem__ = _immutable
|
|
177
|
+
__delitem__ = _immutable
|
|
178
|
+
clear = _immutable
|
|
179
|
+
pop = _immutable
|
|
180
|
+
popitem = _immutable
|
|
181
|
+
setdefault = _immutable
|
|
182
|
+
update = _immutable
|
|
183
|
+
__ior__ = _immutable
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _freeze_json(value: Any) -> Any:
|
|
187
|
+
if isinstance(value, Mapping):
|
|
188
|
+
return _FrozenJSONDict({
|
|
189
|
+
key: _freeze_json(item) for key, item in value.items()
|
|
190
|
+
})
|
|
191
|
+
if isinstance(value, list):
|
|
192
|
+
return tuple(_freeze_json(item) for item in value)
|
|
193
|
+
return value
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _strict_json(raw: bytes, *, maximum: int = _REQUEST_MAX) -> Any:
|
|
197
|
+
if not isinstance(raw, bytes) or not raw or len(raw) > maximum:
|
|
198
|
+
raise GovernedActionValidationError("execution document is outside its size bound")
|
|
199
|
+
|
|
200
|
+
def pairs(values):
|
|
201
|
+
result = {}
|
|
202
|
+
for key, value in values:
|
|
203
|
+
if key in result:
|
|
204
|
+
raise GovernedActionValidationError("JSON object keys must be unique")
|
|
205
|
+
result[key] = value
|
|
206
|
+
return result
|
|
207
|
+
|
|
208
|
+
def constant(_value):
|
|
209
|
+
raise GovernedActionValidationError("JSON numbers must be finite")
|
|
210
|
+
|
|
211
|
+
def integer(value):
|
|
212
|
+
if len(value.lstrip("-")) > 19:
|
|
213
|
+
raise GovernedActionValidationError("JSON integer is outside its bound")
|
|
214
|
+
parsed = int(value)
|
|
215
|
+
if abs(parsed) > (1 << 63) - 1:
|
|
216
|
+
raise GovernedActionValidationError("JSON integer is outside its bound")
|
|
217
|
+
return parsed
|
|
218
|
+
|
|
219
|
+
def floating(value):
|
|
220
|
+
parsed = float(value)
|
|
221
|
+
if not math.isfinite(parsed):
|
|
222
|
+
raise GovernedActionValidationError("JSON numbers must be finite")
|
|
223
|
+
return parsed
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
value = json.loads(
|
|
227
|
+
raw.decode("utf-8", errors="strict"),
|
|
228
|
+
object_pairs_hook=pairs,
|
|
229
|
+
parse_constant=constant,
|
|
230
|
+
parse_int=integer,
|
|
231
|
+
parse_float=floating,
|
|
232
|
+
)
|
|
233
|
+
except GovernedActionValidationError:
|
|
234
|
+
raise
|
|
235
|
+
except (UnicodeError, ValueError, TypeError, OverflowError, RecursionError) as exc:
|
|
236
|
+
raise GovernedActionValidationError("execution document is malformed") from exc
|
|
237
|
+
|
|
238
|
+
stack = [(value, 0)]
|
|
239
|
+
count = 0
|
|
240
|
+
while stack:
|
|
241
|
+
item, depth = stack.pop()
|
|
242
|
+
count += 1
|
|
243
|
+
if count > 1024 or depth > 16:
|
|
244
|
+
raise GovernedActionValidationError("execution document is too complex")
|
|
245
|
+
if isinstance(item, str):
|
|
246
|
+
if len(item) > 16_000 or any(
|
|
247
|
+
ord(character) == 0 or 0xD800 <= ord(character) <= 0xDFFF
|
|
248
|
+
for character in item
|
|
249
|
+
):
|
|
250
|
+
raise GovernedActionValidationError("execution text is unsafe")
|
|
251
|
+
elif isinstance(item, Mapping):
|
|
252
|
+
stack.extend((key, depth + 1) for key in item)
|
|
253
|
+
stack.extend((child, depth + 1) for child in item.values())
|
|
254
|
+
elif isinstance(item, list):
|
|
255
|
+
stack.extend((child, depth + 1) for child in item)
|
|
256
|
+
elif item is not None and not isinstance(item, (bool, int, float)):
|
|
257
|
+
raise GovernedActionValidationError("execution document contains a non-JSON value")
|
|
258
|
+
return value
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _exact_mapping(value: Any, fields, *, name: str) -> dict[str, Any]:
|
|
262
|
+
if not isinstance(value, Mapping) or set(value) != set(fields):
|
|
263
|
+
raise GovernedActionValidationError(f"{name} fields are invalid")
|
|
264
|
+
if any(not isinstance(key, str) for key in value):
|
|
265
|
+
raise GovernedActionValidationError(f"{name} keys must be strings")
|
|
266
|
+
return dict(value)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _text(
|
|
270
|
+
value: Any,
|
|
271
|
+
name: str,
|
|
272
|
+
maximum: int,
|
|
273
|
+
*,
|
|
274
|
+
allow_empty: bool = False,
|
|
275
|
+
identifier: bool = False,
|
|
276
|
+
) -> str:
|
|
277
|
+
if not isinstance(value, str) or len(value) > maximum:
|
|
278
|
+
raise GovernedActionValidationError(f"{name} must be a bounded string")
|
|
279
|
+
if any(ord(character) == 0 or 0xD800 <= ord(character) <= 0xDFFF for character in value):
|
|
280
|
+
raise GovernedActionValidationError(f"{name} contains invalid characters")
|
|
281
|
+
if not allow_empty and not value.strip():
|
|
282
|
+
raise GovernedActionValidationError(f"{name} cannot be empty")
|
|
283
|
+
if identifier and not _REQUEST_ID_RE.fullmatch(value):
|
|
284
|
+
raise GovernedActionValidationError(f"{name} is not a canonical identifier")
|
|
285
|
+
return value
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _integer(value: Any, name: str, minimum: int, maximum: int) -> int:
|
|
289
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
290
|
+
raise GovernedActionValidationError(f"{name} must be an integer")
|
|
291
|
+
if not minimum <= value <= maximum:
|
|
292
|
+
raise GovernedActionValidationError(f"{name} is outside its range")
|
|
293
|
+
return value
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _number(value: Any, name: str, minimum: float, maximum: float) -> int | float:
|
|
297
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
298
|
+
raise GovernedActionValidationError(f"{name} must be a number")
|
|
299
|
+
try:
|
|
300
|
+
finite = math.isfinite(float(value))
|
|
301
|
+
except (ValueError, OverflowError):
|
|
302
|
+
finite = False
|
|
303
|
+
if not finite or not minimum <= value <= maximum:
|
|
304
|
+
raise GovernedActionValidationError(f"{name} is outside its range")
|
|
305
|
+
return value
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _enum(value: Any, name: str, allowed: frozenset[str]) -> str:
|
|
309
|
+
if not isinstance(value, str) or value not in allowed:
|
|
310
|
+
raise GovernedActionValidationError(f"{name} is not an allowed value")
|
|
311
|
+
return value
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _bounded_json(value: Any, name: str, *, depth=0, counter=None) -> Any:
|
|
315
|
+
if counter is None:
|
|
316
|
+
counter = [0]
|
|
317
|
+
counter[0] += 1
|
|
318
|
+
if (
|
|
319
|
+
counter[0] > GOVERNED_DETAILS_MAX_NODES
|
|
320
|
+
or depth > GOVERNED_DETAILS_MAX_DEPTH
|
|
321
|
+
):
|
|
322
|
+
raise GovernedActionValidationError(f"{name} is too complex")
|
|
323
|
+
if value is None or isinstance(value, bool):
|
|
324
|
+
return value
|
|
325
|
+
if isinstance(value, int):
|
|
326
|
+
if abs(value) > GOVERNED_DETAILS_INTEGER_MAX:
|
|
327
|
+
raise GovernedActionValidationError(f"{name} integer is too large")
|
|
328
|
+
return value
|
|
329
|
+
if isinstance(value, float):
|
|
330
|
+
if not math.isfinite(value):
|
|
331
|
+
raise GovernedActionValidationError(f"{name} number must be finite")
|
|
332
|
+
return value
|
|
333
|
+
if isinstance(value, str):
|
|
334
|
+
return _text(
|
|
335
|
+
value, name, GOVERNED_DETAILS_STRING_MAX_CHARS, allow_empty=True,
|
|
336
|
+
)
|
|
337
|
+
if isinstance(value, list):
|
|
338
|
+
return [
|
|
339
|
+
_bounded_json(item, name, depth=depth + 1, counter=counter)
|
|
340
|
+
for item in value
|
|
341
|
+
]
|
|
342
|
+
if isinstance(value, Mapping):
|
|
343
|
+
result = {}
|
|
344
|
+
for key, item in value.items():
|
|
345
|
+
key = _text(
|
|
346
|
+
key, f"{name} key", GOVERNED_DETAILS_KEY_MAX_CHARS,
|
|
347
|
+
identifier=True,
|
|
348
|
+
)
|
|
349
|
+
result[key] = _bounded_json(
|
|
350
|
+
item, name, depth=depth + 1, counter=counter
|
|
351
|
+
)
|
|
352
|
+
return result
|
|
353
|
+
raise GovernedActionValidationError(f"{name} contains a non-JSON value")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _bounded_verification(value: Any, *, depth=0, counter=None) -> Any:
|
|
357
|
+
"""Match the host worker's fixed verification projection bounds exactly."""
|
|
358
|
+
|
|
359
|
+
if counter is None:
|
|
360
|
+
counter = [0]
|
|
361
|
+
counter[0] += 1
|
|
362
|
+
if counter[0] > 256 or depth > 6:
|
|
363
|
+
raise RuntimeError("governed action verification is too complex")
|
|
364
|
+
if value is None or isinstance(value, bool):
|
|
365
|
+
return value
|
|
366
|
+
if isinstance(value, int):
|
|
367
|
+
if abs(value) > (1 << 63) - 1:
|
|
368
|
+
raise RuntimeError("governed action verification integer is unsafe")
|
|
369
|
+
return value
|
|
370
|
+
if isinstance(value, float):
|
|
371
|
+
if not math.isfinite(value):
|
|
372
|
+
raise RuntimeError("governed action verification number is unsafe")
|
|
373
|
+
return value
|
|
374
|
+
if isinstance(value, str):
|
|
375
|
+
if len(value) > 2048 or any(
|
|
376
|
+
ord(character) < 0x20 or ord(character) == 0x7F
|
|
377
|
+
for character in value
|
|
378
|
+
):
|
|
379
|
+
raise RuntimeError("governed action verification text is unsafe")
|
|
380
|
+
return value
|
|
381
|
+
if isinstance(value, list):
|
|
382
|
+
return [
|
|
383
|
+
_bounded_verification(item, depth=depth + 1, counter=counter)
|
|
384
|
+
for item in value
|
|
385
|
+
]
|
|
386
|
+
if isinstance(value, Mapping):
|
|
387
|
+
result = {}
|
|
388
|
+
for key, item in value.items():
|
|
389
|
+
if not isinstance(key, str) or not _SAFE_ID_RE.fullmatch(key):
|
|
390
|
+
raise RuntimeError("governed action verification key is unsafe")
|
|
391
|
+
result[key] = _bounded_verification(
|
|
392
|
+
item, depth=depth + 1, counter=counter
|
|
393
|
+
)
|
|
394
|
+
return result
|
|
395
|
+
raise RuntimeError("governed action verification is not JSON")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _validate_args(tool_name: str, raw: Any) -> dict[str, Any]:
|
|
399
|
+
if tool_name in {"colony_autonomy_disable", "colony_autonomy_enable"}:
|
|
400
|
+
return _exact_mapping(raw, set(), name="args")
|
|
401
|
+
if tool_name == "colony_create_commitment":
|
|
402
|
+
args = _exact_mapping(
|
|
403
|
+
raw,
|
|
404
|
+
set(raw) if isinstance(raw, Mapping) else set(),
|
|
405
|
+
name="args",
|
|
406
|
+
)
|
|
407
|
+
if set(args) - {"description", "due_at", "priority"} or "description" not in args:
|
|
408
|
+
raise GovernedActionValidationError("args fields are invalid")
|
|
409
|
+
_text(
|
|
410
|
+
args["description"], "description",
|
|
411
|
+
GOVERNED_COMMITMENT_DESCRIPTION_MAX_CHARS,
|
|
412
|
+
)
|
|
413
|
+
if "due_at" in args:
|
|
414
|
+
_text(
|
|
415
|
+
args["due_at"], "due_at", GOVERNED_COMMITMENT_DUE_AT_MAX_CHARS,
|
|
416
|
+
allow_empty=True,
|
|
417
|
+
)
|
|
418
|
+
if "priority" in args:
|
|
419
|
+
_integer(args["priority"], "priority", 0, 100)
|
|
420
|
+
return args
|
|
421
|
+
if tool_name == "colony_initiative_feedback":
|
|
422
|
+
if not isinstance(raw, Mapping):
|
|
423
|
+
raise GovernedActionValidationError("args must be an object")
|
|
424
|
+
args = dict(raw)
|
|
425
|
+
if (
|
|
426
|
+
set(args) - {"initiative_id", "action", "details"}
|
|
427
|
+
or not {"initiative_id", "action"} <= set(args)
|
|
428
|
+
):
|
|
429
|
+
raise GovernedActionValidationError("args fields are invalid")
|
|
430
|
+
_text(
|
|
431
|
+
args["initiative_id"], "initiative_id", GOVERNED_IDENTIFIER_MAX_CHARS,
|
|
432
|
+
identifier=True,
|
|
433
|
+
)
|
|
434
|
+
_enum(
|
|
435
|
+
args["action"],
|
|
436
|
+
"action",
|
|
437
|
+
frozenset({"acknowledged", "actioned", "dismissed", "snoozed"}),
|
|
438
|
+
)
|
|
439
|
+
if "details" in args:
|
|
440
|
+
if not isinstance(args["details"], Mapping):
|
|
441
|
+
raise GovernedActionValidationError("details must be an object")
|
|
442
|
+
args["details"] = _bounded_json(args["details"], "details")
|
|
443
|
+
return args
|
|
444
|
+
if tool_name == "colony_record_insight":
|
|
445
|
+
if not isinstance(raw, Mapping):
|
|
446
|
+
raise GovernedActionValidationError("args must be an object")
|
|
447
|
+
args = dict(raw)
|
|
448
|
+
if (
|
|
449
|
+
set(args) - {"confidence", "content", "insight_type"}
|
|
450
|
+
or not {"content", "insight_type"} <= set(args)
|
|
451
|
+
):
|
|
452
|
+
raise GovernedActionValidationError("args fields are invalid")
|
|
453
|
+
_text(
|
|
454
|
+
args["content"], "content", GOVERNED_INSIGHT_CONTENT_MAX_CHARS,
|
|
455
|
+
)
|
|
456
|
+
_enum(
|
|
457
|
+
args["insight_type"],
|
|
458
|
+
"insight_type",
|
|
459
|
+
frozenset(
|
|
460
|
+
{
|
|
461
|
+
"preference",
|
|
462
|
+
"connection",
|
|
463
|
+
"fact",
|
|
464
|
+
"goal_hint",
|
|
465
|
+
"relationship_update",
|
|
466
|
+
}
|
|
467
|
+
),
|
|
468
|
+
)
|
|
469
|
+
if "confidence" in args:
|
|
470
|
+
_number(args["confidence"], "confidence", 0.0, 1.0)
|
|
471
|
+
return args
|
|
472
|
+
if tool_name == "colony_research":
|
|
473
|
+
if not isinstance(raw, Mapping):
|
|
474
|
+
raise GovernedActionValidationError("args must be an object")
|
|
475
|
+
args = dict(raw)
|
|
476
|
+
if set(args) - {"depth", "topic"} or "topic" not in args:
|
|
477
|
+
raise GovernedActionValidationError("args fields are invalid")
|
|
478
|
+
_text(
|
|
479
|
+
args["topic"], "topic", GOVERNED_RESEARCH_TOPIC_MAX_CHARS,
|
|
480
|
+
)
|
|
481
|
+
if "depth" in args:
|
|
482
|
+
_enum(args["depth"], "depth", frozenset({"quick", "standard", "deep"}))
|
|
483
|
+
return args
|
|
484
|
+
if tool_name == "colony_resolve_commitment":
|
|
485
|
+
if not isinstance(raw, Mapping):
|
|
486
|
+
raise GovernedActionValidationError("args must be an object")
|
|
487
|
+
args = dict(raw)
|
|
488
|
+
if set(args) - {"commitment_id", "outcome", "reason"} or "commitment_id" not in args:
|
|
489
|
+
raise GovernedActionValidationError("args fields are invalid")
|
|
490
|
+
_text(
|
|
491
|
+
args["commitment_id"], "commitment_id", GOVERNED_IDENTIFIER_MAX_CHARS,
|
|
492
|
+
identifier=True,
|
|
493
|
+
)
|
|
494
|
+
if "outcome" in args:
|
|
495
|
+
_enum(
|
|
496
|
+
args["outcome"],
|
|
497
|
+
"outcome",
|
|
498
|
+
frozenset({"done", "invalid", "duplicate", "wont_do", "obsolete"}),
|
|
499
|
+
)
|
|
500
|
+
if "reason" in args:
|
|
501
|
+
_text(
|
|
502
|
+
args["reason"], "reason", GOVERNED_FREEFORM_REASON_MAX_CHARS,
|
|
503
|
+
allow_empty=True,
|
|
504
|
+
)
|
|
505
|
+
return args
|
|
506
|
+
if tool_name == "colony_task_complete":
|
|
507
|
+
args = _exact_mapping(raw, {"task_id"}, name="args")
|
|
508
|
+
_text(
|
|
509
|
+
args["task_id"], "task_id", GOVERNED_IDENTIFIER_MAX_CHARS,
|
|
510
|
+
identifier=True,
|
|
511
|
+
)
|
|
512
|
+
return args
|
|
513
|
+
if tool_name == "colony_task_dismiss":
|
|
514
|
+
if not isinstance(raw, Mapping):
|
|
515
|
+
raise GovernedActionValidationError("args must be an object")
|
|
516
|
+
args = dict(raw)
|
|
517
|
+
if set(args) - {"task_id", "reason"} or "task_id" not in args:
|
|
518
|
+
raise GovernedActionValidationError("args fields are invalid")
|
|
519
|
+
_text(
|
|
520
|
+
args["task_id"], "task_id", GOVERNED_IDENTIFIER_MAX_CHARS,
|
|
521
|
+
identifier=True,
|
|
522
|
+
)
|
|
523
|
+
if "reason" in args:
|
|
524
|
+
_enum(
|
|
525
|
+
args["reason"],
|
|
526
|
+
"reason",
|
|
527
|
+
frozenset({"stale", "completed", "abandoned", "not_applicable"}),
|
|
528
|
+
)
|
|
529
|
+
return args
|
|
530
|
+
if tool_name == "colony_task_snooze":
|
|
531
|
+
if not isinstance(raw, Mapping):
|
|
532
|
+
raise GovernedActionValidationError("args must be an object")
|
|
533
|
+
args = dict(raw)
|
|
534
|
+
if set(args) - {"task_id", "hours", "reason"} or "task_id" not in args:
|
|
535
|
+
raise GovernedActionValidationError("args fields are invalid")
|
|
536
|
+
_text(
|
|
537
|
+
args["task_id"], "task_id", GOVERNED_IDENTIFIER_MAX_CHARS,
|
|
538
|
+
identifier=True,
|
|
539
|
+
)
|
|
540
|
+
if "hours" in args:
|
|
541
|
+
_integer(args["hours"], "hours", 1, 168)
|
|
542
|
+
if "reason" in args:
|
|
543
|
+
_text(
|
|
544
|
+
args["reason"], "reason", GOVERNED_FREEFORM_REASON_MAX_CHARS,
|
|
545
|
+
allow_empty=True,
|
|
546
|
+
)
|
|
547
|
+
return args
|
|
548
|
+
raise GovernedActionValidationError("tool is not a governed Colony action")
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _valid_action_id(value: Any) -> str:
|
|
552
|
+
if not isinstance(value, str):
|
|
553
|
+
raise GovernedActionValidationError("action_id is invalid")
|
|
554
|
+
try:
|
|
555
|
+
parsed = uuid.UUID(value)
|
|
556
|
+
except (ValueError, AttributeError) as exc:
|
|
557
|
+
raise GovernedActionValidationError("action_id is invalid") from exc
|
|
558
|
+
if parsed.version != 4 or str(parsed) != value:
|
|
559
|
+
raise GovernedActionValidationError("action_id must be a canonical UUIDv4")
|
|
560
|
+
return value
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _digest(value: Any, name: str) -> str:
|
|
564
|
+
if not isinstance(value, str) or not _SHA256_RE.fullmatch(value):
|
|
565
|
+
raise GovernedActionValidationError(f"{name} is invalid")
|
|
566
|
+
return value
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def parse_execution_request(
|
|
570
|
+
raw: bytes,
|
|
571
|
+
*,
|
|
572
|
+
path_action_id: str,
|
|
573
|
+
) -> dict[str, Any]:
|
|
574
|
+
document = _exact_mapping(_strict_json(raw), _REQUEST_FIELDS, name="execution")
|
|
575
|
+
if (
|
|
576
|
+
document["schema"] != "ColonyGovernedActionExecutionV1"
|
|
577
|
+
or isinstance(document["version"], bool)
|
|
578
|
+
or document["version"] != 1
|
|
579
|
+
):
|
|
580
|
+
raise GovernedActionValidationError("execution schema is invalid")
|
|
581
|
+
action_id = _valid_action_id(document["action_id"])
|
|
582
|
+
if action_id != _valid_action_id(path_action_id):
|
|
583
|
+
raise GovernedActionValidationError("URL action_id does not match the execution")
|
|
584
|
+
_digest(document["action_digest"], "action_digest")
|
|
585
|
+
_digest(document["intent_digest"], "intent_digest")
|
|
586
|
+
_digest(document["args_sha256"], "args_sha256")
|
|
587
|
+
_digest(document["execution_digest"], "execution_digest")
|
|
588
|
+
if not isinstance(document["intent_id"], str) or not _INTENT_ID_RE.fullmatch(
|
|
589
|
+
document["intent_id"]
|
|
590
|
+
):
|
|
591
|
+
raise GovernedActionValidationError("intent_id is invalid")
|
|
592
|
+
tool_name = document["tool_name"]
|
|
593
|
+
if not isinstance(tool_name, str) or tool_name not in ACTION_TOOL_NAMES:
|
|
594
|
+
raise GovernedActionValidationError("tool is not a governed Colony action")
|
|
595
|
+
args = _validate_args(tool_name, document["args"])
|
|
596
|
+
if document["args_sha256"] != sha256_json(args):
|
|
597
|
+
raise GovernedActionValidationError("args digest does not match")
|
|
598
|
+
|
|
599
|
+
approval = _exact_mapping(document["approval"], _APPROVAL_FIELDS, name="approval")
|
|
600
|
+
if (
|
|
601
|
+
approval["schema"] != "ColonyOwnerApprovalExecutionBindingV1"
|
|
602
|
+
or isinstance(approval["version"], bool)
|
|
603
|
+
or approval["version"] != 1
|
|
604
|
+
or not isinstance(approval["approval_id"], str)
|
|
605
|
+
or not _APPROVAL_ID_RE.fullmatch(approval["approval_id"])
|
|
606
|
+
or isinstance(approval["revision"], bool)
|
|
607
|
+
or approval["revision"] != 1
|
|
608
|
+
):
|
|
609
|
+
raise GovernedActionValidationError("approval binding is invalid")
|
|
610
|
+
_text(approval["decision_id"], "decision_id", 128, identifier=True)
|
|
611
|
+
_digest(
|
|
612
|
+
approval["authorization_receipt_sha256"],
|
|
613
|
+
"authorization_receipt_sha256",
|
|
614
|
+
)
|
|
615
|
+
for field in ("decided_at", "expires_at"):
|
|
616
|
+
value = approval[field]
|
|
617
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
618
|
+
raise GovernedActionValidationError("approval time is invalid")
|
|
619
|
+
try:
|
|
620
|
+
finite = math.isfinite(float(value))
|
|
621
|
+
except (ValueError, OverflowError):
|
|
622
|
+
finite = False
|
|
623
|
+
if not finite or float(value) <= 0:
|
|
624
|
+
raise GovernedActionValidationError("approval time is invalid")
|
|
625
|
+
decided_at = float(approval["decided_at"])
|
|
626
|
+
expires_at = float(approval["expires_at"])
|
|
627
|
+
if expires_at <= decided_at or expires_at - decided_at > 86_400:
|
|
628
|
+
raise GovernedActionValidationError("approval lifetime is invalid")
|
|
629
|
+
|
|
630
|
+
normalized = dict(document)
|
|
631
|
+
normalized["args"] = args
|
|
632
|
+
normalized["approval"] = approval
|
|
633
|
+
unsigned = {key: value for key, value in normalized.items() if key != "execution_digest"}
|
|
634
|
+
if normalized["execution_digest"] != _wire_sha256_json(unsigned):
|
|
635
|
+
raise GovernedActionValidationError("execution digest does not match")
|
|
636
|
+
# One final canonicalization catches exotic mapping/value behavior and is
|
|
637
|
+
# also the exact document persisted in the ledger.
|
|
638
|
+
canonical_json(normalized)
|
|
639
|
+
return normalized
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _clock_value(clock) -> float:
|
|
643
|
+
try:
|
|
644
|
+
now = float(clock())
|
|
645
|
+
except (TypeError, ValueError, OverflowError) as exc:
|
|
646
|
+
raise RuntimeError("governed action clock is unavailable") from exc
|
|
647
|
+
if not math.isfinite(now) or now <= 0:
|
|
648
|
+
raise RuntimeError("governed action clock is unavailable")
|
|
649
|
+
return now
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _assert_live_approval(request: Mapping[str, Any], now: float) -> None:
|
|
653
|
+
approval = request["approval"]
|
|
654
|
+
if float(approval["decided_at"]) > now + 30 or float(approval["expires_at"]) <= now:
|
|
655
|
+
raise GovernedActionValidationError("approval binding is not live")
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _owner_from_authority(
|
|
659
|
+
authority: RequestAuthority,
|
|
660
|
+
*,
|
|
661
|
+
required_scope: str,
|
|
662
|
+
) -> str:
|
|
663
|
+
if (
|
|
664
|
+
not isinstance(authority, RequestAuthority)
|
|
665
|
+
or not authority.authenticated
|
|
666
|
+
or authority.legacy
|
|
667
|
+
or authority.anonymous
|
|
668
|
+
or authority.principal_id != GOVERNED_ACTION_PRINCIPAL
|
|
669
|
+
or not authority.credential_id
|
|
670
|
+
or authority.allow_unscoped_api is not False
|
|
671
|
+
or authority.scopes != GOVERNED_ACTION_SCOPES
|
|
672
|
+
or required_scope not in authority.scopes
|
|
673
|
+
or authority.audiences != frozenset({"owner"})
|
|
674
|
+
or not authority.viewer_person_id
|
|
675
|
+
or authority.viewer_person_id not in authority.person_ids
|
|
676
|
+
):
|
|
677
|
+
raise PermissionError("exact owner-bound governed-action principal required")
|
|
678
|
+
return authority.viewer_person_id
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _status_text(value: Any) -> str:
|
|
682
|
+
raw = getattr(value, "value", value)
|
|
683
|
+
text = str(raw or "unknown")
|
|
684
|
+
return text if _SAFE_ID_RE.fullmatch(text) else "unknown"
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _effect_id(kind: str, value: Any) -> str:
|
|
688
|
+
text = str(value or "")
|
|
689
|
+
if _SAFE_ID_RE.fullmatch(text):
|
|
690
|
+
return text
|
|
691
|
+
digest = hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()[:32]
|
|
692
|
+
return f"{kind}-{digest}"
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _validated_effect(value: Any) -> dict[str, Any]:
|
|
696
|
+
effect = _exact_mapping(value, _EFFECT_FIELDS, name="effect")
|
|
697
|
+
if (
|
|
698
|
+
effect["schema"] != "ColonyGovernedActionEffectV1"
|
|
699
|
+
or isinstance(effect["version"], bool)
|
|
700
|
+
or effect["version"] != 1
|
|
701
|
+
or not isinstance(effect["effect_id"], str)
|
|
702
|
+
or not _SAFE_ID_RE.fullmatch(effect["effect_id"])
|
|
703
|
+
or not isinstance(effect["outcome"], str)
|
|
704
|
+
or not _SAFE_ID_RE.fullmatch(effect["outcome"])
|
|
705
|
+
):
|
|
706
|
+
raise RuntimeError("governed action effect projection is invalid")
|
|
707
|
+
verification = _bounded_verification(effect["verification"])
|
|
708
|
+
normalized = dict(effect)
|
|
709
|
+
normalized["verification"] = verification
|
|
710
|
+
if len(canonical_json(normalized).encode("utf-8")) > 8 * 1024:
|
|
711
|
+
raise RuntimeError("governed action effect projection is too large")
|
|
712
|
+
return normalized
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _state_result(
|
|
716
|
+
request: Mapping[str, Any],
|
|
717
|
+
*,
|
|
718
|
+
state: str,
|
|
719
|
+
observed_at: float,
|
|
720
|
+
effect: Mapping[str, Any] | None = None,
|
|
721
|
+
) -> dict[str, Any]:
|
|
722
|
+
if state not in _STATES:
|
|
723
|
+
raise RuntimeError("invalid governed action ledger state")
|
|
724
|
+
if state == "completed":
|
|
725
|
+
if effect is None:
|
|
726
|
+
raise RuntimeError("completed action is missing its effect")
|
|
727
|
+
normalized_effect = _validated_effect(effect)
|
|
728
|
+
effect_state = "performed"
|
|
729
|
+
else:
|
|
730
|
+
normalized_effect = {
|
|
731
|
+
"schema": "ColonyGovernedActionEffectV1",
|
|
732
|
+
"version": 1,
|
|
733
|
+
"effect_id": request["action_id"],
|
|
734
|
+
"outcome": state,
|
|
735
|
+
"verification": {"state": state},
|
|
736
|
+
}
|
|
737
|
+
effect_state = {
|
|
738
|
+
"prepared": "not_started",
|
|
739
|
+
"executing": "uncertain",
|
|
740
|
+
"failed": "not_performed",
|
|
741
|
+
"ambiguous": "uncertain",
|
|
742
|
+
}[state]
|
|
743
|
+
result = {
|
|
744
|
+
"schema": "ColonyGovernedActionExecutionResultV1",
|
|
745
|
+
"version": 1,
|
|
746
|
+
"execution_digest": request["execution_digest"],
|
|
747
|
+
"action_id": request["action_id"],
|
|
748
|
+
"action_digest": request["action_digest"],
|
|
749
|
+
"intent_id": request["intent_id"],
|
|
750
|
+
"intent_digest": request["intent_digest"],
|
|
751
|
+
"tool_name": request["tool_name"],
|
|
752
|
+
"status": state,
|
|
753
|
+
"effect_state": effect_state,
|
|
754
|
+
"effect": normalized_effect,
|
|
755
|
+
"effect_digest": _wire_sha256_json(normalized_effect),
|
|
756
|
+
"observed_at": observed_at,
|
|
757
|
+
}
|
|
758
|
+
encoded = canonical_json(result)
|
|
759
|
+
if len(encoded.encode("utf-8")) > 16 * 1024:
|
|
760
|
+
raise RuntimeError("governed action result is too large")
|
|
761
|
+
# Returning the canonical round-trip keeps the first HTTP projection and
|
|
762
|
+
# every ledger replay identical down to JSON member order.
|
|
763
|
+
return json.loads(encoded)
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
class _PrivateLedgerPath:
|
|
767
|
+
"""Bind SQLite to one no-follow, owner-private local path.
|
|
768
|
+
|
|
769
|
+
The immediate parent is an exact mode-0700 current-user directory, so
|
|
770
|
+
SQLite's transient rollback journal remains private regardless of process
|
|
771
|
+
umask. The database is an atomically created or pre-existing mode-0600,
|
|
772
|
+
current-user, single-link regular file. Directory components and the leaf
|
|
773
|
+
are opened with ``O_NOFOLLOW`` and the leaf inode is re-attested around
|
|
774
|
+
every ledger operation.
|
|
775
|
+
"""
|
|
776
|
+
|
|
777
|
+
def __init__(self, path: str | Path) -> None:
|
|
778
|
+
raw = os.path.expanduser(str(path))
|
|
779
|
+
candidate = Path(raw)
|
|
780
|
+
if (
|
|
781
|
+
not candidate.is_absolute()
|
|
782
|
+
or candidate != Path(os.path.normpath(raw))
|
|
783
|
+
or not candidate.name
|
|
784
|
+
or any(component in {".", ".."} for component in candidate.parts)
|
|
785
|
+
):
|
|
786
|
+
raise PrivateGovernedActionLedgerError(
|
|
787
|
+
"governed action ledger path must be absolute and normalized"
|
|
788
|
+
)
|
|
789
|
+
required = ("O_DIRECTORY", "O_NOFOLLOW")
|
|
790
|
+
if any(not hasattr(os, name) for name in required) or not hasattr(
|
|
791
|
+
os, "geteuid"
|
|
792
|
+
):
|
|
793
|
+
raise PrivateGovernedActionLedgerError(
|
|
794
|
+
"governed action ledger requires POSIX no-follow file access"
|
|
795
|
+
)
|
|
796
|
+
self.path = candidate
|
|
797
|
+
self.euid = os.geteuid()
|
|
798
|
+
|
|
799
|
+
@staticmethod
|
|
800
|
+
def _same_inode(left: os.stat_result, right: os.stat_result) -> bool:
|
|
801
|
+
return (left.st_dev, left.st_ino) == (right.st_dev, right.st_ino)
|
|
802
|
+
|
|
803
|
+
def _validate_directory(
|
|
804
|
+
self, value: os.stat_result, *, private_parent: bool
|
|
805
|
+
) -> None:
|
|
806
|
+
if not stat.S_ISDIR(value.st_mode):
|
|
807
|
+
raise PrivateGovernedActionLedgerError(
|
|
808
|
+
"governed action ledger parent chain is not a directory"
|
|
809
|
+
)
|
|
810
|
+
if private_parent:
|
|
811
|
+
if value.st_uid != self.euid or stat.S_IMODE(value.st_mode) != 0o700:
|
|
812
|
+
raise PrivateGovernedActionLedgerError(
|
|
813
|
+
"governed action ledger parent must be current-user mode 0700"
|
|
814
|
+
)
|
|
815
|
+
elif value.st_uid not in {0, self.euid}:
|
|
816
|
+
raise PrivateGovernedActionLedgerError(
|
|
817
|
+
"governed action ledger ancestor has an untrusted owner"
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
def _open_parent(self) -> int:
|
|
821
|
+
flags = (
|
|
822
|
+
os.O_RDONLY
|
|
823
|
+
| os.O_DIRECTORY
|
|
824
|
+
| os.O_NOFOLLOW
|
|
825
|
+
| getattr(os, "O_CLOEXEC", 0)
|
|
826
|
+
)
|
|
827
|
+
try:
|
|
828
|
+
current_fd = os.open(self.path.anchor, flags)
|
|
829
|
+
except OSError:
|
|
830
|
+
raise PrivateGovernedActionLedgerError(
|
|
831
|
+
"governed action ledger root cannot be opened"
|
|
832
|
+
) from None
|
|
833
|
+
components = self.path.parts[1:-1]
|
|
834
|
+
try:
|
|
835
|
+
self._validate_directory(
|
|
836
|
+
os.fstat(current_fd), private_parent=not components
|
|
837
|
+
)
|
|
838
|
+
for index, component in enumerate(components):
|
|
839
|
+
private_parent = index == len(components) - 1
|
|
840
|
+
try:
|
|
841
|
+
before = os.stat(
|
|
842
|
+
component, dir_fd=current_fd, follow_symlinks=False
|
|
843
|
+
)
|
|
844
|
+
except FileNotFoundError:
|
|
845
|
+
try:
|
|
846
|
+
os.mkdir(component, 0o700, dir_fd=current_fd)
|
|
847
|
+
except FileExistsError:
|
|
848
|
+
pass
|
|
849
|
+
except OSError:
|
|
850
|
+
raise PrivateGovernedActionLedgerError(
|
|
851
|
+
"governed action ledger parent cannot be created"
|
|
852
|
+
) from None
|
|
853
|
+
try:
|
|
854
|
+
before = os.stat(
|
|
855
|
+
component, dir_fd=current_fd, follow_symlinks=False
|
|
856
|
+
)
|
|
857
|
+
except OSError:
|
|
858
|
+
raise PrivateGovernedActionLedgerError(
|
|
859
|
+
"governed action ledger parent is unstable"
|
|
860
|
+
) from None
|
|
861
|
+
self._validate_directory(before, private_parent=private_parent)
|
|
862
|
+
try:
|
|
863
|
+
next_fd = os.open(component, flags, dir_fd=current_fd)
|
|
864
|
+
except OSError:
|
|
865
|
+
raise PrivateGovernedActionLedgerError(
|
|
866
|
+
"governed action ledger parent cannot be opened without links"
|
|
867
|
+
) from None
|
|
868
|
+
try:
|
|
869
|
+
after = os.fstat(next_fd)
|
|
870
|
+
self._validate_directory(after, private_parent=private_parent)
|
|
871
|
+
if not self._same_inode(before, after):
|
|
872
|
+
raise PrivateGovernedActionLedgerError(
|
|
873
|
+
"governed action ledger parent changed during open"
|
|
874
|
+
)
|
|
875
|
+
except BaseException:
|
|
876
|
+
os.close(next_fd)
|
|
877
|
+
raise
|
|
878
|
+
os.close(current_fd)
|
|
879
|
+
current_fd = next_fd
|
|
880
|
+
return current_fd
|
|
881
|
+
except BaseException:
|
|
882
|
+
os.close(current_fd)
|
|
883
|
+
raise
|
|
884
|
+
|
|
885
|
+
def _validate_leaf(self, value: os.stat_result) -> None:
|
|
886
|
+
if (
|
|
887
|
+
not stat.S_ISREG(value.st_mode)
|
|
888
|
+
or value.st_uid != self.euid
|
|
889
|
+
or value.st_nlink != 1
|
|
890
|
+
or stat.S_IMODE(value.st_mode) != 0o600
|
|
891
|
+
):
|
|
892
|
+
raise PrivateGovernedActionLedgerError(
|
|
893
|
+
"governed action ledger must be a current-user mode-0600 "
|
|
894
|
+
"single-link file"
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
def _assert_leaf(self, parent_fd: int, identity: tuple[int, int]) -> None:
|
|
898
|
+
try:
|
|
899
|
+
current = os.stat(
|
|
900
|
+
self.path.name, dir_fd=parent_fd, follow_symlinks=False
|
|
901
|
+
)
|
|
902
|
+
except OSError:
|
|
903
|
+
raise PrivateGovernedActionLedgerError(
|
|
904
|
+
"governed action ledger disappeared"
|
|
905
|
+
) from None
|
|
906
|
+
self._validate_leaf(current)
|
|
907
|
+
if (current.st_dev, current.st_ino) != identity:
|
|
908
|
+
raise PrivateGovernedActionLedgerError(
|
|
909
|
+
"governed action ledger changed identity"
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
def _assert_no_wal_sidecars(self, parent_fd: int) -> None:
|
|
913
|
+
for suffix in ("-wal", "-shm"):
|
|
914
|
+
try:
|
|
915
|
+
os.stat(
|
|
916
|
+
self.path.name + suffix,
|
|
917
|
+
dir_fd=parent_fd,
|
|
918
|
+
follow_symlinks=False,
|
|
919
|
+
)
|
|
920
|
+
except FileNotFoundError:
|
|
921
|
+
continue
|
|
922
|
+
except OSError:
|
|
923
|
+
raise PrivateGovernedActionLedgerError(
|
|
924
|
+
"governed action ledger sidecar posture is unavailable"
|
|
925
|
+
) from None
|
|
926
|
+
raise PrivateGovernedActionLedgerError(
|
|
927
|
+
"governed action ledger must not retain WAL or SHM sidecars"
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
def _open_leaf(self) -> tuple[int, int, tuple[int, int]]:
|
|
931
|
+
parent_fd = self._open_parent()
|
|
932
|
+
flags = os.O_RDWR | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0)
|
|
933
|
+
leaf_fd: int | None = None
|
|
934
|
+
try:
|
|
935
|
+
try:
|
|
936
|
+
before = os.stat(
|
|
937
|
+
self.path.name, dir_fd=parent_fd, follow_symlinks=False
|
|
938
|
+
)
|
|
939
|
+
self._validate_leaf(before)
|
|
940
|
+
leaf_fd = os.open(self.path.name, flags, dir_fd=parent_fd)
|
|
941
|
+
opened = os.fstat(leaf_fd)
|
|
942
|
+
self._validate_leaf(opened)
|
|
943
|
+
if not self._same_inode(before, opened):
|
|
944
|
+
raise PrivateGovernedActionLedgerError(
|
|
945
|
+
"governed action ledger changed during open"
|
|
946
|
+
)
|
|
947
|
+
except FileNotFoundError:
|
|
948
|
+
try:
|
|
949
|
+
leaf_fd = os.open(
|
|
950
|
+
self.path.name,
|
|
951
|
+
flags | os.O_CREAT | os.O_EXCL,
|
|
952
|
+
0o600,
|
|
953
|
+
dir_fd=parent_fd,
|
|
954
|
+
)
|
|
955
|
+
except FileExistsError:
|
|
956
|
+
before = os.stat(
|
|
957
|
+
self.path.name, dir_fd=parent_fd, follow_symlinks=False
|
|
958
|
+
)
|
|
959
|
+
self._validate_leaf(before)
|
|
960
|
+
leaf_fd = os.open(self.path.name, flags, dir_fd=parent_fd)
|
|
961
|
+
opened = os.fstat(leaf_fd)
|
|
962
|
+
if not self._same_inode(before, opened):
|
|
963
|
+
raise PrivateGovernedActionLedgerError(
|
|
964
|
+
"governed action ledger changed during create"
|
|
965
|
+
)
|
|
966
|
+
else:
|
|
967
|
+
os.fchmod(leaf_fd, 0o600)
|
|
968
|
+
opened = os.fstat(leaf_fd)
|
|
969
|
+
self._validate_leaf(opened)
|
|
970
|
+
identity = (opened.st_dev, opened.st_ino)
|
|
971
|
+
self._assert_leaf(parent_fd, identity)
|
|
972
|
+
self._assert_no_wal_sidecars(parent_fd)
|
|
973
|
+
return parent_fd, leaf_fd, identity
|
|
974
|
+
except BaseException:
|
|
975
|
+
if leaf_fd is not None:
|
|
976
|
+
os.close(leaf_fd)
|
|
977
|
+
os.close(parent_fd)
|
|
978
|
+
raise
|
|
979
|
+
|
|
980
|
+
def connect(self) -> tuple[sqlite3.Connection, tuple[int, int]]:
|
|
981
|
+
parent_fd, leaf_fd, identity = self._open_leaf()
|
|
982
|
+
connection: sqlite3.Connection | None = None
|
|
983
|
+
try:
|
|
984
|
+
connection = sqlite3.connect(
|
|
985
|
+
str(self.path), check_same_thread=False, timeout=5.0
|
|
986
|
+
)
|
|
987
|
+
self._assert_leaf(parent_fd, identity)
|
|
988
|
+
mode = connection.execute("PRAGMA journal_mode=DELETE").fetchone()[0]
|
|
989
|
+
if str(mode).lower() != "delete":
|
|
990
|
+
raise PrivateGovernedActionLedgerError(
|
|
991
|
+
"governed action ledger could not disable persistent sidecars"
|
|
992
|
+
)
|
|
993
|
+
self._assert_no_wal_sidecars(parent_fd)
|
|
994
|
+
return connection, identity
|
|
995
|
+
except BaseException:
|
|
996
|
+
if connection is not None:
|
|
997
|
+
connection.close()
|
|
998
|
+
raise
|
|
999
|
+
finally:
|
|
1000
|
+
os.close(leaf_fd)
|
|
1001
|
+
os.close(parent_fd)
|
|
1002
|
+
|
|
1003
|
+
def assert_current(self, identity: tuple[int, int]) -> None:
|
|
1004
|
+
parent_fd = self._open_parent()
|
|
1005
|
+
try:
|
|
1006
|
+
self._assert_leaf(parent_fd, identity)
|
|
1007
|
+
self._assert_no_wal_sidecars(parent_fd)
|
|
1008
|
+
finally:
|
|
1009
|
+
os.close(parent_fd)
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
class GovernedActionLedger:
|
|
1013
|
+
"""SQLite truth for one-mutation-at-most governed effects."""
|
|
1014
|
+
|
|
1015
|
+
def __init__(self, db_path: str | Path, *, clock=time.time) -> None:
|
|
1016
|
+
self._storage = _PrivateLedgerPath(db_path)
|
|
1017
|
+
self.path = self._storage.path
|
|
1018
|
+
self.clock = clock
|
|
1019
|
+
self._lock = threading.RLock()
|
|
1020
|
+
self._conn, self._identity = self._storage.connect()
|
|
1021
|
+
self._conn.row_factory = sqlite3.Row
|
|
1022
|
+
self._conn.execute("PRAGMA synchronous=FULL")
|
|
1023
|
+
self._conn.execute("PRAGMA busy_timeout=5000")
|
|
1024
|
+
self._conn.execute(
|
|
1025
|
+
"""
|
|
1026
|
+
CREATE TABLE IF NOT EXISTS governed_actions (
|
|
1027
|
+
action_id TEXT PRIMARY KEY,
|
|
1028
|
+
request_sha256 TEXT NOT NULL,
|
|
1029
|
+
execution_digest TEXT NOT NULL,
|
|
1030
|
+
request_json TEXT NOT NULL,
|
|
1031
|
+
owner_person_id TEXT NOT NULL,
|
|
1032
|
+
state TEXT NOT NULL CHECK (
|
|
1033
|
+
state IN ('prepared','executing','completed','failed','ambiguous')
|
|
1034
|
+
),
|
|
1035
|
+
result_json TEXT,
|
|
1036
|
+
created_at REAL NOT NULL,
|
|
1037
|
+
updated_at REAL NOT NULL,
|
|
1038
|
+
started_at REAL
|
|
1039
|
+
)
|
|
1040
|
+
"""
|
|
1041
|
+
)
|
|
1042
|
+
self._conn.execute(
|
|
1043
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS governed_actions_execution_digest "
|
|
1044
|
+
"ON governed_actions(execution_digest)"
|
|
1045
|
+
)
|
|
1046
|
+
self._conn.commit()
|
|
1047
|
+
self._storage.assert_current(self._identity)
|
|
1048
|
+
self._recover_executing()
|
|
1049
|
+
|
|
1050
|
+
@staticmethod
|
|
1051
|
+
def _request_sha(request: Mapping[str, Any]) -> str:
|
|
1052
|
+
return sha256_json(dict(request))
|
|
1053
|
+
|
|
1054
|
+
def _recover_executing(self) -> None:
|
|
1055
|
+
now = _clock_value(self.clock)
|
|
1056
|
+
with self._lock:
|
|
1057
|
+
self._storage.assert_current(self._identity)
|
|
1058
|
+
rows = self._conn.execute(
|
|
1059
|
+
"SELECT action_id, request_json FROM governed_actions WHERE state = 'executing'"
|
|
1060
|
+
).fetchall()
|
|
1061
|
+
if not rows:
|
|
1062
|
+
return
|
|
1063
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
1064
|
+
try:
|
|
1065
|
+
for row in rows:
|
|
1066
|
+
request = json.loads(row["request_json"])
|
|
1067
|
+
result = _state_result(request, state="ambiguous", observed_at=now)
|
|
1068
|
+
self._conn.execute(
|
|
1069
|
+
"UPDATE governed_actions SET state = 'ambiguous', result_json = ?, "
|
|
1070
|
+
"updated_at = ? WHERE action_id = ? AND state = 'executing'",
|
|
1071
|
+
(canonical_json(result), now, row["action_id"]),
|
|
1072
|
+
)
|
|
1073
|
+
self._conn.commit()
|
|
1074
|
+
self._storage.assert_current(self._identity)
|
|
1075
|
+
except Exception:
|
|
1076
|
+
self._conn.rollback()
|
|
1077
|
+
self._storage.assert_current(self._identity)
|
|
1078
|
+
raise
|
|
1079
|
+
|
|
1080
|
+
def get(self, action_id: str) -> dict[str, Any] | None:
|
|
1081
|
+
with self._lock:
|
|
1082
|
+
self._storage.assert_current(self._identity)
|
|
1083
|
+
row = self._conn.execute(
|
|
1084
|
+
"SELECT * FROM governed_actions WHERE action_id = ?", (action_id,)
|
|
1085
|
+
).fetchone()
|
|
1086
|
+
self._storage.assert_current(self._identity)
|
|
1087
|
+
if row is None:
|
|
1088
|
+
return None
|
|
1089
|
+
value = dict(row)
|
|
1090
|
+
value["request"] = json.loads(value.pop("request_json"))
|
|
1091
|
+
raw_result = value.pop("result_json")
|
|
1092
|
+
value["result"] = json.loads(raw_result) if raw_result is not None else None
|
|
1093
|
+
return value
|
|
1094
|
+
|
|
1095
|
+
def prepare_execution(
|
|
1096
|
+
self,
|
|
1097
|
+
request: Mapping[str, Any],
|
|
1098
|
+
*,
|
|
1099
|
+
owner_person_id: str,
|
|
1100
|
+
) -> tuple[dict[str, Any], bool]:
|
|
1101
|
+
action_id = request["action_id"]
|
|
1102
|
+
request_json = canonical_json(dict(request))
|
|
1103
|
+
request_sha = hashlib.sha256(request_json.encode("utf-8")).hexdigest()
|
|
1104
|
+
now = _clock_value(self.clock)
|
|
1105
|
+
with self._lock:
|
|
1106
|
+
self._storage.assert_current(self._identity)
|
|
1107
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
1108
|
+
try:
|
|
1109
|
+
row = self._conn.execute(
|
|
1110
|
+
"SELECT * FROM governed_actions WHERE action_id = ?", (action_id,)
|
|
1111
|
+
).fetchone()
|
|
1112
|
+
created = row is None
|
|
1113
|
+
if row is None:
|
|
1114
|
+
try:
|
|
1115
|
+
self._conn.execute(
|
|
1116
|
+
"""
|
|
1117
|
+
INSERT INTO governed_actions (
|
|
1118
|
+
action_id, request_sha256, execution_digest, request_json,
|
|
1119
|
+
owner_person_id, state, result_json, created_at, updated_at
|
|
1120
|
+
) VALUES (?, ?, ?, ?, ?, 'prepared', NULL, ?, ?)
|
|
1121
|
+
""",
|
|
1122
|
+
(
|
|
1123
|
+
action_id,
|
|
1124
|
+
request_sha,
|
|
1125
|
+
request["execution_digest"],
|
|
1126
|
+
request_json,
|
|
1127
|
+
owner_person_id,
|
|
1128
|
+
now,
|
|
1129
|
+
now,
|
|
1130
|
+
),
|
|
1131
|
+
)
|
|
1132
|
+
except sqlite3.IntegrityError as exc:
|
|
1133
|
+
raise GovernedActionConflict(
|
|
1134
|
+
"execution digest is already bound to another action"
|
|
1135
|
+
) from exc
|
|
1136
|
+
else:
|
|
1137
|
+
if (
|
|
1138
|
+
row["request_sha256"] != request_sha
|
|
1139
|
+
or row["execution_digest"] != request["execution_digest"]
|
|
1140
|
+
or row["owner_person_id"] != owner_person_id
|
|
1141
|
+
):
|
|
1142
|
+
raise GovernedActionConflict(
|
|
1143
|
+
"action_id is already bound to another execution"
|
|
1144
|
+
)
|
|
1145
|
+
self._conn.commit()
|
|
1146
|
+
self._storage.assert_current(self._identity)
|
|
1147
|
+
except Exception:
|
|
1148
|
+
self._conn.rollback()
|
|
1149
|
+
self._storage.assert_current(self._identity)
|
|
1150
|
+
raise
|
|
1151
|
+
record = self.get(action_id)
|
|
1152
|
+
assert record is not None
|
|
1153
|
+
return record, created
|
|
1154
|
+
|
|
1155
|
+
def mark_executing(self, action_id: str) -> dict[str, Any]:
|
|
1156
|
+
now = _clock_value(self.clock)
|
|
1157
|
+
with self._lock:
|
|
1158
|
+
self._storage.assert_current(self._identity)
|
|
1159
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
1160
|
+
try:
|
|
1161
|
+
cursor = self._conn.execute(
|
|
1162
|
+
"UPDATE governed_actions SET state = 'executing', result_json = NULL, "
|
|
1163
|
+
"started_at = ?, updated_at = ? "
|
|
1164
|
+
"WHERE action_id = ? AND state = 'prepared'",
|
|
1165
|
+
(now, now, action_id),
|
|
1166
|
+
)
|
|
1167
|
+
if cursor.rowcount != 1:
|
|
1168
|
+
raise GovernedActionConflict("action is not prepared for execution")
|
|
1169
|
+
self._conn.commit()
|
|
1170
|
+
self._storage.assert_current(self._identity)
|
|
1171
|
+
except Exception:
|
|
1172
|
+
self._conn.rollback()
|
|
1173
|
+
self._storage.assert_current(self._identity)
|
|
1174
|
+
raise
|
|
1175
|
+
record = self.get(action_id)
|
|
1176
|
+
assert record is not None
|
|
1177
|
+
return record
|
|
1178
|
+
|
|
1179
|
+
def finish(
|
|
1180
|
+
self,
|
|
1181
|
+
action_id: str,
|
|
1182
|
+
*,
|
|
1183
|
+
state: str,
|
|
1184
|
+
effect: Mapping[str, Any] | None = None,
|
|
1185
|
+
) -> dict[str, Any]:
|
|
1186
|
+
if state not in {"completed", "ambiguous"}:
|
|
1187
|
+
raise ValueError("executing action may only complete or become ambiguous")
|
|
1188
|
+
now = _clock_value(self.clock)
|
|
1189
|
+
record = self.get(action_id)
|
|
1190
|
+
if record is None or record["state"] != "executing":
|
|
1191
|
+
raise GovernedActionConflict("action is not executing")
|
|
1192
|
+
result = _state_result(
|
|
1193
|
+
record["request"], state=state, observed_at=now, effect=effect
|
|
1194
|
+
)
|
|
1195
|
+
encoded = canonical_json(result)
|
|
1196
|
+
with self._lock:
|
|
1197
|
+
self._storage.assert_current(self._identity)
|
|
1198
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
1199
|
+
try:
|
|
1200
|
+
cursor = self._conn.execute(
|
|
1201
|
+
"UPDATE governed_actions SET state = ?, result_json = ?, updated_at = ? "
|
|
1202
|
+
"WHERE action_id = ? AND state = 'executing'",
|
|
1203
|
+
(state, encoded, now, action_id),
|
|
1204
|
+
)
|
|
1205
|
+
if cursor.rowcount != 1:
|
|
1206
|
+
raise GovernedActionConflict("action execution state changed")
|
|
1207
|
+
self._conn.commit()
|
|
1208
|
+
self._storage.assert_current(self._identity)
|
|
1209
|
+
except Exception:
|
|
1210
|
+
self._conn.rollback()
|
|
1211
|
+
self._storage.assert_current(self._identity)
|
|
1212
|
+
raise
|
|
1213
|
+
return result
|
|
1214
|
+
|
|
1215
|
+
def fail_prepared(self, action_id: str) -> dict[str, Any]:
|
|
1216
|
+
now = _clock_value(self.clock)
|
|
1217
|
+
record = self.get(action_id)
|
|
1218
|
+
if record is None or record["state"] != "prepared":
|
|
1219
|
+
raise GovernedActionConflict("action is not prepared")
|
|
1220
|
+
result = _state_result(record["request"], state="failed", observed_at=now)
|
|
1221
|
+
encoded = canonical_json(result)
|
|
1222
|
+
with self._lock:
|
|
1223
|
+
self._storage.assert_current(self._identity)
|
|
1224
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
1225
|
+
try:
|
|
1226
|
+
cursor = self._conn.execute(
|
|
1227
|
+
"UPDATE governed_actions SET state = 'failed', result_json = ?, "
|
|
1228
|
+
"updated_at = ? WHERE action_id = ? AND state = 'prepared'",
|
|
1229
|
+
(encoded, now, action_id),
|
|
1230
|
+
)
|
|
1231
|
+
if cursor.rowcount != 1:
|
|
1232
|
+
raise GovernedActionConflict("action preparation state changed")
|
|
1233
|
+
self._conn.commit()
|
|
1234
|
+
self._storage.assert_current(self._identity)
|
|
1235
|
+
except Exception:
|
|
1236
|
+
self._conn.rollback()
|
|
1237
|
+
self._storage.assert_current(self._identity)
|
|
1238
|
+
raise
|
|
1239
|
+
return result
|
|
1240
|
+
|
|
1241
|
+
def close(self) -> None:
|
|
1242
|
+
with self._lock:
|
|
1243
|
+
self._conn.close()
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
class GovernedActionService:
|
|
1247
|
+
"""Validate, fence, execute once, and project one governed action."""
|
|
1248
|
+
|
|
1249
|
+
def __init__(self, ledger: GovernedActionLedger, executor, *, clock=time.time) -> None:
|
|
1250
|
+
if not isinstance(ledger, GovernedActionLedger):
|
|
1251
|
+
raise TypeError("governed action ledger is required")
|
|
1252
|
+
if not callable(getattr(executor, "prepare", None)) or not callable(
|
|
1253
|
+
getattr(executor, "perform", None)
|
|
1254
|
+
):
|
|
1255
|
+
raise TypeError("governed action executor is invalid")
|
|
1256
|
+
self.ledger = ledger
|
|
1257
|
+
self.executor = executor
|
|
1258
|
+
self.clock = clock
|
|
1259
|
+
# Owner-approved mutations are rare. A single dispatch lock avoids a
|
|
1260
|
+
# growing per-action lock cache and makes same-process concurrency
|
|
1261
|
+
# obviously one-writer without affecting read-only observations.
|
|
1262
|
+
self._dispatch_lock = asyncio.Lock()
|
|
1263
|
+
|
|
1264
|
+
async def execute(
|
|
1265
|
+
self,
|
|
1266
|
+
path_action_id: str,
|
|
1267
|
+
raw: bytes,
|
|
1268
|
+
authority: RequestAuthority,
|
|
1269
|
+
) -> dict[str, Any]:
|
|
1270
|
+
owner = _owner_from_authority(authority, required_scope="actions:execute")
|
|
1271
|
+
request = parse_execution_request(raw, path_action_id=path_action_id)
|
|
1272
|
+
execution_context = _freeze_json(request)
|
|
1273
|
+
async with self._dispatch_lock:
|
|
1274
|
+
existing = self.ledger.get(request["action_id"])
|
|
1275
|
+
if existing is not None:
|
|
1276
|
+
expected = sha256_json(request)
|
|
1277
|
+
if (
|
|
1278
|
+
existing["request_sha256"] != expected
|
|
1279
|
+
or existing["owner_person_id"] != owner
|
|
1280
|
+
):
|
|
1281
|
+
raise GovernedActionConflict(
|
|
1282
|
+
"action_id is already bound to another execution"
|
|
1283
|
+
)
|
|
1284
|
+
if existing["state"] != "prepared":
|
|
1285
|
+
if existing["result"] is None:
|
|
1286
|
+
return _state_result(
|
|
1287
|
+
existing["request"],
|
|
1288
|
+
state=existing["state"],
|
|
1289
|
+
observed_at=_clock_value(self.clock),
|
|
1290
|
+
)
|
|
1291
|
+
return existing["result"]
|
|
1292
|
+
try:
|
|
1293
|
+
_assert_live_approval(request, _clock_value(self.clock))
|
|
1294
|
+
except GovernedActionValidationError:
|
|
1295
|
+
return self.ledger.fail_prepared(request["action_id"])
|
|
1296
|
+
else:
|
|
1297
|
+
_assert_live_approval(request, _clock_value(self.clock))
|
|
1298
|
+
existing, _created = self.ledger.prepare_execution(
|
|
1299
|
+
request, owner_person_id=owner
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
try:
|
|
1303
|
+
await self.executor.prepare(execution_context, owner)
|
|
1304
|
+
except Exception as exc:
|
|
1305
|
+
logger.warning(
|
|
1306
|
+
"Governed action %s failed read-only preparation (type=%s)",
|
|
1307
|
+
request["action_id"], type(exc).__name__,
|
|
1308
|
+
)
|
|
1309
|
+
return self.ledger.fail_prepared(request["action_id"])
|
|
1310
|
+
|
|
1311
|
+
try:
|
|
1312
|
+
_assert_live_approval(request, _clock_value(self.clock))
|
|
1313
|
+
except GovernedActionValidationError:
|
|
1314
|
+
return self.ledger.fail_prepared(request["action_id"])
|
|
1315
|
+
|
|
1316
|
+
self.ledger.mark_executing(request["action_id"])
|
|
1317
|
+
try:
|
|
1318
|
+
effect = await self.executor.perform(
|
|
1319
|
+
execution_context, owner
|
|
1320
|
+
)
|
|
1321
|
+
effect = _validated_effect(effect)
|
|
1322
|
+
except BaseException as exc:
|
|
1323
|
+
# BaseException is deliberate: cancellation, shutdown, or any
|
|
1324
|
+
# other unwind after the durable executing marker has an
|
|
1325
|
+
# uncertain effect outcome and therefore must never retry.
|
|
1326
|
+
logger.warning(
|
|
1327
|
+
"Governed action %s has an ambiguous effect outcome (type=%s)",
|
|
1328
|
+
request["action_id"], type(exc).__name__,
|
|
1329
|
+
)
|
|
1330
|
+
ambiguous = self.ledger.finish(
|
|
1331
|
+
request["action_id"], state="ambiguous"
|
|
1332
|
+
)
|
|
1333
|
+
if isinstance(exc, (asyncio.CancelledError, KeyboardInterrupt, SystemExit)):
|
|
1334
|
+
raise
|
|
1335
|
+
return ambiguous
|
|
1336
|
+
return self.ledger.finish(
|
|
1337
|
+
request["action_id"], state="completed", effect=effect
|
|
1338
|
+
)
|
|
1339
|
+
|
|
1340
|
+
async def observe(
|
|
1341
|
+
self, action_id: str, authority: RequestAuthority
|
|
1342
|
+
) -> dict[str, Any]:
|
|
1343
|
+
_owner_from_authority(authority, required_scope="actions:verify")
|
|
1344
|
+
action_id = _valid_action_id(action_id)
|
|
1345
|
+
record = self.ledger.get(action_id)
|
|
1346
|
+
if record is None:
|
|
1347
|
+
raise GovernedActionNotFound("governed action was not found")
|
|
1348
|
+
if record["owner_person_id"] != authority.viewer_person_id:
|
|
1349
|
+
raise PermissionError("governed action is outside the owner binding")
|
|
1350
|
+
if record["result"] is not None:
|
|
1351
|
+
return record["result"]
|
|
1352
|
+
return _state_result(
|
|
1353
|
+
record["request"],
|
|
1354
|
+
state=record["state"],
|
|
1355
|
+
observed_at=_clock_value(self.clock),
|
|
1356
|
+
)
|
|
1357
|
+
|
|
1358
|
+
def close(self) -> None:
|
|
1359
|
+
self.ledger.close()
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
async def _await(value):
|
|
1363
|
+
return await value if inspect.isawaitable(value) else value
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
class ColonySubsystemActionExecutor:
|
|
1367
|
+
"""Map the ten generic action names onto existing Colony subsystems."""
|
|
1368
|
+
|
|
1369
|
+
def __init__(
|
|
1370
|
+
self,
|
|
1371
|
+
*,
|
|
1372
|
+
graph=None,
|
|
1373
|
+
goals=None,
|
|
1374
|
+
commitments=None,
|
|
1375
|
+
initiatives=None,
|
|
1376
|
+
projects=None,
|
|
1377
|
+
feedback=None,
|
|
1378
|
+
autonomy_enable=None,
|
|
1379
|
+
autonomy_disable=None,
|
|
1380
|
+
autonomy_running=None,
|
|
1381
|
+
) -> None:
|
|
1382
|
+
self.graph = graph
|
|
1383
|
+
self.goals = goals
|
|
1384
|
+
self.commitments = commitments
|
|
1385
|
+
self.initiatives = initiatives
|
|
1386
|
+
self.projects = projects
|
|
1387
|
+
self.feedback = feedback
|
|
1388
|
+
self.autonomy_enable = autonomy_enable
|
|
1389
|
+
self.autonomy_disable = autonomy_disable
|
|
1390
|
+
self.autonomy_running = autonomy_running
|
|
1391
|
+
|
|
1392
|
+
def _dependency(self, value, name: str):
|
|
1393
|
+
if value is None:
|
|
1394
|
+
raise RuntimeError(f"{name} is not initialized")
|
|
1395
|
+
return value
|
|
1396
|
+
|
|
1397
|
+
@staticmethod
|
|
1398
|
+
def _governed_research_project(
|
|
1399
|
+
request: Mapping[str, Any], owner_person_id: str,
|
|
1400
|
+
):
|
|
1401
|
+
from apsimo.projects import Project
|
|
1402
|
+
from apsimo.work_orders import action_authority
|
|
1403
|
+
|
|
1404
|
+
args = request["args"]
|
|
1405
|
+
approval = request["approval"]
|
|
1406
|
+
identity_material = {
|
|
1407
|
+
"schema": "ColonyGovernedResearchProjectIdentityV1",
|
|
1408
|
+
"version": 1,
|
|
1409
|
+
"owner_person_id": owner_person_id,
|
|
1410
|
+
"action_id": request["action_id"],
|
|
1411
|
+
"action_digest": request["action_digest"],
|
|
1412
|
+
"execution_digest": request["execution_digest"],
|
|
1413
|
+
}
|
|
1414
|
+
identity = sha256_json(identity_material)
|
|
1415
|
+
depth = args.get("depth", "quick")
|
|
1416
|
+
return Project(
|
|
1417
|
+
id="proj-governed-" + identity[:20],
|
|
1418
|
+
title="Governed research " + identity[:8],
|
|
1419
|
+
objective=(
|
|
1420
|
+
"Research topic:\n" + args["topic"] + "\nDepth: " + depth
|
|
1421
|
+
),
|
|
1422
|
+
source="governed_action",
|
|
1423
|
+
status="planning",
|
|
1424
|
+
outcome="pending",
|
|
1425
|
+
entity_ids=[],
|
|
1426
|
+
reason="",
|
|
1427
|
+
replans=0,
|
|
1428
|
+
next_review_at=0.0,
|
|
1429
|
+
source_event_refs=[
|
|
1430
|
+
"governed-action:" + request["action_id"],
|
|
1431
|
+
"governed-intent:" + request["intent_id"],
|
|
1432
|
+
],
|
|
1433
|
+
evidence_refs=[
|
|
1434
|
+
"action-digest:" + request["action_digest"],
|
|
1435
|
+
"intent-digest:" + request["intent_digest"],
|
|
1436
|
+
"args-digest:" + request["args_sha256"],
|
|
1437
|
+
"execution-digest:" + request["execution_digest"],
|
|
1438
|
+
],
|
|
1439
|
+
policy_decision_refs=[
|
|
1440
|
+
"approval:" + approval["approval_id"],
|
|
1441
|
+
"decision:" + approval["decision_id"],
|
|
1442
|
+
"approval-revision:" + str(approval["revision"]),
|
|
1443
|
+
"authorization-receipt-digest:"
|
|
1444
|
+
+ approval["authorization_receipt_sha256"],
|
|
1445
|
+
],
|
|
1446
|
+
subject_person_id=owner_person_id,
|
|
1447
|
+
viewer_scope="owner",
|
|
1448
|
+
shareability="owner_private",
|
|
1449
|
+
capability_allowlist=list(action_authority("research")[1]),
|
|
1450
|
+
goal_fingerprint=identity,
|
|
1451
|
+
)
|
|
1452
|
+
|
|
1453
|
+
async def prepare(
|
|
1454
|
+
self, request: Mapping[str, Any], owner_person_id: str,
|
|
1455
|
+
) -> None:
|
|
1456
|
+
tool_name = request["tool_name"]
|
|
1457
|
+
args = request["args"]
|
|
1458
|
+
if tool_name not in ACTION_TOOL_NAMES:
|
|
1459
|
+
raise RuntimeError("unknown governed Colony action")
|
|
1460
|
+
if tool_name == "colony_autonomy_enable":
|
|
1461
|
+
self._dependency(self.autonomy_enable, "autonomy control")
|
|
1462
|
+
self._dependency(self.autonomy_running, "autonomy status")
|
|
1463
|
+
elif tool_name == "colony_autonomy_disable":
|
|
1464
|
+
self._dependency(self.autonomy_disable, "autonomy control")
|
|
1465
|
+
self._dependency(self.autonomy_running, "autonomy status")
|
|
1466
|
+
elif tool_name == "colony_create_commitment":
|
|
1467
|
+
self._dependency(self.commitments, "commitment store")
|
|
1468
|
+
elif tool_name == "colony_resolve_commitment":
|
|
1469
|
+
store = self._dependency(self.commitments, "commitment store")
|
|
1470
|
+
row = store.get(args["commitment_id"])
|
|
1471
|
+
if row is None or row.get("person_id") != owner_person_id:
|
|
1472
|
+
raise RuntimeError("owner commitment was not found")
|
|
1473
|
+
elif tool_name in {
|
|
1474
|
+
"colony_task_complete",
|
|
1475
|
+
"colony_task_snooze",
|
|
1476
|
+
"colony_task_dismiss",
|
|
1477
|
+
}:
|
|
1478
|
+
goals = self._dependency(self.goals, "goal store")
|
|
1479
|
+
try:
|
|
1480
|
+
goals.get_goal(args["task_id"])
|
|
1481
|
+
except Exception as exc:
|
|
1482
|
+
raise RuntimeError("task was not found") from exc
|
|
1483
|
+
elif tool_name == "colony_initiative_feedback":
|
|
1484
|
+
initiatives = self._dependency(self.initiatives, "initiative store")
|
|
1485
|
+
if initiatives.get(args["initiative_id"]) is None:
|
|
1486
|
+
raise RuntimeError("initiative was not found")
|
|
1487
|
+
elif tool_name == "colony_record_insight":
|
|
1488
|
+
graph = self._dependency(self.graph, "memory graph")
|
|
1489
|
+
if not callable(getattr(graph, "store_memory", None)):
|
|
1490
|
+
raise RuntimeError("memory graph insight writer is not initialized")
|
|
1491
|
+
elif tool_name == "colony_research":
|
|
1492
|
+
projects = self._dependency(self.projects, "ProjectEngine")
|
|
1493
|
+
prepare = getattr(projects, "prepare_governed_research", None)
|
|
1494
|
+
enqueue = getattr(projects, "enqueue_governed_research", None)
|
|
1495
|
+
if not callable(prepare) or not callable(enqueue):
|
|
1496
|
+
raise RuntimeError(
|
|
1497
|
+
"ProjectEngine governed research handoff is not initialized"
|
|
1498
|
+
)
|
|
1499
|
+
prepare(self._governed_research_project(request, owner_person_id))
|
|
1500
|
+
|
|
1501
|
+
async def perform(
|
|
1502
|
+
self, request: Mapping[str, Any], owner_person_id: str
|
|
1503
|
+
) -> dict[str, Any]:
|
|
1504
|
+
tool_name = request["tool_name"]
|
|
1505
|
+
args = request["args"]
|
|
1506
|
+
if tool_name == "colony_autonomy_enable":
|
|
1507
|
+
await _await(self.autonomy_enable())
|
|
1508
|
+
running = bool(await _await(self.autonomy_running()))
|
|
1509
|
+
return self._effect(
|
|
1510
|
+
"autonomy",
|
|
1511
|
+
"enabled" if running else "start_requested",
|
|
1512
|
+
{"running": running},
|
|
1513
|
+
)
|
|
1514
|
+
if tool_name == "colony_autonomy_disable":
|
|
1515
|
+
await _await(self.autonomy_disable())
|
|
1516
|
+
running = bool(await _await(self.autonomy_running()))
|
|
1517
|
+
return self._effect(
|
|
1518
|
+
"autonomy",
|
|
1519
|
+
"stop_requested" if running else "disabled",
|
|
1520
|
+
{"running": running},
|
|
1521
|
+
)
|
|
1522
|
+
if tool_name == "colony_create_commitment":
|
|
1523
|
+
row = self.commitments.create(
|
|
1524
|
+
person_id=owner_person_id,
|
|
1525
|
+
description=args["description"],
|
|
1526
|
+
due_at=args.get("due_at") or None,
|
|
1527
|
+
priority=args.get(
|
|
1528
|
+
"priority", GOVERNED_COMMITMENT_PRIORITY_DEFAULT,
|
|
1529
|
+
),
|
|
1530
|
+
source_type="governed_action",
|
|
1531
|
+
source_context="owner_authorized",
|
|
1532
|
+
metadata={"governed": True},
|
|
1533
|
+
)
|
|
1534
|
+
effect_id = _effect_id("commitment", row.get("id"))
|
|
1535
|
+
return self._effect(
|
|
1536
|
+
effect_id,
|
|
1537
|
+
"created",
|
|
1538
|
+
{"commitment_id": effect_id, "status": _status_text(row.get("status"))},
|
|
1539
|
+
)
|
|
1540
|
+
if tool_name == "colony_resolve_commitment":
|
|
1541
|
+
row = self.commitments.resolve(
|
|
1542
|
+
args["commitment_id"],
|
|
1543
|
+
outcome=args.get("outcome", "done"),
|
|
1544
|
+
note=args.get("reason", ""),
|
|
1545
|
+
resolved_by="governed-owner",
|
|
1546
|
+
)
|
|
1547
|
+
if row is None:
|
|
1548
|
+
raise RuntimeError("commitment was not found")
|
|
1549
|
+
effect_id = _effect_id("commitment", row.get("id") or args["commitment_id"])
|
|
1550
|
+
return self._effect(
|
|
1551
|
+
effect_id,
|
|
1552
|
+
"resolved",
|
|
1553
|
+
{"commitment_id": effect_id, "status": _status_text(row.get("status"))},
|
|
1554
|
+
)
|
|
1555
|
+
if tool_name == "colony_initiative_feedback":
|
|
1556
|
+
item = self.initiatives.get(args["initiative_id"])
|
|
1557
|
+
status_map = {
|
|
1558
|
+
"acknowledged": "acknowledged",
|
|
1559
|
+
"dismissed": "cancelled",
|
|
1560
|
+
"snoozed": "pending",
|
|
1561
|
+
"actioned": "completed",
|
|
1562
|
+
}
|
|
1563
|
+
action = args["action"]
|
|
1564
|
+
updated = self.initiatives.update(
|
|
1565
|
+
args["initiative_id"], status=status_map[action]
|
|
1566
|
+
)
|
|
1567
|
+
if updated is None:
|
|
1568
|
+
raise RuntimeError("initiative update failed")
|
|
1569
|
+
try:
|
|
1570
|
+
self.initiatives.log_history(
|
|
1571
|
+
args["initiative_id"],
|
|
1572
|
+
action="governed_" + action,
|
|
1573
|
+
agent_id="governed-action-worker",
|
|
1574
|
+
details=json.loads(canonical_json(args.get("details", {}))),
|
|
1575
|
+
)
|
|
1576
|
+
except Exception:
|
|
1577
|
+
logger.warning("Governed initiative history could not be recorded", exc_info=True)
|
|
1578
|
+
if self.feedback is not None and getattr(item, "type", None):
|
|
1579
|
+
try:
|
|
1580
|
+
self.feedback.record(item.type, action)
|
|
1581
|
+
except Exception:
|
|
1582
|
+
logger.warning("Governed initiative feedback could not be recorded", exc_info=True)
|
|
1583
|
+
effect_id = _effect_id("initiative", args["initiative_id"])
|
|
1584
|
+
return self._effect(
|
|
1585
|
+
effect_id,
|
|
1586
|
+
action,
|
|
1587
|
+
{
|
|
1588
|
+
"initiative_id": effect_id,
|
|
1589
|
+
"action": action,
|
|
1590
|
+
"status": _status_text(getattr(updated, "status", status_map[action])),
|
|
1591
|
+
},
|
|
1592
|
+
)
|
|
1593
|
+
if tool_name == "colony_record_insight":
|
|
1594
|
+
content_hash = sha256_json({
|
|
1595
|
+
"schema": "ColonyGovernedInsightIdentityV1",
|
|
1596
|
+
"version": 1,
|
|
1597
|
+
"person_id": owner_person_id,
|
|
1598
|
+
"insight_type": args["insight_type"],
|
|
1599
|
+
"source": "governed_action",
|
|
1600
|
+
"content": args["content"],
|
|
1601
|
+
})
|
|
1602
|
+
insight_id = await self.graph.store_memory(
|
|
1603
|
+
content=args["content"],
|
|
1604
|
+
memory_type="semantic",
|
|
1605
|
+
entities=[],
|
|
1606
|
+
metadata={
|
|
1607
|
+
"governed": True,
|
|
1608
|
+
"insight_type": args["insight_type"],
|
|
1609
|
+
"source": "governed_action",
|
|
1610
|
+
},
|
|
1611
|
+
importance=args.get("confidence", 0.7),
|
|
1612
|
+
person_id=owner_person_id,
|
|
1613
|
+
source_type="inference",
|
|
1614
|
+
content_hash=content_hash,
|
|
1615
|
+
)
|
|
1616
|
+
effect_id = _effect_id("insight", insight_id)
|
|
1617
|
+
return self._effect(
|
|
1618
|
+
effect_id,
|
|
1619
|
+
"recorded",
|
|
1620
|
+
{"insight_id": effect_id, "insight_type": args["insight_type"]},
|
|
1621
|
+
)
|
|
1622
|
+
if tool_name == "colony_research":
|
|
1623
|
+
project = self.projects.enqueue_governed_research(
|
|
1624
|
+
self._governed_research_project(request, owner_person_id)
|
|
1625
|
+
)
|
|
1626
|
+
effect_id = _effect_id("project", project.id)
|
|
1627
|
+
return self._effect(
|
|
1628
|
+
effect_id,
|
|
1629
|
+
"queued",
|
|
1630
|
+
{
|
|
1631
|
+
"project_id": effect_id,
|
|
1632
|
+
"depth": args.get("depth", "quick"),
|
|
1633
|
+
"status": _status_text(project.status),
|
|
1634
|
+
},
|
|
1635
|
+
)
|
|
1636
|
+
if tool_name == "colony_task_complete":
|
|
1637
|
+
if not self.goals.complete_task(args["task_id"]):
|
|
1638
|
+
raise RuntimeError("task completion failed")
|
|
1639
|
+
effect_id = _effect_id("task", args["task_id"])
|
|
1640
|
+
return self._effect(
|
|
1641
|
+
effect_id,
|
|
1642
|
+
"completed",
|
|
1643
|
+
{"task_id": effect_id, "status": "completed"},
|
|
1644
|
+
)
|
|
1645
|
+
if tool_name == "colony_task_snooze":
|
|
1646
|
+
hours = args.get("hours", 24)
|
|
1647
|
+
if not self.goals.snooze_task(
|
|
1648
|
+
args["task_id"], hours, args.get("reason", "")
|
|
1649
|
+
):
|
|
1650
|
+
raise RuntimeError("task snooze failed")
|
|
1651
|
+
effect_id = _effect_id("task", args["task_id"])
|
|
1652
|
+
return self._effect(
|
|
1653
|
+
effect_id,
|
|
1654
|
+
"snoozed",
|
|
1655
|
+
{"task_id": effect_id, "status": "snoozed", "hours": hours},
|
|
1656
|
+
)
|
|
1657
|
+
if tool_name == "colony_task_dismiss":
|
|
1658
|
+
reason = args.get("reason", "stale")
|
|
1659
|
+
if not self.goals.dismiss_task(args["task_id"], reason):
|
|
1660
|
+
raise RuntimeError("task dismissal failed")
|
|
1661
|
+
effect_id = _effect_id("task", args["task_id"])
|
|
1662
|
+
return self._effect(
|
|
1663
|
+
effect_id,
|
|
1664
|
+
"dismissed",
|
|
1665
|
+
{"task_id": effect_id, "status": "dismissed", "reason": reason},
|
|
1666
|
+
)
|
|
1667
|
+
raise RuntimeError("unknown governed Colony action")
|
|
1668
|
+
|
|
1669
|
+
@staticmethod
|
|
1670
|
+
def _effect(effect_id: str, outcome: str, verification: Mapping[str, Any]):
|
|
1671
|
+
return {
|
|
1672
|
+
"schema": "ColonyGovernedActionEffectV1",
|
|
1673
|
+
"version": 1,
|
|
1674
|
+
"effect_id": _effect_id("effect", effect_id),
|
|
1675
|
+
"outcome": outcome,
|
|
1676
|
+
"verification": dict(verification),
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
|
|
1680
|
+
__all__ = (
|
|
1681
|
+
"ACTION_TOOL_NAMES",
|
|
1682
|
+
"ColonySubsystemActionExecutor",
|
|
1683
|
+
"GOVERNED_ACTION_PRINCIPAL",
|
|
1684
|
+
"GOVERNED_ACTION_REQUEST_MAX_BYTES",
|
|
1685
|
+
"GOVERNED_ACTION_SCOPES",
|
|
1686
|
+
"GOVERNED_COMMITMENT_DESCRIPTION_MAX_CHARS",
|
|
1687
|
+
"GOVERNED_COMMITMENT_DUE_AT_MAX_CHARS",
|
|
1688
|
+
"GOVERNED_COMMITMENT_PRIORITY_DEFAULT",
|
|
1689
|
+
"GOVERNED_DETAILS_INTEGER_MAX",
|
|
1690
|
+
"GOVERNED_DETAILS_KEY_MAX_CHARS",
|
|
1691
|
+
"GOVERNED_DETAILS_MAX_DEPTH",
|
|
1692
|
+
"GOVERNED_DETAILS_MAX_NODES",
|
|
1693
|
+
"GOVERNED_DETAILS_STRING_MAX_CHARS",
|
|
1694
|
+
"GOVERNED_FREEFORM_REASON_MAX_CHARS",
|
|
1695
|
+
"GOVERNED_IDENTIFIER_MAX_CHARS",
|
|
1696
|
+
"GOVERNED_IDENTIFIER_PATTERN",
|
|
1697
|
+
"GOVERNED_INSIGHT_CONTENT_MAX_CHARS",
|
|
1698
|
+
"GOVERNED_RESEARCH_TOPIC_MAX_CHARS",
|
|
1699
|
+
"GovernedActionConflict",
|
|
1700
|
+
"GovernedActionLedger",
|
|
1701
|
+
"GovernedActionNotFound",
|
|
1702
|
+
"GovernedActionService",
|
|
1703
|
+
"GovernedActionValidationError",
|
|
1704
|
+
"PrivateGovernedActionLedgerError",
|
|
1705
|
+
"canonical_json",
|
|
1706
|
+
"parse_execution_request",
|
|
1707
|
+
"sha256_json",
|
|
1708
|
+
)
|