feat: appove and go

This commit is contained in:
thanhnv
2026-07-19 09:37:16 +07:00
parent 13fae3e6c3
commit 709b6cccd6
24 changed files with 1245 additions and 70 deletions
@@ -0,0 +1,18 @@
{
"1b5426346e8198bc61cf8e5620be210895680af6e1cdb82bba8694c7f0da2ef9": {
"model": "openai-compatible:auto/coding",
"provider": "omniroute",
"healthy": false,
"reason": "model_exit_2:MODEL_ROUTER_ERROR backend_unreachable HTTPError: HTTP Error 503: Service Unavailable",
"checked_at": "2026-07-18T15:24:11Z",
"checked_epoch": 1784388251.1839528
},
"0a4a9b1f8e1ff0908e2861bbbdf3dd4b99ac381a8749c94baed4930a3ca11292": {
"model": "ollama:ornith:9b",
"provider": "ollama",
"healthy": false,
"reason": "model_timeout",
"checked_at": "2026-07-18T15:24:41Z",
"checked_epoch": 1784388281.2238398
}
}
+130
View File
@@ -0,0 +1,130 @@
{
"1b5426346e8198bc61cf8e5620be210895680af6e1cdb82bba8694c7f0da2ef9": {
"model": "openai-compatible:auto/coding",
"provider": "omniroute",
"healthy": false,
"reason": "model_timeout",
"checked_at": "2026-07-18T16:20:54Z",
"checked_epoch": 1784391654.9303336
},
"0a4a9b1f8e1ff0908e2861bbbdf3dd4b99ac381a8749c94baed4930a3ca11292": {
"model": "ollama:ornith:9b",
"provider": "ollama",
"healthy": true,
"reason": "ok",
"checked_at": "2026-07-18T15:27:33Z",
"checked_epoch": 1784388453.7658935
},
"2ad50792d2c887dd74476a96e22a2127536692a58e6045e1e3941a9fa79a5df9": {
"model": "account:codex",
"provider": "codex-account",
"healthy": true,
"reason": "ok",
"checked_at": "2026-07-18T15:38:46Z",
"checked_epoch": 1784389126.023104
},
"501c7fb832612cef3360ddd8a02b80c0a656b75353a6706cab6c1ce54bde11ea": {
"model": "openai:gpt-5.3-codex",
"provider": "openai",
"healthy": true,
"reason": "ok",
"checked_at": "2026-07-18T16:14:15Z",
"checked_epoch": 1784391255.5425012
},
"f535de9122c693a1ec0b67cd9248e0e0cf50d02689a7885863ddd2fcb9dac4a2": {
"model": "openai:gpt-5.3-codex",
"provider": "openai",
"healthy": false,
"reason": "goal_patch_missing",
"checked_at": "2026-07-18T16:21:52Z",
"checked_epoch": 1784391712.3473551
},
"fd0849f02e64d143f68870f7f09f8e9054c97efdb32970dc498ba6aa421c25cc": {
"model": "openai-compatible:aug/claude-haiku-4.5",
"provider": "omniroute",
"healthy": false,
"reason": "model_exit_2:MODEL_ROUTER_ERROR backend_unreachable HTTPError: HTTP Error 502: Bad Gateway",
"checked_at": "2026-07-18T16:20:55Z",
"checked_epoch": 1784391655.048939
},
"20ed5fb7e077c5de0c1e22b8b09c9ded068ff9b920ffbad5aee205a2f7a4ced7": {
"model": "openai-compatible:aug/claude-opus-4.6",
"provider": "omniroute",
"healthy": false,
"reason": "model_exit_2:MODEL_ROUTER_ERROR backend_unreachable HTTPError: HTTP Error 502: Bad Gateway",
"checked_at": "2026-07-18T16:20:55Z",
"checked_epoch": 1784391655.140041
},
"5d7bac5e696c4d1433d3c138fd1c310a811f496ba15b58c61bd2ed81d1a4f3ad": {
"model": "openai-compatible:aug/claude-sonnet-4.6",
"provider": "omniroute",
"healthy": false,
"reason": "model_exit_2:MODEL_ROUTER_ERROR backend_unreachable HTTPError: HTTP Error 502: Bad Gateway",
"checked_at": "2026-07-18T16:20:55Z",
"checked_epoch": 1784391655.2397785
},
"e46af2703f62053be4e77849225ca03c3f029da9ec91aaea78f6e0726a3dea2d": {
"model": "openai-compatible:aug/claude-sonnet-4.6-thinking",
"provider": "omniroute",
"healthy": false,
"reason": "model_exit_2:MODEL_ROUTER_ERROR backend_unreachable HTTPError: HTTP Error 502: Bad Gateway",
"checked_at": "2026-07-18T16:20:55Z",
"checked_epoch": 1784391655.3297153
},
"8ec1da73f4fcbc754b93a7ad534b34308dbb3b2235a75c6a787070fd332cfa73": {
"model": "openai-compatible:gateway",
"provider": "omniroute",
"healthy": false,
"reason": "patch_probe_contract_invalid",
"checked_at": "2026-07-18T16:21:52Z",
"checked_epoch": 1784391712.3490422
},
"44f183c901766758ddbb222f6e1f5559c243219bc7077e787bedc71c8e58b636": {
"model": "ollama:ornith",
"provider": "ollama",
"healthy": false,
"reason": "goal_patch_missing",
"checked_at": "2026-07-18T16:21:52Z",
"checked_epoch": 1784391712.3524656
},
"b208348d5436485a8663538b70956752f3497e841af640e25cf107749c5afcc0": {
"model": "ollama:test",
"provider": "ollama",
"healthy": false,
"reason": "patch_probe_contract_invalid",
"checked_at": "2026-07-18T16:21:52Z",
"checked_epoch": 1784391712.3621576
},
"e815d3eac7f27a62a4a217e49c28d456ca789b3a1003f188a788985de068da88": {
"model": "openai:gpt-4o-mini",
"provider": "openai",
"healthy": false,
"reason": "patch_probe_contract_invalid",
"checked_at": "2026-07-18T16:21:52Z",
"checked_epoch": 1784391712.367114
},
"6352a8a41a3ebdd2c9d4c724515838d01993983082d8645025938fd452890d87": {
"model": "openai:gpt-4.1",
"provider": "openai",
"healthy": false,
"reason": "goal_patch_missing",
"checked_at": "2026-07-18T16:21:52Z",
"checked_epoch": 1784391712.3708255
},
"a23aea560f9ebee7ff805d1e4583db4b0e78363095431f0a91a09efa37cff145": {
"model": "openai-compatible:aug/claude-sonnet",
"provider": "omniroute",
"healthy": false,
"reason": "patch_probe_contract_invalid",
"checked_at": "2026-07-18T16:21:52Z",
"checked_epoch": 1784391712.3722892
},
"ce2b1288eeea460eb7c9e01576e04cd30f8ffe2b9a88fe6e59ee527b55ae140e": {
"model": "ollama:worker",
"provider": "ollama",
"healthy": false,
"reason": "patch_probe_contract_invalid",
"checked_at": "2026-07-18T16:21:52Z",
"checked_epoch": 1784391712.5912051
}
}
View File
+158
View File
@@ -371,3 +371,161 @@
{"timestamp":"2026-07-18T08:39:29Z","trace_id":"049a79dc-90bf-4fb6-a08c-133e6fb04a9b","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"5a2d9ab9a60d417a9d53418cc72be21f643025088b2ca76911a946c032ff0679","output_hash":"5a2d9ab9a60d417a9d53418cc72be21f643025088b2ca76911a946c032ff0679"} {"timestamp":"2026-07-18T08:39:29Z","trace_id":"049a79dc-90bf-4fb6-a08c-133e6fb04a9b","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"5a2d9ab9a60d417a9d53418cc72be21f643025088b2ca76911a946c032ff0679","output_hash":"5a2d9ab9a60d417a9d53418cc72be21f643025088b2ca76911a946c032ff0679"}
{"timestamp":"2026-07-18T08:39:55Z","trace_id":"e89746ee-9afa-44b2-bd90-85db0e37731f","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"0aa7555c68e218490b78edd7f665f8dd5d56405720ec6cb4c7be2b34e2baabfa","output_hash":"607dab11dbd23786974b6b2f64daafa2078743f837c722d2f409dad2fbd2adef"} {"timestamp":"2026-07-18T08:39:55Z","trace_id":"e89746ee-9afa-44b2-bd90-85db0e37731f","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"0aa7555c68e218490b78edd7f665f8dd5d56405720ec6cb4c7be2b34e2baabfa","output_hash":"607dab11dbd23786974b6b2f64daafa2078743f837c722d2f409dad2fbd2adef"}
{"timestamp":"2026-07-18T08:39:57Z","trace_id":"36ec47ff-04f9-41e0-94d9-862a380549ea","harness":"H4-security","mode":"output","status":"blocked","action":"block","risk_level":"high","input_hash":"0aa7555c68e218490b78edd7f665f8dd5d56405720ec6cb4c7be2b34e2baabfa","output_hash":"607dab11dbd23786974b6b2f64daafa2078743f837c722d2f409dad2fbd2adef"} {"timestamp":"2026-07-18T08:39:57Z","trace_id":"36ec47ff-04f9-41e0-94d9-862a380549ea","harness":"H4-security","mode":"output","status":"blocked","action":"block","risk_level":"high","input_hash":"0aa7555c68e218490b78edd7f665f8dd5d56405720ec6cb4c7be2b34e2baabfa","output_hash":"607dab11dbd23786974b6b2f64daafa2078743f837c722d2f409dad2fbd2adef"}
{"timestamp":"2026-07-18T10:09:18Z","trace_id":"trace-1784369358-302","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"21a94d09a3c4584be963b86fa294a31ad8303871de74b467ee205d170489fc3c","output_hash":"21a94d09a3c4584be963b86fa294a31ad8303871de74b467ee205d170489fc3c"}
{"timestamp":"2026-07-18T10:09:19Z","trace_id":"trace-1784369359-882","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"d962bb809f87791f22fd5a3e663e08e82646d992f105e01250712d9893f5691a","output_hash":"c001e1f9d3c8f60ca11cc884fe119e01d0611bac7cb0d07e262874a71b317a5e"}
{"timestamp":"2026-07-18T10:09:59Z","trace_id":"trace-1784369399-1558","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"db545408c2e7e7be11a94ff6736fa8db52e9077066c56b8308c34459f2978362","output_hash":"db545408c2e7e7be11a94ff6736fa8db52e9077066c56b8308c34459f2978362"}
{"timestamp":"2026-07-18T10:11:32Z","trace_id":"trace-1784369492-2040","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"6441857790afc3926df927868a07ec153ec8d67adff15f5e3876775b2c96671f","output_hash":"6441857790afc3926df927868a07ec153ec8d67adff15f5e3876775b2c96671f"}
{"timestamp":"2026-07-18T11:17:37Z","trace_id":"trace-1784373457-3116","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"3db6096be1bf9ed65e06f91ce5f2dcf0e8d5e7dc00ed7f1665a94e0e4b7023a6","output_hash":"3db6096be1bf9ed65e06f91ce5f2dcf0e8d5e7dc00ed7f1665a94e0e4b7023a6"}
{"timestamp":"2026-07-18T11:17:37Z","trace_id":"trace-1784373457-3697","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"e8b5ee8e8f7ee2d1d1501e5e5936c17ebbc6a4ff7014af706d0b20b62b2949ea","output_hash":"06d5cad7e425d8d32e05a11ee6587ce518049234499e84caf252684f317a0d5e"}
{"timestamp":"2026-07-18T11:18:39Z","trace_id":"trace-1784373519-4433","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"888719174b5fbcb603419f49c31eea5394e81e53c5a4c8b94e6b8abee1a568b2","output_hash":"888719174b5fbcb603419f49c31eea5394e81e53c5a4c8b94e6b8abee1a568b2"}
{"timestamp":"2026-07-18T11:20:19Z","trace_id":"trace-1784373619-4931","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"571786487c86989de3aa941063d74ecc2040ea81807a3073747bb988ea0c70ea","output_hash":"571786487c86989de3aa941063d74ecc2040ea81807a3073747bb988ea0c70ea"}
{"timestamp":"2026-07-18T11:37:38Z","trace_id":"trace-1784374658-246","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"c29c5a10ead5f18d11b5b8273f36686341c4f7350ff8a989e5d07ec7cf9d5946","output_hash":"c29c5a10ead5f18d11b5b8273f36686341c4f7350ff8a989e5d07ec7cf9d5946"}
{"timestamp":"2026-07-18T11:37:39Z","trace_id":"trace-1784374659-826","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"73df872e3e1e47616c0cc279fd6e27d8cb842477554ae7c2c53ead54cc413980","output_hash":"0eb59ee8fc55d3300acaa4950485f6eede1d5a4251e950151686c23327826428"}
{"timestamp":"2026-07-18T11:39:41Z","trace_id":"trace-1784374781-1539","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"035820c4f12097a7b1cf7824e6707fb78c275607aac75898e5ce81edfd831fb9","output_hash":"035820c4f12097a7b1cf7824e6707fb78c275607aac75898e5ce81edfd831fb9"}
{"timestamp":"2026-07-18T12:19:37Z","trace_id":"trace-1784377177-1784","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"f8eb900cf26466c22669fef89a49d42e22787bf860ca66655a24fd5f9ce31c62","output_hash":"f8eb900cf26466c22669fef89a49d42e22787bf860ca66655a24fd5f9ce31c62"}
{"timestamp":"2026-07-18T12:19:38Z","trace_id":"trace-1784377178-2366","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"73df872e3e1e47616c0cc279fd6e27d8cb842477554ae7c2c53ead54cc413980","output_hash":"0eb59ee8fc55d3300acaa4950485f6eede1d5a4251e950151686c23327826428"}
{"timestamp":"2026-07-18T12:20:41Z","trace_id":"trace-1784377241-3108","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"6736595cf4505f534a6ba1b38cf7bb72c71573c35eb048f84f877750086b4fc0","output_hash":"6736595cf4505f534a6ba1b38cf7bb72c71573c35eb048f84f877750086b4fc0"}
{"timestamp":"2026-07-18T14:19:43Z","trace_id":"trace-1784384383-448","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"f8eb900cf26466c22669fef89a49d42e22787bf860ca66655a24fd5f9ce31c62","output_hash":"f8eb900cf26466c22669fef89a49d42e22787bf860ca66655a24fd5f9ce31c62"}
{"timestamp":"2026-07-18T14:19:44Z","trace_id":"trace-1784384384-1029","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"27b8a71ae30e3e9d2dd04a3a2bc18949587561bf49184170d4cbbc151bb004e7","output_hash":"75813e958e37382304fa3ab1859e351af78cb68f11bfe526923838d34a22f410"}
{"timestamp":"2026-07-18T14:20:43Z","trace_id":"trace-1784384443-1746","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"53daf40f9cb8e48a597c1b14aaaa1ea7cc81daff709cbb8fe7a55e9d64416707","output_hash":"53daf40f9cb8e48a597c1b14aaaa1ea7cc81daff709cbb8fe7a55e9d64416707"}
{"timestamp":"2026-07-18T14:27:29Z","trace_id":"trace-1784384849-314","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"13bb8d002e46be4ad4741f384172f40c3bcbacc0629b4406d58f0c74a3868b56","output_hash":"13bb8d002e46be4ad4741f384172f40c3bcbacc0629b4406d58f0c74a3868b56"}
{"timestamp":"2026-07-18T14:27:29Z","trace_id":"trace-1784384849-895","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"1d5fba6d518205fd4c714fedbfcf1beca3fb266ce3471d2604105071ee7790c6","output_hash":"7573d2b8a3484cf60a62e93a4ff210be151bb98354961b97d00ed6795dea1f84"}
{"timestamp":"2026-07-18T14:28:58Z","trace_id":"trace-1784384938-1686","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"d0a346d08e7a904a0420722920924c7aa6aa35dcf1e4d4f0d53c0c2fd31558a6","output_hash":"d0a346d08e7a904a0420722920924c7aa6aa35dcf1e4d4f0d53c0c2fd31558a6"}
{"timestamp":"2026-07-18T14:39:41Z","trace_id":"trace-1784385581-344","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"4645942c31633c69e905425d80c42fc86f575487bbd032a081bf541d9eae74f0","output_hash":"4645942c31633c69e905425d80c42fc86f575487bbd032a081bf541d9eae74f0"}
{"timestamp":"2026-07-18T14:39:41Z","trace_id":"trace-1784385581-925","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"0b05221919ba3ee3ca96f5ac495f45c555119a4a6e467505561a84e417494525","output_hash":"3549e9d50f28d85b79ee2983b4c87e85ed67f85461a8016b7934c48d97c11071"}
{"timestamp":"2026-07-18T14:40:42Z","trace_id":"trace-1784385642-1588","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"4645942c31633c69e905425d80c42fc86f575487bbd032a081bf541d9eae74f0","output_hash":"4645942c31633c69e905425d80c42fc86f575487bbd032a081bf541d9eae74f0"}
{"timestamp":"2026-07-18T14:40:42Z","trace_id":"trace-1784385642-2170","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"0b05221919ba3ee3ca96f5ac495f45c555119a4a6e467505561a84e417494525","output_hash":"3549e9d50f28d85b79ee2983b4c87e85ed67f85461a8016b7934c48d97c11071"}
{"timestamp":"2026-07-18T14:43:07Z","trace_id":"6a9b9d04-32ad-4490-99b3-95466b2413f1","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"0a7b65ca1bc5dac5251cab6ee3b851d538d734847e50a1437bc7d2b577348df6","output_hash":"0a7b65ca1bc5dac5251cab6ee3b851d538d734847e50a1437bc7d2b577348df6"}
{"timestamp":"2026-07-18T14:43:08Z","trace_id":"cf132b6c-a3c4-407b-a123-57947da281c8","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"2026b9191be5fc04181c4a9a775c7be7368c94e654ebb5e96227076ce9693bbd","output_hash":"2026b9191be5fc04181c4a9a775c7be7368c94e654ebb5e96227076ce9693bbd"}
{"timestamp":"2026-07-18T14:43:09Z","trace_id":"15ceb11b-2889-46bd-bc5f-07de6a88c07e","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"c88e04411941c3a936354426b9502e70af61646e32ef595db538548b341f678a","output_hash":"c88e04411941c3a936354426b9502e70af61646e32ef595db538548b341f678a"}
{"timestamp":"2026-07-18T14:43:10Z","trace_id":"9869ba61-9424-4b68-a8b0-edd8ca22c589","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"21c38bdbce47249b48f52c8a2f12548bd2a20ca859caefcb6fec5688fbef3114","output_hash":"21c38bdbce47249b48f52c8a2f12548bd2a20ca859caefcb6fec5688fbef3114"}
{"timestamp":"2026-07-18T14:43:11Z","trace_id":"45d7a403-a794-4e72-b23c-c73b1c00ff94","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"05d331b33a12215a77e38947ad823dcac7301e870248747cffe422ac164c9e9b","output_hash":"05d331b33a12215a77e38947ad823dcac7301e870248747cffe422ac164c9e9b"}
{"timestamp":"2026-07-18T14:43:12Z","trace_id":"747fceec-62fb-45cf-9c42-f65bea05ff2a","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"d71fc8be325b5429c949372dc2705bf0ff3df55ac91b908fe05c6bf6f4b8a6d1","output_hash":"d71fc8be325b5429c949372dc2705bf0ff3df55ac91b908fe05c6bf6f4b8a6d1"}
{"timestamp":"2026-07-18T14:43:13Z","trace_id":"0e49a838-3b69-4e6f-9288-4845514d2360","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"e45a45f579a0098127d2aba3839e856d6eb9464f8dbbda20f9513f3bb43a0afc","output_hash":"e45a45f579a0098127d2aba3839e856d6eb9464f8dbbda20f9513f3bb43a0afc"}
{"timestamp":"2026-07-18T14:43:15Z","trace_id":"14c9b234-54d8-41a8-8db6-3dba0947dfbe","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"f2d76c459e7ef0ed4cedc3799a0e0b928bc452fa02ac2ae78365a8caf6835c04","output_hash":"f2d76c459e7ef0ed4cedc3799a0e0b928bc452fa02ac2ae78365a8caf6835c04"}
{"timestamp":"2026-07-18T14:43:16Z","trace_id":"9ddebdd0-53b1-40d8-a902-1ef77a2368b5","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"13fc0a22b765b9bb9f0708c8514b01eaad6fba52620a7bd784ffb66c5c833334","output_hash":"13fc0a22b765b9bb9f0708c8514b01eaad6fba52620a7bd784ffb66c5c833334"}
{"timestamp":"2026-07-18T14:43:17Z","trace_id":"062b988f-3ae5-4b3a-a3eb-7a01bbcf49cc","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"39c1928244c578e91ffa5d0c660c7645b450179ad13b7727dfb61036829e7828","output_hash":"39c1928244c578e91ffa5d0c660c7645b450179ad13b7727dfb61036829e7828"}
{"timestamp":"2026-07-18T14:43:18Z","trace_id":"0fb8c866-e1ae-4579-bf52-679b081d3af4","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"d44e8cce83d68f30ae974a55361202da71f8aeb4760c71e39807a751eae59ee7","output_hash":"d44e8cce83d68f30ae974a55361202da71f8aeb4760c71e39807a751eae59ee7"}
{"timestamp":"2026-07-18T14:43:19Z","trace_id":"d64afadb-96f4-4715-90f9-841db89007b2","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"2e7728e6fddd8a38eef2772040d93dfb1b42645b5a937dad6137bbbd6f995b5b","output_hash":"2e7728e6fddd8a38eef2772040d93dfb1b42645b5a937dad6137bbbd6f995b5b"}
{"timestamp":"2026-07-18T14:43:20Z","trace_id":"a267d99d-f4bf-48f6-a6ba-118ce42c9510","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"7c741791a5fae02e9745091b6c1dea70f6828be35eebfaa2709ce93d320856fc","output_hash":"7c741791a5fae02e9745091b6c1dea70f6828be35eebfaa2709ce93d320856fc"}
{"timestamp":"2026-07-18T14:43:22Z","trace_id":"b8666573-7093-44c0-8d36-8ad67f377a8c","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"c12fa684383db79e4d614ef647f32d5593dd82649fddb0b535f47bd29e9e649f","output_hash":"c12fa684383db79e4d614ef647f32d5593dd82649fddb0b535f47bd29e9e649f"}
{"timestamp":"2026-07-18T14:43:23Z","trace_id":"7bf592a4-a98d-4638-905f-fc6aedcebad5","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"6d31ef6f996aef91684a816ce3785b6fd3aeae70f86ff8f232584dadc6648ebf","output_hash":"6d31ef6f996aef91684a816ce3785b6fd3aeae70f86ff8f232584dadc6648ebf"}
{"timestamp":"2026-07-18T14:43:24Z","trace_id":"5bf0ae74-b252-4651-a230-9dd3cf5436a9","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"ce30abd8e84f4bd398f5b8003bb3cb45db4f6a339dfce930399632d0cc9e2d06","output_hash":"ce30abd8e84f4bd398f5b8003bb3cb45db4f6a339dfce930399632d0cc9e2d06"}
{"timestamp":"2026-07-18T14:43:25Z","trace_id":"8551641a-32c9-4756-81e1-d84a9af22500","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"638af166426556c327f6a8c8dd67e78c4d1fee5d5f871641ee2230534c2d2392","output_hash":"638af166426556c327f6a8c8dd67e78c4d1fee5d5f871641ee2230534c2d2392"}
{"timestamp":"2026-07-18T14:43:26Z","trace_id":"91b18fc4-3a14-4484-895d-8d97c28a04a4","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"b7a36fee7ef927b4948d3cb655c62d07ef832d7971bab79f0c58b39fc882c062","output_hash":"b7a36fee7ef927b4948d3cb655c62d07ef832d7971bab79f0c58b39fc882c062"}
{"timestamp":"2026-07-18T14:43:28Z","trace_id":"4712570d-0f19-4e24-aae8-cb9bd7f417ea","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"a6bff2bb626a4279808bbfea106f0ad0332aadf5d97d9dfad6d5d71d52c811c2","output_hash":"a6bff2bb626a4279808bbfea106f0ad0332aadf5d97d9dfad6d5d71d52c811c2"}
{"timestamp":"2026-07-18T14:43:29Z","trace_id":"5e937c7e-6111-4830-ba75-8818a36169df","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"e4c88f6e5737dc0c598fed069fa9a7d9acf2fab998ad9384af99a994ac469b9d","output_hash":"e4c88f6e5737dc0c598fed069fa9a7d9acf2fab998ad9384af99a994ac469b9d"}
{"timestamp":"2026-07-18T14:43:30Z","trace_id":"bff0130f-a1ca-404d-a905-105155b53ebc","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"f6192b27336a5a07fc6f36036e1544f49d18e4f5fb3a72e654eb21ee3bf0a9b0","output_hash":"f6192b27336a5a07fc6f36036e1544f49d18e4f5fb3a72e654eb21ee3bf0a9b0"}
{"timestamp":"2026-07-18T14:43:31Z","trace_id":"51da4bf8-4e3f-47b9-972f-ce401c9a22a5","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"6dd8036fd3966abfd8ffa64ce9d2f0ac258642f1a1327a7d1985a97ad2d101f8","output_hash":"6dd8036fd3966abfd8ffa64ce9d2f0ac258642f1a1327a7d1985a97ad2d101f8"}
{"timestamp":"2026-07-18T14:43:32Z","trace_id":"08dbc24d-bdc0-4ff3-ad9a-f1f94019102f","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"040a3a1b7f383b1b43146c2b417fa40249b230ae9519f5109eb83ad78d89f7bf","output_hash":"040a3a1b7f383b1b43146c2b417fa40249b230ae9519f5109eb83ad78d89f7bf"}
{"timestamp":"2026-07-18T14:43:33Z","trace_id":"dade1894-f205-40c5-9c4b-581bd7304a4f","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"e0f6e13fde69d80953c8714ae47877aa3b1953776b36ba43060115eb0fdcfe5c","output_hash":"e0f6e13fde69d80953c8714ae47877aa3b1953776b36ba43060115eb0fdcfe5c"}
{"timestamp":"2026-07-18T14:43:35Z","trace_id":"574bf6a7-3585-4d89-82c2-c2fad762f816","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"f706189973e7668a4995663642918bfbe02e7ff4af37ef124175218a62ae535e","output_hash":"f706189973e7668a4995663642918bfbe02e7ff4af37ef124175218a62ae535e"}
{"timestamp":"2026-07-18T14:43:36Z","trace_id":"d0d80e67-2791-42cf-a639-2fe233de9673","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"b4c5a413693632cd0549bc52a69491fa4c93dfef428ef3187664afaf86568004","output_hash":"b4c5a413693632cd0549bc52a69491fa4c93dfef428ef3187664afaf86568004"}
{"timestamp":"2026-07-18T14:43:37Z","trace_id":"07b04f14-0386-48bc-b122-d8b4b125d626","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"7a925b78ba31b1a61b4bd4a545459918d32232581dae3ac98dbab9c989db3848","output_hash":"7a925b78ba31b1a61b4bd4a545459918d32232581dae3ac98dbab9c989db3848"}
{"timestamp":"2026-07-18T14:43:38Z","trace_id":"4f9b48a5-53eb-4d06-8802-f4392e8499bf","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"8fe1ea7b9ba035d355a6550958a4bbf9d05e398e45b954824bb6055704400b36","output_hash":"8fe1ea7b9ba035d355a6550958a4bbf9d05e398e45b954824bb6055704400b36"}
{"timestamp":"2026-07-18T14:43:39Z","trace_id":"0774b519-8c87-4c58-8e4b-905353d6f2db","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"5e2381e034d3468bba17fbb1f21e5d3d54b5f1b745a2e574e2033eb0846e41a4","output_hash":"5e2381e034d3468bba17fbb1f21e5d3d54b5f1b745a2e574e2033eb0846e41a4"}
{"timestamp":"2026-07-18T14:43:41Z","trace_id":"f0fd7218-15c8-4925-9235-13f99067ea0a","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"193de17b63b40f174016e36168e04cf11d68c5c2cef1f6685ee59d7857a066cb","output_hash":"193de17b63b40f174016e36168e04cf11d68c5c2cef1f6685ee59d7857a066cb"}
{"timestamp":"2026-07-18T14:43:42Z","trace_id":"d5a1aedf-c687-43e3-a313-e7fa1568bf5c","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"d2aaec4c335231b242328b1bdbc98a5623b486cd4a75b2c41c33420a1c16a70a","output_hash":"d2aaec4c335231b242328b1bdbc98a5623b486cd4a75b2c41c33420a1c16a70a"}
{"timestamp":"2026-07-18T14:43:43Z","trace_id":"5b96b05c-5e6a-4017-9e40-3c13e049e58b","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"5902424959fa129c59b2658d885c599c721d662664a0e7629fdbf23d2be7fa5c","output_hash":"5902424959fa129c59b2658d885c599c721d662664a0e7629fdbf23d2be7fa5c"}
{"timestamp":"2026-07-18T14:43:44Z","trace_id":"f81bdadf-b36a-441d-81c8-b5ba880b2aec","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"504626c964b3729a1e4b618a71e76d11797e90e430f3817b2237566450c87187","output_hash":"504626c964b3729a1e4b618a71e76d11797e90e430f3817b2237566450c87187"}
{"timestamp":"2026-07-18T14:43:46Z","trace_id":"697ee7af-f22c-4075-a2cd-326ca3674462","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"425ef159698de45606c2d1b9ed154574e44bf2155b636af9b467b2f85f1918ae","output_hash":"425ef159698de45606c2d1b9ed154574e44bf2155b636af9b467b2f85f1918ae"}
{"timestamp":"2026-07-18T14:43:47Z","trace_id":"15e57a58-7418-4ea3-ae8c-718b1a02bd16","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"37dfd5e665459e473d5eb2e786e15fabdd0fb663b236e579c481a864a3ac085b","output_hash":"37dfd5e665459e473d5eb2e786e15fabdd0fb663b236e579c481a864a3ac085b"}
{"timestamp":"2026-07-18T14:43:48Z","trace_id":"b4273e93-4bf4-4e79-8117-343bda65454a","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"1c3ea254cfb6fe60fcb3dc1adcd2fd45a8c9fe6b19fec56ea18a431f75b3a5ed","output_hash":"1c3ea254cfb6fe60fcb3dc1adcd2fd45a8c9fe6b19fec56ea18a431f75b3a5ed"}
{"timestamp":"2026-07-18T14:43:49Z","trace_id":"9a91878f-b8cc-4753-9884-cdd92f5169ce","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"80fdac92d5883cd1de34ec3ec8119e137e07db9d45c58ce5aeea598a8ba2c5c2","output_hash":"80fdac92d5883cd1de34ec3ec8119e137e07db9d45c58ce5aeea598a8ba2c5c2"}
{"timestamp":"2026-07-18T14:43:50Z","trace_id":"c719f36f-f708-44da-b6c9-86b38c48bdb0","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"a5d351e56f49bb59c03cd23128b6bbc33fa6298e26d070ce70a8b929454a979a","output_hash":"a5d351e56f49bb59c03cd23128b6bbc33fa6298e26d070ce70a8b929454a979a"}
{"timestamp":"2026-07-18T14:43:51Z","trace_id":"e1531076-3dcd-4481-82c2-bbca76456218","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"98dd1db3eaa2a947125488ec5de9e8544223ff52e5dbac2eeb8457f1658f0d5b","output_hash":"98dd1db3eaa2a947125488ec5de9e8544223ff52e5dbac2eeb8457f1658f0d5b"}
{"timestamp":"2026-07-18T14:43:53Z","trace_id":"ec146914-4d65-4cfb-96fd-2d9be574f68c","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"d5b2719ac405da26f6bed3d590d61cb757b7c296782163073f7f1bad56a4a21f","output_hash":"d5b2719ac405da26f6bed3d590d61cb757b7c296782163073f7f1bad56a4a21f"}
{"timestamp":"2026-07-18T14:43:54Z","trace_id":"8f45a595-977c-4ded-b9d2-ae3f8483b1d8","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"c6cf8c13b061dd09e27af20ffb5f4d2cd3816a99f03ca2ffd6b582fcc0d2d387","output_hash":"c6cf8c13b061dd09e27af20ffb5f4d2cd3816a99f03ca2ffd6b582fcc0d2d387"}
{"timestamp":"2026-07-18T14:43:55Z","trace_id":"3768e10d-d177-4207-b43b-ef1dee211a50","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"8cf91e81f8c88246acb51edd34e23283f2e987a254ffa157660e4d3722066a31","output_hash":"8cf91e81f8c88246acb51edd34e23283f2e987a254ffa157660e4d3722066a31"}
{"timestamp":"2026-07-18T14:43:56Z","trace_id":"06a28b8e-4f69-4ddd-836c-f29f9603895d","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"21c8351a2747dfd79f03a179b5c33ff65c0957720dba7b9ae6779160dd1c4c0c","output_hash":"21c8351a2747dfd79f03a179b5c33ff65c0957720dba7b9ae6779160dd1c4c0c"}
{"timestamp":"2026-07-18T14:43:57Z","trace_id":"1065b7aa-44a2-4d54-940a-f9d3337a30ad","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"9eddc3f87d31e6f985c0e0010bc42975a8ee68c68d123cd3652d45da5c11d2ab","output_hash":"9eddc3f87d31e6f985c0e0010bc42975a8ee68c68d123cd3652d45da5c11d2ab"}
{"timestamp":"2026-07-18T14:43:59Z","trace_id":"95d88d57-f552-4731-9748-3bcff30e60ec","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"4b2eb9ee34011623d69c31aafc7d8910f9df92395b3f717064b6a84a81e3508e","output_hash":"4b2eb9ee34011623d69c31aafc7d8910f9df92395b3f717064b6a84a81e3508e"}
{"timestamp":"2026-07-18T14:44:00Z","trace_id":"995c1c0b-769b-4bb5-902f-fc40f84b6565","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"d81e744b03d2e92f30a9bbb11d1255b94b712b1b5c1ac4ece8054a4cdd0b74e7","output_hash":"d81e744b03d2e92f30a9bbb11d1255b94b712b1b5c1ac4ece8054a4cdd0b74e7"}
{"timestamp":"2026-07-18T14:44:01Z","trace_id":"904f9691-d466-4a81-854a-76baecbaf383","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"08d1390f42f0a9de26f6df55b82845e6bf5ce0227664f1ccef3594267ebdba32","output_hash":"08d1390f42f0a9de26f6df55b82845e6bf5ce0227664f1ccef3594267ebdba32"}
{"timestamp":"2026-07-18T14:43:58Z","trace_id":"acef5f14-cec2-49d6-93d7-f26ee6f70d35","harness":"H4-security","mode":"input","status":"requires_approval","action":"require_approval","risk_level":"high","input_hash":"0b05221919ba3ee3ca96f5ac495f45c555119a4a6e467505561a84e417494525","output_hash":"3549e9d50f28d85b79ee2983b4c87e85ed67f85461a8016b7934c48d97c11071"}
{"timestamp":"2026-07-18T14:44:02Z","trace_id":"379a7f40-bde5-4e91-b88e-de4bfecf2903","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"55e100c7caefcbb9a9a4da56ec4bc0cd97b32d491f65f6ed81e5581fa2a33e11","output_hash":"55e100c7caefcbb9a9a4da56ec4bc0cd97b32d491f65f6ed81e5581fa2a33e11"}
{"timestamp":"2026-07-18T14:44:04Z","trace_id":"182a1c3d-3b00-4bbe-a783-16f55d962e21","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"212f95ec9cd805f6ada1790aefb94cc55a92c3966625bb128ab94ead8dad3dab","output_hash":"212f95ec9cd805f6ada1790aefb94cc55a92c3966625bb128ab94ead8dad3dab"}
{"timestamp":"2026-07-18T14:44:05Z","trace_id":"aec9c579-6e04-43b5-811e-b13a53fc280c","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"97b20409d43d132839f961db78e92fe202cf4b379e630ccf0241375e2efefd31","output_hash":"97b20409d43d132839f961db78e92fe202cf4b379e630ccf0241375e2efefd31"}
{"timestamp":"2026-07-18T14:44:06Z","trace_id":"d0870ec1-ed93-40f3-b893-96bd0408ad1d","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"0d7fb05e444c069a7031ea819398a166fb691834a6402950213f2a0eba3d756d","output_hash":"0d7fb05e444c069a7031ea819398a166fb691834a6402950213f2a0eba3d756d"}
{"timestamp":"2026-07-18T14:44:07Z","trace_id":"4a7c316d-4124-408b-91b2-8ac1163d8f00","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"c80cb439e110fe3bdea13f7415f1a5b6a2a04fd7b2402db7591b7965fa1e6580","output_hash":"c80cb439e110fe3bdea13f7415f1a5b6a2a04fd7b2402db7591b7965fa1e6580"}
{"timestamp":"2026-07-18T14:44:08Z","trace_id":"79d95eec-72f7-42f2-8052-a0a4b8334ba5","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"d434aa811397cf41e5a8b6569411c888958cfce5f930165ee18f32266ecf6216","output_hash":"d434aa811397cf41e5a8b6569411c888958cfce5f930165ee18f32266ecf6216"}
{"timestamp":"2026-07-18T14:44:09Z","trace_id":"5f7288e6-8f83-4c21-a36c-010ba456c5c5","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"78a616322f25b042ac110105dcbca336a501580373c13de849931b470f473bb7","output_hash":"78a616322f25b042ac110105dcbca336a501580373c13de849931b470f473bb7"}
{"timestamp":"2026-07-18T14:44:11Z","trace_id":"f65fdb5c-de90-4f9d-9c68-53bc99e5dabc","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"a6347453d2f75831ab1412f83eae008a2677b34a7612ec86e339b0307ae7f36e","output_hash":"a6347453d2f75831ab1412f83eae008a2677b34a7612ec86e339b0307ae7f36e"}
{"timestamp":"2026-07-18T14:44:12Z","trace_id":"5c54e6a8-0c70-4fd8-897e-4ab48beac983","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"831630985b93cbf5ac4668a4f38409c0740f66062cf39adeb1dbf28fc056bea4","output_hash":"831630985b93cbf5ac4668a4f38409c0740f66062cf39adeb1dbf28fc056bea4"}
{"timestamp":"2026-07-18T14:44:13Z","trace_id":"97bad108-0f7e-4395-91b8-f808339e5059","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"4dbe3a992d1ef74c09c513cfd7e467cd0cd8503899648cc2360f59237dd767d1","output_hash":"4dbe3a992d1ef74c09c513cfd7e467cd0cd8503899648cc2360f59237dd767d1"}
{"timestamp":"2026-07-18T14:44:14Z","trace_id":"63ef51eb-4999-4fe0-94c1-f55e9822f0fb","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"ebdd68c4a94b4a63c3417c6f0a8de064e5bff9cf1811af5d198ef20ea169410a","output_hash":"ebdd68c4a94b4a63c3417c6f0a8de064e5bff9cf1811af5d198ef20ea169410a"}
{"timestamp":"2026-07-18T14:44:15Z","trace_id":"70d62887-023b-4a82-b832-f68d866f7870","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"ad218c93b7a0e3293148f25b70566e85deeb5663d364fc08bd053319b2c568cc","output_hash":"ad218c93b7a0e3293148f25b70566e85deeb5663d364fc08bd053319b2c568cc"}
{"timestamp":"2026-07-18T14:44:16Z","trace_id":"f0353064-013b-4d06-84b0-a87a8557dc2c","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"738c0418b325ff732a3c50e76888831a2981a685bd885ebfee07ea65c83fa691","output_hash":"738c0418b325ff732a3c50e76888831a2981a685bd885ebfee07ea65c83fa691"}
{"timestamp":"2026-07-18T14:44:18Z","trace_id":"f9e32fd2-0411-4ff3-8e1e-f8a52e74f4ea","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"fc797bb71e36678f32c64df3dbc0a8547ac3c067d3aa2f992c7d45ee0e31c7fa","output_hash":"fc797bb71e36678f32c64df3dbc0a8547ac3c067d3aa2f992c7d45ee0e31c7fa"}
{"timestamp":"2026-07-18T14:44:19Z","trace_id":"b66dd74f-e0b0-40a5-8189-7bf2216cea9d","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"74824e277be32ff2b29bf5e1d410299cc3d119b662b5c9850efd37a4d6bb8858","output_hash":"74824e277be32ff2b29bf5e1d410299cc3d119b662b5c9850efd37a4d6bb8858"}
{"timestamp":"2026-07-18T14:44:20Z","trace_id":"21c8b7e8-25f7-4316-a346-cd85ee617f68","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"a3c9f4dcf55edae7b55c9e4728974f6a7cbd825a2c4cf2b6128bf63868108989","output_hash":"a3c9f4dcf55edae7b55c9e4728974f6a7cbd825a2c4cf2b6128bf63868108989"}
{"timestamp":"2026-07-18T14:44:21Z","trace_id":"a34d1f6e-140c-4c80-93ca-b0cdedef331d","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"df4cfaa6a5b068441b8638000c471b2092379d900aa0960cc6342d8b45e3f3bb","output_hash":"df4cfaa6a5b068441b8638000c471b2092379d900aa0960cc6342d8b45e3f3bb"}
{"timestamp":"2026-07-18T14:44:22Z","trace_id":"9ed59c36-51fe-4726-b37c-df82e3046f09","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"e2c02e0cf356f0a94d543b02d772f6336f8cb6a06ba4d17f975fabab6428e588","output_hash":"e2c02e0cf356f0a94d543b02d772f6336f8cb6a06ba4d17f975fabab6428e588"}
{"timestamp":"2026-07-18T14:44:23Z","trace_id":"99443a73-fb98-40d0-81dc-ba581dc997d4","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"abd7b98c2761bad1c2bba17b691b9a6554045a65ca6a7eacd8ddc6e357e949c5","output_hash":"abd7b98c2761bad1c2bba17b691b9a6554045a65ca6a7eacd8ddc6e357e949c5"}
{"timestamp":"2026-07-18T14:44:25Z","trace_id":"6557334e-720e-4cb9-bbbf-10bf01e87aa1","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"85a9ac82a520b14a0f597938ae4897d93ec276316ce462f2b93a01e58fcd12e5","output_hash":"85a9ac82a520b14a0f597938ae4897d93ec276316ce462f2b93a01e58fcd12e5"}
{"timestamp":"2026-07-18T14:44:26Z","trace_id":"7a6d0be0-8ce4-4a95-a020-3433dac3b6d0","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"12e4d2821052ffcd950756b3c32573a26eb173e8d830cbeb208c14fef256227c","output_hash":"12e4d2821052ffcd950756b3c32573a26eb173e8d830cbeb208c14fef256227c"}
{"timestamp":"2026-07-18T14:44:27Z","trace_id":"940b003e-68f6-4fc0-ad2a-6c591f4c1678","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"80fdac92d5883cd1de34ec3ec8119e137e07db9d45c58ce5aeea598a8ba2c5c2","output_hash":"80fdac92d5883cd1de34ec3ec8119e137e07db9d45c58ce5aeea598a8ba2c5c2"}
{"timestamp":"2026-07-18T14:44:28Z","trace_id":"c3c14d00-cac5-480e-ad9b-4a7f3bdb8d6c","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"9d9cb0cf39a6ed22dbc0b42deb99de7c0e530e4fc31d94307aac1959ffca0598","output_hash":"9d9cb0cf39a6ed22dbc0b42deb99de7c0e530e4fc31d94307aac1959ffca0598"}
{"timestamp":"2026-07-18T14:44:30Z","trace_id":"367ce453-1921-4189-b6ed-76ca80fe3027","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"617a887bb41bebcb1ec4501fc82c47f46381da454a4e8eab7fc48538bc111e1f","output_hash":"617a887bb41bebcb1ec4501fc82c47f46381da454a4e8eab7fc48538bc111e1f"}
{"timestamp":"2026-07-18T14:44:31Z","trace_id":"f4ece33c-f1c4-4d0b-8627-1219bb30cd99","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"1ff7da3eca8123cbcb12ec519fd4e4f35f96c469c67d36dd84595ac969934c0f","output_hash":"1ff7da3eca8123cbcb12ec519fd4e4f35f96c469c67d36dd84595ac969934c0f"}
{"timestamp":"2026-07-18T14:44:32Z","trace_id":"9c7a9c22-fa08-48b3-8a90-54f96940e29d","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"208dcb8dc5e487a413439771b559e875440e9728e0a920f88c84c91fd08ed16e","output_hash":"208dcb8dc5e487a413439771b559e875440e9728e0a920f88c84c91fd08ed16e"}
{"timestamp":"2026-07-18T14:44:34Z","trace_id":"c67c06e2-4913-4e1a-8d3d-2430fefaf119","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"adf40d8b70a78805b061db2f1d4a10a10f8f6ffc0550865a73eaf100c48e33ce","output_hash":"adf40d8b70a78805b061db2f1d4a10a10f8f6ffc0550865a73eaf100c48e33ce"}
{"timestamp":"2026-07-18T14:44:35Z","trace_id":"3501fd9b-69cd-4894-b628-7a7db25dbd7a","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"f2d66338e3550fcc527af0f6aa4475eca026b4538d2663a5512ce21bcf62ec14","output_hash":"f2d66338e3550fcc527af0f6aa4475eca026b4538d2663a5512ce21bcf62ec14"}
{"timestamp":"2026-07-18T14:44:36Z","trace_id":"22802ef7-86b0-4261-be0e-3bda72f47801","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"68cf6726e1f23910eae350fbdac88e124c1553a2e557f1d43ff2a03486b94633","output_hash":"68cf6726e1f23910eae350fbdac88e124c1553a2e557f1d43ff2a03486b94633"}
{"timestamp":"2026-07-18T14:44:37Z","trace_id":"cb6e348d-7746-463f-b745-296a155c651f","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"11f1d3168daa61cfb1195bb8aed22b922bd13ccbbdeeec6cc5512b46e10ffedb","output_hash":"11f1d3168daa61cfb1195bb8aed22b922bd13ccbbdeeec6cc5512b46e10ffedb"}
{"timestamp":"2026-07-18T14:44:38Z","trace_id":"902fe32c-a669-455d-a050-f31ab891d812","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"a42b8d76d2bb6169c5ac5966b102d61b736612b2cae7b75d31ed7ba5e1969e3e","output_hash":"a42b8d76d2bb6169c5ac5966b102d61b736612b2cae7b75d31ed7ba5e1969e3e"}
{"timestamp":"2026-07-18T14:44:40Z","trace_id":"d81f2ac9-7d1e-4656-9ae1-7519296bb171","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"0324cae2f82b85eb3ea0b14b6552c806691ed709f5d39b7a869bef1d1f025d8e","output_hash":"0324cae2f82b85eb3ea0b14b6552c806691ed709f5d39b7a869bef1d1f025d8e"}
{"timestamp":"2026-07-18T14:44:41Z","trace_id":"84154b23-173e-456e-b665-ec0a881e6ad2","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"ac8e74508885448754819cf20380a866a4ad6db5c81a6f7fa86f9cf2a0b11588","output_hash":"ac8e74508885448754819cf20380a866a4ad6db5c81a6f7fa86f9cf2a0b11588"}
{"timestamp":"2026-07-18T14:44:42Z","trace_id":"d8e295d3-f99c-4dc5-b145-df82b4513ff0","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"1c300df66c8f200877df867c467c7c463b4945ffb9759058e26d06eac24b227e","output_hash":"1c300df66c8f200877df867c467c7c463b4945ffb9759058e26d06eac24b227e"}
{"timestamp":"2026-07-18T14:44:43Z","trace_id":"dcbc7d4f-5d7b-47f3-a5e8-404dbc4d5ee4","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"6bc4dae2df731fe9ade3a4110185ce8a10b837bb5a2c28888e55be15f74f02ba","output_hash":"6bc4dae2df731fe9ade3a4110185ce8a10b837bb5a2c28888e55be15f74f02ba"}
{"timestamp":"2026-07-18T14:44:44Z","trace_id":"d9a1280e-abde-4bf3-985d-dd3471bea7f0","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"cdf23170afe2506e48af8411b2b43e659acf0d327853342f515879d8ae609a52","output_hash":"cdf23170afe2506e48af8411b2b43e659acf0d327853342f515879d8ae609a52"}
{"timestamp":"2026-07-18T14:44:45Z","trace_id":"66993a92-9dc1-4de4-9a6c-8c2c2ac8efb2","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"630d23ff16317dcfc292c751b39577d35a319099204663b96e01759ba7ce8793","output_hash":"630d23ff16317dcfc292c751b39577d35a319099204663b96e01759ba7ce8793"}
{"timestamp":"2026-07-18T14:44:47Z","trace_id":"299be129-4e9b-4857-8933-70c767d8dba1","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"12770e3923fa48c66a0ce11864489cc8b4bac855d727ad2c9b03a168f194f692","output_hash":"12770e3923fa48c66a0ce11864489cc8b4bac855d727ad2c9b03a168f194f692"}
{"timestamp":"2026-07-18T14:44:48Z","trace_id":"c04b178f-dac6-43f8-be22-a0bc998f021a","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"0b9934cfabf40839989255f5b5ed29b8bba72453baebd685a09b4faca5f299b8","output_hash":"0b9934cfabf40839989255f5b5ed29b8bba72453baebd685a09b4faca5f299b8"}
{"timestamp":"2026-07-18T14:44:49Z","trace_id":"0d8b18c7-8646-42f1-933e-53a3e983b92d","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"ce1f8c05b6f5feb51ac7fa1dc583171d08cb22c089ead8722ca87fb03b6f77ec","output_hash":"ce1f8c05b6f5feb51ac7fa1dc583171d08cb22c089ead8722ca87fb03b6f77ec"}
{"timestamp":"2026-07-18T14:44:50Z","trace_id":"536e170a-357a-4f0b-902c-2b3182219303","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"30b40a2bebc735bdbf15e8a03abfb7df1e7b2032d278fa4422ae400390604ce1","output_hash":"30b40a2bebc735bdbf15e8a03abfb7df1e7b2032d278fa4422ae400390604ce1"}
{"timestamp":"2026-07-18T14:44:51Z","trace_id":"a52076e9-0a94-4a1f-a744-d7cb28880d9c","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"a1569f0fbb8154cdc2d30f981f5f3e59e03506b73b51bb95c789594218b9886b","output_hash":"a1569f0fbb8154cdc2d30f981f5f3e59e03506b73b51bb95c789594218b9886b"}
{"timestamp":"2026-07-18T14:44:52Z","trace_id":"f1127660-89e4-4e89-b642-17b57ad57c55","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"ba30fbe92c8db447c5409ab9650f6507c8244e968174c839227f1e05f4b5fbcb","output_hash":"ba30fbe92c8db447c5409ab9650f6507c8244e968174c839227f1e05f4b5fbcb"}
{"timestamp":"2026-07-18T14:44:54Z","trace_id":"29670b22-6eaa-4176-a64e-3fc3204871b5","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"4488f4bed0c7d5b1c768af626c5d3d646d05ec29e4050aca74d218d258158fa0","output_hash":"4488f4bed0c7d5b1c768af626c5d3d646d05ec29e4050aca74d218d258158fa0"}
{"timestamp":"2026-07-18T14:44:55Z","trace_id":"d6156e1f-758f-47ff-9fe3-6affec1d4480","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"5fcace13b6cd3e5a09bf3485eb37d91f1afe58ebc6e64d6aa5d91eae4e1fafe1","output_hash":"5fcace13b6cd3e5a09bf3485eb37d91f1afe58ebc6e64d6aa5d91eae4e1fafe1"}
{"timestamp":"2026-07-18T14:44:56Z","trace_id":"6e1963dc-5f49-4904-9842-369d7bd2f64f","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"2f33f36d70cfea74bc9c6dc2a3c616359a01dee94f33447de28465b92c7076ba","output_hash":"2f33f36d70cfea74bc9c6dc2a3c616359a01dee94f33447de28465b92c7076ba"}
{"timestamp":"2026-07-18T14:44:57Z","trace_id":"1c0cbf41-b4cd-448b-86d3-bf4e7c2174eb","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"7a9153687f53bb8392e89e4087ae9d8a1a31a2ccb6a561e0e8b46e0236607400","output_hash":"7a9153687f53bb8392e89e4087ae9d8a1a31a2ccb6a561e0e8b46e0236607400"}
{"timestamp":"2026-07-18T14:44:58Z","trace_id":"9855185b-a0b2-439e-b254-0e71ceb92706","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"7d6b48a5d6352f88f4eb27313dc4c26222dadd9529c1f368320935831fb4725a","output_hash":"7d6b48a5d6352f88f4eb27313dc4c26222dadd9529c1f368320935831fb4725a"}
{"timestamp":"2026-07-18T14:44:59Z","trace_id":"00a8eb32-1ef0-4617-ad48-586e9c1cb1f0","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"73ef5f2ab1806fc9ce6ced82ed72fbd1cb7d968845a63380269f3bfe3e2be7b0","output_hash":"73ef5f2ab1806fc9ce6ced82ed72fbd1cb7d968845a63380269f3bfe3e2be7b0"}
{"timestamp":"2026-07-18T14:45:01Z","trace_id":"92e7cd5d-c77e-42fd-929e-d2a93f8753b2","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"cfaa408e081dab46a9970fc52d742d7f169dec36e5ddb063236b3857526c1348","output_hash":"cfaa408e081dab46a9970fc52d742d7f169dec36e5ddb063236b3857526c1348"}
{"timestamp":"2026-07-18T14:45:02Z","trace_id":"64352a28-e5e3-47c8-be82-fd6cf710aeb1","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"14688e49977dbd42157ab19902cce7b5919dd1535254220f46a17259abf7a5b0","output_hash":"14688e49977dbd42157ab19902cce7b5919dd1535254220f46a17259abf7a5b0"}
{"timestamp":"2026-07-18T14:45:03Z","trace_id":"cd4763be-ae76-4d95-b6de-62f676aa61a1","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"4a49e3c9391cd7ffd92ece98f81cd0b3332afff79b654a0a0fc17be20efda151","output_hash":"4a49e3c9391cd7ffd92ece98f81cd0b3332afff79b654a0a0fc17be20efda151"}
{"timestamp":"2026-07-18T14:45:04Z","trace_id":"d4344025-3ae9-4a3b-b97f-b1635ce6503d","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"6d722e76fa3ee546c756e5a49d8e78c9775e4adb4d8067dd7b9056541c940264","output_hash":"6d722e76fa3ee546c756e5a49d8e78c9775e4adb4d8067dd7b9056541c940264"}
{"timestamp":"2026-07-18T14:45:05Z","trace_id":"d9b7d15e-d4e8-4578-be80-5100a4d3c0d5","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"50effbf62a46f5c0e43f7cd3d719ca8862c14df14b8f62d84fa27bed65936d7e","output_hash":"50effbf62a46f5c0e43f7cd3d719ca8862c14df14b8f62d84fa27bed65936d7e"}
{"timestamp":"2026-07-18T14:45:06Z","trace_id":"03126304-9e6b-4ec1-a3c8-4dd0786f4cdc","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"96266ad18d0a23862ced0d564a1034ee1852c73c5cfbde37039b09addeeb69fe","output_hash":"96266ad18d0a23862ced0d564a1034ee1852c73c5cfbde37039b09addeeb69fe"}
{"timestamp":"2026-07-18T14:45:08Z","trace_id":"b5891651-0d47-49a5-ad7b-94ad522ec62e","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"032c309b1d6d64165bd5b012c3a0bcc144f237c6a532d0348c12acbc92f3cd0a","output_hash":"032c309b1d6d64165bd5b012c3a0bcc144f237c6a532d0348c12acbc92f3cd0a"}
{"timestamp":"2026-07-18T14:45:09Z","trace_id":"8d4b7892-6a39-4e90-8a82-216b17bbccba","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"c98e80fdced75d468f38ac6d6fbf87579f192dc8222608b0da6d488c30de49b0","output_hash":"c98e80fdced75d468f38ac6d6fbf87579f192dc8222608b0da6d488c30de49b0"}
{"timestamp":"2026-07-18T14:45:10Z","trace_id":"trace-1784385910-60","harness":"H4-security","mode":"input","status":"requires_approval","action":"require_approval","risk_level":"high","input_hash":"0b05221919ba3ee3ca96f5ac495f45c555119a4a6e467505561a84e417494525","output_hash":"3549e9d50f28d85b79ee2983b4c87e85ed67f85461a8016b7934c48d97c11071"}
{"timestamp":"2026-07-18T14:45:10Z","trace_id":"f8ef40ca-f461-4cd9-87f0-6aab0895c2fb","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"44ba2461b13b30f6264a8c8a63714b3ac96153797fe2267b5b51f82a189cdd69","output_hash":"44ba2461b13b30f6264a8c8a63714b3ac96153797fe2267b5b51f82a189cdd69"}
{"timestamp":"2026-07-18T14:45:11Z","trace_id":"d781086e-1fcc-41de-804c-b7e286032e8d","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"6153fdff9486da5234136acf9c7a041456594ece67b9710269df44a83e83d6da","output_hash":"6153fdff9486da5234136acf9c7a041456594ece67b9710269df44a83e83d6da"}
{"timestamp":"2026-07-18T14:45:12Z","trace_id":"d4d39e1a-9bc8-419e-8fab-aa94bde09c53","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"e5add3e7298ce51b5ad5a0e67449963023bbdc2e5f1ec99107fe40ecd382c7bf","output_hash":"e5add3e7298ce51b5ad5a0e67449963023bbdc2e5f1ec99107fe40ecd382c7bf"}
{"timestamp":"2026-07-18T14:45:14Z","trace_id":"be6b32e8-e004-4525-aa81-821f75183176","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"6c8c3b94661076800e9ee8ef67959c3dfebc15be4f233227da301a8bd5e6d533","output_hash":"6c8c3b94661076800e9ee8ef67959c3dfebc15be4f233227da301a8bd5e6d533"}
{"timestamp":"2026-07-18T14:45:15Z","trace_id":"052cca95-7db8-4b6b-a540-e066822e0397","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"e620c3cc8fee0dfd8766f1af294f4f5b91656c4fb6df2d6d93dea550c5c7a873","output_hash":"e620c3cc8fee0dfd8766f1af294f4f5b91656c4fb6df2d6d93dea550c5c7a873"}
{"timestamp":"2026-07-18T14:45:16Z","trace_id":"0a85149b-d856-4909-a526-67816ff8228e","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"4d6111fbf21f5d06793fc55cf9ba45264255af6d9390b94743bf5345effb8844","output_hash":"4d6111fbf21f5d06793fc55cf9ba45264255af6d9390b94743bf5345effb8844"}
{"timestamp":"2026-07-18T14:45:17Z","trace_id":"3f82d234-43b3-493e-9118-091bd8a261a2","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"b30d2f2ac355eba557710d0f8fc4b02d99f9be976f2d8be544dd64b48099e48e","output_hash":"b30d2f2ac355eba557710d0f8fc4b02d99f9be976f2d8be544dd64b48099e48e"}
{"timestamp":"2026-07-18T15:07:33Z","trace_id":"trace-1784387253-1386","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"2788c9203adce8673587daf8c6b77d8e0773c4a75980fe569ec4549255a6e622","output_hash":"2788c9203adce8673587daf8c6b77d8e0773c4a75980fe569ec4549255a6e622"}
{"timestamp":"2026-07-18T15:07:33Z","trace_id":"trace-1784387253-1927","harness":"H4-security","mode":"input","status":"requires_approval","action":"require_approval","risk_level":"high","input_hash":"0b05221919ba3ee3ca96f5ac495f45c555119a4a6e467505561a84e417494525","output_hash":"3549e9d50f28d85b79ee2983b4c87e85ed67f85461a8016b7934c48d97c11071"}
{"timestamp":"2026-07-18T15:09:12Z","trace_id":"trace-1784387352-2699","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"347c4a4eb327661cb0922068f6d6d3dc77afa018a43c3948d343eff49a97d6a3","output_hash":"347c4a4eb327661cb0922068f6d6d3dc77afa018a43c3948d343eff49a97d6a3"}
{"timestamp":"2026-07-18T15:23:13Z","trace_id":"6a1baf39-7195-4713-8fa0-1cbf742f23d0","harness":"H4-security","mode":"input","status":"requires_approval","action":"require_approval","risk_level":"high","input_hash":"f0fef5b4f847b86e471f2c2a7d82cb6730d0031c3b93821e94aedecf4fc1c258","output_hash":"62f58556df3b1e8d46c614c979f3941c79a5d70f63173b88f803585f7c40b0c7"}
{"timestamp":"2026-07-18T15:28:18Z","trace_id":"trace-1784388498-311","harness":"H4-security","mode":"input","status":"requires_approval","action":"require_approval","risk_level":"high","input_hash":"f0fef5b4f847b86e471f2c2a7d82cb6730d0031c3b93821e94aedecf4fc1c258","output_hash":"62f58556df3b1e8d46c614c979f3941c79a5d70f63173b88f803585f7c40b0c7"}
{"timestamp":"2026-07-18T15:38:46Z","trace_id":"trace-1784389126-45","harness":"H4-security","mode":"input","status":"requires_approval","action":"require_approval","risk_level":"high","input_hash":"f0fef5b4f847b86e471f2c2a7d82cb6730d0031c3b93821e94aedecf4fc1c258","output_hash":"62f58556df3b1e8d46c614c979f3941c79a5d70f63173b88f803585f7c40b0c7"}
{"timestamp":"2026-07-18T15:41:35Z","trace_id":"trace-1784389295-189","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"2788c9203adce8673587daf8c6b77d8e0773c4a75980fe569ec4549255a6e622","output_hash":"2788c9203adce8673587daf8c6b77d8e0773c4a75980fe569ec4549255a6e622"}
{"timestamp":"2026-07-18T15:41:36Z","trace_id":"trace-1784389296-719","harness":"H4-security","mode":"input","status":"requires_approval","action":"require_approval","risk_level":"high","input_hash":"f0fef5b4f847b86e471f2c2a7d82cb6730d0031c3b93821e94aedecf4fc1c258","output_hash":"62f58556df3b1e8d46c614c979f3941c79a5d70f63173b88f803585f7c40b0c7"}
{"timestamp":"2026-07-18T15:42:50Z","trace_id":"trace-1784389370-1333","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"80f6847cb2f1783d23105c1c27890ca6dfd602e1b24e478bce132ea5cd89e430","output_hash":"36f364a676a29e19322f351e7fafc6a39d03795c62ef76cc825054d02e003431"}
{"timestamp":"2026-07-18T15:43:04Z","trace_id":"trace-1784389384-1511","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"b88ace3a8bab6194be459ef31ac5d541e3452c95420e7216765a390e7a05decc","output_hash":"b88ace3a8bab6194be459ef31ac5d541e3452c95420e7216765a390e7a05decc"}
{"timestamp":"2026-07-18T15:43:14Z","trace_id":"trace-1784389394-1649","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"c5e5de6b6429720738fe8a84df76cdde515a90512f579d0d0163afb26cd3a48a","output_hash":"c5e5de6b6429720738fe8a84df76cdde515a90512f579d0d0163afb26cd3a48a"}
{"timestamp":"2026-07-18T16:01:48Z","trace_id":"trace-1784390508-597","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"2788c9203adce8673587daf8c6b77d8e0773c4a75980fe569ec4549255a6e622","output_hash":"2788c9203adce8673587daf8c6b77d8e0773c4a75980fe569ec4549255a6e622"}
{"timestamp":"2026-07-18T16:01:48Z","trace_id":"trace-1784390508-1139","harness":"H4-security","mode":"input","status":"requires_approval","action":"require_approval","risk_level":"high","input_hash":"f0fef5b4f847b86e471f2c2a7d82cb6730d0031c3b93821e94aedecf4fc1c258","output_hash":"62f58556df3b1e8d46c614c979f3941c79a5d70f63173b88f803585f7c40b0c7"}
{"timestamp":"2026-07-18T16:02:02Z","trace_id":"trace-1784390522-1719","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"1beee06bff072daf1459b7ea47128910b4027c1ace52af8ebfe68926942b5952","output_hash":"1beee06bff072daf1459b7ea47128910b4027c1ace52af8ebfe68926942b5952"}
{"timestamp":"2026-07-18T16:14:12Z","trace_id":"trace-1784391252-127","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"2788c9203adce8673587daf8c6b77d8e0773c4a75980fe569ec4549255a6e622","output_hash":"2788c9203adce8673587daf8c6b77d8e0773c4a75980fe569ec4549255a6e622"}
{"timestamp":"2026-07-18T16:14:13Z","trace_id":"trace-1784391253-657","harness":"H4-security","mode":"input","status":"requires_approval","action":"require_approval","risk_level":"high","input_hash":"f0fef5b4f847b86e471f2c2a7d82cb6730d0031c3b93821e94aedecf4fc1c258","output_hash":"62f58556df3b1e8d46c614c979f3941c79a5d70f63173b88f803585f7c40b0c7"}
{"timestamp":"2026-07-18T16:14:27Z","trace_id":"trace-1784391267-1217","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"b464883daf70f5be6a94dffb8f5cb769fe4fbaf6fff5b3d2019e5c00b42cd7b7","output_hash":"744599bc48bbef1a967a8db392abb9df0dc7c271df9bbefe412ea87c1c42beb2"}
{"timestamp":"2026-07-18T16:14:42Z","trace_id":"trace-1784391282-1344","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"83f86d8719a69d526e8de61a676e84bbb9d06da5863ca955309dd11720860ba2","output_hash":"83f86d8719a69d526e8de61a676e84bbb9d06da5863ca955309dd11720860ba2"}
{"timestamp":"2026-07-18T16:20:13Z","trace_id":"trace-1784391613-121","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"2788c9203adce8673587daf8c6b77d8e0773c4a75980fe569ec4549255a6e622","output_hash":"2788c9203adce8673587daf8c6b77d8e0773c4a75980fe569ec4549255a6e622"}
{"timestamp":"2026-07-18T16:20:13Z","trace_id":"trace-1784391613-651","harness":"H4-security","mode":"input","status":"requires_approval","action":"require_approval","risk_level":"high","input_hash":"f0fef5b4f847b86e471f2c2a7d82cb6730d0031c3b93821e94aedecf4fc1c258","output_hash":"62f58556df3b1e8d46c614c979f3941c79a5d70f63173b88f803585f7c40b0c7"}
{"timestamp":"2026-07-18T16:20:24Z","trace_id":"trace-1784391624-1178","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"ebb3132eaf20051de7e2ae12c8fb1501a9e00cdde3fb98616689e2c76482567d","output_hash":"ebb3132eaf20051de7e2ae12c8fb1501a9e00cdde3fb98616689e2c76482567d"}
+5
View File
@@ -56,3 +56,8 @@
{"timestamp":"2026-07-18T08:39:21Z","trace_id":"7c090b63-83a9-47fe-b1b1-c93387a98575","harness":"H6-agentops","agent":"speckit.implement","step":"13b-implement-attempt-2","status":"success","exit_code":0,"latency_ms":366,"retry_count":0,"input_tokens":14,"output_tokens":33,"total_tokens":47,"cost_estimate":9.4e-05,"cost_source":"word_count_estimate","hallucination_signals":0,"alerts":[],"input_hash":"c1302d67a5f8cdcd30aa22955100b3e20a5a3a3e3d15a82011c6bca60cc6638a","output_hash":"fe991db26a3995db8b60dc223a52645a7ee8c8b13d61793e5b62bad7ee5f37b8"} {"timestamp":"2026-07-18T08:39:21Z","trace_id":"7c090b63-83a9-47fe-b1b1-c93387a98575","harness":"H6-agentops","agent":"speckit.implement","step":"13b-implement-attempt-2","status":"success","exit_code":0,"latency_ms":366,"retry_count":0,"input_tokens":14,"output_tokens":33,"total_tokens":47,"cost_estimate":9.4e-05,"cost_source":"word_count_estimate","hallucination_signals":0,"alerts":[],"input_hash":"c1302d67a5f8cdcd30aa22955100b3e20a5a3a3e3d15a82011c6bca60cc6638a","output_hash":"fe991db26a3995db8b60dc223a52645a7ee8c8b13d61793e5b62bad7ee5f37b8"}
{"timestamp":"2026-07-18T08:39:26Z","trace_id":"13c63a9d-f38b-4228-aeaa-3811531daafd","harness":"H6-agentops","agent":"casan.reviewcode","step":"13c-reviewcode-attempt-2","status":"success","exit_code":0,"latency_ms":302,"retry_count":0,"input_tokens":14,"output_tokens":52,"total_tokens":66,"cost_estimate":0.000132,"cost_source":"word_count_estimate","hallucination_signals":0,"alerts":[],"input_hash":"5b8f3df70fd50957b746752b8043d4be1607f4f3ff764bdcea8bbee886c8c07b","output_hash":"9e78849db5e9e9c4cb63e8233c9f9efd9a2a315d7ed6852d11f7f1b51aa93fe5"} {"timestamp":"2026-07-18T08:39:26Z","trace_id":"13c63a9d-f38b-4228-aeaa-3811531daafd","harness":"H6-agentops","agent":"casan.reviewcode","step":"13c-reviewcode-attempt-2","status":"success","exit_code":0,"latency_ms":302,"retry_count":0,"input_tokens":14,"output_tokens":52,"total_tokens":66,"cost_estimate":0.000132,"cost_source":"word_count_estimate","hallucination_signals":0,"alerts":[],"input_hash":"5b8f3df70fd50957b746752b8043d4be1607f4f3ff764bdcea8bbee886c8c07b","output_hash":"9e78849db5e9e9c4cb63e8233c9f9efd9a2a315d7ed6852d11f7f1b51aa93fe5"}
{"timestamp":"2026-07-18T08:39:31Z","trace_id":"1e730cab-c0be-4167-968d-9c07001ded27","harness":"H6-agentops","agent":"casan.testkit run-tests","step":"14-runtests","status":"success","exit_code":0,"latency_ms":23824,"retry_count":0,"input_tokens":15,"output_tokens":955,"total_tokens":970,"cost_estimate":0.00194,"cost_source":"word_count_estimate","hallucination_signals":0,"alerts":["high-latency"],"input_hash":"fe022699a0ecbe27b9b1276d469271cf6fbe25e3080e74774eacb8d7eef14d76","output_hash":"ce2cca9568be33c3d30db21b51359f626e1bfa762d01a4734551ce5e6cfe4563"} {"timestamp":"2026-07-18T08:39:31Z","trace_id":"1e730cab-c0be-4167-968d-9c07001ded27","harness":"H6-agentops","agent":"casan.testkit run-tests","step":"14-runtests","status":"success","exit_code":0,"latency_ms":23824,"retry_count":0,"input_tokens":15,"output_tokens":955,"total_tokens":970,"cost_estimate":0.00194,"cost_source":"word_count_estimate","hallucination_signals":0,"alerts":["high-latency"],"input_hash":"fe022699a0ecbe27b9b1276d469271cf6fbe25e3080e74774eacb8d7eef14d76","output_hash":"ce2cca9568be33c3d30db21b51359f626e1bfa762d01a4734551ce5e6cfe4563"}
{"timestamp": "2026-07-18T10:11:32Z", "trace_id": "f357ad57-0856-47a6-ad41-1c31ffd05151", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "success", "exit_code": 0, "latency_ms": 133660, "input_tokens": 7412, "output_tokens": 1605, "total_tokens": 9017, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "21a94d09a3c4584be963b86fa294a31ad8303871de74b467ee205d170489fc3c", "output_hash": "6441857790afc3926df927868a07ec153ec8d67adff15f5e3876775b2c96671f"}
{"timestamp": "2026-07-18T11:20:19Z", "trace_id": "c8dee8e4-cca3-434d-b87e-d2dd2cf92c22", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "success", "exit_code": 0, "latency_ms": 162448, "input_tokens": 6437, "output_tokens": 2109, "total_tokens": 8546, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "3db6096be1bf9ed65e06f91ce5f2dcf0e8d5e7dc00ed7f1665a94e0e4b7023a6", "output_hash": "571786487c86989de3aa941063d74ecc2040ea81807a3073747bb988ea0c70ea"}
{"timestamp": "2026-07-18T14:31:28Z", "trace_id": "0a95a682-d6f9-4380-b744-e7331c94afeb", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "degraded", "exit_code": 0, "latency_ms": 239177, "input_tokens": 2731, "output_tokens": 1400, "total_tokens": 4131, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "13bb8d002e46be4ad4741f384172f40c3bcbacc0629b4406d58f0c74a3868b56", "output_hash": "d0a346d08e7a904a0420722920924c7aa6aa35dcf1e4d4f0d53c0c2fd31558a6"}
{"timestamp": "2026-07-18T15:43:14Z", "trace_id": "57a9dc54-347a-45b8-bc50-fb70b11b20d7", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "degraded", "exit_code": 0, "latency_ms": 98719, "input_tokens": 13917, "output_tokens": 3269, "total_tokens": 17186, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "2788c9203adce8673587daf8c6b77d8e0773c4a75980fe569ec4549255a6e622", "output_hash": "611508f01c167d8b631fc052488385d12e5f6998683cc347958462d702084bba"}
{"timestamp": "2026-07-18T16:20:55Z", "trace_id": "8d594970-d8bb-46bf-bf48-4c52c6499d75", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "degraded", "exit_code": 0, "latency_ms": 41889, "input_tokens": 5220, "output_tokens": 1494, "total_tokens": 6714, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "2788c9203adce8673587daf8c6b77d8e0773c4a75980fe569ec4549255a6e622", "output_hash": "611508f01c167d8b631fc052488385d12e5f6998683cc347958462d702084bba"}
+21
View File
@@ -3,3 +3,24 @@
5c6def41467ad2932761d368ceac671fa8f66df1c46191a01735d354ffe6535a 5c6def41467ad2932761d368ceac671fa8f66df1c46191a01735d354ffe6535a
8b9d6e3fd9e6efe0d962d7e16a97b0c911f2721d323089ebdc97eb41e06ddadc 8b9d6e3fd9e6efe0d962d7e16a97b0c911f2721d323089ebdc97eb41e06ddadc
cd76a834731b45b1023a188752e46dd1089126ad8bfa7aba5add9e08050e640a cd76a834731b45b1023a188752e46dd1089126ad8bfa7aba5add9e08050e640a
d380470da66b175d55eb11c81ad0d2ca54e15c19702c456291d9755a073da2eb
18e2471ce08092a628b8309aa491ff0c7a06183afe294653e74a961b798d4356
8589bdc57c03d4e9ca1b2f01be50ccc487041009158bf6f0e9389c5212377f31
a176f96b4658ddde5d676d750689893af3f052de6ffafccda0718fe00737bce2
3db172719c869e71ecd50de65341cb71c8f787abc120d8c54722b4e4397349e4
d27b86c756726177d24e61f33632698832a4aded23dc4e27853a2de18e972395
a167a71338a7431453248e5c1cb462d7562863eb33231a2ce2d334e7d146f1f6
8d01dea6e037ebca5afe732b5c537ddb7253fe415416c90f5f688a1eacee1a14
61bc62a93b390dea77b16024dc70fa3f711e419e67f4d716ad3b01a3ec834aa1
2a18df1beb50c19cfd960e7130d65bc323b035e316de670d38cb4467dfaae295
1ebdd1dc6987205d8c544862d36df3b91388778187d04bde5ea946d1c9dc8217
6272e3e159e39f94b76431f2e7b6ec909fd18b52257af09e19d495d486d05d3d
f442a1d89bac8ac962f8e3a3645f7b6ccc3d54936003e5adf0dfa09762047c82
83f3c8255974d337019734ac1f73366daff832fff9777c38a460f33e5dfc5fc5
3d65fedb7d80f5d4c769ec72c0b880cf463cd5118eaa684f378c80e6f9b9bfe8
435e4bbfc987dd0007264f127e6b546e4ef0ef9099025867ab7fe65e20e76704
40166e4cf99b696b66e3edd64faae3a77c5933db3bb9c08a78a9c1568a8423dd
e82da705e80ad9b32a1f62c063d0cee297838316035d9aa2de41a6346269bda5
bcbb58baa5743a85a3507a1023c4cc4535910c0d4d178df65ce36192060551b6
e9865086c7caec136ab8928aca5c772fcf829b247a63e6e598813c559014cda5
ece8298d32b92d9a7dec0f683b7d0e44ddefb92b9b81826743bf4a1c880c9d9c
+38
View File
@@ -55,3 +55,41 @@
{"timestamp": "2026-07-11T08:23:30Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 2448, "output_tokens": 1400, "total_tokens": 3848, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 94149, "status": "success"} {"timestamp": "2026-07-11T08:23:30Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 2448, "output_tokens": 1400, "total_tokens": 3848, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 94149, "status": "success"}
{"timestamp": "2026-07-18T08:37:10Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "04-reviewspec", "role": "judge", "input_tokens": 546, "output_tokens": 3, "total_tokens": 549, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 9200, "usage_available": true, "status": "success"} {"timestamp": "2026-07-18T08:37:10Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "04-reviewspec", "role": "judge", "input_tokens": 546, "output_tokens": 3, "total_tokens": 549, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 9200, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T08:39:16Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "13-reviewcode", "role": "judge", "input_tokens": 585, "output_tokens": 3, "total_tokens": 588, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 3086, "usage_available": true, "status": "success"} {"timestamp": "2026-07-18T08:39:16Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "13-reviewcode", "role": "judge", "input_tokens": 585, "output_tokens": 3, "total_tokens": 588, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 3086, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T10:09:59Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 3666, "output_tokens": 298, "total_tokens": 3964, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 40271, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T10:11:32Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 3746, "output_tokens": 1307, "total_tokens": 5053, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 91828, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T11:18:39Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 2970, "output_tokens": 709, "total_tokens": 3679, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 61918, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T11:20:19Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 3467, "output_tokens": 1400, "total_tokens": 4867, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 99087, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T11:39:41Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 8536, "output_tokens": 1400, "total_tokens": 9936, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 121958, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T11:40:29Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 9657, "output_tokens": 92, "total_tokens": 9749, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 47278, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T12:20:41Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 8536, "output_tokens": 181, "total_tokens": 8717, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 63284, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T14:20:43Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 8667, "output_tokens": 116, "total_tokens": 8783, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 58521, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T14:21:34Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 8812, "output_tokens": 62, "total_tokens": 8874, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 51545, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T14:28:58Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 2731, "output_tokens": 1400, "total_tokens": 4131, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 88380, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T14:31:28Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 3943, "output_tokens": 1400, "total_tokens": 5343, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 102649, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T15:09:12Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 8848, "output_tokens": 1070, "total_tokens": 9918, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 98948, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T15:14:16Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 9891, "output_tokens": 2469, "total_tokens": 12360, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 183220, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T15:20:37Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 112, "output_tokens": 121, "total_tokens": 233, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 24747, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T15:27:33Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 117, "output_tokens": 211, "total_tokens": 328, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 37080, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T15:42:52Z", "harness": "L5-provider-telemetry", "provider": "openai", "model": "gpt-5.3-codex", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 55, "output_tokens": 44, "total_tokens": 99, "cost_usd": 0.0, "cost_source": "openai_api_real_tokens", "latency_ms": 2080, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T15:43:04Z", "harness": "L5-provider-telemetry", "provider": "openai", "model": "gpt-5.3-codex", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 7398, "output_tokens": 1869, "total_tokens": 9267, "cost_usd": 0.0, "cost_source": "openai_api_real_tokens", "latency_ms": 11582, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T15:43:14Z", "harness": "L5-provider-telemetry", "provider": "openai", "model": "gpt-5.3-codex", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 6519, "output_tokens": 1400, "total_tokens": 7919, "cost_usd": 0.0, "cost_source": "openai_api_real_tokens", "latency_ms": 9500, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T15:47:05Z", "harness": "L5-provider-telemetry", "provider": "openai", "model": "gpt-5.3-codex", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 55, "output_tokens": 58, "total_tokens": 113, "cost_usd": 0.0, "cost_source": "openai_api_real_tokens", "latency_ms": 1958, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:01:51Z", "harness": "L5-provider-telemetry", "provider": "openai", "model": "gpt-5.3-codex", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 55, "output_tokens": 61, "total_tokens": 116, "cost_usd": 0.0, "cost_source": "openai_api_real_tokens", "latency_ms": 1969, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:02:02Z", "harness": "L5-provider-telemetry", "provider": "openai", "model": "gpt-5.3-codex", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 5220, "output_tokens": 1737, "total_tokens": 6957, "cost_usd": 0.0, "cost_source": "openai_api_real_tokens", "latency_ms": 10775, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:02:12Z", "harness": "L5-provider-telemetry", "provider": "openai", "model": "gpt-5.3-codex", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 7079, "output_tokens": 1587, "total_tokens": 8666, "cost_usd": 0.0, "cost_source": "openai_api_real_tokens", "latency_ms": 10218, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:02:21Z", "harness": "L5-provider-telemetry", "provider": "openai", "model": "gpt-5.3-codex", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 6929, "output_tokens": 1587, "total_tokens": 8516, "cost_usd": 0.0, "cost_source": "openai_api_real_tokens", "latency_ms": 9335, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:02:31Z", "harness": "L5-provider-telemetry", "provider": "openai", "model": "gpt-5.3-codex", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 6929, "output_tokens": 1626, "total_tokens": 8555, "cost_usd": 0.0, "cost_source": "openai_api_real_tokens", "latency_ms": 9741, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:14:15Z", "harness": "L5-provider-telemetry", "provider": "openai", "model": "gpt-5.3-codex", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 55, "output_tokens": 58, "total_tokens": 113, "cost_usd": 0.0, "cost_source": "openai_api_real_tokens", "latency_ms": 1837, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:14:27Z", "harness": "L5-provider-telemetry", "provider": "openai", "model": "gpt-5.3-codex", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 5220, "output_tokens": 2013, "total_tokens": 7233, "cost_usd": 0.0, "cost_source": "openai_api_real_tokens", "latency_ms": 12235, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:14:42Z", "harness": "L5-provider-telemetry", "provider": "openai", "model": "gpt-5.3-codex", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 7374, "output_tokens": 2416, "total_tokens": 9790, "cost_usd": 0.0, "cost_source": "openai_api_real_tokens", "latency_ms": 14349, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:20:24Z", "harness": "L5-provider-telemetry", "provider": "openai", "model": "gpt-5.3-codex", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 5220, "output_tokens": 1494, "total_tokens": 6714, "cost_usd": 0.0, "cost_source": "openai_api_real_tokens", "latency_ms": 10422, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:31:43Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "classify", "role": "classify", "input_tokens": 216, "output_tokens": 2, "total_tokens": 218, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 6521, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:32:09Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 579, "output_tokens": 254, "total_tokens": 833, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 24956, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:32:10Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "classify", "role": "classify", "input_tokens": 216, "output_tokens": 2, "total_tokens": 218, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 611, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:32:44Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 579, "output_tokens": 368, "total_tokens": 947, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 33412, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:32:46Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "classify", "role": "classify", "input_tokens": 216, "output_tokens": 2, "total_tokens": 218, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 2137, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:33:14Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 666, "output_tokens": 266, "total_tokens": 932, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 27139, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:33:17Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "classify", "role": "classify", "input_tokens": 216, "output_tokens": 2, "total_tokens": 218, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 2214, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:33:44Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 579, "output_tokens": 286, "total_tokens": 865, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 26962, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:33:49Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "classify", "role": "classify", "input_tokens": 216, "output_tokens": 2, "total_tokens": 218, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 2130, "usage_available": true, "status": "success"}
{"timestamp": "2026-07-18T16:34:15Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 579, "output_tokens": 267, "total_tokens": 846, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 25684, "usage_available": true, "status": "success"}
+6 -1
View File
@@ -6,8 +6,11 @@ RUN apt-get update -qq && apt-get install -y -qq python3 python-is-python3 curl
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
COPY packages/casan-control-panel/backend/package.json ./packages/casan-control-panel/backend/ COPY packages/casan-control-panel/backend/package.json ./packages/casan-control-panel/backend/
COPY packages/casan-control-panel/frontend/package.json ./packages/casan-control-panel/frontend/
COPY apps/okr/backend/package.json ./apps/okr/backend/
COPY apps/okr/frontend/package.json ./apps/okr/frontend/
RUN npm ci -w @casan/control-panel-backend RUN npm ci
COPY packages/casan-control-panel/backend ./packages/casan-control-panel/backend COPY packages/casan-control-panel/backend ./packages/casan-control-panel/backend
@@ -21,12 +24,14 @@ RUN apt-get update -qq && apt-get install -y -qq python3 python-is-python3 curl
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
COPY packages/casan-control-panel/backend/package.json ./packages/casan-control-panel/backend/ COPY packages/casan-control-panel/backend/package.json ./packages/casan-control-panel/backend/
COPY packages/casan-control-panel/frontend/package.json ./packages/casan-control-panel/frontend/
COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/packages/casan-control-panel/backend/dist ./packages/casan-control-panel/backend/dist COPY --from=builder /app/packages/casan-control-panel/backend/dist ./packages/casan-control-panel/backend/dist
COPY packages/casan-harness/scripts ./packages/casan-harness/scripts COPY packages/casan-harness/scripts ./packages/casan-harness/scripts
COPY packages/casan-harness/config ./packages/casan-harness/config COPY packages/casan-harness/config ./packages/casan-harness/config
COPY packages/casan-harness/security ./packages/casan-harness/security COPY packages/casan-harness/security ./packages/casan-harness/security
COPY packages/casan-harness/level5/project-registry.json ./packages/casan-harness/level5/project-registry.json COPY packages/casan-harness/level5/project-registry.json ./packages/casan-harness/level5/project-registry.json
COPY packages/casan-devkit ./packages/casan-devkit
# Read-only, build-time workspace snapshots. Goal orchestration resolves only roots # Read-only, build-time workspace snapshots. Goal orchestration resolves only roots
# registered in project-registry.json and never accepts a browser-supplied path. # registered in project-registry.json and never accepts a browser-supplied path.
@@ -70,9 +70,9 @@ export class KeyResultsService {
createdById: user.sub, createdById: user.sub,
}, },
}); });
await this.recalculateObjectiveStatus(existing.objectiveId, tx);
return keyResult; return keyResult;
}); });
await this.recalculateObjectiveStatus(existing.objectiveId);
return updated; return updated;
} }
@@ -88,14 +88,17 @@ export class KeyResultsService {
} }
} }
private async recalculateObjectiveStatus(objectiveId: number): Promise<void> { private async recalculateObjectiveStatus(
const keyResults = await this.prisma.keyResult.findMany({ where: { objectiveId } }); objectiveId: number,
tx: Prisma.TransactionClient = this.prisma,
): Promise<void> {
const keyResults = await tx.keyResult.findMany({ where: { objectiveId } });
const average = const average =
keyResults.length === 0 keyResults.length === 0
? 0 ? 0
: Math.round(keyResults.reduce((total, keyResult) => total + keyResult.progress, 0) / keyResults.length); : Math.round(keyResults.reduce((total, keyResult) => total + keyResult.progress, 0) / keyResults.length);
const status = const status =
average === 0 ? 'NOT_STARTED' : average >= 100 ? 'COMPLETED' : 'IN_PROGRESS'; average === 0 ? 'NOT_STARTED' : average >= 100 ? 'COMPLETED' : 'IN_PROGRESS';
await this.prisma.objective.update({ where: { id: objectiveId }, data: { status } }); await tx.objective.update({ where: { id: objectiveId }, data: { status } });
} }
} }
+35
View File
@@ -312,6 +312,41 @@ test('KeyResultsService.updateProgress non-existent KR throws NotFoundException'
} }
}); });
test('KeyResultsService.updateProgress rolls back progress when status recalculation fails', async () => {
const prisma = new PrismaService();
await prisma.$connect();
const service = new KeyResultsService(prisma);
const testable = service as unknown as {
recalculateObjectiveStatus: (objectiveId: number) => Promise<void>;
};
const recalculateObjectiveStatus = testable.recalculateObjectiveStatus.bind(service);
let beforeProgress = 0;
try {
const before = await prisma.keyResult.findUniqueOrThrow({ where: { id: 1 } });
beforeProgress = before.progress;
testable.recalculateObjectiveStatus = async () => {
throw new Error('forced status recalculation failure');
};
await assert.rejects(
() => service.updateProgress(1, { progress: 99, comment: 'must roll back' }, employeeUser),
/forced status recalculation failure/,
);
const after = await prisma.keyResult.findUniqueOrThrow({ where: { id: 1 } });
assert.equal(after.progress, beforeProgress);
const leakedHistory = await prisma.progressUpdate.findMany({
where: { keyResultId: 1, progress: 99, comment: 'must roll back' },
});
assert.equal(leakedHistory.length, 0);
} finally {
testable.recalculateObjectiveStatus = recalculateObjectiveStatus;
await prisma.keyResult.update({ where: { id: 1 }, data: { progress: beforeProgress } });
await prisma.progressUpdate.deleteMany({ where: { keyResultId: 1, comment: 'must roll back' } });
await prisma.$disconnect();
}
});
// ─── Status recalculation (full NOT_STARTED → IN_PROGRESS → COMPLETED cycle) ─ // ─── Status recalculation (full NOT_STARTED → IN_PROGRESS → COMPLETED cycle) ─
test('KeyResultsService status recalculation: NOT_STARTED → IN_PROGRESS → COMPLETED → IN_PROGRESS', async () => { test('KeyResultsService status recalculation: NOT_STARTED → IN_PROGRESS → COMPLETED → IN_PROGRESS', async () => {
+16 -2
View File
@@ -34,6 +34,13 @@ services:
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
CASAN_GOAL_OPENAI_MODEL: ${CASAN_GOAL_OPENAI_MODEL:-gpt-5.3-codex} CASAN_GOAL_OPENAI_MODEL: ${CASAN_GOAL_OPENAI_MODEL:-gpt-5.3-codex}
CASAN_GOAL_ANTHROPIC_MODEL: ${CASAN_GOAL_ANTHROPIC_MODEL:-claude-3-5-sonnet-latest} CASAN_GOAL_ANTHROPIC_MODEL: ${CASAN_GOAL_ANTHROPIC_MODEL:-claude-3-5-sonnet-latest}
CASAN_GOAL_ENABLE_GATEWAY_PATCH_REPAIR: "1"
CASAN_GOAL_LOCAL_TIMEOUT_SEC: "600"
CASAN_GOAL_MODEL_TIMEOUT: "600"
CASAN_GOAL_MODEL_PREFLIGHT: "1"
CASAN_GOAL_MODEL_PREFLIGHT_TIMEOUT_SEC: "30"
CASAN_GOAL_LOCAL_MODEL_PREFLIGHT_TIMEOUT_SEC: "90"
CASAN_GOAL_MODEL_PREFLIGHT_TTL_SEC: "600"
CASAN_PREFLIGHT: ${CASAN_PREFLIGHT:-0} CASAN_PREFLIGHT: ${CASAN_PREFLIGHT:-0}
CASAN_AUTH_BRIDGE_URL: http://host.docker.internal:20130 CASAN_AUTH_BRIDGE_URL: http://host.docker.internal:20130
CASAN_AUTH_BRIDGE_TOKEN: ${CASAN_AUTH_BRIDGE_TOKEN:-} CASAN_AUTH_BRIDGE_TOKEN: ${CASAN_AUTH_BRIDGE_TOKEN:-}
@@ -47,8 +54,15 @@ services:
CASAN_APPROVAL_SIGNER_ROLE: ops CASAN_APPROVAL_SIGNER_ROLE: ops
CASAN_IDP_JWKS_URL: http://idp:8080/.well-known/jwks.json CASAN_IDP_JWKS_URL: http://idp:8080/.well-known/jwks.json
volumes: volumes:
# Persist only governed state and registered project roots. Dependencies
# stay inside the Linux image, avoiding host/container native-binary drift.
- ./.specify:/app/.specify - ./.specify:/app/.specify
- ./docs/output:/app/docs/output:ro - ./docs/output:/app/docs/output
- ./docs/technical_architecture.md:/app/docs/technical_architecture.md:ro
- ./apps/okr:/app/apps/okr
- ./apps/service-desk:/app/apps/service-desk
- ./apps/projects:/app/apps/projects
- ./packages/casan-harness/level5/project-registry.json:/app/packages/casan-harness/level5/project-registry.json
expose: expose:
- "3010" - "3010"
networks: networks:
@@ -114,7 +128,7 @@ services:
CASAN_IDP_ISSUER: http://localhost:18082 CASAN_IDP_ISSUER: http://localhost:18082
CASAN_IDP_SUB: oidc-ops CASAN_IDP_SUB: oidc-ops
CASAN_IDP_EMAIL: oidc-ops@example.com CASAN_IDP_EMAIL: oidc-ops@example.com
CASAN_IDP_GROUPS: casan-org-admin,casan-approver CASAN_IDP_GROUPS: casan-org-admin,casan-approver,casan-project:AINative_OKR_CASAN4
CASAN_APPROVAL_SIGNER_TOKEN: ${CASAN_APPROVAL_SIGNER_TOKEN:-local-approval-signer-secret} CASAN_APPROVAL_SIGNER_TOKEN: ${CASAN_APPROVAL_SIGNER_TOKEN:-local-approval-signer-secret}
ports: ports:
- "18082:8080" - "18082:8080"
+2 -2
View File
@@ -17,7 +17,7 @@ PORT = int(os.environ.get("CASAN_IDP_PORT", "8080"))
ISSUER = os.environ.get("CASAN_IDP_ISSUER", f"http://127.0.0.1:{PORT}") ISSUER = os.environ.get("CASAN_IDP_ISSUER", f"http://127.0.0.1:{PORT}")
DEFAULT_SUB = os.environ.get("CASAN_IDP_SUB", "oidc-ops") DEFAULT_SUB = os.environ.get("CASAN_IDP_SUB", "oidc-ops")
DEFAULT_EMAIL = os.environ.get("CASAN_IDP_EMAIL", "oidc-ops@example.com") DEFAULT_EMAIL = os.environ.get("CASAN_IDP_EMAIL", "oidc-ops@example.com")
DEFAULT_GROUPS = [g for g in os.environ.get("CASAN_IDP_GROUPS", "casan-org-admin,casan-approver").split(",") if g] DEFAULT_GROUPS = [g for g in os.environ.get("CASAN_IDP_GROUPS", "casan-org-admin,casan-approver,casan-project:AINative_OKR_CASAN4").split(",") if g]
USERS = { USERS = {
DEFAULT_SUB: { DEFAULT_SUB: {
"name": "Operations Owner", "name": "Operations Owner",
@@ -28,7 +28,7 @@ USERS = {
"oidc-reviewer": { "oidc-reviewer": {
"name": "Independent Reviewer", "name": "Independent Reviewer",
"email": "oidc-reviewer@example.com", "email": "oidc-reviewer@example.com",
"groups": ["casan-approver"], "groups": ["casan-approver", "casan-project:AINative_OKR_CASAN4"],
"description": "Reviews and approves another operator's proposal", "description": "Reviews and approves another operator's proposal",
}, },
} }
@@ -83,7 +83,11 @@ export class ApprovalsService {
list(actor: SettingsActor, status = 'pending') { list(actor: SettingsActor, status = 'pending') {
this.requireRbac(actor, 'monitoring', 'read'); this.requireRbac(actor, 'monitoring', 'read');
const res = runFile('python3', [INBOX_CLI, 'list', '--status', status], this.tenantEnv(actor)); const res = runFile('python3', [INBOX_CLI, 'list', '--status', status], this.tenantEnv(actor));
return { ...parseJson<Record<string, any>>(res.stdout, { count: 0, proposals: [], oversight: [] }), audit_verify: this.verifyAudit(actor) }; const inbox = parseJson<{ proposals?: Array<Record<string, any>>; oversight?: Array<Record<string, any>> }>(res.stdout, { proposals: [], oversight: [] });
const proposals = (inbox.proposals ?? []).filter((proposal) => this.canAccessProject(actor, String(proposal.project ?? actor.project)));
const visibleIds = new Set(proposals.map((proposal) => String(proposal.id ?? '')));
const oversight = (inbox.oversight ?? []).filter((record) => visibleIds.has(String(record.proposal_id ?? '')));
return { ...inbox, count: proposals.length, proposals, oversight, audit_verify: this.verifyAudit(actor) };
} }
submit(input: ApprovalSubmit, actor: SettingsActor) { submit(input: ApprovalSubmit, actor: SettingsActor) {
@@ -122,9 +126,9 @@ export class ApprovalsService {
if (!input.id || !input.decision || !input.reason) { if (!input.id || !input.decision || !input.reason) {
throw new ForbiddenException('APPROVAL_DECIDE_DENY id/decision/reason required'); throw new ForbiddenException('APPROVAL_DECIDE_DENY id/decision/reason required');
} }
this.requireRbac(actor, 'approval', 'grant');
try { try {
const pending = this.findProposal(input.id, actor); const pending = this.findProposal(input.id, actor);
this.requireRbac(actor, 'approval', 'grant', String(pending.project ?? actor.project));
await this.verifyApprovalIdentity(input, actor, pending); await this.verifyApprovalIdentity(input, actor, pending);
const res = runFile('python3', [ const res = runFile('python3', [
INBOX_CLI, INBOX_CLI,
@@ -281,7 +285,16 @@ export class ApprovalsService {
} }
} }
private requireRbac(actor: SettingsActor, resource: string, action: string) { private canAccessProject(actor: SettingsActor, targetProject: string): boolean {
try {
this.requireRbac(actor, 'monitoring', 'read', targetProject);
return true;
} catch {
return false;
}
}
private requireRbac(actor: SettingsActor, resource: string, action: string, targetProject = actor.project) {
try { try {
runFile('python3', [ runFile('python3', [
RBAC_CLI, RBAC_CLI,
@@ -295,7 +308,7 @@ export class ApprovalsService {
'--role-project', '--role-project',
actor.project, actor.project,
'--target-project', '--target-project',
actor.project, targetProject,
'--role-tenant', '--role-tenant',
actor.tenant, actor.tenant,
'--target-tenant', '--target-tenant',
@@ -33,6 +33,14 @@ function roleFromClaim(raw: string | undefined): string {
return 'viewer'; return 'viewer';
} }
function projectFromClaims(raw: string | undefined): string | undefined {
if (!raw) return undefined;
const prefix = 'casan-project:';
const projectClaim = raw.split(/[\s,]+/).find((claim) => claim.startsWith(prefix));
const project = projectClaim?.slice(prefix.length);
return project && /^[A-Za-z0-9._-]+$/.test(project) ? project : undefined;
}
export function actorFromHeaders(headers: Record<string, string | string[] | undefined>): SettingsActor { export function actorFromHeaders(headers: Record<string, string | string[] | undefined>): SettingsActor {
const actor = firstHeader(headers['x-casan-actor']) const actor = firstHeader(headers['x-casan-actor'])
|| firstHeader(headers['x-auth-request-user']) || firstHeader(headers['x-auth-request-user'])
@@ -45,7 +53,7 @@ export function actorFromHeaders(headers: Record<string, string | string[] | und
return { return {
actor, actor,
role: roleFromClaim(roleClaim), role: roleFromClaim(roleClaim),
project: firstHeader(headers['x-casan-project']) || 'default', project: firstHeader(headers['x-casan-project']) || projectFromClaims(roleClaim) || 'default',
tenant: firstHeader(headers['x-casan-tenant']) || 'default', tenant: firstHeader(headers['x-casan-tenant']) || 'default',
}; };
} }
@@ -48,6 +48,8 @@ export interface GoalJob {
finished_at?: string; finished_at?: string;
local_provider: string; local_provider: string;
local_model: string; local_model: string;
effective_local_provider?: string;
effective_local_model?: string;
cloud_provider: string; cloud_provider: string;
cloud_model: string; cloud_model: string;
stages: GoalStage[]; stages: GoalStage[];
@@ -182,9 +184,20 @@ export class GoalsService {
// must not silently change provider/model after a direct OpenAI repair // must not silently change provider/model after a direct OpenAI repair
// fails. Gateways can have their own transport and model-specific output // fails. Gateways can have their own transport and model-specific output
// contracts; an operator can explicitly opt in after validating one. // contracts; an operator can explicitly opt in after validating one.
const gatewayPatchRepairModels = process.env.CASAN_GOAL_ENABLE_GATEWAY_PATCH_REPAIR === '1' ? gatewayModels : []; const gatewayPatchRepairModels = process.env.CASAN_GOAL_ENABLE_GATEWAY_PATCH_REPAIR === '1' ? gatewayModels.slice(0, 1) : [];
const preferredCloudModel = cloudCandidates[0]?.model || ''; const preferredCloudModel = cloudCandidates[0]?.model || '';
const preferredCloudProvider = preferredCloudModel.startsWith('openai:') ? 'openai' : preferredCloudModel.startsWith('anthropic:') ? 'anthropic' : (cloud?.id || 'unavailable'); const preferredCloudProvider = preferredCloudModel.startsWith('openai:') ? 'openai' : preferredCloudModel.startsWith('anthropic:') ? 'anthropic' : (cloud?.id || 'unavailable');
// Coding-worker order is based on observed capability, not advertised
// discovery: direct cloud credentials first, then a logged-in account
// bridge, and only then the small local model. The account remains H3 when
// direct cloud is H2, preserving a distinct reviewer channel.
const cloudWorker = cloudCandidates[0];
const accountWorker = cloudWorker ? '' : account;
const accountReviewer = cloudWorker ? account : '';
const workerModel = cloudWorker?.model || (accountWorker ? `account:${accountWorker}` : String(localRuntime.CASAN_CHAT_SELECTED_MODEL || `ollama:${localModel}`));
const workerProvider = cloudWorker
? (workerModel.startsWith('openai:') ? 'openai' : workerModel.startsWith('anthropic:') ? 'anthropic' : 'cloud')
: (accountWorker ? `${accountWorker}-account` : (local?.id || 'local-policy'));
const cloudModel = preferredCloudModel || gateway?.defaultModel || gateway?.models[0] || ''; const cloudModel = preferredCloudModel || gateway?.defaultModel || gateway?.models[0] || '';
const id = randomUUID(); const id = randomUUID();
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
@@ -199,13 +212,13 @@ export class GoalsService {
workspace, workspace,
created_at: timestamp, created_at: timestamp,
updated_at: timestamp, updated_at: timestamp,
local_provider: local?.id || 'local-policy', local_provider: workerProvider,
local_model: String(localRuntime.CASAN_CHAT_SELECTED_MODEL || `ollama:${localModel}`), local_model: workerModel,
cloud_provider: account ? `${account}-account` : (preferredCloudProvider !== 'unavailable' ? preferredCloudProvider : (selectedReviewer?.id || 'unavailable')), cloud_provider: accountReviewer ? `${accountReviewer}-account` : (preferredCloudProvider !== 'unavailable' ? preferredCloudProvider : (selectedReviewer?.id || 'unavailable')),
cloud_model: account ? `${account}-account-default` : preferredCloudModel || String(cloudRuntime.CASAN_CHAT_SELECTED_MODEL || ''), cloud_model: accountReviewer ? `${accountReviewer}-account-default` : preferredCloudModel || String(cloudRuntime.CASAN_CHAT_SELECTED_MODEL || ''),
stages: [ stages: [
{ id: 'local-worker', status: 'queued', detail: 'Waiting for local worker', provider: local?.id || 'local-policy', model: localModel }, { id: 'local-worker', status: 'queued', detail: 'Waiting for primary coding worker', provider: workerProvider, model: workerModel },
{ id: 'cloud-reviewer', status: 'queued', detail: account || preferredCloudModel || selectedReviewer ? 'Waiting for independent reviewer' : 'Cloud unavailable; local reviewer will be used', provider: account ? `${account}-account` : (preferredCloudProvider !== 'unavailable' ? preferredCloudProvider : (selectedReviewer?.id || 'local-policy')), model: account ? `${account}-account-default` : cloudModel || localModel }, { id: 'cloud-reviewer', status: 'queued', detail: accountReviewer || preferredCloudModel || selectedReviewer ? 'Waiting for independent reviewer' : 'Cloud unavailable; local reviewer will be used', provider: accountReviewer ? `${accountReviewer}-account` : (preferredCloudProvider !== 'unavailable' ? preferredCloudProvider : (selectedReviewer?.id || 'local-policy')), model: accountReviewer ? `${accountReviewer}-account-default` : cloudModel || localModel },
], ],
}; };
const jobFile = this.jobPath(actor.tenant, id); const jobFile = this.jobPath(actor.tenant, id);
@@ -226,7 +239,7 @@ export class GoalsService {
CASAN_GOAL_CLOUD_MODEL: job.cloud_model, CASAN_GOAL_CLOUD_MODEL: job.cloud_model,
CASAN_GOAL_LOCAL_PROVIDER: job.local_provider, CASAN_GOAL_LOCAL_PROVIDER: job.local_provider,
CASAN_GOAL_CLOUD_PROVIDER: job.cloud_provider, CASAN_GOAL_CLOUD_PROVIDER: job.cloud_provider,
CASAN_GOAL_ACCOUNT_PROVIDER: account || '', CASAN_GOAL_ACCOUNT_PROVIDER: accountReviewer || '',
CASAN_GOAL_CLOUD_FALLBACK_MODEL: String(cloudRuntime.CASAN_CHAT_SELECTED_MODEL || ''), CASAN_GOAL_CLOUD_FALLBACK_MODEL: String(cloudRuntime.CASAN_CHAT_SELECTED_MODEL || ''),
CASAN_GOAL_CLOUD_MODELS: cloudCandidates.map(({ model }) => model).join(','), CASAN_GOAL_CLOUD_MODELS: cloudCandidates.map(({ model }) => model).join(','),
CASAN_GOAL_OMNIROUTE_MODELS: gatewayModels.join(','), CASAN_GOAL_OMNIROUTE_MODELS: gatewayModels.join(','),
@@ -234,6 +247,8 @@ export class GoalsService {
// explicit opt-in because it must not disguise a Codex patch failure. // explicit opt-in because it must not disguise a Codex patch failure.
CASAN_GOAL_PATCH_REPAIR_MODELS: [...cloudCandidates.map(({ model }) => model), ...gatewayPatchRepairModels, String(localRuntime.CASAN_CHAT_SELECTED_MODEL || `ollama:${localModel}`)].filter((model, index, rows) => rows.indexOf(model) === index).join(','), CASAN_GOAL_PATCH_REPAIR_MODELS: [...cloudCandidates.map(({ model }) => model), ...gatewayPatchRepairModels, String(localRuntime.CASAN_CHAT_SELECTED_MODEL || `ollama:${localModel}`)].filter((model, index, rows) => rows.indexOf(model) === index).join(','),
CASAN_GOAL_LOCAL_REVIEWER_MODEL: String(localRuntime.CASAN_CHAT_SELECTED_MODEL || `ollama:${localModel}`), CASAN_GOAL_LOCAL_REVIEWER_MODEL: String(localRuntime.CASAN_CHAT_SELECTED_MODEL || `ollama:${localModel}`),
CASAN_GOAL_LOCAL_REVIEWER_PROVIDER: local?.id || 'local-policy',
CASAN_GOAL_ENABLE_LOCAL_REVIEWER: process.env.CASAN_GOAL_ENABLE_LOCAL_REVIEWER || (account || preferredCloudModel ? '0' : '1'),
CASAN_GOAL_REVIEWER_MAX_ATTEMPTS: process.env.CASAN_GOAL_REVIEWER_MAX_ATTEMPTS || '8', CASAN_GOAL_REVIEWER_MAX_ATTEMPTS: process.env.CASAN_GOAL_REVIEWER_MAX_ATTEMPTS || '8',
CASAN_GOAL_REVIEWER_DEADLINE_SEC: process.env.CASAN_GOAL_REVIEWER_DEADLINE_SEC || '600', CASAN_GOAL_REVIEWER_DEADLINE_SEC: process.env.CASAN_GOAL_REVIEWER_DEADLINE_SEC || '600',
}, },
@@ -393,7 +408,8 @@ export class GoalsService {
apply(id: string, actor: SettingsActor): GoalJob { apply(id: string, actor: SettingsActor): GoalJob {
const job = this.get(id, actor); const job = this.get(id, actor);
if (!job.patch_artifact || job.status !== 'requires_approval') { const retryableRollback = job.status === 'failed' && job.error === 'GOAL_APPLY_VERIFICATION_FAILED_ROLLED_BACK';
if (!job.patch_artifact || (job.status !== 'requires_approval' && !retryableRollback)) {
throw new BadRequestException('GOAL_PATCH_NOT_READY'); throw new BadRequestException('GOAL_PATCH_NOT_READY');
} }
try { try {
@@ -102,6 +102,29 @@ test('operations owner request remains visible and actionable for an independent
}); });
}); });
test('project reviewer cannot see or decide another project approval', async () => {
await withTempGovernance(async () => {
const svc = new ApprovalsService();
const submitted = svc.submit({
action: 'goal.workspace.execute',
target: 'project-alpha',
risk: 'high',
sensitive: true,
reason: 'project alpha patch',
payload: { goal_id: 'goal-alpha', project_id: 'project-alpha' },
}, { ...projectAdmin, project: 'project-alpha' }) as { proposal: { id: string } };
const otherProjectReviewer = { ...approver, project: 'project-beta' };
const reviewerInbox = svc.list(otherProjectReviewer, 'pending') as { count: number; proposals: Array<{ id: string }> };
assert.equal(reviewerInbox.count, 0);
assert.equal(reviewerInbox.proposals.length, 0);
await assert.rejects(
svc.decide({ id: submitted.proposal.id, decision: 'approve', reason: 'wrong project' }, otherProjectReviewer),
ForbiddenException,
);
});
});
test('approval inbox denies forged JWT in strict mode without deciding proposal', async () => { test('approval inbox denies forged JWT in strict mode without deciding proposal', async () => {
await withTempGovernance(async ({ inbox }) => { await withTempGovernance(async ({ inbox }) => {
const svc = new ApprovalsService(); const svc = new ApprovalsService();
@@ -15,10 +15,11 @@ test('auth context keeps local explicit roles for dev', () => {
test('auth context maps IdP group claim to RBAC role', () => { test('auth context maps IdP group claim to RBAC role', () => {
const actor = actorFromHeaders({ const actor = actorFromHeaders({
'x-auth-request-user': 'bob@example.com', 'x-auth-request-user': 'bob@example.com',
'x-auth-request-groups': 'engineering,casan-approver', 'x-auth-request-groups': 'engineering,casan-approver,casan-project:AINative_OKR_CASAN4',
}); });
assert.equal(actor.actor, 'bob@example.com'); assert.equal(actor.actor, 'bob@example.com');
assert.equal(actor.role, 'approver'); assert.equal(actor.role, 'approver');
assert.equal(actor.project, 'AINative_OKR_CASAN4');
}); });
test('auth context fails closed to viewer for unknown role claim', () => { test('auth context fails closed to viewer for unknown role claim', () => {
@@ -99,19 +99,39 @@ export function Approvals() {
}); });
const decide = useMutation({ const decide = useMutation({
mutationFn: ({ id, decision }: { id: string; decision: 'approve' | 'reject' }) => { mutationFn: async ({ proposal, decision }: { proposal: ApprovalProposal; decision: 'approve' | 'reject' }) => {
if (!actor) throw new Error('Authenticated session is unavailable.'); if (!actor) throw new Error('Authenticated session is unavailable.');
return api.decideApproval(actor, { id, decision, reason: decisionReason.trim() }); const response = await api.decideApproval(actor, { id: proposal.id, decision, reason: decisionReason.trim() });
if (decision !== 'approve' || proposal.action !== 'goal.workspace.execute') {
return { response, continuedGoalId: null };
}
const goalId = proposal.payload.goal_id;
if (typeof goalId !== 'string' || goalId.length === 0) {
throw new Error(`Request ${proposal.id} was approved, but its goal identifier is missing. Continue from Goal Orchestrator.`);
}
try {
await api.applyGoal({ ...actor, project: proposal.project }, goalId);
return { response, continuedGoalId: goalId };
} catch (error) {
throw new Error(`Request ${proposal.id} was approved, but apply/verification did not complete: ${errorMessage(error, 'unknown apply error')}`);
}
}, },
onSuccess: (response) => { onSuccess: ({ response, continuedGoalId }) => {
const verb = response.proposal.status === 'approved' ? 'approved' : 'rejected'; const verb = response.proposal.status === 'approved' ? 'approved' : 'rejected';
setNotice({ tone: 'success', text: `Request ${response.proposal.id} was ${verb}.` }); const continuation = continuedGoalId ? ' The governed change was applied and verified.' : '';
setNotice({ tone: 'success', text: `Request ${response.proposal.id} was ${verb}.${continuation}` });
void queryClient.invalidateQueries({ queryKey: ['approvals'] }); void queryClient.invalidateQueries({ queryKey: ['approvals'] });
void queryClient.invalidateQueries({ queryKey: ['settings'] }); void queryClient.invalidateQueries({ queryKey: ['settings'] });
void queryClient.invalidateQueries({ queryKey: ['goals'] }); void queryClient.invalidateQueries({ queryKey: ['goals'] });
void queryClient.invalidateQueries({ queryKey: ['goal'] }); void queryClient.invalidateQueries({ queryKey: ['goal'] });
}, },
onError: (error) => setNotice({ tone: 'error', text: errorMessage(error, 'The approval decision could not be recorded.') }), onError: (error) => {
setNotice({ tone: 'error', text: errorMessage(error, 'The approval decision could not be recorded.') });
void queryClient.invalidateQueries({ queryKey: ['approvals'] });
void queryClient.invalidateQueries({ queryKey: ['goals'] });
void queryClient.invalidateQueries({ queryKey: ['goal'] });
},
}); });
if (session.isLoading || (actor && inbox.isLoading)) { if (session.isLoading || (actor && inbox.isLoading)) {
@@ -184,7 +204,7 @@ export function Approvals() {
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{inbox.data.proposals.map((proposal) => { {inbox.data.proposals.map((proposal) => {
const eligibility = reviewEligibility(actor, proposal); const eligibility = reviewEligibility(actor, proposal);
const isCurrentDecision = decide.isPending && decide.variables?.id === proposal.id; const isCurrentDecision = decide.isPending && decide.variables?.proposal.id === proposal.id;
return ( return (
<article key={proposal.id} className="rounded-2xl border border-slate-200 bg-white p-4 transition hover:border-slate-300 hover:shadow-[0_12px_28px_rgba(15,23,42,0.055)] sm:p-5"> <article key={proposal.id} className="rounded-2xl border border-slate-200 bg-white p-4 transition hover:border-slate-300 hover:shadow-[0_12px_28px_rgba(15,23,42,0.055)] sm:p-5">
<div className="flex flex-wrap items-start justify-between gap-4"> <div className="flex flex-wrap items-start justify-between gap-4">
@@ -211,8 +231,8 @@ export function Approvals() {
<span className={`text-xs font-medium ${eligibility.allowed ? 'text-emerald-700' : 'text-amber-700'}`}>{eligibility.reason}</span> <span className={`text-xs font-medium ${eligibility.allowed ? 'text-emerald-700' : 'text-amber-700'}`}>{eligibility.reason}</span>
{eligibility.allowed && ( {eligibility.allowed && (
<div className="flex gap-2"> <div className="flex gap-2">
<button type="button" disabled={decide.isPending || decisionReason.trim().length < 5} onClick={() => decide.mutate({ id: proposal.id, decision: 'reject' })} className="rounded-lg border border-rose-200 bg-white px-3.5 py-2 text-xs font-semibold text-rose-700 transition hover:bg-rose-50 focus:outline-none focus:ring-4 focus:ring-rose-100 disabled:cursor-not-allowed disabled:opacity-40">{isCurrentDecision && decide.variables?.decision === 'reject' ? 'Rejecting…' : 'Reject'}</button> <button type="button" disabled={decide.isPending || decisionReason.trim().length < 5} onClick={() => decide.mutate({ proposal, decision: 'reject' })} className="rounded-lg border border-rose-200 bg-white px-3.5 py-2 text-xs font-semibold text-rose-700 transition hover:bg-rose-50 focus:outline-none focus:ring-4 focus:ring-rose-100 disabled:cursor-not-allowed disabled:opacity-40">{isCurrentDecision && decide.variables?.decision === 'reject' ? 'Rejecting…' : 'Reject'}</button>
<button type="button" disabled={decide.isPending || decisionReason.trim().length < 5} onClick={() => decide.mutate({ id: proposal.id, decision: 'approve' })} className="rounded-lg bg-emerald-700 px-4 py-2 text-xs font-semibold text-white transition hover:bg-emerald-800 focus:outline-none focus:ring-4 focus:ring-emerald-100 disabled:cursor-not-allowed disabled:bg-slate-300">{isCurrentDecision && decide.variables?.decision === 'approve' ? 'Approving…' : 'Approve'}</button> <button type="button" disabled={decide.isPending || decisionReason.trim().length < 5} onClick={() => decide.mutate({ proposal, decision: 'approve' })} className="rounded-lg bg-emerald-700 px-4 py-2 text-xs font-semibold text-white transition hover:bg-emerald-800 focus:outline-none focus:ring-4 focus:ring-emerald-100 disabled:cursor-not-allowed disabled:bg-slate-300">{isCurrentDecision && decide.variables?.decision === 'approve' ? 'Approving and continuing…' : proposal.action === 'goal.workspace.execute' ? 'Approve & continue' : 'Approve'}</button>
</div> </div>
)} )}
</div> </div>
@@ -8,10 +8,12 @@ audit, or metric records.
import argparse import argparse
import fcntl import fcntl
import hashlib import hashlib
import importlib.util
import json import json
import os import os
import re import re
import shlex import shlex
import shutil
import subprocess import subprocess
import tempfile import tempfile
import time import time
@@ -36,6 +38,11 @@ SECURITY = os.path.join(BIN, "security-check.sh")
STATE_ROOT = os.environ.get("CASAN_STATE_ROOT") or os.path.join(ROOT, ".specify") STATE_ROOT = os.environ.get("CASAN_STATE_ROOT") or os.path.join(ROOT, ".specify")
PROJECT_REGISTRY = os.path.join(ROOT, "packages", "casan-harness", "level5", "project-registry.json") PROJECT_REGISTRY = os.path.join(ROOT, "packages", "casan-harness", "level5", "project-registry.json")
APPROVAL_INBOX = os.path.join(BIN, "approval-inbox.py") APPROVAL_INBOX = os.path.join(BIN, "approval-inbox.py")
_MANIFEST_SPEC = importlib.util.spec_from_file_location(
"casan_goal_project_manifest", os.path.join(BIN, "project_manifest.py")
)
PROJECT_MANIFEST = importlib.util.module_from_spec(_MANIFEST_SPEC)
_MANIFEST_SPEC.loader.exec_module(PROJECT_MANIFEST)
CONTEXT_EXTENSIONS = {".md", ".txt", ".json", ".yaml", ".yml", ".ts", ".tsx", ".js", ".mjs", ".py", ".sh", ".prisma", ".css", ".html"} CONTEXT_EXTENSIONS = {".md", ".txt", ".json", ".yaml", ".yml", ".ts", ".tsx", ".js", ".mjs", ".py", ".sh", ".prisma", ".css", ".html"}
CONTEXT_IGNORED = {"node_modules", ".git", "dist", "build", "coverage", ".vite", "tmp", "logs", "__pycache__"} CONTEXT_IGNORED = {"node_modules", ".git", "dist", "build", "coverage", ".vite", "tmp", "logs", "__pycache__"}
SENSITIVE_NAMES = {".env", ".env.local", "credentials", "credentials.json", "secrets.json", "id_rsa", "id_ed25519"} SENSITIVE_NAMES = {".env", ".env.local", "credentials", "credentials.json", "secrets.json", "id_rsa", "id_ed25519"}
@@ -173,8 +180,35 @@ def context_excerpt_is_sensitive(text: str) -> bool:
)) ))
def restricted_context_paths(goal: str):
"""Return an explicit user-declared file/directory boundary, if present."""
section = re.search(
r"(?:chỉ được đọc và thay đổi|chỉ làm việc trong|only (?:read and )?(?:modify|change)|only work (?:in|within))\s*:\s*"
r"(.*?)(?=\n\s*(?:không sửa bất kỳ file nào khác|do not (?:modify|change) any other file|không thay đổi|không thêm|vấn đề|yêu cầu|verification|$))",
goal,
re.IGNORECASE | re.DOTALL,
)
if not section:
return []
paths = []
for match in re.findall(r"(?:apps|packages|docs)/[A-Za-z0-9_./*-]+", section.group(1), re.IGNORECASE):
cleaned = match.rstrip(".,:;)")
scoped_directory = cleaned.endswith(("/*", "/**"))
normalized = cleaned.rstrip("/*")
if normalized and all(path != normalized for path, _ in paths):
paths.append((normalized, scoped_directory))
return paths
def context_candidates(project: dict, goal: str): def context_candidates(project: dict, goal: str):
terms = {term.lower() for term in re.findall(r"[A-Za-z0-9_-]{3,}", goal)} terms = {term.lower() for term in re.findall(r"[A-Za-z0-9_-]{3,}", goal)}
explicit_paths = []
for match in re.findall(r"(?:apps|packages|docs)/[A-Za-z0-9_./*-]+", goal, re.IGNORECASE):
cleaned = match.rstrip(".,:;)")
scoped_directory = cleaned.endswith(("/*", "/**"))
normalized_path = cleaned.rstrip("/*").lower()
if normalized_path and all(path != normalized_path for path, _ in explicit_paths):
explicit_paths.append((normalized_path, scoped_directory))
candidates = [] candidates = []
seen = set() seen = set()
for relative_root, absolute_root in project["roots"]: for relative_root, absolute_root in project["roots"]:
@@ -200,6 +234,22 @@ def context_candidates(project: dict, goal: str):
continue continue
haystack = (relative + "\n" + raw[:4000]).lower() haystack = (relative + "\n" + raw[:4000]).lower()
score = sum(4 if term in relative.lower() else 1 for term in terms if term in haystack) score = sum(4 if term in relative.lower() else 1 for term in terms if term in haystack)
relative_lower = relative.lower()
matching_paths = [
(path, scoped_directory)
for path, scoped_directory in explicit_paths
if relative_lower == path or relative_lower.startswith(path + "/")
]
if matching_paths:
# Directory globs under "only work in" sections describe the
# actual patch surface. Exact source-file mentions come next;
# architecture/requirement references remain supporting context.
if any(scoped_directory for _, scoped_directory in matching_paths):
score += 3_000
elif os.path.splitext(relative_lower)[1] in {".ts", ".tsx", ".js", ".jsx", ".py", ".prisma", ".sql", ".sh"}:
score += 2_000
else:
score += 1_000
if relative.endswith(("README.md", "architecture.md", "technical_architecture.md", "package.json")): if relative.endswith(("README.md", "architecture.md", "technical_architecture.md", "package.json")):
score += 3 score += 3
candidates.append((score, relative, raw)) candidates.append((score, relative, raw))
@@ -209,6 +259,17 @@ def context_candidates(project: dict, goal: str):
def build_context(job_path: str, project_id: str, goal: str, write_intent=False): def build_context(job_path: str, project_id: str, goal: str, write_intent=False):
project = registered_project(project_id) project = registered_project(project_id)
candidates = context_candidates(project, goal) candidates = context_candidates(project, goal)
restricted_paths = restricted_context_paths(goal)
if restricted_paths:
candidates = [
candidate for candidate in candidates
if any(
candidate[1] == path or (scoped_directory and candidate[1].startswith(path.rstrip("/") + "/"))
for path, scoped_directory in restricted_paths
)
]
if not candidates:
raise ValueError("goal_context_explicit_scope_empty")
excerpts, manifest_files, characters = [], [], 0 excerpts, manifest_files, characters = [], [], 0
# A diff can only apply when the model sees the exact target-file content. # A diff can only apply when the model sees the exact target-file content.
# Read-only analysis stays compact; write-intent gives the three most # Read-only analysis stays compact; write-intent gives the three most
@@ -261,12 +322,39 @@ def build_context(job_path: str, project_id: str, goal: str, write_intent=False)
def requests_side_effect(goal: str) -> bool: def requests_side_effect(goal: str) -> bool:
normalized = " ".join(goal.lower().split()) normalized = " ".join(goal.lower().split())
patterns = [ # The objective's leading command is authoritative. A scoped constraint
r"^(hãy\s+)?(làm luôn|sửa|thay đổi|triển khai|thực hiện|hoàn thành|chạy|tạo|xóa|cài đặt|commit|push)\b", # such as "Không thay đổi API contract" must not turn "Hoàn thiện ..."
# into a read-only request. Conversely, a genuinely read-only audit does
# not begin with one of these implementation commands.
leading_write_patterns = [
r"^(hãy\s+)?(làm luôn|sửa|thay đổi|triển khai|thực hiện|hoàn thiện|hoàn thành|chạy|tạo|xóa|cài đặt|commit|push)\b",
r"^(please\s+)?(implement|fix|change|deploy|run|create|delete|install|commit|push)\b", r"^(please\s+)?(implement|fix|change|deploy|run|create|delete|install|commit|push)\b",
r"\b(thực hiện thao tác|sửa code|ghi file|mở pull request|create a pull request)\b",
] ]
return any(re.search(pattern, normalized) for pattern in patterns) if any(re.search(pattern, normalized) for pattern in leading_write_patterns):
return True
patch_requests = re.finditer(r"\b(tạo|generate|xuất|produce)\b.{0,40}\b(patch|unified git diff)\b", normalized)
explicit_patch_request = any(
not normalized[max(0, match.start() - 8):match.start()].endswith(("không ", "do not "))
for match in patch_requests
)
explicit_read_only = bool(re.search(r"\b(không|do not)\s+(sửa|thay đổi|triển khai|implement|fix|tạo|apply|áp dụng)\b", normalized))
if explicit_read_only and not explicit_patch_request:
return False
imperative_patterns = [r"\b(thực hiện thao tác|sửa code|ghi file|mở pull request|create a pull request)\b"]
if any(re.search(pattern, normalized) for pattern in imperative_patterns):
return True
# Structured objectives commonly start with a phase label rather than the
# imperative itself (for example "PHASE 1 — Hoàn thiện backend..."). Keep
# those write requests on the patch + approval path without classifying a
# read-only audit that merely mentions source files as a side effect.
structured_write_patterns = [
r"\b(hoàn thiện|triển khai|implement|fix|sửa|thay đổi)\b.{0,120}\b(ứng dụng|backend|frontend|module|component|api|source|code|file)\b",
r"\b(tạo|generate|xuất|produce)\b.{0,40}\b(patch|unified git diff)\b",
r"\b(apply|áp dụng)\b.{0,40}\b(patch|thay đổi|change)\b",
]
return any(re.search(pattern, normalized) for pattern in structured_write_patterns)
def extract_patch(text: str) -> str: def extract_patch(text: str) -> str:
@@ -344,6 +432,17 @@ def normalize_unified_diff(patch: str) -> str:
return "\n".join(normalized) + "\n" if changed else patch return "\n".join(normalized) + "\n" if changed else patch
def mechanical_patch_error(error: object) -> bool:
"""Identify git parser errors that hunk recounting can safely repair."""
normalized = str(error).lower()
return any(marker in normalized for marker in (
"corrupt patch",
"patch fragment without header",
"malformed patch",
"unexpected end of file in patch",
))
def validate_write_output(text: str) -> str: def validate_write_output(text: str) -> str:
"""Fail at the producing harness when a write-intent reply is not a diff. """Fail at the producing harness when a write-intent reply is not a diff.
@@ -356,7 +455,7 @@ def validate_write_output(text: str) -> str:
validate_patch_check(patch) validate_patch_check(patch)
return patch return patch
except ValueError as original_error: except ValueError as original_error:
if "corrupt patch" not in str(original_error): if not mechanical_patch_error(original_error):
raise raise
normalized = normalize_unified_diff(patch) normalized = normalize_unified_diff(patch)
if normalized == patch: if normalized == patch:
@@ -387,14 +486,106 @@ def patch_repair_models(primary_model: str):
"""Use direct cloud -> gateway -> local order for one bounded H2 recovery.""" """Use direct cloud -> gateway -> local order for one bounded H2 recovery."""
configured = [item.strip() for item in os.environ.get("CASAN_GOAL_PATCH_REPAIR_MODELS", "").split(",") if item.strip()] configured = [item.strip() for item in os.environ.get("CASAN_GOAL_PATCH_REPAIR_MODELS", "").split(",") if item.strip()]
unique, seen = [], set() unique, seen = [], set()
for candidate in configured + [primary_model]: ordered = [primary_model, *configured] if primary_model.startswith("account:") else [*configured, primary_model]
for candidate in ordered:
if candidate and candidate not in seen: if candidate and candidate not in seen:
seen.add(candidate) seen.add(candidate)
unique.append(candidate) unique.append(candidate)
return unique return unique
def model_preflight_enabled() -> bool:
configured = os.environ.get("CASAN_GOAL_MODEL_PREFLIGHT")
return configured == "1" if configured is not None else os.environ.get("CASAN_PROFILE") == "prod"
def model_preflight(model: str):
"""Probe real patch generation before assigning a model to H2/H3.
Model discovery only proves that an ID is advertised. This bounded probe
verifies the generation route and unified-diff capability, and caches the
verdict so a broken gateway is not retried for every goal.
"""
if not model_preflight_enabled():
return True, "preflight_disabled"
if model.startswith("account:"):
endpoint = os.environ.get("CASAN_AUTH_BRIDGE_URL", "")
elif model.startswith("openai-compatible:"):
endpoint = os.environ.get("CASAN_OPENAI_COMPATIBLE_BASE_URL", "")
else:
endpoint = os.environ.get("OLLAMA_HOST", "")
cache_key = sha(f"{model}|{endpoint}")
cache_directory = os.path.join(STATE_ROOT, "cache")
cache_path = os.path.join(cache_directory, "goal-model-preflight.json")
lock_path = os.path.join(cache_directory, "goal-model-preflight.lock")
os.makedirs(cache_directory, mode=0o700, exist_ok=True)
ttl = int(os.environ.get("CASAN_GOAL_MODEL_PREFLIGHT_TTL_SEC", "600"))
with open(lock_path, "a", encoding="utf-8") as lock:
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
try:
cache = load_json(cache_path) if os.path.isfile(cache_path) else {}
except (OSError, ValueError, json.JSONDecodeError):
cache = {}
cached = cache.get(cache_key, {}) if isinstance(cache, dict) else {}
age = time.time() - float(cached.get("checked_epoch", 0))
if age <= max(30, ttl):
fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
return bool(cached.get("healthy")), f"cached:{cached.get('reason', 'unknown')}"
fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
prompt = (
"Patch capability probe. Return ONLY a unified git diff inside a ```diff fence that changes "
"the only line in probe.txt from old to new. No prose. The diff must begin with "
"`diff --git a/probe.txt b/probe.txt`."
)
timeout_setting = (
os.environ.get("CASAN_GOAL_LOCAL_MODEL_PREFLIGHT_TIMEOUT_SEC", "90")
if model.startswith("ollama:")
else os.environ.get("CASAN_GOAL_MODEL_PREFLIGHT_TIMEOUT_SEC", "30")
)
timeout = max(5, min(int(timeout_setting), 120))
if model.startswith("account:"):
ok, output, _, reason = call_account_model(model.split(":", 1)[1], prompt, timeout)
else:
ok, output, _, reason = call_model(
model, prompt, model.startswith(("openai:", "anthropic:", "openai-compatible:")),
timeout_seconds=timeout, max_output_tokens=512,
)
healthy = False
if ok:
try:
patch = extract_patch(output)
healthy = (
"diff --git a/probe.txt b/probe.txt" in patch
and "\n-old\n" in patch
and "\n+new\n" in patch
)
reason = "ok" if healthy else "patch_probe_contract_invalid"
except ValueError as error:
reason = str(error)
record = {
"model": model,
"provider": provider_for_model(model),
"healthy": healthy,
"reason": reason[:180],
"checked_at": now(),
"checked_epoch": time.time(),
}
with open(lock_path, "a", encoding="utf-8") as lock:
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
try:
cache = load_json(cache_path) if os.path.isfile(cache_path) else {}
except (OSError, ValueError, json.JSONDecodeError):
cache = {}
cache[cache_key] = record
atomic_json(cache_path, cache)
fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
return healthy, reason
def provider_for_model(model: str) -> str: def provider_for_model(model: str) -> str:
if model.startswith("account:"):
return f"{model.split(':', 1)[1]}-account"
if model.startswith("openai:"): if model.startswith("openai:"):
return "openai" return "openai"
if model.startswith("anthropic:"): if model.startswith("anthropic:"):
@@ -420,7 +611,7 @@ def repair_write_output(model: str, original_prompt: str, invalid_output: str, c
def repair_prompt_for(previous_output: str, error: str) -> str: def repair_prompt_for(previous_output: str, error: str) -> str:
hunk_instruction = ( hunk_instruction = (
"The previous diff is syntactically corrupt. Recompute every `@@ -old,count +new,count @@` header from the exact added/removed/context lines that follow it; do not omit or invent any hunk line. " "The previous diff is syntactically corrupt. Recompute every `@@ -old,count +new,count @@` header from the exact added/removed/context lines that follow it; do not omit or invent any hunk line. "
if "corrupt patch" in error.lower() else "" if mechanical_patch_error(error) else ""
) )
return ( return (
f"Your previous response violated the required write-output contract: {error}. " + f"Your previous response violated the required write-output contract: {error}. " +
@@ -434,6 +625,20 @@ def repair_write_output(model: str, original_prompt: str, invalid_output: str, c
last_metadata, last_reason = {}, "goal_patch_missing" last_metadata, last_reason = {}, "goal_patch_missing"
attempts = [] attempts = []
for attempt_number, candidate in enumerate(candidates, start=1): for attempt_number, candidate in enumerate(candidates, start=1):
healthy, preflight_reason = model_preflight(candidate)
if not healthy:
last_reason = f"goal_patch_model_preflight_failed:{preflight_reason}"
attempts.append({
"attempt": attempt_number,
"provider": provider_for_model(candidate),
"model": candidate,
"status": "failed",
"reason": last_reason,
})
continue
if candidate.startswith("account:"):
ok, output, metadata, reason = call_account_model(candidate.split(":", 1)[1], repair_prompt, 300)
else:
ok, output, metadata, reason = call_model(candidate, repair_prompt, candidate.startswith(("openai:", "anthropic:", "openai-compatible:")), max_output_tokens=patch_output_tokens()) ok, output, metadata, reason = call_model(candidate, repair_prompt, candidate.startswith(("openai:", "anthropic:", "openai-compatible:")), max_output_tokens=patch_output_tokens())
metadata = dict(metadata) metadata = dict(metadata)
metadata["repair_model"] = candidate metadata["repair_model"] = candidate
@@ -464,7 +669,7 @@ def repair_write_output(model: str, original_prompt: str, invalid_output: str, c
# incomplete first diff. Give the same stronger direct model one # incomplete first diff. Give the same stronger direct model one
# corrective pass containing its own failed patch and git error # corrective pass containing its own failed patch and git error
# before sending source context to a weaker gateway. # before sending source context to a weaker gateway.
if candidate.endswith("-codex") and attempt_number < patch_repair_attempts(): if (candidate.endswith("-codex") or candidate == "account:codex") and attempt_number < patch_repair_attempts():
candidates.insert(attempt_number, candidate) candidates.insert(attempt_number, candidate)
del candidates[patch_repair_attempts():] del candidates[patch_repair_attempts():]
repair_prompt = repair_prompt_for(output, last_reason) repair_prompt = repair_prompt_for(output, last_reason)
@@ -493,8 +698,52 @@ def merge_usage(primary: dict, additional: dict) -> dict:
return merged return merged
def validate_patch_semantics(job: dict, artifact: str, changed_files: list[str]) -> list[dict]:
"""Build and test an applied patch in an isolated workspace before approval."""
artifact = os.path.realpath(artifact)
if os.path.commonpath([ROOT, artifact]) != ROOT or not os.path.isfile(artifact):
raise ValueError("goal_patch_artifact_path_denied")
manifest = PROJECT_MANIFEST.load(ROOT, project_id=str(job.get("project") or ""))
commands = PROJECT_MANIFEST.verification_commands(manifest, changed_files)
if not commands:
raise ValueError("goal_patch_verification_unmapped")
results = []
with tempfile.TemporaryDirectory(prefix="casan-goal-verify-") as sandbox:
paths = ["package.json", "package-lock.json", *manifest["source_roots"]]
paths.extend(str(value) for value in job.get("workspace", {}).get("context_roots", []))
for relative in dict.fromkeys(paths):
source = os.path.realpath(os.path.join(ROOT, relative))
if os.path.commonpath([ROOT, source]) != ROOT or not os.path.exists(source):
continue
destination = os.path.join(sandbox, relative)
os.makedirs(os.path.dirname(destination), exist_ok=True)
if os.path.isdir(source):
shutil.copytree(source, destination, dirs_exist_ok=True, symlinks=True, ignore=shutil.ignore_patterns("node_modules", "dist", "coverage"))
else:
shutil.copy2(source, destination)
dependencies = os.path.join(ROOT, "node_modules")
if os.path.isdir(dependencies):
os.symlink(dependencies, os.path.join(sandbox, "node_modules"), target_is_directory=True)
applied = subprocess.run(
["git", "apply", "--whitespace=error", artifact], cwd=sandbox,
capture_output=True, text=True, timeout=30,
)
if applied.returncode != 0:
raise ValueError("goal_patch_sandbox_apply_failed:" + (applied.stderr or applied.stdout).strip()[:300])
timeout = max(30, int(os.environ.get("CASAN_GOAL_PATCH_VERIFY_TIMEOUT_SEC", "300")))
for command in commands:
result = subprocess.run(command, cwd=sandbox, capture_output=True, text=True, timeout=timeout)
output = (result.stdout + result.stderr)[-4000:]
results.append({"command": " ".join(command), "exit_code": result.returncode, "output": output})
if result.returncode != 0:
raise ValueError("goal_patch_verification_failed:" + output[-1200:])
return results
def validate_and_store_patch(job_path: str, job: dict, patch: str) -> dict: def validate_and_store_patch(job_path: str, job: dict, patch: str) -> dict:
roots = [str(value).strip("/") for value in job.get("workspace", {}).get("context_roots", [])] explicit_scope = restricted_context_paths(str(job.get("goal") or ""))
roots = [path.strip("/") for path, _ in explicit_scope] or [str(value).strip("/") for value in job.get("workspace", {}).get("context_roots", [])]
directory_roots = {path.strip("/") for path, scoped_directory in explicit_scope if scoped_directory}
changed = [] changed = []
header_paths = [] header_paths = []
for line in patch.splitlines(): for line in patch.splitlines():
@@ -514,7 +763,7 @@ def validate_and_store_patch(job_path: str, job: dict, patch: str) -> dict:
path = path[2:] path = path[2:]
if path.startswith("/") or ".." in path.split("/"): if path.startswith("/") or ".." in path.split("/"):
raise ValueError("goal_patch_path_denied") raise ValueError("goal_patch_path_denied")
if not any(path == root or path.startswith(root + "/") for root in roots): if not any(path == root or (root in directory_roots and path.startswith(root + "/")) for root in roots):
raise ValueError(f"goal_patch_outside_workspace:{path}") raise ValueError(f"goal_patch_outside_workspace:{path}")
changed.append(path) changed.append(path)
if not changed or len(set(changed)) > 20: if not changed or len(set(changed)) > 20:
@@ -529,7 +778,12 @@ def validate_and_store_patch(job_path: str, job: dict, patch: str) -> dict:
if check.returncode != 0: if check.returncode != 0:
os.unlink(artifact) os.unlink(artifact)
raise ValueError("goal_patch_check_failed:" + (check.stderr or check.stdout).strip()[:160]) raise ValueError("goal_patch_check_failed:" + (check.stderr or check.stdout).strip()[:160])
return {"path": os.path.relpath(artifact, ROOT), "sha256": sha(patch), "files": sorted(set(changed)), "bytes": len(patch.encode("utf-8")), "status": "awaiting_approval", "preview": patch[:50_000]} try:
verification = validate_patch_semantics(job, artifact, sorted(set(changed)))
except Exception:
os.unlink(artifact)
raise
return {"path": os.path.relpath(artifact, ROOT), "sha256": sha(patch), "files": sorted(set(changed)), "bytes": len(patch.encode("utf-8")), "status": "awaiting_approval", "preview": patch[:50_000], "preapproval_verification": verification}
def submit_side_effect(job: dict, manifest: dict, patch_artifact: dict) -> dict: def submit_side_effect(job: dict, manifest: dict, patch_artifact: dict) -> dict:
@@ -702,8 +956,8 @@ def reviewer_candidates(account_provider: str, cloud_model: str, local_model: st
candidates.append({"kind": "model", "provider": "omniroute", "model": value, "value": value}) candidates.append({"kind": "model", "provider": "omniroute", "model": value, "value": value})
local_reviewer = os.environ.get("CASAN_GOAL_LOCAL_REVIEWER_MODEL", "").strip() or local_model local_reviewer = os.environ.get("CASAN_GOAL_LOCAL_REVIEWER_MODEL", "").strip() or local_model
if local_reviewer: if local_reviewer and os.environ.get("CASAN_GOAL_ENABLE_LOCAL_REVIEWER", "1") == "1":
candidates.append({"kind": "model", "provider": os.environ.get("CASAN_GOAL_LOCAL_PROVIDER", "local-policy"), "model": local_reviewer, "value": local_reviewer}) candidates.append({"kind": "model", "provider": os.environ.get("CASAN_GOAL_LOCAL_REVIEWER_PROVIDER", os.environ.get("CASAN_GOAL_LOCAL_PROVIDER", "local-policy")), "model": local_reviewer, "value": local_reviewer})
unique, seen = [], set() unique, seen = [], set()
for candidate in candidates: for candidate in candidates:
@@ -714,15 +968,17 @@ def reviewer_candidates(account_provider: str, cloud_model: str, local_model: st
return unique return unique
def run_reviewer_chain(job_path: str, prompt: str, account_provider: str, cloud_model: str, local_model: str): def run_reviewer_chain(job_path: str, prompt: str, account_provider: str, cloud_model: str, local_model: str, max_output_tokens=None, excluded_models=None):
max_attempts = max(1, min(int(os.environ.get("CASAN_GOAL_REVIEWER_MAX_ATTEMPTS", "5")), 10)) max_attempts = max(1, min(int(os.environ.get("CASAN_GOAL_REVIEWER_MAX_ATTEMPTS", "5")), 10))
deadline_seconds = max(1, min(int(os.environ.get("CASAN_GOAL_REVIEWER_DEADLINE_SEC", "360")), 900)) deadline_seconds = max(1, min(int(os.environ.get("CASAN_GOAL_REVIEWER_DEADLINE_SEC", "360")), 900))
deadline = time.monotonic() + deadline_seconds deadline = time.monotonic() + deadline_seconds
ledger = [] ledger = []
update_job(job_path, reviewer_attempts=ledger) update_job(job_path, reviewer_attempts=ledger)
last_reason = "reviewer_candidates_unavailable" last_reason = "reviewer_candidates_unavailable"
candidates = reviewer_candidates(account_provider, cloud_model, local_model) excluded = set(excluded_models or [])
if len(candidates) > max_attempts and candidates[-1].get("provider") == os.environ.get("CASAN_GOAL_LOCAL_PROVIDER", "local-policy"): candidates = [candidate for candidate in reviewer_candidates(account_provider, cloud_model, local_model) if candidate.get("value") not in excluded]
local_reviewer_provider = os.environ.get("CASAN_GOAL_LOCAL_REVIEWER_PROVIDER", os.environ.get("CASAN_GOAL_LOCAL_PROVIDER", "local-policy"))
if len(candidates) > max_attempts and candidates[-1].get("provider") == local_reviewer_provider:
candidates = candidates[:max_attempts - 1] + [candidates[-1]] if max_attempts > 1 else [candidates[-1]] candidates = candidates[:max_attempts - 1] + [candidates[-1]] if max_attempts > 1 else [candidates[-1]]
else: else:
candidates = candidates[:max_attempts] candidates = candidates[:max_attempts]
@@ -734,10 +990,26 @@ def run_reviewer_chain(job_path: str, prompt: str, account_provider: str, cloud_
attempt_number = len(ledger) + 1 attempt_number = len(ledger) + 1
stage(job_path, "cloud-reviewer", "running", f"Reviewer attempt {attempt_number}/{max_attempts}", candidate["provider"], candidate["model"]) stage(job_path, "cloud-reviewer", "running", f"Reviewer attempt {attempt_number}/{max_attempts}", candidate["provider"], candidate["model"])
started_at, started_clock = now(), time.monotonic() started_at, started_clock = now(), time.monotonic()
if candidate["kind"] == "model" and max_output_tokens is not None:
healthy, preflight_reason = model_preflight(candidate["value"])
if not healthy:
reason = f"model_preflight_failed:{preflight_reason}"
ledger.append({
"attempt": attempt_number, "provider": candidate["provider"], "model": candidate["model"],
"status": "failed", "reason": reason, "retryable": True,
"started_at": started_at, "finished_at": now(),
"latency_ms": int((time.monotonic() - started_clock) * 1000),
})
update_job(job_path, reviewer_attempts=ledger)
last_reason = reason
continue
if candidate["kind"] == "account": if candidate["kind"] == "account":
ok, result, metadata, reason = call_account_model(candidate["value"], prompt, remaining) ok, result, metadata, reason = call_account_model(candidate["value"], prompt, remaining)
else: else:
ok, result, metadata, reason = call_model(candidate["value"], prompt, candidate["provider"] != os.environ.get("CASAN_GOAL_LOCAL_PROVIDER", "local-policy"), remaining) if max_output_tokens is None:
ok, result, metadata, reason = call_model(candidate["value"], prompt, candidate["provider"] != local_reviewer_provider, remaining)
else:
ok, result, metadata, reason = call_model(candidate["value"], prompt, candidate["provider"] != local_reviewer_provider, remaining, max_output_tokens=max_output_tokens)
retryable = False if ok else reviewer_failure_retryable(reason) retryable = False if ok else reviewer_failure_retryable(reason)
ledger.append({ ledger.append({
"attempt": attempt_number, "provider": candidate["provider"], "model": candidate["model"], "attempt": attempt_number, "provider": candidate["provider"], "model": candidate["model"],
@@ -846,6 +1118,12 @@ def run(job_path: str) -> int:
}) })
emit(goal_id, "H4-security", "running", "Objective and workspace snapshot passed; model outputs pending") emit(goal_id, "H4-security", "running", "Objective and workspace snapshot passed; model outputs pending")
local_healthy, local_preflight_reason = model_preflight(local_model)
if not local_healthy:
stage(job_path, "local-worker", "error", f"model_preflight_failed:{local_preflight_reason}", job.get("local_provider", ""), local_model)
emit(goal_id, "H2-tool", "error", "Local worker failed patch-generation preflight", {"provider": job.get("local_provider", ""), "model": local_model, "reason": local_preflight_reason})
raise RuntimeError(f"local_worker_preflight_failed:{local_preflight_reason}")
stage(job_path, "local-worker", "running", "Local model is developing the primary solution", job.get("local_provider", ""), local_model) stage(job_path, "local-worker", "running", "Local model is developing the primary solution", job.get("local_provider", ""), local_model)
emit(goal_id, "H2-tool", "running", "Local worker is developing a solution", {"provider": job.get("local_provider", ""), "model": local_model}) emit(goal_id, "H2-tool", "running", "Local worker is developing a solution", {"provider": job.get("local_provider", ""), "model": local_model})
output_contract = ( output_contract = (
@@ -859,7 +1137,15 @@ def run(job_path: str) -> int:
"Do not claim to inspect any filesystem outside this snapshot and do not perform side effects.\n\n" "Do not claim to inspect any filesystem outside this snapshot and do not perform side effects.\n\n"
f"OBJECTIVE:\n{safe_goal}\n\nWORKSPACE SNAPSHOT ({project_id}):\n{context_bundle}" f"OBJECTIVE:\n{safe_goal}\n\nWORKSPACE SNAPSHOT ({project_id}):\n{context_bundle}"
) )
ok, local_draft, local_meta, reason = call_model(local_model, local_prompt, False) if local_model.startswith("account:"):
ok, local_draft, local_meta, reason = call_account_model(local_model.split(":", 1)[1], local_prompt, 300)
else:
ok, local_draft, local_meta, reason = call_model(
local_model,
local_prompt,
False,
max_output_tokens=patch_output_tokens() if write_intent else None,
)
if not ok: if not ok:
stage(job_path, "local-worker", "error", reason, job.get("local_provider", ""), local_model) stage(job_path, "local-worker", "error", reason, job.get("local_provider", ""), local_model)
emit(goal_id, "H2-tool", "error", "Local worker failed", {"reason": reason}) emit(goal_id, "H2-tool", "error", "Local worker failed", {"reason": reason})
@@ -897,14 +1183,18 @@ def run(job_path: str) -> int:
stage(job_path, "local-worker", "error", reason, provider_for_model(actual_repair_model), actual_repair_model) stage(job_path, "local-worker", "error", reason, provider_for_model(actual_repair_model), actual_repair_model)
emit(goal_id, "H2-tool", "error", "Worker violated patch output contract after repair", {"reason": reason, "repair_provider": provider_for_model(actual_repair_model), "repair_model": actual_repair_model, "repair_attempts": repaired_meta.get("repair_attempts", [])}) emit(goal_id, "H2-tool", "error", "Worker violated patch output contract after repair", {"reason": reason, "repair_provider": provider_for_model(actual_repair_model), "repair_model": actual_repair_model, "repair_attempts": repaired_meta.get("repair_attempts", [])})
raise ValueError(reason) raise ValueError(reason)
stage(job_path, "local-worker", "pass", "Primary solution prepared", job.get("local_provider", ""), local_model) effective_local_model = str(local_meta.get("repair_model") or local_model)
effective_local_provider = provider_for_model(effective_local_model)
stage(job_path, "local-worker", "pass", "Primary solution prepared", effective_local_provider, effective_local_model)
update_job( update_job(
job_path, job_path,
local_draft=safe_local, local_draft=safe_local,
local_usage=local_meta, local_usage=local_meta,
patch_repair_attempts=local_meta.get("repair_attempts", []), patch_repair_attempts=local_meta.get("repair_attempts", []),
effective_local_provider=effective_local_provider,
effective_local_model=effective_local_model,
) )
emit(goal_id, "H2-tool", "pass", "Local solution prepared", {"provider": job.get("local_provider", ""), "model": local_model, **local_meta}) emit(goal_id, "H2-tool", "pass", "Primary solution prepared", {"provider": effective_local_provider, "model": effective_local_model, **local_meta})
stage(job_path, "cloud-reviewer", "running", "Independent reviewer is challenging and improving the local solution", job.get("cloud_provider", ""), cloud_model) stage(job_path, "cloud-reviewer", "running", "Independent reviewer is challenging and improving the local solution", job.get("cloud_provider", ""), cloud_model)
emit(goal_id, "H3-eval", "running", "Cloud reviewer is evaluating the local solution", {"provider": job.get("cloud_provider", ""), "model": cloud_model}) emit(goal_id, "H3-eval", "running", "Cloud reviewer is evaluating the local solution", {"provider": job.get("cloud_provider", ""), "model": cloud_model})
@@ -923,7 +1213,13 @@ def run(job_path: str) -> int:
f"LOCAL WORKER PROPOSAL:\n{safe_local[:5000]}" f"LOCAL WORKER PROPOSAL:\n{safe_local[:5000]}"
) )
cloud_ok, cloud_result, cloud_meta, cloud_reason, reviewer = run_reviewer_chain( cloud_ok, cloud_result, cloud_meta, cloud_reason, reviewer = run_reviewer_chain(
job_path, review_prompt, account_provider, cloud_model, local_model job_path,
review_prompt,
account_provider,
cloud_model,
local_model,
max_output_tokens=patch_output_tokens() if write_intent else None,
excluded_models={effective_local_model},
) )
reviewer_provider = str(reviewer.get("provider") or job.get("cloud_provider", "")) reviewer_provider = str(reviewer.get("provider") or job.get("cloud_provider", ""))
reviewer_model = str(reviewer.get("model") or cloud_model) reviewer_model = str(reviewer.get("model") or cloud_model)
@@ -965,8 +1261,13 @@ def run(job_path: str) -> int:
patch_artifact["approval_id"] = proposal["id"] patch_artifact["approval_id"] = proposal["id"]
final_status = "requires_approval" final_status = "requires_approval"
metric_status = "degraded" metric_status = "degraded"
if cloud_ok:
safe_result = "Implementation patch generated and independently reviewed. Approval is required before applying it to the workspace." safe_result = "Implementation patch generated and independently reviewed. Approval is required before applying it to the workspace."
emit(goal_id, "H7-orchestration", "blocked", "Reviewed patch awaits approval", {"proposal_id": proposal["id"], "patch_sha256": patch_artifact["sha256"]}) wait_detail = "Independently reviewed patch awaits approval"
else:
safe_result = "Implementation patch generated and validated. Automated H3 review was unavailable, so Independent Reviewer approval is required before applying it to the workspace."
wait_detail = "Validated patch awaits Independent Reviewer approval after H3 degradation"
emit(goal_id, "H7-orchestration", "blocked", wait_detail, {"proposal_id": proposal["id"], "patch_sha256": patch_artifact["sha256"], "cloud_incorporated": cloud_ok})
job = update_job(job_path, status=final_status, result=safe_result, patch_artifact=patch_artifact, approval=approval, cloud_usage=cloud_meta, finished_at=now()) job = update_job(job_path, status=final_status, result=safe_result, patch_artifact=patch_artifact, approval=approval, cloud_usage=cloud_meta, finished_at=now())
else: else:
job = update_job(job_path, status=final_status, result=safe_result, cloud_usage=cloud_meta, finished_at=now()) job = update_job(job_path, status=final_status, result=safe_result, cloud_usage=cloud_meta, finished_at=now())
@@ -97,14 +97,29 @@ def _manifest_for_files(files: list[str]) -> dict:
def verification_commands(files: list[str], manifest: dict | None = None) -> list[list[str]]: def verification_commands(files: list[str], manifest: dict | None = None) -> list[list[str]]:
project = manifest or _manifest_for_files(files) project = manifest or _manifest_for_files(files)
return [["git", "diff", "--check", "--", *files], *PROJECT_MANIFEST.verification_commands(project, files)] # `git apply --check --whitespace=error` already validates the exact patch
# before mutation. Runtime images intentionally do not need repository
# metadata, so post-apply verification is limited to manifest build/tests.
return PROJECT_MANIFEST.verification_commands(project, files)
def ready_for_apply(job: dict) -> bool:
if not job.get("patch_artifact"):
return False
if job.get("status") == "requires_approval":
return True
return (
job.get("status") == "failed"
and job.get("error") == "GOAL_APPLY_VERIFICATION_FAILED_ROLLED_BACK"
)
def execute(job_path: str, actor: str) -> dict: def execute(job_path: str, actor: str) -> dict:
job = load(job_path) job = load(job_path)
if job.get("status") != "requires_approval" or not job.get("patch_artifact"): if not ready_for_apply(job):
raise RuntimeError("GOAL_APPLY_JOB_NOT_READY") raise RuntimeError("GOAL_APPLY_JOB_NOT_READY")
proposal = verify_approval(job) proposal = verify_approval(job)
job.setdefault("approval", {})["status"] = "approved"
if proposal.get("approver") != actor: if proposal.get("approver") != actor:
raise PermissionError("GOAL_APPLY_APPROVER_IDENTITY_MISMATCH") raise PermissionError("GOAL_APPLY_APPROVER_IDENTITY_MISMATCH")
artifact = job["patch_artifact"] artifact = job["patch_artifact"]
@@ -135,7 +150,8 @@ def execute(job_path: str, actor: str) -> dict:
rollback = run(["git", "apply", "--reverse", patch_path], 30) rollback = run(["git", "apply", "--reverse", patch_path], 30)
if rollback.returncode != 0: if rollback.returncode != 0:
raise RuntimeError("GOAL_APPLY_ROLLBACK_FAILED") raise RuntimeError("GOAL_APPLY_ROLLBACK_FAILED")
job.update(status="failed", error="GOAL_APPLY_VERIFICATION_FAILED_ROLLED_BACK", verification=checks, finished_at=now(), updated_at=now()) artifact["status"] = "awaiting_apply_retry"
job.update(status="requires_approval", error="GOAL_APPLY_VERIFICATION_FAILED_ROLLED_BACK", patch_artifact=artifact, verification=checks, finished_at=now(), updated_at=now())
save(job_path, job) save(job_path, job)
raise raise
@@ -25,6 +25,22 @@ TRACE_DIR="$LOG_DIR/trace"
AUDIT_DIR="$LOG_DIR/audit" AUDIT_DIR="$LOG_DIR/audit"
SECURITY_DIR="$CASAN_HARNESS_ROOT/security" SECURITY_DIR="$CASAN_HARNESS_ROOT/security"
# Prefer the OS Python over framework/shim installations that may exist in a
# developer shell but cannot execute. The runtime image also exposes this path.
PYTHON_BIN="${CASAN_PYTHON_BIN:-}"
if [[ -z "$PYTHON_BIN" ]]; then
for candidate in /usr/bin/python3 python3 python; do
if command -v "$candidate" >/dev/null 2>&1 && "$candidate" --version >/dev/null 2>&1; then
PYTHON_BIN="$candidate"
break
fi
done
fi
if [[ -z "$PYTHON_BIN" ]]; then
echo "SECURITY_RUNTIME_UNAVAILABLE: working Python 3 interpreter not found" >&2
exit 69
fi
# Shared log taxonomy (error<warn<info<debug<trace via CASAN_LOG_LEVEL). Used to # Shared log taxonomy (error<warn<info<debug<trace via CASAN_LOG_LEVEL). Used to
# make semantic skips loud (never silent) — stderr only, stdout contract intact. # make semantic skips loud (never silent) — stderr only, stdout contract intact.
# shellcheck source=casan-log.sh # shellcheck source=casan-log.sh
@@ -59,7 +75,7 @@ new_trace_id() {
} }
json_escape() { json_escape() {
python -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null || sed 's/\\/\\\\/g; s/"/\\"/g' "$PYTHON_BIN" -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null || sed 's/\\/\\\\/g; s/"/\\"/g'
} }
hash_text() { hash_text() {
@@ -85,7 +101,7 @@ load_yaml_values() {
local file="$1" local file="$1"
local key="$2" local key="$2"
[[ -f "$file" ]] || return 0 [[ -f "$file" ]] || return 0
python - "$file" "$key" <<'PY' "$PYTHON_BIN" - "$file" "$key" <<'PY'
import re import re
import sys import sys
path, key = sys.argv[1], sys.argv[2] path, key = sys.argv[1], sys.argv[2]
@@ -98,6 +114,45 @@ with open(path, encoding="utf-8") as f:
PY PY
} }
# Load only rule patterns whose declared action matches the requested action.
# The previous generic loader returned every `pattern:` in prompt-filter.yaml,
# which accidentally promoted `require_approval`, `alert`, and `log` rules to
# hard blocks. That made ordinary source code containing methods such as
# `delete()` fail the workspace-context scan as prompt injection.
load_yaml_rule_patterns() {
local file="$1"
local requested_action="$2"
[[ -f "$file" ]] || return 0
"$PYTHON_BIN" - "$file" "$requested_action" <<'PY'
import re
import sys
path, requested_action = sys.argv[1], sys.argv[2]
pattern = None
action = None
def flush():
if pattern is not None and action == requested_action:
print(pattern)
with open(path, encoding="utf-8") as handle:
for line in handle:
if re.match(r"^\s*-\s+id:\s*", line):
flush()
pattern = None
action = None
continue
pattern_match = re.match(r'^\s*pattern:\s*"(.*)"\s*$', line)
if pattern_match:
pattern = pattern_match.group(1)
continue
action_match = re.match(r"^\s*action:\s*([A-Za-z_]+)\s*$", line)
if action_match:
action = action_match.group(1)
flush()
PY
}
TRACE_ID="$(new_trace_id)" TRACE_ID="$(new_trace_id)"
TIMESTAMP="$(timestamp)" TIMESTAMP="$(timestamp)"
CONTENT="$(cat "$INPUT_FILE")" CONTENT="$(cat "$INPUT_FILE")"
@@ -126,7 +181,7 @@ BLOCK_PATTERNS=(
while IFS= read -r pattern; do while IFS= read -r pattern; do
[[ -n "$pattern" ]] && BLOCK_PATTERNS+=("$pattern") [[ -n "$pattern" ]] && BLOCK_PATTERNS+=("$pattern")
done < <(load_yaml_values "$SECURITY_DIR/prompt-filter.yaml" "pattern") done < <(load_yaml_rule_patterns "$SECURITY_DIR/prompt-filter.yaml" "block")
APPROVAL_PATTERNS=( APPROVAL_PATTERNS=(
"delete[[:space:]].*" "delete[[:space:]].*"
@@ -169,8 +224,8 @@ NORM_CONTENT="$(normalize_for_match "$CONTENT")"
# fullwidth/zero-width/Cyrillic-lookalike obfuscation cannot split or disguise # fullwidth/zero-width/Cyrillic-lookalike obfuscation cannot split or disguise
# a blocked phrase (V3). Falls back to the raw content if python is missing. # a blocked phrase (V3). Falls back to the raw content if python is missing.
UNI_CONTENT="$CONTENT" UNI_CONTENT="$CONTENT"
if command -v python >/dev/null 2>&1; then if [[ -n "$PYTHON_BIN" ]]; then
UNI_CONTENT="$(printf '%s' "$CONTENT" | python "$SCRIPT_DIR/unicode-normalize.py" 2>/dev/null)" UNI_CONTENT="$(printf '%s' "$CONTENT" | "$PYTHON_BIN" "$SCRIPT_DIR/unicode-normalize.py" 2>/dev/null)"
[[ -n "$UNI_CONTENT" ]] || UNI_CONTENT="$CONTENT" [[ -n "$UNI_CONTENT" ]] || UNI_CONTENT="$CONTENT"
fi fi
UNI_NORM_CONTENT="$(normalize_for_match "$UNI_CONTENT")" UNI_NORM_CONTENT="$(normalize_for_match "$UNI_CONTENT")"
@@ -180,8 +235,8 @@ UNI_NORM_CONTENT="$(normalize_for_match "$UNI_CONTENT")"
# Only mostly-printable decodes survive, so random base64-looking words never # Only mostly-printable decodes survive, so random base64-looking words never
# create a false positive. # create a false positive.
DECODED_CONTENT="" DECODED_CONTENT=""
if command -v python >/dev/null 2>&1; then if [[ -n "$PYTHON_BIN" ]]; then
DECODED_CONTENT="$(printf '%s' "$CONTENT" | python "$SCRIPT_DIR/decode-suspicious.py" 2>/dev/null || true)" DECODED_CONTENT="$(printf '%s' "$CONTENT" | "$PYTHON_BIN" "$SCRIPT_DIR/decode-suspicious.py" 2>/dev/null || true)"
fi fi
# Matches a pattern against the raw (case-insensitive), leetspeak-folded, # Matches a pattern against the raw (case-insensitive), leetspeak-folded,
@@ -279,7 +334,7 @@ if [[ "$MODE" == "input" ]]; then
SEM_JSON="$TRACE_DIR/semantic-$TRACE_ID.json" SEM_JSON="$TRACE_DIR/semantic-$TRACE_ID.json"
"$SCRIPT_DIR/model-router.sh" "$INPUT_FILE" "$SEM_JSON" --role classify >/dev/null 2>&1 || true "$SCRIPT_DIR/model-router.sh" "$INPUT_FILE" "$SEM_JSON" --role classify >/dev/null 2>&1 || true
if [[ -f "$SEM_JSON" ]]; then if [[ -f "$SEM_JSON" ]]; then
SEM_VERDICT="$(python -c "import json;print(json.load(open('$SEM_JSON')).get('verdict',''))" 2>/dev/null || echo "")" SEM_VERDICT="$("$PYTHON_BIN" -c "import json;print(json.load(open('$SEM_JSON')).get('verdict',''))" 2>/dev/null || echo "")"
fi fi
fi fi
if [[ "$SEM_VERDICT" == "INJECTION" ]]; then if [[ "$SEM_VERDICT" == "INJECTION" ]]; then
@@ -302,8 +357,8 @@ fi
SAFE_CONTENT="$CONTENT" SAFE_CONTENT="$CONTENT"
# Policy-driven PII masking (source of truth: pii-rules.yaml). Built-in sed # Policy-driven PII masking (source of truth: pii-rules.yaml). Built-in sed
# masking below remains as defense-in-depth if the policy file is unavailable. # masking below remains as defense-in-depth if the policy file is unavailable.
if [[ -f "$SECURITY_DIR/pii-rules.yaml" ]] && command -v python >/dev/null 2>&1; then if [[ -f "$SECURITY_DIR/pii-rules.yaml" ]] && [[ -n "$PYTHON_BIN" ]]; then
SAFE_CONTENT="$(printf '%s' "$SAFE_CONTENT" | python "$SCRIPT_DIR/pii-mask.py" "$SECURITY_DIR/pii-rules.yaml")" SAFE_CONTENT="$(printf '%s' "$SAFE_CONTENT" | "$PYTHON_BIN" "$SCRIPT_DIR/pii-mask.py" "$SECURITY_DIR/pii-rules.yaml")"
fi fi
SAFE_CONTENT="$(printf '%s' "$SAFE_CONTENT" | sed -E "s/$EMAIL_REGEX/***MASKED_EMAIL***/g")" SAFE_CONTENT="$(printf '%s' "$SAFE_CONTENT" | sed -E "s/$EMAIL_REGEX/***MASKED_EMAIL***/g")"
SAFE_CONTENT="$(printf '%s' "$SAFE_CONTENT" | sed -E "s/$PHONE_REGEX/***MASKED_PHONE***/g")" SAFE_CONTENT="$(printf '%s' "$SAFE_CONTENT" | sed -E "s/$PHONE_REGEX/***MASKED_PHONE***/g")"
@@ -333,7 +388,7 @@ fi
INPUT_HASH="$(printf '%s' "$CONTENT" | hash_text)" INPUT_HASH="$(printf '%s' "$CONTENT" | hash_text)"
OUTPUT_HASH="$(printf '%s' "$SAFE_CONTENT" | hash_text)" OUTPUT_HASH="$(printf '%s' "$SAFE_CONTENT" | hash_text)"
RULES_JSON="$(printf '%s\n' "${MATCHED_RULES[@]:-}" | python -c 'import json,sys; print(json.dumps([x for x in sys.stdin.read().splitlines() if x]))')" RULES_JSON="$(printf '%s\n' "${MATCHED_RULES[@]:-}" | "$PYTHON_BIN" -c 'import json,sys; print(json.dumps([x for x in sys.stdin.read().splitlines() if x]))')"
TRACE_FILE="$TRACE_DIR/security-$TRACE_ID.json" TRACE_FILE="$TRACE_DIR/security-$TRACE_ID.json"
cat > "$TRACE_FILE" <<EOF cat > "$TRACE_FILE" <<EOF
@@ -26,6 +26,15 @@ def job_file(directory):
class ReviewerFallbackTests(unittest.TestCase): class ReviewerFallbackTests(unittest.TestCase):
def setUp(self):
# Production enables route preflight. Individual tests opt in explicitly
# so cached/live provider health cannot affect deterministic unit tests.
self.environment = patch.dict(
os.environ, {"CASAN_GOAL_MODEL_PREFLIGHT": "0"}, clear=False,
)
self.environment.start()
self.addCleanup(self.environment.stop)
def test_account_then_omniroute_then_local_and_persists_ledger(self): def test_account_then_omniroute_then_local_and_persists_ledger(self):
calls = [] calls = []
@@ -115,6 +124,78 @@ class ReviewerFallbackTests(unittest.TestCase):
self.assertEqual(calls, ["openai:gpt", "openai-compatible:route-a", "ollama:local"]) self.assertEqual(calls, ["openai:gpt", "openai-compatible:route-a", "ollama:local"])
self.assertFalse(job["reviewer_attempts"][0]["retryable"]) self.assertFalse(job["reviewer_attempts"][0]["retryable"])
def test_patch_reviewer_excludes_h2_model_and_receives_full_output_budget(self):
calls = []
def model(model, prompt, cloud, timeout, max_output_tokens=None):
calls.append((model, max_output_tokens))
return True, "reviewed patch", {}, "ok"
environment = {
"CASAN_GOAL_OMNIROUTE_MODELS": "route-a",
"CASAN_GOAL_REVIEWER_MAX_ATTEMPTS": "3",
"CASAN_GOAL_REVIEWER_DEADLINE_SEC": "30",
}
with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, environment, clear=False), patch.object(MODULE, "call_model", model):
path = job_file(directory)
ok, _, _, _, reviewer = MODULE.run_reviewer_chain(
path, "prompt", "", "openai:gpt-worker", "ollama:local",
max_output_tokens=8192, excluded_models={"openai:gpt-worker"},
)
self.assertTrue(ok)
self.assertEqual(reviewer["model"], "openai-compatible:route-a")
self.assertEqual(calls, [("openai-compatible:route-a", 8192)])
def test_local_reviewer_can_be_disabled_when_human_approval_is_available(self):
calls = []
def model(model, prompt, cloud, timeout):
calls.append(model)
return False, "", {}, "gateway_unavailable"
environment = {
"CASAN_GOAL_OMNIROUTE_MODELS": "route-a",
"CASAN_GOAL_ENABLE_LOCAL_REVIEWER": "0",
"CASAN_GOAL_REVIEWER_MAX_ATTEMPTS": "3",
"CASAN_GOAL_REVIEWER_DEADLINE_SEC": "30",
}
with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, environment, clear=False), patch.object(MODULE, "call_model", model):
path = job_file(directory)
ok, _, _, reason, _ = MODULE.run_reviewer_chain(path, "prompt", "", "", "ollama:ornith:9b")
self.assertFalse(ok)
self.assertEqual(reason, "gateway_unavailable")
self.assertEqual(calls, ["openai-compatible:route-a"])
def test_patch_reviewer_skips_unhealthy_route_before_full_generation(self):
calls = []
def model(model, prompt, cloud, timeout, max_output_tokens=None):
calls.append(model)
return True, "reviewed", {}, "ok"
environment = {
"CASAN_GOAL_OMNIROUTE_MODELS": "route-b",
"CASAN_GOAL_ENABLE_LOCAL_REVIEWER": "0",
"CASAN_GOAL_REVIEWER_MAX_ATTEMPTS": "3",
"CASAN_GOAL_REVIEWER_DEADLINE_SEC": "30",
}
with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, environment, clear=False), \
patch.object(MODULE, "model_preflight", side_effect=[(False, "gateway_503"), (True, "ok")]), \
patch.object(MODULE, "call_model", model):
path = job_file(directory)
ok, _, _, _, reviewer = MODULE.run_reviewer_chain(
path, "prompt", "", "openai-compatible:route-a", "ollama:ornith:9b", max_output_tokens=8192,
)
with open(path, encoding="utf-8") as handle:
job = json.load(handle)
self.assertTrue(ok)
self.assertEqual(reviewer["model"], "openai-compatible:route-b")
self.assertEqual(calls, ["openai-compatible:route-b"])
self.assertIn("model_preflight_failed", job["reviewer_attempts"][0]["reason"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
@@ -29,9 +29,79 @@ class Result:
class GoalPatchWorkflowTests(unittest.TestCase): class GoalPatchWorkflowTests(unittest.TestCase):
def setUp(self):
# Keep unit tests independent from production route-health settings.
# Tests that exercise preflight mock model_preflight explicitly.
self.environment = patch.dict(
os.environ, {"CASAN_GOAL_MODEL_PREFLIGHT": "0"}, clear=False,
)
self.environment.start()
self.addCleanup(self.environment.stop)
def test_vietnamese_completion_goal_is_write_intent(self): def test_vietnamese_completion_goal_is_write_intent(self):
self.assertTrue(ORCHESTRATOR.requests_side_effect("Hoàn thành component KeyResultDetail với form update progress đầy đủ")) self.assertTrue(ORCHESTRATOR.requests_side_effect("Hoàn thành component KeyResultDetail với form update progress đầy đủ"))
def test_phase_labeled_backend_completion_is_write_intent(self):
objective = """PHASE 1 — Hoàn thiện backend Objective và Key Result cho ứng dụng OKR.
Chỉ làm việc trong phạm vi apps/okr/backend.
Tạo patch nhưng không tự apply. Chờ Independent Reviewer phê duyệt.
"""
self.assertTrue(ORCHESTRATOR.requests_side_effect(objective))
def test_read_only_backend_audit_is_not_write_intent(self):
objective = "Rà soát backend và liệt kê các file có rủi ro. Không sửa code, không tạo patch."
self.assertFalse(ORCHESTRATOR.requests_side_effect(objective))
def test_scoped_negative_constraint_does_not_cancel_write_intent(self):
objective = """Hoàn thiện tính nguyên tử của luồng cập nhật tiến độ Key Result trong ứng dụng OKR.
Không thay đổi API contract. Chỉ trả về unified git diff và chờ Independent Reviewer phê duyệt.
"""
self.assertTrue(ORCHESTRATOR.requests_side_effect(objective))
def test_valid_worker_patch_still_reaches_human_approval_when_h3_is_unavailable(self):
diff = "diff --git a/apps/okr/backend/a.ts b/apps/okr/backend/a.ts\n--- a/apps/okr/backend/a.ts\n+++ b/apps/okr/backend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
manifest = {"file_count": 1, "characters": 10, "truncated": False, "bundle_sha256": "context-sha"}
artifact = {"path": ".specify/state/goals/default/job.patch", "sha256": "patch-sha", "files": ["apps/okr/backend/a.ts"], "status": "awaiting_approval"}
with tempfile.TemporaryDirectory() as directory:
path = os.path.join(directory, "job.json")
with open(path, "w", encoding="utf-8") as handle:
json.dump({
"id": "goal-1", "goal": "Hoàn thiện backend nhưng không thay đổi API contract.",
"project": "AINative_OKR_CASAN4", "actor": "owner", "local_provider": "ollama",
"cloud_provider": "omniroute", "workspace": {"context_roots": ["apps/okr/backend"]},
"stages": [],
}, handle)
with (
patch.dict(os.environ, {"CASAN_GOAL_LOCAL_MODEL": "ollama:worker", "CASAN_GOAL_CLOUD_MODEL": "openai-compatible:reviewer"}, clear=False),
patch.object(ORCHESTRATOR, "scan", side_effect=lambda text, mode: (True, text)),
patch.object(ORCHESTRATOR, "build_context", return_value=("snapshot", manifest, "manifest.json")),
patch.object(ORCHESTRATOR, "call_model", return_value=(True, diff, {}, "ok")),
patch.object(ORCHESTRATOR, "validate_write_output", return_value=diff),
patch.object(ORCHESTRATOR, "run_reviewer_chain", return_value=(False, "", {}, "model_output_empty", {"provider": "ollama", "model": "ollama:worker"})),
patch.object(ORCHESTRATOR, "validate_and_store_patch", return_value=artifact),
patch.object(ORCHESTRATOR, "submit_side_effect", return_value={"id": "AP-1", "status": "pending", "action": "goal.workspace.execute"}),
patch.object(ORCHESTRATOR, "emit"), patch.object(ORCHESTRATOR, "metric"),
patch.object(ORCHESTRATOR, "audit", return_value="audit-sha"),
):
exit_code = ORCHESTRATOR.run(path)
with open(path, encoding="utf-8") as handle:
job = json.load(handle)
self.assertEqual(exit_code, 0)
self.assertEqual(job["status"], "requires_approval")
self.assertEqual(job["approval"]["id"], "AP-1")
self.assertIn("Automated H3 review was unavailable", job["result"])
self.assertNotIn("independently reviewed", job["result"])
def test_source_code_delete_method_is_not_promoted_to_security_block(self):
source = "export class Service { async delete(id: number) { return this.repo.delete({ where: { id } }); } }"
with tempfile.TemporaryDirectory() as directory, \
patch.dict(os.environ, {"CASAN_STATE_ROOT": directory, "CASAN_SECURITY_STRICT": "0"}, clear=False):
allowed, safe_source = ORCHESTRATOR.scan(source, "input")
self.assertTrue(allowed)
self.assertIn("repo.delete", safe_source)
def test_extract_patch_requires_unified_diff(self): def test_extract_patch_requires_unified_diff(self):
with self.assertRaisesRegex(ValueError, "goal_patch_missing"): with self.assertRaisesRegex(ValueError, "goal_patch_missing"):
ORCHESTRATOR.extract_patch("implementation plan only") ORCHESTRATOR.extract_patch("implementation plan only")
@@ -60,6 +130,31 @@ class GoalPatchWorkflowTests(unittest.TestCase):
self.assertIn("@@ -1,1 +1,1 @@", normalized) self.assertIn("@@ -1,1 +1,1 @@", normalized)
self.assertEqual(check.call_count, 2) self.assertEqual(check.call_count, 2)
def test_patch_fragment_error_triggers_deterministic_hunk_recount(self):
wrong_counts = (
"diff --git a/a b/a\n--- a/a\n+++ b/a\n"
"@@ -1,5 +1,5 @@\n-old\n+new\n"
"@@ -10,7 +10,7 @@\n-tail-old\n+tail-new\n"
)
with patch.object(
ORCHESTRATOR,
"validate_patch_check",
side_effect=[ValueError("goal_patch_check_failed:error: patch fragment without header at line 7: @@ -10,7 +10,7 @@"), None],
) as check:
normalized = ORCHESTRATOR.validate_write_output(wrong_counts)
self.assertIn("@@ -1,1 +1,1 @@", normalized)
self.assertIn("@@ -10,1 +10,1 @@", normalized)
self.assertEqual(check.call_count, 2)
def test_patch_fragment_error_adds_recount_instruction_to_model_repair(self):
with (
patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "1", "CASAN_GOAL_PATCH_REPAIR_MODELS": "openai:gpt"}, clear=False),
patch.object(ORCHESTRATOR, "model_preflight", return_value=(True, "ok")),
patch.object(ORCHESTRATOR, "call_model", return_value=(False, "", {}, "stopped")) as call,
):
ORCHESTRATOR.repair_write_output("openai:gpt", "original", "invalid", "goal_patch_check_failed:patch fragment without header")
self.assertIn("Recompute every `@@ -old,count +new,count @@` header", call.call_args.args[1])
def test_truncated_replacement_is_not_reinterpreted_as_deletion(self): def test_truncated_replacement_is_not_reinterpreted_as_deletion(self):
truncated = "diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1,1 +1,1 @@\n-old\n" truncated = "diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1,1 +1,1 @@\n-old\n"
self.assertEqual(ORCHESTRATOR.normalize_unified_diff(truncated), truncated) self.assertEqual(ORCHESTRATOR.normalize_unified_diff(truncated), truncated)
@@ -80,6 +175,50 @@ class GoalPatchWorkflowTests(unittest.TestCase):
self.assertEqual(manifest["files"][0]["characters"], len(raw.strip())) self.assertEqual(manifest["files"][0]["characters"], len(raw.strip()))
self.assertFalse(manifest["files"][0]["truncated"]) self.assertFalse(manifest["files"][0]["truncated"])
def test_explicit_scope_paths_rank_source_ahead_of_general_architecture(self):
project = ORCHESTRATOR.registered_project("AINative_OKR_CASAN4")
objective = """PHASE 1 — Hoàn thiện backend Objective và Key Result.
Nguồn sự thật:
- docs/technical_architecture.md
- apps/okr/domain/input/okr-requirement.md
Chỉ làm việc trong:
- apps/okr/backend/src/objectives/**
- apps/okr/backend/src/key-results/**
- apps/okr/backend/prisma/schema.prisma
Tạo patch nhưng không tự apply.
"""
ranked = ORCHESTRATOR.context_candidates(project, objective)
top_paths = [row[1] for row in ranked[:10]]
self.assertTrue(any(path.startswith("apps/okr/backend/src/objectives/") for path in top_paths))
self.assertTrue(any(path.startswith("apps/okr/backend/src/key-results/") for path in top_paths))
self.assertIn("apps/okr/backend/prisma/schema.prisma", top_paths)
self.assertNotIn("docs/technical_architecture.md", top_paths)
self.assertNotIn("apps/okr/domain/input/okr-requirement.md", top_paths)
def test_explicit_only_scope_excludes_unrequested_context_files(self):
goal = """Hoàn thiện backend.
Chỉ được đọc và thay đổi:
apps/okr/backend/src/key-results/key-results.service.ts
apps/okr/backend/test/services.test.ts
Không sửa bất kỳ file nào khác.
"""
candidates = [
(2000, "apps/okr/backend/src/key-results/key-results.service.ts", "service"),
(2000, "apps/okr/backend/test/services.test.ts", "tests"),
(50, "apps/okr/backend/src/objectives/objectives.service.ts", "unrequested"),
]
project = {"domain": "OKR", "domain_root": "apps/okr", "roots": []}
with tempfile.TemporaryDirectory() as directory, \
patch.object(ORCHESTRATOR, "registered_project", return_value=project), \
patch.object(ORCHESTRATOR, "context_candidates", return_value=candidates), \
patch.object(ORCHESTRATOR, "scan", side_effect=lambda text, mode: (True, text)):
bundle, manifest, _ = ORCHESTRATOR.build_context(os.path.join(directory, "goal.json"), "okr", goal, True)
self.assertEqual([row["path"] for row in manifest["files"]], [
"apps/okr/backend/src/key-results/key-results.service.ts",
"apps/okr/backend/test/services.test.ts",
])
self.assertNotIn("unrequested", bundle)
def test_invalid_write_output_gets_one_bounded_repair_attempt(self): def test_invalid_write_output_gets_one_bounded_repair_attempt(self):
repaired = "```diff\ndiff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n```" repaired = "```diff\ndiff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n```"
with patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "1"}, clear=False), \ with patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "1"}, clear=False), \
@@ -114,6 +253,53 @@ class GoalPatchWorkflowTests(unittest.TestCase):
self.assertEqual(call.call_args.args[0], "openai:gpt-4o-mini") self.assertEqual(call.call_args.args[0], "openai:gpt-4o-mini")
self.assertEqual(call.call_args.kwargs["max_output_tokens"], ORCHESTRATOR.patch_output_tokens()) self.assertEqual(call.call_args.kwargs["max_output_tokens"], ORCHESTRATOR.patch_output_tokens())
def test_repair_skips_model_that_fails_generation_preflight(self):
repaired = "diff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
with (
patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "2", "CASAN_GOAL_PATCH_REPAIR_MODELS": "openai-compatible:auto/coding,ollama:ornith:9b"}, clear=False),
patch.object(ORCHESTRATOR, "model_preflight", side_effect=[(False, "gateway_503"), (True, "ok")]),
patch.object(ORCHESTRATOR, "call_model", return_value=(True, repaired, {}, "ok")) as call,
patch.object(ORCHESTRATOR, "validate_write_output", return_value=repaired),
):
ok, _, usage, reason = ORCHESTRATOR.repair_write_output("ollama:ornith:9b", "original", "invalid")
self.assertTrue(ok)
self.assertEqual(reason, "ok")
self.assertEqual(call.call_count, 1)
self.assertEqual(call.call_args.args[0], "ollama:ornith:9b")
self.assertIn("model_preflight_failed", usage["repair_attempts"][0]["reason"])
def test_logged_in_account_worker_is_first_patch_repair_candidate(self):
repaired = "diff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
with (
patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "2", "CASAN_GOAL_PATCH_REPAIR_MODELS": "openai-compatible:auto/coding,ollama:ornith:9b"}, clear=False),
patch.object(ORCHESTRATOR, "model_preflight", return_value=(True, "ok")),
patch.object(ORCHESTRATOR, "call_account_model", return_value=(True, repaired, {}, "ok")) as account_call,
patch.object(ORCHESTRATOR, "call_model") as routed_call,
patch.object(ORCHESTRATOR, "validate_write_output", return_value=repaired),
):
ok, _, usage, reason = ORCHESTRATOR.repair_write_output("account:codex", "original", "invalid")
self.assertTrue(ok)
self.assertEqual(reason, "ok")
self.assertEqual(account_call.call_args.args[0], "codex")
routed_call.assert_not_called()
self.assertEqual(usage["repair_attempts"][0]["provider"], "codex-account")
def test_account_codex_gets_corrective_pass_before_other_routes(self):
repaired = "diff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
with (
patch.dict(os.environ, {"CASAN_GOAL_PATCH_REPAIR_ATTEMPTS": "3", "CASAN_GOAL_PATCH_REPAIR_MODELS": "openai-compatible:auto/coding,ollama:ornith:9b"}, clear=False),
patch.object(ORCHESTRATOR, "model_preflight", return_value=(True, "ok")),
patch.object(ORCHESTRATOR, "call_account_model", side_effect=[(True, "corrupt", {}, "ok"), (True, repaired, {}, "ok")]) as account_call,
patch.object(ORCHESTRATOR, "call_model") as routed_call,
patch.object(ORCHESTRATOR, "validate_write_output", side_effect=[ValueError("goal_patch_check_failed:patch fragment without header"), repaired]),
):
ok, _, usage, reason = ORCHESTRATOR.repair_write_output("account:codex", "original", "invalid")
self.assertTrue(ok)
self.assertEqual(reason, "ok")
self.assertEqual(account_call.call_count, 2)
routed_call.assert_not_called()
self.assertEqual([row["model"] for row in usage["repair_attempts"]], ["account:codex", "account:codex"])
def test_repair_tries_next_stronger_candidate_when_first_patch_is_corrupt(self): def test_repair_tries_next_stronger_candidate_when_first_patch_is_corrupt(self):
repaired = "diff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n" repaired = "diff --git a/apps/okr/frontend/a.ts b/apps/okr/frontend/a.ts\n--- a/apps/okr/frontend/a.ts\n+++ b/apps/okr/frontend/a.ts\n@@ -1 +1 @@\n-a\n+b\n"
with ( with (
@@ -178,6 +364,16 @@ class GoalPatchWorkflowTests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, "goal_patch_outside_workspace"): with self.assertRaisesRegex(ValueError, "goal_patch_outside_workspace"):
ORCHESTRATOR.validate_and_store_patch(path, job, content) ORCHESTRATOR.validate_and_store_patch(path, job, content)
def test_patch_outside_explicit_file_scope_is_denied(self):
job = {
"goal": "Chỉ được đọc và thay đổi:\napps/okr/backend/allowed.ts\nKhông sửa bất kỳ file nào khác.",
"workspace": {"context_roots": ["apps/okr/backend"]},
}
content = "diff --git a/apps/okr/backend/other.ts b/apps/okr/backend/other.ts\n--- a/apps/okr/backend/other.ts\n+++ b/apps/okr/backend/other.ts\n@@ -1 +1 @@\n-a\n+b\n"
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(ValueError, "goal_patch_outside_workspace"):
ORCHESTRATOR.validate_and_store_patch(os.path.join(directory, "job.json"), job, content)
def test_patch_rename_source_outside_workspace_is_denied(self): def test_patch_rename_source_outside_workspace_is_denied(self):
job = {"workspace": {"context_roots": ["apps/okr/frontend"]}} job = {"workspace": {"context_roots": ["apps/okr/frontend"]}}
content = "diff --git a/package.json b/apps/okr/frontend/package.json\nsimilarity index 100%\nrename from package.json\nrename to apps/okr/frontend/package.json\n" content = "diff --git a/package.json b/apps/okr/frontend/package.json\nsimilarity index 100%\nrename from package.json\nrename to apps/okr/frontend/package.json\n"
@@ -199,9 +395,20 @@ class GoalPatchWorkflowTests(unittest.TestCase):
def test_frontend_patch_runs_build_and_tests(self): def test_frontend_patch_runs_build_and_tests(self):
commands = EXECUTOR.verification_commands(["apps/okr/frontend/src/pages/KeyResultDetail.tsx"]) commands = EXECUTOR.verification_commands(["apps/okr/frontend/src/pages/KeyResultDetail.tsx"])
self.assertFalse(any(command[:2] == ["git", "diff"] for command in commands))
self.assertIn(["npm", "run", "build", "-w", "@ainative-okr/frontend"], commands) self.assertIn(["npm", "run", "build", "-w", "@ainative-okr/frontend"], commands)
self.assertIn(["npm", "test", "-w", "@ainative-okr/frontend"], commands) self.assertIn(["npm", "test", "-w", "@ainative-okr/frontend"], commands)
def test_executor_can_retry_only_a_verified_rollback_failure(self):
artifact = {"path": "job.patch"}
self.assertTrue(EXECUTOR.ready_for_apply({"status": "requires_approval", "patch_artifact": artifact}))
self.assertTrue(EXECUTOR.ready_for_apply({
"status": "failed",
"error": "GOAL_APPLY_VERIFICATION_FAILED_ROLLED_BACK",
"patch_artifact": artifact,
}))
self.assertFalse(EXECUTOR.ready_for_apply({"status": "failed", "error": "OTHER", "patch_artifact": artifact}))
def test_service_desk_patch_uses_service_desk_manifest_commands(self): def test_service_desk_patch_uses_service_desk_manifest_commands(self):
commands = EXECUTOR.verification_commands(["apps/service-desk/src/ticket.js"]) commands = EXECUTOR.verification_commands(["apps/service-desk/src/ticket.js"])
self.assertIn(["node", "--check", "apps/service-desk/src/ticket.js"], commands) self.assertIn(["node", "--check", "apps/service-desk/src/ticket.js"], commands)
@@ -16,3 +16,10 @@
{"timestamp": "2026-07-18T05:20:08Z", "provider": "codex", "status": "success", "prompt_hash": "4df9515b74f7155effd0194c68c80dcb0288522afed800be3ee36ea28f8f7bd2", "prompt_characters": 8827, "latency_ms": 9374} {"timestamp": "2026-07-18T05:20:08Z", "provider": "codex", "status": "success", "prompt_hash": "4df9515b74f7155effd0194c68c80dcb0288522afed800be3ee36ea28f8f7bd2", "prompt_characters": 8827, "latency_ms": 9374}
{"timestamp": "2026-07-18T06:43:49Z", "provider": "codex", "status": "prompt_length_invalid", "prompt_hash": "35693f959c55dc585831ed44a715be2a34e5b072480fb07c6c6fe85602bfd89a", "prompt_characters": 30549, "latency_ms": 0} {"timestamp": "2026-07-18T06:43:49Z", "provider": "codex", "status": "prompt_length_invalid", "prompt_hash": "35693f959c55dc585831ed44a715be2a34e5b072480fb07c6c6fe85602bfd89a", "prompt_characters": 30549, "latency_ms": 0}
{"timestamp": "2026-07-18T06:47:26Z", "provider": "codex", "status": "prompt_length_invalid", "prompt_hash": "2e9a89a9d9afa26d96fa1ffef84cbcf76279e0b7cc69afa569e3198f508dd071", "prompt_characters": 30908, "latency_ms": 0} {"timestamp": "2026-07-18T06:47:26Z", "provider": "codex", "status": "prompt_length_invalid", "prompt_hash": "2e9a89a9d9afa26d96fa1ffef84cbcf76279e0b7cc69afa569e3198f508dd071", "prompt_characters": 30908, "latency_ms": 0}
{"timestamp": "2026-07-18T15:36:19Z", "provider": "codex", "status": "success", "prompt_hash": "91f089026b31117023d356636e9642f200a2d90a5721bab6e7b286cf60f1a462", "prompt_characters": 120, "latency_ms": 16116}
{"timestamp": "2026-07-18T15:38:46Z", "provider": "codex", "status": "success", "prompt_hash": "c1416254e31b9fed991bdbfa5bf18700c5dab258909344f86719b62fb5a8f26b", "prompt_characters": 207, "latency_ms": 6469}
{"timestamp": "2026-07-18T15:39:50Z", "provider": "codex", "status": "success", "prompt_hash": "b62566e3fe31ae61e416d99627cd553339ed5e9943f0086dc293dd453f27d1a4", "prompt_characters": 22099, "latency_ms": 63836}
{"timestamp": "2026-07-18T15:42:50Z", "provider": "codex", "status": "success", "prompt_hash": "5aba8aa1495f0e4761a91ed038df4c0d911ee263238a6b69b9e103e51f1a8a0a", "prompt_characters": 22155, "latency_ms": 73955}
{"timestamp": "2026-07-18T15:42:50Z", "provider": "codex", "status": "prompt_length_invalid", "prompt_hash": "90baacbab43a8860bea72914e447b752808f86faaee3086ea7e200aa999ff40b", "prompt_characters": 30755, "latency_ms": 0}
{"timestamp": "2026-07-18T16:14:42Z", "provider": "codex", "status": "prompt_length_invalid", "prompt_hash": "44e861ae34aead72b7078884ec3de81a1a063fe28baf763042075f41a41815f6", "prompt_characters": 27302, "latency_ms": 0}
{"timestamp": "2026-07-18T16:20:24Z", "provider": "codex", "status": "prompt_length_invalid", "prompt_hash": "324f4c2bf8e981127e87a220801491eeaacad31101778bac1d7bf84db8c5ba8a", "prompt_characters": 27302, "latency_ms": 0}