feat: appove and go
This commit is contained in:
+18
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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 } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,7 +625,21 @@ 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):
|
||||||
ok, output, metadata, reason = call_model(candidate, repair_prompt, candidate.startswith(("openai:", "anthropic:", "openai-compatible:")), max_output_tokens=patch_output_tokens())
|
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())
|
||||||
metadata = dict(metadata)
|
metadata = dict(metadata)
|
||||||
metadata["repair_model"] = candidate
|
metadata["repair_model"] = candidate
|
||||||
metadata["repair_attempt"] = attempt_number
|
metadata["repair_attempt"] = attempt_number
|
||||||
@@ -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"
|
||||||
safe_result = "Implementation patch generated and independently reviewed. Approval is required before applying it to the workspace."
|
if cloud_ok:
|
||||||
emit(goal_id, "H7-orchestration", "blocked", "Reviewed patch awaits approval", {"proposal_id": proposal["id"], "patch_sha256": patch_artifact["sha256"]})
|
safe_result = "Implementation patch generated and independently reviewed. Approval is required before applying it to the workspace."
|
||||||
|
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}
|
||||||
|
|||||||
Reference in New Issue
Block a user