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,559 @@
|
|
|
1
|
+
"""Toolsmith engine: draft -> verify -> shadow -> graduate (Mind M1).
|
|
2
|
+
|
|
3
|
+
Each stage composes shipped infrastructure:
|
|
4
|
+
draft LLM router writes source + input schema + a self-contained test
|
|
5
|
+
verify the test runs inside the egress-none Docker sandbox (replay)
|
|
6
|
+
shadow incumbent/candidate comparisons on bounded captured inputs
|
|
7
|
+
graduate one-shot owner authority publishes one exact artifact
|
|
8
|
+
retire unused or failing tools demote automatically
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
from apsimo.toolsmith.authority import GraduationAuthorityV1
|
|
21
|
+
from apsimo.toolsmith.integrity import (
|
|
22
|
+
MAX_CAPTURE_BYTES,
|
|
23
|
+
IntegrityError,
|
|
24
|
+
candidate_digest,
|
|
25
|
+
canonical_json,
|
|
26
|
+
digest_json,
|
|
27
|
+
)
|
|
28
|
+
from apsimo.toolsmith.miner import ToolCandidate, ToolsmithMiner
|
|
29
|
+
from apsimo.toolsmith.policy import validate_pure_tool
|
|
30
|
+
from apsimo.toolsmith.registry import Tool, ToolRegistry, ToolStatus
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
TRUST_DOMAIN = "toolsmith"
|
|
35
|
+
_VERDICT_RE = re.compile(r"@@VERDICT@@\s*(\{.*\})", re.DOTALL)
|
|
36
|
+
_JSON_RE = re.compile(r"\{.*\}", re.DOTALL)
|
|
37
|
+
_CAPTURE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/-]{7,127}$")
|
|
38
|
+
_CAPTURE_SOURCES = frozenset({"real", "captured", "operator_canary", "production_trace"})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def toolsmith_enabled() -> bool:
|
|
42
|
+
return os.environ.get(
|
|
43
|
+
"COLONY_TOOLSMITH", "off").strip().lower() in ("shadow", "live")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def toolsmith_mode() -> str:
|
|
47
|
+
m = os.environ.get("COLONY_TOOLSMITH", "off").strip().lower()
|
|
48
|
+
return m if m in ("off", "shadow", "live") else "off"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _shadow_min() -> int:
|
|
52
|
+
try:
|
|
53
|
+
return max(1, min(100, int(os.environ.get(
|
|
54
|
+
"COLONY_TOOLSMITH_SHADOW_MIN", "5"))))
|
|
55
|
+
except ValueError:
|
|
56
|
+
return 5
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_DRAFT_SYSTEM = """You write small, dependency-free Python tools for an agent.
|
|
60
|
+
|
|
61
|
+
Given a recurring procedure the agent performs, output ONE JSON object (no
|
|
62
|
+
prose) with exactly these keys:
|
|
63
|
+
name snake_case identifier, 3-48 chars, [a-z][a-z0-9_]*
|
|
64
|
+
description one sentence: what the tool does and when to call it
|
|
65
|
+
input_schema JSON-Schema "properties" object for the tool's arguments
|
|
66
|
+
source_code a Python module defining `def run(**kwargs) -> dict:`
|
|
67
|
+
test_source Python that imports nothing external, calls run() with
|
|
68
|
+
representative inputs, and asserts on the result
|
|
69
|
+
|
|
70
|
+
Hard rules:
|
|
71
|
+
- Standard library only. No network, no file writes, no subprocess, no
|
|
72
|
+
environment access. The tool runs in a locked-down sandbox with no egress.
|
|
73
|
+
- `run` must be pure and deterministic given its inputs.
|
|
74
|
+
- The test must exercise run() and raise AssertionError on wrong output.
|
|
75
|
+
- If the procedure cannot be expressed as a safe pure function, output
|
|
76
|
+
{"name": null} and nothing else."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Toolsmith:
|
|
80
|
+
def __init__(self, registry: ToolRegistry, *, miner: Optional[ToolsmithMiner] = None,
|
|
81
|
+
sandbox: Any = None, self_model: Any = None,
|
|
82
|
+
router: Any = None) -> None:
|
|
83
|
+
self.registry = registry
|
|
84
|
+
self.miner = miner or ToolsmithMiner(registry=registry)
|
|
85
|
+
self._sandbox = sandbox
|
|
86
|
+
self._self_model = self_model
|
|
87
|
+
self._router = router
|
|
88
|
+
|
|
89
|
+
# -- lazy wiring ------------------------------------------------------
|
|
90
|
+
def _sb(self) -> Any:
|
|
91
|
+
if self._sandbox is not None:
|
|
92
|
+
return self._sandbox
|
|
93
|
+
try:
|
|
94
|
+
from apsimo.api.routers import host
|
|
95
|
+
return getattr(host, "_sandbox", None)
|
|
96
|
+
except Exception:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def _sm(self) -> Any:
|
|
100
|
+
if self._self_model is not None:
|
|
101
|
+
return self._self_model
|
|
102
|
+
try:
|
|
103
|
+
from apsimo.api.routers import host
|
|
104
|
+
return getattr(host, "_self_model", None)
|
|
105
|
+
except Exception:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def _llm(self) -> Any:
|
|
109
|
+
if self._router is not None:
|
|
110
|
+
return self._router
|
|
111
|
+
try:
|
|
112
|
+
from apsimo.api.routers import host
|
|
113
|
+
return getattr(host, "_llm_router", None)
|
|
114
|
+
except Exception:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
def _journal(self, desc: str, *, decision: str, outcome: str = "",
|
|
118
|
+
ref: str = "") -> None:
|
|
119
|
+
sm = self._sm()
|
|
120
|
+
journal = getattr(sm, "journal", None) if sm is not None else None
|
|
121
|
+
if journal is None:
|
|
122
|
+
return
|
|
123
|
+
try:
|
|
124
|
+
journal.record(TRUST_DOMAIN, desc, decision=decision,
|
|
125
|
+
outcome=outcome, ref=ref)
|
|
126
|
+
except Exception:
|
|
127
|
+
logger.debug("toolsmith journal write failed", exc_info=True)
|
|
128
|
+
|
|
129
|
+
# -- draft ------------------------------------------------------------
|
|
130
|
+
async def draft(self, candidate: ToolCandidate) -> Optional[Tool]:
|
|
131
|
+
router = self._llm()
|
|
132
|
+
if router is None:
|
|
133
|
+
return None
|
|
134
|
+
samples = "\n".join(f"- {d}" for d in candidate.sample_descriptions[:5])
|
|
135
|
+
prompt = (
|
|
136
|
+
f"Recurring procedure in domain '{candidate.domain}', observed "
|
|
137
|
+
f"{candidate.occurrences} times. Representative descriptions:\n"
|
|
138
|
+
f"{samples}\n\nWrite the tool.")
|
|
139
|
+
try:
|
|
140
|
+
resp = await router.complete(
|
|
141
|
+
[{"role": "system", "content": _DRAFT_SYSTEM},
|
|
142
|
+
{"role": "user", "content": prompt}],
|
|
143
|
+
context={"task": "toolsmith_draft"})
|
|
144
|
+
spec = self._parse_spec(getattr(resp, "content", "") or "")
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
logger.warning("toolsmith draft LLM failed: %s", exc)
|
|
147
|
+
return None
|
|
148
|
+
if not spec or not spec.get("name"):
|
|
149
|
+
return None
|
|
150
|
+
static_verdict = validate_pure_tool(
|
|
151
|
+
spec["source_code"], spec.get("test_source", ""))
|
|
152
|
+
if not static_verdict.allowed:
|
|
153
|
+
self._journal(
|
|
154
|
+
"rejected drafted tool at static policy",
|
|
155
|
+
decision="blocked",
|
|
156
|
+
outcome="failure",
|
|
157
|
+
ref=candidate_digest(candidate),
|
|
158
|
+
)
|
|
159
|
+
return None
|
|
160
|
+
tool = self.registry.create_draft(
|
|
161
|
+
name=spec["name"], description=spec.get("description", "")[:300],
|
|
162
|
+
source_code=spec["source_code"],
|
|
163
|
+
input_schema=spec.get("input_schema") or {},
|
|
164
|
+
test_source=spec.get("test_source", ""),
|
|
165
|
+
origin_kind=candidate and "mined" or "requested",
|
|
166
|
+
evidence=candidate.evidence if candidate else [],
|
|
167
|
+
candidate_digest=candidate_digest(candidate))
|
|
168
|
+
if tool is not None:
|
|
169
|
+
self._journal(
|
|
170
|
+
f"drafted tool {tool.name} from {candidate.occurrences} "
|
|
171
|
+
f"occurrences", decision="acted", ref=tool.tool_id)
|
|
172
|
+
return tool
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def _parse_spec(content: str) -> Optional[Dict[str, Any]]:
|
|
176
|
+
m = _JSON_RE.search(content)
|
|
177
|
+
if not m:
|
|
178
|
+
return None
|
|
179
|
+
try:
|
|
180
|
+
spec = json.loads(m.group(0))
|
|
181
|
+
except json.JSONDecodeError:
|
|
182
|
+
return None
|
|
183
|
+
if spec.get("name") in (None, "", "null"):
|
|
184
|
+
return None
|
|
185
|
+
for k in ("source_code", "test_source"):
|
|
186
|
+
if not isinstance(spec.get(k), str) or not spec[k].strip():
|
|
187
|
+
return None
|
|
188
|
+
if not ToolRegistry.valid_name(spec["name"]):
|
|
189
|
+
return None
|
|
190
|
+
return spec
|
|
191
|
+
|
|
192
|
+
# -- verify -----------------------------------------------------------
|
|
193
|
+
async def verify(self, tool: Tool) -> Tuple[bool, Dict[str, Any]]:
|
|
194
|
+
"""Run the tool's test inside the sandbox. Verified -> shadow."""
|
|
195
|
+
sandbox = self._sb()
|
|
196
|
+
if sandbox is None:
|
|
197
|
+
return False, {"reason": "sandbox_unavailable"}
|
|
198
|
+
if not self.registry.artifact_intact(tool):
|
|
199
|
+
return False, {"reason": "artifact_integrity_failure"}
|
|
200
|
+
static_verdict = validate_pure_tool(tool.source_code, tool.test_source)
|
|
201
|
+
if not static_verdict.allowed:
|
|
202
|
+
detail = {
|
|
203
|
+
"reason": "static_policy_blocked",
|
|
204
|
+
"policy_reasons": list(static_verdict.reasons),
|
|
205
|
+
"candidate_digest": tool.candidate_digest,
|
|
206
|
+
"artifact_digest": tool.artifact_digest,
|
|
207
|
+
}
|
|
208
|
+
self.registry.set_status(
|
|
209
|
+
tool.tool_id, ToolStatus.REJECTED, verify_detail=detail)
|
|
210
|
+
return False, detail
|
|
211
|
+
script = self._verify_script(tool)
|
|
212
|
+
try:
|
|
213
|
+
# This is an autonomous, contained read-only evaluation. It is
|
|
214
|
+
# fast-path eligible because the sandbox has no egress/secrets or
|
|
215
|
+
# writable host mount, not because Toolsmith impersonates owner
|
|
216
|
+
# direction.
|
|
217
|
+
res = await asyncio.to_thread(
|
|
218
|
+
sandbox.run, script, "python",
|
|
219
|
+
purpose=f"toolsmith verify {tool.name}",
|
|
220
|
+
owner_directed=False,
|
|
221
|
+
read_only=True)
|
|
222
|
+
except Exception as exc:
|
|
223
|
+
return False, {"reason": f"sandbox_error: {exc}"}
|
|
224
|
+
if not res.get("ran"):
|
|
225
|
+
return False, {"reason": res.get("reason", "not_run"),
|
|
226
|
+
"detail": res.get("detail")}
|
|
227
|
+
result = res.get("result") or {}
|
|
228
|
+
stdout = result.get("stdout", "")
|
|
229
|
+
verdict = self._parse_verdict(stdout)
|
|
230
|
+
passed = bool(verdict.get("passed")) and result.get("exit_code") == 0
|
|
231
|
+
detail = {"passed": passed, "exit_code": result.get("exit_code"),
|
|
232
|
+
"verdict": verdict, "stderr": (result.get("stderr") or "")[:500]}
|
|
233
|
+
detail.update({
|
|
234
|
+
"candidate_digest": tool.candidate_digest,
|
|
235
|
+
"artifact_digest": tool.artifact_digest,
|
|
236
|
+
"evidence_kind": "self_test_only",
|
|
237
|
+
})
|
|
238
|
+
self.registry.set_status(
|
|
239
|
+
tool.tool_id,
|
|
240
|
+
ToolStatus.SHADOW if passed else ToolStatus.REJECTED,
|
|
241
|
+
verify_detail=detail)
|
|
242
|
+
self._journal(
|
|
243
|
+
f"verify tool {tool.name}: {'passed' if passed else 'failed'}",
|
|
244
|
+
decision="acted", outcome="success" if passed else "failure",
|
|
245
|
+
ref=tool.tool_id)
|
|
246
|
+
return passed, detail
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def _verify_script(tool: Tool) -> str:
|
|
250
|
+
return (
|
|
251
|
+
"import json, traceback\n"
|
|
252
|
+
"_ok = True\n_err = None\n"
|
|
253
|
+
"try:\n"
|
|
254
|
+
+ _indent(tool.source_code) + "\n"
|
|
255
|
+
+ _indent(tool.test_source) + "\n"
|
|
256
|
+
"except AssertionError as e:\n"
|
|
257
|
+
" _ok = False; _err = 'assertion: ' + str(e)\n"
|
|
258
|
+
"except Exception as e:\n"
|
|
259
|
+
" _ok = False; _err = type(e).__name__ + ': ' + str(e)\n"
|
|
260
|
+
"print('@@VERDICT@@ ' + json.dumps({'passed': _ok, 'error': _err}))\n"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def _parse_verdict(stdout: str) -> Dict[str, Any]:
|
|
265
|
+
m = _VERDICT_RE.search(stdout or "")
|
|
266
|
+
if not m:
|
|
267
|
+
return {"passed": False, "error": "no verdict emitted"}
|
|
268
|
+
try:
|
|
269
|
+
return json.loads(m.group(1))
|
|
270
|
+
except json.JSONDecodeError:
|
|
271
|
+
return {"passed": False, "error": "verdict parse failed"}
|
|
272
|
+
|
|
273
|
+
# -- shadow accumulation ----------------------------------------------
|
|
274
|
+
async def verify_shadow_run(
|
|
275
|
+
self,
|
|
276
|
+
tool: Tool,
|
|
277
|
+
*,
|
|
278
|
+
captured_input: Dict[str, Any],
|
|
279
|
+
incumbent_output: Any,
|
|
280
|
+
capture_id: str,
|
|
281
|
+
capture_source: str = "captured",
|
|
282
|
+
principal_id: str = "internal-capture",
|
|
283
|
+
) -> Tuple[bool, Dict[str, Any]]:
|
|
284
|
+
"""Compare incumbent and candidate on one exact captured input.
|
|
285
|
+
|
|
286
|
+
The candidate runs twice to prove determinism. Only a deterministic
|
|
287
|
+
output equal to the captured incumbent output is clean graduation
|
|
288
|
+
evidence. Inputs and outputs are bounded canonical JSON; the durable
|
|
289
|
+
audit projection stores their digests, not potentially sensitive data.
|
|
290
|
+
"""
|
|
291
|
+
sandbox = self._sb()
|
|
292
|
+
if sandbox is None:
|
|
293
|
+
return False, {"reason": "sandbox_unavailable"}
|
|
294
|
+
if tool.status != ToolStatus.SHADOW:
|
|
295
|
+
return False, {"reason": "tool_not_shadow"}
|
|
296
|
+
if not self.registry.artifact_intact(tool):
|
|
297
|
+
return False, {"reason": "artifact_integrity_failure"}
|
|
298
|
+
capture_id = str(capture_id or "").strip()
|
|
299
|
+
capture_source = str(capture_source or "").strip().lower()
|
|
300
|
+
principal_id = str(principal_id or "").strip()
|
|
301
|
+
if not _CAPTURE_ID_RE.fullmatch(capture_id):
|
|
302
|
+
return False, {"reason": "invalid_capture_id"}
|
|
303
|
+
if capture_source not in _CAPTURE_SOURCES:
|
|
304
|
+
return False, {"reason": "invalid_capture_source"}
|
|
305
|
+
if not principal_id:
|
|
306
|
+
return False, {"reason": "capture_principal_required"}
|
|
307
|
+
try:
|
|
308
|
+
self._validate_captured_input(tool, captured_input)
|
|
309
|
+
input_digest = digest_json(
|
|
310
|
+
captured_input, max_bytes=MAX_CAPTURE_BYTES)
|
|
311
|
+
incumbent_digest = digest_json(
|
|
312
|
+
incumbent_output, max_bytes=MAX_CAPTURE_BYTES)
|
|
313
|
+
except IntegrityError as exc:
|
|
314
|
+
return False, {"reason": "invalid_capture", "detail": str(exc)}
|
|
315
|
+
script = self._call_script(tool, captured_input)
|
|
316
|
+
|
|
317
|
+
async def one_run() -> Tuple[bool, Any, Dict[str, Any]]:
|
|
318
|
+
res = await asyncio.to_thread(
|
|
319
|
+
sandbox.run,
|
|
320
|
+
script,
|
|
321
|
+
"python",
|
|
322
|
+
purpose=f"toolsmith compare {tool.name}",
|
|
323
|
+
owner_directed=False,
|
|
324
|
+
read_only=True,
|
|
325
|
+
)
|
|
326
|
+
if not res.get("ran"):
|
|
327
|
+
return False, None, {
|
|
328
|
+
"reason": res.get("reason", "not_run"),
|
|
329
|
+
"detail": res.get("detail"),
|
|
330
|
+
}
|
|
331
|
+
raw_result = res.get("result") or {}
|
|
332
|
+
verdict = self._parse_verdict(raw_result.get("stdout", ""))
|
|
333
|
+
ok = bool(verdict.get("passed")) and raw_result.get("exit_code") == 0
|
|
334
|
+
return ok, verdict.get("result"), verdict
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
first_ok, first_output, first_detail = await one_run()
|
|
338
|
+
second_ok, second_output, second_detail = await one_run()
|
|
339
|
+
except Exception as exc:
|
|
340
|
+
return False, {"reason": f"sandbox_error: {exc}"}
|
|
341
|
+
try:
|
|
342
|
+
first_digest = digest_json(first_output, max_bytes=MAX_CAPTURE_BYTES)
|
|
343
|
+
second_digest = digest_json(second_output, max_bytes=MAX_CAPTURE_BYTES)
|
|
344
|
+
except IntegrityError:
|
|
345
|
+
# Preserve an auditable failure without accepting an unbounded or
|
|
346
|
+
# non-JSON candidate output as comparison evidence.
|
|
347
|
+
first_digest = digest_json({"invalid_output": True, "run": 1})
|
|
348
|
+
second_digest = digest_json({"invalid_output": True, "run": 2})
|
|
349
|
+
first_ok = second_ok = False
|
|
350
|
+
deterministic = bool(first_ok and second_ok and first_digest == second_digest)
|
|
351
|
+
matched = bool(deterministic and first_digest == incumbent_digest)
|
|
352
|
+
evidence = self.registry.record_shadow_comparison(
|
|
353
|
+
tool=tool,
|
|
354
|
+
capture_id=capture_id,
|
|
355
|
+
capture_source=capture_source,
|
|
356
|
+
principal_id=principal_id,
|
|
357
|
+
input_digest=input_digest,
|
|
358
|
+
incumbent_output_digest=incumbent_digest,
|
|
359
|
+
candidate_output_digest=first_digest,
|
|
360
|
+
repeat_output_digest=second_digest,
|
|
361
|
+
deterministic=deterministic,
|
|
362
|
+
matched=matched,
|
|
363
|
+
)
|
|
364
|
+
passed = bool(evidence["success"])
|
|
365
|
+
if not evidence["replayed"]:
|
|
366
|
+
sm = self._sm()
|
|
367
|
+
if sm is not None:
|
|
368
|
+
try:
|
|
369
|
+
sm.record(
|
|
370
|
+
TRUST_DOMAIN,
|
|
371
|
+
"success" if passed else "failure",
|
|
372
|
+
shadow=True,
|
|
373
|
+
)
|
|
374
|
+
except Exception:
|
|
375
|
+
logger.debug("toolsmith shadow record failed", exc_info=True)
|
|
376
|
+
return passed, {
|
|
377
|
+
**evidence,
|
|
378
|
+
"passed": passed,
|
|
379
|
+
"first_error": first_detail.get("error"),
|
|
380
|
+
"second_error": second_detail.get("error"),
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
@staticmethod
|
|
384
|
+
def _validate_captured_input(tool: Tool, captured_input: Dict[str, Any]) -> None:
|
|
385
|
+
if not isinstance(captured_input, dict):
|
|
386
|
+
raise IntegrityError("captured_input must be a JSON object")
|
|
387
|
+
canonical_json(captured_input, max_bytes=MAX_CAPTURE_BYTES)
|
|
388
|
+
declared = tool.input_schema or {}
|
|
389
|
+
if not isinstance(declared, dict):
|
|
390
|
+
raise IntegrityError("tool input schema is malformed")
|
|
391
|
+
unknown = set(captured_input) - set(declared)
|
|
392
|
+
if declared and unknown:
|
|
393
|
+
raise IntegrityError("captured_input contains undeclared fields")
|
|
394
|
+
|
|
395
|
+
# -- shadow invocation ------------------------------------------------
|
|
396
|
+
def record_live(self, tool_id: str, success: bool) -> None:
|
|
397
|
+
self.registry.record_invocation(tool_id, success=success)
|
|
398
|
+
sm = self._sm()
|
|
399
|
+
if sm is not None:
|
|
400
|
+
try:
|
|
401
|
+
sm.record(TRUST_DOMAIN,
|
|
402
|
+
"success" if success else "failure",
|
|
403
|
+
violation=not success)
|
|
404
|
+
except Exception:
|
|
405
|
+
logger.debug("toolsmith live record failed", exc_info=True)
|
|
406
|
+
|
|
407
|
+
# -- graduation -------------------------------------------------------
|
|
408
|
+
def graduation_candidates(self) -> List[Tool]:
|
|
409
|
+
"""Shadow tools with enough receipt-backed clean comparisons."""
|
|
410
|
+
floor = _shadow_min()
|
|
411
|
+
return [t for t in self.registry.list(status=ToolStatus.SHADOW)
|
|
412
|
+
if self.registry.clean_comparison_count(t.tool_id) >= floor
|
|
413
|
+
and t.failures == 0]
|
|
414
|
+
|
|
415
|
+
def trust_stage(self) -> str:
|
|
416
|
+
sm = self._sm()
|
|
417
|
+
trust = getattr(sm, "trust", None) if sm is not None else None
|
|
418
|
+
if trust is None:
|
|
419
|
+
return "shadow"
|
|
420
|
+
try:
|
|
421
|
+
return trust.stage(TRUST_DOMAIN, default="shadow")
|
|
422
|
+
except Exception:
|
|
423
|
+
return "shadow"
|
|
424
|
+
|
|
425
|
+
def graduate(
|
|
426
|
+
self,
|
|
427
|
+
tool_id: str,
|
|
428
|
+
*,
|
|
429
|
+
authority: GraduationAuthorityV1,
|
|
430
|
+
) -> Dict[str, Any]:
|
|
431
|
+
"""Publish one exact artifact using one bounded owner authority."""
|
|
432
|
+
|
|
433
|
+
if authority.tool_id != tool_id:
|
|
434
|
+
raise ValueError("graduation authority is bound to another tool")
|
|
435
|
+
tool = self.registry.get(tool_id)
|
|
436
|
+
if tool is None:
|
|
437
|
+
raise ValueError("tool was not found")
|
|
438
|
+
static_verdict = validate_pure_tool(tool.source_code, tool.test_source)
|
|
439
|
+
if not static_verdict.allowed:
|
|
440
|
+
raise ValueError("tool violates the pure capability manifest")
|
|
441
|
+
result = self.registry.graduate_with_authority(
|
|
442
|
+
authority, shadow_min=_shadow_min())
|
|
443
|
+
tool = self.registry.get(tool_id)
|
|
444
|
+
assert tool is not None
|
|
445
|
+
self._journal(f"graduated tool {tool.name} to live",
|
|
446
|
+
decision="acted", outcome="success",
|
|
447
|
+
ref=result["authority_digest"])
|
|
448
|
+
return result
|
|
449
|
+
|
|
450
|
+
def retire(self, tool_id: str, reason: str = "") -> bool:
|
|
451
|
+
tool = self.registry.get(tool_id)
|
|
452
|
+
if tool is None:
|
|
453
|
+
return False
|
|
454
|
+
self.registry.set_status(tool_id, ToolStatus.RETIRED)
|
|
455
|
+
self._journal(f"retired tool {tool.name}: {reason}"[:200],
|
|
456
|
+
decision="acted", ref=tool_id)
|
|
457
|
+
return True
|
|
458
|
+
|
|
459
|
+
# -- live invocation (from the reasoning loop) ------------------------
|
|
460
|
+
async def invoke_live(self, tool_id: str,
|
|
461
|
+
kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
462
|
+
"""Run a graduated tool's run(**kwargs) inside the sandbox and return
|
|
463
|
+
its result. Records the outcome against trust."""
|
|
464
|
+
tool = self.registry.get(tool_id)
|
|
465
|
+
if tool is None or tool.status != ToolStatus.LIVE:
|
|
466
|
+
return {"error": "tool not live"}
|
|
467
|
+
static_verdict = validate_pure_tool(tool.source_code, tool.test_source)
|
|
468
|
+
if not static_verdict.allowed:
|
|
469
|
+
self.record_live(tool_id, success=False)
|
|
470
|
+
return {"error": "tool violates pure capability manifest"}
|
|
471
|
+
sandbox = self._sb()
|
|
472
|
+
if sandbox is None:
|
|
473
|
+
return {"error": "sandbox unavailable"}
|
|
474
|
+
try:
|
|
475
|
+
self._validate_captured_input(tool, kwargs)
|
|
476
|
+
script = self._call_script(tool, kwargs)
|
|
477
|
+
except IntegrityError as exc:
|
|
478
|
+
return {"error": f"invalid tool input: {exc}"}
|
|
479
|
+
try:
|
|
480
|
+
res = await asyncio.to_thread(
|
|
481
|
+
sandbox.run, script, "python",
|
|
482
|
+
purpose=f"toolsmith invoke {tool.name}",
|
|
483
|
+
owner_directed=False,
|
|
484
|
+
read_only=True)
|
|
485
|
+
except Exception as exc:
|
|
486
|
+
self.record_live(tool_id, success=False)
|
|
487
|
+
return {"error": f"sandbox_error: {exc}"}
|
|
488
|
+
result = res.get("result") or {}
|
|
489
|
+
out = self._parse_verdict(result.get("stdout", ""))
|
|
490
|
+
success = bool(out.get("passed")) and result.get("exit_code") == 0
|
|
491
|
+
if success:
|
|
492
|
+
try:
|
|
493
|
+
canonical_json(out.get("result"), max_bytes=MAX_CAPTURE_BYTES)
|
|
494
|
+
except IntegrityError:
|
|
495
|
+
success = False
|
|
496
|
+
out["error"] = "tool output is not bounded finite JSON"
|
|
497
|
+
self.record_live(tool_id, success=success)
|
|
498
|
+
if not success:
|
|
499
|
+
return {"error": out.get("error") or "tool failed",
|
|
500
|
+
"stderr": (result.get("stderr") or "")[:300]}
|
|
501
|
+
return {"result": out.get("result")}
|
|
502
|
+
|
|
503
|
+
@staticmethod
|
|
504
|
+
def _call_script(tool: Tool, kwargs: Dict[str, Any]) -> str:
|
|
505
|
+
encoded_args = canonical_json(kwargs, max_bytes=MAX_CAPTURE_BYTES)
|
|
506
|
+
return (
|
|
507
|
+
"import json\n_ok=True\n_err=None\n_res=None\n"
|
|
508
|
+
"try:\n"
|
|
509
|
+
+ _indent(tool.source_code) + "\n"
|
|
510
|
+
+ _indent(f"_res = run(**json.loads({encoded_args!r}))") + "\n"
|
|
511
|
+
"except Exception as e:\n"
|
|
512
|
+
" _ok=False; _err=type(e).__name__+': '+str(e)\n"
|
|
513
|
+
"print('@@VERDICT@@ ' + json.dumps({'passed': _ok, "
|
|
514
|
+
"'error': _err, 'result': _res}))\n"
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
def build_dynamic_provider(self):
|
|
518
|
+
"""Return a callable for ToolExecutor.set_dynamic_provider that maps
|
|
519
|
+
every LIVE tool to its (openai_definition, async_handler)."""
|
|
520
|
+
def provider() -> Dict[str, Any]:
|
|
521
|
+
out: Dict[str, Any] = {}
|
|
522
|
+
for tool in self.registry.list(status=ToolStatus.LIVE):
|
|
523
|
+
definition = {
|
|
524
|
+
"type": "function",
|
|
525
|
+
"function": {
|
|
526
|
+
"name": tool.name,
|
|
527
|
+
"description": tool.description,
|
|
528
|
+
"parameters": {
|
|
529
|
+
"type": "object",
|
|
530
|
+
"properties": tool.input_schema or {},
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
def make_handler(tid: str):
|
|
536
|
+
async def _handler(args: Dict[str, Any]) -> str:
|
|
537
|
+
r = await self.invoke_live(tid, args or {})
|
|
538
|
+
return json.dumps(r)
|
|
539
|
+
return _handler
|
|
540
|
+
|
|
541
|
+
out[tool.name] = (definition, make_handler(tool.tool_id))
|
|
542
|
+
return out
|
|
543
|
+
return provider
|
|
544
|
+
|
|
545
|
+
def retirement_candidates(self) -> List[Tool]:
|
|
546
|
+
"""Live tools failing repeatedly, or shadow tools stuck failing."""
|
|
547
|
+
out: List[Tool] = []
|
|
548
|
+
for t in self.registry.list(status=ToolStatus.LIVE):
|
|
549
|
+
if t.invocations >= 3 and t.failures >= max(2, t.invocations // 2):
|
|
550
|
+
out.append(t)
|
|
551
|
+
for t in self.registry.list(status=ToolStatus.SHADOW):
|
|
552
|
+
if t.failures >= 3:
|
|
553
|
+
out.append(t)
|
|
554
|
+
return out
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _indent(code: str, spaces: int = 4) -> str:
|
|
558
|
+
pad = " " * spaces
|
|
559
|
+
return "\n".join(pad + line for line in code.splitlines())
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Canonical digests for Toolsmith artifacts and comparison captures.
|
|
2
|
+
|
|
3
|
+
The helpers in this module deliberately accept JSON-shaped values only. A
|
|
4
|
+
Toolsmith artifact or shadow input must have one stable byte representation so
|
|
5
|
+
that audit records cannot be rebound to different code or data later.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
from typing import Any, Mapping
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
MAX_CAPTURE_BYTES = 32 * 1024
|
|
16
|
+
PURE_CAPABILITY_MANIFEST = {
|
|
17
|
+
"version": "toolsmith.capability-manifest.v1",
|
|
18
|
+
"entrypoint": "run",
|
|
19
|
+
"effects": "none",
|
|
20
|
+
"filesystem": "none",
|
|
21
|
+
"network": "none",
|
|
22
|
+
"subprocess": False,
|
|
23
|
+
"environment": False,
|
|
24
|
+
"deterministic": True,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class IntegrityError(ValueError):
|
|
29
|
+
"""A value cannot be represented by the bounded canonical contract."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def canonical_json(value: Any, *, max_bytes: int | None = None) -> str:
|
|
33
|
+
"""Return deterministic JSON, rejecting non-JSON and oversized values."""
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
rendered = json.dumps(
|
|
37
|
+
value,
|
|
38
|
+
ensure_ascii=False,
|
|
39
|
+
allow_nan=False,
|
|
40
|
+
sort_keys=True,
|
|
41
|
+
separators=(",", ":"),
|
|
42
|
+
)
|
|
43
|
+
except (TypeError, ValueError) as exc:
|
|
44
|
+
raise IntegrityError("value must be finite JSON") from exc
|
|
45
|
+
size = len(rendered.encode("utf-8"))
|
|
46
|
+
if max_bytes is not None and size > max_bytes:
|
|
47
|
+
raise IntegrityError(f"canonical value exceeds {max_bytes} bytes")
|
|
48
|
+
return rendered
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def digest_text(value: str) -> str:
|
|
52
|
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def digest_json(value: Any, *, max_bytes: int | None = None) -> str:
|
|
56
|
+
return digest_text(canonical_json(value, max_bytes=max_bytes))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def candidate_digest(candidate: Any) -> str:
|
|
60
|
+
"""Bind a draft to the exact mined/requested evidence it came from."""
|
|
61
|
+
|
|
62
|
+
payload = {
|
|
63
|
+
"signature": str(getattr(candidate, "signature", "") or ""),
|
|
64
|
+
"domain": str(getattr(candidate, "domain", "") or ""),
|
|
65
|
+
"description": str(getattr(candidate, "description", "") or ""),
|
|
66
|
+
"occurrences": int(getattr(candidate, "occurrences", 0) or 0),
|
|
67
|
+
"evidence": list(getattr(candidate, "evidence", []) or []),
|
|
68
|
+
"sample_descriptions": list(
|
|
69
|
+
getattr(candidate, "sample_descriptions", []) or []
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
return digest_json(payload)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def artifact_digest(
|
|
76
|
+
*,
|
|
77
|
+
name: str,
|
|
78
|
+
description: str,
|
|
79
|
+
source_code: str,
|
|
80
|
+
input_schema: Mapping[str, Any],
|
|
81
|
+
test_source: str,
|
|
82
|
+
origin_kind: str,
|
|
83
|
+
evidence: list[str],
|
|
84
|
+
candidate_digest_value: str,
|
|
85
|
+
capability_manifest: Mapping[str, Any] | None = None,
|
|
86
|
+
) -> str:
|
|
87
|
+
"""Bind all executable and provenance-bearing fields of one tool."""
|
|
88
|
+
|
|
89
|
+
return digest_json({
|
|
90
|
+
"name": name,
|
|
91
|
+
"description": description,
|
|
92
|
+
"source_code": source_code,
|
|
93
|
+
"input_schema": dict(input_schema),
|
|
94
|
+
"test_source": test_source,
|
|
95
|
+
"origin_kind": origin_kind,
|
|
96
|
+
"evidence": list(evidence),
|
|
97
|
+
"candidate_digest": candidate_digest_value,
|
|
98
|
+
"capability_manifest": dict(
|
|
99
|
+
capability_manifest or PURE_CAPABILITY_MANIFEST),
|
|
100
|
+
})
|