feat: updade workspace

This commit is contained in:
thanhnv
2026-07-11 15:56:31 +09:00
parent 4fc72332f5
commit 193a449829
120 changed files with 868 additions and 350 deletions
+25
View File
@@ -146,3 +146,28 @@
{"timestamp":"2026-07-11T03:14:47Z","trace_id":"trace-1783739687-743","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"41787b8b4a629028ae972d8db1cf4ac491af928978e0469269655a140a41e20c","output_hash":"41787b8b4a629028ae972d8db1cf4ac491af928978e0469269655a140a41e20c"} {"timestamp":"2026-07-11T03:14:47Z","trace_id":"trace-1783739687-743","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"41787b8b4a629028ae972d8db1cf4ac491af928978e0469269655a140a41e20c","output_hash":"41787b8b4a629028ae972d8db1cf4ac491af928978e0469269655a140a41e20c"}
{"timestamp":"2026-07-11T03:15:44Z","trace_id":"trace-1783739744-857","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"8367afe050a5086cac52162ee740dc4f68949e6901538d86f49802ad069e1e54","output_hash":"8367afe050a5086cac52162ee740dc4f68949e6901538d86f49802ad069e1e54"} {"timestamp":"2026-07-11T03:15:44Z","trace_id":"trace-1783739744-857","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"8367afe050a5086cac52162ee740dc4f68949e6901538d86f49802ad069e1e54","output_hash":"8367afe050a5086cac52162ee740dc4f68949e6901538d86f49802ad069e1e54"}
{"timestamp":"2026-07-11T03:17:50Z","trace_id":"trace-1783739870-1068","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"744ebdf28580ca15b50dd647f69de9e8ae2288bd6267a2a792f4ca2816f856f9","output_hash":"744ebdf28580ca15b50dd647f69de9e8ae2288bd6267a2a792f4ca2816f856f9"} {"timestamp":"2026-07-11T03:17:50Z","trace_id":"trace-1783739870-1068","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"744ebdf28580ca15b50dd647f69de9e8ae2288bd6267a2a792f4ca2816f856f9","output_hash":"744ebdf28580ca15b50dd647f69de9e8ae2288bd6267a2a792f4ca2816f856f9"}
{"timestamp":"2026-07-11T03:19:04Z","trace_id":"trace-1783739944-1756","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"e806101b2d4b3cec8f28fd45c77399235e8ca2b2c1bce040ab3ed4391aa35af9","output_hash":"e806101b2d4b3cec8f28fd45c77399235e8ca2b2c1bce040ab3ed4391aa35af9"}
{"timestamp":"2026-07-11T03:19:53Z","trace_id":"trace-1783739993-1916","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"5919bfe3362dac3ae263c409e8bde9ea28c19ccb60f66a577d87dbd1c79155a9","output_hash":"5919bfe3362dac3ae263c409e8bde9ea28c19ccb60f66a577d87dbd1c79155a9"}
{"timestamp":"2026-07-11T06:13:04Z","trace_id":"trace-1783750384-563","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"9c44c4098753d99eac0564d83466196e8b06676603197bb1989f59c8cea5cfee","output_hash":"9c44c4098753d99eac0564d83466196e8b06676603197bb1989f59c8cea5cfee"}
{"timestamp":"2026-07-11T06:13:40Z","trace_id":"trace-1783750420-1381","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"4e6dfed3949d4e086a6036a774eb32c32c3a7db28ff2df7964700d6ed1827c47","output_hash":"4e6dfed3949d4e086a6036a774eb32c32c3a7db28ff2df7964700d6ed1827c47"}
{"timestamp":"2026-07-11T06:14:13Z","trace_id":"trace-1783750453-1912","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"b4ab420209da88d5049e56e761583f033a6e3a06dc4b799449fc92742971d025","output_hash":"b4ab420209da88d5049e56e761583f033a6e3a06dc4b799449fc92742971d025"}
{"timestamp":"2026-07-11T06:14:21Z","trace_id":"trace-1783750461-2695","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"9262a241dadbea3d7fa31c1ad0ef8ee1b023eaf2c18031fa909af0e7ac22c471","output_hash":"9879dfe2a22879365df9fadaf0050b3ebeb772adf2f8f6b4890326da09768dd5"}
{"timestamp":"2026-07-11T06:14:26Z","trace_id":"trace-1783750466-3503","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"beb5005900229b0238f883d36f360b0d68ccf6c8eebbee0bbf41c113f2c217b1","output_hash":"beb5005900229b0238f883d36f360b0d68ccf6c8eebbee0bbf41c113f2c217b1"}
{"timestamp":"2026-07-11T06:14:34Z","trace_id":"trace-1783750474-4286","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"b4ab420209da88d5049e56e761583f033a6e3a06dc4b799449fc92742971d025","output_hash":"b4ab420209da88d5049e56e761583f033a6e3a06dc4b799449fc92742971d025"}
{"timestamp":"2026-07-11T06:15:13Z","trace_id":"trace-1783750513-5038","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"d445befd39096a8026a34bd3284860a37e41797b21c9aad95fe18ca084c49f80","output_hash":"d445befd39096a8026a34bd3284860a37e41797b21c9aad95fe18ca084c49f80"}
{"timestamp":"2026-07-11T06:15:26Z","trace_id":"trace-1783750526-5538","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"2b1e37e270ab1f033240271acc714127b9ce72d72ad512441718b0e7d70233fe","output_hash":"2b1e37e270ab1f033240271acc714127b9ce72d72ad512441718b0e7d70233fe"}
{"timestamp":"2026-07-11T06:15:59Z","trace_id":"trace-1783750559-5676","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"92df10583db53f111389bf7f43092009479f63acbc77688b09931ab2f1826c94","output_hash":"92df10583db53f111389bf7f43092009479f63acbc77688b09931ab2f1826c94"}
{"timestamp":"2026-07-11T06:16:44Z","trace_id":"trace-1783750604-5875","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"d5305f9161e6a9924b631099d25bbfda83864774409abaea5cff8fe3b8e7078c","output_hash":"d5305f9161e6a9924b631099d25bbfda83864774409abaea5cff8fe3b8e7078c"}
{"timestamp":"2026-07-11T06:17:12Z","trace_id":"trace-1783750632-6501","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"3e5021bb069709d14b1204e08929822d9d6df26a85a88d180b667e5852c072de","output_hash":"3e5021bb069709d14b1204e08929822d9d6df26a85a88d180b667e5852c072de"}
{"timestamp":"2026-07-11T06:17:44Z","trace_id":"trace-1783750664-6635","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"85b5f71079fbab26d86b21be709e4cfecd9ffb412b8b140cb8d1a1d8661c8158","output_hash":"85b5f71079fbab26d86b21be709e4cfecd9ffb412b8b140cb8d1a1d8661c8158"}
{"timestamp":"2026-07-11T06:32:56Z","trace_id":"trace-1783751576-96","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"2b844ca284348014088be9eea9455a4bb00ce40960799ca974c239bd9a562cca","output_hash":"2b844ca284348014088be9eea9455a4bb00ce40960799ca974c239bd9a562cca"}
{"timestamp":"2026-07-11T06:32:56Z","trace_id":"trace-1783751576-562","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"5a1dcfbeb016bda513d7bcb284735be662d790cf22bdd64d3da54a2f6e362033","output_hash":"5a1dcfbeb016bda513d7bcb284735be662d790cf22bdd64d3da54a2f6e362033"}
{"timestamp":"2026-07-11T06:34:46Z","trace_id":"1f24d39b-bb2f-4f7b-be32-239649137e5a","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"2b844ca284348014088be9eea9455a4bb00ce40960799ca974c239bd9a562cca","output_hash":"2b844ca284348014088be9eea9455a4bb00ce40960799ca974c239bd9a562cca"}
{"timestamp":"2026-07-11T06:34:50Z","trace_id":"4b439015-f562-4848-86c7-05278f67fdac","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"5a1dcfbeb016bda513d7bcb284735be662d790cf22bdd64d3da54a2f6e362033","output_hash":"5a1dcfbeb016bda513d7bcb284735be662d790cf22bdd64d3da54a2f6e362033"}
{"timestamp":"2026-07-11T06:36:54Z","trace_id":"trace-1783751814-1797","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"d445befd39096a8026a34bd3284860a37e41797b21c9aad95fe18ca084c49f80","output_hash":"d445befd39096a8026a34bd3284860a37e41797b21c9aad95fe18ca084c49f80"}
{"timestamp":"2026-07-11T06:36:54Z","trace_id":"trace-1783751814-2278","harness":"H4-security","mode":"input","status":"pass","action":"allow","risk_level":"low","input_hash":"b518b88dfbe9e555e583429e9454e4eab3cf5a6b7e7aeedae2d964794ad7751c","output_hash":"b518b88dfbe9e555e583429e9454e4eab3cf5a6b7e7aeedae2d964794ad7751c"}
{"timestamp":"2026-07-11T06:38:28Z","trace_id":"trace-1783751908-3155","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"610292a955a24da58cf6019b0299ea4453b87ea336ea12b37fc732702d9e22ce","output_hash":"610292a955a24da58cf6019b0299ea4453b87ea336ea12b37fc732702d9e22ce"}
{"timestamp":"2026-07-11T06:38:58Z","trace_id":"trace-1783751938-3335","harness":"H4-security","mode":"output","status":"pass","action":"allow","risk_level":"low","input_hash":"62dd13b6753d71fd098d5929443afe324b0b8e965f43e97bf88d8eddcf797ecd","output_hash":"62dd13b6753d71fd098d5929443afe324b0b8e965f43e97bf88d8eddcf797ecd"}
{"timestamp":"2026-07-11T06:51:41Z","trace_id":"e22f32cf-615f-416c-8148-7d0c1d438dc1","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"b837d3eb2da4298e443aad246d4a8b13092af5332cd49c58a44c8ac9144db2b7","output_hash":"c56dc3b5c36c8da3652796b65c47b52f36d7a4ac0fe60dcbf5632471575369f7"}
{"timestamp":"2026-07-11T06:52:39Z","trace_id":"22155f0f-2095-4c9e-8624-4e476167d6ca","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"b837d3eb2da4298e443aad246d4a8b13092af5332cd49c58a44c8ac9144db2b7","output_hash":"c56dc3b5c36c8da3652796b65c47b52f36d7a4ac0fe60dcbf5632471575369f7"}
{"timestamp":"2026-07-11T06:53:49Z","trace_id":"d70ead8a-1f79-473b-aefe-62746cda2ac4","harness":"H4-security","mode":"input","status":"blocked","action":"block","risk_level":"high","input_hash":"578eaad95e6441e9e3221d3117d11ef179a9c041b768a1de1ac9cc8b8f1320d7","output_hash":"578eaad95e6441e9e3221d3117d11ef179a9c041b768a1de1ac9cc8b8f1320d7"}
+6
View File
@@ -29,3 +29,9 @@
{"timestamp": "2026-07-11T03:05:27Z", "trace_id": "832cc182-7bb8-4666-9bbc-c077a943e7f9", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "success", "exit_code": 0, "latency_ms": 85641, "input_tokens": 127, "output_tokens": 620, "total_tokens": 747, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "d2f690b77d00aa01559991486f7825c7a1ea402429c5638e8dbf2a40e7b6eb9a", "output_hash": "d94f7cb51d952fcaffb585e76c16542aba2f774d64c6d270eab32e7888eccc40"} {"timestamp": "2026-07-11T03:05:27Z", "trace_id": "832cc182-7bb8-4666-9bbc-c077a943e7f9", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "success", "exit_code": 0, "latency_ms": 85641, "input_tokens": 127, "output_tokens": 620, "total_tokens": 747, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "d2f690b77d00aa01559991486f7825c7a1ea402429c5638e8dbf2a40e7b6eb9a", "output_hash": "d94f7cb51d952fcaffb585e76c16542aba2f774d64c6d270eab32e7888eccc40"}
{"timestamp": "2026-07-11T03:07:54Z", "trace_id": "3fc2c4a0-32ab-4be3-b12f-ca83a65869c7", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "success", "exit_code": 0, "latency_ms": 85595, "input_tokens": 127, "output_tokens": 471, "total_tokens": 598, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "d2f690b77d00aa01559991486f7825c7a1ea402429c5638e8dbf2a40e7b6eb9a", "output_hash": "fd6094f85e823a0be31d54bc5dbecf5e174275ad8b85ca2aa56f58658789e7a3"} {"timestamp": "2026-07-11T03:07:54Z", "trace_id": "3fc2c4a0-32ab-4be3-b12f-ca83a65869c7", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "success", "exit_code": 0, "latency_ms": 85595, "input_tokens": 127, "output_tokens": 471, "total_tokens": 598, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "d2f690b77d00aa01559991486f7825c7a1ea402429c5638e8dbf2a40e7b6eb9a", "output_hash": "fd6094f85e823a0be31d54bc5dbecf5e174275ad8b85ca2aa56f58658789e7a3"}
{"timestamp": "2026-07-11T03:15:44Z", "trace_id": "d1f5f078-63dc-4f21-a4f7-c0ccab19bc3d", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "success", "exit_code": 0, "latency_ms": 140071, "input_tokens": 2406, "output_tokens": 1400, "total_tokens": 3806, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "63073e9ea949155b9fc8db84dca22a3b8a635b11ab60e45b00c5914c842473fa", "output_hash": "8367afe050a5086cac52162ee740dc4f68949e6901538d86f49802ad069e1e54"} {"timestamp": "2026-07-11T03:15:44Z", "trace_id": "d1f5f078-63dc-4f21-a4f7-c0ccab19bc3d", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "success", "exit_code": 0, "latency_ms": 140071, "input_tokens": 2406, "output_tokens": 1400, "total_tokens": 3806, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "63073e9ea949155b9fc8db84dca22a3b8a635b11ab60e45b00c5914c842473fa", "output_hash": "8367afe050a5086cac52162ee740dc4f68949e6901538d86f49802ad069e1e54"}
{"timestamp": "2026-07-11T03:19:53Z", "trace_id": "ecd5d171-5967-45b6-8823-4ec1648d75a9", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "success", "exit_code": 0, "latency_ms": 123701, "input_tokens": 2128, "output_tokens": 1400, "total_tokens": 3528, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "744ebdf28580ca15b50dd647f69de9e8ae2288bd6267a2a792f4ca2816f856f9", "output_hash": "5919bfe3362dac3ae263c409e8bde9ea28c19ccb60f66a577d87dbd1c79155a9"}
{"timestamp": "2026-07-11T06:15:59Z", "trace_id": "d5928317-30a8-434f-83f7-c344d777695e", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "success", "exit_code": 0, "latency_ms": 46051, "input_tokens": 126, "output_tokens": 213, "total_tokens": 339, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "d445befd39096a8026a34bd3284860a37e41797b21c9aad95fe18ca084c49f80", "output_hash": "92df10583db53f111389bf7f43092009479f63acbc77688b09931ab2f1826c94"}
{"timestamp": "2026-07-11T06:17:44Z", "trace_id": "41f2f9c0-3bc4-4e03-af65-b1aff6bdb593", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "success", "exit_code": 0, "latency_ms": 60390, "input_tokens": 499, "output_tokens": 454, "total_tokens": 953, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "d5305f9161e6a9924b631099d25bbfda83864774409abaea5cff8fe3b8e7078c", "output_hash": "85b5f71079fbab26d86b21be709e4cfecd9ffb412b8b140cb8d1a1d8661c8158"}
{"timestamp": "2026-07-11T06:32:57Z", "trace_id": "07fbb2de-5044-4cad-90de-ac87387d74e8", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "degraded", "exit_code": 0, "latency_ms": 647, "input_tokens": 0, "output_tokens": 0, "total_tokens": 0, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "2b844ca284348014088be9eea9455a4bb00ce40960799ca974c239bd9a562cca", "output_hash": "a96d7829c4e1453035563d1e36c0e1cc5e8aabcf18ecb79e0934a54c53df579f"}
{"timestamp": "2026-07-11T06:34:56Z", "trace_id": "092ded09-3bbf-4469-9438-f4fe5b9e3d61", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "degraded", "exit_code": 0, "latency_ms": 9940, "input_tokens": 0, "output_tokens": 0, "total_tokens": 0, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "2b844ca284348014088be9eea9455a4bb00ce40960799ca974c239bd9a562cca", "output_hash": "a96d7829c4e1453035563d1e36c0e1cc5e8aabcf18ecb79e0934a54c53df579f"}
{"timestamp": "2026-07-11T06:38:58Z", "trace_id": "8cb7ab8b-c84e-425c-a761-673888ebce72", "harness": "H6-agentops", "agent": "goal.orchestrator", "step": "local-worker-cloud-reviewer", "status": "success", "exit_code": 0, "latency_ms": 124131, "input_tokens": 2435, "output_tokens": 1400, "total_tokens": 3835, "cost_estimate": 0.0, "cost_source": "provider_usage_logs", "input_hash": "d445befd39096a8026a34bd3284860a37e41797b21c9aad95fe18ca084c49f80", "output_hash": "62dd13b6753d71fd098d5929443afe324b0b8e965f43e97bf88d8eddcf797ecd"}
@@ -44,3 +44,9 @@
{"timestamp": "2026-07-11T03:04:43Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 127, "output_tokens": 620, "total_tokens": 747, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 41710, "status": "success"} {"timestamp": "2026-07-11T03:04:43Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 127, "output_tokens": 620, "total_tokens": 747, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 41710, "status": "success"}
{"timestamp": "2026-07-11T03:07:06Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 127, "output_tokens": 471, "total_tokens": 598, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 37299, "status": "success"} {"timestamp": "2026-07-11T03:07:06Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 127, "output_tokens": 471, "total_tokens": 598, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 37299, "status": "success"}
{"timestamp": "2026-07-11T03:14:47Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 2406, "output_tokens": 1400, "total_tokens": 3806, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 82969, "status": "success"} {"timestamp": "2026-07-11T03:14:47Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 2406, "output_tokens": 1400, "total_tokens": 3806, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 82969, "status": "success"}
{"timestamp": "2026-07-11T03:19:04Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 2128, "output_tokens": 1400, "total_tokens": 3528, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 74123, "status": "success"}
{"timestamp": "2026-07-11T06:13:11Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "classify", "role": "classify", "input_tokens": 215, "output_tokens": 2, "total_tokens": 217, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 6582, "status": "success"}
{"timestamp": "2026-07-11T06:13:40Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 625, "output_tokens": 533, "total_tokens": 1158, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 28228, "status": "success"}
{"timestamp": "2026-07-11T06:15:26Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 126, "output_tokens": 213, "total_tokens": 339, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 12167, "status": "success"}
{"timestamp": "2026-07-11T06:17:12Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 499, "output_tokens": 454, "total_tokens": 953, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 27972, "status": "success"}
{"timestamp": "2026-07-11T06:38:28Z", "harness": "L5-provider-telemetry", "provider": "ollama", "model": "ornith:9b", "run_id": "adhoc", "step": "generate", "role": "generate", "input_tokens": 2435, "output_tokens": 1400, "total_tokens": 3835, "cost_usd": 0.0, "cost_source": "ollama_local_real_tokens", "latency_ms": 93084, "status": "success"}
+21 -21
View File
@@ -205,13 +205,13 @@ export default {
### Controller Rules: ### Controller Rules:
- **Location:** Must be in `backend/src/[module-name]/[module-name].controller.ts`. - **Location:** Must be in `apps/okr/backend/src/[module-name]/[module-name].controller.ts`.
- **Responsibility:** Keep controllers "thin". They only receive requests, trigger guards, validate DTOs, and call a single service method. - **Responsibility:** Keep controllers "thin". They only receive requests, trigger guards, validate DTOs, and call a single service method.
- **Auth:** Use `@UseGuards(JwtAuthGuard)` and `@Roles()` decorator on all protected routes. - **Auth:** Use `@UseGuards(JwtAuthGuard)` and `@Roles()` decorator on all protected routes.
### Service Rules: ### Service Rules:
- **Location:** Must be in `backend/src/[module-name]/[module-name].service.ts`. - **Location:** Must be in `apps/okr/backend/src/[module-name]/[module-name].service.ts`.
- **Responsibility:** All business logic lives here. - **Responsibility:** All business logic lives here.
- **Key Logic:** - **Key Logic:**
- Use Prisma client for all DB operations — **no raw SQL** in application code. - Use Prisma client for all DB operations — **no raw SQL** in application code.
@@ -220,14 +220,14 @@ export default {
### Prisma Schema Rules: ### Prisma Schema Rules:
- **Single source of truth:** `backend/prisma/schema.prisma` defines ALL tables. - **Single source of truth:** `apps/okr/backend/prisma/schema.prisma` defines ALL tables.
- **Migrations:** Use `npx prisma migrate dev --name <migration-name>` — never edit migration files manually. - **Migrations:** Use `npx prisma migrate dev --name <migration-name>` — never edit migration files manually.
- **Types:** Always use Prisma-generated types (`Prisma.ObjectiveCreateInput`, `Prisma.KeyResultUpdateInput`). - **Types:** Always use Prisma-generated types (`Prisma.ObjectiveCreateInput`, `Prisma.KeyResultUpdateInput`).
### Module Structure (OKR Domain): ### Module Structure (OKR Domain):
``` ```
backend/src/ apps/okr/backend/src/
├── auth/ # JWT login, refresh token endpoints ├── auth/ # JWT login, refresh token endpoints
├── users/ # User CRUD (Admin/Manager only) ├── users/ # User CRUD (Admin/Manager only)
├── objectives/ # Objective CRUD, filtering by quarter/owner/status ├── objectives/ # Objective CRUD, filtering by quarter/owner/status
@@ -238,12 +238,12 @@ backend/src/
### Database Seed Management: ### Database Seed Management:
- **CRITICAL:** After completing backend code with schema changes, **ALWAYS** update the seed file. - **CRITICAL:** After completing backend code with schema changes, **ALWAYS** update the seed file.
- **Seed file location:** `backend/prisma/seed.ts` - **Seed file location:** `apps/okr/backend/prisma/seed.ts`
- **Execution:** `npx prisma db seed` (or automatically on container start — always seeded in workshop environment) - **Execution:** `npx prisma db seed` (or automatically on container start — always seeded in workshop environment)
- **Idempotency:** Use Prisma `upsert` keyed on stable identifiers — running seed twice must produce no duplicates. - **Idempotency:** Use Prisma `upsert` keyed on stable identifiers — running seed twice must produce no duplicates.
```typescript ```typescript
// backend/prisma/seed.ts // apps/okr/backend/prisma/seed.ts
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
@@ -324,7 +324,7 @@ main()
### Routing Rules (React Router DOM v6): ### Routing Rules (React Router DOM v6):
```tsx ```tsx
// frontend/src/App.tsx — route structure // apps/okr/frontend/src/App.tsx — route structure
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route element={<ProtectedRoute />}> <Route element={<ProtectedRoute />}>
@@ -340,14 +340,14 @@ main()
### Component & File Location Rules: ### Component & File Location Rules:
- **Route-level pages:** `frontend/src/pages/` (Login, Dashboard, OKRDetail, CreateObjective, KeyResultDetail) - **Route-level pages:** `apps/okr/frontend/src/pages/` (Login, Dashboard, OKRDetail, CreateObjective, KeyResultDetail)
- **Layout components:** `frontend/src/components/layout/` (Sidebar, Header, AppLayout) - **Layout components:** `apps/okr/frontend/src/components/layout/` (Sidebar, Header, AppLayout)
- **Reusable UI components:** `frontend/src/components/ui/` (Button, ProgressBar, Badge, Table) - **Reusable UI components:** `apps/okr/frontend/src/components/ui/` (Button, ProgressBar, Badge, Table)
- **Custom hooks:** `frontend/src/hooks/` (useAuth, useObjectives, useKeyResults) - **Custom hooks:** `apps/okr/frontend/src/hooks/` (useAuth, useObjectives, useKeyResults)
- **API client:** `frontend/src/lib/api.ts` — all Axios calls go here - **API client:** `apps/okr/frontend/src/lib/api.ts` — all Axios calls go here
- **Query client config:** `frontend/src/lib/queryClient.ts` - **Query client config:** `apps/okr/frontend/src/lib/queryClient.ts`
- **Zod schemas:** `frontend/src/schemas/` - **Zod schemas:** `apps/okr/frontend/src/schemas/`
- **TypeScript interfaces:** `frontend/src/types/` - **TypeScript interfaces:** `apps/okr/frontend/src/types/`
### Layout Construction Rules: ### Layout Construction Rules:
@@ -389,7 +389,7 @@ main()
### API Call Rules: ### API Call Rules:
- All functions that make network requests must be in `frontend/src/lib/api.ts`. - All functions that make network requests must be in `apps/okr/frontend/src/lib/api.ts`.
- Components call functions from `lib/api.ts` — they **never** call Axios directly. - Components call functions from `lib/api.ts` — they **never** call Axios directly.
- Base URL: `import.meta.env.VITE_API_BASE_URL` (e.g., `http://localhost:3000/api/v1`). - Base URL: `import.meta.env.VITE_API_BASE_URL` (e.g., `http://localhost:3000/api/v1`).
- Auth tokens are in HttpOnly cookies — do **not** manually attach `Authorization` headers. - Auth tokens are in HttpOnly cookies — do **not** manually attach `Authorization` headers.
@@ -397,7 +397,7 @@ main()
### Form Rules (React Hook Form + Zod): ### Form Rules (React Hook Form + Zod):
```typescript ```typescript
// frontend/src/schemas/objective.schema.ts // apps/okr/frontend/src/schemas/objective.schema.ts
import { z } from 'zod'; import { z } from 'zod';
export const createObjectiveSchema = z.object({ export const createObjectiveSchema = z.object({
@@ -455,7 +455,7 @@ const { register, handleSubmit, formState: { errors } } = useForm<CreateObjectiv
meta?: { page: number; limit: number; total: number }; meta?: { page: number; limit: number; total: number };
} }
// ✅ OKR domain types (frontend/src/types/okr.types.ts) // ✅ OKR domain types (apps/okr/frontend/src/types/okr.types.ts)
interface Objective { interface Objective {
id: number; id: number;
title: string; title: string;
@@ -499,11 +499,11 @@ const { register, handleSubmit, formState: { errors } } = useForm<CreateObjectiv
### Type Consistency Rules: ### Type Consistency Rules:
- **Frontend-Backend Alignment:** DTOs in backend must have matching interfaces in `frontend/src/types/`. - **Frontend-Backend Alignment:** DTOs in backend must have matching interfaces in `apps/okr/frontend/src/types/`.
- **Enum Consistency:** Role (`ADMIN | MANAGER | EMPLOYEE`) and Status (`NOT_STARTED | IN_PROGRESS | COMPLETED`) enums must be identical between frontend and backend. - **Enum Consistency:** Role (`ADMIN | MANAGER | EMPLOYEE`) and Status (`NOT_STARTED | IN_PROGRESS | COMPLETED`) enums must be identical between frontend and backend.
- **API Response Types:** Every API endpoint must have typed response interfaces matching the standard envelope `{ success, data, meta? }`. - **API Response Types:** Every API endpoint must have typed response interfaces matching the standard envelope `{ success, data, meta? }`.
- **Component Props:** Every component must have a properly typed props interface. - **Component Props:** Every component must have a properly typed props interface.
- **Zod Schemas:** Schemas in `frontend/src/schemas/` must align with backend `class-validator` rules on the corresponding DTO. - **Zod Schemas:** Schemas in `apps/okr/frontend/src/schemas/` must align with backend `class-validator` rules on the corresponding DTO.
### Type Verification Checklist: ### Type Verification Checklist:
@@ -511,7 +511,7 @@ Before submitting any code, verify:
- [ ] No `any` types used - [ ] No `any` types used
- [ ] All component props properly typed - [ ] All component props properly typed
- [ ] API calls have typed parameters and responses - [ ] API calls have typed parameters and responses
- [ ] DTOs match between frontend/backend - [ ] DTOs match between apps/okr/frontend/backend
- [ ] Role/Status enum values consistent across codebase - [ ] Role/Status enum values consistent across codebase
- [ ] Optional vs required properties correctly defined - [ ] Optional vs required properties correctly defined
- [ ] Zod schemas align with backend `class-validator` rules - [ ] Zod schemas align with backend `class-validator` rules
+21 -21
View File
@@ -205,13 +205,13 @@ export default {
### Controller Rules: ### Controller Rules:
- **Location:** Must be in `backend/src/[module-name]/[module-name].controller.ts`. - **Location:** Must be in `apps/okr/backend/src/[module-name]/[module-name].controller.ts`.
- **Responsibility:** Keep controllers "thin". They only receive requests, trigger guards, validate DTOs, and call a single service method. - **Responsibility:** Keep controllers "thin". They only receive requests, trigger guards, validate DTOs, and call a single service method.
- **Auth:** Use `@UseGuards(JwtAuthGuard)` and `@Roles()` decorator on all protected routes. - **Auth:** Use `@UseGuards(JwtAuthGuard)` and `@Roles()` decorator on all protected routes.
### Service Rules: ### Service Rules:
- **Location:** Must be in `backend/src/[module-name]/[module-name].service.ts`. - **Location:** Must be in `apps/okr/backend/src/[module-name]/[module-name].service.ts`.
- **Responsibility:** All business logic lives here. - **Responsibility:** All business logic lives here.
- **Key Logic:** - **Key Logic:**
- Use Prisma client for all DB operations — **no raw SQL** in application code. - Use Prisma client for all DB operations — **no raw SQL** in application code.
@@ -220,14 +220,14 @@ export default {
### Prisma Schema Rules: ### Prisma Schema Rules:
- **Single source of truth:** `backend/prisma/schema.prisma` defines ALL tables. - **Single source of truth:** `apps/okr/backend/prisma/schema.prisma` defines ALL tables.
- **Migrations:** Use `npx prisma migrate dev --name <migration-name>` — never edit migration files manually. - **Migrations:** Use `npx prisma migrate dev --name <migration-name>` — never edit migration files manually.
- **Types:** Always use Prisma-generated types (`Prisma.ObjectiveCreateInput`, `Prisma.KeyResultUpdateInput`). - **Types:** Always use Prisma-generated types (`Prisma.ObjectiveCreateInput`, `Prisma.KeyResultUpdateInput`).
### Module Structure (OKR Domain): ### Module Structure (OKR Domain):
``` ```
backend/src/ apps/okr/backend/src/
├── auth/ # JWT login, refresh token endpoints ├── auth/ # JWT login, refresh token endpoints
├── users/ # User CRUD (Admin/Manager only) ├── users/ # User CRUD (Admin/Manager only)
├── objectives/ # Objective CRUD, filtering by quarter/owner/status ├── objectives/ # Objective CRUD, filtering by quarter/owner/status
@@ -238,12 +238,12 @@ backend/src/
### Database Seed Management: ### Database Seed Management:
- **CRITICAL:** After completing backend code with schema changes, **ALWAYS** update the seed file. - **CRITICAL:** After completing backend code with schema changes, **ALWAYS** update the seed file.
- **Seed file location:** `backend/prisma/seed.ts` - **Seed file location:** `apps/okr/backend/prisma/seed.ts`
- **Execution:** `npx prisma db seed` (or automatically on container start — always seeded in workshop environment) - **Execution:** `npx prisma db seed` (or automatically on container start — always seeded in workshop environment)
- **Idempotency:** Use Prisma `upsert` keyed on stable identifiers — running seed twice must produce no duplicates. - **Idempotency:** Use Prisma `upsert` keyed on stable identifiers — running seed twice must produce no duplicates.
```typescript ```typescript
// backend/prisma/seed.ts // apps/okr/backend/prisma/seed.ts
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
@@ -324,7 +324,7 @@ main()
### Routing Rules (React Router DOM v6): ### Routing Rules (React Router DOM v6):
```tsx ```tsx
// frontend/src/App.tsx — route structure // apps/okr/frontend/src/App.tsx — route structure
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route element={<ProtectedRoute />}> <Route element={<ProtectedRoute />}>
@@ -340,14 +340,14 @@ main()
### Component & File Location Rules: ### Component & File Location Rules:
- **Route-level pages:** `frontend/src/pages/` (Login, Dashboard, OKRDetail, CreateObjective, KeyResultDetail) - **Route-level pages:** `apps/okr/frontend/src/pages/` (Login, Dashboard, OKRDetail, CreateObjective, KeyResultDetail)
- **Layout components:** `frontend/src/components/layout/` (Sidebar, Header, AppLayout) - **Layout components:** `apps/okr/frontend/src/components/layout/` (Sidebar, Header, AppLayout)
- **Reusable UI components:** `frontend/src/components/ui/` (Button, ProgressBar, Badge, Table) - **Reusable UI components:** `apps/okr/frontend/src/components/ui/` (Button, ProgressBar, Badge, Table)
- **Custom hooks:** `frontend/src/hooks/` (useAuth, useObjectives, useKeyResults) - **Custom hooks:** `apps/okr/frontend/src/hooks/` (useAuth, useObjectives, useKeyResults)
- **API client:** `frontend/src/lib/api.ts` — all Axios calls go here - **API client:** `apps/okr/frontend/src/lib/api.ts` — all Axios calls go here
- **Query client config:** `frontend/src/lib/queryClient.ts` - **Query client config:** `apps/okr/frontend/src/lib/queryClient.ts`
- **Zod schemas:** `frontend/src/schemas/` - **Zod schemas:** `apps/okr/frontend/src/schemas/`
- **TypeScript interfaces:** `frontend/src/types/` - **TypeScript interfaces:** `apps/okr/frontend/src/types/`
### Layout Construction Rules: ### Layout Construction Rules:
@@ -389,7 +389,7 @@ main()
### API Call Rules: ### API Call Rules:
- All functions that make network requests must be in `frontend/src/lib/api.ts`. - All functions that make network requests must be in `apps/okr/frontend/src/lib/api.ts`.
- Components call functions from `lib/api.ts` — they **never** call Axios directly. - Components call functions from `lib/api.ts` — they **never** call Axios directly.
- Base URL: `import.meta.env.VITE_API_BASE_URL` (e.g., `http://localhost:3000/api/v1`). - Base URL: `import.meta.env.VITE_API_BASE_URL` (e.g., `http://localhost:3000/api/v1`).
- Auth tokens are in HttpOnly cookies — do **not** manually attach `Authorization` headers. - Auth tokens are in HttpOnly cookies — do **not** manually attach `Authorization` headers.
@@ -397,7 +397,7 @@ main()
### Form Rules (React Hook Form + Zod): ### Form Rules (React Hook Form + Zod):
```typescript ```typescript
// frontend/src/schemas/objective.schema.ts // apps/okr/frontend/src/schemas/objective.schema.ts
import { z } from 'zod'; import { z } from 'zod';
export const createObjectiveSchema = z.object({ export const createObjectiveSchema = z.object({
@@ -455,7 +455,7 @@ const { register, handleSubmit, formState: { errors } } = useForm<CreateObjectiv
meta?: { page: number; limit: number; total: number }; meta?: { page: number; limit: number; total: number };
} }
// ✅ OKR domain types (frontend/src/types/okr.types.ts) // ✅ OKR domain types (apps/okr/frontend/src/types/okr.types.ts)
interface Objective { interface Objective {
id: number; id: number;
title: string; title: string;
@@ -499,11 +499,11 @@ const { register, handleSubmit, formState: { errors } } = useForm<CreateObjectiv
### Type Consistency Rules: ### Type Consistency Rules:
- **Frontend-Backend Alignment:** DTOs in backend must have matching interfaces in `frontend/src/types/`. - **Frontend-Backend Alignment:** DTOs in backend must have matching interfaces in `apps/okr/frontend/src/types/`.
- **Enum Consistency:** Role (`ADMIN | MANAGER | EMPLOYEE`) and Status (`NOT_STARTED | IN_PROGRESS | COMPLETED`) enums must be identical between frontend and backend. - **Enum Consistency:** Role (`ADMIN | MANAGER | EMPLOYEE`) and Status (`NOT_STARTED | IN_PROGRESS | COMPLETED`) enums must be identical between frontend and backend.
- **API Response Types:** Every API endpoint must have typed response interfaces matching the standard envelope `{ success, data, meta? }`. - **API Response Types:** Every API endpoint must have typed response interfaces matching the standard envelope `{ success, data, meta? }`.
- **Component Props:** Every component must have a properly typed props interface. - **Component Props:** Every component must have a properly typed props interface.
- **Zod Schemas:** Schemas in `frontend/src/schemas/` must align with backend `class-validator` rules on the corresponding DTO. - **Zod Schemas:** Schemas in `apps/okr/frontend/src/schemas/` must align with backend `class-validator` rules on the corresponding DTO.
### Type Verification Checklist: ### Type Verification Checklist:
@@ -511,7 +511,7 @@ Before submitting any code, verify:
- [ ] No `any` types used - [ ] No `any` types used
- [ ] All component props properly typed - [ ] All component props properly typed
- [ ] API calls have typed parameters and responses - [ ] API calls have typed parameters and responses
- [ ] DTOs match between frontend/backend - [ ] DTOs match between apps/okr/frontend/backend
- [ ] Role/Status enum values consistent across codebase - [ ] Role/Status enum values consistent across codebase
- [ ] Optional vs required properties correctly defined - [ ] Optional vs required properties correctly defined
- [ ] Zod schemas align with backend `class-validator` rules - [ ] Zod schemas align with backend `class-validator` rules
+12 -12
View File
@@ -6,16 +6,16 @@ WORKDIR /app
RUN apt-get update -qq && apt-get install -y -qq openssl python3 && rm -rf /var/lib/apt/lists/* RUN apt-get update -qq && apt-get install -y -qq openssl python3 && rm -rf /var/lib/apt/lists/*
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
COPY backend/package.json ./backend/ COPY apps/okr/backend/package.json ./apps/okr/backend/
COPY frontend/package.json ./frontend/ COPY apps/okr/frontend/package.json ./apps/okr/frontend/
COPY backend/prisma ./backend/prisma COPY apps/okr/backend/prisma ./apps/okr/backend/prisma
RUN npm ci RUN npm ci
COPY backend/src ./backend/src COPY apps/okr/backend/src ./apps/okr/backend/src
COPY backend/tsconfig*.json ./backend/ COPY apps/okr/backend/tsconfig*.json ./apps/okr/backend/
RUN cd backend && npx prisma generate && npx tsc -p tsconfig.build.json RUN cd apps/okr/backend && npx prisma generate && npx tsc -p tsconfig.build.json
# ─── Stage 2: Runtime ──────────────────────────────────────────────────────── # ─── Stage 2: Runtime ────────────────────────────────────────────────────────
FROM node:20-slim AS runtime FROM node:20-slim AS runtime
@@ -25,9 +25,9 @@ WORKDIR /app
RUN apt-get update -qq && apt-get install -y -qq openssl && rm -rf /var/lib/apt/lists/* RUN apt-get update -qq && apt-get install -y -qq openssl && rm -rf /var/lib/apt/lists/*
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
COPY backend/package.json ./backend/ COPY apps/okr/backend/package.json ./apps/okr/backend/
COPY frontend/package.json ./frontend/ COPY apps/okr/frontend/package.json ./apps/okr/frontend/
COPY backend/prisma ./backend/prisma COPY apps/okr/backend/prisma ./apps/okr/backend/prisma
# Reuse the builder's node_modules: it already contains bcrypt's compiled # Reuse the builder's node_modules: it already contains bcrypt's compiled
# native addon (built WITH install scripts) and the generated Prisma client. # native addon (built WITH install scripts) and the generated Prisma client.
@@ -37,11 +37,11 @@ COPY backend/prisma ./backend/prisma
# node:20-slim base, so the native binaries are ABI/platform-compatible. # node:20-slim base, so the native binaries are ABI/platform-compatible.
COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/backend/dist ./backend/dist COPY --from=builder /app/apps/okr/backend/dist ./apps/okr/backend/dist
COPY backend/entrypoint.sh /entrypoint.sh COPY apps/okr/backend/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh
WORKDIR /app/backend WORKDIR /app/apps/okr/backend
EXPOSE 3001 EXPOSE 3001
+9
View File
@@ -26,6 +26,15 @@ COPY --from=builder /app/packages/casan-control-panel/backend/dist ./packages/ca
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
# Read-only, build-time workspace snapshots. Goal orchestration resolves only roots
# registered in project-registry.json and never accepts a browser-supplied path.
COPY apps/okr/domain ./apps/okr/domain
COPY apps/service-desk ./apps/service-desk
COPY apps/okr/frontend ./apps/okr/frontend
COPY apps/okr/backend ./apps/okr/backend
COPY docs/technical_architecture.md ./docs/technical_architecture.md
WORKDIR /app/packages/casan-control-panel/backend WORKDIR /app/packages/casan-control-panel/backend
+6 -6
View File
@@ -10,19 +10,19 @@ ARG VITE_API_BASE_URL=/api/v1
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
COPY frontend/package.json ./frontend/ COPY apps/okr/frontend/package.json ./apps/okr/frontend/
COPY backend/package.json ./backend/ COPY apps/okr/backend/package.json ./apps/okr/backend/
RUN npm ci -w frontend RUN npm ci -w @ainative-okr/frontend
COPY frontend ./frontend COPY apps/okr/frontend ./apps/okr/frontend
RUN npm run build -w frontend RUN npm run build -w @ainative-okr/frontend
# ─── Stage 2: nginx runtime ────────────────────────────────────────────────── # ─── Stage 2: nginx runtime ──────────────────────────────────────────────────
FROM nginx:alpine AS runtime FROM nginx:alpine AS runtime
COPY --from=builder /app/frontend/dist /usr/share/nginx/html COPY --from=builder /app/apps/okr/frontend/dist /usr/share/nginx/html
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80 EXPOSE 80
+7 -7
View File
@@ -62,7 +62,7 @@ Apply AI to the SDLC process to automate and optimize the creation of software p
| **AI Agent Logs** | `docs/output/output_logs/` | Execution logs from AI agents | | **AI Agent Logs** | `docs/output/output_logs/` | Execution logs from AI agents |
| **SRS-Systems** | `docs/output/srs-systems/` | System-wide SRS (generated once during input clarification phase, not part of the main flow steps) | | **SRS-Systems** | `docs/output/srs-systems/` | System-wide SRS (generated once during input clarification phase, not part of the main flow steps) |
| **Spec-Kit Artifacts** | `specs/[FEATURE_NAME]/` | Feature artifacts generated by Spec-Kit agents (`spec.md`, `plan.md`, `tasks.md`, contracts, checklists) | | **Spec-Kit Artifacts** | `specs/[FEATURE_NAME]/` | Feature artifacts generated by Spec-Kit agents (`spec.md`, `plan.md`, `tasks.md`, contracts, checklists) |
| **Source Code** | `backend/` + `frontend/` | Application source code (NestJS backend + React frontend) | | **Source Code** | `apps/okr/backend/` + `apps/okr/frontend/` | Application source code (NestJS backend + React frontend) |
--- ---
@@ -195,7 +195,7 @@ The entire flow is orchestrated by **`okr.bossbuiltin`**, which executes all ste
- `docs/output/output_logs/` — AI agent execution logs - `docs/output/output_logs/` — AI agent execution logs
- `docs/output/srs-systems/` — System-wide SRS (generated once) - `docs/output/srs-systems/` — System-wide SRS (generated once)
- `specs/[FEATURE_NAME]/` — Spec-Kit artifacts (`spec.md`, `plan.md`, `tasks.md`, etc.) - `specs/[FEATURE_NAME]/` — Spec-Kit artifacts (`spec.md`, `plan.md`, `tasks.md`, etc.)
- `backend/` + `frontend/` — Source code (NestJS backend + React frontend) - `apps/okr/backend/` + `apps/okr/frontend/` — Source code (NestJS backend + React frontend)
--- ---
@@ -259,9 +259,9 @@ The entire flow is orchestrated by **`okr.bossbuiltin`**, which executes all ste
├── e2e/ # End-to-end tests (Playwright) ├── e2e/ # End-to-end tests (Playwright)
│ └── auth/ # Auth E2E tests │ └── auth/ # Auth E2E tests
│ │
├── frontend/ # React + Vite SPA (source code) ├── apps/okr/frontend/ # React + Vite SPA (source code)
│ │
├── backend/ # NestJS API service (source code) ├── apps/okr/backend/ # NestJS API service (source code)
│ │
├── docker/ # Docker utilities ├── docker/ # Docker utilities
└── docker-compose.yml # Docker Compose orchestration └── docker-compose.yml # Docker Compose orchestration
@@ -309,9 +309,9 @@ Each folder contains `spec.md`, `plan.md`, `tasks.md`, `checklists/`, and `contr
#### Source Code #### Source Code
Source code generated by Agents during `speckit.implement` (STEP 10). Source code generated by Agents during `speckit.implement` (STEP 10).
`backend/` and `frontend/` contain the application source code, co-located with the monorepo root: `apps/okr/backend/` and `apps/okr/frontend/` contain the application source code, co-located with the monorepo root:
- **`backend/`** — NestJS application. `src/` contains feature modules (`auth/`, `users/`, `objectives/`, `workspaces/`, `common/`). `prisma/` contains `schema.prisma`, migrations, and `seed.ts`. `test/` contains unit tests organized by module (`auth/`, `objectives/`, `users/`). Includes `Dockerfile`. - **`apps/okr/backend/`** — NestJS application. `src/` contains feature modules (`auth/`, `users/`, `objectives/`, `workspaces/`, `common/`). `prisma/` contains `schema.prisma`, migrations, and `seed.ts`. `test/` contains unit tests organized by module (`auth/`, `objectives/`, `users/`). Includes `Dockerfile`.
- **`frontend/`** — React + Vite SPA (TypeScript). `src/pages/` for route-level components (Login, ForgotPassword, Dashboard, CreateObjective, EditObjective, ObjectiveDetail, KeyResultDetail); `src/components/` organized by feature (`auth/`, `dashboard/`, `objective-detail/`, `objective-form/`), `layout/` (Sidebar, AppHeader, AppLayout) and `ui/` (Button, Input, Alert). `src/hooks/` (useAuth, useObjectives, useUsers, useWorkspaces), `src/lib/` (api, queryClient), `src/schemas/`, `src/types/`. `test/` contains unit tests (`hooks/`, `pages/`). Includes `Dockerfile`. - **`apps/okr/frontend/`** — React + Vite SPA (TypeScript). `src/pages/` for route-level components (Login, ForgotPassword, Dashboard, CreateObjective, EditObjective, ObjectiveDetail, KeyResultDetail); `src/components/` organized by feature (`auth/`, `dashboard/`, `objective-detail/`, `objective-form/`), `layout/` (Sidebar, AppHeader, AppLayout) and `ui/` (Button, Input, Alert). `src/hooks/` (useAuth, useObjectives, useUsers, useWorkspaces), `src/lib/` (api, queryClient), `src/schemas/`, `src/types/`. `test/` contains unit tests (`hooks/`, `pages/`). Includes `Dockerfile`.
- **`e2e/`** — End-to-end tests using Playwright, organized by feature (e.g., `auth/auth.spec.ts`). - **`e2e/`** — End-to-end tests using Playwright, organized by feature (e.g., `auth/auth.spec.ts`).
- **`docker/`** — Docker utilities. Main orchestration is in `docker-compose.yml` at the project root. - **`docker/`** — Docker utilities. Main orchestration is in `docker-compose.yml` at the project root.
+32 -32
View File
@@ -2,68 +2,68 @@
"FR-01": { "FR-01": {
"name": "Login", "name": "Login",
"code": [ "code": [
"backend/src/auth/auth.controller.ts", "apps/okr/backend/src/auth/auth.controller.ts",
{ "file": "backend/src/auth/auth.service.ts", "symbols": ["AuthService", "login"] }, { "file": "apps/okr/backend/src/auth/auth.service.ts", "symbols": ["AuthService", "login"] },
"frontend/src/pages/Login.tsx", "apps/okr/frontend/src/pages/Login.tsx",
"frontend/src/hooks/useAuth.tsx" "apps/okr/frontend/src/hooks/useAuth.tsx"
], ],
"tests": [ "tests": [
"backend/test/services.test.ts", "apps/okr/backend/test/services.test.ts",
"backend/test/e2e.test.ts" "apps/okr/backend/test/e2e.test.ts"
] ]
}, },
"FR-02": { "FR-02": {
"name": "Create Objective", "name": "Create Objective",
"code": [ "code": [
"backend/src/objectives/objectives.controller.ts", "apps/okr/backend/src/objectives/objectives.controller.ts",
{ "file": "backend/src/objectives/objectives.service.ts", "symbols": ["ObjectivesService", "create"] }, { "file": "apps/okr/backend/src/objectives/objectives.service.ts", "symbols": ["ObjectivesService", "create"] },
"frontend/src/pages/CreateObjective.tsx", "apps/okr/frontend/src/pages/CreateObjective.tsx",
"frontend/src/schemas/objective.schema.ts" "apps/okr/frontend/src/schemas/objective.schema.ts"
], ],
"tests": [ "tests": [
"backend/test/services.test.ts", "apps/okr/backend/test/services.test.ts",
"backend/test/e2e.test.ts", "apps/okr/backend/test/e2e.test.ts",
"frontend/src/__tests__/okr.test.tsx" "apps/okr/frontend/src/__tests__/okr.test.tsx"
] ]
}, },
"FR-03": { "FR-03": {
"name": "Create Key Result", "name": "Create Key Result",
"code": [ "code": [
"backend/src/key-results/key-results.controller.ts", "apps/okr/backend/src/key-results/key-results.controller.ts",
{ "file": "backend/src/key-results/key-results.service.ts", "symbols": ["KeyResultsService", "create"] }, { "file": "apps/okr/backend/src/key-results/key-results.service.ts", "symbols": ["KeyResultsService", "create"] },
"backend/src/key-results/dto/create-key-result.dto.ts" "apps/okr/backend/src/key-results/dto/create-key-result.dto.ts"
], ],
"tests": [ "tests": [
"backend/test/services.test.ts", "apps/okr/backend/test/services.test.ts",
"backend/test/e2e.test.ts" "apps/okr/backend/test/e2e.test.ts"
] ]
}, },
"FR-04": { "FR-04": {
"name": "Update Progress", "name": "Update Progress",
"code": [ "code": [
"backend/src/key-results/key-results.controller.ts", "apps/okr/backend/src/key-results/key-results.controller.ts",
{ "file": "backend/src/key-results/key-results.service.ts", "symbols": ["updateProgress"] }, { "file": "apps/okr/backend/src/key-results/key-results.service.ts", "symbols": ["updateProgress"] },
"backend/src/key-results/dto/update-progress.dto.ts", "apps/okr/backend/src/key-results/dto/update-progress.dto.ts",
"frontend/src/pages/KeyResultDetail.tsx" "apps/okr/frontend/src/pages/KeyResultDetail.tsx"
], ],
"tests": [ "tests": [
"backend/test/services.test.ts", "apps/okr/backend/test/services.test.ts",
"backend/test/e2e.test.ts", "apps/okr/backend/test/e2e.test.ts",
"frontend/src/__tests__/okr.test.tsx" "apps/okr/frontend/src/__tests__/okr.test.tsx"
] ]
}, },
"FR-05": { "FR-05": {
"name": "Dashboard", "name": "Dashboard",
"code": [ "code": [
"backend/src/objectives/objectives.controller.ts", "apps/okr/backend/src/objectives/objectives.controller.ts",
{ "file": "backend/src/objectives/objectives.service.ts", "symbols": ["ObjectivesService", "list"] }, { "file": "apps/okr/backend/src/objectives/objectives.service.ts", "symbols": ["ObjectivesService", "list"] },
"frontend/src/pages/Dashboard.tsx", "apps/okr/frontend/src/pages/Dashboard.tsx",
"frontend/src/hooks/useObjectives.ts" "apps/okr/frontend/src/hooks/useObjectives.ts"
], ],
"tests": [ "tests": [
"backend/test/services.test.ts", "apps/okr/backend/test/services.test.ts",
"backend/test/e2e.test.ts", "apps/okr/backend/test/e2e.test.ts",
"frontend/src/__tests__/okr.test.tsx" "apps/okr/frontend/src/__tests__/okr.test.tsx"
] ]
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

+1 -1
View File
@@ -29,7 +29,7 @@ cp packages/casan-devkit/templates/gitea-workflow/ci.yml .gitea/workflows/casan-
## 4. Runner ## 4. Runner
Uses `runs-on: ci-runner` (a self-hosted Gitea Actions runner). To set one up on your host, Uses `runs-on: ci-runner` (a self-hosted Gitea Actions runner). To set one up on your host,
see `.gitea/vps-setup-runbook.md` and `scripts/setup-ci-runner.sh`. The runner needs see `.gitea/vps-setup-runbook.md` and `scripts/setup-ci-runner.sh`. The runner needs
`bash`, `python3`, `openssl` (and `node`/`npm` only if you enable frontend/backend tests). `bash`, `python3`, `openssl` (and `node`/`npm` only if you enable apps/okr/frontend/backend tests).
## 5. Expected result ## 5. Expected result
`CI_GATE_SUMMARY PASS=<n> FAIL=0 SKIP=<k>`. Any FAIL fails the job (exit 1). The gate is `CI_GATE_SUMMARY PASS=<n> FAIL=0 SKIP=<k>`. Any FAIL fails the job (exit 1). The gate is
+1 -1
View File
@@ -30,7 +30,7 @@ apps/<project>/domain/
a run against it; similarity 1.0 = no drift. Real drift detection is proven separately. a run against it; similarity 1.0 = no drift. Real drift detection is proven separately.
- **corpus/** — red-team attack vectors (scored for H4 recall) + a benign corpus (bounds the - **corpus/** — red-team attack vectors (scored for H4 recall) + a benign corpus (bounds the
false-positive rate). Domain-specific injections make the H4 score meaningful. false-positive rate). Domain-specific injections make the H4 score meaningful.
- **traceability-map.json** — `{"FR-01": {"name": "...", "code": ["backend/src/...", {"file":"...","symbols":["Foo"],"lines":[12]}], "tests": ["..."]}}`. Symbol/line refs are - **traceability-map.json** — `{"FR-01": {"name": "...", "code": ["apps/okr/backend/src/...", {"file":"...","symbols":["Foo"],"lines":[12]}], "tests": ["..."]}}`. Symbol/line refs are
optional but tighten the gate. optional but tighten the gate.
- **domain-pack.yaml** — documents the above + optional threshold overrides. - **domain-pack.yaml** — documents the above + optional threshold overrides.
+4 -4
View File
@@ -79,7 +79,7 @@
``` ```
okr-web/ okr-web/
├── backend/ # NestJS API service ├── apps/okr/backend/ # NestJS API service
│ ├── src/ │ ├── src/
│ │ ├── main.ts # Bootstrap, Swagger, global pipes │ │ ├── main.ts # Bootstrap, Swagger, global pipes
│ │ ├── app.module.ts # Root module │ │ ├── app.module.ts # Root module
@@ -97,7 +97,7 @@ okr-web/
│ ├── Dockerfile │ ├── Dockerfile
│ └── package.json │ └── package.json
│ │
├── frontend/ # React + Vite SPA ├── apps/okr/frontend/ # React + Vite SPA
│ ├── src/ │ ├── src/
│ │ ├── main.tsx # React root │ │ ├── main.tsx # React root
│ │ ├── App.tsx # Router setup + error boundary │ │ ├── App.tsx # Router setup + error boundary
@@ -285,7 +285,7 @@ exec "$@"
The seed script uses `upsert` (Prisma's `createOrUpdate`) keyed on stable identifiers (email for users, slug for objectives). Running the seed twice produces no duplicates: The seed script uses `upsert` (Prisma's `createOrUpdate`) keyed on stable identifiers (email for users, slug for objectives). Running the seed twice produces no duplicates:
```typescript ```typescript
// backend/prisma/seed.ts // apps/okr/backend/prisma/seed.ts
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
@@ -616,7 +616,7 @@ TanStack Query's `onError` global callback handles API error toasts without cras
- **Unit tests:** Services tested in isolation with Prisma mocked via `jest.mock`. - **Unit tests:** Services tested in isolation with Prisma mocked via `jest.mock`.
- **Integration tests:** `@nestjs/testing` spins up full NestJS app with SQLite in-memory database override. - **Integration tests:** `@nestjs/testing` spins up full NestJS app with SQLite in-memory database override.
- Test files co-located under `backend/test/`. - Test files co-located under `apps/okr/backend/test/`.
### Frontend ### Frontend
+6 -6
View File
@@ -6,13 +6,13 @@
"": { "": {
"name": "ainative-okr-casan5", "name": "ainative-okr-casan5",
"workspaces": [ "workspaces": [
"backend", "apps/okr/backend",
"frontend", "apps/okr/frontend",
"packages/casan-control-panel/backend", "packages/casan-control-panel/backend",
"packages/casan-control-panel/frontend" "packages/casan-control-panel/frontend"
] ]
}, },
"backend": { "apps/okr/backend": {
"name": "@ainative-okr/backend", "name": "@ainative-okr/backend",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
@@ -39,7 +39,7 @@
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
}, },
"frontend": { "apps/okr/frontend": {
"name": "@ainative-okr/frontend", "name": "@ainative-okr/frontend",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
@@ -77,11 +77,11 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@ainative-okr/backend": { "node_modules/@ainative-okr/backend": {
"resolved": "backend", "resolved": "apps/okr/backend",
"link": true "link": true
}, },
"node_modules/@ainative-okr/frontend": { "node_modules/@ainative-okr/frontend": {
"resolved": "frontend", "resolved": "apps/okr/frontend",
"link": true "link": true
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
+4 -4
View File
@@ -2,14 +2,14 @@
"name": "ainative-okr-casan5", "name": "ainative-okr-casan5",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"backend", "apps/okr/backend",
"frontend", "apps/okr/frontend",
"packages/casan-control-panel/backend", "packages/casan-control-panel/backend",
"packages/casan-control-panel/frontend" "packages/casan-control-panel/frontend"
], ],
"scripts": { "scripts": {
"build": "npm run build -w backend && npm run build -w frontend", "build": "npm run build -w @ainative-okr/backend && npm run build -w @ainative-okr/frontend",
"test": "npm test -w backend && npm test -w frontend", "test": "npm test -w @ainative-okr/backend && npm test -w @ainative-okr/frontend",
"console:api": "npm run dev -w @casan/control-panel-backend", "console:api": "npm run dev -w @casan/control-panel-backend",
"console:ui": "npm run dev -w @casan/control-panel-frontend", "console:ui": "npm run dev -w @casan/control-panel-frontend",
"console:build": "npm run build -w @casan/control-panel-backend && npm run build -w @casan/control-panel-frontend", "console:build": "npm run build -w @casan/control-panel-backend && npm run build -w @casan/control-panel-frontend",
+11
View File
@@ -38,6 +38,17 @@ Settings management:
permission; calls `rbac-check.py` before `control-plane-settings.py set`. permission; calls `rbac-check.py` before `control-plane-settings.py set`.
- `POST /api/v1/settings/rollback` — governed rollback through the same core CLI. - `POST /api/v1/settings/rollback` — governed rollback through the same core CLI.
Goal workspace context:
- `GET /api/v1/goals/projects` — lists active project IDs and context roots from the
harness-owned project registry after RBAC filtering; browser-supplied paths are never accepted.
- `POST /api/v1/goals` requires `{ goal, projectId }`. H1 resolves the registry again,
produces a size-limited redacted manifest/snapshot, and gives the exact same snapshot to
local and cloud models. Account-model CLIs remain inside an empty temporary sandbox.
- Goals requesting workspace side effects create a tenant-scoped
`goal.workspace.execute` approval proposal and finish as `requires_approval`; this flow
does not write source files or execute a coding action.
Local management headers: `x-casan-actor`, `x-casan-role`, `x-casan-project`, Local management headers: `x-casan-actor`, `x-casan-role`, `x-casan-project`,
`x-casan-tenant`. Missing role defaults to `viewer`, so writes fail closed. `x-casan-tenant`. Missing role defaults to `viewer`, so writes fail closed.
@@ -77,8 +77,8 @@ function stableJson(value: unknown): string {
export class ApprovalsService { 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]); 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() }; return { ...parseJson<Record<string, any>>(res.stdout, { count: 0, proposals: [], oversight: [] }), audit_verify: this.verifyAudit(actor) };
} }
submit(input: ApprovalSubmit, actor: SettingsActor) { submit(input: ApprovalSubmit, actor: SettingsActor) {
@@ -109,8 +109,8 @@ export class ApprovalsService {
JSON.stringify(input.payload ?? {}), JSON.stringify(input.payload ?? {}),
]; ];
if (input.sensitive) args.push('--sensitive'); if (input.sensitive) args.push('--sensitive');
const res = runFile('python3', args); const res = runFile('python3', args, this.tenantEnv(actor));
return { proposal: parseJson<Record<string, any>>(res.stdout, {}), audit_verify: this.verifyAudit() }; return { proposal: parseJson<Record<string, any>>(res.stdout, {}), audit_verify: this.verifyAudit(actor) };
} }
decide(input: ApprovalDecision, actor: SettingsActor) { decide(input: ApprovalDecision, actor: SettingsActor) {
@@ -119,7 +119,7 @@ export class ApprovalsService {
} }
this.requireRbac(actor, 'approval', 'grant'); this.requireRbac(actor, 'approval', 'grant');
try { try {
const pending = this.findProposal(input.id); const pending = this.findProposal(input.id, actor);
this.verifyApprovalIdentity(input, actor, pending); this.verifyApprovalIdentity(input, actor, pending);
const res = runFile('python3', [ const res = runFile('python3', [
INBOX_CLI, INBOX_CLI,
@@ -132,10 +132,10 @@ export class ApprovalsService {
actor.actor, actor.actor,
'--reason', '--reason',
input.reason, input.reason,
]); ], this.tenantEnv(actor));
const proposal = parseJson<Record<string, any>>(res.stdout, {}); const proposal = parseJson<Record<string, any>>(res.stdout, {});
const applied = input.decision === 'approve' ? this.applyApprovedProposal(proposal, actor) : null; const applied = input.decision === 'approve' ? this.applyApprovedProposal(proposal, actor) : null;
return { proposal, applied, audit_verify: this.verifyAudit() }; return { proposal, applied, audit_verify: this.verifyAudit(actor) };
} catch (err: any) { } catch (err: any) {
if (err instanceof ForbiddenException) throw err; if (err instanceof ForbiddenException) throw err;
if (Number(err.status) === 3 || Number(err.status) === 1) { if (Number(err.status) === 3 || Number(err.status) === 1) {
@@ -145,8 +145,8 @@ export class ApprovalsService {
} }
} }
private findProposal(id: string) { private findProposal(id: string, actor: SettingsActor) {
const res = runFile('python3', [INBOX_CLI, 'list', '--status', 'all']); const res = runFile('python3', [INBOX_CLI, 'list', '--status', 'all'], this.tenantEnv(actor));
const store = parseJson<Record<string, any>>(res.stdout, { proposals: [] }); const store = parseJson<Record<string, any>>(res.stdout, { proposals: [] });
const proposal = (store.proposals ?? []).find((p: Record<string, any>) => p.id === id); const proposal = (store.proposals ?? []).find((p: Record<string, any>) => p.id === id);
if (!proposal) throw new ForbiddenException(`APPROVAL_DECIDE_DENY unknown_id ${id}`); if (!proposal) throw new ForbiddenException(`APPROVAL_DECIDE_DENY unknown_id ${id}`);
@@ -199,7 +199,7 @@ export class ApprovalsService {
`approved:${proposal.id}:${proposal.decision_reason ?? ''}`, `approved:${proposal.id}:${proposal.decision_reason ?? ''}`,
'--approval', '--approval',
`inbox:${proposal.id}:${actor.actor}`, `inbox:${proposal.id}:${actor.actor}`,
]); ], this.tenantEnv(actor));
return parseJson<Record<string, any>>(res.stdout, {}); return parseJson<Record<string, any>>(res.stdout, {});
} catch (err: any) { } catch (err: any) {
if (Number(err.status) === 2 || Number(err.status) === 3) { if (Number(err.status) === 2 || Number(err.status) === 3) {
@@ -234,9 +234,13 @@ export class ApprovalsService {
} }
} }
private verifyAudit() { private tenantEnv(actor: SettingsActor): NodeJS.ProcessEnv {
return { CASAN_TENANT_ID: actor.tenant || 'default' };
}
private verifyAudit(actor: SettingsActor) {
try { try {
const res = runFile('python3', [INBOX_CLI, 'verify-audit']); const res = runFile('python3', [INBOX_CLI, 'verify-audit'], this.tenantEnv(actor));
return { ok: true, output: res.stdout }; return { ok: true, output: res.stdout };
} catch (err: any) { } catch (err: any) {
return { ok: false, output: err.stderr || err.stdout || err.message }; return { ok: false, output: err.stderr || err.stdout || err.message };
@@ -12,6 +12,11 @@ export class GoalsController {
return ok(await this.service.start(body, actorFromHeaders(headers))); return ok(await this.service.start(body, actorFromHeaders(headers)));
} }
@Get('projects')
projects(@Headers() headers: Record<string, string | string[] | undefined>) {
return ok(this.service.projects(actorFromHeaders(headers)));
}
@Get() @Get()
list(@Headers() headers: Record<string, string | string[] | undefined>, @Query('limit') limit?: string) { list(@Headers() headers: Record<string, string | string[] | undefined>, @Query('limit') limit?: string) {
return ok(this.service.list(actorFromHeaders(headers), Number(limit) || 20)); return ok(this.service.list(actorFromHeaders(headers), Number(limit) || 20));
@@ -1,5 +1,5 @@
import { BadRequestException, ForbiddenException, HttpException, HttpStatus, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { BadRequestException, ForbiddenException, HttpException, HttpStatus, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
import { execFileSync, spawn } from 'node:child_process'; import { execFileSync, spawn } from 'node:child_process';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { join } from 'node:path'; import { join } from 'node:path';
@@ -8,6 +8,14 @@ import type { SettingsActor } from '../settings/settings.service.js';
export interface GoalStartInput { export interface GoalStartInput {
goal: string; goal: string;
projectId: string;
}
export interface GoalProject {
project_id: string;
domain: string;
domain_root: string;
context_roots: string[];
} }
export interface GoalStage { export interface GoalStage {
@@ -23,7 +31,7 @@ export interface GoalJob {
id: string; id: string;
trace_id: string; trace_id: string;
goal: string; goal: string;
status: 'queued' | 'running' | 'completed' | 'degraded' | 'failed'; status: 'queued' | 'running' | 'completed' | 'degraded' | 'failed' | 'requires_approval';
actor: string; actor: string;
tenant: string; tenant: string;
project: string; project: string;
@@ -42,6 +50,9 @@ export interface GoalJob {
audit_hash?: string; audit_hash?: string;
local_usage?: Record<string, number>; local_usage?: Record<string, number>;
cloud_usage?: Record<string, number>; cloud_usage?: Record<string, number>;
workspace?: GoalProject;
context_manifest?: { files: number; characters: number; truncated: boolean; path?: string };
approval?: { id: string; status: string; action: string };
} }
interface ModelConnection { interface ModelConnection {
@@ -67,6 +78,7 @@ const HARNESS_BIN = join(APP_ROOT, 'packages', 'casan-harness', 'scripts', 'bash
const CONNECTIONS_CLI = join(HARNESS_BIN, 'model-connections.py'); const CONNECTIONS_CLI = join(HARNESS_BIN, 'model-connections.py');
const ORCHESTRATOR_CLI = join(HARNESS_BIN, 'goal-orchestrator.py'); const ORCHESTRATOR_CLI = join(HARNESS_BIN, 'goal-orchestrator.py');
const RBAC_CLI = join(HARNESS_BIN, 'rbac-check.py'); const RBAC_CLI = join(HARNESS_BIN, 'rbac-check.py');
const PROJECT_REGISTRY = join(APP_ROOT, 'packages', 'casan-harness', 'level5', 'project-registry.json');
function parseJson<T>(value: string): T | null { function parseJson<T>(value: string): T | null {
try { try {
@@ -86,7 +98,8 @@ export class GoalsService {
private readonly startWindows = new Map<string, number[]>(); private readonly startWindows = new Map<string, number[]>();
async start(input: GoalStartInput, actor: SettingsActor): Promise<GoalJob> { async start(input: GoalStartInput, actor: SettingsActor): Promise<GoalJob> {
this.requireRead(actor); const workspace = this.resolveProject(String(input.projectId ?? ''));
this.requireRead(actor, workspace.project_id);
const goal = String(input.goal ?? '').trim(); const goal = String(input.goal ?? '').trim();
if (goal.length < 10 || goal.length > 8000) { if (goal.length < 10 || goal.length > 8000) {
throw new BadRequestException('GOAL_LENGTH_INVALID'); throw new BadRequestException('GOAL_LENGTH_INVALID');
@@ -115,7 +128,8 @@ export class GoalsService {
status: 'queued', status: 'queued',
actor: actor.actor, actor: actor.actor,
tenant: actor.tenant, tenant: actor.tenant,
project: actor.project, project: workspace.project_id,
workspace,
created_at: timestamp, created_at: timestamp,
updated_at: timestamp, updated_at: timestamp,
local_provider: local?.id || 'local-policy', local_provider: local?.id || 'local-policy',
@@ -156,24 +170,39 @@ export class GoalsService {
return job; return job;
} }
projects(actor: SettingsActor): { count: number; projects: GoalProject[] } {
const projects = this.registeredProjects().filter((project) => {
try {
this.requireRead(actor, project.project_id);
return true;
} catch {
return false;
}
});
return { count: projects.length, projects };
}
get(id: string, actor: SettingsActor): GoalJob { get(id: string, actor: SettingsActor): GoalJob {
this.requireRead(actor);
if (!/^[a-f0-9-]{36}$/.test(id)) throw new NotFoundException('GOAL_NOT_FOUND'); if (!/^[a-f0-9-]{36}$/.test(id)) throw new NotFoundException('GOAL_NOT_FOUND');
const path = this.jobPath(actor.tenant, id); const path = this.jobPath(actor.tenant, id);
if (!existsSync(path)) throw new NotFoundException('GOAL_NOT_FOUND'); if (!existsSync(path)) throw new NotFoundException('GOAL_NOT_FOUND');
const job = parseJson<GoalJob>(readFileSync(path, 'utf8')); const job = parseJson<GoalJob>(readFileSync(path, 'utf8'));
if (!job || job.tenant !== actor.tenant) throw new NotFoundException('GOAL_NOT_FOUND'); if (!job || job.tenant !== actor.tenant) throw new NotFoundException('GOAL_NOT_FOUND');
this.requireRead(actor, job.project);
return job; return job;
} }
list(actor: SettingsActor, limit = 20): { count: number; goals: GoalJob[] } { list(actor: SettingsActor, limit = 20): { count: number; goals: GoalJob[] } {
this.requireRead(actor); this.requireRead(actor, actor.project);
const directory = join(APP_ROOT, '.specify', 'state', 'goals', safeTenant(actor.tenant)); const directory = join(APP_ROOT, '.specify', 'state', 'goals', safeTenant(actor.tenant));
if (!existsSync(directory)) return { count: 0, goals: [] }; if (!existsSync(directory)) return { count: 0, goals: [] };
const goals = readdirSync(directory) const goals = readdirSync(directory)
.filter((name) => /^[a-f0-9-]{36}\.json$/.test(name)) .filter((name) => /^[a-f0-9-]{36}\.json$/.test(name))
.map((name) => parseJson<GoalJob>(readFileSync(join(directory, name), 'utf8'))) .map((name) => parseJson<GoalJob>(readFileSync(join(directory, name), 'utf8')))
.filter((job): job is GoalJob => Boolean(job && job.tenant === actor.tenant)) .filter((job): job is GoalJob => Boolean(job && job.tenant === actor.tenant))
.filter((job) => {
try { this.requireRead(actor, job.project); return true; } catch { return false; }
})
.sort((left, right) => right.created_at.localeCompare(left.created_at)); .sort((left, right) => right.created_at.localeCompare(left.created_at));
return { count: goals.length, goals: goals.slice(0, Math.max(1, Math.min(limit, 100))) }; return { count: goals.length, goals: goals.slice(0, Math.max(1, Math.min(limit, 100))) };
} }
@@ -182,6 +211,34 @@ export class GoalsService {
return join(APP_ROOT, '.specify', 'state', 'goals', safeTenant(tenant), `${id}.json`); return join(APP_ROOT, '.specify', 'state', 'goals', safeTenant(tenant), `${id}.json`);
} }
private registeredProjects(): GoalProject[] {
const parsed = parseJson<{ projects?: Array<Record<string, unknown>> }>(readFileSync(PROJECT_REGISTRY, 'utf8'));
const root = realpathSync(APP_ROOT);
return (parsed?.projects ?? []).filter((entry) => entry.status === 'active').map((entry) => {
const projectId = String(entry.project_id ?? '');
const domainRoot = String(entry.domain_root ?? '');
const rawRoots = Array.isArray(entry.context_roots) ? entry.context_roots.map(String) : [domainRoot];
if (!/^[A-Za-z0-9._-]+$/.test(projectId) || !domainRoot || rawRoots.length === 0) {
throw new InternalServerErrorException('GOAL_PROJECT_REGISTRY_INVALID');
}
const contextRoots = rawRoots.map((relative) => {
const absolute = realpathSync(join(APP_ROOT, relative));
if (!(absolute === root || absolute.startsWith(`${root}/`)) || !statSync(absolute).isDirectory() && !statSync(absolute).isFile()) {
throw new InternalServerErrorException('GOAL_PROJECT_CONTEXT_ROOT_DENIED');
}
return relative;
});
return { project_id: projectId, domain: String(entry.domain ?? projectId), domain_root: domainRoot, context_roots: contextRoots };
});
}
private resolveProject(projectId: string): GoalProject {
if (!projectId) throw new BadRequestException('GOAL_PROJECT_REQUIRED');
const project = this.registeredProjects().find((entry) => entry.project_id === projectId);
if (!project) throw new BadRequestException('GOAL_PROJECT_NOT_ALLOWED');
return project;
}
private connections(actor: SettingsActor): ModelConnection[] { private connections(actor: SettingsActor): ModelConnection[] {
const payload = this.runPython(CONNECTIONS_CLI, ['list'], { CASAN_TENANT_ID: actor.tenant || 'default' }); const payload = this.runPython(CONNECTIONS_CLI, ['list'], { CASAN_TENANT_ID: actor.tenant || 'default' });
const parsed = parseJson<ConnectionList>(payload); const parsed = parseJson<ConnectionList>(payload);
@@ -259,10 +316,10 @@ export class GoalsService {
} }
} }
private requireRead(actor: SettingsActor): void { private requireRead(actor: SettingsActor, targetProject: string): void {
try { try {
execFileSync('python3', [RBAC_CLI, 'check', '--role', actor.role, '--resource', 'monitoring', '--action', 'read', execFileSync('python3', [RBAC_CLI, 'check', '--role', actor.role, '--resource', 'monitoring', '--action', 'read',
'--role-project', actor.project, '--target-project', actor.project, '--role-project', actor.project, '--target-project', targetProject,
'--role-tenant', actor.tenant, '--target-tenant', actor.tenant], { '--role-tenant', actor.tenant, '--target-tenant', actor.tenant], {
cwd: APP_ROOT, cwd: APP_ROOT,
env: process.env, env: process.env,
@@ -1,4 +1,4 @@
import { ForbiddenException, Injectable, InternalServerErrorException } from '@nestjs/common'; import { BadRequestException, ForbiddenException, Injectable, InternalServerErrorException } from '@nestjs/common';
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import { join } from 'node:path'; import { join } from 'node:path';
import { APP_ROOT } from '../common/app-root.js'; import { APP_ROOT } from '../common/app-root.js';
@@ -60,10 +60,11 @@ function parseJson<T>(raw: string, fallback: T): T {
export class SettingsService { export class SettingsService {
list(actor: SettingsActor) { list(actor: SettingsActor) {
this.requireRbac(actor, 'read', false); this.requireRbac(actor, 'read', false);
const policy = parseJson<Record<string, any>>(runPython(CP_CLI, ['list-policy']).stdout, {}); const tenantEnv = this.tenantEnv(actor);
const settings = parseJson<Record<string, any>>(runPython(CP_CLI, ['get-all']).stdout, {}); const policy = parseJson<Record<string, any>>(runPython(CP_CLI, ['list-policy'], tenantEnv).stdout, {});
const audit = parseJson<any[]>(runPython(CP_CLI, ['get-audit']).stdout, []); const settings = parseJson<Record<string, any>>(runPython(CP_CLI, ['get-all'], tenantEnv).stdout, {});
const auditVerify = this.verifyAudit(); const audit = parseJson<any[]>(runPython(CP_CLI, ['get-audit'], tenantEnv).stdout, []);
const auditVerify = this.verifyAudit(actor);
return { return {
actor, actor,
@@ -83,7 +84,7 @@ export class SettingsService {
if (!input.key || input.value === undefined || !input.reason) { if (!input.key || input.value === undefined || !input.reason) {
throw new ForbiddenException('SETTINGS_DENY key/value/reason required'); throw new ForbiddenException('SETTINGS_DENY key/value/reason required');
} }
const sensitive = this.isSensitive(input.key); const sensitive = this.isSensitive(input.key, actor);
this.requireRbac(actor, 'write', sensitive); this.requireRbac(actor, 'write', sensitive);
try { try {
const res = runPython(CP_CLI, [ const res = runPython(CP_CLI, [
@@ -96,11 +97,12 @@ export class SettingsService {
input.reason, input.reason,
'--approval', '--approval',
input.approval ?? '', input.approval ?? '',
]); ], this.tenantEnv(actor));
return { key: input.key, setting: parseJson<Record<string, any>>(res.stdout, {}), audit_verify: this.verifyAudit() }; return { key: input.key, setting: parseJson<Record<string, any>>(res.stdout, {}), audit_verify: this.verifyAudit(actor) };
} catch (err: any) { } catch (err: any) {
if (Number(err.status) === 3) throw new ForbiddenException(err.stderr || 'APPROVAL_REQUIRED'); if (Number(err.status) === 3) throw new ForbiddenException(err.stderr || 'APPROVAL_REQUIRED');
if (Number(err.status) === 2) throw new ForbiddenException(err.stderr || 'SETTING_NOT_ALLOWED'); if (Number(err.status) === 2) throw new ForbiddenException(err.stderr || 'SETTING_NOT_ALLOWED');
if (Number(err.status) === 5) throw new BadRequestException(err.stderr || 'SETTING_VALIDATION_ERROR');
throw new InternalServerErrorException(err.stderr || err.message); throw new InternalServerErrorException(err.stderr || err.message);
} }
} }
@@ -109,19 +111,19 @@ export class SettingsService {
if (!input.key || !input.reason) { if (!input.key || !input.reason) {
throw new ForbiddenException('SETTINGS_DENY key/reason required'); throw new ForbiddenException('SETTINGS_DENY key/reason required');
} }
const sensitive = this.isSensitive(input.key); const sensitive = this.isSensitive(input.key, actor);
this.requireRbac(actor, 'write', sensitive); this.requireRbac(actor, 'write', sensitive);
try { try {
const res = runPython(CP_CLI, ['rollback', input.key, '--actor', actor.actor, '--reason', input.reason]); const res = runPython(CP_CLI, ['rollback', input.key, '--actor', actor.actor, '--reason', input.reason], this.tenantEnv(actor));
return { key: input.key, setting: parseJson<Record<string, any>>(res.stdout, {}), audit_verify: this.verifyAudit() }; return { key: input.key, setting: parseJson<Record<string, any>>(res.stdout, {}), audit_verify: this.verifyAudit(actor) };
} catch (err: any) { } catch (err: any) {
if (Number(err.status) === 4) throw new ForbiddenException(err.stderr || 'NO_PRIOR_VERSION'); if (Number(err.status) === 4) throw new ForbiddenException(err.stderr || 'NO_PRIOR_VERSION');
throw new InternalServerErrorException(err.stderr || err.message); throw new InternalServerErrorException(err.stderr || err.message);
} }
} }
private isSensitive(key: string): boolean { private isSensitive(key: string, actor: SettingsActor): boolean {
const policy = parseJson<Record<string, any>>(runPython(CP_CLI, ['list-policy']).stdout, {}); const policy = parseJson<Record<string, any>>(runPython(CP_CLI, ['list-policy'], this.tenantEnv(actor)).stdout, {});
return Boolean(policy[key]?.securitySensitive); return Boolean(policy[key]?.securitySensitive);
} }
@@ -164,9 +166,13 @@ export class SettingsService {
return runPython(RBAC_CLI, args); return runPython(RBAC_CLI, args);
} }
private verifyAudit() { private tenantEnv(actor: SettingsActor): NodeJS.ProcessEnv {
return { CASAN_TENANT_ID: actor.tenant };
}
private verifyAudit(actor: SettingsActor) {
try { try {
const res = runPython(CP_CLI, ['verify-audit']); const res = runPython(CP_CLI, ['verify-audit'], this.tenantEnv(actor));
return { ok: true, output: res.stdout }; return { ok: true, output: res.stdout };
} catch (err: any) { } catch (err: any) {
return { ok: false, output: err.stderr || err.stdout || err.message }; return { ok: false, output: err.stderr || err.stdout || err.message };
@@ -16,8 +16,10 @@ function withTempGovernance(fn: (paths: { inbox: string; store: string }) => voi
const prevStore = process.env.CASAN_CP_STORE_FILE; const prevStore = process.env.CASAN_CP_STORE_FILE;
const prevKeyDir = process.env.CASAN_CP_KEY_DIR; const prevKeyDir = process.env.CASAN_CP_KEY_DIR;
const prevPub = process.env.CASAN_CP_PUB; const prevPub = process.env.CASAN_CP_PUB;
const prevTenantRoot = process.env.CASAN_TENANT_STATE_ROOT;
const work = mkdtempSync(join(tmpdir(), 'cp-approval-')); const work = mkdtempSync(join(tmpdir(), 'cp-approval-'));
process.env.CASAN_APPROVAL_INBOX_FILE = join(work, 'approval-inbox.json'); process.env.CASAN_TENANT_STATE_ROOT = work;
process.env.CASAN_APPROVAL_INBOX_FILE = join(work, 'default', 'approvals', 'approval-inbox.json');
process.env.CASAN_CP_STORE_FILE = join(work, 'settings.json'); process.env.CASAN_CP_STORE_FILE = join(work, 'settings.json');
process.env.CASAN_CP_KEY_DIR = join(work, 'keys'); process.env.CASAN_CP_KEY_DIR = join(work, 'keys');
process.env.CASAN_CP_PUB = join(work, 'cp.pub'); process.env.CASAN_CP_PUB = join(work, 'cp.pub');
@@ -34,6 +36,8 @@ function withTempGovernance(fn: (paths: { inbox: string; store: string }) => voi
else process.env.CASAN_CP_KEY_DIR = prevKeyDir; else process.env.CASAN_CP_KEY_DIR = prevKeyDir;
if (prevPub === undefined) delete process.env.CASAN_CP_PUB; if (prevPub === undefined) delete process.env.CASAN_CP_PUB;
else process.env.CASAN_CP_PUB = prevPub; else process.env.CASAN_CP_PUB = prevPub;
if (prevTenantRoot === undefined) delete process.env.CASAN_TENANT_STATE_ROOT;
else process.env.CASAN_TENANT_STATE_ROOT = prevTenantRoot;
} }
} }

Some files were not shown because too many files have changed in this diff Show More