generated-scene: add scheduled monitoring runtime and helper lifecycle hardening
This commit is contained in:
@@ -1,10 +1,565 @@
|
||||
use std::any::Any;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::panic::{self, AssertUnwindSafe};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
|
||||
use sgclaw::config::SgClawSettings;
|
||||
use sgclaw::generated_scene::scheduled_monitoring_runtime::{
|
||||
run_scheduled_monitoring_command_adapter, run_scheduled_monitoring_skill_command_adapter,
|
||||
run_scheduled_monitoring_skill_command_adapter_many,
|
||||
ScheduledMonitoringCommandAdapterRequest, ScheduledMonitoringSkillCommandAdapterRequest,
|
||||
ScheduledMonitoringSkillCommandAdapterManyRequest,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
if let Err(err) = sgclaw::service::run() {
|
||||
eprintln!("sg_claw failed: {err}");
|
||||
return ExitCode::FAILURE;
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let args_for_log = args.clone();
|
||||
let working_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let exit_code =
|
||||
execute_with_top_level_logging(&working_dir, &args_for_log, || run_main(args));
|
||||
ExitCode::from(exit_code as u8)
|
||||
}
|
||||
|
||||
fn run_main(args: Vec<String>) -> i32 {
|
||||
match parse_scheduled_monitoring_cli(args) {
|
||||
Ok(Some(config)) => {
|
||||
let result = if let Some(skills_dir) = config.skills_dir.as_ref() {
|
||||
if config.trigger_paths.len() == 1 {
|
||||
run_scheduled_monitoring_skill_command_adapter(
|
||||
ScheduledMonitoringSkillCommandAdapterRequest {
|
||||
trigger_path: &config.trigger_paths[0],
|
||||
skills_dir,
|
||||
config_path: config.config_path.as_deref(),
|
||||
output_path: &config.output_path,
|
||||
watch: config.watch,
|
||||
max_runs: config.max_runs,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
run_scheduled_monitoring_skill_command_adapter_many(
|
||||
ScheduledMonitoringSkillCommandAdapterManyRequest {
|
||||
trigger_paths: &config.trigger_paths,
|
||||
skills_dir,
|
||||
config_path: config.config_path.as_deref(),
|
||||
output_path: &config.output_path,
|
||||
watch: config.watch,
|
||||
max_runs: config.max_runs,
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if config.trigger_paths.len() != 1 {
|
||||
eprintln!(
|
||||
"scheduled monitoring trigger failed: multiple --scheduled-monitoring-trigger values require --skills-dir"
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
run_scheduled_monitoring_command_adapter(ScheduledMonitoringCommandAdapterRequest {
|
||||
trigger_path: &config.trigger_paths[0],
|
||||
contract_path: &config.contract_path,
|
||||
preview_fixtures_path: &config.fixtures_path,
|
||||
output_path: &config.output_path,
|
||||
})
|
||||
};
|
||||
match result {
|
||||
Ok(_) => return 0,
|
||||
Err(err) => {
|
||||
eprintln!("scheduled monitoring trigger failed: {err}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(err) => {
|
||||
eprintln!("sg_claw argument error: {err}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
ExitCode::SUCCESS
|
||||
if let Err(err) = sgclaw::service::run() {
|
||||
eprintln!("sg_claw failed: {err}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
fn execute_with_top_level_logging<F>(working_dir: &Path, args: &[String], runner: F) -> i32
|
||||
where
|
||||
F: FnOnce() -> i32,
|
||||
{
|
||||
let started_at = chrono::Local::now().to_rfc3339();
|
||||
let outcome = panic::catch_unwind(AssertUnwindSafe(runner));
|
||||
|
||||
match outcome {
|
||||
Ok(exit_code) => {
|
||||
append_watch_exit_log(
|
||||
working_dir,
|
||||
&started_at,
|
||||
args,
|
||||
"process_exit",
|
||||
if exit_code == 0 { "success" } else { "failure" },
|
||||
exit_code,
|
||||
None,
|
||||
);
|
||||
exit_code
|
||||
}
|
||||
Err(payload) => {
|
||||
let panic_message = extract_panic_message(payload.as_ref());
|
||||
append_watch_exit_log(
|
||||
working_dir,
|
||||
&started_at,
|
||||
args,
|
||||
"process_panic",
|
||||
"panic",
|
||||
1,
|
||||
Some(&panic_message),
|
||||
);
|
||||
eprintln!("sg_claw panicked: {panic_message}");
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_panic_message(payload: &(dyn Any + Send)) -> String {
|
||||
if let Some(message) = payload.downcast_ref::<String>() {
|
||||
return message.clone();
|
||||
}
|
||||
if let Some(message) = payload.downcast_ref::<&str>() {
|
||||
return (*message).to_string();
|
||||
}
|
||||
"unknown panic payload".to_string()
|
||||
}
|
||||
|
||||
fn append_watch_exit_log(
|
||||
working_dir: &Path,
|
||||
started_at: &str,
|
||||
args: &[String],
|
||||
event: &str,
|
||||
outcome: &str,
|
||||
exit_code: i32,
|
||||
message: Option<&str>,
|
||||
) {
|
||||
let log_path = working_dir.join("results").join("sgclaw-watch-fatal.log");
|
||||
if let Some(parent) = log_path.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
let payload = json!({
|
||||
"ts": chrono::Local::now().to_rfc3339(),
|
||||
"event": event,
|
||||
"outcome": outcome,
|
||||
"exitCode": exit_code,
|
||||
"startedAt": started_at,
|
||||
"cwd": working_dir.display().to_string(),
|
||||
"watch": args.iter().any(|arg| arg == "--watch"),
|
||||
"args": args,
|
||||
"message": message,
|
||||
});
|
||||
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&log_path) {
|
||||
let _ = writeln!(file, "{payload}");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct ScheduledMonitoringCliConfig {
|
||||
trigger_paths: Vec<PathBuf>,
|
||||
output_path: PathBuf,
|
||||
skills_dir: Option<PathBuf>,
|
||||
config_path: Option<PathBuf>,
|
||||
contract_path: PathBuf,
|
||||
fixtures_path: PathBuf,
|
||||
watch: bool,
|
||||
max_runs: Option<usize>,
|
||||
}
|
||||
|
||||
fn parse_scheduled_monitoring_cli(
|
||||
args: Vec<String>,
|
||||
) -> Result<Option<ScheduledMonitoringCliConfig>, String> {
|
||||
let mut trigger_paths = Vec::new();
|
||||
let mut output_path = None;
|
||||
let mut skills_dir = None;
|
||||
let mut config_path = None;
|
||||
let mut contract_path = None;
|
||||
let mut fixtures_path = None;
|
||||
let mut watch = false;
|
||||
let mut max_runs = None;
|
||||
|
||||
let mut iter = args.into_iter();
|
||||
while let Some(arg) = iter.next() {
|
||||
match arg.as_str() {
|
||||
"--scheduled-monitoring-trigger" => {
|
||||
trigger_paths.push(PathBuf::from(next_arg(
|
||||
&mut iter,
|
||||
"--scheduled-monitoring-trigger",
|
||||
)?));
|
||||
}
|
||||
"--output" => {
|
||||
output_path = Some(PathBuf::from(next_arg(&mut iter, "--output")?));
|
||||
}
|
||||
"--skills-dir" => {
|
||||
skills_dir = Some(PathBuf::from(next_arg(&mut iter, "--skills-dir")?));
|
||||
}
|
||||
"--config-path" => {
|
||||
config_path = Some(resolve_process_path(PathBuf::from(next_arg(
|
||||
&mut iter,
|
||||
"--config-path",
|
||||
)?)));
|
||||
}
|
||||
"--scheduled-monitoring-contract" => {
|
||||
contract_path = Some(PathBuf::from(next_arg(
|
||||
&mut iter,
|
||||
"--scheduled-monitoring-contract",
|
||||
)?));
|
||||
}
|
||||
"--scheduled-monitoring-fixtures" => {
|
||||
fixtures_path = Some(PathBuf::from(next_arg(
|
||||
&mut iter,
|
||||
"--scheduled-monitoring-fixtures",
|
||||
)?));
|
||||
}
|
||||
"--watch" => {
|
||||
watch = true;
|
||||
}
|
||||
"--max-runs" => {
|
||||
max_runs = Some(parse_usize_arg(&mut iter, "--max-runs")?);
|
||||
}
|
||||
_ => {
|
||||
if let Some(value) = arg.strip_prefix("--scheduled-monitoring-trigger=") {
|
||||
trigger_paths.push(PathBuf::from(value));
|
||||
} else if let Some(value) = arg.strip_prefix("--output=") {
|
||||
output_path = Some(PathBuf::from(value));
|
||||
} else if let Some(value) = arg.strip_prefix("--skills-dir=") {
|
||||
skills_dir = Some(PathBuf::from(value));
|
||||
} else if let Some(value) = arg.strip_prefix("--config-path=") {
|
||||
config_path = Some(resolve_process_path(PathBuf::from(value)));
|
||||
} else if let Some(value) = arg.strip_prefix("--scheduled-monitoring-contract=") {
|
||||
contract_path = Some(PathBuf::from(value));
|
||||
} else if let Some(value) = arg.strip_prefix("--scheduled-monitoring-fixtures=") {
|
||||
fixtures_path = Some(PathBuf::from(value));
|
||||
} else if let Some(value) = arg.strip_prefix("--max-runs=") {
|
||||
max_runs = Some(
|
||||
value
|
||||
.parse::<usize>()
|
||||
.map_err(|_| format!("invalid value for --max-runs: {value}"))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if trigger_paths.is_empty() {
|
||||
if watch {
|
||||
if let Some(config_path) = config_path.as_deref() {
|
||||
if let Some(settings) =
|
||||
SgClawSettings::load(Some(config_path)).map_err(|err| err.to_string())?
|
||||
{
|
||||
trigger_paths = settings
|
||||
.scheduled_monitoring_watch_tasks
|
||||
.into_iter()
|
||||
.map(PathBuf::from)
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
if trigger_paths.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
let output_path = output_path.ok_or_else(|| {
|
||||
"missing required --output for --scheduled-monitoring-trigger".to_string()
|
||||
})?;
|
||||
|
||||
Ok(Some(ScheduledMonitoringCliConfig {
|
||||
trigger_paths,
|
||||
output_path,
|
||||
skills_dir,
|
||||
config_path,
|
||||
contract_path: contract_path.unwrap_or_else(|| {
|
||||
PathBuf::from(
|
||||
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
|
||||
)
|
||||
}),
|
||||
fixtures_path: fixtures_path.unwrap_or_else(|| {
|
||||
PathBuf::from(
|
||||
"tests/fixtures/generated_scene/monitoring_action_mock_validation_fixtures_2026-04-22.json",
|
||||
)
|
||||
}),
|
||||
watch,
|
||||
max_runs,
|
||||
}))
|
||||
}
|
||||
|
||||
fn next_arg(iter: &mut impl Iterator<Item = String>, flag: &str) -> Result<String, String> {
|
||||
iter.next()
|
||||
.ok_or_else(|| format!("missing value for {flag}"))
|
||||
}
|
||||
|
||||
fn parse_usize_arg(iter: &mut impl Iterator<Item = String>, flag: &str) -> Result<usize, String> {
|
||||
let value = next_arg(iter, flag)?;
|
||||
value
|
||||
.parse::<usize>()
|
||||
.map_err(|_| format!("invalid value for {flag}: {value}"))
|
||||
}
|
||||
|
||||
fn resolve_process_path(path: PathBuf) -> PathBuf {
|
||||
if path.is_absolute() {
|
||||
path
|
||||
} else {
|
||||
std::env::current_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("."))
|
||||
.join(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{execute_with_top_level_logging, parse_scheduled_monitoring_cli};
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn temp_json_file(prefix: &str, body: &str) -> PathBuf {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!("{prefix}-{nanos}.json"));
|
||||
fs::write(&path, body).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
fn temp_workspace(prefix: &str) -> PathBuf {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!("{prefix}-{nanos}"));
|
||||
fs::create_dir_all(path.join("results")).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
fn read_exit_log_line(workspace: &Path) -> Value {
|
||||
let raw = fs::read_to_string(workspace.join("results").join("sgclaw-watch-fatal.log"))
|
||||
.unwrap();
|
||||
serde_json::from_str(raw.lines().last().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduled_monitoring_cli_is_absent_by_default() {
|
||||
assert!(parse_scheduled_monitoring_cli(vec![
|
||||
"--config-path".to_string(),
|
||||
"sgclaw_config.json".to_string(),
|
||||
])
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduled_monitoring_cli_requires_output() {
|
||||
let error = parse_scheduled_monitoring_cli(vec![
|
||||
"--scheduled-monitoring-trigger".to_string(),
|
||||
"trigger.json".to_string(),
|
||||
])
|
||||
.unwrap_err();
|
||||
|
||||
assert!(error.contains("missing required --output"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduled_monitoring_cli_parses_trigger_output_and_defaults() {
|
||||
let config = parse_scheduled_monitoring_cli(vec![
|
||||
"--scheduled-monitoring-trigger".to_string(),
|
||||
"trigger.json".to_string(),
|
||||
"--output".to_string(),
|
||||
"run-record.json".to_string(),
|
||||
])
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.trigger_paths, vec![PathBuf::from("trigger.json")]);
|
||||
assert_eq!(config.output_path, PathBuf::from("run-record.json"));
|
||||
assert_eq!(config.skills_dir, None);
|
||||
assert!(config
|
||||
.contract_path
|
||||
.ends_with("scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json"));
|
||||
assert!(config
|
||||
.fixtures_path
|
||||
.ends_with("monitoring_action_mock_validation_fixtures_2026-04-22.json"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduled_monitoring_cli_parses_optional_skills_dir() {
|
||||
let config = parse_scheduled_monitoring_cli(vec![
|
||||
"--scheduled-monitoring-trigger".to_string(),
|
||||
"trigger.json".to_string(),
|
||||
"--output".to_string(),
|
||||
"run-record.json".to_string(),
|
||||
"--skills-dir".to_string(),
|
||||
"skills".to_string(),
|
||||
"--config-path".to_string(),
|
||||
"sgclaw_config.json".to_string(),
|
||||
])
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.skills_dir, Some(PathBuf::from("skills")));
|
||||
assert!(
|
||||
config
|
||||
.config_path
|
||||
.as_ref()
|
||||
.is_some_and(|path| path.ends_with("sgclaw_config.json"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduled_monitoring_cli_parses_watch_and_max_runs() {
|
||||
let config = parse_scheduled_monitoring_cli(vec![
|
||||
"--scheduled-monitoring-trigger".to_string(),
|
||||
"trigger.json".to_string(),
|
||||
"--output".to_string(),
|
||||
"run-record.json".to_string(),
|
||||
"--watch".to_string(),
|
||||
"--max-runs".to_string(),
|
||||
"2".to_string(),
|
||||
])
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(config.watch);
|
||||
assert_eq!(config.max_runs, Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduled_monitoring_cli_parses_multiple_triggers() {
|
||||
let config = parse_scheduled_monitoring_cli(vec![
|
||||
"--scheduled-monitoring-trigger".to_string(),
|
||||
"fee.json".to_string(),
|
||||
"--scheduled-monitoring-trigger".to_string(),
|
||||
"archive.json".to_string(),
|
||||
"--output".to_string(),
|
||||
"watch-run-record.json".to_string(),
|
||||
"--skills-dir".to_string(),
|
||||
"skills".to_string(),
|
||||
"--watch".to_string(),
|
||||
])
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config.trigger_paths,
|
||||
vec![PathBuf::from("fee.json"), PathBuf::from("archive.json")]
|
||||
);
|
||||
assert_eq!(config.output_path, PathBuf::from("watch-run-record.json"));
|
||||
assert_eq!(config.skills_dir, Some(PathBuf::from("skills")));
|
||||
assert!(config.watch);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduled_monitoring_cli_loads_watch_tasks_from_config_when_watch_has_no_trigger() {
|
||||
let config_path = temp_json_file(
|
||||
"sgclaw-watch-config",
|
||||
r#"{
|
||||
"apiKey": "sk-test",
|
||||
"baseUrl": "https://api.deepseek.com",
|
||||
"model": "deepseek-chat",
|
||||
"scheduledMonitoring": {
|
||||
"watchTasks": [
|
||||
"handoff/trigger.read_only.template.json",
|
||||
"handoff/trigger.archive_workorder_grid_push.template.json"
|
||||
]
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
|
||||
let config = parse_scheduled_monitoring_cli(vec![
|
||||
"--config-path".to_string(),
|
||||
config_path.to_string_lossy().into_owned(),
|
||||
"--skills-dir".to_string(),
|
||||
"skills".to_string(),
|
||||
"--output".to_string(),
|
||||
"watch-run-record.json".to_string(),
|
||||
"--watch".to_string(),
|
||||
])
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config.trigger_paths,
|
||||
vec![
|
||||
PathBuf::from("handoff/trigger.read_only.template.json"),
|
||||
PathBuf::from("handoff/trigger.archive_workorder_grid_push.template.json")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduled_monitoring_cli_explicit_trigger_overrides_config_watch_tasks() {
|
||||
let config_path = temp_json_file(
|
||||
"sgclaw-watch-config-override",
|
||||
r#"{
|
||||
"apiKey": "sk-test",
|
||||
"baseUrl": "https://api.deepseek.com",
|
||||
"model": "deepseek-chat",
|
||||
"scheduledMonitoring": {
|
||||
"watchTasks": [
|
||||
"handoff/trigger.read_only.template.json",
|
||||
"handoff/trigger.archive_workorder_grid_push.template.json"
|
||||
]
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
|
||||
let config = parse_scheduled_monitoring_cli(vec![
|
||||
"--config-path".to_string(),
|
||||
config_path.to_string_lossy().into_owned(),
|
||||
"--scheduled-monitoring-trigger".to_string(),
|
||||
"only-this.json".to_string(),
|
||||
"--output".to_string(),
|
||||
"watch-run-record.json".to_string(),
|
||||
"--watch".to_string(),
|
||||
])
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.trigger_paths, vec![PathBuf::from("only-this.json")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_logging_records_non_zero_exit() {
|
||||
let workspace = temp_workspace("sgclaw-exit-log");
|
||||
let args = vec!["--watch".to_string(), "--output".to_string(), "watch-run-record.json".to_string()];
|
||||
|
||||
let exit_code = execute_with_top_level_logging(&workspace, &args, || 1);
|
||||
|
||||
assert_eq!(exit_code, 1);
|
||||
let payload = read_exit_log_line(&workspace);
|
||||
assert_eq!(payload["event"], "process_exit");
|
||||
assert_eq!(payload["outcome"], "failure");
|
||||
assert_eq!(payload["exitCode"], 1);
|
||||
assert_eq!(payload["watch"], true);
|
||||
assert_eq!(payload["args"][0], "--watch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_logging_records_panic_as_failure() {
|
||||
let workspace = temp_workspace("sgclaw-panic-log");
|
||||
let args = vec!["--watch".to_string()];
|
||||
|
||||
let exit_code = execute_with_top_level_logging(&workspace, &args, || {
|
||||
panic!("boom");
|
||||
});
|
||||
|
||||
assert_eq!(exit_code, 1);
|
||||
let payload = read_exit_log_line(&workspace);
|
||||
assert_eq!(payload["event"], "process_panic");
|
||||
assert_eq!(payload["outcome"], "panic");
|
||||
assert_eq!(payload["exitCode"], 1);
|
||||
assert_eq!(payload["message"], "boom");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,21 +442,69 @@ fn build_get_text_js(source_url: &str, selector: &str) -> String {
|
||||
|
||||
fn build_eval_js(source_url: &str, script: &str) -> String {
|
||||
let escaped_source_url = escape_js_single_quoted(source_url);
|
||||
let escaped_script = escape_js_single_quoted(script);
|
||||
let callback = EVAL_CALLBACK_NAME;
|
||||
let events_url = escape_js_single_quoted(&events_endpoint_url(source_url));
|
||||
let script_len = script.len();
|
||||
let eval_sentinel = format!("sgclaw-eval-wrapper-{script_len}");
|
||||
eprintln!(
|
||||
"[callback_backend] build_eval_js source_url={} script_len={} sentinel={}",
|
||||
source_url,
|
||||
script_len,
|
||||
eval_sentinel
|
||||
);
|
||||
|
||||
format!(
|
||||
"(function(){{try{{\
|
||||
var v=(function(){{return {script}}})();\
|
||||
function _s(v){{\
|
||||
var t=(typeof v==='string')?v:JSON.stringify(v);\
|
||||
var __sgclawEvalSentinel='{eval_sentinel}';\
|
||||
function _j(v){{\
|
||||
if(typeof v==='string')return v;\
|
||||
try{{return JSON.stringify(v);}}catch(_){{return String(v);}}\
|
||||
}}\
|
||||
function _e(err){{\
|
||||
if(!err)return{{message:'unknown error'}};\
|
||||
if(typeof err==='string')return{{message:err}};\
|
||||
return {{\
|
||||
name: err.name||'Error',\
|
||||
message: err.message||String(err),\
|
||||
stack: err.stack||'',\
|
||||
code: err.code||'',\
|
||||
stage: err.stage||'',\
|
||||
url: err.url||'',\
|
||||
timeoutMs: err.timeoutMs||0,\
|
||||
trace: err.trace||null\
|
||||
}};\
|
||||
}}\
|
||||
function _emit(payload){{\
|
||||
var t=_j(payload);\
|
||||
try{{callBackJsToCpp('{escaped_source_url}@_@'+window.location.href+'@_@{callback}@_@sgBrowserExcuteJsCodeByDomain@_@'+(t??''))}}catch(_){{}}\
|
||||
var j=JSON.stringify({{type:'callback',callback:'{callback}',request_url:'{escaped_source_url}',payload:{{value:(t??'')}}}});\
|
||||
var j=JSON.stringify({{type:'callback',callback:'{callback}',request_url:'{escaped_source_url}',payload:payload}});\
|
||||
try{{var r=new XMLHttpRequest();r.open('POST','{events_url}',true);r.setRequestHeader('Content-Type','application/json');r.send(j)}}catch(_){{}}\
|
||||
try{{navigator.sendBeacon('{events_url}',new Blob([j],{{type:'application/json'}}))}}catch(_){{}}\
|
||||
}}\
|
||||
if(v&&typeof v.then==='function'){{v.then(_s).catch(function(){{}});}}else{{_s(v);}}\
|
||||
}}catch(e){{}}}})()"
|
||||
var _compiled;\
|
||||
try{{\
|
||||
_compiled = new Function('return ({escaped_script});');\
|
||||
}}catch(compileErr){{\
|
||||
_emit({{value:null,error:_e(compileErr),phase:'eval_compile_failed'}});\
|
||||
return;\
|
||||
}}\
|
||||
var v;\
|
||||
try{{\
|
||||
v = _compiled();\
|
||||
}}catch(runErr){{\
|
||||
_emit({{value:null,error:_e(runErr),phase:'eval_runtime_failed'}});\
|
||||
return;\
|
||||
}}\
|
||||
function _s(v){{_emit({{value:v??null,error:null}});}}\
|
||||
function _f(err){{_emit({{value:null,error:_e(err)}});}}\
|
||||
if(v&&typeof v.then==='function'){{v.then(_s).catch(_f);}}else{{_s(v);}}\
|
||||
}}catch(e){{\
|
||||
var payload={{value:null,error:{{name:e&&e.name||'Error',message:e&&e.message||String(e),stack:e&&e.stack||''}}}};\
|
||||
try{{callBackJsToCpp('{escaped_source_url}@_@'+window.location.href+'@_@{callback}@_@sgBrowserExcuteJsCodeByDomain@_@'+JSON.stringify(payload))}}catch(_){{}}\
|
||||
try{{var j=JSON.stringify({{type:'callback',callback:'{callback}',request_url:'{escaped_source_url}',payload:payload}});var r=new XMLHttpRequest();r.open('POST','{events_url}',true);r.setRequestHeader('Content-Type','application/json');r.send(j)}}catch(_){{}}\
|
||||
try{{navigator.sendBeacon('{events_url}',new Blob([JSON.stringify({{type:'callback',callback:'{callback}',request_url:'{escaped_source_url}',payload:payload}})],{{type:'application/json'}}))}}catch(_){{}}\
|
||||
}}}})()"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
297
src/browser/callback_host_lifecycle_log.rs
Normal file
297
src/browser/callback_host_lifecycle_log.rs
Normal file
@@ -0,0 +1,297 @@
|
||||
use chrono::Local;
|
||||
use serde::Serialize;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CallbackHostLifecycleLogger {
|
||||
path: PathBuf,
|
||||
writer: Mutex<Option<BufWriter<File>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SharedLifecycleFields {
|
||||
pub ts: String,
|
||||
pub event: String,
|
||||
pub host_instance_id: String,
|
||||
pub run_id: String,
|
||||
pub skill_id: String,
|
||||
pub helper_url: String,
|
||||
pub listener_port: u16,
|
||||
pub thread: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct HostCreateEvent {
|
||||
#[serde(flatten)]
|
||||
pub shared: SharedLifecycleFields,
|
||||
pub browser_ws_url: String,
|
||||
pub use_hidden_domain: bool,
|
||||
pub request_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ReadyStateTransitionEvent {
|
||||
#[serde(flatten)]
|
||||
pub shared: SharedLifecycleFields,
|
||||
pub helper_loaded: bool,
|
||||
pub ready: bool,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CommandIssueEvent {
|
||||
#[serde(flatten)]
|
||||
pub shared: SharedLifecycleFields,
|
||||
pub seq: u64,
|
||||
pub action: String,
|
||||
pub script_len: usize,
|
||||
pub expected_domain: String,
|
||||
pub request_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PollErrorEvent {
|
||||
#[serde(flatten)]
|
||||
pub shared: SharedLifecycleFields,
|
||||
pub seq: Option<u64>,
|
||||
pub message: String,
|
||||
pub elapsed_ms: Option<u128>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct HostDropEvent {
|
||||
#[serde(flatten)]
|
||||
pub shared: SharedLifecycleFields,
|
||||
pub uptime_ms: u128,
|
||||
pub matched_callback_count: u64,
|
||||
pub unmatched_callback_count: u64,
|
||||
pub poll_error_count: u64,
|
||||
pub helper_loaded: bool,
|
||||
pub ready: bool,
|
||||
pub drop_join_ok: bool,
|
||||
}
|
||||
|
||||
impl CallbackHostLifecycleLogger {
|
||||
pub(crate) fn new(path: PathBuf) -> Self {
|
||||
Self {
|
||||
path,
|
||||
writer: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn now_local_rfc3339() -> String {
|
||||
Local::now().to_rfc3339()
|
||||
}
|
||||
|
||||
pub(crate) fn write_host_create(&self, event: HostCreateEvent) -> std::io::Result<()> {
|
||||
self.write_line(&event)
|
||||
}
|
||||
|
||||
pub(crate) fn write_ready_state_transition(
|
||||
&self,
|
||||
event: ReadyStateTransitionEvent,
|
||||
) -> std::io::Result<()> {
|
||||
self.write_line(&event)
|
||||
}
|
||||
|
||||
pub(crate) fn write_command_issue(&self, event: CommandIssueEvent) -> std::io::Result<()> {
|
||||
self.write_line(&event)
|
||||
}
|
||||
|
||||
pub(crate) fn write_poll_error(&self, event: PollErrorEvent) -> std::io::Result<()> {
|
||||
self.write_line(&event)
|
||||
}
|
||||
|
||||
pub(crate) fn write_host_drop(&self, event: HostDropEvent) -> std::io::Result<()> {
|
||||
self.write_line(&event)
|
||||
}
|
||||
|
||||
fn ensure_writer(&self) -> std::io::Result<()> {
|
||||
let mut guard = self.writer.lock().unwrap();
|
||||
if guard.is_none() {
|
||||
if let Some(parent) = self.path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&self.path)?;
|
||||
*guard = Some(BufWriter::new(file));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_line<T: Serialize>(&self, value: &T) -> std::io::Result<()> {
|
||||
self.ensure_writer()?;
|
||||
let mut guard = self.writer.lock().unwrap();
|
||||
let writer = guard.as_mut().expect("writer initialized");
|
||||
serde_json::to_writer(&mut *writer, value)?;
|
||||
writer.write_all(b"\n")?;
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
CallbackHostLifecycleLogger, CommandIssueEvent, HostCreateEvent, HostDropEvent,
|
||||
PollErrorEvent, ReadyStateTransitionEvent,
|
||||
SharedLifecycleFields,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn lifecycle_log_writes_host_create_event_with_shared_fields() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let log_path = dir.path().join("callback-host-lifecycle.ndjson");
|
||||
|
||||
let logger = CallbackHostLifecycleLogger::new(log_path.clone());
|
||||
logger
|
||||
.write_host_create(HostCreateEvent {
|
||||
shared: SharedLifecycleFields {
|
||||
ts: "2026-04-29T21:15:32.123+08:00".to_string(),
|
||||
event: "host_create".to_string(),
|
||||
host_instance_id: "cbh-000123".to_string(),
|
||||
run_id: "run-001".to_string(),
|
||||
skill_id: "command-center-fee-control-monitor".to_string(),
|
||||
helper_url: "http://127.0.0.1:60882/sgclaw/browser-helper.html".to_string(),
|
||||
listener_port: 60882,
|
||||
thread: "callback-host".to_string(),
|
||||
},
|
||||
browser_ws_url: "ws://127.0.0.1:12345".to_string(),
|
||||
use_hidden_domain: true,
|
||||
request_url: "http://yx.gs.sgcc.com.cn/".to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let text = std::fs::read_to_string(log_path).unwrap();
|
||||
assert!(text.contains("\"event\":\"host_create\""));
|
||||
assert!(text.contains("\"hostInstanceId\":\"cbh-000123\""));
|
||||
assert!(text.contains("\"listenerPort\":60882"));
|
||||
assert!(text.contains("\"browserWsUrl\":\"ws://127.0.0.1:12345\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lifecycle_log_writes_ready_state_transition_event() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let log_path = dir.path().join("callback-host-lifecycle.ndjson");
|
||||
|
||||
let logger = CallbackHostLifecycleLogger::new(log_path.clone());
|
||||
logger
|
||||
.write_ready_state_transition(ReadyStateTransitionEvent {
|
||||
shared: SharedLifecycleFields {
|
||||
ts: "2026-04-29T21:15:32.441+08:00".to_string(),
|
||||
event: "ready_state_transition".to_string(),
|
||||
host_instance_id: "cbh-000123".to_string(),
|
||||
run_id: "run-001".to_string(),
|
||||
skill_id: "command-center-fee-control-monitor".to_string(),
|
||||
helper_url: "http://127.0.0.1:60882/sgclaw/browser-helper.html".to_string(),
|
||||
listener_port: 60882,
|
||||
thread: "callback-host".to_string(),
|
||||
},
|
||||
helper_loaded: true,
|
||||
ready: true,
|
||||
source: "ready_endpoint_hit".to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let text = std::fs::read_to_string(log_path).unwrap();
|
||||
assert!(text.contains("\"event\":\"ready_state_transition\""));
|
||||
assert!(text.contains("\"helperLoaded\":true"));
|
||||
assert!(text.contains("\"ready\":true"));
|
||||
assert!(text.contains("\"source\":\"ready_endpoint_hit\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lifecycle_log_writes_command_issue_and_poll_error_events() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let log_path = dir.path().join("callback-host-lifecycle.ndjson");
|
||||
|
||||
let logger = CallbackHostLifecycleLogger::new(log_path.clone());
|
||||
logger
|
||||
.write_command_issue(CommandIssueEvent {
|
||||
shared: SharedLifecycleFields {
|
||||
ts: "2026-04-29T21:15:33.018+08:00".to_string(),
|
||||
event: "command_issue".to_string(),
|
||||
host_instance_id: "cbh-000123".to_string(),
|
||||
run_id: "run-001".to_string(),
|
||||
skill_id: "command-center-fee-control-monitor".to_string(),
|
||||
helper_url: "http://127.0.0.1:60882/sgclaw/browser-helper.html".to_string(),
|
||||
listener_port: 60882,
|
||||
thread: "callback-host".to_string(),
|
||||
},
|
||||
seq: 2,
|
||||
action: "eval".to_string(),
|
||||
script_len: 29470,
|
||||
expected_domain: "yx.gs.sgcc.com.cn".to_string(),
|
||||
request_url: "http://127.0.0.1:60882/sgclaw/browser-helper.html".to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
logger
|
||||
.write_poll_error(PollErrorEvent {
|
||||
shared: SharedLifecycleFields {
|
||||
ts: "2026-04-29T21:15:36.566+08:00".to_string(),
|
||||
event: "poll_error".to_string(),
|
||||
host_instance_id: "cbh-000123".to_string(),
|
||||
run_id: "run-001".to_string(),
|
||||
skill_id: "command-center-fee-control-monitor".to_string(),
|
||||
helper_url: "http://127.0.0.1:60882/sgclaw/browser-helper.html".to_string(),
|
||||
listener_port: 60882,
|
||||
thread: "callback-host".to_string(),
|
||||
},
|
||||
seq: Some(2),
|
||||
message: "Failed to fetch".to_string(),
|
||||
elapsed_ms: Some(3548),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let text = std::fs::read_to_string(log_path).unwrap();
|
||||
assert!(text.contains("\"event\":\"command_issue\""));
|
||||
assert!(text.contains("\"scriptLen\":29470"));
|
||||
assert!(text.contains("\"event\":\"poll_error\""));
|
||||
assert!(text.contains("\"message\":\"Failed to fetch\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lifecycle_log_writes_host_drop_event() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let log_path = dir.path().join("callback-host-lifecycle.ndjson");
|
||||
|
||||
let logger = CallbackHostLifecycleLogger::new(log_path.clone());
|
||||
logger
|
||||
.write_host_drop(HostDropEvent {
|
||||
shared: SharedLifecycleFields {
|
||||
ts: "2026-04-29T21:15:58.020+08:00".to_string(),
|
||||
event: "host_drop".to_string(),
|
||||
host_instance_id: "cbh-000123".to_string(),
|
||||
run_id: "run-001".to_string(),
|
||||
skill_id: "command-center-fee-control-monitor".to_string(),
|
||||
helper_url: "http://127.0.0.1:60882/sgclaw/browser-helper.html".to_string(),
|
||||
listener_port: 60882,
|
||||
thread: "callback-host".to_string(),
|
||||
},
|
||||
uptime_ms: 23340,
|
||||
matched_callback_count: 2,
|
||||
unmatched_callback_count: 14,
|
||||
poll_error_count: 11,
|
||||
helper_loaded: true,
|
||||
ready: true,
|
||||
drop_join_ok: true,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let text = std::fs::read_to_string(log_path).unwrap();
|
||||
assert!(text.contains("\"event\":\"host_drop\""));
|
||||
assert!(text.contains("\"uptimeMs\":23340"));
|
||||
assert!(text.contains("\"dropJoinOk\":true"));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod bridge_contract;
|
||||
pub mod bridge_transport;
|
||||
pub mod callback_backend;
|
||||
pub(crate) mod callback_host;
|
||||
pub(crate) mod callback_host_lifecycle_log;
|
||||
mod pipe_backend;
|
||||
pub mod ws_backend;
|
||||
pub mod ws_probe;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::fs;
|
||||
use std::net::TcpStream;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -83,24 +84,23 @@ pub fn parse_probe_args(args: &[String]) -> Result<ProbeCliConfig, ProbeError> {
|
||||
let value = args
|
||||
.get(index)
|
||||
.ok_or_else(|| ProbeError::Args("missing value for --step".to_string()))?;
|
||||
let (label, payload) = value.split_once("::").ok_or_else(|| {
|
||||
ProbeError::Args(format!(
|
||||
"invalid --step value (expected <label>::<payload>): {value}"
|
||||
))
|
||||
steps.push(parse_step_value(value, "--step")?);
|
||||
}
|
||||
"--step-file" => {
|
||||
index += 1;
|
||||
let value = args.get(index).ok_or_else(|| {
|
||||
ProbeError::Args("missing value for --step-file".to_string())
|
||||
})?;
|
||||
if label.is_empty() {
|
||||
return Err(ProbeError::Args("step label must not be empty".to_string()));
|
||||
let contents = fs::read_to_string(value).map_err(|err| {
|
||||
ProbeError::Args(format!("failed to read --step-file {value}: {err}"))
|
||||
})?;
|
||||
let normalized = contents.trim();
|
||||
if normalized.is_empty() {
|
||||
return Err(ProbeError::Args(format!(
|
||||
"--step-file must not be empty: {value}"
|
||||
)));
|
||||
}
|
||||
if payload.is_empty() {
|
||||
return Err(ProbeError::Args(
|
||||
"step payload must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
steps.push(ProbeStep {
|
||||
label: label.to_string(),
|
||||
payload: payload.to_string(),
|
||||
expect_reply: true,
|
||||
});
|
||||
steps.push(parse_step_value(normalized, "--step-file")?);
|
||||
}
|
||||
flag => {
|
||||
return Err(ProbeError::Args(format!("unknown argument: {flag}")));
|
||||
@@ -127,6 +127,27 @@ pub fn parse_probe_args(args: &[String]) -> Result<ProbeCliConfig, ProbeError> {
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_step_value(value: &str, source: &str) -> Result<ProbeStep, ProbeError> {
|
||||
let (label, payload) = value.split_once("::").ok_or_else(|| {
|
||||
ProbeError::Args(format!(
|
||||
"invalid {source} value (expected <label>::<payload>): {value}"
|
||||
))
|
||||
})?;
|
||||
if label.is_empty() {
|
||||
return Err(ProbeError::Args("step label must not be empty".to_string()));
|
||||
}
|
||||
if payload.is_empty() {
|
||||
return Err(ProbeError::Args(
|
||||
"step payload must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(ProbeStep {
|
||||
label: label.to_string(),
|
||||
payload: payload.to_string(),
|
||||
expect_reply: true,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_ws_url(ws_url: &str) -> Result<(), ProbeError> {
|
||||
if ws_url.starts_with("ws://") {
|
||||
return Ok(());
|
||||
|
||||
@@ -12,6 +12,18 @@ use zeroclaw::tools::{Tool, ToolResult};
|
||||
use crate::browser::BrowserBackend;
|
||||
use crate::pipe::Action;
|
||||
|
||||
fn preview_utf8(text: &str, max_chars: usize) -> &str {
|
||||
if text.chars().count() <= max_chars {
|
||||
return text;
|
||||
}
|
||||
|
||||
let mut end = 0usize;
|
||||
for (idx, ch) in text.char_indices().take(max_chars) {
|
||||
end = idx + ch.len_utf8();
|
||||
}
|
||||
&text[..end]
|
||||
}
|
||||
|
||||
pub struct BrowserScriptSkillTool {
|
||||
tool_name: String,
|
||||
tool_description: String,
|
||||
@@ -269,11 +281,7 @@ fn execute_browser_script_impl(
|
||||
);
|
||||
eprintln!(
|
||||
"[execute_browser_script_impl] 包装后脚本前500字符: {}",
|
||||
if wrapped_script.len() > 500 {
|
||||
&wrapped_script[..500]
|
||||
} else {
|
||||
&wrapped_script
|
||||
}
|
||||
preview_utf8(&wrapped_script, 500)
|
||||
);
|
||||
eprintln!("[execute_browser_script_impl] 调用 browser_tool.invoke(Action::Eval)...");
|
||||
|
||||
|
||||
@@ -165,13 +165,23 @@ fn build_scene_execution_plan(
|
||||
instruction: &str,
|
||||
mut args: Map<String, Value>,
|
||||
) -> SceneExecutionPlan {
|
||||
let bootstrap = entry
|
||||
.manifest
|
||||
.bootstrap
|
||||
.as_ref()
|
||||
.expect("report scene registry should only contain manifests with bootstrap");
|
||||
let artifact = entry
|
||||
.manifest
|
||||
.artifact
|
||||
.as_ref()
|
||||
.expect("report scene registry should only contain manifests with artifact");
|
||||
args.insert(
|
||||
"expected_domain".to_string(),
|
||||
Value::String(entry.manifest.bootstrap.expected_domain.clone()),
|
||||
Value::String(bootstrap.expected_domain.clone()),
|
||||
);
|
||||
args.insert(
|
||||
"target_url".to_string(),
|
||||
Value::String(entry.manifest.bootstrap.target_url.clone()),
|
||||
Value::String(bootstrap.target_url.clone()),
|
||||
);
|
||||
|
||||
SceneExecutionPlan {
|
||||
@@ -181,11 +191,11 @@ fn build_scene_execution_plan(
|
||||
"{}.{}",
|
||||
entry.manifest.scene.skill, entry.manifest.scene.tool
|
||||
),
|
||||
expected_domain: entry.manifest.bootstrap.expected_domain.clone(),
|
||||
target_url: entry.manifest.bootstrap.target_url.clone(),
|
||||
expected_domain: bootstrap.expected_domain.clone(),
|
||||
target_url: bootstrap.target_url.clone(),
|
||||
args,
|
||||
success_statuses: entry.manifest.artifact.success_status.clone(),
|
||||
failure_statuses: entry.manifest.artifact.failure_status.clone(),
|
||||
success_statuses: artifact.success_status.clone(),
|
||||
failure_statuses: artifact.failure_status.clone(),
|
||||
postprocess: entry.manifest.postprocess.clone(),
|
||||
}
|
||||
}
|
||||
@@ -252,7 +262,8 @@ fn score_scene(
|
||||
page_url: Option<&str>,
|
||||
page_title: Option<&str>,
|
||||
) -> Option<usize> {
|
||||
let deterministic = &entry.manifest.deterministic;
|
||||
let deterministic = entry.manifest.deterministic.as_ref()?;
|
||||
let bootstrap = entry.manifest.bootstrap.as_ref()?;
|
||||
if deterministic.suffix != DETERMINISTIC_SUFFIX {
|
||||
return None;
|
||||
}
|
||||
@@ -279,11 +290,7 @@ fn score_scene(
|
||||
let normalized_url = page_url.unwrap_or_default().to_ascii_lowercase();
|
||||
if !normalized_url.is_empty() {
|
||||
if normalized_url.contains(
|
||||
&entry
|
||||
.manifest
|
||||
.bootstrap
|
||||
.expected_domain
|
||||
.to_ascii_lowercase(),
|
||||
&bootstrap.expected_domain.to_ascii_lowercase(),
|
||||
) {
|
||||
score += 100;
|
||||
} else if normalized_url.contains(&entry.manifest.scene.id.to_ascii_lowercase()) {
|
||||
@@ -296,6 +303,7 @@ fn score_scene(
|
||||
&& entry
|
||||
.manifest
|
||||
.bootstrap
|
||||
.as_ref()?
|
||||
.page_title_keywords
|
||||
.iter()
|
||||
.any(|keyword| !keyword.trim().is_empty() && title.contains(keyword.as_str()))
|
||||
@@ -324,8 +332,18 @@ fn log_registry_diag(registry: &[SceneRegistryEntry]) {
|
||||
registry.len(),
|
||||
DIAGNOSTIC_SCENE_ID,
|
||||
entry.skill_root.display(),
|
||||
entry.manifest.deterministic.suffix == DETERMINISTIC_SUFFIX,
|
||||
entry.manifest.deterministic.include_keywords
|
||||
entry
|
||||
.manifest
|
||||
.deterministic
|
||||
.as_ref()
|
||||
.map(|deterministic| deterministic.suffix == DETERMINISTIC_SUFFIX)
|
||||
.unwrap_or(false),
|
||||
entry
|
||||
.manifest
|
||||
.deterministic
|
||||
.as_ref()
|
||||
.map(|deterministic| deterministic.include_keywords.clone())
|
||||
.unwrap_or_default()
|
||||
)),
|
||||
None => log_deterministic_diag(format!(
|
||||
"registry loaded count={} diagnostic_scene={} registered=false",
|
||||
@@ -336,7 +354,13 @@ fn log_registry_diag(registry: &[SceneRegistryEntry]) {
|
||||
}
|
||||
|
||||
fn log_scene_match_diag(entry: &SceneRegistryEntry, instruction: &str) {
|
||||
let deterministic = &entry.manifest.deterministic;
|
||||
let Some(deterministic) = entry.manifest.deterministic.as_ref() else {
|
||||
log_deterministic_diag(format!(
|
||||
"diagnostic_scene={} deterministic=false",
|
||||
entry.manifest.scene.id
|
||||
));
|
||||
return;
|
||||
};
|
||||
let include_hits = deterministic
|
||||
.include_keywords
|
||||
.iter()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod dispatch;
|
||||
pub mod registry;
|
||||
pub mod resolvers;
|
||||
pub mod scheduled_registry;
|
||||
|
||||
293
src/compat/scene_platform/scheduled_registry.rs
Normal file
293
src/compat/scene_platform/scheduled_registry.rs
Normal file
@@ -0,0 +1,293 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use thiserror::Error;
|
||||
use zeroclaw::skills::{load_skills_from_directory, Skill};
|
||||
|
||||
use crate::scene_contract::manifest::{
|
||||
SceneManifest, SCENE_MANIFEST_FILE_NAME, SUPPORTED_SCHEMA_VERSION_V1,
|
||||
SUPPORTED_SCHEDULED_MONITORING_CATEGORY_V1, SUPPORTED_SCHEDULED_MONITORING_KIND_V1,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScheduledMonitoringRegistryEntry {
|
||||
pub manifest: SceneManifest,
|
||||
pub skill_root: PathBuf,
|
||||
pub workflow_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ScheduledMonitoringRegistryError {
|
||||
#[error("failed to read skills directory {path}: {source}")]
|
||||
ReadSkillsDir {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("failed to read scene manifest {path}: {source}")]
|
||||
ReadManifest {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("failed to parse scene manifest {path}: {source}")]
|
||||
ParseManifest {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: toml::de::Error,
|
||||
},
|
||||
#[error("scheduled scene manifest {path} declares unsupported schema_version {version}; only {supported} is supported in v1")]
|
||||
UnsupportedSchemaVersion {
|
||||
path: PathBuf,
|
||||
version: String,
|
||||
supported: &'static str,
|
||||
},
|
||||
#[error("scheduled scene manifest {path} declares unsupported kind {kind}; only {supported} is supported")]
|
||||
UnsupportedSceneKind {
|
||||
path: PathBuf,
|
||||
kind: String,
|
||||
supported: &'static str,
|
||||
},
|
||||
#[error("scheduled scene manifest {path} declares unsupported category {category}; only {supported} is supported")]
|
||||
UnsupportedSceneCategory {
|
||||
path: PathBuf,
|
||||
category: String,
|
||||
supported: &'static str,
|
||||
},
|
||||
#[error("scheduled scene manifest {path} points to missing skill package {skill}")]
|
||||
MissingSkill { path: PathBuf, skill: String },
|
||||
#[error("scheduled scene manifest {path} declares skill {manifest_skill}, but containing skill package is {package_skill}")]
|
||||
SkillPackageMismatch {
|
||||
path: PathBuf,
|
||||
manifest_skill: String,
|
||||
package_skill: String,
|
||||
},
|
||||
#[error("scheduled scene manifest {path} is missing trigger section")]
|
||||
MissingTriggerSection { path: PathBuf },
|
||||
#[error("scheduled scene manifest {path} is missing modes section")]
|
||||
MissingModesSection { path: PathBuf },
|
||||
#[error("scheduled scene manifest {path} is missing runtime_context section")]
|
||||
MissingRuntimeContextSection { path: PathBuf },
|
||||
#[error("scheduled scene manifest {path} is missing safety section")]
|
||||
MissingSafetySection { path: PathBuf },
|
||||
#[error("scheduled scene manifest {path} is missing tools section")]
|
||||
MissingToolsSection { path: PathBuf },
|
||||
#[error("scheduled scene manifest {path} is missing references section")]
|
||||
MissingReferencesSection { path: PathBuf },
|
||||
#[error("scheduled scene manifest {path} is missing workflow_id")]
|
||||
MissingWorkflowId { path: PathBuf },
|
||||
#[error("scheduled scene manifest {path} has unsafe active or queue_process enablement")]
|
||||
UnsafeModesEnabled { path: PathBuf },
|
||||
#[error("scheduled scene manifest {path} incorrectly exposes natural-language primary trigger")]
|
||||
NaturalLanguagePrimaryEnabled { path: PathBuf },
|
||||
#[error("scheduled workflow id {workflow_id} is declared twice: {first_path} and {second_path}")]
|
||||
DuplicateWorkflowId {
|
||||
workflow_id: String,
|
||||
first_path: PathBuf,
|
||||
second_path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn load_scheduled_monitoring_registry(
|
||||
skills_dir: &Path,
|
||||
) -> Result<Vec<ScheduledMonitoringRegistryEntry>, ScheduledMonitoringRegistryError> {
|
||||
if !skills_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut skill_roots = Vec::new();
|
||||
for entry in fs::read_dir(skills_dir).map_err(|source| {
|
||||
ScheduledMonitoringRegistryError::ReadSkillsDir {
|
||||
path: skills_dir.to_path_buf(),
|
||||
source,
|
||||
}
|
||||
})? {
|
||||
let entry = entry.map_err(|source| ScheduledMonitoringRegistryError::ReadSkillsDir {
|
||||
path: skills_dir.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
skill_roots.push(path);
|
||||
}
|
||||
}
|
||||
skill_roots.sort();
|
||||
|
||||
let skills_by_root = index_skills_by_root(skills_dir);
|
||||
let mut workflow_ids = HashMap::new();
|
||||
let mut registry = Vec::new();
|
||||
|
||||
for skill_root in skill_roots {
|
||||
let manifest_path = skill_root.join(SCENE_MANIFEST_FILE_NAME);
|
||||
if !manifest_path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let manifest = load_manifest(&manifest_path)?;
|
||||
if manifest.scene.kind != SUPPORTED_SCHEDULED_MONITORING_KIND_V1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let workflow_id = validate_manifest(&manifest, &manifest_path, &skill_root, &skills_by_root)?;
|
||||
if let Some(first_path) = workflow_ids.insert(workflow_id.clone(), manifest_path.clone()) {
|
||||
return Err(ScheduledMonitoringRegistryError::DuplicateWorkflowId {
|
||||
workflow_id,
|
||||
first_path,
|
||||
second_path: manifest_path,
|
||||
});
|
||||
}
|
||||
|
||||
registry.push(ScheduledMonitoringRegistryEntry {
|
||||
manifest,
|
||||
skill_root,
|
||||
workflow_id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(registry)
|
||||
}
|
||||
|
||||
fn index_skills_by_root(skills_dir: &Path) -> HashMap<PathBuf, Skill> {
|
||||
load_skills_from_directory(skills_dir, true)
|
||||
.into_iter()
|
||||
.filter_map(|skill| {
|
||||
let skill_root = skill
|
||||
.location
|
||||
.as_deref()
|
||||
.and_then(Path::parent)
|
||||
.map(Path::to_path_buf)?;
|
||||
Some((skill_root, skill))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn load_manifest(path: &Path) -> Result<SceneManifest, ScheduledMonitoringRegistryError> {
|
||||
let content =
|
||||
fs::read_to_string(path).map_err(|source| ScheduledMonitoringRegistryError::ReadManifest {
|
||||
path: path.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
toml::from_str(&content).map_err(|source| ScheduledMonitoringRegistryError::ParseManifest {
|
||||
path: path.to_path_buf(),
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_manifest(
|
||||
manifest: &SceneManifest,
|
||||
manifest_path: &Path,
|
||||
skill_root: &Path,
|
||||
skills_by_root: &HashMap<PathBuf, Skill>,
|
||||
) -> Result<String, ScheduledMonitoringRegistryError> {
|
||||
if manifest.manifest.schema_version != SUPPORTED_SCHEMA_VERSION_V1 {
|
||||
return Err(ScheduledMonitoringRegistryError::UnsupportedSchemaVersion {
|
||||
path: manifest_path.to_path_buf(),
|
||||
version: manifest.manifest.schema_version.clone(),
|
||||
supported: SUPPORTED_SCHEMA_VERSION_V1,
|
||||
});
|
||||
}
|
||||
|
||||
if manifest.scene.kind != SUPPORTED_SCHEDULED_MONITORING_KIND_V1 {
|
||||
return Err(ScheduledMonitoringRegistryError::UnsupportedSceneKind {
|
||||
path: manifest_path.to_path_buf(),
|
||||
kind: manifest.scene.kind.clone(),
|
||||
supported: SUPPORTED_SCHEDULED_MONITORING_KIND_V1,
|
||||
});
|
||||
}
|
||||
|
||||
if manifest.scene.category != SUPPORTED_SCHEDULED_MONITORING_CATEGORY_V1 {
|
||||
return Err(ScheduledMonitoringRegistryError::UnsupportedSceneCategory {
|
||||
path: manifest_path.to_path_buf(),
|
||||
category: manifest.scene.category.clone(),
|
||||
supported: SUPPORTED_SCHEDULED_MONITORING_CATEGORY_V1,
|
||||
});
|
||||
}
|
||||
|
||||
let trigger = manifest
|
||||
.trigger
|
||||
.as_ref()
|
||||
.ok_or_else(|| ScheduledMonitoringRegistryError::MissingTriggerSection {
|
||||
path: manifest_path.to_path_buf(),
|
||||
})?;
|
||||
let modes = manifest
|
||||
.modes
|
||||
.as_ref()
|
||||
.ok_or_else(|| ScheduledMonitoringRegistryError::MissingModesSection {
|
||||
path: manifest_path.to_path_buf(),
|
||||
})?;
|
||||
let _runtime_context = manifest.runtime_context.as_ref().ok_or_else(|| {
|
||||
ScheduledMonitoringRegistryError::MissingRuntimeContextSection {
|
||||
path: manifest_path.to_path_buf(),
|
||||
}
|
||||
})?;
|
||||
let safety = manifest
|
||||
.safety
|
||||
.as_ref()
|
||||
.ok_or_else(|| ScheduledMonitoringRegistryError::MissingSafetySection {
|
||||
path: manifest_path.to_path_buf(),
|
||||
})?;
|
||||
let tools = manifest
|
||||
.tools
|
||||
.as_ref()
|
||||
.ok_or_else(|| ScheduledMonitoringRegistryError::MissingToolsSection {
|
||||
path: manifest_path.to_path_buf(),
|
||||
})?;
|
||||
let _references = manifest.references.as_ref().ok_or_else(|| {
|
||||
ScheduledMonitoringRegistryError::MissingReferencesSection {
|
||||
path: manifest_path.to_path_buf(),
|
||||
}
|
||||
})?;
|
||||
|
||||
let workflow_id = manifest
|
||||
.scene
|
||||
.workflow_id
|
||||
.clone()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| ScheduledMonitoringRegistryError::MissingWorkflowId {
|
||||
path: manifest_path.to_path_buf(),
|
||||
})?;
|
||||
|
||||
if safety.active_enabled || safety.queue_process_enabled {
|
||||
return Err(ScheduledMonitoringRegistryError::UnsafeModesEnabled {
|
||||
path: manifest_path.to_path_buf(),
|
||||
});
|
||||
}
|
||||
|
||||
if trigger.natural_language_primary {
|
||||
return Err(ScheduledMonitoringRegistryError::NaturalLanguagePrimaryEnabled {
|
||||
path: manifest_path.to_path_buf(),
|
||||
});
|
||||
}
|
||||
|
||||
if !modes.enabled.iter().any(|mode| mode == "dry_run" || mode == "monitor_only") {
|
||||
return Err(ScheduledMonitoringRegistryError::UnsafeModesEnabled {
|
||||
path: manifest_path.to_path_buf(),
|
||||
});
|
||||
}
|
||||
|
||||
let Some(skill) = skills_by_root.get(skill_root) else {
|
||||
return Err(ScheduledMonitoringRegistryError::MissingSkill {
|
||||
path: manifest_path.to_path_buf(),
|
||||
skill: manifest.scene.skill.clone(),
|
||||
});
|
||||
};
|
||||
|
||||
if skill.name != manifest.scene.skill {
|
||||
return Err(ScheduledMonitoringRegistryError::SkillPackageMismatch {
|
||||
path: manifest_path.to_path_buf(),
|
||||
manifest_skill: manifest.scene.skill.clone(),
|
||||
package_skill: skill.name.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
for expected_tool in [&tools.detect, &tools.decide, &tools.action_plan] {
|
||||
if expected_tool.trim().is_empty() {
|
||||
return Err(ScheduledMonitoringRegistryError::MissingToolsSection {
|
||||
path: manifest_path.to_path_buf(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(workflow_id)
|
||||
}
|
||||
@@ -152,6 +152,8 @@ pub struct SgClawSettings {
|
||||
pub office_backend: OfficeBackend,
|
||||
pub browser_ws_url: Option<String>,
|
||||
pub service_ws_listen_addr: Option<String>,
|
||||
pub scheduled_monitoring_platform_service_base_url: Option<String>,
|
||||
pub scheduled_monitoring_watch_tasks: Vec<String>,
|
||||
}
|
||||
|
||||
impl SgClawSettings {
|
||||
@@ -190,6 +192,8 @@ impl SgClawSettings {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -237,6 +241,16 @@ impl SgClawSettings {
|
||||
}),
|
||||
browser_ws_url: self.browser_ws_url.clone(),
|
||||
service_ws_listen_addr: self.service_ws_listen_addr.clone(),
|
||||
scheduled_monitoring_platform_service_base_url: self
|
||||
.scheduled_monitoring_platform_service_base_url
|
||||
.clone(),
|
||||
scheduled_monitoring: if self.scheduled_monitoring_watch_tasks.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(SerializableScheduledMonitoringSettings {
|
||||
watch_tasks: self.scheduled_monitoring_watch_tasks.clone(),
|
||||
})
|
||||
},
|
||||
providers: self
|
||||
.providers
|
||||
.iter()
|
||||
@@ -290,6 +304,8 @@ impl SgClawSettings {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
)?))
|
||||
}
|
||||
|
||||
@@ -376,6 +392,11 @@ impl SgClawSettings {
|
||||
office_backend,
|
||||
config.browser_ws_url,
|
||||
config.service_ws_listen_addr,
|
||||
config.scheduled_monitoring_platform_service_base_url,
|
||||
config
|
||||
.scheduled_monitoring
|
||||
.map(|value| value.watch_tasks)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.map_err(|err| err.with_path(path))
|
||||
}
|
||||
@@ -395,6 +416,8 @@ impl SgClawSettings {
|
||||
office_backend: Option<OfficeBackend>,
|
||||
browser_ws_url: Option<String>,
|
||||
service_ws_listen_addr: Option<String>,
|
||||
scheduled_monitoring_platform_service_base_url: Option<String>,
|
||||
scheduled_monitoring_watch_tasks: Vec<String>,
|
||||
) -> Result<Self, ConfigError> {
|
||||
let direct_submit_skill = normalize_direct_submit_skill(direct_submit_skill)?;
|
||||
let providers = if providers.is_empty() {
|
||||
@@ -438,6 +461,10 @@ impl SgClawSettings {
|
||||
office_backend: office_backend.unwrap_or(OfficeBackend::OpenXml),
|
||||
browser_ws_url: normalize_optional_value(browser_ws_url),
|
||||
service_ws_listen_addr: normalize_optional_value(service_ws_listen_addr),
|
||||
scheduled_monitoring_platform_service_base_url: normalize_optional_value(
|
||||
scheduled_monitoring_platform_service_base_url,
|
||||
),
|
||||
scheduled_monitoring_watch_tasks,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -627,10 +654,26 @@ struct SerializableRawSgClawSettings {
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
service_ws_listen_addr: Option<String>,
|
||||
#[serde(
|
||||
rename = "scheduledMonitoringPlatformServiceBaseUrl",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
scheduled_monitoring_platform_service_base_url: Option<String>,
|
||||
#[serde(
|
||||
rename = "scheduledMonitoring",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
scheduled_monitoring: Option<SerializableScheduledMonitoringSettings>,
|
||||
#[serde(default)]
|
||||
providers: Vec<SerializableProviderSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SerializableScheduledMonitoringSettings {
|
||||
#[serde(rename = "watchTasks")]
|
||||
watch_tasks: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SerializableProviderSettings {
|
||||
id: String,
|
||||
@@ -680,10 +723,24 @@ struct RawSgClawSettings {
|
||||
default
|
||||
)]
|
||||
service_ws_listen_addr: Option<String>,
|
||||
#[serde(
|
||||
rename = "scheduledMonitoringPlatformServiceBaseUrl",
|
||||
alias = "scheduled_monitoring_platform_service_base_url",
|
||||
default
|
||||
)]
|
||||
scheduled_monitoring_platform_service_base_url: Option<String>,
|
||||
#[serde(rename = "scheduledMonitoring", default)]
|
||||
scheduled_monitoring: Option<RawScheduledMonitoringSettings>,
|
||||
#[serde(default)]
|
||||
providers: Vec<RawProviderSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawScheduledMonitoringSettings {
|
||||
#[serde(rename = "watchTasks", alias = "watch_tasks", default)]
|
||||
watch_tasks: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawProviderSettings {
|
||||
#[serde(default)]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,8 @@ pub enum WorkflowArchetype {
|
||||
LocalDocPipeline,
|
||||
#[serde(rename = "page_state_eval")]
|
||||
PageStateEval,
|
||||
#[serde(rename = "monitoring_action_workflow")]
|
||||
MonitoringActionWorkflow,
|
||||
}
|
||||
|
||||
impl WorkflowArchetype {
|
||||
@@ -34,6 +36,7 @@ impl WorkflowArchetype {
|
||||
Self::MultiEndpointInventory => "multi_endpoint_inventory",
|
||||
Self::LocalDocPipeline => "local_doc_pipeline",
|
||||
Self::PageStateEval => "page_state_eval",
|
||||
Self::MonitoringActionWorkflow => "monitoring_action_workflow",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +50,7 @@ impl WorkflowArchetype {
|
||||
"multi_endpoint_inventory" => Some(Self::MultiEndpointInventory),
|
||||
"local_doc_pipeline" => Some(Self::LocalDocPipeline),
|
||||
"page_state_eval" => Some(Self::PageStateEval),
|
||||
"monitoring_action_workflow" => Some(Self::MonitoringActionWorkflow),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -252,6 +256,259 @@ pub struct RuntimeDependencyIr {
|
||||
pub subordinate_to_business_chain: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct MonitoringRuntimeContextIr {
|
||||
#[serde(rename = "runtimeContextUrl", default)]
|
||||
pub runtime_context_url: String,
|
||||
#[serde(rename = "expectedDomain", default)]
|
||||
pub expected_domain: String,
|
||||
#[serde(rename = "gatewayDomain", default)]
|
||||
pub gateway_domain: String,
|
||||
#[serde(rename = "localhostServiceBase", default)]
|
||||
pub localhost_service_base: String,
|
||||
#[serde(rename = "browserAttachedRequired", default)]
|
||||
pub browser_attached_required: bool,
|
||||
#[serde(rename = "hostBridgeRequired", default)]
|
||||
pub host_bridge_required: bool,
|
||||
#[serde(
|
||||
rename = "executionContextMode",
|
||||
default = "default_execution_context_mode"
|
||||
)]
|
||||
pub execution_context_mode: String,
|
||||
#[serde(
|
||||
rename = "requestClientMode",
|
||||
default = "default_request_client_mode"
|
||||
)]
|
||||
pub request_client_mode: String,
|
||||
#[serde(rename = "encryptionMode", default = "default_encryption_mode")]
|
||||
pub encryption_mode: String,
|
||||
#[serde(
|
||||
rename = "attachedPageBrowserActionPolicy",
|
||||
default = "default_attached_page_browser_action_policy"
|
||||
)]
|
||||
pub attached_page_browser_action_policy: String,
|
||||
#[serde(
|
||||
rename = "platformWritePolicy",
|
||||
default = "default_platform_write_policy"
|
||||
)]
|
||||
pub platform_write_policy: String,
|
||||
#[serde(rename = "storageReads", default)]
|
||||
pub storage_reads: Vec<MonitoringStorageReadIr>,
|
||||
#[serde(rename = "readSlices", default)]
|
||||
pub read_slices: Vec<MonitoringReadSliceIr>,
|
||||
#[serde(rename = "encryptionResolution", default)]
|
||||
pub encryption_resolution: MonitoringEncryptionResolutionIr,
|
||||
#[serde(rename = "timeoutContract", default)]
|
||||
pub timeout_contract: MonitoringTimeoutContractIr,
|
||||
#[serde(rename = "outputContract", default)]
|
||||
pub output_contract: MonitoringOutputContractIr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct MonitoringStorageReadIr {
|
||||
#[serde(default)]
|
||||
pub key: String,
|
||||
#[serde(default)]
|
||||
pub source: String,
|
||||
#[serde(rename = "fallbackOrder", default)]
|
||||
pub fallback_order: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
#[serde(rename = "parseMode", default)]
|
||||
pub parse_mode: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct MonitoringReadSliceIr {
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(rename = "endpointBinding", default)]
|
||||
pub endpoint_binding: String,
|
||||
#[serde(rename = "requestTemplateOverride", default)]
|
||||
pub request_template_override: Value,
|
||||
#[serde(rename = "responsePath", default)]
|
||||
pub response_path: String,
|
||||
#[serde(rename = "timeoutMs", default)]
|
||||
pub timeout_ms: u64,
|
||||
#[serde(rename = "mergeRole", default)]
|
||||
pub merge_role: String,
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct MonitoringEncryptionResolutionIr {
|
||||
#[serde(rename = "primaryMethod", default)]
|
||||
pub primary_method: String,
|
||||
#[serde(rename = "fallbackMethods", default)]
|
||||
pub fallback_methods: Vec<String>,
|
||||
#[serde(rename = "requiredContext", default)]
|
||||
pub required_context: Vec<String>,
|
||||
#[serde(rename = "hardFail", default)]
|
||||
pub hard_fail: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct MonitoringTimeoutContractIr {
|
||||
#[serde(rename = "perStepTimeoutMs", default)]
|
||||
pub per_step_timeout_ms: u64,
|
||||
#[serde(rename = "overallDetectTimeoutMs", default)]
|
||||
pub overall_detect_timeout_ms: u64,
|
||||
#[serde(rename = "statusOnTimeout", default)]
|
||||
pub status_on_timeout: String,
|
||||
#[serde(rename = "statusOnPartial", default)]
|
||||
pub status_on_partial: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct MonitoringSidecarOutputIr {
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(rename = "relativePath", default)]
|
||||
pub relative_path: String,
|
||||
#[serde(rename = "payloadSchema", default)]
|
||||
pub payload_schema: Value,
|
||||
#[serde(rename = "sourceField", default)]
|
||||
pub source_field: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MonitoringOutputContractIr {
|
||||
#[serde(rename = "runRecordMode", default = "default_run_record_mode")]
|
||||
pub run_record_mode: String,
|
||||
#[serde(rename = "businessLogEnabled", default = "default_true")]
|
||||
pub business_log_enabled: bool,
|
||||
#[serde(rename = "sidecarOutputs", default)]
|
||||
pub sidecar_outputs: Vec<MonitoringSidecarOutputIr>,
|
||||
#[serde(rename = "deltaState", default)]
|
||||
pub delta_state: Option<MonitoringDeltaStateIr>,
|
||||
}
|
||||
|
||||
impl Default for MonitoringOutputContractIr {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
run_record_mode: default_run_record_mode(),
|
||||
business_log_enabled: true,
|
||||
sidecar_outputs: Vec::new(),
|
||||
delta_state: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct MonitoringDeltaStateIr {
|
||||
#[serde(rename = "identityFields", default)]
|
||||
pub identity_fields: Vec<String>,
|
||||
#[serde(rename = "stateSidecarPath", default)]
|
||||
pub state_sidecar_path: String,
|
||||
#[serde(rename = "comparisonMode", default)]
|
||||
pub comparison_mode: String,
|
||||
#[serde(rename = "emitPolicy", default)]
|
||||
pub emit_policy: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct MonitoringDependencyIr {
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub url: String,
|
||||
#[serde(default)]
|
||||
pub classification: String,
|
||||
#[serde(rename = "sideEffect", default)]
|
||||
pub side_effect: bool,
|
||||
#[serde(rename = "blockedByDefault", default)]
|
||||
pub blocked_by_default: bool,
|
||||
}
|
||||
|
||||
impl MonitoringDependencyIr {
|
||||
pub fn normalized_classification(&self) -> String {
|
||||
let url = self.url.trim().to_ascii_lowercase();
|
||||
if url.starts_with("http://localhost:13313/")
|
||||
|| url.starts_with("https://localhost:13313/")
|
||||
{
|
||||
return "host_runtime_local_service".to_string();
|
||||
}
|
||||
if url.contains("/monitorservices/")
|
||||
|| url.contains("/marketingservices/")
|
||||
|| url.contains("/configservices/")
|
||||
|| url.contains("/reportservices/")
|
||||
|| url.contains("/surfaceservices/")
|
||||
|| url.contains("/msgoffilecenter/")
|
||||
{
|
||||
return "remote_platform_service".to_string();
|
||||
}
|
||||
let classification = self.classification.trim();
|
||||
if classification.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
classification.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct MonitoringSideEffectIr {
|
||||
#[serde(default)]
|
||||
pub action: String,
|
||||
#[serde(default)]
|
||||
pub signals: Vec<String>,
|
||||
#[serde(rename = "blockedByDefault", default)]
|
||||
pub blocked_by_default: bool,
|
||||
#[serde(rename = "requiredFutureGate", default)]
|
||||
pub required_future_gate: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct MonitoringSideEffectPolicyIr {
|
||||
#[serde(rename = "dryRunDefault", default)]
|
||||
pub dry_run_default: bool,
|
||||
#[serde(rename = "requiresExplicitConfirmation", default)]
|
||||
pub requires_explicit_confirmation: bool,
|
||||
#[serde(rename = "previewBeforeAction", default)]
|
||||
pub preview_before_action: bool,
|
||||
#[serde(rename = "maxItemsRequiredForActionModes", default)]
|
||||
pub max_items_required_for_action_modes: bool,
|
||||
#[serde(rename = "auditRecordRequired", default)]
|
||||
pub audit_record_required: bool,
|
||||
#[serde(rename = "blockedCallSignatures", default)]
|
||||
pub blocked_call_signatures: Vec<String>,
|
||||
#[serde(rename = "blockedActions", default)]
|
||||
pub blocked_actions: Vec<MonitoringSideEffectIr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct MonitoringActionWorkflowIr {
|
||||
#[serde(rename = "workflowId", default)]
|
||||
pub workflow_id: String,
|
||||
#[serde(rename = "displayName", default)]
|
||||
pub display_name: String,
|
||||
#[serde(rename = "defaultMode", default)]
|
||||
pub default_mode: String,
|
||||
#[serde(rename = "workflowStages", default)]
|
||||
pub workflow_stages: Vec<String>,
|
||||
#[serde(rename = "mvpAllowedStages", default)]
|
||||
pub mvp_allowed_stages: Vec<String>,
|
||||
#[serde(rename = "blockedByDefaultStages", default)]
|
||||
pub blocked_by_default_stages: Vec<String>,
|
||||
#[serde(rename = "runtimeContext", default)]
|
||||
pub runtime_context: MonitoringRuntimeContextIr,
|
||||
#[serde(rename = "localStorageReads", default)]
|
||||
pub local_storage_reads: Vec<String>,
|
||||
#[serde(rename = "sessionStorageReads", default)]
|
||||
pub session_storage_reads: Vec<String>,
|
||||
#[serde(rename = "localServiceDependencies", default)]
|
||||
pub local_service_dependencies: Vec<MonitoringDependencyIr>,
|
||||
#[serde(rename = "businessApiDependencies", default)]
|
||||
pub business_api_dependencies: Vec<MonitoringDependencyIr>,
|
||||
#[serde(rename = "previewSchema", default)]
|
||||
pub preview_schema: Vec<String>,
|
||||
#[serde(rename = "sideEffectPolicy", default)]
|
||||
pub side_effect_policy: MonitoringSideEffectPolicyIr,
|
||||
#[serde(rename = "archetype", default = "default_monitoring_archetype")]
|
||||
pub archetype: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct MergeFieldMappingIr {
|
||||
#[serde(rename = "outputField", default)]
|
||||
@@ -296,6 +553,38 @@ impl Default for ArtifactContractIr {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_execution_context_mode() -> String {
|
||||
"attached_page_direct".to_string()
|
||||
}
|
||||
|
||||
fn default_request_client_mode() -> String {
|
||||
"isolated_xhr".to_string()
|
||||
}
|
||||
|
||||
fn default_encryption_mode() -> String {
|
||||
"emsslib_data_encrypt_pub".to_string()
|
||||
}
|
||||
|
||||
fn default_attached_page_browser_action_policy() -> String {
|
||||
"forbid_secondary_jump".to_string()
|
||||
}
|
||||
|
||||
fn default_platform_write_policy() -> String {
|
||||
"skip_when_zero".to_string()
|
||||
}
|
||||
|
||||
fn default_run_record_mode() -> String {
|
||||
"per_scene_file".to_string()
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_monitoring_archetype() -> String {
|
||||
"marketing_gateway_monitor".to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ValidationHintsIr {
|
||||
#[serde(
|
||||
@@ -447,6 +736,8 @@ pub struct SceneIr {
|
||||
pub confidence: f64,
|
||||
#[serde(default)]
|
||||
pub uncertainties: Vec<String>,
|
||||
#[serde(rename = "monitoringActionWorkflow", default)]
|
||||
pub monitoring_action_workflow: Option<MonitoringActionWorkflowIr>,
|
||||
}
|
||||
|
||||
impl SceneIr {
|
||||
@@ -620,6 +911,7 @@ impl From<LegacySceneInfoJson> for SceneIr {
|
||||
column_defs: value.column_defs,
|
||||
confidence: 0.0,
|
||||
uncertainties: Vec::new(),
|
||||
monitoring_action_workflow: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ pub mod analyzer;
|
||||
pub mod generator;
|
||||
pub mod ir;
|
||||
pub mod lessons;
|
||||
pub mod scheduled_monitoring_runtime;
|
||||
|
||||
2430
src/generated_scene/scheduled_monitoring_runtime.rs
Normal file
2430
src/generated_scene/scheduled_monitoring_runtime.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,28 +5,50 @@ pub const SCENE_MANIFEST_FILE_NAME: &str = "scene.toml";
|
||||
pub const SUPPORTED_SCHEMA_VERSION_V1: &str = "1";
|
||||
pub const SUPPORTED_SCENE_KIND_V1: &str = "browser_script";
|
||||
pub const SUPPORTED_SCENE_CATEGORY_V1: &str = "report_collection";
|
||||
pub const SUPPORTED_SCHEDULED_MONITORING_KIND_V1: &str = "scheduled_monitoring_action_workflow";
|
||||
pub const SUPPORTED_SCHEDULED_MONITORING_CATEGORY_V1: &str = "monitoring";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SceneManifest {
|
||||
pub scene: SceneSection,
|
||||
pub manifest: ManifestSection,
|
||||
pub bootstrap: BootstrapSection,
|
||||
pub deterministic: DeterministicSection,
|
||||
#[serde(default)]
|
||||
pub bootstrap: Option<BootstrapSection>,
|
||||
#[serde(default)]
|
||||
pub deterministic: Option<DeterministicSection>,
|
||||
#[serde(default)]
|
||||
pub params: Vec<SceneParam>,
|
||||
pub artifact: ArtifactSection,
|
||||
#[serde(default)]
|
||||
pub artifact: Option<ArtifactSection>,
|
||||
#[serde(default)]
|
||||
pub postprocess: Option<PostprocessSection>,
|
||||
#[serde(default)]
|
||||
pub trigger: Option<TriggerSection>,
|
||||
#[serde(default)]
|
||||
pub modes: Option<ModesSection>,
|
||||
#[serde(default)]
|
||||
pub runtime_context: Option<RuntimeContextSection>,
|
||||
#[serde(default)]
|
||||
pub safety: Option<SafetySection>,
|
||||
#[serde(default)]
|
||||
pub tools: Option<ScheduledToolsSection>,
|
||||
#[serde(default)]
|
||||
pub references: Option<ScheduledReferencesSection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SceneSection {
|
||||
pub id: String,
|
||||
pub skill: String,
|
||||
#[serde(default)]
|
||||
pub tool: String,
|
||||
pub kind: String,
|
||||
pub version: String,
|
||||
pub category: String,
|
||||
#[serde(default)]
|
||||
pub workflow_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub archetype: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -34,7 +56,7 @@ pub struct ManifestSection {
|
||||
pub schema_version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BootstrapSection {
|
||||
pub expected_domain: String,
|
||||
pub target_url: String,
|
||||
@@ -43,7 +65,7 @@ pub struct BootstrapSection {
|
||||
pub requires_target_page: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct DeterministicSection {
|
||||
pub suffix: String,
|
||||
#[serde(default)]
|
||||
@@ -63,7 +85,7 @@ pub struct SceneParam {
|
||||
pub resolver_config: Table,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ArtifactSection {
|
||||
#[serde(rename = "type")]
|
||||
pub artifact_type: String,
|
||||
@@ -79,3 +101,201 @@ pub struct PostprocessSection {
|
||||
#[serde(default)]
|
||||
pub auto_open: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct TriggerSection {
|
||||
#[serde(default)]
|
||||
pub primary: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub validation: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allowed: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub natural_language_primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ModesSection {
|
||||
#[serde(default)]
|
||||
pub enabled: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub disabled: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub default: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct RuntimeContextSection {
|
||||
#[serde(default)]
|
||||
pub runtime_context_url: String,
|
||||
#[serde(default)]
|
||||
pub expected_domain: String,
|
||||
#[serde(default)]
|
||||
pub gateway_domain: String,
|
||||
#[serde(default)]
|
||||
pub localhost_service_base: String,
|
||||
#[serde(default)]
|
||||
pub browser_attached_required: bool,
|
||||
#[serde(default)]
|
||||
pub host_bridge_required: bool,
|
||||
#[serde(default)]
|
||||
pub execution_context_mode: String,
|
||||
#[serde(default)]
|
||||
pub request_client_mode: String,
|
||||
#[serde(default)]
|
||||
pub encryption_mode: String,
|
||||
#[serde(default)]
|
||||
pub attached_page_browser_action_policy: String,
|
||||
#[serde(default)]
|
||||
pub platform_write_policy: String,
|
||||
#[serde(default)]
|
||||
pub storage_reads: Vec<StorageReadSection>,
|
||||
#[serde(default)]
|
||||
pub read_slices: Vec<ReadSliceSection>,
|
||||
#[serde(default)]
|
||||
pub encryption_resolution: EncryptionResolutionSection,
|
||||
#[serde(default)]
|
||||
pub timeout_contract: TimeoutContractSection,
|
||||
#[serde(default)]
|
||||
pub output_contract: OutputContractSection,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct StorageReadSection {
|
||||
#[serde(default)]
|
||||
pub key: String,
|
||||
#[serde(default)]
|
||||
pub source: String,
|
||||
#[serde(default)]
|
||||
pub fallback_order: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
#[serde(default)]
|
||||
pub parse_mode: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ReadSliceSection {
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub endpoint_binding: String,
|
||||
#[serde(default)]
|
||||
pub request_template_override: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub response_path: String,
|
||||
#[serde(default)]
|
||||
pub timeout_ms: u64,
|
||||
#[serde(default)]
|
||||
pub merge_role: String,
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct EncryptionResolutionSection {
|
||||
#[serde(default)]
|
||||
pub primary_method: String,
|
||||
#[serde(default)]
|
||||
pub fallback_methods: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub required_context: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub hard_fail: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct TimeoutContractSection {
|
||||
#[serde(default)]
|
||||
pub per_step_timeout_ms: u64,
|
||||
#[serde(default)]
|
||||
pub overall_detect_timeout_ms: u64,
|
||||
#[serde(default)]
|
||||
pub status_on_timeout: String,
|
||||
#[serde(default)]
|
||||
pub status_on_partial: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct OutputContractSection {
|
||||
#[serde(default)]
|
||||
pub run_record_mode: String,
|
||||
#[serde(default)]
|
||||
pub business_log_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub sidecar_outputs: Vec<SidecarOutputSection>,
|
||||
#[serde(default)]
|
||||
pub delta_state: Option<DeltaStateSection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SidecarOutputSection {
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub relative_path: String,
|
||||
#[serde(default)]
|
||||
pub source_field: String,
|
||||
#[serde(default)]
|
||||
pub payload_schema: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct DeltaStateSection {
|
||||
#[serde(default)]
|
||||
pub identity_fields: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub state_sidecar_path: String,
|
||||
#[serde(default)]
|
||||
pub comparison_mode: String,
|
||||
#[serde(default)]
|
||||
pub emit_policy: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SafetySection {
|
||||
#[serde(default)]
|
||||
pub dry_run_default: bool,
|
||||
#[serde(default)]
|
||||
pub active_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub queue_process_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub side_effects_default: String,
|
||||
#[serde(default)]
|
||||
pub audit_required_before_active: bool,
|
||||
#[serde(default)]
|
||||
pub idempotency_required_before_active: bool,
|
||||
#[serde(default)]
|
||||
pub blocked_call_signatures: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ScheduledToolsSection {
|
||||
#[serde(default)]
|
||||
pub detect: String,
|
||||
#[serde(default)]
|
||||
pub decide: String,
|
||||
#[serde(default)]
|
||||
pub action_plan: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ScheduledReferencesSection {
|
||||
#[serde(default)]
|
||||
pub source_evidence: String,
|
||||
#[serde(default)]
|
||||
pub workflow_ir: String,
|
||||
#[serde(default)]
|
||||
pub trigger_contract: String,
|
||||
#[serde(default)]
|
||||
pub platform_dependencies: String,
|
||||
#[serde(default)]
|
||||
pub side_effect_policy: String,
|
||||
#[serde(default)]
|
||||
pub audit_policy: String,
|
||||
#[serde(default)]
|
||||
pub idempotency_policy: String,
|
||||
#[serde(default)]
|
||||
pub generation_report: String,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
pub mod manifest;
|
||||
|
||||
pub use manifest::{
|
||||
ArtifactSection, BootstrapSection, DeterministicSection, ManifestSection, PostprocessSection,
|
||||
SceneManifest, SceneParam, SceneSection, SCENE_MANIFEST_FILE_NAME, SUPPORTED_SCENE_CATEGORY_V1,
|
||||
SUPPORTED_SCENE_KIND_V1, SUPPORTED_SCHEMA_VERSION_V1,
|
||||
ArtifactSection, BootstrapSection, DeterministicSection, ManifestSection, ModesSection,
|
||||
PostprocessSection, RuntimeContextSection, SafetySection, ScheduledReferencesSection,
|
||||
ScheduledToolsSection, SceneManifest, SceneParam, SceneSection, TriggerSection,
|
||||
SCENE_MANIFEST_FILE_NAME, SUPPORTED_SCENE_CATEGORY_V1, SUPPORTED_SCENE_KIND_V1,
|
||||
SUPPORTED_SCHEMA_VERSION_V1, SUPPORTED_SCHEDULED_MONITORING_CATEGORY_V1,
|
||||
SUPPORTED_SCHEDULED_MONITORING_KIND_V1,
|
||||
};
|
||||
|
||||
@@ -364,6 +364,7 @@ pub(crate) fn serve_client(
|
||||
Duration::from_secs(15),
|
||||
BROWSER_RESPONSE_TIMEOUT,
|
||||
true, // use_hidden_domain: hidden domain for invisible helper
|
||||
None,
|
||||
) {
|
||||
Ok(host) => {
|
||||
send_info_log(sink.as_ref(), "callback-host startup ready")?;
|
||||
|
||||
Reference in New Issue
Block a user