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,232 @@
|
|
|
1
|
+
"""1Password secrets backend — supports op CLI and 1Password Connect Server API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
from apsimo.secrets.backends.base import SecretsBackend
|
|
12
|
+
from apsimo.secrets.types import SecretType, VAULT_NAME
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class OPItem:
|
|
19
|
+
"""Represents a 1Password item as used by Colony."""
|
|
20
|
+
|
|
21
|
+
item_id: str
|
|
22
|
+
title: str # Secret key (e.g., "OPENAI_API_KEY")
|
|
23
|
+
value: str
|
|
24
|
+
category: str # Derived from SecretType
|
|
25
|
+
tags: list[str] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OnePasswordBackend(SecretsBackend):
|
|
29
|
+
"""1Password secrets backend.
|
|
30
|
+
|
|
31
|
+
Supports two modes:
|
|
32
|
+
1. CLI mode (default): uses ``op`` CLI — requires 1Password desktop app
|
|
33
|
+
or 1Password CLI with biometric unlock configured.
|
|
34
|
+
2. Connect mode: uses 1Password Connect Server API — suitable for
|
|
35
|
+
headless/server deployments. Requires OP_CONNECT_HOST and OP_CONNECT_TOKEN.
|
|
36
|
+
|
|
37
|
+
Vault structure:
|
|
38
|
+
- Default vault name: "Colony" (overridable in config)
|
|
39
|
+
- Items organised by section: LLM Keys, Infrastructure, Messaging
|
|
40
|
+
Gateways, Mesh / Federation, Email, Backup, Push Notifications,
|
|
41
|
+
Colony Core.
|
|
42
|
+
|
|
43
|
+
Auto-detects mode based on available configuration.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
name = "1password"
|
|
47
|
+
|
|
48
|
+
# 1Password item sections by SecretType
|
|
49
|
+
SECTION_MAP: dict[SecretType, str] = {
|
|
50
|
+
SecretType.LLM_API_KEY: "LLM Keys",
|
|
51
|
+
SecretType.NEO4J_CREDENTIAL: "Infrastructure",
|
|
52
|
+
SecretType.GATEWAY_TOKEN: "Messaging Gateways",
|
|
53
|
+
SecretType.MESH_PAIRING_KEY: "Mesh / Federation",
|
|
54
|
+
SecretType.EMAIL_CREDENTIAL: "Email",
|
|
55
|
+
SecretType.BACKUP_PASSPHRASE: "Backup",
|
|
56
|
+
SecretType.COLONY_TOKEN: "Colony Core",
|
|
57
|
+
SecretType.FEDERATION_CERT: "Mesh / Federation",
|
|
58
|
+
SecretType.PUSH_CREDENTIAL: "Push Notifications",
|
|
59
|
+
SecretType.CALDAV_CREDENTIAL: "Calendar",
|
|
60
|
+
SecretType.HEALTH_CREDENTIAL: "Health",
|
|
61
|
+
SecretType.OTHER: "Miscellaneous",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
vault_name: str = VAULT_NAME,
|
|
67
|
+
connect_host: str | None = None,
|
|
68
|
+
connect_token: str | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
self._vault = vault_name
|
|
71
|
+
self._connect_host = connect_host
|
|
72
|
+
self._connect_token = connect_token
|
|
73
|
+
self._use_connect = bool(connect_host and connect_token)
|
|
74
|
+
self._cache: dict[str, str] = {} # in-process cache
|
|
75
|
+
|
|
76
|
+
def is_available(self) -> bool:
|
|
77
|
+
if self._use_connect:
|
|
78
|
+
return bool(self._connect_host and self._connect_token)
|
|
79
|
+
return shutil.which("op") is not None
|
|
80
|
+
|
|
81
|
+
def get(self, key: str, default: str | None = None) -> str | None:
|
|
82
|
+
if key in self._cache:
|
|
83
|
+
return self._cache[key]
|
|
84
|
+
if self._use_connect:
|
|
85
|
+
return self._get_via_connect(key, default)
|
|
86
|
+
return self._get_via_cli(key, default)
|
|
87
|
+
|
|
88
|
+
def set(self, key: str, value: str, *, secret_type: SecretType | None = None) -> None:
|
|
89
|
+
if self._use_connect:
|
|
90
|
+
self._set_via_connect(key, value, secret_type)
|
|
91
|
+
else:
|
|
92
|
+
self._set_via_cli(key, value, secret_type)
|
|
93
|
+
self._cache[key] = value
|
|
94
|
+
|
|
95
|
+
def delete(self, key: str) -> None:
|
|
96
|
+
try:
|
|
97
|
+
self._run_op(["item", "delete", self._item_title(key), f"--vault={self._vault}"])
|
|
98
|
+
except subprocess.CalledProcessError as exc:
|
|
99
|
+
logger.debug("1Password delete failed for %s: %s", key, exc)
|
|
100
|
+
self._cache.pop(key, None)
|
|
101
|
+
|
|
102
|
+
def list(self) -> list[str]:
|
|
103
|
+
try:
|
|
104
|
+
result = self._run_op([
|
|
105
|
+
"item", "list",
|
|
106
|
+
f"--vault={self._vault}",
|
|
107
|
+
"--format=json",
|
|
108
|
+
"--tags=colony",
|
|
109
|
+
])
|
|
110
|
+
items = json.loads(result)
|
|
111
|
+
prefix = "colony:"
|
|
112
|
+
return [
|
|
113
|
+
item["title"][len(prefix):]
|
|
114
|
+
if item["title"].startswith(prefix)
|
|
115
|
+
else item["title"]
|
|
116
|
+
for item in items
|
|
117
|
+
]
|
|
118
|
+
except (subprocess.CalledProcessError, json.JSONDecodeError) as exc:
|
|
119
|
+
logger.debug("1Password list failed: %s", exc)
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
# -------------------------------------------------------------------------
|
|
123
|
+
# CLI helpers
|
|
124
|
+
# -------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def _get_via_cli(self, key: str, default: str | None) -> str | None:
|
|
127
|
+
try:
|
|
128
|
+
result = self._run_op([
|
|
129
|
+
"item", "get", self._item_title(key),
|
|
130
|
+
f"--vault={self._vault}",
|
|
131
|
+
"--fields=password",
|
|
132
|
+
"--format=json",
|
|
133
|
+
])
|
|
134
|
+
data = json.loads(result)
|
|
135
|
+
# op returns {"value": "..."} or {"password": "..."} depending on version
|
|
136
|
+
value = data.get("value") or data.get("password")
|
|
137
|
+
if value:
|
|
138
|
+
self._cache[key] = value
|
|
139
|
+
return value or default
|
|
140
|
+
except (subprocess.CalledProcessError, json.JSONDecodeError):
|
|
141
|
+
return default
|
|
142
|
+
|
|
143
|
+
def _set_via_cli(
|
|
144
|
+
self, key: str, value: str, secret_type: SecretType | None
|
|
145
|
+
) -> None:
|
|
146
|
+
section = self.SECTION_MAP.get(secret_type or SecretType.OTHER, "Miscellaneous")
|
|
147
|
+
title = self._item_title(key)
|
|
148
|
+
|
|
149
|
+
# Try update first, create if item not found
|
|
150
|
+
try:
|
|
151
|
+
self._run_op([
|
|
152
|
+
"item", "edit", title,
|
|
153
|
+
f"--vault={self._vault}",
|
|
154
|
+
f"password={value}",
|
|
155
|
+
])
|
|
156
|
+
except subprocess.CalledProcessError:
|
|
157
|
+
self._run_op([
|
|
158
|
+
"item", "create",
|
|
159
|
+
"--category=login",
|
|
160
|
+
f"--title={title}",
|
|
161
|
+
f"--vault={self._vault}",
|
|
162
|
+
f"--section={section}",
|
|
163
|
+
f"password={value}",
|
|
164
|
+
"--tags=colony",
|
|
165
|
+
])
|
|
166
|
+
|
|
167
|
+
def _run_op(self, args: list[str]) -> str:
|
|
168
|
+
result = subprocess.run(
|
|
169
|
+
["op"] + args,
|
|
170
|
+
capture_output=True,
|
|
171
|
+
text=True,
|
|
172
|
+
check=True,
|
|
173
|
+
timeout=10,
|
|
174
|
+
)
|
|
175
|
+
return result.stdout.strip()
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _item_title(key: str) -> str:
|
|
179
|
+
"""Map a secret key to a 1Password item title."""
|
|
180
|
+
return f"colony:{key}"
|
|
181
|
+
|
|
182
|
+
# -------------------------------------------------------------------------
|
|
183
|
+
# 1Password Connect Server API helpers
|
|
184
|
+
# -------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
def _get_via_connect(self, key: str, default: str | None) -> str | None:
|
|
187
|
+
"""Retrieve via 1Password Connect Server REST API."""
|
|
188
|
+
import urllib.request
|
|
189
|
+
url = f"{self._connect_host}/v1/vaults/{self._vault}/items"
|
|
190
|
+
req = urllib.request.Request(
|
|
191
|
+
url,
|
|
192
|
+
headers={"Authorization": f"Bearer {self._connect_token}"},
|
|
193
|
+
)
|
|
194
|
+
try:
|
|
195
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
196
|
+
items = json.loads(resp.read())
|
|
197
|
+
target = self._item_title(key)
|
|
198
|
+
for item in items:
|
|
199
|
+
if item.get("title") == target:
|
|
200
|
+
fields = item.get("fields", [])
|
|
201
|
+
if fields:
|
|
202
|
+
value = fields[0].get("value")
|
|
203
|
+
if value:
|
|
204
|
+
self._cache[key] = value
|
|
205
|
+
return value
|
|
206
|
+
except Exception as exc:
|
|
207
|
+
logger.debug("1Password Connect get failed for %s: %s", key, exc)
|
|
208
|
+
return default
|
|
209
|
+
|
|
210
|
+
def _set_via_connect(
|
|
211
|
+
self, key: str, value: str, secret_type: SecretType | None
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Store via 1Password Connect Server REST API."""
|
|
214
|
+
import urllib.request
|
|
215
|
+
section = self.SECTION_MAP.get(secret_type or SecretType.OTHER, "Miscellaneous")
|
|
216
|
+
payload = json.dumps({
|
|
217
|
+
"title": self._item_title(key),
|
|
218
|
+
"category": "LOGIN",
|
|
219
|
+
"tags": ["colony"],
|
|
220
|
+
"sections": [{"label": section}],
|
|
221
|
+
"fields": [{"label": "password", "value": value, "type": "CONCEALED"}],
|
|
222
|
+
}).encode()
|
|
223
|
+
req = urllib.request.Request(
|
|
224
|
+
f"{self._connect_host}/v1/vaults/{self._vault}/items",
|
|
225
|
+
data=payload,
|
|
226
|
+
headers={
|
|
227
|
+
"Authorization": f"Bearer {self._connect_token}",
|
|
228
|
+
"Content-Type": "application/json",
|
|
229
|
+
},
|
|
230
|
+
method="POST",
|
|
231
|
+
)
|
|
232
|
+
urllib.request.urlopen(req, timeout=5)
|
apsimo/secrets/cli.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""CLI command handlers for `colony secrets`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
import argparse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# -------------------------------------------------------------------------
|
|
13
|
+
# Section grouping for pretty-print
|
|
14
|
+
# -------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
_SECTION_KEYS: dict[str, list[str]] = {
|
|
17
|
+
"LLM Keys": [
|
|
18
|
+
"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY",
|
|
19
|
+
"GEMINI_API_KEY", "GROQ_API_KEY", "TOGETHER_API_KEY",
|
|
20
|
+
],
|
|
21
|
+
"Infrastructure": ["NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD", "COLONY_TOKEN_SECRET"],
|
|
22
|
+
"Messaging Gateways": [
|
|
23
|
+
"TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN", "SLACK_BOT_TOKEN",
|
|
24
|
+
"WHATSAPP_API_KEY", "TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "SIGNAL_API_KEY",
|
|
25
|
+
],
|
|
26
|
+
"Email": ["EMAIL_IMAP_PASSWORD", "EMAIL_SMTP_PASSWORD"],
|
|
27
|
+
"Backup": ["BACKUP_PASSPHRASE", "S3_ACCESS_KEY", "S3_SECRET_KEY"],
|
|
28
|
+
"Push Notifications": ["APNS_AUTH_KEY", "APNS_KEY_ID", "APNS_TEAM_ID", "FCM_SERVICE_ACCOUNT_JSON"],
|
|
29
|
+
"Calendar / Health": ["CALDAV_PASSWORD", "OURA_PERSONAL_ACCESS_TOKEN"],
|
|
30
|
+
"1Password Connect": ["OP_CONNECT_HOST", "OP_CONNECT_TOKEN"],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_manager():
|
|
35
|
+
from apsimo.secrets.manager import SecretsManager
|
|
36
|
+
return SecretsManager()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cmd_secrets_list(args: "argparse.Namespace") -> None:
|
|
40
|
+
"""colony secrets list — show configured secrets grouped by category."""
|
|
41
|
+
sm = _get_manager()
|
|
42
|
+
configured = set(sm.list())
|
|
43
|
+
|
|
44
|
+
print(f"\nBackend: {sm.backend_name}\n")
|
|
45
|
+
|
|
46
|
+
unconfigured: list[str] = []
|
|
47
|
+
for section, keys in _SECTION_KEYS.items():
|
|
48
|
+
section_configured = [k for k in keys if k in configured]
|
|
49
|
+
section_missing = [k for k in keys if k not in configured]
|
|
50
|
+
unconfigured.extend(section_missing)
|
|
51
|
+
if section_configured:
|
|
52
|
+
print(f"{section} ({len(section_configured)} configured):")
|
|
53
|
+
for k in section_configured:
|
|
54
|
+
print(f" \u2713 {k}")
|
|
55
|
+
print()
|
|
56
|
+
|
|
57
|
+
if unconfigured:
|
|
58
|
+
shown = unconfigured[:10]
|
|
59
|
+
remaining = len(unconfigured) - len(shown)
|
|
60
|
+
print("Not configured (run `colony secrets set KEY VALUE`):")
|
|
61
|
+
for k in shown:
|
|
62
|
+
print(f" \u2717 {k}")
|
|
63
|
+
if remaining > 0:
|
|
64
|
+
print(f" ... ({remaining} more)")
|
|
65
|
+
print()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cmd_secrets_get(args: "argparse.Namespace") -> None:
|
|
69
|
+
"""colony secrets get KEY — retrieve a secret."""
|
|
70
|
+
sm = _get_manager()
|
|
71
|
+
value = sm.get(args.key)
|
|
72
|
+
if value is None:
|
|
73
|
+
print(f"Secret '{args.key}' not found in {sm.backend_name} backend.", file=sys.stderr)
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
print(value)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def cmd_secrets_set(args: "argparse.Namespace") -> None:
|
|
79
|
+
"""colony secrets set KEY VALUE — store a secret."""
|
|
80
|
+
from apsimo.secrets.types import infer_secret_type
|
|
81
|
+
sm = _get_manager()
|
|
82
|
+
secret_type = infer_secret_type(args.key)
|
|
83
|
+
sm.set(args.key, args.value, secret_type=secret_type)
|
|
84
|
+
print(f"\u2713 Set {args.key} in {sm.backend_name} backend.")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def cmd_secrets_delete(args: "argparse.Namespace") -> None:
|
|
88
|
+
"""colony secrets delete KEY — remove a secret."""
|
|
89
|
+
sm = _get_manager()
|
|
90
|
+
sm.delete(args.key)
|
|
91
|
+
print(f"\u2713 Deleted {args.key} from {sm.backend_name} backend.")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_secrets_backend(args: "argparse.Namespace") -> None:
|
|
95
|
+
"""colony secrets backend — show current backend."""
|
|
96
|
+
sm = _get_manager()
|
|
97
|
+
print(f"Current secrets backend: {sm.backend_name}")
|
|
98
|
+
if sm.backend_name == "1password":
|
|
99
|
+
print(" Storage: 1Password vault")
|
|
100
|
+
elif sm.backend_name == "keyring":
|
|
101
|
+
print(" Storage: System keychain (macOS Keychain / Linux SecretService)")
|
|
102
|
+
else:
|
|
103
|
+
print(" Storage: ~/.colony/.env (plaintext, mode 0600)")
|
|
104
|
+
print("\nChange backend: colony setup (re-run onboarding wizard)")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cmd_secrets_status(args: "argparse.Namespace") -> None:
|
|
108
|
+
"""colony secrets status — check backend availability."""
|
|
109
|
+
from apsimo.secrets.backends.env import EnvBackend
|
|
110
|
+
from apsimo.secrets.backends.keyring import KeyringBackend
|
|
111
|
+
from apsimo.secrets.backends.onepassword import OnePasswordBackend
|
|
112
|
+
|
|
113
|
+
sm = _get_manager()
|
|
114
|
+
print(f"\nActive backend: {sm.backend_name}")
|
|
115
|
+
print()
|
|
116
|
+
|
|
117
|
+
backends = [
|
|
118
|
+
("1password", OnePasswordBackend()),
|
|
119
|
+
("keyring", KeyringBackend()),
|
|
120
|
+
("env", EnvBackend()),
|
|
121
|
+
]
|
|
122
|
+
for name, backend in backends:
|
|
123
|
+
mark = "\u2713" if backend.is_available() else "\u2717"
|
|
124
|
+
active = " (active)" if name == sm.backend_name else ""
|
|
125
|
+
print(f" {mark} {name}{active}")
|
|
126
|
+
|
|
127
|
+
print()
|
|
128
|
+
configured = sm.list()
|
|
129
|
+
print(f"Configured secrets: {len(configured)}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def cmd_secrets_migrate(args: "argparse.Namespace") -> None:
|
|
133
|
+
"""colony secrets migrate — move secrets between backends."""
|
|
134
|
+
from apsimo.secrets.backends.env import EnvBackend
|
|
135
|
+
from apsimo.secrets.backends.keyring import KeyringBackend
|
|
136
|
+
from apsimo.secrets.backends.onepassword import OnePasswordBackend
|
|
137
|
+
from apsimo.secrets.migration import SecretsMigrator
|
|
138
|
+
|
|
139
|
+
_backend_map = {
|
|
140
|
+
"env": EnvBackend,
|
|
141
|
+
"keyring": KeyringBackend,
|
|
142
|
+
"1password": OnePasswordBackend,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
from_name = args.from_backend
|
|
146
|
+
to_name = args.to_backend
|
|
147
|
+
|
|
148
|
+
if from_name not in _backend_map:
|
|
149
|
+
print(f"Unknown source backend: {from_name}", file=sys.stderr)
|
|
150
|
+
sys.exit(1)
|
|
151
|
+
if to_name not in _backend_map:
|
|
152
|
+
print(f"Unknown target backend: {to_name}", file=sys.stderr)
|
|
153
|
+
sys.exit(1)
|
|
154
|
+
|
|
155
|
+
source = _backend_map[from_name]()
|
|
156
|
+
target = _backend_map[to_name]()
|
|
157
|
+
|
|
158
|
+
if not target.is_available():
|
|
159
|
+
print(
|
|
160
|
+
f"Target backend '{to_name}' is not available on this system.",
|
|
161
|
+
file=sys.stderr,
|
|
162
|
+
)
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
|
|
165
|
+
migrator = SecretsMigrator(source, target)
|
|
166
|
+
|
|
167
|
+
dry_run: bool = getattr(args, "dry_run", False)
|
|
168
|
+
delete_source: bool = getattr(args, "delete_source", False)
|
|
169
|
+
|
|
170
|
+
if dry_run:
|
|
171
|
+
keys = migrator.preview()
|
|
172
|
+
if not keys:
|
|
173
|
+
print(f"No secrets found in '{from_name}' backend.")
|
|
174
|
+
return
|
|
175
|
+
print(f"Would migrate {len(keys)} secrets from '{from_name}' to '{to_name}':")
|
|
176
|
+
print(" " + ", ".join(keys))
|
|
177
|
+
print("\nRun without --dry-run to proceed.")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
print(f"Migrating secrets from '{from_name}' to '{to_name}'...")
|
|
181
|
+
result = migrator.migrate(dry_run=False, delete_from_source=delete_source)
|
|
182
|
+
|
|
183
|
+
for key in result.migrated:
|
|
184
|
+
print(f" \u2713 {key}")
|
|
185
|
+
for key, err in result.failed:
|
|
186
|
+
print(f" \u2717 {key}: {err}", file=sys.stderr)
|
|
187
|
+
|
|
188
|
+
print(f"\n{result.summary()}")
|
|
189
|
+
|
|
190
|
+
if delete_source and result.migrated:
|
|
191
|
+
print(f"Removed {len(result.migrated)} secrets from '{from_name}' backend.")
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""SecretsManager — unified secrets access layer for Colony.
|
|
2
|
+
|
|
3
|
+
All Colony code MUST use SecretsManager to access secrets.
|
|
4
|
+
Direct reads from os.environ or .env files are DEPRECATED in V2
|
|
5
|
+
and will be removed in V3.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
sm = SecretsManager()
|
|
10
|
+
api_key = sm.get("OPENAI_API_KEY")
|
|
11
|
+
sm.set("OPENAI_API_KEY", "sk-...")
|
|
12
|
+
keys = sm.list()
|
|
13
|
+
|
|
14
|
+
The backend is loaded from ~/.colony/config.yaml on first instantiation
|
|
15
|
+
and cached for the process lifetime. Override ``COLONY_SECRETS_BACKEND``
|
|
16
|
+
env var to ``env``, ``keyring``, or ``1password`` for testing.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
from apsimo.secrets.backends.base import SecretsBackend
|
|
24
|
+
from apsimo.secrets.types import SecretType
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
_BACKEND_REGISTRY: dict[str, type[SecretsBackend]] = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _register_backends() -> None:
|
|
32
|
+
"""Lazy-load and register all known backends."""
|
|
33
|
+
global _BACKEND_REGISTRY
|
|
34
|
+
if _BACKEND_REGISTRY:
|
|
35
|
+
return
|
|
36
|
+
from apsimo.secrets.backends.env import EnvBackend
|
|
37
|
+
from apsimo.secrets.backends.keyring import KeyringBackend
|
|
38
|
+
from apsimo.secrets.backends.onepassword import OnePasswordBackend
|
|
39
|
+
|
|
40
|
+
_BACKEND_REGISTRY = {
|
|
41
|
+
"env": EnvBackend,
|
|
42
|
+
"keyring": KeyringBackend,
|
|
43
|
+
"1password": OnePasswordBackend,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SecretsManager:
|
|
48
|
+
"""Unified secrets access layer for Colony.
|
|
49
|
+
|
|
50
|
+
Singleton — one instance per process, backend determined from config.
|
|
51
|
+
Use ``SecretsManager.reset()`` in tests to clear cached state.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
_instance: "SecretsManager | None" = None
|
|
55
|
+
|
|
56
|
+
def __new__(cls) -> "SecretsManager":
|
|
57
|
+
if cls._instance is None:
|
|
58
|
+
cls._instance = super().__new__(cls)
|
|
59
|
+
cls._instance._backend = cls._load_backend()
|
|
60
|
+
return cls._instance
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def _load_backend(cls) -> SecretsBackend:
|
|
64
|
+
_register_backends()
|
|
65
|
+
|
|
66
|
+
# Allow override via env var (useful for testing and CI)
|
|
67
|
+
backend_name = os.environ.get("COLONY_SECRETS_BACKEND")
|
|
68
|
+
|
|
69
|
+
if not backend_name:
|
|
70
|
+
backend_name = cls._read_config_backend()
|
|
71
|
+
|
|
72
|
+
if not backend_name:
|
|
73
|
+
backend_name = "env"
|
|
74
|
+
|
|
75
|
+
if backend_name not in _BACKEND_REGISTRY:
|
|
76
|
+
logger.warning(
|
|
77
|
+
"Unknown secrets backend %r — falling back to 'env'", backend_name
|
|
78
|
+
)
|
|
79
|
+
backend_name = "env"
|
|
80
|
+
|
|
81
|
+
backend_cls = _BACKEND_REGISTRY[backend_name]
|
|
82
|
+
backend = backend_cls()
|
|
83
|
+
|
|
84
|
+
if not backend.is_available():
|
|
85
|
+
logger.warning(
|
|
86
|
+
"Secrets backend %r is not available on this system — "
|
|
87
|
+
"falling back to 'env'. Run `colony secrets status` for details.",
|
|
88
|
+
backend_name,
|
|
89
|
+
)
|
|
90
|
+
backend = _BACKEND_REGISTRY["env"]()
|
|
91
|
+
|
|
92
|
+
return backend
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def _read_config_backend(cls) -> str | None:
|
|
96
|
+
"""Read secrets.backend from ~/.colony/config.yaml."""
|
|
97
|
+
try:
|
|
98
|
+
import yaml
|
|
99
|
+
from pathlib import Path
|
|
100
|
+
|
|
101
|
+
colony_home = Path(os.environ.get("COLONY_HOME", Path.home() / ".colony"))
|
|
102
|
+
config_path = colony_home / "config.yaml"
|
|
103
|
+
if not config_path.exists():
|
|
104
|
+
return None
|
|
105
|
+
with open(config_path, encoding="utf-8") as f:
|
|
106
|
+
cfg = yaml.safe_load(f) or {}
|
|
107
|
+
return cfg.get("secrets", {}).get("backend")
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
logger.debug("Could not read secrets backend from config: %s", exc)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
# -------------------------------------------------------------------------
|
|
113
|
+
# Public API
|
|
114
|
+
# -------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def get(self, key: str, default: str | None = None) -> str | None:
|
|
117
|
+
"""Retrieve a secret by key. Returns default if not found."""
|
|
118
|
+
return self._backend.get(key, default)
|
|
119
|
+
|
|
120
|
+
def get_required(self, key: str) -> str:
|
|
121
|
+
"""Retrieve a secret by key. Raises KeyError if not found."""
|
|
122
|
+
value = self._backend.get(key)
|
|
123
|
+
if value is None:
|
|
124
|
+
raise KeyError(
|
|
125
|
+
f"Required secret '{key}' not found in {self._backend.name} backend. "
|
|
126
|
+
f"Run `colony secrets set {key} <value>` to configure it."
|
|
127
|
+
)
|
|
128
|
+
return value
|
|
129
|
+
|
|
130
|
+
def set(self, key: str, value: str, *, secret_type: SecretType | None = None) -> None:
|
|
131
|
+
"""Store a secret."""
|
|
132
|
+
self._backend.set(key, value, secret_type=secret_type)
|
|
133
|
+
|
|
134
|
+
def delete(self, key: str) -> None:
|
|
135
|
+
"""Remove a secret."""
|
|
136
|
+
self._backend.delete(key)
|
|
137
|
+
|
|
138
|
+
def list(self) -> list[str]:
|
|
139
|
+
"""List all known secret keys (not values)."""
|
|
140
|
+
return self._backend.list()
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def backend_name(self) -> str:
|
|
144
|
+
return self._backend.name
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def backend(self) -> SecretsBackend:
|
|
148
|
+
return self._backend
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def reset(cls) -> None:
|
|
152
|
+
"""Clear the singleton — for testing only."""
|
|
153
|
+
cls._instance = None
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def with_backend(cls, backend: SecretsBackend) -> "SecretsManager":
|
|
157
|
+
"""Create a SecretsManager instance with a specific backend — for testing."""
|
|
158
|
+
instance = object.__new__(cls)
|
|
159
|
+
instance._backend = backend
|
|
160
|
+
return instance
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Auto-migration utilities for moving secrets between backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from apsimo.secrets.backends.base import SecretsBackend
|
|
9
|
+
from apsimo.secrets.types import infer_secret_type
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class MigrationResult:
|
|
16
|
+
"""Result summary from a secrets migration run."""
|
|
17
|
+
|
|
18
|
+
migrated: list[str] = field(default_factory=list)
|
|
19
|
+
skipped: list[str] = field(default_factory=list)
|
|
20
|
+
failed: list[tuple[str, str]] = field(default_factory=list)
|
|
21
|
+
would_migrate: list[str] = field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def success_count(self) -> int:
|
|
25
|
+
return len(self.migrated)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def failed_count(self) -> int:
|
|
29
|
+
return len(self.failed)
|
|
30
|
+
|
|
31
|
+
def summary(self) -> str:
|
|
32
|
+
parts = []
|
|
33
|
+
if self.would_migrate:
|
|
34
|
+
parts.append(f"Would migrate {len(self.would_migrate)} secrets (dry run)")
|
|
35
|
+
if self.migrated:
|
|
36
|
+
parts.append(f"Migrated {len(self.migrated)} secrets")
|
|
37
|
+
if self.skipped:
|
|
38
|
+
parts.append(f"Skipped {len(self.skipped)} (no value)")
|
|
39
|
+
if self.failed:
|
|
40
|
+
parts.append(f"Failed {len(self.failed)}")
|
|
41
|
+
return ", ".join(parts) if parts else "Nothing to migrate"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SecretsMigrator:
|
|
45
|
+
"""Detect existing secrets and migrate them between backends.
|
|
46
|
+
|
|
47
|
+
Called by the onboarding wizard when a user changes their secrets backend,
|
|
48
|
+
and optionally on first startup after a V2 upgrade if the user has a .env
|
|
49
|
+
file but has selected 1Password or keyring as their backend.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, source: SecretsBackend, target: SecretsBackend) -> None:
|
|
53
|
+
self._source = source
|
|
54
|
+
self._target = target
|
|
55
|
+
|
|
56
|
+
def preview(self) -> list[str]:
|
|
57
|
+
"""Return list of secret keys that would be migrated."""
|
|
58
|
+
return self._source.list()
|
|
59
|
+
|
|
60
|
+
def migrate(
|
|
61
|
+
self,
|
|
62
|
+
keys: list[str] | None = None,
|
|
63
|
+
*,
|
|
64
|
+
dry_run: bool = False,
|
|
65
|
+
delete_from_source: bool = False,
|
|
66
|
+
) -> MigrationResult:
|
|
67
|
+
"""Migrate secrets from source to target backend.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
keys: Specific keys to migrate. None = migrate all from source.
|
|
71
|
+
dry_run: If True, only report what would be migrated without writing.
|
|
72
|
+
delete_from_source: If True, delete from source after verifying target.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
MigrationResult with counts of migrated, skipped, and failed keys.
|
|
76
|
+
"""
|
|
77
|
+
to_migrate = keys if keys is not None else self._source.list()
|
|
78
|
+
result = MigrationResult()
|
|
79
|
+
|
|
80
|
+
for key in to_migrate:
|
|
81
|
+
value = self._source.get(key)
|
|
82
|
+
if value is None:
|
|
83
|
+
result.skipped.append(key)
|
|
84
|
+
continue
|
|
85
|
+
if dry_run:
|
|
86
|
+
result.would_migrate.append(key)
|
|
87
|
+
continue
|
|
88
|
+
try:
|
|
89
|
+
secret_type = infer_secret_type(key)
|
|
90
|
+
self._target.set(key, value, secret_type=secret_type)
|
|
91
|
+
# Verify the write succeeded
|
|
92
|
+
if self._target.get(key) != value:
|
|
93
|
+
raise RuntimeError("Read-back verification failed")
|
|
94
|
+
result.migrated.append(key)
|
|
95
|
+
if delete_from_source:
|
|
96
|
+
self._source.delete(key)
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
logger.warning("Failed to migrate secret %s: %s", key, exc)
|
|
99
|
+
result.failed.append((key, str(exc)))
|
|
100
|
+
|
|
101
|
+
return result
|