feat: updade workspace
This commit is contained in:
@@ -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"}
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Generated
+6
-6
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user