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,347 @@
|
|
|
1
|
+
"""Server-attested, hot-reloaded exact contact grants for API principals.
|
|
2
|
+
|
|
3
|
+
The credential keyring remains static and secret-bearing. This separate
|
|
4
|
+
mode-private projection contains only exact Colony person IDs learned at the
|
|
5
|
+
ParticipantResolver chokepoint. A request body can use an existing grant; it
|
|
6
|
+
can never create one.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
import re
|
|
17
|
+
import stat
|
|
18
|
+
import tempfile
|
|
19
|
+
import threading
|
|
20
|
+
from typing import Any, Mapping
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
_PRINCIPAL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,127}$")
|
|
26
|
+
_PERSON_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$")
|
|
27
|
+
_RESERVED_PERSON_IDS = frozenset({"*", "owner", "shared", "global", "viewer", "system"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ContactGrantError(ValueError):
|
|
31
|
+
"""A grant projection is unsafe or malformed."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _now() -> str:
|
|
35
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _exact_principal(value: Any) -> str:
|
|
39
|
+
principal = str(value or "").strip()
|
|
40
|
+
if not _PRINCIPAL_RE.fullmatch(principal):
|
|
41
|
+
raise ContactGrantError("invalid principal ID in contact grant projection")
|
|
42
|
+
return principal
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _exact_person(value: Any) -> str:
|
|
46
|
+
person_id = str(value or "").strip()
|
|
47
|
+
if (
|
|
48
|
+
not _PERSON_RE.fullmatch(person_id)
|
|
49
|
+
or person_id.lower() in _RESERVED_PERSON_IDS
|
|
50
|
+
or "*" in person_id
|
|
51
|
+
):
|
|
52
|
+
raise ContactGrantError("contact grants require one exact non-reserved person ID")
|
|
53
|
+
return person_id
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _private_regular_file(path: Path) -> os.stat_result:
|
|
57
|
+
info = path.lstat()
|
|
58
|
+
if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode):
|
|
59
|
+
raise ContactGrantError("contact grant path must be a regular file")
|
|
60
|
+
if stat.S_IMODE(info.st_mode) != 0o600:
|
|
61
|
+
raise ContactGrantError("contact grant file must be mode 0600")
|
|
62
|
+
if hasattr(os, "geteuid") and info.st_uid != os.geteuid():
|
|
63
|
+
raise ContactGrantError("contact grant file must be owned by the service user")
|
|
64
|
+
return info
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _parse_document(raw: Any) -> dict[str, frozenset[str]]:
|
|
68
|
+
if not isinstance(raw, Mapping) or raw.get("version") != 1:
|
|
69
|
+
raise ContactGrantError("contact grant projection version must be 1")
|
|
70
|
+
if set(raw) - {"version", "principals"}:
|
|
71
|
+
raise ContactGrantError("contact grant projection contains unsupported fields")
|
|
72
|
+
records = raw.get("principals")
|
|
73
|
+
if not isinstance(records, Mapping):
|
|
74
|
+
raise ContactGrantError("contact grant principals must be an object")
|
|
75
|
+
result: dict[str, frozenset[str]] = {}
|
|
76
|
+
for raw_principal, record in records.items():
|
|
77
|
+
principal = _exact_principal(raw_principal)
|
|
78
|
+
if not isinstance(record, Mapping):
|
|
79
|
+
raise ContactGrantError(f"contact grant record for {principal!r} must be an object")
|
|
80
|
+
raw_ids = record.get("person_ids")
|
|
81
|
+
if not isinstance(raw_ids, list):
|
|
82
|
+
raise ContactGrantError(f"contact grant person_ids for {principal!r} must be an array")
|
|
83
|
+
exact_ids = frozenset(_exact_person(item) for item in raw_ids)
|
|
84
|
+
if len(exact_ids) != len(raw_ids):
|
|
85
|
+
raise ContactGrantError(f"contact grant person_ids for {principal!r} contain duplicates")
|
|
86
|
+
# Projection records deliberately cannot add audiences/lanes or scopes.
|
|
87
|
+
unknown = set(record) - {"person_ids", "updated_at"}
|
|
88
|
+
if unknown:
|
|
89
|
+
raise ContactGrantError(
|
|
90
|
+
f"contact grant record for {principal!r} contains unsupported fields"
|
|
91
|
+
)
|
|
92
|
+
updated_at = str(record.get("updated_at") or "").strip()
|
|
93
|
+
if updated_at:
|
|
94
|
+
candidate = updated_at[:-1] + "+00:00" if updated_at.endswith("Z") else updated_at
|
|
95
|
+
try:
|
|
96
|
+
parsed = datetime.fromisoformat(candidate)
|
|
97
|
+
except ValueError as exc:
|
|
98
|
+
raise ContactGrantError(
|
|
99
|
+
f"contact grant updated_at for {principal!r} is invalid"
|
|
100
|
+
) from exc
|
|
101
|
+
if parsed.tzinfo is None:
|
|
102
|
+
raise ContactGrantError(
|
|
103
|
+
f"contact grant updated_at for {principal!r} needs a timezone"
|
|
104
|
+
)
|
|
105
|
+
result[principal] = exact_ids
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ContactGrantRegistry:
|
|
110
|
+
"""Mode-private exact-ID projection with atomic write and hot reload."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, path: str | os.PathLike[str] | None) -> None:
|
|
113
|
+
self.path = Path(path).expanduser() if path else None
|
|
114
|
+
self._signature: tuple[int, int, int, int, int] | tuple[str] | None = None
|
|
115
|
+
self._grants: dict[str, frozenset[str]] = {}
|
|
116
|
+
self._updated_at: dict[str, str] = {}
|
|
117
|
+
self._error: str | None = None
|
|
118
|
+
self._document_available = False
|
|
119
|
+
self._last_loaded_at: str | None = None
|
|
120
|
+
self._last_written_at: str | None = None
|
|
121
|
+
self._policy_error: str | None = None
|
|
122
|
+
self._lock = threading.RLock()
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def configured(self) -> bool:
|
|
126
|
+
return self.path is not None
|
|
127
|
+
|
|
128
|
+
def _current_signature(self) -> tuple[int, int, int, int, int] | tuple[str]:
|
|
129
|
+
assert self.path is not None
|
|
130
|
+
try:
|
|
131
|
+
info = self.path.lstat()
|
|
132
|
+
except OSError:
|
|
133
|
+
return ("missing",)
|
|
134
|
+
return (
|
|
135
|
+
info.st_ino,
|
|
136
|
+
info.st_size,
|
|
137
|
+
info.st_mtime_ns,
|
|
138
|
+
info.st_ctime_ns,
|
|
139
|
+
stat.S_IMODE(info.st_mode),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def _reload_if_needed(self) -> None:
|
|
143
|
+
if self.path is None:
|
|
144
|
+
return
|
|
145
|
+
signature = self._current_signature()
|
|
146
|
+
with self._lock:
|
|
147
|
+
if signature == self._signature:
|
|
148
|
+
return
|
|
149
|
+
self._signature = signature
|
|
150
|
+
self._policy_error = None
|
|
151
|
+
if signature == ("missing",):
|
|
152
|
+
self._grants = {}
|
|
153
|
+
self._updated_at = {}
|
|
154
|
+
self._document_available = False
|
|
155
|
+
self._error = None
|
|
156
|
+
self._last_loaded_at = _now()
|
|
157
|
+
return
|
|
158
|
+
try:
|
|
159
|
+
_private_regular_file(self.path)
|
|
160
|
+
raw = json.loads(self.path.read_text(encoding="utf-8"))
|
|
161
|
+
grants = _parse_document(raw)
|
|
162
|
+
records = raw.get("principals") or {}
|
|
163
|
+
self._grants = grants
|
|
164
|
+
self._document_available = True
|
|
165
|
+
self._updated_at = {
|
|
166
|
+
principal: str((records.get(principal) or {}).get("updated_at") or "")
|
|
167
|
+
for principal in grants
|
|
168
|
+
}
|
|
169
|
+
self._error = None
|
|
170
|
+
self._last_loaded_at = _now()
|
|
171
|
+
logger.info(
|
|
172
|
+
"Loaded %d exact contact grant set(s) from %s",
|
|
173
|
+
len(grants), self.path,
|
|
174
|
+
)
|
|
175
|
+
except (OSError, UnicodeError, ValueError, ContactGrantError) as exc:
|
|
176
|
+
# Fail closed on malformed replacement; never retain stale grants.
|
|
177
|
+
self._grants = {}
|
|
178
|
+
self._updated_at = {}
|
|
179
|
+
self._document_available = False
|
|
180
|
+
self._error = str(exc)
|
|
181
|
+
self._last_loaded_at = _now()
|
|
182
|
+
logger.error("Exact contact grants unavailable: %s", exc)
|
|
183
|
+
|
|
184
|
+
def person_ids(self, principal: str, *, max_person_ids: int) -> frozenset[str]:
|
|
185
|
+
"""Return exact IDs only when the projection respects the policy cap."""
|
|
186
|
+
|
|
187
|
+
self._reload_if_needed()
|
|
188
|
+
with self._lock:
|
|
189
|
+
ids = self._grants.get(principal, frozenset())
|
|
190
|
+
if len(ids) > max_person_ids:
|
|
191
|
+
self._policy_error = (
|
|
192
|
+
f"principal {principal!r} has {len(ids)} contact grants, "
|
|
193
|
+
f"above its cap of {max_person_ids}"
|
|
194
|
+
)
|
|
195
|
+
return frozenset()
|
|
196
|
+
return ids
|
|
197
|
+
|
|
198
|
+
def principal_projection(
|
|
199
|
+
self,
|
|
200
|
+
principal: str,
|
|
201
|
+
*,
|
|
202
|
+
max_person_ids: int,
|
|
203
|
+
) -> dict[str, Any]:
|
|
204
|
+
"""Return one caller's exact, server-attested contact-grant posture.
|
|
205
|
+
|
|
206
|
+
This is a read model for an already authenticated principal. It never
|
|
207
|
+
enumerates another principal and never adds a person, audience, or API
|
|
208
|
+
scope. Callers must still enforce their own authentication boundary;
|
|
209
|
+
this method only applies the same configured cap as ``person_ids``.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
principal = _exact_principal(principal)
|
|
213
|
+
if not isinstance(max_person_ids, int) or not 1 <= max_person_ids <= 4096:
|
|
214
|
+
raise ContactGrantError("contact grant cap must be 1..4096")
|
|
215
|
+
self._reload_if_needed()
|
|
216
|
+
with self._lock:
|
|
217
|
+
ids = self._grants.get(principal, frozenset())
|
|
218
|
+
if len(ids) > max_person_ids:
|
|
219
|
+
self._policy_error = (
|
|
220
|
+
f"principal {principal!r} has {len(ids)} contact grants, "
|
|
221
|
+
f"above its cap of {max_person_ids}"
|
|
222
|
+
)
|
|
223
|
+
return {
|
|
224
|
+
"available": False,
|
|
225
|
+
"reason": "principal_contact_grant_cap_exceeded",
|
|
226
|
+
"person_ids": [],
|
|
227
|
+
"updated_at": self._updated_at.get(principal) or None,
|
|
228
|
+
}
|
|
229
|
+
available = (
|
|
230
|
+
self.configured
|
|
231
|
+
and self._document_available
|
|
232
|
+
and self._error is None
|
|
233
|
+
)
|
|
234
|
+
return {
|
|
235
|
+
"available": available,
|
|
236
|
+
"reason": None if available else (
|
|
237
|
+
"contact_grant_projection_unconfigured"
|
|
238
|
+
if not self.configured
|
|
239
|
+
else "contact_grant_projection_missing"
|
|
240
|
+
if not self._document_available and self._error is None
|
|
241
|
+
else "contact_grant_projection_invalid"
|
|
242
|
+
),
|
|
243
|
+
"person_ids": sorted(ids) if available else [],
|
|
244
|
+
"updated_at": self._updated_at.get(principal) or None,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
def grant(self, principal: str, person_id: str, *, max_person_ids: int) -> bool:
|
|
248
|
+
"""Atomically add one server-attested exact ID.
|
|
249
|
+
|
|
250
|
+
Returns ``False`` when no projection is configured, it is invalid, or
|
|
251
|
+
the bounded principal cap would be exceeded. Existing grants are
|
|
252
|
+
idempotent and return ``True``.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
if self.path is None:
|
|
256
|
+
return False
|
|
257
|
+
principal = _exact_principal(principal)
|
|
258
|
+
person_id = _exact_person(person_id)
|
|
259
|
+
if not isinstance(max_person_ids, int) or not 1 <= max_person_ids <= 4096:
|
|
260
|
+
raise ContactGrantError("contact grant cap must be 1..4096")
|
|
261
|
+
with self._lock:
|
|
262
|
+
self._reload_if_needed()
|
|
263
|
+
if self._error:
|
|
264
|
+
return False
|
|
265
|
+
current = set(self._grants.get(principal, frozenset()))
|
|
266
|
+
if person_id in current:
|
|
267
|
+
return True
|
|
268
|
+
if len(current) >= max_person_ids:
|
|
269
|
+
self._policy_error = (
|
|
270
|
+
f"principal {principal!r} reached its contact grant cap "
|
|
271
|
+
f"of {max_person_ids}"
|
|
272
|
+
)
|
|
273
|
+
logger.warning("%s", self._policy_error)
|
|
274
|
+
return False
|
|
275
|
+
current.add(person_id)
|
|
276
|
+
all_grants = dict(self._grants)
|
|
277
|
+
all_grants[principal] = frozenset(current)
|
|
278
|
+
updated = dict(self._updated_at)
|
|
279
|
+
updated[principal] = _now()
|
|
280
|
+
document = {
|
|
281
|
+
"version": 1,
|
|
282
|
+
"principals": {
|
|
283
|
+
key: {
|
|
284
|
+
"person_ids": sorted(values),
|
|
285
|
+
"updated_at": updated.get(key) or _now(),
|
|
286
|
+
}
|
|
287
|
+
for key, values in sorted(all_grants.items())
|
|
288
|
+
},
|
|
289
|
+
}
|
|
290
|
+
self._atomic_write(document)
|
|
291
|
+
self._signature = None
|
|
292
|
+
self._reload_if_needed()
|
|
293
|
+
self._last_written_at = updated[principal]
|
|
294
|
+
return self._error is None and person_id in self._grants.get(
|
|
295
|
+
principal, frozenset()
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def _atomic_write(self, document: dict[str, Any]) -> None:
|
|
299
|
+
assert self.path is not None
|
|
300
|
+
parent = self.path.parent
|
|
301
|
+
parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
302
|
+
fd, raw_tmp = tempfile.mkstemp(prefix=f".{self.path.name}.", dir=str(parent))
|
|
303
|
+
tmp = Path(raw_tmp)
|
|
304
|
+
try:
|
|
305
|
+
os.fchmod(fd, 0o600)
|
|
306
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
307
|
+
json.dump(document, handle, sort_keys=True, separators=(",", ":"))
|
|
308
|
+
handle.write("\n")
|
|
309
|
+
handle.flush()
|
|
310
|
+
os.fsync(handle.fileno())
|
|
311
|
+
os.replace(tmp, self.path)
|
|
312
|
+
os.chmod(self.path, 0o600)
|
|
313
|
+
try:
|
|
314
|
+
directory = os.open(parent, os.O_RDONLY)
|
|
315
|
+
try:
|
|
316
|
+
os.fsync(directory)
|
|
317
|
+
finally:
|
|
318
|
+
os.close(directory)
|
|
319
|
+
except OSError:
|
|
320
|
+
pass
|
|
321
|
+
finally:
|
|
322
|
+
try:
|
|
323
|
+
tmp.unlink()
|
|
324
|
+
except FileNotFoundError:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
def status(self) -> dict[str, Any]:
|
|
328
|
+
"""Return privacy-safe counts and loader state, never exact IDs."""
|
|
329
|
+
|
|
330
|
+
self._reload_if_needed()
|
|
331
|
+
with self._lock:
|
|
332
|
+
return {
|
|
333
|
+
"configured": self.configured,
|
|
334
|
+
"available": (
|
|
335
|
+
self.configured
|
|
336
|
+
and self._document_available
|
|
337
|
+
and self._error is None
|
|
338
|
+
),
|
|
339
|
+
"error": self._error or self._policy_error,
|
|
340
|
+
"last_loaded_at": self._last_loaded_at,
|
|
341
|
+
"last_written_at": self._last_written_at,
|
|
342
|
+
"principal_counts": {
|
|
343
|
+
principal: len(values)
|
|
344
|
+
for principal, values in sorted(self._grants.items())
|
|
345
|
+
},
|
|
346
|
+
"total_exact_person_ids": sum(len(values) for values in self._grants.values()),
|
|
347
|
+
}
|