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,195 @@
|
|
|
1
|
+
"""Sandbox backends: pluggable isolated code execution (Phase B item 6).
|
|
2
|
+
|
|
3
|
+
The backend -- NOT the caller -- enforces containment: no egress, no
|
|
4
|
+
credentials, capped CPU/memory/pids/wall-clock, read-only rootfs with a single
|
|
5
|
+
writable workdir. A tool can ask to run a script; it can never widen these
|
|
6
|
+
limits (they come from env/policy, resolved by the manager, and are passed to
|
|
7
|
+
the backend which applies them literally).
|
|
8
|
+
|
|
9
|
+
Reference backend is Docker; a DisabledSandbox is the default when Docker is
|
|
10
|
+
absent so the feature degrades to a clear "unavailable" rather than an unsafe
|
|
11
|
+
host-side exec.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import tempfile
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Dict, List, Optional
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
_LANG_INTERP = {
|
|
28
|
+
"python": ["python", "/work/script"],
|
|
29
|
+
"py": ["python", "/work/script"],
|
|
30
|
+
"bash": ["sh", "/work/script"],
|
|
31
|
+
"sh": ["sh", "/work/script"],
|
|
32
|
+
"node": ["node", "/work/script"],
|
|
33
|
+
"javascript": ["node", "/work/script"],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class SandboxLimits:
|
|
39
|
+
"""Containment limits, resolved from policy/env and applied by the backend."""
|
|
40
|
+
image: str = "python:3.12-slim"
|
|
41
|
+
cpus: float = 1.0
|
|
42
|
+
memory: str = "512m"
|
|
43
|
+
timeout_secs: int = 30
|
|
44
|
+
pids_limit: int = 128
|
|
45
|
+
egress: str = "none" # none | allowlist
|
|
46
|
+
max_artifact_bytes: int = 1_048_576 # 1 MiB total read-back cap
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def network(self) -> str:
|
|
50
|
+
# Only "none" is honored as a hard no-egress; allowlist requires an
|
|
51
|
+
# egress proxy the deployment provides (documented, not default).
|
|
52
|
+
return "none" if self.egress == "none" else self.egress
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class SandboxResult:
|
|
57
|
+
stdout: str = ""
|
|
58
|
+
stderr: str = ""
|
|
59
|
+
exit_code: Optional[int] = None
|
|
60
|
+
timed_out: bool = False
|
|
61
|
+
artifacts: Dict[str, str] = field(default_factory=dict)
|
|
62
|
+
error: str = ""
|
|
63
|
+
|
|
64
|
+
def as_dict(self) -> Dict[str, object]:
|
|
65
|
+
return {
|
|
66
|
+
"stdout": self.stdout, "stderr": self.stderr,
|
|
67
|
+
"exit_code": self.exit_code, "timed_out": self.timed_out,
|
|
68
|
+
"artifacts": list(self.artifacts.keys()),
|
|
69
|
+
"artifact_preview": {k: v[:500] for k, v in self.artifacts.items()},
|
|
70
|
+
"error": self.error,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SandboxBackend(ABC):
|
|
75
|
+
name = "abstract"
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def available(self) -> bool:
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def run(self, script: str, lang: str, limits: SandboxLimits) -> SandboxResult:
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DisabledSandbox(SandboxBackend):
|
|
87
|
+
"""Default backend when no isolation is available. Never executes."""
|
|
88
|
+
name = "disabled"
|
|
89
|
+
|
|
90
|
+
def __init__(self, reason: str = "no container runtime available") -> None:
|
|
91
|
+
self._reason = reason
|
|
92
|
+
|
|
93
|
+
def available(self) -> bool:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
def run(self, script: str, lang: str, limits: SandboxLimits) -> SandboxResult:
|
|
97
|
+
return SandboxResult(error=f"sandbox unavailable: {self._reason}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class DockerSandbox(SandboxBackend):
|
|
101
|
+
"""Ephemeral-container backend: no egress, no creds, capped resources."""
|
|
102
|
+
name = "docker"
|
|
103
|
+
|
|
104
|
+
def __init__(self, docker_bin: str = "docker") -> None:
|
|
105
|
+
self._docker = docker_bin
|
|
106
|
+
|
|
107
|
+
def available(self) -> bool:
|
|
108
|
+
return shutil.which(self._docker) is not None
|
|
109
|
+
|
|
110
|
+
def build_command(self, workdir: str, lang: str,
|
|
111
|
+
limits: SandboxLimits) -> List[str]:
|
|
112
|
+
"""The exact docker invocation (pure -- unit-tested for containment).
|
|
113
|
+
|
|
114
|
+
Guarantees: --network none (no egress), no -e/env (no credentials),
|
|
115
|
+
--read-only rootfs with a single writable bind workdir, dropped
|
|
116
|
+
capabilities + no-new-privileges, hard CPU/memory/pids caps, and a
|
|
117
|
+
wall-clock timeout inside the container.
|
|
118
|
+
"""
|
|
119
|
+
interp = _LANG_INTERP.get((lang or "python").lower(),
|
|
120
|
+
_LANG_INTERP["python"])
|
|
121
|
+
return [
|
|
122
|
+
self._docker, "run", "--rm",
|
|
123
|
+
"--network", limits.network,
|
|
124
|
+
"--read-only",
|
|
125
|
+
"--tmpfs", "/tmp:rw,size=64m",
|
|
126
|
+
"-v", f"{workdir}:/work:rw",
|
|
127
|
+
"-w", "/work",
|
|
128
|
+
"--cpus", str(limits.cpus),
|
|
129
|
+
"--memory", str(limits.memory),
|
|
130
|
+
"--pids-limit", str(limits.pids_limit),
|
|
131
|
+
"--security-opt", "no-new-privileges",
|
|
132
|
+
"--cap-drop", "ALL",
|
|
133
|
+
limits.image,
|
|
134
|
+
"timeout", str(limits.timeout_secs), *interp,
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
def run(self, script: str, lang: str, limits: SandboxLimits) -> SandboxResult:
|
|
138
|
+
workdir = tempfile.mkdtemp(prefix="colony-sandbox-")
|
|
139
|
+
try:
|
|
140
|
+
script_path = os.path.join(workdir, "script")
|
|
141
|
+
with open(script_path, "w") as f:
|
|
142
|
+
f.write(script or "")
|
|
143
|
+
cmd = self.build_command(workdir, lang, limits)
|
|
144
|
+
try:
|
|
145
|
+
proc = subprocess.run(
|
|
146
|
+
cmd, capture_output=True, text=True,
|
|
147
|
+
timeout=limits.timeout_secs + 10) # outer guard > inner
|
|
148
|
+
except subprocess.TimeoutExpired:
|
|
149
|
+
return SandboxResult(timed_out=True,
|
|
150
|
+
error="wall-clock timeout")
|
|
151
|
+
except Exception as exc:
|
|
152
|
+
return SandboxResult(error=f"backend error: {exc}")
|
|
153
|
+
result = SandboxResult(
|
|
154
|
+
stdout=(proc.stdout or "")[:100_000],
|
|
155
|
+
stderr=(proc.stderr or "")[:20_000],
|
|
156
|
+
exit_code=proc.returncode,
|
|
157
|
+
timed_out=(proc.returncode == 124)) # timeout(1) exit code
|
|
158
|
+
self._read_artifacts(workdir, script_path, limits, result)
|
|
159
|
+
return result
|
|
160
|
+
finally:
|
|
161
|
+
shutil.rmtree(workdir, ignore_errors=True)
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _read_artifacts(workdir: str, script_path: str,
|
|
165
|
+
limits: SandboxLimits, result: SandboxResult) -> None:
|
|
166
|
+
budget = limits.max_artifact_bytes
|
|
167
|
+
for root, _dirs, files in os.walk(workdir):
|
|
168
|
+
for fn in files:
|
|
169
|
+
path = os.path.join(root, fn)
|
|
170
|
+
if path == script_path:
|
|
171
|
+
continue
|
|
172
|
+
if budget <= 0:
|
|
173
|
+
result.error = (result.error
|
|
174
|
+
or "artifact size cap reached").strip()
|
|
175
|
+
return
|
|
176
|
+
try:
|
|
177
|
+
with open(path, "rb") as fh:
|
|
178
|
+
data = fh.read(budget + 1)
|
|
179
|
+
except Exception:
|
|
180
|
+
continue
|
|
181
|
+
if len(data) > budget:
|
|
182
|
+
result.error = "artifact size cap reached"
|
|
183
|
+
data = data[:budget]
|
|
184
|
+
budget -= len(data)
|
|
185
|
+
rel = os.path.relpath(path, workdir)
|
|
186
|
+
result.artifacts[rel] = data.decode("utf-8", "replace")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def select_backend(mode: str) -> SandboxBackend:
|
|
190
|
+
"""Pick a backend: Docker when present, else Disabled. Mode gates whether
|
|
191
|
+
the manager actually executes, not which backend exists."""
|
|
192
|
+
docker = DockerSandbox()
|
|
193
|
+
if docker.available():
|
|
194
|
+
return docker
|
|
195
|
+
return DisabledSandbox()
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""SandboxManager -- policy front end for gated isolated execution (item 6).
|
|
2
|
+
|
|
3
|
+
Wraps a backend with the safety envelope:
|
|
4
|
+
- mode gate: off (never), dry_run (validate + log the command, run nothing),
|
|
5
|
+
live (execute in the backend).
|
|
6
|
+
- boundary gate: the purpose + script are checked against DirectiveGuard
|
|
7
|
+
(ACT capability, fail-closed); a boundaried subject blocks the run.
|
|
8
|
+
- approval tiering: an owner-directed experiment or a contained read-only
|
|
9
|
+
evaluation runs AUTO; other mutations are FLAGGED for owner approval.
|
|
10
|
+
- server-side limits: the caller cannot widen containment -- limits are
|
|
11
|
+
resolved here from env and handed to the backend, which applies them.
|
|
12
|
+
- never mounts secrets: the backend passes no env/credentials into the
|
|
13
|
+
container.
|
|
14
|
+
|
|
15
|
+
Every run (auto, flagged, dry-run, blocked) is journaled through the
|
|
16
|
+
self-model action journal.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
from typing import Any, Dict, Optional
|
|
24
|
+
|
|
25
|
+
from apsimo.sandbox.backend import (
|
|
26
|
+
SandboxLimits, SandboxResult, select_backend,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def sandbox_mode() -> str:
|
|
33
|
+
from apsimo.util.autonomy_preset import resolve
|
|
34
|
+
return resolve("COLONY_SANDBOX_MODE", ("off", "dry_run", "live"), "off")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _fenv(name: str, default: float) -> float:
|
|
38
|
+
try:
|
|
39
|
+
return float(os.environ.get(name, str(default)))
|
|
40
|
+
except (TypeError, ValueError):
|
|
41
|
+
return default
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _ienv(name: str, default: int) -> int:
|
|
45
|
+
try:
|
|
46
|
+
return int(os.environ.get(name, str(default)))
|
|
47
|
+
except (TypeError, ValueError):
|
|
48
|
+
return default
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def resolve_limits() -> SandboxLimits:
|
|
52
|
+
"""Containment limits from env/policy. The caller can never widen these."""
|
|
53
|
+
return SandboxLimits(
|
|
54
|
+
image=os.environ.get("COLONY_SANDBOX_IMAGE", "python:3.12-slim"),
|
|
55
|
+
cpus=_fenv("COLONY_SANDBOX_CPUS", 1.0),
|
|
56
|
+
memory=os.environ.get("COLONY_SANDBOX_MEMORY", "512m"),
|
|
57
|
+
timeout_secs=_ienv("COLONY_SANDBOX_TIMEOUT", 30),
|
|
58
|
+
pids_limit=_ienv("COLONY_SANDBOX_PIDS", 128),
|
|
59
|
+
egress=os.environ.get("COLONY_SANDBOX_EGRESS", "none").strip().lower(),
|
|
60
|
+
max_artifact_bytes=_ienv("COLONY_SANDBOX_MAX_ARTIFACT_BYTES", 1_048_576),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SandboxManager:
|
|
65
|
+
def __init__(self, *, directive_manager: Any = None,
|
|
66
|
+
self_model: Any = None) -> None:
|
|
67
|
+
self._directives = directive_manager
|
|
68
|
+
self._self_model = self_model
|
|
69
|
+
self._backend = select_backend(sandbox_mode())
|
|
70
|
+
|
|
71
|
+
def backend_name(self) -> str:
|
|
72
|
+
return self._backend.name
|
|
73
|
+
|
|
74
|
+
# -- approval tiering -------------------------------------------------
|
|
75
|
+
@staticmethod
|
|
76
|
+
def approval_tier(owner_directed: bool, read_only: bool = False) -> str:
|
|
77
|
+
"""Keep contained read-only work fast without inventing authority."""
|
|
78
|
+
|
|
79
|
+
return "auto" if owner_directed or read_only else "flagged"
|
|
80
|
+
|
|
81
|
+
# -- boundary gate ----------------------------------------------------
|
|
82
|
+
def _boundary_ok(self, purpose: str, script: str) -> Dict[str, Any]:
|
|
83
|
+
if self._directives is None:
|
|
84
|
+
# No directive manager: nothing was evaluated. Never report the
|
|
85
|
+
# fabricated "ok" of a real allow — callers/journals must be able
|
|
86
|
+
# to tell "unchecked" from "checked and allowed".
|
|
87
|
+
return {"allowed": True, "reason": "boundary_unchecked"}
|
|
88
|
+
try:
|
|
89
|
+
from apsimo.directives import Action
|
|
90
|
+
verdict = self._directives.check(Action(
|
|
91
|
+
kind="execute_tool",
|
|
92
|
+
text=f"run sandbox script: {purpose}",
|
|
93
|
+
target=purpose,
|
|
94
|
+
args={"script": (script or "")[:500]},
|
|
95
|
+
high_risk=True))
|
|
96
|
+
return {"allowed": bool(verdict.allowed), "reason": verdict.reason}
|
|
97
|
+
except Exception:
|
|
98
|
+
logger.warning("sandbox boundary check failed closed", exc_info=True)
|
|
99
|
+
return {"allowed": False, "reason": "boundary_check_error"}
|
|
100
|
+
|
|
101
|
+
# -- the run entry point ----------------------------------------------
|
|
102
|
+
def run(self, script: str, lang: str = "python", *, purpose: str = "",
|
|
103
|
+
owner_directed: bool = False,
|
|
104
|
+
approved: bool = False,
|
|
105
|
+
read_only: bool = False) -> Dict[str, Any]:
|
|
106
|
+
mode = sandbox_mode()
|
|
107
|
+
limits = resolve_limits()
|
|
108
|
+
tier = self.approval_tier(owner_directed, read_only)
|
|
109
|
+
|
|
110
|
+
if mode == "off":
|
|
111
|
+
self._journal("sandbox", purpose, decision="held",
|
|
112
|
+
reasoning="sandbox mode off")
|
|
113
|
+
return {"ran": False, "reason": "sandbox_off", "mode": mode}
|
|
114
|
+
|
|
115
|
+
boundary = self._boundary_ok(purpose, script)
|
|
116
|
+
if not boundary["allowed"]:
|
|
117
|
+
self._journal("sandbox", purpose, decision="blocked",
|
|
118
|
+
reasoning=f"boundary: {boundary['reason']}")
|
|
119
|
+
return {"ran": False, "reason": "boundary_blocked",
|
|
120
|
+
"detail": boundary["reason"], "mode": mode}
|
|
121
|
+
|
|
122
|
+
if tier == "flagged" and not approved:
|
|
123
|
+
self._journal("sandbox", purpose, decision="asked",
|
|
124
|
+
reasoning="not owner-directed -> owner approval required")
|
|
125
|
+
return {"ran": False, "reason": "approval_required",
|
|
126
|
+
"tier": tier, "mode": mode}
|
|
127
|
+
|
|
128
|
+
if mode == "dry_run":
|
|
129
|
+
from apsimo.sandbox.backend import DockerSandbox
|
|
130
|
+
command = DockerSandbox().build_command("<workdir>", lang, limits)
|
|
131
|
+
self._journal("sandbox", purpose, decision="held",
|
|
132
|
+
reasoning="dry_run: validated, executed nothing")
|
|
133
|
+
return {"ran": False, "dry_run": True, "tier": tier,
|
|
134
|
+
"read_only": bool(read_only),
|
|
135
|
+
"command": command, "limits": limits.__dict__, "mode": mode}
|
|
136
|
+
|
|
137
|
+
# live
|
|
138
|
+
if not self._backend.available():
|
|
139
|
+
self._journal("sandbox", purpose, decision="held",
|
|
140
|
+
reasoning=f"backend {self._backend.name} unavailable")
|
|
141
|
+
return {"ran": False, "reason": "backend_unavailable",
|
|
142
|
+
"backend": self._backend.name, "mode": mode}
|
|
143
|
+
|
|
144
|
+
result: SandboxResult = self._backend.run(script, lang, limits)
|
|
145
|
+
outcome = ("timeout" if result.timed_out else
|
|
146
|
+
"success" if (result.exit_code == 0 and not result.error)
|
|
147
|
+
else "failure")
|
|
148
|
+
if self._self_model is not None:
|
|
149
|
+
try:
|
|
150
|
+
self._self_model.record("sandbox", outcome)
|
|
151
|
+
except Exception:
|
|
152
|
+
logger.debug("sandbox self-model record failed", exc_info=True)
|
|
153
|
+
self._journal("sandbox", purpose, decision="acted", outcome=outcome,
|
|
154
|
+
reasoning=f"tier={tier}, exit={result.exit_code}")
|
|
155
|
+
return {"ran": True, "tier": tier, "read_only": bool(read_only),
|
|
156
|
+
"mode": mode, "outcome": outcome,
|
|
157
|
+
"result": result.as_dict()}
|
|
158
|
+
|
|
159
|
+
def status(self) -> Dict[str, Any]:
|
|
160
|
+
return {"mode": sandbox_mode(), "backend": self._backend.name,
|
|
161
|
+
"backend_available": self._backend.available(),
|
|
162
|
+
"limits": resolve_limits().__dict__}
|
|
163
|
+
|
|
164
|
+
# -- helpers ----------------------------------------------------------
|
|
165
|
+
def _journal(self, domain: str, description: str, **kw: Any) -> None:
|
|
166
|
+
journal = getattr(self._self_model, "journal", None)
|
|
167
|
+
if journal is None:
|
|
168
|
+
return
|
|
169
|
+
try:
|
|
170
|
+
journal.record(domain, description or "(sandbox run)",
|
|
171
|
+
reversibility="reversible", **kw)
|
|
172
|
+
except Exception:
|
|
173
|
+
logger.debug("sandbox journal failed", exc_info=True)
|
apsimo/scope_bounds.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Colony secrets management — pluggable credential vault."""
|
|
2
|
+
|
|
3
|
+
from apsimo.secrets.manager import SecretsManager
|
|
4
|
+
from apsimo.secrets.types import SecretType, ALL_SECRET_KEYS, VAULT_NAME
|
|
5
|
+
|
|
6
|
+
__all__ = ["SecretsManager", "SecretType", "ALL_SECRET_KEYS", "VAULT_NAME"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Secrets backend implementations."""
|
|
2
|
+
|
|
3
|
+
from apsimo.secrets.backends.base import SecretsBackend
|
|
4
|
+
from apsimo.secrets.backends.env import EnvBackend
|
|
5
|
+
from apsimo.secrets.backends.keyring import KeyringBackend
|
|
6
|
+
from apsimo.secrets.backends.onepassword import OnePasswordBackend
|
|
7
|
+
|
|
8
|
+
__all__ = ["SecretsBackend", "EnvBackend", "KeyringBackend", "OnePasswordBackend"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Abstract base class for Colony secrets backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
from apsimo.secrets.types import SecretType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SecretsBackend(ABC):
|
|
11
|
+
"""Abstract secrets storage backend.
|
|
12
|
+
|
|
13
|
+
All backends must implement get/set/delete/list and is_available.
|
|
14
|
+
Backends are selected during onboarding and persisted in config.yaml.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
name: str = "abstract"
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def get(self, key: str, default: str | None = None) -> str | None:
|
|
21
|
+
"""Retrieve a secret by key. Returns default if not found."""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def set(self, key: str, value: str, *, secret_type: SecretType | None = None) -> None:
|
|
26
|
+
"""Store a secret."""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def delete(self, key: str) -> None:
|
|
31
|
+
"""Remove a secret."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def list(self) -> list[str]:
|
|
36
|
+
"""List all known secret keys (not values)."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def is_available(self) -> bool:
|
|
41
|
+
"""Return True if this backend can be used on the current system."""
|
|
42
|
+
...
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Legacy .env file secrets backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from apsimo.secrets.backends.base import SecretsBackend
|
|
9
|
+
from apsimo.secrets.types import SecretType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EnvBackend(SecretsBackend):
|
|
13
|
+
"""Legacy .env file backend.
|
|
14
|
+
|
|
15
|
+
Reads from environment variables (which may be loaded from .env by
|
|
16
|
+
python-dotenv at startup). Writes update the .env file in place.
|
|
17
|
+
|
|
18
|
+
This backend is maintained for backward compatibility and for users
|
|
19
|
+
in containerized environments who inject secrets via environment variables.
|
|
20
|
+
It is the DEFAULT backend when no other backend is configured, ensuring
|
|
21
|
+
zero-friction upgrade from V1.
|
|
22
|
+
|
|
23
|
+
The .env file is always written with mode 0o600 (SEC-007).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
name = "env"
|
|
27
|
+
|
|
28
|
+
def __init__(self, env_path: str | None = None) -> None:
|
|
29
|
+
self._env_path = env_path or str(
|
|
30
|
+
Path.home() / ".colony" / ".env"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def is_available(self) -> bool:
|
|
34
|
+
return True # Always available
|
|
35
|
+
|
|
36
|
+
def get(self, key: str, default: str | None = None) -> str | None:
|
|
37
|
+
v = os.environ.get(key)
|
|
38
|
+
if v is not None:
|
|
39
|
+
return v
|
|
40
|
+
# Fall back to the .env file itself: set() writes there, and a fresh
|
|
41
|
+
# process (CLI, migration) has NOT loaded it into os.environ — without
|
|
42
|
+
# this read-back, get() returns None for every secret set() stored.
|
|
43
|
+
env_path = Path(self._env_path)
|
|
44
|
+
if env_path.exists():
|
|
45
|
+
try:
|
|
46
|
+
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
47
|
+
if line.startswith(f"{key}="):
|
|
48
|
+
return line.split("=", 1)[1]
|
|
49
|
+
except OSError:
|
|
50
|
+
pass
|
|
51
|
+
return default
|
|
52
|
+
|
|
53
|
+
def set(self, key: str, value: str, *, secret_type: SecretType | None = None) -> None:
|
|
54
|
+
"""Write to .env file with 0o600 permissions (SEC-007 pattern)."""
|
|
55
|
+
env_path = Path(self._env_path)
|
|
56
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
lines: list[str] = []
|
|
59
|
+
if env_path.exists():
|
|
60
|
+
lines = env_path.read_text(encoding="utf-8").splitlines(keepends=True)
|
|
61
|
+
|
|
62
|
+
key_found = False
|
|
63
|
+
for i, line in enumerate(lines):
|
|
64
|
+
if line.startswith(f"{key}="):
|
|
65
|
+
lines[i] = f"{key}={value}\n"
|
|
66
|
+
key_found = True
|
|
67
|
+
break
|
|
68
|
+
if not key_found:
|
|
69
|
+
lines.append(f"{key}={value}\n")
|
|
70
|
+
|
|
71
|
+
# Write atomically with 0o600 (SEC-007: no TOCTOU window)
|
|
72
|
+
fd = os.open(
|
|
73
|
+
str(env_path),
|
|
74
|
+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
|
|
75
|
+
0o600,
|
|
76
|
+
)
|
|
77
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
78
|
+
f.writelines(lines)
|
|
79
|
+
|
|
80
|
+
os.environ[key] = value
|
|
81
|
+
|
|
82
|
+
def delete(self, key: str) -> None:
|
|
83
|
+
env_path = Path(self._env_path)
|
|
84
|
+
if not env_path.exists():
|
|
85
|
+
os.environ.pop(key, None)
|
|
86
|
+
return
|
|
87
|
+
lines = [
|
|
88
|
+
line
|
|
89
|
+
for line in env_path.read_text(encoding="utf-8").splitlines(keepends=True)
|
|
90
|
+
if not line.startswith(f"{key}=")
|
|
91
|
+
]
|
|
92
|
+
fd = os.open(
|
|
93
|
+
str(env_path),
|
|
94
|
+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
|
|
95
|
+
0o600,
|
|
96
|
+
)
|
|
97
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
98
|
+
f.writelines(lines)
|
|
99
|
+
os.environ.pop(key, None)
|
|
100
|
+
|
|
101
|
+
def list(self) -> list[str]:
|
|
102
|
+
env_path = Path(self._env_path)
|
|
103
|
+
if not env_path.exists():
|
|
104
|
+
return []
|
|
105
|
+
keys = []
|
|
106
|
+
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
107
|
+
stripped = line.strip()
|
|
108
|
+
if stripped and not stripped.startswith("#") and "=" in stripped:
|
|
109
|
+
keys.append(stripped.split("=", 1)[0])
|
|
110
|
+
return keys
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""System keyring secrets backend (macOS Keychain, Linux SecretService, Windows Credential Manager)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from apsimo.secrets.backends.base import SecretsBackend
|
|
8
|
+
from apsimo.secrets.types import SecretType, ALL_SECRET_KEYS
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
_SERVICE = "colony-ai"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class KeyringBackend(SecretsBackend):
|
|
16
|
+
"""System keyring backend using the ``keyring`` library.
|
|
17
|
+
|
|
18
|
+
macOS: stores in Keychain.
|
|
19
|
+
Linux: uses SecretService (GNOME Keyring or KWallet).
|
|
20
|
+
Windows: uses Windows Credential Manager.
|
|
21
|
+
|
|
22
|
+
Requires: pip install keyring (or colony[keyring] optional dep)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
name = "keyring"
|
|
26
|
+
|
|
27
|
+
def is_available(self) -> bool:
|
|
28
|
+
try:
|
|
29
|
+
import keyring
|
|
30
|
+
import keyring.errors
|
|
31
|
+
except ImportError:
|
|
32
|
+
return False
|
|
33
|
+
try:
|
|
34
|
+
# Probe with a read to verify the backend is actually usable.
|
|
35
|
+
# get_keyring() alone doesn't raise on headless systems; a real
|
|
36
|
+
# read/write attempt is required to confirm a backend exists.
|
|
37
|
+
keyring.get_password(_SERVICE, "_colony_probe_")
|
|
38
|
+
return True
|
|
39
|
+
except keyring.errors.NoKeyringError:
|
|
40
|
+
return False
|
|
41
|
+
except Exception:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
def get(self, key: str, default: str | None = None) -> str | None:
|
|
45
|
+
try:
|
|
46
|
+
import keyring
|
|
47
|
+
value = keyring.get_password(_SERVICE, key)
|
|
48
|
+
return value if value is not None else default
|
|
49
|
+
except Exception:
|
|
50
|
+
return default
|
|
51
|
+
|
|
52
|
+
def set(self, key: str, value: str, *, secret_type: SecretType | None = None) -> None:
|
|
53
|
+
import keyring
|
|
54
|
+
keyring.set_password(_SERVICE, key, value)
|
|
55
|
+
|
|
56
|
+
def delete(self, key: str) -> None:
|
|
57
|
+
try:
|
|
58
|
+
import keyring
|
|
59
|
+
import keyring.errors
|
|
60
|
+
keyring.delete_password(_SERVICE, key)
|
|
61
|
+
except keyring.errors.PasswordDeleteError:
|
|
62
|
+
pass # Key didn't exist — idempotent delete
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
logger.warning("KeyringBackend: failed to delete secret %r: %s", key, exc)
|
|
65
|
+
|
|
66
|
+
def list(self) -> list[str]:
|
|
67
|
+
"""Return all configured keys by probing ALL_SECRET_KEYS.
|
|
68
|
+
|
|
69
|
+
The keyring library does not support listing all keys for a service,
|
|
70
|
+
so we iterate the known key set and check which ones are set.
|
|
71
|
+
"""
|
|
72
|
+
return [k for k in sorted(ALL_SECRET_KEYS) if self.get(k) is not None]
|