ldcon 0.2.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.
- ldcon/__init__.py +302 -0
- ldcon/_moved.py +49 -0
- ldcon/agent_tools.py +4 -0
- ldcon/agents/__init__.py +1 -0
- ldcon/agents/adjustment_guidance.py +73 -0
- ldcon/agents/adjustment_quality.py +399 -0
- ldcon/agents/agent_adapters.py +709 -0
- ldcon/agents/agent_background.py +307 -0
- ldcon/agents/agent_bundle.py +75 -0
- ldcon/agents/agent_cards.py +171 -0
- ldcon/agents/agent_config_access.py +22 -0
- ldcon/agents/agent_configuration.py +224 -0
- ldcon/agents/agent_configuration_policy.py +67 -0
- ldcon/agents/agent_connect.py +93 -0
- ldcon/agents/agent_device_parameters.py +41 -0
- ldcon/agents/agent_execution.py +77 -0
- ldcon/agents/agent_history.py +112 -0
- ldcon/agents/agent_inbox_watch.py +121 -0
- ldcon/agents/agent_interactive.py +386 -0
- ldcon/agents/agent_interactive_hooks.py +381 -0
- ldcon/agents/agent_language.py +48 -0
- ldcon/agents/agent_log_context.py +308 -0
- ldcon/agents/agent_log_read.py +250 -0
- ldcon/agents/agent_log_watch.py +27 -0
- ldcon/agents/agent_metrics.py +111 -0
- ldcon/agents/agent_monitor.py +272 -0
- ldcon/agents/agent_opencode_http.py +270 -0
- ldcon/agents/agent_opencode_mcp.py +64 -0
- ldcon/agents/agent_owner_status.py +216 -0
- ldcon/agents/agent_profiles.py +186 -0
- ldcon/agents/agent_providers.py +53 -0
- ldcon/agents/agent_repairs.py +105 -0
- ldcon/agents/agent_routing.py +90 -0
- ldcon/agents/agent_runtime.py +70 -0
- ldcon/agents/agent_sandbox.py +141 -0
- ldcon/agents/agent_startup_notice.py +142 -0
- ldcon/agents/agent_task.py +336 -0
- ldcon/agents/agent_tools.py +488 -0
- ldcon/agents/agent_window.py +70 -0
- ldcon/agents/agent_workspace.py +340 -0
- ldcon/agents/card_fallback.py +284 -0
- ldcon/agents/dlc_pi_adjustment.py +250 -0
- ldcon/agents/dlc_pi_plan.py +24 -0
- ldcon/agents/dlc_pi_response_plan.py +26 -0
- ldcon/agents/failure_policy.py +473 -0
- ldcon/agents/fallback_service.py +146 -0
- ldcon/agents/fallback_targets.py +96 -0
- ldcon/agents/gateway_role.py +31 -0
- ldcon/agents/manual_adjustment_grant.py +66 -0
- ldcon/agents/measurement_grant.py +95 -0
- ldcon/agents/monitor_quality_binding.py +33 -0
- ldcon/agents/notification_policy.py +346 -0
- ldcon/agents/operating_mode_client.py +99 -0
- ldcon/agents/report_pdf.py +105 -0
- ldcon/agents/resident_adjustment.py +572 -0
- ldcon/agents/resident_agent.py +438 -0
- ldcon/agents/resident_quality_apply.py +233 -0
- ldcon/agents/task_decisions.py +218 -0
- ldcon/agents/teams_card.py +100 -0
- ldcon/agents/teams_destinations.py +157 -0
- ldcon/cld_owner.py +4 -0
- ldcon/cli.py +19 -0
- ldcon/core/__init__.py +1 -0
- ldcon/core/adjustment_plan.py +47 -0
- ldcon/core/adjustment_registry.py +100 -0
- ldcon/core/agent_authority.py +903 -0
- ldcon/core/card_admission.py +15 -0
- ldcon/core/card_definition.py +14 -0
- ldcon/core/card_reporting.py +238 -0
- ldcon/core/configuration.py +357 -0
- ldcon/core/configuration_transaction.py +225 -0
- ldcon/core/contracts.py +86 -0
- ldcon/core/current_settings.py +86 -0
- ldcon/core/deck_yaml.py +45 -0
- ldcon/core/device_conditions.py +133 -0
- ldcon/core/execution_context.py +3 -0
- ldcon/core/instrument_client.py +190 -0
- ldcon/core/interface_cards.py +520 -0
- ldcon/core/interface_schema.py +640 -0
- ldcon/core/laser_assets.py +238 -0
- ldcon/core/metric_quantities.py +38 -0
- ldcon/core/modulation.py +107 -0
- ldcon/core/operation_registry.py +195 -0
- ldcon/core/paths.py +26 -0
- ldcon/core/period_context.py +118 -0
- ldcon/core/records.py +4 -0
- ldcon/core/result_reservations.py +469 -0
- ldcon/core/site_bindings.py +25 -0
- ldcon/core/state_layout.py +70 -0
- ldcon/core/target_policy.py +79 -0
- ldcon/core/telegraf.py +106 -0
- ldcon/core/telemetry.py +130 -0
- ldcon/core/temperature_settings.py +12 -0
- ldcon/dashboard_metrics.py +4 -0
- ldcon/device_registry.py +4 -0
- ldcon/drivers/__init__.py +1 -0
- ldcon/drivers/cld1015.py +296 -0
- ldcon/drivers/cld1015_config.py +31 -0
- ldcon/drivers/dlcpro.py +177 -0
- ldcon/drivers/dlcpro_analog.py +33 -0
- ldcon/drivers/dlcpro_console.py +133 -0
- ldcon/drivers/dlcpro_priority.py +46 -0
- ldcon/drivers/ecdl.py +334 -0
- ldcon/drivers/esp32_servo.py +187 -0
- ldcon/drivers/fizeau.py +114 -0
- ldcon/drivers/frequency_sweep.py +69 -0
- ldcon/drivers/influx_wavelength.py +70 -0
- ldcon/drivers/metric_sources.py +82 -0
- ldcon/drivers/native_dither_telemetry.py +50 -0
- ldcon/drivers/rigol_scope.py +126 -0
- ldcon/drivers/siglent.py +418 -0
- ldcon/drivers/staircase.py +155 -0
- ldcon/drivers/wsu_udp.py +164 -0
- ldcon/esp32_time_telemetry.py +4 -0
- ldcon/example_config.py +41 -0
- ldcon/fallback_service.py +4 -0
- ldcon/instrument_owner.py +4 -0
- ldcon/instrument_service.py +4 -0
- ldcon/interface_worker.py +4 -0
- ldcon/job_service.py +4 -0
- ldcon/jobs/__init__.py +1 -0
- ldcon/jobs/adaptive_pid.py +234 -0
- ldcon/jobs/adaptive_window_session.py +922 -0
- ldcon/jobs/adjustment_continuation.py +284 -0
- ldcon/jobs/axis_frequency.py +219 -0
- ldcon/jobs/axis_frequency_report.py +86 -0
- ldcon/jobs/background_parameter_session.py +142 -0
- ldcon/jobs/background_pid_session.py +447 -0
- ldcon/jobs/branch_maps.py +71 -0
- ldcon/jobs/branch_target_migration.py +33 -0
- ldcon/jobs/card_runtime.py +157 -0
- ldcon/jobs/change_measure.py +293 -0
- ldcon/jobs/change_measure_adapter.py +360 -0
- ldcon/jobs/change_measure_dlc.py +94 -0
- ldcon/jobs/change_measure_parameters.py +222 -0
- ldcon/jobs/change_measure_target.py +135 -0
- ldcon/jobs/current_cards.py +159 -0
- ldcon/jobs/current_devices.py +71 -0
- ldcon/jobs/dashboard_api.py +61 -0
- ldcon/jobs/device_parameters.py +426 -0
- ldcon/jobs/dlc_pi_measurement.py +151 -0
- ldcon/jobs/dlcpro_awg_analysis.py +77 -0
- ldcon/jobs/dlcpro_awg_card.py +116 -0
- ldcon/jobs/dlcpro_cards.py +240 -0
- ldcon/jobs/ecdl_branch_hop.py +175 -0
- ldcon/jobs/ecdl_branch_recovery.py +176 -0
- ldcon/jobs/ecdl_devices.py +306 -0
- ldcon/jobs/ecdl_locked_sweep_proxy.py +526 -0
- ldcon/jobs/ecdl_mapped_recovery.py +6 -0
- ldcon/jobs/ecdl_mode_map.py +469 -0
- ldcon/jobs/ecdl_mode_recovery.py +20 -0
- ldcon/jobs/ecdl_online.py +158 -0
- ldcon/jobs/ecdl_operating_point.py +403 -0
- ldcon/jobs/ecdl_pi_commission.py +161 -0
- ldcon/jobs/ecdl_recovery.py +25 -0
- ldcon/jobs/ecdl_recovery_plan.py +90 -0
- ldcon/jobs/ecdl_recovery_test.py +59 -0
- ldcon/jobs/ecdl_relock.py +7 -0
- ldcon/jobs/ecdl_response_study.py +369 -0
- ldcon/jobs/ecdl_steady.py +343 -0
- ldcon/jobs/ecdl_thermal_context.py +242 -0
- ldcon/jobs/ecdl_window_center.py +734 -0
- ldcon/jobs/emission_card.py +102 -0
- ldcon/jobs/fringe_sweep_reference.py +89 -0
- ldcon/jobs/interface_devices.py +122 -0
- ldcon/jobs/interface_worker.py +471 -0
- ldcon/jobs/job_service.py +125 -0
- ldcon/jobs/laser_live.py +270 -0
- ldcon/jobs/learned_settling.py +450 -0
- ldcon/jobs/lock_target.py +461 -0
- ldcon/jobs/locked_triangle.py +36 -0
- ldcon/jobs/measurement_acquisition.py +51 -0
- ldcon/jobs/measurement_analysis.py +59 -0
- ldcon/jobs/measurement_execution.py +40 -0
- ldcon/jobs/measurement_notice.py +78 -0
- ldcon/jobs/measurement_presets.py +52 -0
- ldcon/jobs/measurement_report.py +76 -0
- ldcon/jobs/measurement_statistics.py +22 -0
- ldcon/jobs/metric_cards.py +34 -0
- ldcon/jobs/metric_interface.py +100 -0
- ldcon/jobs/modulation_sweep.py +274 -0
- ldcon/jobs/native_background_session.py +91 -0
- ldcon/jobs/native_current_acquire.py +143 -0
- ldcon/jobs/native_current_card.py +328 -0
- ldcon/jobs/native_current_online.py +129 -0
- ldcon/jobs/native_ff_proxy.py +1361 -0
- ldcon/jobs/native_ff_sweep.py +802 -0
- ldcon/jobs/online_pid.py +178 -0
- ldcon/jobs/operating_point.py +1399 -0
- ldcon/jobs/operating_point_proxy.py +236 -0
- ldcon/jobs/operation_execution.py +126 -0
- ldcon/jobs/operation_history.py +419 -0
- ldcon/jobs/optical_broker.py +23 -0
- ldcon/jobs/pi_algorithms.py +30 -0
- ldcon/jobs/pi_response.py +239 -0
- ldcon/jobs/progress.py +63 -0
- ldcon/jobs/quality_baseline.py +411 -0
- ldcon/jobs/recovery_decisions.py +131 -0
- ldcon/jobs/recovery_history.py +152 -0
- ldcon/jobs/recovery_procedure.py +285 -0
- ldcon/jobs/recovery_reference.py +123 -0
- ldcon/jobs/recovery_stationary.py +138 -0
- ldcon/jobs/response_algorithms.py +47 -0
- ldcon/jobs/response_comparison.py +62 -0
- ldcon/jobs/response_record.py +90 -0
- ldcon/jobs/runtime_parameters.py +434 -0
- ldcon/jobs/settling_profile.py +140 -0
- ldcon/jobs/shot_analysis.py +1300 -0
- ldcon/jobs/simulation.py +127 -0
- ldcon/jobs/slow_control.py +208 -0
- ldcon/jobs/slow_current.py +88 -0
- ldcon/jobs/study_adoption.py +122 -0
- ldcon/jobs/study_execution.py +182 -0
- ldcon/jobs/sweep_ff_analysis.py +320 -0
- ldcon/jobs/sweep_ff_optimization.py +202 -0
- ldcon/jobs/sweep_ff_plan.py +166 -0
- ldcon/jobs/sweep_ff_report.py +254 -0
- ldcon/jobs/sweep_ff_transport.py +326 -0
- ldcon/jobs/sweep_tracking.py +424 -0
- ldcon/jobs/sweep_verdict.py +288 -0
- ldcon/jobs/target_response.py +638 -0
- ldcon/jobs/target_response_analysis.py +65 -0
- ldcon/jobs/target_response_control.py +215 -0
- ldcon/jobs/target_response_decisions.py +152 -0
- ldcon/jobs/target_response_experiment.py +110 -0
- ldcon/jobs/target_response_feedforward.py +130 -0
- ldcon/jobs/target_response_search.py +225 -0
- ldcon/jobs/target_response_shift.py +475 -0
- ldcon/jobs/target_response_thermal.py +113 -0
- ldcon/jobs/target_response_thermal_experiment.py +120 -0
- ldcon/jobs/target_sequence.py +166 -0
- ldcon/jobs/temperature_identification.py +86 -0
- ldcon/jobs/wavelength_history.py +17 -0
- ldcon/jobs/window_actions.py +641 -0
- ldcon/jobs/window_center.py +784 -0
- ldcon/jobs/window_control.py +130 -0
- ldcon/jobs/window_decisions.py +105 -0
- ldcon/jobs/window_sampling.py +66 -0
- ldcon/jobs/workflow_artifacts.py +73 -0
- ldcon/migration/__init__.py +810 -0
- ldcon/migration/migration_decks.py +239 -0
- ldcon/migration/migration_readiness.py +397 -0
- ldcon/migration/migration_relocation.py +283 -0
- ldcon/monitor_owner.py +4 -0
- ldcon/owner_telemetry.py +4 -0
- ldcon/owners/__init__.py +1 -0
- ldcon/owners/adc_log_threshold.py +209 -0
- ldcon/owners/background_grants.py +44 -0
- ldcon/owners/cld_instrument_owner.py +96 -0
- ldcon/owners/cld_owner.py +706 -0
- ldcon/owners/cld_projection.py +194 -0
- ldcon/owners/cld_system_owner.py +276 -0
- ldcon/owners/current_owner.py +68 -0
- ldcon/owners/device_registry.py +229 -0
- ldcon/owners/dlcpro_awg_plan.py +115 -0
- ldcon/owners/dlcpro_awg_port.py +98 -0
- ldcon/owners/dlcpro_awg_protocol.py +190 -0
- ldcon/owners/dlcpro_awg_return.py +63 -0
- ldcon/owners/dlcpro_awg_sweep.py +437 -0
- ldcon/owners/dlcpro_awg_timing.py +35 -0
- ldcon/owners/dlcpro_awg_tracking.py +175 -0
- ldcon/owners/dlcpro_awg_tracking_guard.py +87 -0
- ldcon/owners/dlcpro_awg_tracking_report.py +49 -0
- ldcon/owners/dlcpro_awg_zero.py +82 -0
- ldcon/owners/dlcpro_deployment.py +66 -0
- ldcon/owners/dlcpro_ff.py +64 -0
- ldcon/owners/dlcpro_ff_adjustment.py +159 -0
- ldcon/owners/dlcpro_ff_delta_adjustment.py +194 -0
- ldcon/owners/dlcpro_frequency_window.py +61 -0
- ldcon/owners/dlcpro_observation.py +62 -0
- ldcon/owners/dlcpro_owner.py +830 -0
- ldcon/owners/dlcpro_pi_protocol.py +44 -0
- ldcon/owners/dlcpro_pi_trial.py +161 -0
- ldcon/owners/dlcpro_readback.py +50 -0
- ldcon/owners/dlcpro_service.py +203 -0
- ldcon/owners/dlcpro_target_changes.py +330 -0
- ldcon/owners/dlcpro_target_step.py +44 -0
- ldcon/owners/dlcpro_target_store.py +75 -0
- ldcon/owners/dlcpro_telemetry.py +72 -0
- ldcon/owners/ecdl_background_parameters.py +248 -0
- ldcon/owners/ecdl_background_pid.py +184 -0
- ldcon/owners/ecdl_hot_control.py +255 -0
- ldcon/owners/ecdl_independent_pid.py +95 -0
- ldcon/owners/ecdl_locked_study.py +122 -0
- ldcon/owners/ecdl_locked_sweep.py +1875 -0
- ldcon/owners/ecdl_native_pid.py +181 -0
- ldcon/owners/ecdl_optical.py +37 -0
- ldcon/owners/ecdl_owner.py +1058 -0
- ldcon/owners/ecdl_policy_reload.py +494 -0
- ldcon/owners/ecdl_prelock.py +612 -0
- ldcon/owners/ecdl_projection.py +636 -0
- ldcon/owners/ecdl_steady_owner.py +1859 -0
- ldcon/owners/ecdl_steady_policy.py +212 -0
- ldcon/owners/ecdl_target_ff.py +214 -0
- ldcon/owners/ecdl_target_settling.py +163 -0
- ldcon/owners/ecdl_tec_pair.py +88 -0
- ldcon/owners/ecdl_thermal_study.py +251 -0
- ldcon/owners/emission.py +365 -0
- ldcon/owners/emission_adapters.py +268 -0
- ldcon/owners/esp32_native_client.py +335 -0
- ldcon/owners/esp32_native_owner.py +2166 -0
- ldcon/owners/esp32_time_telemetry.py +253 -0
- ldcon/owners/esp32_udp_gate.py +136 -0
- ldcon/owners/external_sweep_guard.py +44 -0
- ldcon/owners/flexible_native_trial.py +172 -0
- ldcon/owners/fringe_peaks.py +92 -0
- ldcon/owners/full_data_log.py +133 -0
- ldcon/owners/instrument_identity.py +125 -0
- ldcon/owners/instrument_owner.py +1041 -0
- ldcon/owners/instrument_projection.py +104 -0
- ldcon/owners/instrument_service.py +638 -0
- ldcon/owners/migration_physical_lock.py +162 -0
- ldcon/owners/monitor_owner.py +322 -0
- ldcon/owners/native_assembly.py +37 -0
- ldcon/owners/native_assembly_host.py +260 -0
- ldcon/owners/native_background.py +233 -0
- ldcon/owners/native_current_tuning.py +251 -0
- ldcon/owners/native_ff_proof.py +433 -0
- ldcon/owners/native_instrument_owner.py +161 -0
- ldcon/owners/native_modulation_limits.py +45 -0
- ldcon/owners/native_policy_reload.py +186 -0
- ldcon/owners/native_policy_safety.py +213 -0
- ldcon/owners/native_policy_update.py +473 -0
- ldcon/owners/native_system_owner.py +420 -0
- ldcon/owners/operating_mode_owner.py +128 -0
- ldcon/owners/owner_projection.py +530 -0
- ldcon/owners/owner_telemetry.py +166 -0
- ldcon/owners/pi_handover.py +154 -0
- ldcon/owners/projection_transport.py +143 -0
- ldcon/owners/remote_instruments.py +323 -0
- ldcon/owners/sdg_client.py +122 -0
- ldcon/owners/sdg_owner.py +468 -0
- ldcon/owners/tec_rate.py +29 -0
- ldcon/owners/wsu_udp_monitor.py +113 -0
- ldcon/report_pdf.py +4 -0
- ldcon/resources/agent-model-options.json +142 -0
- ldcon/resources/agent_contract/adjustment.txt +45 -0
- ldcon/resources/agent_contract/common.txt +42 -0
- ldcon/resources/agent_contract/diagnosis.txt +11 -0
- ldcon/resources/agent_contract/research.txt +20 -0
- ldcon/resources/agent_contract/response_optimization.txt +13 -0
- ldcon/resources/examples/README.html +10 -0
- ldcon/resources/examples/assignments/sr1092.yaml +100 -0
- ldcon/resources/examples/assignments/sr422.yaml +677 -0
- ldcon/resources/examples/assignments/thf685.yaml +7 -0
- ldcon/resources/examples/interface-decks/dfb-dbr-ldc-tec.yaml +430 -0
- ldcon/resources/examples/interface-decks/ecdl-dlcpro.yaml +288 -0
- ldcon/resources/examples/interface-decks/ecdl-pzt-ldc-tec.yaml +698 -0
- ldcon/resources/examples/lasers/sr1092.yaml +267 -0
- ldcon/resources/examples/lasers/sr422.yaml +403 -0
- ldcon/resources/examples/lasers/thf685.yaml +158 -0
- ldcon/resources/report_template/ldconreport.cls +42 -0
- ldcon/resources/report_template/report.tex +47 -0
- ldcon/web/__init__.py +1 -0
- ldcon/web/agent_approval_api.py +126 -0
- ldcon/web/agent_settings_api.py +54 -0
- ldcon/web/agent_task_api.py +211 -0
- ldcon/web/control_readback.py +63 -0
- ldcon/web/dashboard_activity.py +109 -0
- ldcon/web/dashboard_controls_revision.py +29 -0
- ldcon/web/dashboard_metrics.py +146 -0
- ldcon/web/dashboard_snapshot.py +163 -0
- ldcon/web/dashboard_state.py +104 -0
- ldcon/web/dashboard_wavelength.py +150 -0
- ldcon/web/device_parameters_api.py +52 -0
- ldcon/web/dither_quality.py +46 -0
- ldcon/web/dlcpro_target_api.py +91 -0
- ldcon/web/dlcpro_target_query.py +23 -0
- ldcon/web/failure_api.py +62 -0
- ldcon/web/gateway_lan.py +208 -0
- ldcon/web/grafana_activity.py +84 -0
- ldcon/web/grafana_dashboard.py +492 -0
- ldcon/web/grafana_dlcpro.py +158 -0
- ldcon/web/grafana_quality.py +167 -0
- ldcon/web/interface_projection.py +27 -0
- ldcon/web/job_registry.py +68 -0
- ldcon/web/job_report.py +122 -0
- ldcon/web/laser_operations.py +445 -0
- ldcon/web/lock_target_api.py +554 -0
- ldcon/web/lock_target_queue.py +212 -0
- ldcon/web/log_archive.py +191 -0
- ldcon/web/metric_query_profiles.py +378 -0
- ldcon/web/online_position.py +26 -0
- ldcon/web/operating_mode_api.py +72 -0
- ldcon/web/operation_api.py +34 -0
- ldcon/web/operation_observation.py +57 -0
- ldcon/web/operations_settings.py +92 -0
- ldcon/web/quality_threshold_metrics.py +64 -0
- ldcon/web/shot_analysis_api.py +136 -0
- ldcon/web/sweep_api.py +1186 -0
- ldcon/web/sweep_guard.py +45 -0
- ldcon/web/target_reference_panel.py +38 -0
- ldcon/web/teams_destinations_api.py +81 -0
- ldcon/web/web_fleet.py +482 -0
- ldcon/web/web_runtime.py +811 -0
- ldcon/web/web_server.py +402 -0
- ldcon/web/window_api.py +28 -0
- ldcon/web_assets/agent-settings.css +4 -0
- ldcon/web_assets/agent-settings.html +53 -0
- ldcon/web_assets/agent-settings.js +270 -0
- ldcon/web_assets/app.css +9 -0
- ldcon/web_assets/app.js +802 -0
- ldcon/web_assets/card-browser.js +160 -0
- ldcon/web_assets/card-guide.js +9 -0
- ldcon/web_assets/cards/acquire.metrics.html +82 -0
- ldcon/web_assets/cards/agent.history_review.html +13 -0
- ldcon/web_assets/cards/agent.log_watch.html +1 -0
- ldcon/web_assets/cards/agent.monitor.html +13 -0
- ldcon/web_assets/cards/agent.request.html +1 -0
- ldcon/web_assets/cards/agent.resident.html +1 -0
- ldcon/web_assets/cards/agent.task.html +17 -0
- ldcon/web_assets/cards/commission.ecdl_pi.html +1 -0
- ldcon/web_assets/cards/identify.axis_frequency.html +1 -0
- ldcon/web_assets/cards/identify.current_response.html +1 -0
- ldcon/web_assets/cards/laser.emission.html +8 -0
- ldcon/web_assets/cards/measure.batch.html +1 -0
- ldcon/web_assets/cards/measure.change.html +1 -0
- ldcon/web_assets/cards/measure.target_response.html +1 -0
- ldcon/web_assets/cards/monitor.wavelength.html +1 -0
- ldcon/web_assets/cards/notify.teams.html +1 -0
- ldcon/web_assets/cards/optimize.settling_criteria.html +1 -0
- ldcon/web_assets/cards/optimize.target_feedforward.html +1 -0
- ldcon/web_assets/cards/optimize.target_step.html +1 -0
- ldcon/web_assets/cards/optimize.target_thermal.html +1 -0
- ldcon/web_assets/cards/plan.ecdl_recovery.html +14 -0
- ldcon/web_assets/cards/prepare.ecdl_recovery.html +18 -0
- ldcon/web_assets/cards/recover.ecdl_mode_hop.html +12 -0
- ldcon/web_assets/cards/report.grafana_dashboard.html +22 -0
- ldcon/web_assets/cards/servo.current_pid.html +1 -0
- ldcon/web_assets/cards/servo.dlcpro_frequency.html +1 -0
- ldcon/web_assets/cards/servo.ecdl_steady_lock.html +11 -0
- ldcon/web_assets/cards/servo.native_current.html +1 -0
- ldcon/web_assets/cards/study.ecdl_current_response.html +1 -0
- ldcon/web_assets/cards/study.ecdl_locked_current_map.html +1 -0
- ldcon/web_assets/cards/study.ecdl_mode_map.html +6 -0
- ldcon/web_assets/cards/study.ecdl_thermal_context.html +5 -0
- ldcon/web_assets/cards/study.target_response.html +1 -0
- ldcon/web_assets/cards/sweep.current_modulation.html +1 -0
- ldcon/web_assets/cards/sweep.dlcpro_frequency.html +1 -0
- ldcon/web_assets/cards/sweep.servo_modulation.html +1 -0
- ldcon/web_assets/cards/test.ecdl_recovery.html +4 -0
- ldcon/web_assets/cards/tune.ecdl_initial_point.html +1 -0
- ldcon/web_assets/cards/tune.ecdl_operating_point.html +13 -0
- ldcon/web_assets/cards/tune.ecdl_window_center.html +7 -0
- ldcon/web_assets/cards/tune.native_current.html +1 -0
- ldcon/web_assets/cards/tune.pid_coordinate_search.html +1 -0
- ldcon/web_assets/cards/tune.pid_factor_reduction.html +1 -0
- ldcon/web_assets/cards/tune.pid_online.html +1 -0
- ldcon/web_assets/cards/tune.temperature_once.html +18 -0
- ldcon/web_assets/committing-input.js +35 -0
- ldcon/web_assets/configuration-editor.js +194 -0
- ldcon/web_assets/dashboard-state.js +39 -0
- ldcon/web_assets/failures.html +1 -0
- ldcon/web_assets/failures.js +19 -0
- ldcon/web_assets/fallback.css +3 -0
- ldcon/web_assets/fallback.html +1 -0
- ldcon/web_assets/fallback.js +21 -0
- ldcon/web_assets/fleet-settings.js +20 -0
- ldcon/web_assets/fleet.css +9 -0
- ldcon/web_assets/fleet.html +8 -0
- ldcon/web_assets/fleet.js +130 -0
- ldcon/web_assets/gateway-presentation.js +27 -0
- ldcon/web_assets/grafana-help.js +31 -0
- ldcon/web_assets/help.html +22 -0
- ldcon/web_assets/i18n.js +279 -0
- ldcon/web_assets/index.html +14 -0
- ldcon/web_assets/laser-workspace.js +279 -0
- ldcon/web_assets/modulation.html +1 -0
- ldcon/web_assets/modulation.js +14 -0
- ldcon/web_assets/reservations.html +16 -0
- ldcon/web_assets/reservations.js +77 -0
- ldcon/web_assets/teams-settings.js +91 -0
- ldcon/web_assets/window-actions.js +37 -0
- ldcon/web_assets/workspace.css +99 -0
- ldcon/web_fleet.py +4 -0
- ldcon/web_server.py +4 -0
- ldcon-0.2.0.dist-info/METADATA +85 -0
- ldcon-0.2.0.dist-info/RECORD +482 -0
- ldcon-0.2.0.dist-info/WHEEL +5 -0
- ldcon-0.2.0.dist-info/entry_points.txt +5 -0
- ldcon-0.2.0.dist-info/licenses/LICENSE +21 -0
- ldcon-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Host gateway for approved background current experiments; never raw I/O."""
|
|
2
|
+
import fcntl
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from experiment_core.records import atomic_json, safe_id
|
|
10
|
+
from ldcon.core.records import read_json
|
|
11
|
+
from ldcon.agents.operating_mode_client import request as mode_read, revision as mode_revision, describe as owner_describe
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def control_root(root):
|
|
15
|
+
value = os.environ.get('LDCON_ADAPTIVE_ROOT')
|
|
16
|
+
if not value:
|
|
17
|
+
raise ValueError('Background adjustment unavailable: trusted adaptive root is not configured')
|
|
18
|
+
path = Path(value)
|
|
19
|
+
if (not path.is_absolute() or not path.is_dir() or path.resolve() != path
|
|
20
|
+
or path.is_relative_to(Path(root).resolve())):
|
|
21
|
+
raise ValueError('Trusted adaptive root must be outside the deployment')
|
|
22
|
+
return path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def revoke_monitor(root, laser_id, monitor_id, generation):
|
|
26
|
+
"""Revoke only this monitor's grants. Owner restores independently of model."""
|
|
27
|
+
try:directory = control_root(root)/safe_id(laser_id)
|
|
28
|
+
except ValueError:return
|
|
29
|
+
grant_path=directory/'monitors'/(safe_id(monitor_id)+'.json')
|
|
30
|
+
grant=read_json(grant_path,{})
|
|
31
|
+
if isinstance(grant,dict) and grant.get('generation')==generation:
|
|
32
|
+
atomic_json(grant_path,{**grant,'enabled':False})
|
|
33
|
+
for path in directory.glob('*/background-control.json'):
|
|
34
|
+
value = read_json(path, {})
|
|
35
|
+
if isinstance(value,dict) and value.get('monitor_id') == monitor_id and value.get('generation') == generation:
|
|
36
|
+
atomic_json(path, {**value, 'enabled': False})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def revoke_task(root, laser_id, task_id):
|
|
40
|
+
try:directory = control_root(root)/safe_id(laser_id)
|
|
41
|
+
except ValueError:return
|
|
42
|
+
for selector in directory.glob('*/adaptive-experiment.json'):
|
|
43
|
+
value = read_json(selector, {})
|
|
44
|
+
if not isinstance(value,dict) or value.get('id') != 'task-'+task_id:
|
|
45
|
+
continue
|
|
46
|
+
control = selector.parent/'background-control.json'
|
|
47
|
+
existing = read_json(control, {})
|
|
48
|
+
grant = value.get('background', {})
|
|
49
|
+
if isinstance(existing,dict) and isinstance(grant,dict) and all(existing.get(k) == grant.get(k) for k in ('monitor_id','generation')):
|
|
50
|
+
atomic_json(control, {**existing, 'enabled': False})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
CONTROLLERS = frozenset(('servo.ecdl_steady_lock','servo.native_current'))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _capabilities(operation='servo.ecdl_steady_lock',device=None):
|
|
57
|
+
from ldcon.core.adjustment_registry import capabilities
|
|
58
|
+
return capabilities(operation,device)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _adopted_capabilities(runtime, operation, laser):
|
|
62
|
+
"""Runtime-adjustment capabilities of the profile the rig owner has ADOPTED now (2026-10-28).
|
|
63
|
+
|
|
64
|
+
The controller job's laser.yaml is the copy frozen at lock start; a live-adopted envelope change
|
|
65
|
+
(settings_adopt of background_adjustments.*) never reaches it. Read the owner's own report instead.
|
|
66
|
+
Older owners report only their device revision: the saved definition is used when its device digest
|
|
67
|
+
equals that revision (it then is the adopted device). Otherwise the parameter families are reported
|
|
68
|
+
unavailable with the reason (data validity), never from the stale copy.
|
|
69
|
+
"""
|
|
70
|
+
rigs=[(k,v) for k,v in laser.get('devices',{}).items() if v.get('type')=='ecdl.rig.owner']
|
|
71
|
+
if operation!='servo.ecdl_steady_lock' or len(rigs)!=1:
|
|
72
|
+
return _capabilities(operation,rigs[0][1] if len(rigs)==1 else None)
|
|
73
|
+
key=rigs[0][0]
|
|
74
|
+
try:
|
|
75
|
+
described=owner_describe(laser)
|
|
76
|
+
reported=described.get('background_capabilities')
|
|
77
|
+
if isinstance(reported,dict) and isinstance(reported.get('supported_adjustments'),list):
|
|
78
|
+
return dict(reported,capability_source=dict(source='owner_adopted',revision=described.get('revision')))
|
|
79
|
+
from ldcon.core.interface_schema import digest
|
|
80
|
+
saved=runtime.interface_repository.read('lasers',laser['id'])['document']['devices'][key]
|
|
81
|
+
if digest(saved)!=described.get('revision'):
|
|
82
|
+
raise ValueError('Saved definition is not the owner-adopted device (adoption pending or refused)')
|
|
83
|
+
return dict(_capabilities(operation,saved),capability_source=dict(source='saved_equals_owner_adopted',revision=described['revision']))
|
|
84
|
+
except (OSError,ValueError,KeyError,TypeError,AttributeError) as error:
|
|
85
|
+
result=_capabilities(operation,None)
|
|
86
|
+
reason='Owner-adopted runtime envelope unavailable ('+type(error).__name__+'); no frozen copy is reported'
|
|
87
|
+
for item in result['parameter_adjustments'].values():item['reason']=reason
|
|
88
|
+
return dict(result,capability_source=dict(source='unavailable',reason=reason))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _live_status(status):
|
|
92
|
+
return bool(status.get('active') or status.get('state') in
|
|
93
|
+
('idle','waiting_for_decision','measuring','waiting_for_measurement','waiting_recovery','awaiting_replan'))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def route(runtime, task_id, data=None):
|
|
97
|
+
if data is not None and not isinstance(data, dict):
|
|
98
|
+
raise ValueError('Invalid background step')
|
|
99
|
+
from ldcon.core.interface_schema import load_mapping
|
|
100
|
+
task_dir = runtime.directory(safe_id(task_id))
|
|
101
|
+
task = runtime.get_job(task_id)
|
|
102
|
+
invocation = read_json(task_dir/'invocation.json')
|
|
103
|
+
params = invocation['parameters']
|
|
104
|
+
monitor_id = params.get('monitor_job_id')
|
|
105
|
+
if (params.get('instruction_profile') != 'adjustment'
|
|
106
|
+
or not any(c.get('operation',c.get('card'))=='agent.task' for c in task.get('cards',[]))):
|
|
107
|
+
raise ValueError('Task has no background adjustment grant')
|
|
108
|
+
laser_id = load_mapping((task_dir/'laser.yaml').read_text())['id']
|
|
109
|
+
manual=not monitor_id
|
|
110
|
+
if manual:
|
|
111
|
+
monitor_id='task-'+task_id
|
|
112
|
+
monitor=task
|
|
113
|
+
else:
|
|
114
|
+
monitor_dir = runtime.directory(safe_id(monitor_id))
|
|
115
|
+
monitor = runtime.get_job(monitor_id)
|
|
116
|
+
if (monitor.get('set_id') != task.get('set_id')
|
|
117
|
+
or load_mapping((monitor_dir/'laser.yaml').read_text())['id'] != laser_id
|
|
118
|
+
or not any(c.get('operation', c.get('card')) == 'agent.monitor' for c in monitor.get('cards', []))):
|
|
119
|
+
raise ValueError('Monitor grant belongs to another laser or operation')
|
|
120
|
+
root=control_root(runtime.root)
|
|
121
|
+
grant_path=root/safe_id(laser_id)/'monitors'/(safe_id(monitor_id)+'.json')
|
|
122
|
+
if any(path.is_symlink() for path in (grant_path,grant_path.parent,grant_path.parent.parent)):
|
|
123
|
+
raise ValueError('Trusted monitor grant must not be a symlink')
|
|
124
|
+
grant=read_json(grant_path,{})
|
|
125
|
+
if not isinstance(grant,dict):grant={}
|
|
126
|
+
if manual:
|
|
127
|
+
from ldcon.agents.manual_adjustment_grant import plan
|
|
128
|
+
from experiment_core.records import digest
|
|
129
|
+
expected=plan(task_dir,params)
|
|
130
|
+
if (grant.get('source')!='manual_task' or grant.get('task_id')!=task_id
|
|
131
|
+
or grant.get('bounds')!=expected or grant.get('parameters_hash')!=digest(expected)):
|
|
132
|
+
raise ValueError('Manual grant does not match the frozen approved plan')
|
|
133
|
+
updated=grant.get('last_seen');max_age=grant.get('max_age_s')
|
|
134
|
+
live_grant=bool(task.get('status')=='running' and monitor.get('status')=='running'
|
|
135
|
+
and grant.get('enabled') is True and isinstance(grant.get('bounds'),dict) and grant['bounds'].get('enabled') is True
|
|
136
|
+
and isinstance(grant.get('generation'),str) and grant['generation']
|
|
137
|
+
and isinstance(grant.get('parameters_hash'),str) and grant['parameters_hash']
|
|
138
|
+
and type(updated) in (int,float) and math.isfinite(updated)
|
|
139
|
+
and type(max_age) in (int,float) and math.isfinite(max_age) and 0<max_age<=180
|
|
140
|
+
and 0<=time.time()-updated<=max_age)
|
|
141
|
+
# Runtime parameter receipt, rather than editable model text, determines the
|
|
142
|
+
# enabled envelope. Physical range/step/slew validation remains in the owner.
|
|
143
|
+
bounds = grant.get('bounds',{})
|
|
144
|
+
jobs = runtime.active_jobs(set_id=task['set_id'])
|
|
145
|
+
controllers = [j for j in jobs if j.get('status') == 'running' and any(
|
|
146
|
+
c.get('operation', c.get('card')) in CONTROLLERS for c in j.get('cards', []))]
|
|
147
|
+
if len(controllers) != 1:
|
|
148
|
+
return dict(status='unavailable', reason='One running supported controller is required; no lock is started',
|
|
149
|
+
supported_adjustments=[],unsupported_adjustments=['current','pi','slow_loop','feedforward'])
|
|
150
|
+
controller = controllers[0]
|
|
151
|
+
operation=next(c.get('operation',c.get('card')) for c in controller['cards']
|
|
152
|
+
if c.get('operation',c.get('card')) in CONTROLLERS)
|
|
153
|
+
laser=load_mapping((runtime.directory(safe_id(controller['id']))/'laser.yaml').read_text())
|
|
154
|
+
if laser['id']!=laser_id:
|
|
155
|
+
raise ValueError('Controller belongs to another laser')
|
|
156
|
+
capabilities=_adopted_capabilities(runtime,operation,laser)
|
|
157
|
+
directory = root/safe_id(laser_id)/safe_id(controller['id'])
|
|
158
|
+
if directory.parent.is_symlink() or directory.is_symlink() or not directory.is_dir():
|
|
159
|
+
raise ValueError('Controller has not enabled the trusted adaptive manager')
|
|
160
|
+
selection_id = 'task-'+task_id
|
|
161
|
+
experiment = directory/'adaptive-experiments'/selection_id/'adaptive-window'
|
|
162
|
+
status = read_json(experiment/'status.json', {})
|
|
163
|
+
mode_laser=load_mapping((runtime.directory(safe_id(controller['id']))/'laser.yaml').read_text())
|
|
164
|
+
operating_mode=mode_read(mode_laser) if data is None or data.get('action')!='cancel' else None
|
|
165
|
+
if data is None:
|
|
166
|
+
from ldcon.agents.adjustment_guidance import current as current_guidance
|
|
167
|
+
guidance=current_guidance(runtime,laser_id)
|
|
168
|
+
results = sorted((experiment/'results').glob('*.json'), key=lambda p:p.stat().st_mtime)
|
|
169
|
+
latest = read_json(results[-1], {}) if results else None
|
|
170
|
+
return dict(status=status or dict(state='not_started'), latest_result=latest, operating_mode=operating_mode, adjustment_guidance=guidance,
|
|
171
|
+
checkpoint=read_json(experiment/'checkpoint.json', {}),
|
|
172
|
+
handoff=read_json(directory/'background-handoff.json',{}),
|
|
173
|
+
controller_job_id=controller['id'],envelope=bounds,grant_active=live_grant,
|
|
174
|
+
**capabilities)
|
|
175
|
+
if not isinstance(data, dict) or set(data)-{'action','request_id','value','based_on','family','pi_candidate','parameter_candidate','candidate','expected_operating_mode_revision'}:
|
|
176
|
+
raise ValueError('Invalid background step')
|
|
177
|
+
action = data.get('action')
|
|
178
|
+
safe_id(data.get('request_id'))
|
|
179
|
+
if action not in ('begin','measure_here','move_and_measure','adopt','cancel','replan'):
|
|
180
|
+
raise ValueError('Unsupported background step')
|
|
181
|
+
if 'family' in data and action!='begin':raise ValueError('Adjustment family is selected only at begin')
|
|
182
|
+
if 'candidate' in data and action!='replan':raise ValueError('A continuation candidate is valid only at replan')
|
|
183
|
+
if 'pi_candidate' in data and (action!='begin' or data.get('family')!='pi'):
|
|
184
|
+
raise ValueError('PI candidate is selected only at PI begin')
|
|
185
|
+
from ldcon.core.adjustment_registry import FAMILIES,PARAMETER_FAMILIES,parameters as trial_parameters
|
|
186
|
+
family=data.get('family','current')
|
|
187
|
+
if 'parameter_candidate' in data and (action!='begin' or family not in PARAMETER_FAMILIES):
|
|
188
|
+
raise ValueError('Runtime parameter candidate is selected only at slow-loop/feedforward begin')
|
|
189
|
+
if family not in FAMILIES:raise ValueError('Unsupported background adjustment family')
|
|
190
|
+
if action=='begin' and family not in capabilities['supported_adjustments']:
|
|
191
|
+
raise ValueError('This controller does not support the requested adjustment family')
|
|
192
|
+
if action=='begin' and params.get('adjustment_family','auto') not in ('auto',family):
|
|
193
|
+
raise ValueError('Adjustment family differs from this task selection')
|
|
194
|
+
if action in ('move_and_measure','adopt'):
|
|
195
|
+
if type(data.get('value')) not in (int,float) or not math.isfinite(data['value']):
|
|
196
|
+
raise ValueError('Current movement/adoption requires a finite numeric value')
|
|
197
|
+
elif 'value' in data:raise ValueError('This background action accepts no current value')
|
|
198
|
+
if action!='cancel' and not live_grant:
|
|
199
|
+
raise ValueError('Monitor/task grant is stopped, disabled or stale; no new background action')
|
|
200
|
+
if action!='cancel':
|
|
201
|
+
expected=data.get('expected_operating_mode_revision')
|
|
202
|
+
if type(expected) is not int or expected != mode_revision(operating_mode):
|
|
203
|
+
raise ValueError('Operating mode changed or is missing; refresh owner mode before planning')
|
|
204
|
+
descriptor=os.open(directory/'gateway.lock',os.O_CREAT|os.O_RDWR|os.O_NOFOLLOW,0o600)
|
|
205
|
+
with os.fdopen(descriptor,'a') as lock:
|
|
206
|
+
fcntl.flock(lock, fcntl.LOCK_EX)
|
|
207
|
+
selector_path = directory/'adaptive-experiment.json'
|
|
208
|
+
old = read_json(selector_path, {})
|
|
209
|
+
if old and old.get('id') != selection_id:
|
|
210
|
+
other = read_json(directory/'adaptive-experiments'/safe_id(old['id'])/'adaptive-window/status.json', {})
|
|
211
|
+
if not other or _live_status(other):
|
|
212
|
+
raise ValueError('Another adaptive task owns this controller')
|
|
213
|
+
if action == 'begin':
|
|
214
|
+
if family not in bounds.get('families',['current']):
|
|
215
|
+
raise ValueError('Adjustment family is outside this monitor grant')
|
|
216
|
+
selected=dict(id=selection_id,background=dict(monitor_id=monitor_id,generation=grant['generation'],
|
|
217
|
+
handoff_timeout_s=bounds['handoff_timeout_s'],grant_revision=grant['parameters_hash'],
|
|
218
|
+
expected_operating_mode_revision=data['expected_operating_mode_revision']))
|
|
219
|
+
if read_json(directory/'protocol.json',{}).get('continuation') is True:
|
|
220
|
+
selected['continuation']=True
|
|
221
|
+
if family=='pi':
|
|
222
|
+
from ldcon.jobs.background_pid_session import background_parameters
|
|
223
|
+
approved=bounds.get('pid_parameters',{})
|
|
224
|
+
p=background_parameters({'parameters':approved},
|
|
225
|
+
{'candidate':data['pi_candidate']} if 'pi_candidate' in data else {},require_candidate=True)
|
|
226
|
+
if p['save_settings']:
|
|
227
|
+
raise ValueError('Background PI requires explicit save_settings=false')
|
|
228
|
+
if p['strategy']=='validated_candidates' and operation not in ('servo.native_current','servo.ecdl_steady_lock'):
|
|
229
|
+
raise ValueError('This controller does not support explicit PI candidates')
|
|
230
|
+
controller_ids={c.get('id',c.get('card_id')) for c in controller.get('cards',[]) if c.get('operation',c.get('card'))==operation}
|
|
231
|
+
if p['controller_card_id'] not in controller_ids:
|
|
232
|
+
raise ValueError('Approved PI parameters name another controller card')
|
|
233
|
+
selected.update(family='pi',pid_parameters=p)
|
|
234
|
+
elif family in PARAMETER_FAMILIES:
|
|
235
|
+
plan=dict((bounds.get('adjustment_parameters') or {}).get(family,{}) or {})
|
|
236
|
+
if 'parameter_candidate' in data:plan['candidate']=data['parameter_candidate']
|
|
237
|
+
p=trial_parameters(plan,family,require_candidate=True)
|
|
238
|
+
selected.update(family=family,adjustment_parameters=p)
|
|
239
|
+
else:
|
|
240
|
+
selected.update(half_span_v=bounds['half_span_v'],timeout_s=bounds['timeout_s'],
|
|
241
|
+
max_points=bounds['max_points'],require_dither=True)
|
|
242
|
+
if old and old.get('id') == selection_id:
|
|
243
|
+
if old != selected:raise ValueError('Background admission changed; do not replace an existing session')
|
|
244
|
+
control=read_json(directory/'background-control.json',{})
|
|
245
|
+
if not control.get('enabled') or not all(control.get(k)==selected['background'][k] for k in ('monitor_id','generation')):
|
|
246
|
+
return dict(status='revoked',experiment_id=selection_id,message='This immutable session cannot be revived; create a new task')
|
|
247
|
+
if status and not _live_status(status):return dict(status='already_terminal',experiment_id=selection_id,experiment_status=status)
|
|
248
|
+
return dict(status='already_submitted', experiment_id=selection_id)
|
|
249
|
+
atomic_json(directory/'background-control.json', dict(enabled=True, monitor_id=monitor_id, generation=grant['generation']))
|
|
250
|
+
atomic_json(selector_path, selected)
|
|
251
|
+
return dict(status='submitted', experiment_id=selection_id, message='Await owner admission; no measurement yet')
|
|
252
|
+
if old.get('id') != selection_id or not (experiment/'inbox').is_dir():
|
|
253
|
+
raise ValueError('Background experiment is not admitted yet')
|
|
254
|
+
if old.get('background',{}).get('monitor_id')!=monitor_id:
|
|
255
|
+
raise ValueError('Experiment belongs to another monitor')
|
|
256
|
+
if action!='cancel' and old['background'].get('expected_operating_mode_revision')!=mode_revision(operating_mode):
|
|
257
|
+
raise ValueError('Operating mode changed after admission; cleanup remains available')
|
|
258
|
+
if old.get('family') in ('pi',*PARAMETER_FAMILIES) and action not in ('cancel','replan'):
|
|
259
|
+
raise ValueError('Parameter comparison runs autonomously; inspect status or cancel this admitted trial')
|
|
260
|
+
if status and not _live_status(status):
|
|
261
|
+
if action=='cancel':return dict(status='already_terminal',experiment_status=status)
|
|
262
|
+
raise ValueError('Background experiment is terminal; no late proposal may act')
|
|
263
|
+
control=read_json(directory/'background-control.json',{})
|
|
264
|
+
if action!='cancel' and (not control.get('enabled') or not all(
|
|
265
|
+
control.get(k)==old['background'].get(k)==({'monitor_id':monitor_id,'generation':grant['generation']})[k]
|
|
266
|
+
for k in ('monitor_id','generation')) or old['background'].get('grant_revision')!=grant['parameters_hash']):
|
|
267
|
+
raise ValueError('Background intervention generation was revoked')
|
|
268
|
+
command = dict(id=data['request_id'], action=action, based_on=data.get('based_on'))
|
|
269
|
+
if 'value' in data:command['value'] = data['value']
|
|
270
|
+
if action=='replan':
|
|
271
|
+
if old.get('continuation') is not True or status.get('state')!='awaiting_replan':
|
|
272
|
+
raise ValueError('Owner has not confirmed a recoverable continuation')
|
|
273
|
+
if 'candidate' in data:
|
|
274
|
+
if old.get('family')=='pi':
|
|
275
|
+
from ldcon.jobs.background_pid_session import background_parameters
|
|
276
|
+
checked=background_parameters({'parameters':bounds['pid_parameters']},{'candidate':data['candidate']},require_candidate=True)
|
|
277
|
+
elif old.get('family') in PARAMETER_FAMILIES:
|
|
278
|
+
checked=trial_parameters({**bounds['adjustment_parameters'][old['family']],'candidate':data['candidate']},old['family'],require_candidate=True)
|
|
279
|
+
else:raise ValueError('Current replan requires a fresh baseline, not a candidate override')
|
|
280
|
+
command['candidate']=checked['candidate']
|
|
281
|
+
# Immutable ID, no command replay even after the worker claimed it.
|
|
282
|
+
for folder in ('inbox','claimed'):
|
|
283
|
+
previous = read_json(experiment/folder/(command['id']+'.json'))
|
|
284
|
+
if previous:
|
|
285
|
+
if previous != command:raise ValueError('Background command ID conflicts')
|
|
286
|
+
return dict(status='already_submitted', request_id=command['id'])
|
|
287
|
+
if action!='cancel' and (not isinstance(command['based_on'],str) or command['based_on'] != status.get('decision_revision')):
|
|
288
|
+
raise ValueError('Background decision evidence changed')
|
|
289
|
+
atomic_json(experiment/'inbox'/(command['id']+'.json'), command)
|
|
290
|
+
return dict(status='submitted', request_id=command['id'], message='Queued; inspect result and owner receipt')
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def call(box, name, args):
|
|
294
|
+
from ldcon.agents.agent_cards import _api
|
|
295
|
+
if box.context.get('task_scope') != 'background_adjustment':
|
|
296
|
+
raise ValueError('Only a bounded adjustment task may use this route')
|
|
297
|
+
tail = 'agent-tasks/'+box.meta['id']+'/background'
|
|
298
|
+
return _api(box, tail) if name == 'background_adjustment_status' else _api(box, tail, 'POST', args)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def install_routes(app, prefix, body):
|
|
302
|
+
from fastapi import Request
|
|
303
|
+
@app.get(prefix+'/api/agent-tasks/{task_id}/background')
|
|
304
|
+
def status(task_id: str):return route(app.state.runtime, task_id)
|
|
305
|
+
|
|
306
|
+
@app.post(prefix+'/api/agent-tasks/{task_id}/background')
|
|
307
|
+
async def step(task_id: str, request: Request):return route(app.state.runtime, task_id, await body(request))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Installed English instruction sources and bounded reproducible task input."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from ldcon.core.paths import resources_dir
|
|
6
|
+
from experiment_core.agent_contracts import instruction_bundle
|
|
7
|
+
from experiment_core.records import atomic_json, digest
|
|
8
|
+
|
|
9
|
+
REVISION = '1.7'
|
|
10
|
+
PROFILES = {'diagnosis', 'adjustment', 'research'}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build(directory, profile, *, context, evidence, max_input_bytes=32000):
|
|
14
|
+
if profile not in PROFILES:
|
|
15
|
+
raise ValueError('Unknown instruction profile')
|
|
16
|
+
resources = resources_dir() / 'agent_contract'
|
|
17
|
+
from ldcon.core.records import read_json
|
|
18
|
+
context = {**context, 'operating_mode_at_card_start':
|
|
19
|
+
read_json(Path(directory)/'operating-mode-at-start.json',
|
|
20
|
+
dict(status='unavailable', reason='No owner observation recorded at card start'))}
|
|
21
|
+
from experiment_core.adjustment_guidance import instructions as guidance_instructions
|
|
22
|
+
context['adjustment_guidance_at_card_start']=read_json(
|
|
23
|
+
Path(directory)/'adjustment-guidance-at-start.json',
|
|
24
|
+
dict(availability='not_recorded',advisory_only=True))
|
|
25
|
+
from ldcon.agents.report_pdf import instructions as report_instructions
|
|
26
|
+
bundle = instruction_bundle((resources/'common.txt').read_bytes()+b'\n'+(resources/'response_optimization.txt').read_bytes()+b'\n'+guidance_instructions().encode()+b'\n'+report_instructions().encode(),
|
|
27
|
+
(resources/(profile+'.txt')).read_bytes(), revision=REVISION,
|
|
28
|
+
context=context, evidence=evidence, max_input_bytes=max_input_bytes)
|
|
29
|
+
bundle['instruction_profile'] = profile
|
|
30
|
+
bundle['bundle_revision'] = digest(bundle)
|
|
31
|
+
atomic_json(Path(directory)/'agent-bundle.json', bundle)
|
|
32
|
+
return bundle
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def turn_origin(directory, request_id, kind, *, active=True, task_id=None):
|
|
36
|
+
# The inbox wrapper is host-created. Its messages themselves still grant no
|
|
37
|
+
# physical authority; language selection is not authorization.
|
|
38
|
+
from ldcon.core.records import atomic_json
|
|
39
|
+
value = dict(request_id=request_id, kind=kind, active=active,
|
|
40
|
+
job_id=Path(directory).name,
|
|
41
|
+
origin='human' if kind in ('request', 'hub_inbox_watch') else 'automatic')
|
|
42
|
+
if task_id:
|
|
43
|
+
value['task_id'] = task_id
|
|
44
|
+
atomic_json(Path(directory)/'agent-current-request.json', value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def capability_probe(box, args):
|
|
48
|
+
"""Local, non-actuating end-to-end MCP probe with an unpredictable challenge."""
|
|
49
|
+
from ldcon.core.records import read_json
|
|
50
|
+
from ldcon.agents.agent_tools import TOOLS
|
|
51
|
+
challenge = read_json(box.directory/'agent-probe-challenge.json', {})
|
|
52
|
+
if args.get('nonce') != challenge.get('nonce') or not challenge:
|
|
53
|
+
raise ValueError('Capability probe challenge changed')
|
|
54
|
+
manifest = read_json(box.directory/'agent-bundle.json', {})
|
|
55
|
+
if not manifest or manifest['bundle_revision'] != challenge['bundle_revision']:
|
|
56
|
+
raise ValueError('Capability probe bundle mismatch')
|
|
57
|
+
if digest({k:v for k,v in manifest.items() if k!='bundle_revision'})!=manifest['bundle_revision']:
|
|
58
|
+
raise ValueError('Capability probe bundle integrity mismatch')
|
|
59
|
+
if (hashlib.sha256(manifest['instructions'].encode()).hexdigest()!=manifest['instructions_sha256']
|
|
60
|
+
or digest(manifest['input'])!=manifest['input_sha256']):
|
|
61
|
+
raise ValueError('Capability probe content integrity mismatch')
|
|
62
|
+
if manifest['input']['context']['laser_id']!=box._laser_id:
|
|
63
|
+
raise ValueError('Capability probe laser mismatch')
|
|
64
|
+
names = {item['name'] for item in TOOLS}
|
|
65
|
+
allowed = box.context.get('task_allowed_tools')
|
|
66
|
+
if allowed is not None:
|
|
67
|
+
names &= set(allowed)
|
|
68
|
+
receipt = dict(nonce=challenge['nonce'], bundle_revision=manifest['bundle_revision'],
|
|
69
|
+
laser_id=box._laser_id, tools=sorted(names),
|
|
70
|
+
scoped_bundle_read=True, hardware_operations=0,
|
|
71
|
+
probe_type='mcp_probe_invocation_and_scoped_manifest_read',
|
|
72
|
+
tool_listing='enumerated', operation_permissions='not_probed',
|
|
73
|
+
image_interpretation='not_probed')
|
|
74
|
+
atomic_json(box.directory/'agent-probe-receipt.json', receipt)
|
|
75
|
+
return receipt
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Same-laser card gateway; no raw instrument transport or API token exposure."""
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from urllib.request import Request,build_opener,ProxyHandler,HTTPRedirectHandler
|
|
5
|
+
from urllib.error import HTTPError
|
|
6
|
+
from ldcon.core.interface_schema import identifier,RETIRED_OPERATIONS,parameters
|
|
7
|
+
from ldcon.core.records import read_json
|
|
8
|
+
from ldcon.agents.failure_policy import sanitize
|
|
9
|
+
|
|
10
|
+
# Laser-agent authority policy (2026-10-21): card operations and settings are
|
|
11
|
+
# the agent's own work. Safety ranges are gated by the host approval ledger
|
|
12
|
+
# (agent_authority), not by the Claude auto-mode classifier, so every scoped
|
|
13
|
+
# MCP tool is preapproved. Put a tool name here to hand it back to native review.
|
|
14
|
+
REVIEWED_TOOLS=frozenset()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NoRedirect(HTTPRedirectHandler):
|
|
18
|
+
def redirect_request(self,*args,**kwargs):
|
|
19
|
+
raise ValueError('Local card API must not redirect')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _api(box,tail,method='GET',payload=None):
|
|
23
|
+
server=read_json(box.root/'web-server.json',{})
|
|
24
|
+
port=server.get('port');prefix=server.get('path')
|
|
25
|
+
if (type(port) is not int or not 1<=port<=65535 or not isinstance(prefix,str)
|
|
26
|
+
or not re.fullmatch(r'/s/[A-Za-z0-9_-]{24,80}/',prefix)):
|
|
27
|
+
raise ValueError('Local dashboard is not configured')
|
|
28
|
+
request=Request('http://127.0.0.1:'+str(port)+prefix+'api/'+tail,
|
|
29
|
+
data=None if payload is None else json.dumps(payload,allow_nan=False).encode(),method=method,
|
|
30
|
+
headers={'Content-Type':'application/json','X-LDCon-Client':'web'})
|
|
31
|
+
try:
|
|
32
|
+
with build_opener(ProxyHandler({}),NoRedirect()).open(request,timeout=15) as response:
|
|
33
|
+
raw=response.read(2**20+1)
|
|
34
|
+
if len(raw)>2**20:raise ValueError('Card API response exceeds limit')
|
|
35
|
+
return json.loads(raw)
|
|
36
|
+
except HTTPError as error:
|
|
37
|
+
if error.code not in (400,403,404,409,422):raise
|
|
38
|
+
try:
|
|
39
|
+
rejected=json.loads(error.read(65536))
|
|
40
|
+
detail=rejected.get('detail','Card API rejected the request')
|
|
41
|
+
operation_error=rejected.get('operation_error')
|
|
42
|
+
except (ValueError,AttributeError):
|
|
43
|
+
detail='Card API rejected the request';operation_error=None
|
|
44
|
+
result=dict(status='rejected',http_status=error.code,reason=sanitize(detail))
|
|
45
|
+
if isinstance(operation_error,dict):result['operation_error']=sanitize(operation_error)
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _scope(box):
|
|
50
|
+
identifier(box.meta['set_id']);identifier(box.meta['deck_id'])
|
|
51
|
+
bundle=_api(box,'sets/'+box.meta['set_id'])
|
|
52
|
+
if bundle.get('status')=='rejected':return bundle,None
|
|
53
|
+
if bundle.get('laser_definition',{}).get('document',{}).get('id')!=box._laser_id:
|
|
54
|
+
raise ValueError('Current assignment belongs to another laser')
|
|
55
|
+
deck=next((d for d in bundle['deck_definitions'] if d['id']==box.meta['deck_id']),None)
|
|
56
|
+
if not deck:raise ValueError('Resident deck is no longer assigned')
|
|
57
|
+
return bundle,deck
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _overrides(text):
|
|
61
|
+
if len(text.encode())>32768:raise ValueError('Card parameter override exceeds limit')
|
|
62
|
+
def reject(_):raise ValueError('Finite JSON required')
|
|
63
|
+
result=json.loads(text,parse_constant=reject)
|
|
64
|
+
if not isinstance(result,dict):raise ValueError('Card overrides must be a JSON mapping')
|
|
65
|
+
# JSON can parse 1e999 as infinity without calling parse_constant.
|
|
66
|
+
json.dumps(result,allow_nan=False)
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def active_jobs(box):
|
|
72
|
+
"""Same-laser discovery across assigned decks; never infer a device lease."""
|
|
73
|
+
value=_api(box,'jobs?active_only=true')
|
|
74
|
+
if value.get('status')=='rejected':return value
|
|
75
|
+
rows=[]
|
|
76
|
+
for job in value.get('jobs',[]):
|
|
77
|
+
if (job.get('set_id')!=box.meta['set_id'] or job.get('laser',{}).get('id')!=box._laser_id
|
|
78
|
+
or job.get('status') in ('failed','cancelled','interrupted','succeeded','completed','completed_with_failures')):
|
|
79
|
+
continue
|
|
80
|
+
rows.append(dict(job_id=job['id'],deck_id=job.get('deck_id'),status=job.get('status'),
|
|
81
|
+
controller_state=job.get('controller_state'),read_only=bool(job.get('read_only')),
|
|
82
|
+
blocks_new_control_card=not job.get('read_only',False),
|
|
83
|
+
cards=[dict(id=c.get('id'),operation=c.get('operation',c.get('card'))) for c in job.get('cards',[])],
|
|
84
|
+
created_utc=job.get('created_utc'),message=sanitize(job.get('message'))))
|
|
85
|
+
return dict(laser_id=box._laser_id,jobs=rows,
|
|
86
|
+
control_slot_blockers=[r['job_id'] for r in rows if r['blocks_new_control_card']],
|
|
87
|
+
evidence='Current dashboard job metadata, not an independent owner lease or physical lock verification. '
|
|
88
|
+
'Use laser_card_job then laser_log_read for current readings. Never clear or stop an existing card solely because a duplicate start was rejected.')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def call(box,name,args):
|
|
92
|
+
if name=='agent_work_capabilities':
|
|
93
|
+
return box.context.get('execution_profile',dict(status='unavailable',reason='Reconnect with a current work profile'))
|
|
94
|
+
bundle,deck=_scope(box)
|
|
95
|
+
if deck is None:return bundle
|
|
96
|
+
if name=='laser_active_jobs':return active_jobs(box)
|
|
97
|
+
if name=='laser_cards':
|
|
98
|
+
description=_api(box,'lasers/'+box._laser_id+'/operations')
|
|
99
|
+
if description.get('status')=='rejected':return description
|
|
100
|
+
capabilities={row['card_id']:row for row in description.get('cards',[])
|
|
101
|
+
if row.get('set_id')==box.meta['set_id'] and row.get('deck_id')==box.meta['deck_id']}
|
|
102
|
+
rows=[]
|
|
103
|
+
for card in deck['document']['cards']:
|
|
104
|
+
info=capabilities.get(card['id'],dict(runnable=False,
|
|
105
|
+
blocked_reasons=['Operation is not registered for this laser'],parameters={}))
|
|
106
|
+
rows.append(dict(id=card['id'],operation=card['operation'],retired=card['operation'] in RETIRED_OPERATIONS,
|
|
107
|
+
runnable=info['runnable'],blocked_reasons=sanitize(info.get('blocked_reasons',[])),
|
|
108
|
+
parameters=sanitize(info.get('parameters',{}))))
|
|
109
|
+
return dict(laser_id=box._laser_id,set_id=box.meta['set_id'],deck_id=box.meta['deck_id'],
|
|
110
|
+
expected_revision=bundle['revision'],cards=rows,
|
|
111
|
+
parameter_note='Displayed parameters may be redacted. Supply only explicit overrides; defaults remain in the backend.',
|
|
112
|
+
execution_note='Hourly monitoring is read-only; separately authorized card work uses native permission review and existing owners.')
|
|
113
|
+
if name in ('laser_card_job','laser_card_stop'):
|
|
114
|
+
if not re.fullmatch('[0-9a-f]{32}',args['job_id']):raise ValueError('Expected a job ID, not a path')
|
|
115
|
+
job=_api(box,'jobs/'+args['job_id'])
|
|
116
|
+
if job.get('status')=='rejected':return job
|
|
117
|
+
if (job.get('set_id')!=box.meta['set_id'] or job.get('laser',{}).get('id')!=box._laser_id):
|
|
118
|
+
raise ValueError('Job belongs to another laser/deck')
|
|
119
|
+
if name=='laser_card_stop':
|
|
120
|
+
reference=args['human_confirmation'].strip()
|
|
121
|
+
if not reference or len(reference)>2000:raise ValueError('Record the actual human stop/switch request reference')
|
|
122
|
+
if job['id']==box.meta['id']:raise ValueError('Do not stop your own resident transport; use the dashboard for resident replacement')
|
|
123
|
+
def stop():
|
|
124
|
+
result=_api(box,'jobs/'+job['id']+'/stop','POST',{})
|
|
125
|
+
if result.get('status')=='rejected':return result
|
|
126
|
+
return dict(status='stop_requested',job_id=job['id'],job_status=result.get('status'),
|
|
127
|
+
stop_reconciliation=sanitize(result.get('stop_reconciliation')),
|
|
128
|
+
human_confirmation=sanitize(reference),
|
|
129
|
+
note='Poll laser_card_job until terminal. For an orphan operating-point proxy, inspect stop_reconciliation for the correlated parent/queue and independent owner proof; unknown prior effects remain unknown. For a stopped lock, use laser_stop_verify. Never kill, clear holds or infer physical state from pre-stop readings.')
|
|
130
|
+
return box.once(name,args,stop)
|
|
131
|
+
return dict(id=job['id'],status=job['status'],controller_state=job.get('controller_state'),
|
|
132
|
+
stop_reconciliation=sanitize(job.get('stop_reconciliation')),
|
|
133
|
+
message=sanitize(job.get('message')),online_tuning=sanitize(job.get('online_tuning',{})),
|
|
134
|
+
summary=sanitize(job.get('summary',{})),readings=sanitize(job.get('readings',{})),
|
|
135
|
+
cards=[dict(id=c.get('id'),operation=c.get('operation',c.get('card'))) for c in job.get('cards',[])],
|
|
136
|
+
evidence='Recorded dashboard state; use laser_log_read for details. Not independent hardware verification.')
|
|
137
|
+
identifier(args['card_id'])
|
|
138
|
+
card=next((c for c in deck['document']['cards'] if c['id']==args['card_id']),None)
|
|
139
|
+
if not card or card['operation'] in RETIRED_OPERATIONS:raise ValueError('Select a current card in this laser deck')
|
|
140
|
+
if card['operation'] in ('agent.resident','agent.log_watch','agent.request','notify.teams'):
|
|
141
|
+
raise ValueError('Use the dedicated resident/notification tools; no recursive agent launch')
|
|
142
|
+
if args['expected_revision']!=bundle['revision']:
|
|
143
|
+
return dict(status='rejected',reason='Assignment changed; refresh laser_cards and validate again')
|
|
144
|
+
overrides=_overrides(args.get('parameters_json','{}'))
|
|
145
|
+
# Validate locally before any HTTP mutation, with current assignment defaults.
|
|
146
|
+
p=parameters(card,{**deck.get('invocation_parameters',{}).get(card['id'],{}),**overrides})
|
|
147
|
+
route=f"sets/{box.meta['set_id']}/decks/{box.meta['deck_id']}"
|
|
148
|
+
payload=dict(parameters=overrides,expected_revision=bundle['revision'])
|
|
149
|
+
if name=='laser_card_validate':
|
|
150
|
+
result=_api(box,route+'/cards/'+card['id']+'/validate','POST',payload)
|
|
151
|
+
return dict(status='rejected' if result.get('status')=='rejected' else 'validated',
|
|
152
|
+
runnable=result.get('runnable',False),reason=result.get('reason'),
|
|
153
|
+
blocked_reasons=sanitize(result.get('blocked_reasons',[])),
|
|
154
|
+
parameters=sanitize(result.get('parameters',p)),expected_revision=bundle['revision'],
|
|
155
|
+
operation_error=sanitize(result.get('operation_error')))
|
|
156
|
+
if name!='laser_card_start':raise ValueError('Unknown card tool')
|
|
157
|
+
reference=args.get('human_confirmation','').strip()
|
|
158
|
+
if not reference or len(reference)>2000:raise ValueError('Record the actual human request/confirmation reference')
|
|
159
|
+
def start():
|
|
160
|
+
# No automatic stop, service restart, lease takeover or hold release.
|
|
161
|
+
result=_api(box,route+'/start','POST',dict(payload,card_id=card['id']))
|
|
162
|
+
if result.get('status')=='rejected':
|
|
163
|
+
if result.get('http_status')==409:
|
|
164
|
+
try:result=dict(result,active_jobs=active_jobs(box))
|
|
165
|
+
except (OSError,ValueError):result=dict(result,active_jobs=dict(status='unavailable'))
|
|
166
|
+
return result
|
|
167
|
+
return dict(status='submitted',job_id=result['id'],card_id=card['id'],
|
|
168
|
+
job_status=result.get('status'),online_tuning=sanitize(result.get('online_tuning',{})),
|
|
169
|
+
operation_request_id=result.get('operation_request_id'),
|
|
170
|
+
human_confirmation=sanitize(reference),note='Accepted by card system; not proof of completion. Inspect laser_card_job.')
|
|
171
|
+
return box.once(name,args,start)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Read grants for shared code and assigned configuration; never grants edits."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
SHARED_CODE_INSTRUCTIONS = ('Read the actual installed shared source before implementation: '
|
|
6
|
+
'Use the configured immutable source roots and their installed version metadata. '
|
|
7
|
+
'Distinguish the current card release from older pinned owner code. State the exact path/version '
|
|
8
|
+
'and functions inspected. Subagent reports do not replace direct source verification. '
|
|
9
|
+
'These are Read grants only; never edit a shared immutable release, bypass a denied tool, '
|
|
10
|
+
'or treat source access as permission to change hardware or safety limits.')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def read_rules(parameters):
|
|
14
|
+
from ldcon.core.site_bindings import shared_roots
|
|
15
|
+
rules = ['Read(/' + str(root) + '/**)' for root in shared_roots()]
|
|
16
|
+
for name in parameters.get('config_read_paths', []):
|
|
17
|
+
path = Path(name)
|
|
18
|
+
# Claude permission syntax treats these characters as patterns/separators.
|
|
19
|
+
if not path.is_absolute() or '..' in path.parts or any(c in str(path) for c in '*?[](),\n\r'):
|
|
20
|
+
raise ValueError('Expected a literal absolute configuration path')
|
|
21
|
+
rules.append('Read(/' + str(path) + ')')
|
|
22
|
+
return rules
|