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,187 @@
|
|
|
1
|
+
"""Colony Local Key Manager — PEM-based Ed25519 signing for Phase 1/2.
|
|
2
|
+
|
|
3
|
+
Provides the same interface as ColonyKeyManager (sign, public_key_hex, rotate)
|
|
4
|
+
but stores the key as a simple PEM file. Designed to be swapped out for the
|
|
5
|
+
Shamir-backed ColonyKeyManager when Phase 3/4/5 ship.
|
|
6
|
+
|
|
7
|
+
Key storage:
|
|
8
|
+
{state_dir}/colony-keys/
|
|
9
|
+
private.pem — Ed25519 private key (optionally encrypted)
|
|
10
|
+
public.pem — Ed25519 public key (always readable)
|
|
11
|
+
|
|
12
|
+
Migration path:
|
|
13
|
+
Phase 1/2: LocalKeyManager (PEM, optional passphrase)
|
|
14
|
+
Phase 3: LocalKeyManager with passphrase (colony key set-passphrase)
|
|
15
|
+
Phase 4/5: ColonyKeyManager (Shamir-split, colony key migrate-to-shamir)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Callable, Optional
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LocalKeyManager:
|
|
29
|
+
"""PEM-based Ed25519 key manager for single-node Colony deployments."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
keys_dir: str | Path,
|
|
34
|
+
colony_id: str,
|
|
35
|
+
passphrase: Optional[bytes] = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
self._keys_dir = Path(keys_dir)
|
|
38
|
+
self.colony_id = colony_id
|
|
39
|
+
self._passphrase = passphrase
|
|
40
|
+
self._keys_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
# ------------------------------------------------------------------
|
|
43
|
+
# Public interface (matches ColonyKeyManager)
|
|
44
|
+
# ------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
def sign(self, payload: bytes) -> str:
|
|
47
|
+
"""Sign payload bytes, returning hex-encoded Ed25519 signature."""
|
|
48
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
49
|
+
|
|
50
|
+
private_key = self._load_private_key()
|
|
51
|
+
signature = private_key.sign(payload)
|
|
52
|
+
return signature.hex()
|
|
53
|
+
|
|
54
|
+
def public_key_hex(self) -> str:
|
|
55
|
+
"""Return the hex-encoded raw Ed25519 public key."""
|
|
56
|
+
from cryptography.hazmat.primitives import serialization
|
|
57
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
58
|
+
|
|
59
|
+
private_key = self._load_private_key()
|
|
60
|
+
pub = private_key.public_key().public_bytes(
|
|
61
|
+
encoding=serialization.Encoding.Raw,
|
|
62
|
+
format=serialization.PublicFormat.Raw,
|
|
63
|
+
)
|
|
64
|
+
return pub.hex()
|
|
65
|
+
|
|
66
|
+
def public_key_pem(self) -> str:
|
|
67
|
+
"""Return the PEM-encoded public key."""
|
|
68
|
+
pub_path = self._keys_dir / "public.pem"
|
|
69
|
+
if pub_path.exists():
|
|
70
|
+
return pub_path.read_text()
|
|
71
|
+
# Generate from private key
|
|
72
|
+
from cryptography.hazmat.primitives import serialization
|
|
73
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
74
|
+
|
|
75
|
+
private_key = self._load_private_key()
|
|
76
|
+
pem = private_key.public_key().public_bytes(
|
|
77
|
+
encoding=serialization.Encoding.PEM,
|
|
78
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
79
|
+
)
|
|
80
|
+
pub_path.write_bytes(pem)
|
|
81
|
+
return pem.decode()
|
|
82
|
+
|
|
83
|
+
def rotate(self, new_passphrase: Optional[bytes] = None) -> None:
|
|
84
|
+
"""Generate a new Ed25519 keypair, replacing the existing one.
|
|
85
|
+
|
|
86
|
+
The colony_id does NOT change — only the signing key rotates.
|
|
87
|
+
"""
|
|
88
|
+
from cryptography.hazmat.primitives import serialization
|
|
89
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
90
|
+
|
|
91
|
+
# Backup old key
|
|
92
|
+
priv_path = self._keys_dir / "private.pem"
|
|
93
|
+
if priv_path.exists():
|
|
94
|
+
backup = self._keys_dir / "private.pem.backup"
|
|
95
|
+
backup.write_bytes(priv_path.read_bytes())
|
|
96
|
+
|
|
97
|
+
# Generate new keypair
|
|
98
|
+
private_key = Ed25519PrivateKey.generate()
|
|
99
|
+
self._save_keypair(private_key, new_passphrase or self._passphrase)
|
|
100
|
+
logger.info("Key rotated for colony %s", self.colony_id)
|
|
101
|
+
|
|
102
|
+
def set_passphrase(self, new_passphrase: bytes) -> None:
|
|
103
|
+
"""Re-encrypt the private key with a new passphrase."""
|
|
104
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
105
|
+
|
|
106
|
+
private_key = self._load_private_key()
|
|
107
|
+
self._save_keypair(private_key, new_passphrase)
|
|
108
|
+
self._passphrase = new_passphrase
|
|
109
|
+
logger.info("Passphrase updated for colony %s", self.colony_id)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def has_keys(self) -> bool:
|
|
113
|
+
"""Check if keypair exists on disk."""
|
|
114
|
+
return (self._keys_dir / "private.pem").exists()
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
# Internal
|
|
118
|
+
# ------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def _load_private_key(self):
|
|
121
|
+
from cryptography.hazmat.primitives import serialization
|
|
122
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
123
|
+
|
|
124
|
+
priv_path = self._keys_dir / "private.pem"
|
|
125
|
+
if not priv_path.exists():
|
|
126
|
+
raise FileNotFoundError(
|
|
127
|
+
f"No private key found at {priv_path}. Run 'colony init' or 'colony key generate'."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
pem_data = priv_path.read_bytes()
|
|
131
|
+
try:
|
|
132
|
+
return serialization.load_pem_private_key(
|
|
133
|
+
pem_data,
|
|
134
|
+
password=self._passphrase,
|
|
135
|
+
)
|
|
136
|
+
except (ValueError, TypeError):
|
|
137
|
+
# Wrong passphrase or unencrypted key with passphrase given
|
|
138
|
+
if self._passphrase:
|
|
139
|
+
raise ValueError("Wrong passphrase for private key") from None
|
|
140
|
+
# Try without passphrase
|
|
141
|
+
return serialization.load_pem_private_key(pem_data, password=None)
|
|
142
|
+
|
|
143
|
+
def _save_keypair(self, private_key, passphrase: Optional[bytes] = None) -> None:
|
|
144
|
+
from cryptography.hazmat.primitives import serialization
|
|
145
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
146
|
+
|
|
147
|
+
# Save private key
|
|
148
|
+
encryption: Any = serialization.NoEncryption()
|
|
149
|
+
if passphrase:
|
|
150
|
+
encryption = serialization.BestAvailableEncryption(passphrase)
|
|
151
|
+
|
|
152
|
+
priv_pem = private_key.private_bytes(
|
|
153
|
+
encoding=serialization.Encoding.PEM,
|
|
154
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
155
|
+
encryption_algorithm=encryption,
|
|
156
|
+
)
|
|
157
|
+
(self._keys_dir / "private.pem").write_bytes(priv_pem)
|
|
158
|
+
|
|
159
|
+
# Save public key
|
|
160
|
+
pub_pem = private_key.public_key().public_bytes(
|
|
161
|
+
encoding=serialization.Encoding.PEM,
|
|
162
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
163
|
+
)
|
|
164
|
+
(self._keys_dir / "public.pem").write_bytes(pub_pem)
|
|
165
|
+
|
|
166
|
+
# Restrict permissions
|
|
167
|
+
os.chmod(self._keys_dir / "private.pem", 0o600)
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def generate(
|
|
171
|
+
cls,
|
|
172
|
+
keys_dir: str | Path,
|
|
173
|
+
colony_id: str,
|
|
174
|
+
passphrase: Optional[bytes] = None,
|
|
175
|
+
) -> "LocalKeyManager":
|
|
176
|
+
"""Generate a new Ed25519 keypair and return a LocalKeyManager."""
|
|
177
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
178
|
+
|
|
179
|
+
mgr = cls(keys_dir=keys_dir, colony_id=colony_id, passphrase=passphrase)
|
|
180
|
+
private_key = Ed25519PrivateKey.generate()
|
|
181
|
+
mgr._save_keypair(private_key, passphrase)
|
|
182
|
+
logger.info("Generated new Ed25519 keypair for colony %s", colony_id)
|
|
183
|
+
return mgr
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# Fix type annotation for Python 3.11 compat
|
|
187
|
+
from typing import Any
|
apsimo/chain/manager.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""ChainManager — main coordinator tying all chain components together.
|
|
2
|
+
|
|
3
|
+
Responsibilities:
|
|
4
|
+
- Owns the ChainStore, ChainStateMachine, and Mempool
|
|
5
|
+
- Chain sync protocol (pull blocks from Sentinels)
|
|
6
|
+
- New block announcement broadcasting
|
|
7
|
+
- Transaction submission and nonce tracking
|
|
8
|
+
- Integration hook for the federation module
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import sqlite3
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Callable, Coroutine, Optional
|
|
22
|
+
|
|
23
|
+
from apsimo import get_state_dir
|
|
24
|
+
|
|
25
|
+
from .block import Block
|
|
26
|
+
from .state_machine import ChainStateMachine
|
|
27
|
+
from .storage import ChainStore
|
|
28
|
+
from .transactions import ChainState, Transaction, TxType
|
|
29
|
+
from .validation import BlockValidator, TransactionValidator, ValidationResult
|
|
30
|
+
from .protocol import (
|
|
31
|
+
ChainAnnouncement,
|
|
32
|
+
ChainStateResponse,
|
|
33
|
+
ChainSyncRequest,
|
|
34
|
+
ChainSyncResponse,
|
|
35
|
+
TxSubmitResponse,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
_CHECKPOINT_INTERVAL = 100
|
|
41
|
+
|
|
42
|
+
_chain_manager_instance: Optional["ChainManager"] = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class SubmitResult:
|
|
47
|
+
accepted: bool
|
|
48
|
+
tx_id: str
|
|
49
|
+
reason: str = ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ChainManager:
|
|
53
|
+
"""Main coordinator for ColonyChain.
|
|
54
|
+
|
|
55
|
+
Manages the local chain replica, handles sync with Sentinels,
|
|
56
|
+
validates and queues transactions, and broadcasts new blocks.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
db_path: Path | str,
|
|
62
|
+
colony_id: str,
|
|
63
|
+
send_message: Callable[[str, dict[str, Any]], Coroutine] | None = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
self.colony_id = colony_id
|
|
66
|
+
self.store = ChainStore(db_path)
|
|
67
|
+
self.state_machine = ChainStateMachine(self.store)
|
|
68
|
+
self._send_message = send_message
|
|
69
|
+
self._state: ChainState | None = None
|
|
70
|
+
self._state_lock = asyncio.Lock()
|
|
71
|
+
self._block_validator = BlockValidator()
|
|
72
|
+
|
|
73
|
+
# ── State access ────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
async def get_state(self) -> ChainState:
|
|
76
|
+
"""Return current chain state (thread-safe cached)."""
|
|
77
|
+
async with self._state_lock:
|
|
78
|
+
if self._state is None:
|
|
79
|
+
self._state = self.state_machine.get_current_state()
|
|
80
|
+
return self._state
|
|
81
|
+
|
|
82
|
+
def _invalidate_state(self) -> None:
|
|
83
|
+
self._state = None
|
|
84
|
+
|
|
85
|
+
# ── Transaction submission ───────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
async def submit_transaction(self, tx: Transaction) -> SubmitResult:
|
|
88
|
+
"""Validate and add a transaction to the mempool."""
|
|
89
|
+
state = await self.get_state()
|
|
90
|
+
|
|
91
|
+
# Nonce check (against persisted chain)
|
|
92
|
+
if tx.from_colony_id != "system":
|
|
93
|
+
persisted_nonce = self.store.get_nonce(tx.from_colony_id)
|
|
94
|
+
if tx.nonce <= persisted_nonce:
|
|
95
|
+
return SubmitResult(
|
|
96
|
+
accepted=False,
|
|
97
|
+
tx_id=tx.tx_id,
|
|
98
|
+
reason=f"nonce {tx.nonce} <= last accepted {persisted_nonce}",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
validator = TransactionValidator(state, chain_store=self.store)
|
|
102
|
+
result = validator.validate(tx)
|
|
103
|
+
if not result.ok:
|
|
104
|
+
return SubmitResult(accepted=False, tx_id=tx.tx_id, reason=result.reason)
|
|
105
|
+
|
|
106
|
+
self.store.add_to_mempool(tx)
|
|
107
|
+
return SubmitResult(accepted=True, tx_id=tx.tx_id)
|
|
108
|
+
|
|
109
|
+
# ── Block commitment ─────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
async def commit_block(self, block: Block) -> None:
|
|
112
|
+
"""Commit a block to the chain and update state."""
|
|
113
|
+
latest = self.store.get_latest_block()
|
|
114
|
+
bv = self._block_validator
|
|
115
|
+
result = bv.validate_block(block, latest)
|
|
116
|
+
if not result.ok:
|
|
117
|
+
raise ValueError(f"Block validation failed: {result.reason}")
|
|
118
|
+
|
|
119
|
+
self.store.append_block(block)
|
|
120
|
+
self._invalidate_state()
|
|
121
|
+
|
|
122
|
+
# Save checkpoint every 100 blocks
|
|
123
|
+
if block.index > 0 and block.index % _CHECKPOINT_INTERVAL == 0:
|
|
124
|
+
state = await self.get_state()
|
|
125
|
+
self.store.save_checkpoint(state, block.index)
|
|
126
|
+
|
|
127
|
+
# Announce to peers
|
|
128
|
+
announcement = ChainAnnouncement(
|
|
129
|
+
block_index=block.index,
|
|
130
|
+
block_hash=block.block_hash,
|
|
131
|
+
producer_id=block.producer_id,
|
|
132
|
+
tx_count=len(block.transactions),
|
|
133
|
+
)
|
|
134
|
+
await self._broadcast(announcement.to_dict())
|
|
135
|
+
|
|
136
|
+
logger.info(
|
|
137
|
+
"Block %d committed (%d txs, hash %s…)",
|
|
138
|
+
block.index,
|
|
139
|
+
len(block.transactions),
|
|
140
|
+
block.block_hash[:16],
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# ── Chain sync ───────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
async def sync_from_sentinel(
|
|
146
|
+
self,
|
|
147
|
+
sentinel_id: str,
|
|
148
|
+
from_height: int | None = None,
|
|
149
|
+
) -> int:
|
|
150
|
+
"""Pull blocks from a Sentinel to catch up. Returns blocks added."""
|
|
151
|
+
if from_height is None:
|
|
152
|
+
from_height = self.store.get_height() + 1
|
|
153
|
+
|
|
154
|
+
req = ChainSyncRequest(
|
|
155
|
+
from_index=from_height,
|
|
156
|
+
to_index=from_height + 499, # batch up to 500 blocks
|
|
157
|
+
requester_id=self.colony_id,
|
|
158
|
+
)
|
|
159
|
+
if self._send_message:
|
|
160
|
+
await self._send_message(sentinel_id, req.to_dict())
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
async def handle_chain_sync_response(self, data: dict[str, Any]) -> int:
|
|
164
|
+
"""Process a CHAIN_SYNC_RESPONSE message. Returns blocks committed."""
|
|
165
|
+
blocks_data = data.get("blocks", [])
|
|
166
|
+
committed = 0
|
|
167
|
+
for block_dict in blocks_data:
|
|
168
|
+
block = Block.from_dict(block_dict)
|
|
169
|
+
try:
|
|
170
|
+
await self.commit_block(block)
|
|
171
|
+
committed += 1
|
|
172
|
+
except ValueError as exc:
|
|
173
|
+
logger.warning("Skipping block %d: %s", block.index, exc)
|
|
174
|
+
break
|
|
175
|
+
return committed
|
|
176
|
+
|
|
177
|
+
async def handle_chain_announce(self, data: dict[str, Any]) -> None:
|
|
178
|
+
"""Handle a CHAIN_ANNOUNCE — trigger sync if we're behind."""
|
|
179
|
+
announced_height = data.get("block_index", 0)
|
|
180
|
+
our_height = self.store.get_height()
|
|
181
|
+
if announced_height > our_height + 1:
|
|
182
|
+
sender_id = data.get("producer_id", "")
|
|
183
|
+
if sender_id:
|
|
184
|
+
await self.sync_from_sentinel(sender_id, from_height=our_height + 1)
|
|
185
|
+
|
|
186
|
+
async def handle_tx_submit(
|
|
187
|
+
self, data: dict[str, Any]
|
|
188
|
+
) -> TxSubmitResponse:
|
|
189
|
+
"""Handle a TX_SUBMIT message from another colony."""
|
|
190
|
+
tx_json = data.get("tx_json", "{}")
|
|
191
|
+
try:
|
|
192
|
+
tx_dict = json.loads(tx_json)
|
|
193
|
+
tx = Transaction.from_dict(tx_dict)
|
|
194
|
+
except Exception as exc:
|
|
195
|
+
return TxSubmitResponse(tx_id="", accepted=False, reason=str(exc))
|
|
196
|
+
|
|
197
|
+
result = await self.submit_transaction(tx)
|
|
198
|
+
return TxSubmitResponse(
|
|
199
|
+
tx_id=result.tx_id,
|
|
200
|
+
accepted=result.accepted,
|
|
201
|
+
reason=result.reason,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
async def get_chain_state_summary(
|
|
205
|
+
self, responder_id: str | None = None
|
|
206
|
+
) -> ChainStateResponse:
|
|
207
|
+
"""Build a ChainStateResponse summary."""
|
|
208
|
+
state = await self.get_state()
|
|
209
|
+
return ChainStateResponse(
|
|
210
|
+
height=state.height,
|
|
211
|
+
last_block_hash=state.last_block_hash,
|
|
212
|
+
n_colonies=len(state.colony_registry),
|
|
213
|
+
n_sentinels=len(state.active_sentinels()),
|
|
214
|
+
n_suspended=len(state.suspended_colonies),
|
|
215
|
+
network_id=state.network_id,
|
|
216
|
+
responder_id=responder_id or self.colony_id,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# ── Mempool helpers ──────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
def drain_mempool(self, max_txs: int = 500) -> list[Transaction]:
|
|
222
|
+
"""Drain pending transactions for block production."""
|
|
223
|
+
return self.store.get_mempool(limit=max_txs)
|
|
224
|
+
|
|
225
|
+
def evict_included(self, tx_ids: list[str]) -> None:
|
|
226
|
+
self.store.remove_from_mempool(tx_ids)
|
|
227
|
+
|
|
228
|
+
# ── Internal helpers ─────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
async def _broadcast(self, msg: dict[str, Any]) -> None:
|
|
231
|
+
"""Broadcast a message to all known peers (best-effort)."""
|
|
232
|
+
# In a real deployment this would iterate discovered peers.
|
|
233
|
+
# Here we call the send_message hook if provided.
|
|
234
|
+
if self._send_message:
|
|
235
|
+
state = await self.get_state()
|
|
236
|
+
for sentinel in state.active_sentinels():
|
|
237
|
+
try:
|
|
238
|
+
await self._send_message(sentinel.sentinel_id, msg)
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
logger.debug(
|
|
241
|
+
"Failed to send to sentinel %s: %s", sentinel.sentinel_id, exc
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def get_height(self) -> int:
|
|
245
|
+
return self.store.get_height()
|
|
246
|
+
|
|
247
|
+
@classmethod
|
|
248
|
+
def get_instance(cls) -> "ChainManager":
|
|
249
|
+
"""Return the process-wide singleton backed by ~/.colony/data/chain.db."""
|
|
250
|
+
global _chain_manager_instance
|
|
251
|
+
if _chain_manager_instance is None:
|
|
252
|
+
colony_home = get_state_dir()
|
|
253
|
+
db_path = colony_home / "chain.db"
|
|
254
|
+
# Derive colony_id from genesis block cryptographic identity
|
|
255
|
+
colony_id = "local"
|
|
256
|
+
if db_path.exists():
|
|
257
|
+
try:
|
|
258
|
+
conn = sqlite3.connect(str(db_path))
|
|
259
|
+
row = conn.execute(
|
|
260
|
+
"SELECT raw_json FROM chain_blocks WHERE block_index = 0"
|
|
261
|
+
).fetchone()
|
|
262
|
+
if row:
|
|
263
|
+
genesis_data = json.loads(row[0])
|
|
264
|
+
# Use the cryptographic colony_id (sha256 of Ed25519 pubkey)
|
|
265
|
+
# from genesis block metadata — not producer_id which is
|
|
266
|
+
# just a human-readable label and trivially spoofable.
|
|
267
|
+
colony_id = (
|
|
268
|
+
genesis_data.get("metadata", {}).get("genesis_colony_id")
|
|
269
|
+
or genesis_data.get("producer_id", "local")
|
|
270
|
+
)
|
|
271
|
+
conn.close()
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
|
274
|
+
_chain_manager_instance = cls(db_path=db_path, colony_id=colony_id)
|
|
275
|
+
return _chain_manager_instance
|
|
276
|
+
|
|
277
|
+
def get_status(self) -> dict:
|
|
278
|
+
"""Return chain status as a plain dict for the API router."""
|
|
279
|
+
height = self.store.get_height()
|
|
280
|
+
latest = self.store.get_latest_block()
|
|
281
|
+
mempool = self.store.get_mempool(limit=10000)
|
|
282
|
+
return {
|
|
283
|
+
"height": height,
|
|
284
|
+
"last_block_hash": latest.block_hash if latest else "0" * 64,
|
|
285
|
+
"last_block_timestamp": latest.timestamp if latest else None,
|
|
286
|
+
"colony_id": self.colony_id,
|
|
287
|
+
"mempool_size": len(mempool),
|
|
288
|
+
"protocol_version": "1.0.0",
|
|
289
|
+
"sync_status": "synced",
|
|
290
|
+
}
|
apsimo/chain/node.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Colony Node Identity — per-device identity within a Colony.
|
|
2
|
+
|
|
3
|
+
Each physical device running a Colony instance is a Node. Nodes have:
|
|
4
|
+
node_id — unique UUID per device, generated on first `colony start`
|
|
5
|
+
node keypair — Ed25519, independent from Colony keypair
|
|
6
|
+
node cert — signed by Colony private key, proving membership
|
|
7
|
+
|
|
8
|
+
The Colony is the logical identity. Nodes are the physical instances.
|
|
9
|
+
One Colony can run on many devices — each gets its own node_id.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import uuid
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
NODE_ID_FILE = "node-id"
|
|
24
|
+
NODE_KEYS_DIR = "node-keys"
|
|
25
|
+
NODE_CERT_FILE = "node-cert.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_or_create_node_id(state_dir: str | Path) -> str:
|
|
29
|
+
"""Get or create the node_id for this device."""
|
|
30
|
+
id_path = Path(state_dir) / NODE_ID_FILE
|
|
31
|
+
if id_path.exists():
|
|
32
|
+
node_id = id_path.read_text().strip()
|
|
33
|
+
if node_id:
|
|
34
|
+
return node_id
|
|
35
|
+
|
|
36
|
+
node_id = str(uuid.uuid4())
|
|
37
|
+
id_path.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
id_path.write_text(node_id)
|
|
39
|
+
logger.info("Generated new node_id: %s", node_id)
|
|
40
|
+
return node_id
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def ensure_node_keypair(state_dir: str | Path) -> "LocalKeyManager":
|
|
44
|
+
"""Ensure this device has a node keypair. Generate if missing.
|
|
45
|
+
|
|
46
|
+
Returns a LocalKeyManager for the node keys.
|
|
47
|
+
"""
|
|
48
|
+
from apsimo.chain.local_keys import LocalKeyManager
|
|
49
|
+
|
|
50
|
+
node_id = get_or_create_node_id(state_dir)
|
|
51
|
+
keys_dir = Path(state_dir) / NODE_KEYS_DIR
|
|
52
|
+
keys_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
if (keys_dir / "private.pem").exists():
|
|
55
|
+
return LocalKeyManager(keys_dir=keys_dir, colony_id=node_id)
|
|
56
|
+
|
|
57
|
+
km = LocalKeyManager.generate(keys_dir=keys_dir, colony_id=node_id)
|
|
58
|
+
logger.info("Generated node keypair for node %s", node_id)
|
|
59
|
+
return km
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def create_node_certificate(
|
|
63
|
+
state_dir: str | Path,
|
|
64
|
+
colony_key_manager: Optional["LocalKeyManager"] = None,
|
|
65
|
+
) -> dict:
|
|
66
|
+
"""Create a node certificate signed by the Colony's private key.
|
|
67
|
+
|
|
68
|
+
The certificate binds: colony_id, node_id, node_public_key, issued_at.
|
|
69
|
+
It proves this device belongs to the Colony.
|
|
70
|
+
"""
|
|
71
|
+
from apsimo.chain.local_keys import LocalKeyManager
|
|
72
|
+
from apsimo.chain.identity import get_or_create_colony_id
|
|
73
|
+
|
|
74
|
+
state_dir = Path(state_dir)
|
|
75
|
+
colony_id = get_or_create_colony_id(state_dir)
|
|
76
|
+
node_id = get_or_create_node_id(state_dir)
|
|
77
|
+
node_km = ensure_node_keypair(state_dir)
|
|
78
|
+
node_pubkey = node_km.public_key_hex()
|
|
79
|
+
|
|
80
|
+
cert = {
|
|
81
|
+
"colony_id": colony_id,
|
|
82
|
+
"node_id": node_id,
|
|
83
|
+
"node_public_key_ed25519": node_pubkey,
|
|
84
|
+
"issued_at": datetime.now(timezone.utc).isoformat(),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Sign with Colony private key
|
|
88
|
+
if colony_key_manager is None:
|
|
89
|
+
colony_keys_dir = state_dir / "colony-keys"
|
|
90
|
+
passphrase = None # Caller should handle passphrase
|
|
91
|
+
if (colony_keys_dir / "private.pem").exists():
|
|
92
|
+
colony_key_manager = LocalKeyManager(keys_dir=colony_keys_dir, colony_id=colony_id, passphrase=passphrase)
|
|
93
|
+
|
|
94
|
+
if colony_key_manager is not None:
|
|
95
|
+
# Deterministic payload for signing (exclude signature field)
|
|
96
|
+
payload = json.dumps(cert, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
97
|
+
cert["signature"] = colony_key_manager.sign(payload)
|
|
98
|
+
logger.info("Node certificate signed by Colony key")
|
|
99
|
+
else:
|
|
100
|
+
logger.warning("No Colony key manager available — node certificate is unsigned")
|
|
101
|
+
|
|
102
|
+
cert_path = state_dir / NODE_CERT_FILE
|
|
103
|
+
cert_path.write_text(json.dumps(cert, indent=2) + "\n")
|
|
104
|
+
return cert
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def verify_node_certificate(cert: dict, colony_public_key_hex: str) -> bool:
|
|
108
|
+
"""Verify a node certificate's signature against a Colony public key."""
|
|
109
|
+
from apsimo.chain.identity import _verify_ed25519_signature
|
|
110
|
+
|
|
111
|
+
signature = cert.get("signature")
|
|
112
|
+
if not signature:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
payload = {k: v for k, v in cert.items() if k != "signature"}
|
|
116
|
+
payload_bytes = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
117
|
+
return _verify_ed25519_signature(colony_public_key_hex, payload_bytes, signature)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def load_node_certificate(state_dir: str | Path) -> Optional[dict]:
|
|
121
|
+
"""Load the node certificate from disk."""
|
|
122
|
+
cert_path = Path(state_dir) / NODE_CERT_FILE
|
|
123
|
+
if not cert_path.exists():
|
|
124
|
+
return None
|
|
125
|
+
try:
|
|
126
|
+
return json.loads(cert_path.read_text())
|
|
127
|
+
except (json.JSONDecodeError, OSError):
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_node_info(state_dir: str | Path) -> dict:
|
|
132
|
+
"""Get full node identity info: node_id, public key, cert status."""
|
|
133
|
+
state_dir = Path(state_dir)
|
|
134
|
+
|
|
135
|
+
node_id = None
|
|
136
|
+
node_pubkey = None
|
|
137
|
+
certified = False
|
|
138
|
+
issued_at = None
|
|
139
|
+
|
|
140
|
+
node_id_path = state_dir / NODE_ID_FILE
|
|
141
|
+
if node_id_path.exists():
|
|
142
|
+
node_id = node_id_path.read_text().strip() or None
|
|
143
|
+
|
|
144
|
+
node_keys_dir = state_dir / NODE_KEYS_DIR
|
|
145
|
+
if (node_keys_dir / "private.pem").exists():
|
|
146
|
+
try:
|
|
147
|
+
from apsimo.chain.local_keys import LocalKeyManager
|
|
148
|
+
km = LocalKeyManager(keys_dir=node_keys_dir, colony_id=node_id or "unknown")
|
|
149
|
+
node_pubkey = km.public_key_hex()
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
cert = load_node_certificate(state_dir)
|
|
154
|
+
if cert and cert.get("signature"):
|
|
155
|
+
certified = True
|
|
156
|
+
issued_at = cert.get("issued_at")
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
"node_id": node_id,
|
|
160
|
+
"node_public_key": node_pubkey,
|
|
161
|
+
"certified": certified,
|
|
162
|
+
"issued_at": issued_at,
|
|
163
|
+
}
|