apsimo 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apsimo/__init__.py +38 -0
- apsimo/__main__.py +6 -0
- apsimo/agent/__init__.py +6 -0
- apsimo/agent/client.py +276 -0
- apsimo/agent/models.py +46 -0
- apsimo/agents/__init__.py +20 -0
- apsimo/agents/models.py +264 -0
- apsimo/agents/store.py +861 -0
- apsimo/agents/websocket.py +522 -0
- apsimo/api/__init__.py +1 -0
- apsimo/api/auth_telemetry.py +287 -0
- apsimo/api/authority.py +1203 -0
- apsimo/api/contact_grants.py +347 -0
- apsimo/api/middleware.py +483 -0
- apsimo/api/routers/__init__.py +1 -0
- apsimo/api/routers/commitment_work.py +265 -0
- apsimo/api/routers/context_gate.py +123 -0
- apsimo/api/routers/executions.py +140 -0
- apsimo/api/routers/followup_plans.py +147 -0
- apsimo/api/routers/governed_actions.py +162 -0
- apsimo/api/routers/host.py +14473 -0
- apsimo/api/routers/initiative_work.py +115 -0
- apsimo/api/routers/mining.py +104 -0
- apsimo/api/routers/observations.py +110 -0
- apsimo/api/routers/social_state.py +225 -0
- apsimo/api/routers/task_queue.py +2715 -0
- apsimo/api/routers/temporal_followups.py +251 -0
- apsimo/api/routers/transport.py +110 -0
- apsimo/api/routers/transport_ingress_api.py +240 -0
- apsimo/api/schemas/__init__.py +1 -0
- apsimo/api/schemas/host.py +1949 -0
- apsimo/autonomy/cli.py +110 -0
- apsimo/autonomy/condition_worker.py +437 -0
- apsimo/autonomy/config.py +424 -0
- apsimo/autonomy/loop.py +4316 -0
- apsimo/autonomy/registry.py +339 -0
- apsimo/autonomy/scheduler.py +1822 -0
- apsimo/autonomy/synthesis.py +449 -0
- apsimo/backup.py +962 -0
- apsimo/beliefs/__init__.py +23 -0
- apsimo/beliefs/contradictions.py +109 -0
- apsimo/beliefs/decay.py +61 -0
- apsimo/beliefs/engine.py +479 -0
- apsimo/beliefs/models.py +67 -0
- apsimo/beliefs/promotion.py +41 -0
- apsimo/beliefs/resolve.py +58 -0
- apsimo/beliefs/source_claims.py +690 -0
- apsimo/beliefs/source_projection.py +883 -0
- apsimo/beliefs/source_time.py +208 -0
- apsimo/beliefs/store.py +133 -0
- apsimo/briefings/aggregators.py +824 -0
- apsimo/briefings/composer.py +420 -0
- apsimo/briefings/config.py +55 -0
- apsimo/briefings/delivery.py +439 -0
- apsimo/briefings/engagement.py +97 -0
- apsimo/briefings/engine.py +274 -0
- apsimo/briefings/enhancer.py +99 -0
- apsimo/briefings/models.py +183 -0
- apsimo/briefings/scheduler.py +382 -0
- apsimo/briefings/store.py +435 -0
- apsimo/chain/__init__.py +48 -0
- apsimo/chain/block.py +100 -0
- apsimo/chain/cli.py +704 -0
- apsimo/chain/genesis.py +443 -0
- apsimo/chain/identity.py +416 -0
- apsimo/chain/keys.py +1025 -0
- apsimo/chain/local_keys.py +187 -0
- apsimo/chain/manager.py +290 -0
- apsimo/chain/node.py +163 -0
- apsimo/chain/plugin_transactions.py +371 -0
- apsimo/chain/protocol.py +220 -0
- apsimo/chain/state_machine.py +676 -0
- apsimo/chain/storage.py +503 -0
- apsimo/chain/transactions.py +250 -0
- apsimo/chain/validation.py +397 -0
- apsimo/channels/__init__.py +1 -0
- apsimo/channels/manifest.py +31 -0
- apsimo/channels/migrations/001_channels_schema.sql +12 -0
- apsimo/channels/phone_gateways.py +42 -0
- apsimo/channels/presence.py +188 -0
- apsimo/channels/router.py +235 -0
- apsimo/channels/store.py +231 -0
- apsimo/cli.py +2688 -0
- apsimo/cognition/__init__.py +11 -0
- apsimo/cognition/charter.py +398 -0
- apsimo/cognition/drive_governance.py +3530 -0
- apsimo/cognition/evidence_pipeline.py +1627 -0
- apsimo/cognition/external_events.py +932 -0
- apsimo/cognition/goal_spine.py +3488 -0
- apsimo/cognition/introspection.py +214 -0
- apsimo/cognition/prompt.py +150 -0
- apsimo/cognition/runtime.py +108 -0
- apsimo/cognition/trigger.py +154 -0
- apsimo/commitments/__init__.py +18 -0
- apsimo/commitments/local_work.py +355 -0
- apsimo/commitments/store.py +1052 -0
- apsimo/commitments/work.py +91 -0
- apsimo/compat.py +53 -0
- apsimo/compression/__init__.py +467 -0
- apsimo/connectors/__init__.py +21 -0
- apsimo/connectors/base.py +152 -0
- apsimo/connectors/caldav_calendar.py +125 -0
- apsimo/connectors/fs_documents.py +85 -0
- apsimo/connectors/imap_email.py +138 -0
- apsimo/connectors/manager.py +218 -0
- apsimo/connectors/webhook_pull.py +88 -0
- apsimo/contacts/__init__.py +33 -0
- apsimo/contacts/comms.py +357 -0
- apsimo/contacts/config.py +79 -0
- apsimo/contacts/exporters/__init__.py +1 -0
- apsimo/contacts/exporters/vcard.py +71 -0
- apsimo/contacts/identity_links.py +251 -0
- apsimo/contacts/importer.py +280 -0
- apsimo/contacts/importers/__init__.py +1 -0
- apsimo/contacts/importers/batch.py +43 -0
- apsimo/contacts/importers/macos_contacts.py +101 -0
- apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
- apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
- apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
- apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
- apsimo/contacts/migrations/005_identity_links.sql +27 -0
- apsimo/contacts/models.py +308 -0
- apsimo/contacts/scoring.py +16 -0
- apsimo/contacts/store.py +1623 -0
- apsimo/contacts/transport_ingress.py +252 -0
- apsimo/contacts/world_bridge.py +314 -0
- apsimo/contextgate/__init__.py +69 -0
- apsimo/contextgate/chunker.py +169 -0
- apsimo/contextgate/estimate.py +54 -0
- apsimo/contextgate/gate.py +313 -0
- apsimo/contextgate/retrieve.py +115 -0
- apsimo/delivery/__init__.py +16 -0
- apsimo/delivery/bridge.py +1260 -0
- apsimo/delivery/channels.py +526 -0
- apsimo/delivery/classification.py +50 -0
- apsimo/delivery/rate_limiter.py +268 -0
- apsimo/delivery/reachout_policy.py +206 -0
- apsimo/directed/__init__.py +22 -0
- apsimo/directed/audit.py +167 -0
- apsimo/directed/intake.py +95 -0
- apsimo/directed/models.py +191 -0
- apsimo/directed/service.py +509 -0
- apsimo/directives/__init__.py +25 -0
- apsimo/directives/evidence.py +87 -0
- apsimo/directives/extractor.py +188 -0
- apsimo/directives/guard.py +364 -0
- apsimo/directives/models.py +206 -0
- apsimo/directives/service.py +372 -0
- apsimo/directives/store.py +167 -0
- apsimo/doctor.py +2173 -0
- apsimo/environment.py +43 -0
- apsimo/events/__init__.py +33 -0
- apsimo/events/broadcaster.py +98 -0
- apsimo/events/bus.py +217 -0
- apsimo/events/journal.py +863 -0
- apsimo/events/stream.py +131 -0
- apsimo/events/types.py +150 -0
- apsimo/execution_results.py +357 -0
- apsimo/feedback/__init__.py +5 -0
- apsimo/feedback/store.py +76 -0
- apsimo/feeds/__init__.py +19 -0
- apsimo/feeds/cli.py +84 -0
- apsimo/feeds/engine.py +437 -0
- apsimo/feeds/example-feed.yaml +77 -0
- apsimo/feeds/hermes_cron.py +126 -0
- apsimo/feeds/manager.py +235 -0
- apsimo/feeds/spec.py +250 -0
- apsimo/feeds/template.py +202 -0
- apsimo/gate/__init__.py +18 -0
- apsimo/gate/audit.py +61 -0
- apsimo/gate/communication_policy.py +166 -0
- apsimo/gate/config.py +72 -0
- apsimo/gate/context_provenance.py +170 -0
- apsimo/gate/env_risk.py +226 -0
- apsimo/gate/guard_audit.py +353 -0
- apsimo/gate/layers/__init__.py +1 -0
- apsimo/gate/layers/base.py +15 -0
- apsimo/gate/layers/l1_recipient.py +66 -0
- apsimo/gate/layers/l2_pii.py +134 -0
- apsimo/gate/layers/l3_cross_context.py +50 -0
- apsimo/gate/layers/l4_trust_tier.py +78 -0
- apsimo/gate/layers/l5_injection.py +199 -0
- apsimo/gate/layers/l6_review.py +86 -0
- apsimo/gate/layers/l7_delay.py +100 -0
- apsimo/gate/layers/tom2_epistemic.py +185 -0
- apsimo/gate/models.py +64 -0
- apsimo/gate/pending_dispatch.py +5 -0
- apsimo/gate/pipeline.py +206 -0
- apsimo/gate/rejection.py +259 -0
- apsimo/gate/response_guard.py +700 -0
- apsimo/gate/rulesets/injection_v1.yaml +51 -0
- apsimo/gate/surface_policy.py +189 -0
- apsimo/gate/taint.py +226 -0
- apsimo/genesis.json +9 -0
- apsimo/goals/__init__.py +100 -0
- apsimo/goals/config.py +38 -0
- apsimo/goals/decomposer.py +421 -0
- apsimo/goals/engine.py +617 -0
- apsimo/goals/inference.py +354 -0
- apsimo/goals/models.py +302 -0
- apsimo/goals/priority.py +270 -0
- apsimo/goals/queue_bridge.py +149 -0
- apsimo/goals/replan.py +450 -0
- apsimo/goals/schema.sql +89 -0
- apsimo/goals/store.py +692 -0
- apsimo/governed_actions.py +1708 -0
- apsimo/harness_integration/__init__.py +45 -0
- apsimo/harness_integration/context.py +41 -0
- apsimo/harness_integration/skills.py +231 -0
- apsimo/identity/__init__.py +26 -0
- apsimo/identity/participants.py +181 -0
- apsimo/identity/resolver.py +329 -0
- apsimo/identity_bootstrap/__init__.py +5 -0
- apsimo/identity_bootstrap/builder.py +208 -0
- apsimo/identity_bootstrap/corpus.py +443 -0
- apsimo/identity_bootstrap/models.py +54 -0
- apsimo/identity_bootstrap/runner.py +353 -0
- apsimo/identity_bootstrap/seeders/__init__.py +25 -0
- apsimo/identity_bootstrap/seeders/briefings.py +109 -0
- apsimo/identity_bootstrap/seeders/chain.py +57 -0
- apsimo/identity_bootstrap/seeders/goals.py +128 -0
- apsimo/identity_bootstrap/seeders/memory.py +191 -0
- apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
- apsimo/identity_bootstrap/seeders/relationship.py +152 -0
- apsimo/identity_bootstrap/seeders/sessions.py +67 -0
- apsimo/identity_bootstrap/seeders/skills.py +92 -0
- apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
- apsimo/identity_bootstrap/seeders/world_model.py +143 -0
- apsimo/identity_bootstrap/self_query.py +92 -0
- apsimo/identity_bootstrap/self_reflection.py +155 -0
- apsimo/identity_bootstrap/skill.py +37 -0
- apsimo/identity_bootstrap/verifier.py +436 -0
- apsimo/initiatives/__init__.py +20 -0
- apsimo/initiatives/action_registry.py +454 -0
- apsimo/initiatives/approval_authority.py +2105 -0
- apsimo/initiatives/approval_policy.py +123 -0
- apsimo/initiatives/assignment.py +263 -0
- apsimo/initiatives/backup_evidence.py +100 -0
- apsimo/initiatives/context_freshness.py +103 -0
- apsimo/initiatives/models.py +318 -0
- apsimo/initiatives/native_work.py +270 -0
- apsimo/initiatives/standing_approvals.py +232 -0
- apsimo/initiatives/store.py +1081 -0
- apsimo/initiatives/temporal_followup.py +410 -0
- apsimo/intelligence/__init__.py +1 -0
- apsimo/intelligence/cognition/__init__.py +24 -0
- apsimo/intelligence/cognition/gap_detector.py +148 -0
- apsimo/intelligence/cognition/metalearner.py +547 -0
- apsimo/intelligence/cognition/metrics_collector.py +217 -0
- apsimo/intelligence/cognition/performance_index.py +299 -0
- apsimo/intelligence/cognition/registry.py +192 -0
- apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
- apsimo/intelligence/cognition/types.py +16 -0
- apsimo/intelligence/components/__init__.py +66 -0
- apsimo/intelligence/components/anomaly_detector.py +413 -0
- apsimo/intelligence/components/initiative_engine.py +2643 -0
- apsimo/intelligence/components/preference_learner.py +521 -0
- apsimo/intelligence/components/research_orchestrator.py +358 -0
- apsimo/intelligence/components/self_directed_thinker.py +221 -0
- apsimo/intelligence/components/self_reflector.py +252 -0
- apsimo/intelligence/components/session_continuity.py +154 -0
- apsimo/intelligence/components/task_planner.py +320 -0
- apsimo/intelligence/components/tool_learner.py +217 -0
- apsimo/intelligence/graph/__init__.py +79 -0
- apsimo/intelligence/graph/client.py +2483 -0
- apsimo/intelligence/graph/consolidator.py +405 -0
- apsimo/intelligence/graph/distiller.py +312 -0
- apsimo/intelligence/graph/migrations.py +129 -0
- apsimo/intelligence/graph/queries.py +248 -0
- apsimo/intelligence/graph/recall.py +281 -0
- apsimo/intelligence/graph/reconciler.py +144 -0
- apsimo/intelligence/graph/schema.py +337 -0
- apsimo/intelligence/graph/selection.py +252 -0
- apsimo/intelligence/learning/__init__.py +17 -0
- apsimo/intelligence/learning/continuous_learner.py +245 -0
- apsimo/intelligence/learning/feedback_store.py +321 -0
- apsimo/intelligence/mind_model/__init__.py +1 -0
- apsimo/intelligence/mind_model/graph_baseline.py +136 -0
- apsimo/intelligence/mind_model/signal_collector.py +361 -0
- apsimo/intelligence/relationships/__init__.py +11 -0
- apsimo/intelligence/relationships/profiler.py +389 -0
- apsimo/intelligence/relationships/scorer.py +560 -0
- apsimo/intelligence/relationships/signal_floor.py +66 -0
- apsimo/intelligence/relationships/trust_tiers.py +300 -0
- apsimo/intelligence/synthesis/__init__.py +40 -0
- apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
- apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
- apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
- apsimo/intelligence/synthesis/insight_store.py +79 -0
- apsimo/intelligence/synthesis/insight_validator.py +183 -0
- apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
- apsimo/intelligence/turn_middleware/__init__.py +15 -0
- apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
- apsimo/mcp/__init__.py +41 -0
- apsimo/mcp/__main__.py +6 -0
- apsimo/mcp/config.py +287 -0
- apsimo/mcp/server.py +501 -0
- apsimo/migrations.py +187 -0
- apsimo/mining/__init__.py +27 -0
- apsimo/mining/corpus.py +239 -0
- apsimo/mining/escalations.py +289 -0
- apsimo/mining/models.py +169 -0
- apsimo/mining/store.py +210 -0
- apsimo/models/__init__.py +30 -0
- apsimo/models/memory.py +80 -0
- apsimo/models/mesh.py +72 -0
- apsimo/models/person.py +104 -0
- apsimo/models/signal.py +108 -0
- apsimo/observations/__init__.py +15 -0
- apsimo/observations/store.py +277 -0
- apsimo/patterns/__init__.py +6 -0
- apsimo/patterns/extract.py +187 -0
- apsimo/patterns/store.py +227 -0
- apsimo/persona/__init__.py +1 -0
- apsimo/persona/engine.py +611 -0
- apsimo/persona/manifest.py +140 -0
- apsimo/projects/__init__.py +28 -0
- apsimo/projects/engine.py +1681 -0
- apsimo/projects/event_outbox.py +188 -0
- apsimo/projects/models.py +216 -0
- apsimo/projects/planner.py +181 -0
- apsimo/projects/store.py +1446 -0
- apsimo/proposals/__init__.py +12 -0
- apsimo/proposals/engine.py +114 -0
- apsimo/proposals/models.py +207 -0
- apsimo/qualification/__init__.py +1 -0
- apsimo/qualification/cases.py +75 -0
- apsimo/qualification/cli.py +51 -0
- apsimo/qualification/memory_cases.py +209 -0
- apsimo/qualification/records.py +92 -0
- apsimo/qualification/report.py +87 -0
- apsimo/qualification/runner.py +311 -0
- apsimo/qualification/structured_cases.py +131 -0
- apsimo/reasoning/__init__.py +13 -0
- apsimo/reasoning/executor.py +506 -0
- apsimo/reasoning/loop.py +373 -0
- apsimo/reasoning/native_tools/__init__.py +16 -0
- apsimo/reasoning/native_tools/calculate.py +141 -0
- apsimo/reasoning/native_tools/file_ops.py +150 -0
- apsimo/reasoning/native_tools/web_search.py +49 -0
- apsimo/reasoning/tool_policy.py +182 -0
- apsimo/redact/__init__.py +176 -0
- apsimo/repos/__init__.py +5 -0
- apsimo/repos/mirrors.py +204 -0
- apsimo/research/__init__.py +41 -0
- apsimo/research/artifact.py +482 -0
- apsimo/research/gatherer.py +387 -0
- apsimo/research/pipeline.py +513 -0
- apsimo/research/search/__init__.py +7 -0
- apsimo/research/search/base.py +41 -0
- apsimo/research/search/brave.py +59 -0
- apsimo/research/search/cache.py +51 -0
- apsimo/research/search/duckduckgo.py +103 -0
- apsimo/research/search/orchestrator.py +119 -0
- apsimo/research/search/serpapi.py +59 -0
- apsimo/research/search/tavily.py +59 -0
- apsimo/research/synthesizer.py +309 -0
- apsimo/router/__init__.py +30 -0
- apsimo/router/complexity_scorer.py +148 -0
- apsimo/router/endpoints.py +153 -0
- apsimo/router/fallback.py +58 -0
- apsimo/router/functions.py +243 -0
- apsimo/router/native_policy.py +52 -0
- apsimo/router/router.py +762 -0
- apsimo/router/self_learning.py +174 -0
- apsimo/router/tiers.py +677 -0
- apsimo/sandbox/__init__.py +21 -0
- apsimo/sandbox/backend.py +195 -0
- apsimo/sandbox/manager.py +173 -0
- apsimo/scope_bounds.py +7 -0
- apsimo/secrets/__init__.py +6 -0
- apsimo/secrets/backends/__init__.py +8 -0
- apsimo/secrets/backends/base.py +42 -0
- apsimo/secrets/backends/env.py +110 -0
- apsimo/secrets/backends/keyring.py +72 -0
- apsimo/secrets/backends/onepassword.py +232 -0
- apsimo/secrets/cli.py +191 -0
- apsimo/secrets/manager.py +160 -0
- apsimo/secrets/migration.py +101 -0
- apsimo/secrets/types.py +98 -0
- apsimo/seed.py +41 -0
- apsimo/self_model/__init__.py +37 -0
- apsimo/self_model/appraisals.py +673 -0
- apsimo/self_model/benchmark.py +1314 -0
- apsimo/self_model/brief.py +40 -0
- apsimo/self_model/event_concerns.py +1128 -0
- apsimo/self_model/execution_forecasts.py +353 -0
- apsimo/self_model/expectations.py +1595 -0
- apsimo/self_model/experiments.py +1150 -0
- apsimo/self_model/journal.py +148 -0
- apsimo/self_model/judgments.py +705 -0
- apsimo/self_model/native_outcomes.py +55 -0
- apsimo/self_model/params.py +220 -0
- apsimo/self_model/perspective.py +246 -0
- apsimo/self_model/reconcile.py +183 -0
- apsimo/self_model/reply_forecasts.py +381 -0
- apsimo/self_model/runtime_forecasts.py +296 -0
- apsimo/self_model/runtime_models.py +67 -0
- apsimo/self_model/settlement.py +207 -0
- apsimo/self_model/situation.py +1731 -0
- apsimo/self_model/store.py +883 -0
- apsimo/self_model/supervised.py +137 -0
- apsimo/self_model/thinker.py +99 -0
- apsimo/self_model/trust.py +388 -0
- apsimo/self_model/workspace.py +2388 -0
- apsimo/server.py +4197 -0
- apsimo/services/__init__.py +1 -0
- apsimo/services/agent_bridge.py +474 -0
- apsimo/services/initiative_executor.py +914 -0
- apsimo/services/instance.py +297 -0
- apsimo/sessions/__init__.py +22 -0
- apsimo/sessions/config.py +13 -0
- apsimo/sessions/context_loader.py +88 -0
- apsimo/sessions/federation_session.py +75 -0
- apsimo/sessions/isolated_session.py +98 -0
- apsimo/sessions/reports.py +84 -0
- apsimo/sessions/store.py +148 -0
- apsimo/setup.py +2818 -0
- apsimo/setup_hermes.py +879 -0
- apsimo/setup_local_work.py +218 -0
- apsimo/setup_native_goals.py +134 -0
- apsimo/setup_native_reviews.py +115 -0
- apsimo/skills/__init__.py +10 -0
- apsimo/skills/base.py +108 -0
- apsimo/skills/budget.py +28 -0
- apsimo/skills/executor.py +493 -0
- apsimo/skills/executors/__init__.py +1 -0
- apsimo/skills/executors/behavioral_correction.py +75 -0
- apsimo/skills/executors/capability_gap.py +38 -0
- apsimo/skills/executors/data_quality.py +163 -0
- apsimo/skills/executors/knowledge_acquisition.py +41 -0
- apsimo/skills/executors/operational_hygiene.py +185 -0
- apsimo/skills/executors/subsystem_health.py +169 -0
- apsimo/skills/hermes_export.py +431 -0
- apsimo/skills/index.py +123 -0
- apsimo/skills/learning/__init__.py +21 -0
- apsimo/skills/learning/novelty_detector.py +206 -0
- apsimo/skills/learning/pattern_extractor.py +199 -0
- apsimo/skills/learning/triggers.py +159 -0
- apsimo/skills/loader.py +246 -0
- apsimo/skills/migrations/002_progressive_loading.sql +6 -0
- apsimo/skills/migrations/backfill_triggers.py +20 -0
- apsimo/skills/models.py +202 -0
- apsimo/skills/packager.py +128 -0
- apsimo/skills/protocols.py +70 -0
- apsimo/skills/registry.py +191 -0
- apsimo/skills/runtime.py +58 -0
- apsimo/skills/sandbox_runner.py +229 -0
- apsimo/skills/scheduler.py +129 -0
- apsimo/skills/schema.py +79 -0
- apsimo/skills/security/__init__.py +12 -0
- apsimo/skills/security/guards.py +53 -0
- apsimo/skills/security/scanner.py +223 -0
- apsimo/skills_memory/__init__.py +26 -0
- apsimo/skills_memory/distill.py +159 -0
- apsimo/skills_memory/models.py +85 -0
- apsimo/skills_memory/retrieve.py +62 -0
- apsimo/skills_memory/store.py +172 -0
- apsimo/surprise/__init__.py +6 -0
- apsimo/surprise/accumulation.py +57 -0
- apsimo/surprise/scorer.py +102 -0
- apsimo/surprise/store.py +203 -0
- apsimo/task_queue/__init__.py +69 -0
- apsimo/task_queue/action_receipts.py +148 -0
- apsimo/task_queue/approval_relay_canary.py +108 -0
- apsimo/task_queue/config.py +85 -0
- apsimo/task_queue/contract.py +361 -0
- apsimo/task_queue/events.py +130 -0
- apsimo/task_queue/governor.py +1031 -0
- apsimo/task_queue/handlers/__init__.py +16 -0
- apsimo/task_queue/handlers/base.py +37 -0
- apsimo/task_queue/handlers/inference.py +640 -0
- apsimo/task_queue/handlers/monitoring.py +116 -0
- apsimo/task_queue/handlers/registry.py +75 -0
- apsimo/task_queue/handlers/subtask_handler.py +173 -0
- apsimo/task_queue/handlers/system_maintenance.py +147 -0
- apsimo/task_queue/mesh_integration.py +111 -0
- apsimo/task_queue/models.py +317 -0
- apsimo/task_queue/queue_manager.py +8286 -0
- apsimo/task_queue/routing.py +287 -0
- apsimo/task_queue/scheduler.py +252 -0
- apsimo/task_queue/schema.sql +197 -0
- apsimo/task_queue/work_control.py +342 -0
- apsimo/task_queue/worker.py +993 -0
- apsimo/telemetry.py +145 -0
- apsimo/tom/__init__.py +6 -0
- apsimo/tom/affect.py +387 -0
- apsimo/tom/approvals.py +171 -0
- apsimo/tom/arcs.py +896 -0
- apsimo/tom/asymmetry.py +131 -0
- apsimo/tom/eligibility.py +248 -0
- apsimo/tom/engagement.py +214 -0
- apsimo/tom/exposure.py +214 -0
- apsimo/tom/extractor.py +306 -0
- apsimo/tom/fact_adapters.py +144 -0
- apsimo/tom/facts.py +326 -0
- apsimo/tom/integration.py +592 -0
- apsimo/tom/leveled.py +118 -0
- apsimo/tom/levels.py +247 -0
- apsimo/tom/recipient_audit.py +995 -0
- apsimo/tom/recipient_simulator.py +593 -0
- apsimo/tom/source_lineage.py +93 -0
- apsimo/tom/tom2.py +277 -0
- apsimo/tom/visibility.py +559 -0
- apsimo/tom/visibility_store.py +414 -0
- apsimo/tools/__init__.py +0 -0
- apsimo/tools/definitions.py +740 -0
- apsimo/tools/handlers.py +943 -0
- apsimo/toolsmith/__init__.py +26 -0
- apsimo/toolsmith/authority.py +166 -0
- apsimo/toolsmith/engine.py +559 -0
- apsimo/toolsmith/integrity.py +100 -0
- apsimo/toolsmith/miner.py +145 -0
- apsimo/toolsmith/policy.py +110 -0
- apsimo/toolsmith/registry.py +635 -0
- apsimo/turns/__init__.py +17 -0
- apsimo/turns/audio.py +134 -0
- apsimo/turns/documents.py +235 -0
- apsimo/turns/executions.py +486 -0
- apsimo/turns/hermes_history.py +245 -0
- apsimo/turns/hermes_kanban.py +268 -0
- apsimo/turns/hermes_work.py +96 -0
- apsimo/turns/idempotency.py +752 -0
- apsimo/turns/local_work.py +115 -0
- apsimo/turns/media.py +581 -0
- apsimo/turns/reported_workers.py +196 -0
- apsimo/turns/source_annotations.py +283 -0
- apsimo/turns/source_attribution.py +154 -0
- apsimo/turns/source_read.py +351 -0
- apsimo/turns/source_vectors.py +263 -0
- apsimo/turns/video.py +210 -0
- apsimo/util/autonomy_preset.py +220 -0
- apsimo/util/instance.py +92 -0
- apsimo/util/model_output.py +25 -0
- apsimo/util/quiet_hours.py +27 -0
- apsimo/util/session_safety.py +37 -0
- apsimo/util/temporal.py +343 -0
- apsimo/vector/__init__.py +75 -0
- apsimo/vector/backfill.py +171 -0
- apsimo/vector/caption.py +114 -0
- apsimo/vector/collections.py +51 -0
- apsimo/vector/config.py +102 -0
- apsimo/vector/embedder.py +670 -0
- apsimo/vector/image_preprocess.py +406 -0
- apsimo/vector/image_store.py +296 -0
- apsimo/vector/indexes.py +162 -0
- apsimo/vector/migrate.py +334 -0
- apsimo/vector/multimodal_provider.py +417 -0
- apsimo/vector/multimodal_types.py +87 -0
- apsimo/vector/openai_provider.py +119 -0
- apsimo/vector/query.py +49 -0
- apsimo/vector/reranker.py +565 -0
- apsimo/vector/safety_image.py +159 -0
- apsimo/vector/scanner.py +197 -0
- apsimo/vector/setup.py +289 -0
- apsimo/vector/store.py +533 -0
- apsimo/vector/tiers.py +263 -0
- apsimo/work_orders.py +925 -0
- apsimo/workers/__init__.py +21 -0
- apsimo/workers/agent_bridge.py +640 -0
- apsimo/workers/colony_worker.py +382 -0
- apsimo/workers/queue_worker.py +441 -0
- apsimo/workers/skills_sync.py +152 -0
- apsimo/world_model/__init__.py +71 -0
- apsimo/world_model/causal_maintenance.py +131 -0
- apsimo/world_model/causal_policy.py +43 -0
- apsimo/world_model/causal_query.py +125 -0
- apsimo/world_model/confidence.py +54 -0
- apsimo/world_model/config.py +64 -0
- apsimo/world_model/constants.py +97 -0
- apsimo/world_model/entities.py +145 -0
- apsimo/world_model/expectation_resolvers.py +177 -0
- apsimo/world_model/extraction/__init__.py +7 -0
- apsimo/world_model/extraction/base.py +62 -0
- apsimo/world_model/extraction/conversation_extractor.py +262 -0
- apsimo/world_model/extraction/detector.py +74 -0
- apsimo/world_model/extraction/document_extractor.py +78 -0
- apsimo/world_model/extraction/formats/__init__.py +24 -0
- apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
- apsimo/world_model/extraction/formats/html_fmt.py +72 -0
- apsimo/world_model/extraction/formats/json_fmt.py +68 -0
- apsimo/world_model/extraction/formats/pdf.py +43 -0
- apsimo/world_model/extraction/formats/text.py +27 -0
- apsimo/world_model/extraction/llm_extractor.py +164 -0
- apsimo/world_model/extraction/pipeline.py +73 -0
- apsimo/world_model/integrations/__init__.py +5 -0
- apsimo/world_model/integrations/mind_model_bridge.py +115 -0
- apsimo/world_model/integrations/social_intel_bridge.py +120 -0
- apsimo/world_model/jobs/__init__.py +4 -0
- apsimo/world_model/jobs/extraction_job.py +168 -0
- apsimo/world_model/llm_extract.py +572 -0
- apsimo/world_model/neo4j/__init__.py +5 -0
- apsimo/world_model/neo4j/backend.py +654 -0
- apsimo/world_model/observations.py +155 -0
- apsimo/world_model/populator.py +307 -0
- apsimo/world_model/postgres/__init__.py +1 -0
- apsimo/world_model/postgres/backend.py +683 -0
- apsimo/world_model/relationships.py +25 -0
- apsimo/world_model/resolution/__init__.py +13 -0
- apsimo/world_model/resolution/entity_resolver.py +232 -0
- apsimo/world_model/resolution/merge_audit.py +16 -0
- apsimo/world_model/resolution/merge_workflow.py +117 -0
- apsimo/world_model/source_reports.py +121 -0
- apsimo/world_model/sqlite/__init__.py +4 -0
- apsimo/world_model/sqlite/backend.py +855 -0
- apsimo/world_model/sqlite/schema.sql +132 -0
- apsimo/world_model/store.py +545 -0
- apsimo-1.3.0.dist-info/METADATA +78 -0
- apsimo-1.3.0.dist-info/RECORD +614 -0
- apsimo-1.3.0.dist-info/WHEEL +5 -0
- apsimo-1.3.0.dist-info/entry_points.txt +11 -0
- apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
- apsimo-1.3.0.dist-info/top_level.txt +2 -0
- colony_sidecar/__init__.py +4 -0
apsimo/setup_hermes.py
ADDED
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
"""The guided Hermes path of ``apsimo init``.
|
|
2
|
+
|
|
3
|
+
Use canonical adapter resources and one private instance. This is an installer,
|
|
4
|
+
not a release controller: existing instances are retained and runtime upgrades
|
|
5
|
+
remain explicit. No model, container, OS service or Hermes core is downloaded.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import getpass
|
|
11
|
+
import hashlib
|
|
12
|
+
import importlib.metadata
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
import re
|
|
17
|
+
import secrets
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
import types
|
|
23
|
+
from urllib.parse import urlsplit
|
|
24
|
+
import zipfile
|
|
25
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
26
|
+
|
|
27
|
+
import httpx
|
|
28
|
+
import yaml
|
|
29
|
+
|
|
30
|
+
from .environment import apply_environment_aliases, clear_environment_aliases, normalize_environment
|
|
31
|
+
from .util.instance import plugin_settings
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _select_state_environment(state):
|
|
35
|
+
clear_environment_aliases()
|
|
36
|
+
# Continue an explicitly canonical selection, but do not create canonical
|
|
37
|
+
# input variables for a legacy caller. The remaining CLI bootstrap reads
|
|
38
|
+
# COLONY_STATE_DIR; minting its reverse alias would pin a stale state over
|
|
39
|
+
# that caller's later configuration selection.
|
|
40
|
+
if 'APSIMO_STATE_DIR' in os.environ:
|
|
41
|
+
os.environ.pop('COLONY_STATE_DIR', None)
|
|
42
|
+
os.environ['APSIMO_STATE_DIR'] = str(state)
|
|
43
|
+
else:
|
|
44
|
+
os.environ['COLONY_STATE_DIR'] = str(state)
|
|
45
|
+
apply_environment_aliases()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _private_write(path, content):
|
|
49
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
50
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
51
|
+
with os.fdopen(fd, 'wb') as stream:
|
|
52
|
+
stream.write(content if isinstance(content, bytes) else content.encode())
|
|
53
|
+
stream.flush()
|
|
54
|
+
os.fsync(stream.fileno())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _json(value):
|
|
58
|
+
return json.dumps(value, indent=2, ensure_ascii=False) + '\n'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _receipt_preference(config, choice):
|
|
62
|
+
"""Project one native shared key, respecting Hermes' existing YAML layouts.
|
|
63
|
+
|
|
64
|
+
Shared-key bridging chooses whatsapp, gateway.platforms.whatsapp, then
|
|
65
|
+
platforms.whatsapp. Nested extra maps are merged separately. Synchronize
|
|
66
|
+
existing aliases so an older spelling cannot override the explicit choice.
|
|
67
|
+
Copy each touched mapping to avoid changing unrelated YAML anchor aliases.
|
|
68
|
+
"""
|
|
69
|
+
if choice not in {'on', 'off'}:
|
|
70
|
+
raise ValueError('WhatsApp read receipts must be on or off')
|
|
71
|
+
candidate = dict(config)
|
|
72
|
+
key, enabled, changes = 'send_read_receipts', choice == 'on', []
|
|
73
|
+
|
|
74
|
+
def mapping(parent, name, create=False):
|
|
75
|
+
if name not in parent and not create:
|
|
76
|
+
return None
|
|
77
|
+
value = parent.get(name, {})
|
|
78
|
+
if not isinstance(value, dict):
|
|
79
|
+
raise ValueError('WhatsApp configuration sections and extra settings must be mappings')
|
|
80
|
+
parent[name] = dict(value)
|
|
81
|
+
return parent[name]
|
|
82
|
+
|
|
83
|
+
slots = []
|
|
84
|
+
for path, merged in [(('whatsapp',), False),
|
|
85
|
+
(('gateway', 'platforms', 'whatsapp'), True),
|
|
86
|
+
(('platforms', 'whatsapp'), True),
|
|
87
|
+
(('gateway', 'whatsapp'), True)]:
|
|
88
|
+
node = candidate
|
|
89
|
+
for part in path:
|
|
90
|
+
node = mapping(node, part)
|
|
91
|
+
if node is None:
|
|
92
|
+
break
|
|
93
|
+
if node is not None:
|
|
94
|
+
extra = mapping(node, 'extra')
|
|
95
|
+
slots.append((path, node, extra, merged))
|
|
96
|
+
chosen = next((row for row in slots if row[0] != ('gateway', 'whatsapp')), None)
|
|
97
|
+
effective = bool(chosen and key in chosen[1]) or any(
|
|
98
|
+
merged and extra is not None and key in extra for _, _, extra, merged in slots)
|
|
99
|
+
# Stock Hermes treats any merged extras as connection configuration. Do not
|
|
100
|
+
# introduce that signal when channel selection exists only outside YAML.
|
|
101
|
+
explicit_selection = any('enabled' in node for _, node, _, merged in slots
|
|
102
|
+
if merged or node is (chosen[1] if chosen else None))
|
|
103
|
+
existing_extras = any(merged and extra for _, _, extra, merged in slots)
|
|
104
|
+
if not (explicit_selection or effective or existing_extras):
|
|
105
|
+
raise ValueError('Select WhatsApp enabled: true or enabled: false explicitly in Hermes config.yaml before adding a receipt preference')
|
|
106
|
+
|
|
107
|
+
def assign(node, path):
|
|
108
|
+
if node.get(key) is not enabled:
|
|
109
|
+
node[key] = enabled
|
|
110
|
+
changes.append('.'.join((*path, key)))
|
|
111
|
+
|
|
112
|
+
for path, node, extra, _ in slots:
|
|
113
|
+
if key in node:
|
|
114
|
+
assign(node, path)
|
|
115
|
+
if extra is not None and key in extra:
|
|
116
|
+
assign(extra, (*path, 'extra'))
|
|
117
|
+
if not effective:
|
|
118
|
+
if chosen:
|
|
119
|
+
assign(chosen[1], chosen[0])
|
|
120
|
+
elif slots: # gateway.whatsapp.extra is merged, its direct keys are not.
|
|
121
|
+
path, node, _, _ = slots[0]
|
|
122
|
+
assign(mapping(node, 'extra', True), (*path, 'extra'))
|
|
123
|
+
else:
|
|
124
|
+
platforms = mapping(candidate, 'platforms', True)
|
|
125
|
+
whatsapp = mapping(platforms, 'whatsapp', True)
|
|
126
|
+
assign(mapping(whatsapp, 'extra', True), ('platforms', 'whatsapp', 'extra'))
|
|
127
|
+
return candidate, changes
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _write_receipt_preference(home, choice, preview=False):
|
|
131
|
+
from . import setup
|
|
132
|
+
path = home/'config.yaml'
|
|
133
|
+
original, config = setup._read_hermes_config(path)
|
|
134
|
+
if original is None:
|
|
135
|
+
raise ValueError('Preference updates require an existing Hermes config.yaml')
|
|
136
|
+
candidate, changes = _receipt_preference(config, choice)
|
|
137
|
+
if not preview and changes:
|
|
138
|
+
setup._atomic_hermes_config_write(path, original,
|
|
139
|
+
yaml.safe_dump(candidate, sort_keys=False, allow_unicode=True).encode())
|
|
140
|
+
print(('Preview' if preview else 'Configured') + ': WhatsApp read receipts ' + choice)
|
|
141
|
+
print('Changed preference paths: ' + (', '.join(changes) if changes else 'none'))
|
|
142
|
+
if not preview:
|
|
143
|
+
print('Use the selected gateway lifecycle to reconnect and load this setting; no process was restarted.')
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _agent_preferences(ask, args, config):
|
|
147
|
+
"""Private identity and time preferences; no automatic permission grants."""
|
|
148
|
+
value_text = ask('Guiding values (comma-separated, optional)',
|
|
149
|
+
getattr(args, 'agent_values', None) or '')
|
|
150
|
+
values = list(dict.fromkeys(item.strip() for item in value_text.split(',') if item.strip()))
|
|
151
|
+
if len(values) > 12 or any(len(value) > 120 for value in values):
|
|
152
|
+
raise ValueError('Use at most twelve guiding values, each at most 120 characters')
|
|
153
|
+
timezone_name = ask('Timezone for deadlines and follow-ups',
|
|
154
|
+
getattr(args, 'timezone', None) or config.get('timezone') or 'UTC', True)
|
|
155
|
+
try:
|
|
156
|
+
ZoneInfo(timezone_name)
|
|
157
|
+
except (ZoneInfoNotFoundError, ValueError):
|
|
158
|
+
raise ValueError('Use a named timezone such as UTC or Europe/Paris') from None
|
|
159
|
+
quiet = ask('Quiet hours for optional follow-ups (HH:MM-HH:MM, optional)',
|
|
160
|
+
getattr(args, 'quiet_hours', None) or '')
|
|
161
|
+
if quiet:
|
|
162
|
+
times = quiet.split('-')
|
|
163
|
+
valid_time = re.compile(r'^(?:[01][0-9]|2[0-3]):[0-5][0-9]$')
|
|
164
|
+
if len(times) != 2 or not all(valid_time.fullmatch(value) for value in times) or times[0] == times[1]:
|
|
165
|
+
raise ValueError('Quiet hours require two different times, HH:MM-HH:MM')
|
|
166
|
+
return {'values': values, 'timezone': timezone_name, 'quiet_hours': quiet}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _endpoint(value):
|
|
170
|
+
try:
|
|
171
|
+
parsed = urlsplit(value)
|
|
172
|
+
port = parsed.port
|
|
173
|
+
if (parsed.scheme not in {'http', 'https'} or not parsed.hostname or parsed.username
|
|
174
|
+
or parsed.password or parsed.query or parsed.fragment or port == 0):
|
|
175
|
+
raise ValueError()
|
|
176
|
+
except (ValueError, TypeError):
|
|
177
|
+
raise ValueError('Use an HTTP(S) API root with a valid port and no embedded credential') from None
|
|
178
|
+
return value.rstrip('/') + ('/v1' if not parsed.path.strip('/') else '')
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _verify_local_endpoint(endpoint):
|
|
182
|
+
"""Declare the selected host and reuse the runtime's address check."""
|
|
183
|
+
from apsimo.router.router import LLMRouter
|
|
184
|
+
hosts = [urlsplit(endpoint).hostname]
|
|
185
|
+
router = LLMRouter(tiers={}, self_learner=object())
|
|
186
|
+
router.configure({'provider': 'local', 'baseUrl': endpoint, 'apiKey': 'local-no-key',
|
|
187
|
+
'models': {'small': 'setup-endpoint-check'}, 'localHosts': hosts})
|
|
188
|
+
if not asyncio.run(router._local_addresses(router._snapshot, router._snapshot.bindings['small'])):
|
|
189
|
+
raise ValueError('The selected model endpoint must resolve to the configured local networks')
|
|
190
|
+
return hosts
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _interpreter(candidate):
|
|
194
|
+
if not candidate:
|
|
195
|
+
hermes = shutil.which('hermes')
|
|
196
|
+
if hermes:
|
|
197
|
+
first = Path(hermes).read_text().splitlines()[0]
|
|
198
|
+
if first.startswith('#!/') and ' ' not in first[2:]:
|
|
199
|
+
candidate = first[2:]
|
|
200
|
+
if not candidate:
|
|
201
|
+
candidate = sys.executable
|
|
202
|
+
python = Path(candidate).expanduser().absolute()
|
|
203
|
+
probe = subprocess.run([str(python), '-I', '-c',
|
|
204
|
+
'import importlib.metadata,json; import httpx,httpcore,yaml; '
|
|
205
|
+
'from agent.memory_manager import MemoryManager; '
|
|
206
|
+
'from hermes_cli.plugins import get_plugin_manager; '
|
|
207
|
+
'print(json.dumps({"version":importlib.metadata.version("hermes-agent")}))'],
|
|
208
|
+
capture_output=True, text=True, timeout=30)
|
|
209
|
+
if probe.returncode or json.loads(probe.stdout.splitlines()[-1]).get('version') not in {'0.21.0', '0.21.1'}:
|
|
210
|
+
raise ValueError('Select the Python interpreter of Hermes 0.21.0 or 0.21.1 with its native dependencies installed')
|
|
211
|
+
return python
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _profile_homes(python):
|
|
215
|
+
"""Ask the selected native runtime for live profile paths, without reading their config."""
|
|
216
|
+
probe = subprocess.run([str(python), '-I', '-B', '-c',
|
|
217
|
+
'import json; from hermes_cli.profiles import list_profile_names,profile_exists,get_profile_dir; '
|
|
218
|
+
'print(json.dumps([{ "name":name,"path":str(get_profile_dir(name))} '
|
|
219
|
+
'for name in list_profile_names() if profile_exists(name) '
|
|
220
|
+
'and (get_profile_dir(name)/"config.yaml").is_file()]))'],
|
|
221
|
+
capture_output=True, text=True, timeout=10)
|
|
222
|
+
try:
|
|
223
|
+
rows = json.loads(probe.stdout.splitlines()[-1]) if probe.returncode == 0 else None
|
|
224
|
+
if not isinstance(rows, list):
|
|
225
|
+
raise ValueError()
|
|
226
|
+
result = []
|
|
227
|
+
for row in rows:
|
|
228
|
+
if not isinstance(row, dict) or set(row) != {'name', 'path'}:
|
|
229
|
+
raise ValueError()
|
|
230
|
+
if not all(isinstance(row[key], str) and row[key] and not any(ord(c) < 32 for c in row[key])
|
|
231
|
+
for key in ('name', 'path')) or not Path(row['path']).is_absolute():
|
|
232
|
+
raise ValueError()
|
|
233
|
+
path = Path(row['path']).resolve()
|
|
234
|
+
if path not in [item['path'] for item in result]:
|
|
235
|
+
result.append({'name': row['name'], 'path': path})
|
|
236
|
+
return result
|
|
237
|
+
except (ValueError, IndexError, TypeError):
|
|
238
|
+
raise ValueError('Could not list native Hermes profiles; select one with --hermes-home') from None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _select_home(args, ask):
|
|
242
|
+
from apsimo import setup
|
|
243
|
+
explicit = getattr(args, 'hermes_home', None) or os.environ.get('HERMES_HOME')
|
|
244
|
+
if explicit or getattr(args, 'non_interactive', False):
|
|
245
|
+
return setup._resolve_hermes_home(explicit), None
|
|
246
|
+
python = _interpreter(getattr(args, 'hermes_python', None))
|
|
247
|
+
homes = _profile_homes(python)
|
|
248
|
+
if homes:
|
|
249
|
+
print('Existing Hermes profiles:')
|
|
250
|
+
for row in homes:
|
|
251
|
+
print(f" {row['name']}: {row['path']}")
|
|
252
|
+
default = str(homes[0]['path']) if homes else str(setup._resolve_hermes_home())
|
|
253
|
+
selected = ask('Hermes home to attach (one profile)', default, True)
|
|
254
|
+
return setup._resolve_hermes_home(selected), python
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _adapter_resources(wheel=None):
|
|
258
|
+
packages = ('apsimo_hermes/', 'apsimo_memory/', 'colony_hermes/', 'colony_memory/')
|
|
259
|
+
if wheel:
|
|
260
|
+
with zipfile.ZipFile(wheel) as archive:
|
|
261
|
+
resources = {name: archive.read(name) for name in archive.namelist()
|
|
262
|
+
if name.startswith(packages) and not name.endswith('/') and '__pycache__' not in name}
|
|
263
|
+
else:
|
|
264
|
+
try:
|
|
265
|
+
distribution = importlib.metadata.distribution('apsimo-hermes')
|
|
266
|
+
except importlib.metadata.PackageNotFoundError:
|
|
267
|
+
raise ValueError('Install the canonical apsimo-hermes package alongside apsimo, or supply --adapter-wheel') from None
|
|
268
|
+
resources = {str(path): distribution.locate_file(path).read_bytes() for path in distribution.files or []
|
|
269
|
+
if str(path).startswith(packages) and '__pycache__' not in str(path)}
|
|
270
|
+
if any(Path(name).is_absolute() or '..' in Path(name).parts for name in resources):
|
|
271
|
+
raise ValueError('Adapter resource escapes its package')
|
|
272
|
+
for name in ('apsimo_hermes/__init__.py', 'apsimo_hermes/evidence.py', 'apsimo_hermes/client.py',
|
|
273
|
+
'apsimo_hermes/commitment_work.py', 'apsimo_memory/__init__.py', 'apsimo_memory/provider.py'):
|
|
274
|
+
if name not in resources:
|
|
275
|
+
raise ValueError('Canonical adapter artifact is incomplete')
|
|
276
|
+
return resources
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _preflight_outbox(home, resources):
|
|
280
|
+
"""Check the selected runtime's existing path rules without creating state.
|
|
281
|
+
|
|
282
|
+
The adapter may only be supplied as a wheel, so inspect that exact client
|
|
283
|
+
resource rather than importing a possibly different installed adapter.
|
|
284
|
+
Runtime descriptor checks still enforce the boundary when the file opens.
|
|
285
|
+
"""
|
|
286
|
+
name = '_colony_setup_pathcheck'
|
|
287
|
+
module = types.ModuleType(name)
|
|
288
|
+
sys.modules[name] = module
|
|
289
|
+
selected = home/'state'/'colony-turn-outbox.sqlite3'
|
|
290
|
+
inspected = selected
|
|
291
|
+
try:
|
|
292
|
+
exec(compile(resources['apsimo_hermes/client.py'], '<canonical Apsimo client>', 'exec'), module.__dict__)
|
|
293
|
+
boundary = module.PrivateSQLitePath(selected)
|
|
294
|
+
for inspected in reversed(selected.parents):
|
|
295
|
+
try:
|
|
296
|
+
value = inspected.lstat()
|
|
297
|
+
except FileNotFoundError:
|
|
298
|
+
continue # The installer/runtime creates missing directories privately.
|
|
299
|
+
boundary._validate_directory(value, private_parent=inspected == selected.parent,
|
|
300
|
+
label='ancestor')
|
|
301
|
+
inspected = selected
|
|
302
|
+
if selected.exists() or selected.is_symlink():
|
|
303
|
+
boundary._validate_leaf(selected.lstat())
|
|
304
|
+
except module.PrivateSQLitePathError as error:
|
|
305
|
+
raise ValueError(f'Cannot use the Hermes conversation outbox at {inspected}: {error}. '
|
|
306
|
+
'Choose --hermes-home beneath a directory you own without group/other write access; '
|
|
307
|
+
'an existing state directory must be mode 0700. No permissions were changed.') from None
|
|
308
|
+
finally:
|
|
309
|
+
sys.modules.pop(name, None)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _adapter_binding(python, resources):
|
|
313
|
+
"""Respect native entry-point precedence, verifying the selected code first."""
|
|
314
|
+
expected = {name: hashlib.sha256(value).hexdigest() for name, value in resources.items()}
|
|
315
|
+
probe = subprocess.run([str(python), '-I', '-c', r'''
|
|
316
|
+
import hashlib, importlib.metadata, importlib.util, json, sys
|
|
317
|
+
from pathlib import Path
|
|
318
|
+
expected = json.load(sys.stdin)
|
|
319
|
+
entries = importlib.metadata.entry_points()
|
|
320
|
+
aliases = []
|
|
321
|
+
for group, name, target in [('hermes_agent.plugins', 'colony', 'apsimo_hermes'),
|
|
322
|
+
('hermes_agent.memory_providers', 'colony-memory', 'apsimo_memory')]:
|
|
323
|
+
matches = [ep for ep in entries.select(group=group) if ep.name == name]
|
|
324
|
+
if len(matches) > 1 or any(ep.value != target for ep in matches):
|
|
325
|
+
raise ValueError('Conflicting legacy adapter distribution or target')
|
|
326
|
+
aliases.extend(matches)
|
|
327
|
+
selected = []
|
|
328
|
+
for group, name, module in [('hermes_agent.plugins', 'apsimo', 'apsimo_hermes'),
|
|
329
|
+
('hermes_agent.memory_providers', 'apsimo-memory', 'apsimo_memory')]:
|
|
330
|
+
matches = [ep for ep in entries.select(group=group) if ep.name == name]
|
|
331
|
+
if len(matches) > 1 or (matches and matches[0].value != module):
|
|
332
|
+
raise ValueError('Conflicting Apsimo entry point')
|
|
333
|
+
selected.append(matches[0] if matches else None)
|
|
334
|
+
if not any(selected) and not aliases:
|
|
335
|
+
print(json.dumps({'mode': 'private-directory'}))
|
|
336
|
+
sys.exit(0)
|
|
337
|
+
if not all(selected):
|
|
338
|
+
raise ValueError('Both canonical native Apsimo entry points are required')
|
|
339
|
+
identities = {(ep.dist.metadata['Name'].lower().replace('_', '-'), ep.dist.version) for ep in [*selected, *aliases]}
|
|
340
|
+
if len(identities) != 1:
|
|
341
|
+
raise ValueError('Canonical and legacy adapter registrations come from different distributions')
|
|
342
|
+
sources, external_modules, versions = {}, {}, set()
|
|
343
|
+
for ep in selected:
|
|
344
|
+
module = ep.value
|
|
345
|
+
spec = importlib.util.find_spec(module)
|
|
346
|
+
if spec is None or not spec.origin:
|
|
347
|
+
raise ValueError('Installed adapter package cannot be resolved')
|
|
348
|
+
root = Path(spec.origin).resolve().parent
|
|
349
|
+
sources[module] = str(root)
|
|
350
|
+
versions.add(ep.dist.version)
|
|
351
|
+
names = {name for name in expected if name.startswith(module+'/')}
|
|
352
|
+
# Editable installs include source-only ops/examples and resolve the two
|
|
353
|
+
# shared catalog modules through the existing hostworker namespace bridge.
|
|
354
|
+
# Verify every packaged module at the path Python will actually import.
|
|
355
|
+
for name in sorted(names):
|
|
356
|
+
path = root/name.split('/', 1)[1]
|
|
357
|
+
if name.endswith('.py'):
|
|
358
|
+
qualified = name[:-3].replace('/', '.')
|
|
359
|
+
if qualified.endswith('.__init__'):
|
|
360
|
+
qualified = qualified[:-9]
|
|
361
|
+
resolved = importlib.util.find_spec(qualified)
|
|
362
|
+
if resolved is None or not resolved.origin:
|
|
363
|
+
raise ValueError('Installed adapter module cannot be resolved')
|
|
364
|
+
path = Path(resolved.origin).resolve()
|
|
365
|
+
if not path.is_relative_to(root):
|
|
366
|
+
external_modules[qualified] = str(path)
|
|
367
|
+
if not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest() != expected[name]:
|
|
368
|
+
raise ValueError('Installed adapter bytes differ from the selected artifact')
|
|
369
|
+
if len(versions) != 1:
|
|
370
|
+
raise ValueError('Installed adapter entry-point versions disagree')
|
|
371
|
+
print(json.dumps({'mode': 'native-installed', 'version': versions.pop(),
|
|
372
|
+
'sources': sources, 'external_modules': external_modules}))
|
|
373
|
+
'''], input=json.dumps(expected), capture_output=True, text=True, timeout=30)
|
|
374
|
+
if probe.returncode:
|
|
375
|
+
raise ValueError('Hermes has an incomplete or different installed Apsimo adapter; select its matching artifact, upgrade that package explicitly, or use a separate Hermes interpreter')
|
|
376
|
+
return json.loads(probe.stdout.splitlines()[-1])
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _forwarder(site, module, memory=False):
|
|
380
|
+
# Native Hermes memory discovery is import-free and looks for this symbol.
|
|
381
|
+
hint = '# ColonyMemoryProvider native provider entry point.\n' if memory else ''
|
|
382
|
+
return hint + ('import importlib as _importlib\nimport sys as _sys\n'
|
|
383
|
+
f'_site = {str(site)!r}\n'
|
|
384
|
+
'if _site not in _sys.path:\n _sys.path.insert(0, _site)\n'
|
|
385
|
+
f'_implementation = _importlib.import_module({module!r})\n'
|
|
386
|
+
'for _name, _value in vars(_implementation).items():\n'
|
|
387
|
+
' if not _name.startswith("__") or _name in ("__all__", "__doc__"):\n'
|
|
388
|
+
' globals()[_name] = _value\n')
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _native_environment(original, values):
|
|
392
|
+
# Preserve every unrelated line and refuse to replace an existing secret.
|
|
393
|
+
text = original.decode() if original is not None else ''
|
|
394
|
+
for name, value in values.items():
|
|
395
|
+
aliases = (name, ('COLONY_' if name.startswith('APSIMO_') else 'APSIMO_') + name[7:]) if name.startswith(('COLONY_', 'APSIMO_')) else (name,)
|
|
396
|
+
if any(re.search(r'^\s*(?:export\s+)?' + re.escape(alias) + r'\s*=', text, re.M) for alias in aliases):
|
|
397
|
+
raise ValueError(f'{name} already exists; retain the existing instance or select a new Hermes home')
|
|
398
|
+
return (text + ('\n' if text and not text.endswith('\n') else '') +
|
|
399
|
+
'\n'.join(name + '=' + json.dumps(value) for name, value in values.items()) + '\n').encode()
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _resource_digest(resources):
|
|
403
|
+
return hashlib.sha256(b''.join(name.encode()+resources[name] for name in sorted(resources))).hexdigest()
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _copied_resources(directory):
|
|
407
|
+
if directory.is_symlink() or not directory.is_dir():
|
|
408
|
+
raise ValueError('Expected this instance\'s copied adapter directory')
|
|
409
|
+
resources = {}
|
|
410
|
+
for path in directory.rglob('*'):
|
|
411
|
+
if '__pycache__' in path.parts:
|
|
412
|
+
continue
|
|
413
|
+
if path.is_symlink():
|
|
414
|
+
raise ValueError('Copied adapter contains a local symlink; reconcile it before refreshing')
|
|
415
|
+
if path.is_file():
|
|
416
|
+
resources[str(path.relative_to(directory))] = path.read_bytes()
|
|
417
|
+
return resources
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def refresh_adapter(state, args):
|
|
421
|
+
"""Refresh canonical code for one stopped attachment, preserving its data."""
|
|
422
|
+
from .setup import _atomic_hermes_config_write, _read_hermes_config, _align_hermes_memory_spill, _canonicalize_hermes_binding
|
|
423
|
+
path = state/'instance.json'; before = path.read_bytes(); manifest = json.loads(before)
|
|
424
|
+
if (manifest.get('version') != 1 or manifest.get('profile') != 'local'
|
|
425
|
+
or manifest.get('adapter_binding', {}).get('mode') not in {'native-installed', 'private-directory'}):
|
|
426
|
+
raise ValueError('Adapter refresh requires a supported local attachment')
|
|
427
|
+
home = Path(manifest['hermes_home'])
|
|
428
|
+
python = _interpreter(getattr(args, 'hermes_python', None) or manifest['hermes_python'])
|
|
429
|
+
resources = _adapter_resources(getattr(args, 'adapter_wheel', None))
|
|
430
|
+
binding = _adapter_binding(python, resources)
|
|
431
|
+
old_binding = manifest['adapter_binding']
|
|
432
|
+
if old_binding['mode'] != binding['mode']:
|
|
433
|
+
raise ValueError('Adapter loading mode changed; use the original interpreter topology before refreshing')
|
|
434
|
+
updates = []
|
|
435
|
+
adapter = state/'adapter'; staged = backup = None
|
|
436
|
+
old_resources = None
|
|
437
|
+
retained_memory_name = None
|
|
438
|
+
if binding['mode'] == 'private-directory':
|
|
439
|
+
old_resources = _copied_resources(adapter)
|
|
440
|
+
if _resource_digest(old_resources) != manifest['adapter_sha256']:
|
|
441
|
+
raise ValueError('Copied adapter was edited locally; retain or reconcile those edits before refreshing')
|
|
442
|
+
def retain_forwarder(profile, *, memory=False):
|
|
443
|
+
names = ('apsimo-memory', 'colony-memory') if memory else ('apsimo', 'colony')
|
|
444
|
+
directories = [profile/'plugins'/name for name in names if (profile/'plugins'/name).exists()]
|
|
445
|
+
if len(directories) != 1:
|
|
446
|
+
raise ValueError('Expected one managed adapter directory; reconcile duplicate or missing bindings')
|
|
447
|
+
directory = directories[0]
|
|
448
|
+
forwarder = directory/'__init__.py'
|
|
449
|
+
modules = ('apsimo_memory','colony_memory') if memory else ('apsimo_hermes','colony_hermes')
|
|
450
|
+
matches = [module for module in modules if not forwarder.is_symlink()
|
|
451
|
+
and forwarder.read_text() == _forwarder(adapter, module, memory)]
|
|
452
|
+
if len(matches) != 1:
|
|
453
|
+
raise ValueError('The selected profile adapter forwarder changed; reconcile it before refreshing')
|
|
454
|
+
module = matches[0]
|
|
455
|
+
if module+'/__init__.py' not in resources:
|
|
456
|
+
raise ValueError('New adapter lacks the compatibility module required by this forwarder')
|
|
457
|
+
target = directory/'plugin.yaml'
|
|
458
|
+
if target.is_symlink() or target.read_bytes() != old_resources[module+'/plugin.yaml']:
|
|
459
|
+
raise ValueError('The selected profile adapter manifest changed; reconcile it before refreshing')
|
|
460
|
+
canonical = 'apsimo_memory' if memory else 'apsimo_hermes'
|
|
461
|
+
updates.append((target, target.read_bytes(), resources[canonical+'/plugin.yaml']))
|
|
462
|
+
return directory.name
|
|
463
|
+
retain_forwarder(home)
|
|
464
|
+
retained_memory_name = retain_forwarder(home, memory=True)
|
|
465
|
+
lane = manifest.get('local_work') or {}
|
|
466
|
+
if lane.get('executor') == 'kanban':
|
|
467
|
+
from .setup_local_work import native_root
|
|
468
|
+
worker = native_root(home)/'profiles'/lane['worker_profile']
|
|
469
|
+
config = yaml.safe_load((worker/'config.yaml').read_text())
|
|
470
|
+
if plugin_settings(config).get('instance_dir') != str(state):
|
|
471
|
+
raise ValueError('The recorded draft worker binding changed; reconcile it before refreshing')
|
|
472
|
+
retain_forwarder(worker)
|
|
473
|
+
updated = {**manifest, 'hermes_python':str(python), 'sidecar_python':sys.executable,
|
|
474
|
+
'sidecar_module_root':str(Path(__file__).resolve().parents[1]),
|
|
475
|
+
'adapter_sha256':_resource_digest(resources), 'adapter_binding':binding}
|
|
476
|
+
updates.append((path, before, _json(updated).encode()))
|
|
477
|
+
config_paths = [home/'config.yaml']
|
|
478
|
+
lane = manifest.get('local_work') or {}
|
|
479
|
+
if lane.get('executor') == 'kanban':
|
|
480
|
+
from .setup_local_work import native_root
|
|
481
|
+
config_paths.append(native_root(home)/'profiles'/lane['worker_profile']/'config.yaml')
|
|
482
|
+
for config_path in config_paths:
|
|
483
|
+
config_before, config = _read_hermes_config(config_path)
|
|
484
|
+
if config_path != config_paths[0] and plugin_settings(config).get('instance_dir') != str(state):
|
|
485
|
+
raise ValueError('The recorded draft worker belongs to another instance')
|
|
486
|
+
renamed = _canonicalize_hermes_binding(config)
|
|
487
|
+
if config_path == config_paths[0] and retained_memory_name:
|
|
488
|
+
# Hermes finds directory memory providers by directory name, unlike
|
|
489
|
+
# general plugins which select their manifest name. Keep this alias
|
|
490
|
+
# while its physical directory is retained; its code is canonical.
|
|
491
|
+
config['memory']['provider'] = retained_memory_name
|
|
492
|
+
aligned = _align_hermes_memory_spill(config)
|
|
493
|
+
if renamed or aligned:
|
|
494
|
+
updates.append((config_path, config_before, yaml.safe_dump(config, sort_keys=False,
|
|
495
|
+
allow_unicode=True).encode()))
|
|
496
|
+
if aligned:
|
|
497
|
+
print('Hermes memory prefetch spill allowance: max_chars -> 65536 for '+str(config_path))
|
|
498
|
+
copied_change = old_resources is not None and old_resources != resources
|
|
499
|
+
if not copied_change and all(original == after for _, original, after in updates):
|
|
500
|
+
print('Selected adapter already matches; private instance unchanged.')
|
|
501
|
+
return
|
|
502
|
+
completed = []
|
|
503
|
+
try:
|
|
504
|
+
if copied_change:
|
|
505
|
+
staged = Path(tempfile.mkdtemp(prefix='.colony-adapter-', dir=state))
|
|
506
|
+
for name, content in resources.items():
|
|
507
|
+
_private_write(staged/name, content)
|
|
508
|
+
# Hermes and Apsimo are stopped by the operator's existing lifecycle.
|
|
509
|
+
# Keep the complete old directory for recovery, including caches.
|
|
510
|
+
if _copied_resources(adapter) != old_resources or path.read_bytes() != before:
|
|
511
|
+
raise ValueError('Attachment changed during refresh; retry after reconciling it')
|
|
512
|
+
backup = Path(tempfile.mkdtemp(prefix='adapter-previous-', dir=state)); backup.rmdir()
|
|
513
|
+
os.replace(adapter, backup)
|
|
514
|
+
os.replace(staged, adapter)
|
|
515
|
+
for target, original, after in updates:
|
|
516
|
+
if original != after:
|
|
517
|
+
_atomic_hermes_config_write(target, original, after)
|
|
518
|
+
completed.append((target, original, after))
|
|
519
|
+
except Exception:
|
|
520
|
+
for target, original, after in reversed(completed):
|
|
521
|
+
if not target.is_symlink() and target.read_bytes() == after:
|
|
522
|
+
_atomic_hermes_config_write(target, after, original)
|
|
523
|
+
if backup is not None and backup.exists():
|
|
524
|
+
if not adapter.exists():
|
|
525
|
+
os.replace(backup, adapter)
|
|
526
|
+
elif _copied_resources(adapter) == resources:
|
|
527
|
+
shutil.rmtree(adapter)
|
|
528
|
+
os.replace(backup, adapter)
|
|
529
|
+
raise
|
|
530
|
+
finally:
|
|
531
|
+
if staged is not None and staged.exists():
|
|
532
|
+
shutil.rmtree(staged)
|
|
533
|
+
print('Selected adapter refreshed ('+binding['mode']+'); identity and databases retained.')
|
|
534
|
+
if backup is not None:
|
|
535
|
+
print('Previous copied adapter retained at '+str(backup))
|
|
536
|
+
print('Start this Apsimo instance and new Hermes processes through their existing lifecycle.')
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def run(root_dir=None, args=None):
|
|
540
|
+
from apsimo import setup
|
|
541
|
+
noninteractive = bool(getattr(args, 'non_interactive', False))
|
|
542
|
+
def ask(label, value='', required=False):
|
|
543
|
+
result = setup._prompt(label, str(value or ''), noninteractive).strip()
|
|
544
|
+
if required and not result:
|
|
545
|
+
raise ValueError(label + ' is required')
|
|
546
|
+
if any(ord(char) < 32 for char in result):
|
|
547
|
+
raise ValueError('Configuration values must fit on one line')
|
|
548
|
+
return result
|
|
549
|
+
try:
|
|
550
|
+
receipt_choice = getattr(args, 'whatsapp_read_receipts', None)
|
|
551
|
+
preferences_only = bool(getattr(args, 'preferences_only', False))
|
|
552
|
+
preview = bool(getattr(args, 'preview', False))
|
|
553
|
+
if preferences_only:
|
|
554
|
+
home = setup._resolve_hermes_home(getattr(args, 'hermes_home', None))
|
|
555
|
+
selected_python = None
|
|
556
|
+
else:
|
|
557
|
+
home, selected_python = _select_home(args, ask)
|
|
558
|
+
if preview and not preferences_only:
|
|
559
|
+
raise ValueError('--preview requires --preferences-only')
|
|
560
|
+
if preferences_only:
|
|
561
|
+
if receipt_choice is None:
|
|
562
|
+
raise ValueError('--preferences-only requires --whatsapp-read-receipts on or off')
|
|
563
|
+
if root_dir or any(getattr(args, name, None) for name in (
|
|
564
|
+
'start', 'refresh_adapter', 'replace_memory_provider', 'local_work',
|
|
565
|
+
'native_goals', 'native_reviews', 'model_url', 'model', 'adapter_wheel', 'agent_name',
|
|
566
|
+
'agent_values', 'timezone', 'quiet_hours', 'contact_name', 'encrypt',
|
|
567
|
+
'passphrase', 'claim_genesis', 'mcp_harnesses', 'no_harness')) or (
|
|
568
|
+
getattr(args, 'host_framework', None) not in (None, 'hermes')):
|
|
569
|
+
raise ValueError('--preferences-only cannot be combined with instance or setup options')
|
|
570
|
+
_write_receipt_preference(home, receipt_choice, preview)
|
|
571
|
+
return 0
|
|
572
|
+
_, current_config = setup._read_hermes_config(home/'config.yaml')
|
|
573
|
+
env = normalize_environment()
|
|
574
|
+
state = Path(root_dir or env.get('COLONY_STATE_DIR') or
|
|
575
|
+
plugin_settings(current_config).get('instance_dir') or home/'apsimo').expanduser().resolve()
|
|
576
|
+
if state == home or home.is_relative_to(state):
|
|
577
|
+
raise ValueError('The private Apsimo directory must not contain the Hermes home')
|
|
578
|
+
# State, credentials and private identity are never written into a checkout.
|
|
579
|
+
for destination in (state, home):
|
|
580
|
+
if any((parent/'.git').is_file() or (parent/'.git'/'HEAD').is_file()
|
|
581
|
+
for parent in (destination, *destination.parents)):
|
|
582
|
+
raise ValueError('Choose a private Hermes home and instance directory outside Git checkouts')
|
|
583
|
+
config_path = home/'config.yaml'
|
|
584
|
+
original, config = setup._read_hermes_config(config_path)
|
|
585
|
+
if receipt_choice is not None:
|
|
586
|
+
_receipt_preference(config, receipt_choice) # Validate before setup effects.
|
|
587
|
+
for section in ('plugins', 'memory', 'compression'):
|
|
588
|
+
if section in config and not isinstance(config[section], dict):
|
|
589
|
+
raise ValueError('Hermes plugin, memory and compression settings must be mappings')
|
|
590
|
+
plugin_settings(config)
|
|
591
|
+
if any(name in config.get('plugins', {}).get('disabled', []) for name in ('colony','apsimo')):
|
|
592
|
+
raise ValueError('Apsimo is explicitly disabled in this home; resolve that setting before attachment')
|
|
593
|
+
if any((home/name).exists() for name in ('colony-memory.json', 'apsimo-memory.json')):
|
|
594
|
+
raise ValueError('Existing native Apsimo settings need an explicit migration; select a new home')
|
|
595
|
+
if (state/'instance.json').exists():
|
|
596
|
+
manifest = json.loads((state/'instance.json').read_text())
|
|
597
|
+
if manifest.get('hermes_home') != str(home):
|
|
598
|
+
raise ValueError('This instance belongs to another Hermes home')
|
|
599
|
+
if plugin_settings(config).get('instance_dir') != str(state):
|
|
600
|
+
raise ValueError('The Hermes binding changed; restore its saved config or select another instance')
|
|
601
|
+
if getattr(args, 'native_goals', False):
|
|
602
|
+
from dotenv import dotenv_values
|
|
603
|
+
from .setup_native_goals import prepare
|
|
604
|
+
prepare(config, home, native_env=dotenv_values(home/'.env'),
|
|
605
|
+
observer_env=dotenv_values(state/'.env'), local_work=bool(getattr(args, 'local_work', False)
|
|
606
|
+
or manifest.get('local_work', {}).get('executor') == 'kanban'),
|
|
607
|
+
draft_board=manifest.get('local_work', {}).get('board'))
|
|
608
|
+
if getattr(args, 'refresh_adapter', False):
|
|
609
|
+
refresh_adapter(state, args)
|
|
610
|
+
if getattr(args, 'local_work', False):
|
|
611
|
+
# A native registration failure can follow successful attachment.
|
|
612
|
+
# Retry that missing step with the retained role and interpreter.
|
|
613
|
+
if not manifest.get('sidecar_python') or not manifest.get('sidecar_module_root'):
|
|
614
|
+
raise ValueError('This older instance requires an adapter upgrade before local drafts can be installed')
|
|
615
|
+
from .router.native_policy import planning
|
|
616
|
+
from .setup_local_work import install, verify_tools
|
|
617
|
+
options, _ = asyncio.run(planning(json.loads((state/'.colony-llm-config.json').read_text())))
|
|
618
|
+
verify_tools(options['base_url'], options['model'], options['api_key'])
|
|
619
|
+
install(state)
|
|
620
|
+
print('Accepted local drafts use native Kanban. Restart this Apsimo instance and Hermes gateway to load the binding.')
|
|
621
|
+
if getattr(args, 'native_goals', False):
|
|
622
|
+
from .setup_native_goals import enable
|
|
623
|
+
enable(state)
|
|
624
|
+
if (getattr(args, 'native_reviews', False) or
|
|
625
|
+
(getattr(args, 'refresh_adapter', False) and
|
|
626
|
+
(plugin_settings(config).get('native_reviews') or {}).get('enabled') is True)):
|
|
627
|
+
from .setup_native_reviews import configure
|
|
628
|
+
configure(state, install=True)
|
|
629
|
+
if receipt_choice is not None:
|
|
630
|
+
_write_receipt_preference(home, receipt_choice)
|
|
631
|
+
_select_state_environment(state)
|
|
632
|
+
print(f'Existing private instance retained: {state}')
|
|
633
|
+
print(f'Use apsimo --instance {str(state)!r} start, then status.')
|
|
634
|
+
return 0
|
|
635
|
+
if getattr(args, 'refresh_adapter', False):
|
|
636
|
+
raise ValueError('Adapter refresh requires an existing private instance')
|
|
637
|
+
if state.exists() and any(state.iterdir()):
|
|
638
|
+
raise ValueError('The selected directory has existing state; use its existing configuration or a new private directory')
|
|
639
|
+
if (home/'plugins').is_symlink():
|
|
640
|
+
raise ValueError('Symlinked plugin directories require explicit migration')
|
|
641
|
+
for name in ('colony', 'colony-memory', 'apsimo', 'apsimo-memory'):
|
|
642
|
+
if (home/'plugins'/name).exists():
|
|
643
|
+
raise ValueError('An existing Apsimo directory adapter needs an explicit upgrade; choose another home for this installer')
|
|
644
|
+
selected_provider = config.get('memory', {}).get('provider')
|
|
645
|
+
replace_provider = bool(getattr(args, 'replace_memory_provider', False))
|
|
646
|
+
if selected_provider not in (None, '', 'colony', 'colony-memory', 'apsimo', 'apsimo-memory') and not replace_provider:
|
|
647
|
+
if noninteractive or ask('Another memory provider is selected. Replace only its selection and retain its files? [y/N]', 'N').lower() not in {'y', 'yes'}:
|
|
648
|
+
raise ValueError('Existing provider retained; choose another --hermes-home or explicitly request --replace-memory-provider')
|
|
649
|
+
replace_provider = True
|
|
650
|
+
python = selected_python or _interpreter(getattr(args, 'hermes_python', None))
|
|
651
|
+
resources = _adapter_resources(getattr(args, 'adapter_wheel', None))
|
|
652
|
+
_preflight_outbox(home, resources)
|
|
653
|
+
binding = _adapter_binding(python, resources)
|
|
654
|
+
owner_name = ask('Your name', getattr(args, 'contact_name', None) or os.environ.get('USER', 'Owner'), True)
|
|
655
|
+
agent_name = ask('Agent name', getattr(args, 'agent_name', None) or 'Assistant', True)
|
|
656
|
+
agent_preferences = _agent_preferences(ask, args, config)
|
|
657
|
+
endpoint = _endpoint(ask('Local model API root', getattr(args, 'model_url', None), True))
|
|
658
|
+
local_hosts = _verify_local_endpoint(endpoint)
|
|
659
|
+
model_key = env.get('COLONY_MODEL_API_KEY', '')
|
|
660
|
+
if not noninteractive and not model_key:
|
|
661
|
+
model_key = getpass.getpass('Model API key (blank if not required): ')
|
|
662
|
+
model_key = model_key or 'local-no-key'
|
|
663
|
+
if any(ord(char) < 32 or ord(char) > 126 for char in model_key):
|
|
664
|
+
raise ValueError('Model API key must be a single printable ASCII value')
|
|
665
|
+
model = getattr(args, 'model', None)
|
|
666
|
+
if model:
|
|
667
|
+
model = ask('Model identifier', model, True)
|
|
668
|
+
if not model:
|
|
669
|
+
response = httpx.get(endpoint+'/models', headers={'Authorization': 'Bearer '+model_key}, timeout=5, trust_env=False)
|
|
670
|
+
response.raise_for_status()
|
|
671
|
+
names = [row['id'] for row in response.json().get('data', []) if isinstance(row, dict) and isinstance(row.get('id'), str)]
|
|
672
|
+
if names:
|
|
673
|
+
print('Available models: ' + ', '.join(names[:20]))
|
|
674
|
+
model = ask('Model identifier', names[0] if len(names) == 1 else '', True)
|
|
675
|
+
probe = httpx.post(endpoint+'/chat/completions', headers={'Authorization': 'Bearer '+model_key},
|
|
676
|
+
json={'model': model, 'messages': [{'role': 'user', 'content': 'Reply OK.'}], 'max_tokens': 8},
|
|
677
|
+
timeout=30, trust_env=False)
|
|
678
|
+
probe.raise_for_status()
|
|
679
|
+
if not probe.json().get('choices'):
|
|
680
|
+
raise ValueError('The selected endpoint did not return an OpenAI-compatible chat response')
|
|
681
|
+
local_work = bool(getattr(args, 'local_work', False))
|
|
682
|
+
if not noninteractive and not local_work:
|
|
683
|
+
local_work = ask('Run explicitly accepted local summaries in the background? [Y/n]', 'Y').lower() in {'y','yes'}
|
|
684
|
+
native_goals = bool(getattr(args, 'native_goals', False))
|
|
685
|
+
if not noninteractive and not native_goals:
|
|
686
|
+
native_goals = ask('Enable persistent tasks using this Hermes profile and its gateway? [y/N]', 'N').lower() in {'y','yes'}
|
|
687
|
+
native_reviews = bool(getattr(args, 'native_reviews', False))
|
|
688
|
+
if not noninteractive and not native_reviews:
|
|
689
|
+
native_reviews = ask('Enable bounded read-only operational reviews? [y/N]', 'N').lower() in {'y','yes'}
|
|
690
|
+
if local_work or native_goals or native_reviews:
|
|
691
|
+
from .setup_local_work import verify_tools
|
|
692
|
+
verify_tools(endpoint, model, model_key)
|
|
693
|
+
port = int(getattr(args, 'port', 7777))
|
|
694
|
+
if getattr(args, 'bind', '127.0.0.1') != '127.0.0.1':
|
|
695
|
+
raise ValueError('The local Hermes profile listens on 127.0.0.1; configure remote access separately')
|
|
696
|
+
if not 1 <= port <= 65535 or setup._check_port(port):
|
|
697
|
+
raise ValueError('Choose a free sidecar port between 1 and 65535; no existing process will be stopped')
|
|
698
|
+
url = f'http://127.0.0.1:{port}'
|
|
699
|
+
original_env = (home/'.env').read_bytes() if (home/'.env').exists() else None
|
|
700
|
+
key = secrets.token_urlsafe(32)
|
|
701
|
+
env_updates = {'COLONY_NATIVE_API_KEY': key, 'COLONY_GENERAL_PLUGIN_ACTIVE': '1',
|
|
702
|
+
'COLONY_MEMORY_WORKER_TOOLS': '0', 'COLONY_MEMORY_TURN_WRITER': 'disabled',
|
|
703
|
+
'COLONY_MEMORY_DEFAULT_CONTEXT_AUTHORITY': 'owner_system'}
|
|
704
|
+
fresh_model = not config.get('model')
|
|
705
|
+
if fresh_model:
|
|
706
|
+
env_updates['OPENAI_API_KEY'] = model_key
|
|
707
|
+
env_updates['OPENAI_BASE_URL'] = endpoint
|
|
708
|
+
env_updates = {('APSIMO_'+key[7:] if key.startswith('COLONY_') else key):value for key,value in env_updates.items()}
|
|
709
|
+
native_env = _native_environment(original_env, env_updates)
|
|
710
|
+
goal_configuration, goal_details = None, None
|
|
711
|
+
if native_goals:
|
|
712
|
+
from dotenv import dotenv_values
|
|
713
|
+
from .setup_native_goals import prepare
|
|
714
|
+
selected = dict(config)
|
|
715
|
+
if fresh_model:
|
|
716
|
+
selected['model'] = {'provider': 'custom', 'default': model, 'base_url': endpoint}
|
|
717
|
+
goal_configuration, goal_details = prepare(selected, home,
|
|
718
|
+
native_env=dotenv_values(home/'.env'), observer_env={}, local_work=local_work)
|
|
719
|
+
# Every runtime/resource/config preflight above occurs before state creation.
|
|
720
|
+
state.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
721
|
+
staged = Path(tempfile.mkdtemp(prefix='.colony-init-', dir=state.parent))
|
|
722
|
+
try:
|
|
723
|
+
from apsimo.contacts.store import SQLiteContactStore
|
|
724
|
+
from apsimo.contacts.config import ContactsConfig
|
|
725
|
+
async def owner():
|
|
726
|
+
store = SQLiteContactStore(ContactsConfig(sqlite_path=str(staged/'contacts.db')))
|
|
727
|
+
await store.connect()
|
|
728
|
+
try:
|
|
729
|
+
return await setup.build_owner_contact(store, owner_name)
|
|
730
|
+
finally:
|
|
731
|
+
await store.close()
|
|
732
|
+
owner_id = asyncio.run(owner())
|
|
733
|
+
from apsimo.util.autonomy_preset import PRESETS
|
|
734
|
+
values = {**PRESETS['passive'], 'COLONY_INSTALL_PROFILE': 'local', 'COLONY_STATE_DIR': str(state),
|
|
735
|
+
'COLONY_SIDECAR_HOST': '127.0.0.1', 'COLONY_SIDECAR_PORT': str(port),
|
|
736
|
+
'COLONY_OWNER_CONTACT_ID': owner_id, 'COLONY_OWNER_NAME': owner_name,
|
|
737
|
+
'COLONY_PERSONA_NAME': agent_name, 'COLONY_CONTACTS_DB': str(state/'contacts.db'),
|
|
738
|
+
'COLONY_API_KEYRING_PATH': str(state/'api-keyring.json'), 'COLONY_API_KEY': '',
|
|
739
|
+
'COLONY_CLIENT_API_KEY': key, 'COLONY_GRAPH_ENABLED': 'false',
|
|
740
|
+
'COLONY_EMBED_PROVIDER': 'skip', 'WORLD_MODEL_BACKEND': 'sqlite',
|
|
741
|
+
'COLONY_AUTONOMY_PRESET': 'passive', 'COLONY_EMBEDDED_WORKER_ENABLED': 'false',
|
|
742
|
+
'COLONY_SOURCE_CLAIMS': 'on'}
|
|
743
|
+
values.update({
|
|
744
|
+
'COLONY_AGENT_VALUES': json.dumps(agent_preferences['values'], ensure_ascii=True),
|
|
745
|
+
'COLONY_AGENT_TIMEZONE': agent_preferences['timezone'],
|
|
746
|
+
'COLONY_AGENT_QUIET_HOURS': agent_preferences['quiet_hours'],
|
|
747
|
+
})
|
|
748
|
+
if goal_details is not None:
|
|
749
|
+
values['COLONY_HERMES_WORK_BOARDS'] = json.dumps(goal_details['boards'], separators=(',', ':'))
|
|
750
|
+
values = {('APSIMO_'+key[7:] if key.startswith('COLONY_') else key):value for key,value in values.items()}
|
|
751
|
+
_private_write(staged/'.env', _native_environment(None, values))
|
|
752
|
+
principal = {'principal': 'hermes-local', 'status': 'active', 'viewer_person_id': owner_id,
|
|
753
|
+
'audiences': ['viewer'], 'allow_unscoped_api': False, 'turn_ingress_platforms': ['cli'],
|
|
754
|
+
'scopes': ['context:read', 'memory:read', 'memory:search', 'memory:write', 'turns:write'],
|
|
755
|
+
'credentials': [{'id': 'initial', 'secret': key, 'status': 'active'}]}
|
|
756
|
+
_private_write(staged/'api-keyring.json', _json({'version': 1, 'principals': [principal]}))
|
|
757
|
+
model_configuration = {'provider': 'local', 'baseUrl': endpoint,
|
|
758
|
+
'apiKey': model_key, 'localHosts': local_hosts,
|
|
759
|
+
'models': {name: model for name in ('small', 'medium', 'large')}}
|
|
760
|
+
if local_work or native_reviews:
|
|
761
|
+
from .setup_local_work import planning_configuration
|
|
762
|
+
planning_configuration(model_configuration)
|
|
763
|
+
_private_write(staged/'.colony-llm-config.json', _json(model_configuration))
|
|
764
|
+
for name, content in resources.items():
|
|
765
|
+
_private_write(staged/'adapter'/name, content)
|
|
766
|
+
manifest = {'version': 1, 'hermes_home': str(home), 'hermes_python': str(python),
|
|
767
|
+
'sidecar_python': sys.executable, 'sidecar_module_root': str(Path(__file__).resolve().parents[1]),
|
|
768
|
+
'owner_id': owner_id, 'agent_name': agent_name, 'endpoint': endpoint, 'model': model,
|
|
769
|
+
'agent_preferences': agent_preferences,
|
|
770
|
+
'adapter_sha256': hashlib.sha256(b''.join(name.encode()+resources[name] for name in sorted(resources))).hexdigest(),
|
|
771
|
+
'adapter_binding': binding,
|
|
772
|
+
'profile': 'local', 'status': 'configured_not_behaviorally_verified'}
|
|
773
|
+
_private_write(staged/'instance.json', _json(manifest))
|
|
774
|
+
# Prepare the existing config path with the canonical provider helper.
|
|
775
|
+
prepared_path = staged/'config.yaml'
|
|
776
|
+
candidate = dict(goal_configuration if goal_configuration is not None else config)
|
|
777
|
+
if replace_provider:
|
|
778
|
+
candidate['memory'] = dict(candidate.get('memory') or {})
|
|
779
|
+
candidate['memory']['provider'] = 'apsimo-memory'
|
|
780
|
+
candidate['memory']['config'] = {} # Saved original retains incumbent settings.
|
|
781
|
+
_private_write(prepared_path, yaml.safe_dump(candidate, sort_keys=False))
|
|
782
|
+
_, prepared = setup._prepare_hermes_config(prepared_path, url, owner_id)
|
|
783
|
+
candidate = yaml.safe_load(prepared)
|
|
784
|
+
plugin = candidate['plugins']['apsimo']
|
|
785
|
+
plugin.update(instance_dir=str(state), owner_contact_id=owner_id,
|
|
786
|
+
api_key='${APSIMO_NATIVE_API_KEY}', execution_registry_enabled=True,
|
|
787
|
+
attested_system_platforms=['cli'], enabled_action_tools=[], enabled_message_tools=[],
|
|
788
|
+
turn_outbox_path=str(home/'state'/'colony-turn-outbox.sqlite3'))
|
|
789
|
+
candidate['memory']['config'].update(api_key='${APSIMO_NATIVE_API_KEY}', turn_writer='disabled')
|
|
790
|
+
enabled = candidate['plugins'].setdefault('enabled', [])
|
|
791
|
+
if not isinstance(enabled, list):
|
|
792
|
+
raise ValueError('Hermes plugins.enabled must be a list')
|
|
793
|
+
if 'apsimo' not in enabled:
|
|
794
|
+
enabled.append('apsimo')
|
|
795
|
+
candidate.setdefault('compression', {})['checkpoint_required'] = True
|
|
796
|
+
if fresh_model:
|
|
797
|
+
candidate['model'] = {'provider': 'custom', 'default': model, 'base_url': endpoint}
|
|
798
|
+
if receipt_choice is not None:
|
|
799
|
+
candidate, _ = _receipt_preference(candidate, receipt_choice)
|
|
800
|
+
final_config = yaml.safe_dump(candidate, sort_keys=False, allow_unicode=True).encode()
|
|
801
|
+
prepared_path.unlink()
|
|
802
|
+
backups = staged/'hermes-original'
|
|
803
|
+
if original is not None: _private_write(backups/'config.yaml', original)
|
|
804
|
+
if original_env is not None: _private_write(backups/'.env', original_env)
|
|
805
|
+
if state.exists(): state.rmdir() # Only an empty installer-selected directory is allowed.
|
|
806
|
+
os.replace(staged, state)
|
|
807
|
+
finally:
|
|
808
|
+
if staged.exists(): shutil.rmtree(staged)
|
|
809
|
+
home.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
810
|
+
created, replaced = [], []
|
|
811
|
+
try:
|
|
812
|
+
if (home/'.env').is_symlink() or ((home/'.env').read_bytes() if (home/'.env').exists() else None) != original_env:
|
|
813
|
+
raise ValueError('Hermes private environment changed during installation')
|
|
814
|
+
setup._atomic_hermes_config_write(home/'.env', original_env, native_env)
|
|
815
|
+
os.chmod(home/'.env', 0o600)
|
|
816
|
+
replaced.append((home/'.env', original_env, native_env))
|
|
817
|
+
def create(path, value):
|
|
818
|
+
raw = value if isinstance(value, bytes) else value.encode()
|
|
819
|
+
_private_write(path, raw)
|
|
820
|
+
created.append((path, raw))
|
|
821
|
+
if binding['mode'] == 'private-directory':
|
|
822
|
+
for directory, module in (('apsimo', 'apsimo_hermes'), ('apsimo-memory', 'apsimo_memory')):
|
|
823
|
+
create(home/'plugins'/directory/'__init__.py', _forwarder(state/'adapter', module, directory == 'apsimo-memory'))
|
|
824
|
+
create(home/'plugins'/directory/'plugin.yaml', resources[module+'/plugin.yaml'])
|
|
825
|
+
create(home/'plugins'/'apsimo-memory'/'cli.py', _forwarder(state/'adapter', 'apsimo_memory.cli'))
|
|
826
|
+
setup._atomic_hermes_config_write(config_path, original, final_config)
|
|
827
|
+
replaced.append((config_path, original, final_config))
|
|
828
|
+
if not (home/'SOUL.md').exists():
|
|
829
|
+
guiding = ('Guiding values: ' + ', '.join(agent_preferences['values']) + '.\n'
|
|
830
|
+
if agent_preferences['values'] else '')
|
|
831
|
+
create(home/'SOUL.md', f'# {agent_name}\n\nYou are {agent_name}, the personal assistant of {owner_name}.\n{guiding}Use retained evidence with its provenance; ask about uncertainty.\nYour opinions are revisable interpretations; distinguish them from facts and permissions.\nOwner consent is required for consequential external actions; an existing task-scoped consent covers only its stated scope.\n')
|
|
832
|
+
except Exception:
|
|
833
|
+
# Undo only this installation's exact bytes. Concurrent edits stay
|
|
834
|
+
# intact, with original files still retained in the private state.
|
|
835
|
+
for path, before, after in reversed(replaced):
|
|
836
|
+
if not path.is_symlink() and path.is_file() and path.read_bytes() == after:
|
|
837
|
+
if before is None:
|
|
838
|
+
path.unlink()
|
|
839
|
+
else:
|
|
840
|
+
setup._atomic_hermes_config_write(path, after, before)
|
|
841
|
+
for path, raw in reversed(created):
|
|
842
|
+
if not path.is_symlink() and path.is_file() and path.read_bytes() == raw:
|
|
843
|
+
path.unlink()
|
|
844
|
+
for name in ('apsimo', 'apsimo-memory'):
|
|
845
|
+
directory = home/'plugins'/name
|
|
846
|
+
if directory.is_dir() and not any(directory.iterdir()):
|
|
847
|
+
directory.rmdir()
|
|
848
|
+
print(f'Attachment failed; prepared state and original files remain in {state}.')
|
|
849
|
+
raise
|
|
850
|
+
_select_state_environment(state)
|
|
851
|
+
if local_work:
|
|
852
|
+
from .setup_local_work import install
|
|
853
|
+
install(state)
|
|
854
|
+
if native_reviews:
|
|
855
|
+
from .setup_native_reviews import configure
|
|
856
|
+
configure(state, install=True)
|
|
857
|
+
print(f'Private agent configured in {home}; state in {state}.')
|
|
858
|
+
print('Adapter loading: ' + binding['mode'] + ' (canonical artifact bytes verified).')
|
|
859
|
+
print('Canonical memory capture and recollection are enabled for new Hermes sessions.')
|
|
860
|
+
print('Hermes hook output spill allowance is at least 65536 characters or already disabled; retrieval budgets are unchanged.')
|
|
861
|
+
print('Source memory, temporal claims, contacts, commitments and self state persist without a graph.')
|
|
862
|
+
print('Graph/vector recall and consequential background work are optional and currently disabled.')
|
|
863
|
+
if local_work:
|
|
864
|
+
print('Accepted local drafts use the native Kanban board and dedicated worker profile.')
|
|
865
|
+
print('Keep the selected Hermes gateway running. Its dispatch ticks refresh the planning role for future attempts.')
|
|
866
|
+
if goal_details is not None:
|
|
867
|
+
from .setup_native_goals import describe
|
|
868
|
+
describe(goal_details)
|
|
869
|
+
print(f'Start: apsimo --instance {str(state)!r} start --detach')
|
|
870
|
+
print(f'Status: apsimo --instance {str(state)!r} status')
|
|
871
|
+
print('No existing Hermes process was restarted. Begin a new session to load the adapter.')
|
|
872
|
+
if getattr(args, 'start', False) or (not noninteractive and ask('Start this sidecar now? [Y/n]', 'Y').lower() in {'y','yes'}):
|
|
873
|
+
result = subprocess.run([sys.executable, '-m', 'apsimo', '--instance', str(state), 'start', '--detach'], timeout=60)
|
|
874
|
+
return result.returncode
|
|
875
|
+
return 0
|
|
876
|
+
except (OSError, ValueError, KeyError, httpx.HTTPError, subprocess.SubprocessError) as error:
|
|
877
|
+
detail = str(error) if isinstance(error, ValueError) else type(error).__name__
|
|
878
|
+
print('Hermes initialization failed: ' + detail)
|
|
879
|
+
return 1
|