- 663
} - 664
} - 665
- 666
/// Resolve the Agent-scoped `Core` for the rest of an async handler body, or - 667
/// return early with `resolve_scoped_core`'s own error `Response` (forwarding - 668
/// its status/message unchanged, e.g. 409 CONFLICT for a paused/archived - 669
/// Agent). Every endpoint scoped this way needs the identical - 670
/// match-and-early-return, so it lives here once instead of copy-pasted at - 671
/// each of the 50+ call sites (see `resolve_scoped_core` below). - 672
macro_rules! scoped_core { - 673
($state:expr, $session_id:expr, $agent:expr) => { - 674
match resolve_scoped_core($state, $session_id, $agent) { - 675
Ok(core) => core, - 676
Err(response) => return response, - 677
} - 678
}; - 679
} - 680
- 681
pub fn router(core: Core) -> Router { - 682
router_with_state(AppState::new(core)) - 683
} - 684
- 685
/// Unauthenticated router with the gateway force-enabled and no background - 686
/// scheduler. For embedders that run their own supervision loop and need - 687
/// clean teardown: dropping this router releases every session lock, - 688
/// whereas `secured_router`'s scheduler pins handles until process exit. - 689
pub fn gateway_router(core: Core) -> Router { - 690
let mut state = AppState::new(core); - 691
state.enable_gateway(); - 692
router_with_state(state) - 693
} - 694
- 695
fn router_with_state(state: AppState) -> Router { - 696
Router::new() - 697
.route("/health", get(health)) - 698
.route("/sessions", get(list_sessions).post(create_session)) - 699
.route("/sessions/{id}/attach", post(attach_session)) - 700
.route("/sessions/{id}/diff", get(session_diff)) - 701
.route("/sessions/{id}/receipts", get(session_receipts)) - 702
.route( - 703
"/sessions/{id}/work", - 704
get(session_work).post(session_work_command), - 705
) - 706
.route("/sessions/{id}/work/confirm", post(session_work_confirm)) - 707
.route("/sessions/{id}/work/revise", post(session_work_revise)) - 708
.route( - 709
"/sessions/{id}/work/items/{item_id}/retry", - 710
post(session_work_retry), - 711
) - 712
.route( - 713
"/sessions/{id}/work/items/{item_id}/cancel", - 714
post(session_work_cancel_item), - 715
) - 716
.route( - 717
"/sessions/{id}/work/items/{item_id}/reassign", - 718
post(session_work_reassign), - 719
) - 720
.route("/flows", get(flows_list)) - 721
.route("/flows/{name}/runs", get(flow_runs_list)) - 722
.route("/flows/{name}/runs/{run}/graph", get(flow_run_graph)) - 723
.route("/sessions/{id}/checkpoints", get(list_checkpoints)) - 724
.route( - 725
"/sessions/{id}/checkpoints/{seq}/restore", - 726
post(restore_checkpoint), - 727
) - 728
.route("/sessions/{id}/archive", post(set_archived)) - 729
.route("/sessions/archived", delete(delete_all_archived)) - 730
.route("/sessions/{id}", delete(delete_session)) - 731
.route("/sessions/{id}/restore", post(restore_session)) - 732
.route("/skills", get(list_skills)) - 733
.route("/commands", get(list_commands)) - 734
.route("/plugins", get(list_plugins)) - 735
.route("/plugins/catalog", get(plugin_catalog)) - 736
.route("/plugins/audit", get(plugin_audit)) - 737
.route("/plugins/invocations", get(plugin_invocations)) - 738
.route( - 739
"/presentations", - 740
get(list_presentations).post(register_presentations), - 741
) - 742
.route( - 743
"/presentations/specs/{id}/{revision}", - 744
get(get_presentation_spec), - 745
) - 746
.route( - 747
"/presentations/revisions", - 748
post(propose_presentation_revision), - 749
) - 750
.route( - 751
"/sessions/{id}/presentation/proposals", - 752
post(propose_session_presentation_revision), - 753
) - 754
.route("/presentations/export", get(export_presentations)) - 755
.route("/presentations/import", post(import_presentations)) - 756
.route( - 757
"/presentations/activate-all", - 758
post(activate_all_presentations), - 759
) - 760
.route( - 761
"/presentations/deactivate-all", - 762
post(deactivate_all_presentations), - 763
) - 764
.route( - 765
"/presentations/{id}/{revision}/activate", - 766
post(activate_presentation), - 767
) - 768
.route( - 769
"/presentations/{id}/deactivate", - 770
post(deactivate_presentation), - 771
) - 772
.route("/presentations/{id}/reset", post(reset_presentation)) - 773
.route( - 774
"/presentations/plugins/{plugin_id}", - 775
delete(revoke_presentations_plugin), - 776
) - 777
.route( - 778
"/plugins/retired", - 779
get(list_retired_plugins).delete(remove_retired_plugins), - 780
) - 781
.route( - 782
"/plugins/sources", - 783
get(plugin_sources).post(plugin_register_source), - 784
) - 785
.route("/plugins/sources/{id}/enable", post(plugin_source_enable)) - 786
.route("/plugins/sources/{id}/disable", post(plugin_source_disable)) - 787
.route("/plugins/keys/{id}/revoke", post(plugin_key_revoke)) - 788
.route("/plugins/keys/{id}/restore", post(plugin_key_restore)) - 789
.route("/plugins/install", post(plugin_install)) - 790
.route("/plugins/update", post(plugin_update)) - 791
.route("/plugins/{name}/enable", post(plugin_enable)) - 792
.route("/plugins/{name}/disable", post(plugin_disable)) - 793
.route("/plugins/{name}/rollback", post(plugin_rollback)) - 794
.route("/plugins/{name}", delete(plugin_remove)) - 795
.route("/sessions/{id}/pr", get(session_pr)) - 796
.route("/sessions/{id}/pr/merge", post(pr_merge)) - 797
.route("/tasks", get(list_tasks).post(create_task)) - 798
.route( - 799
"/tasks/{id}", - 800
axum::routing::patch(patch_task).delete(delete_task), - 801
) - 802
.route("/tasks/{id}/run-now", post(run_task_now)) - 803
.route("/tasks/{id}/retry-delivery", post(retry_task_delivery)) - 804
.route("/sessions/{id}/launch", get(get_launch)) - 805
.route("/sessions/{id}/launch/prepare", post(prepare_launch)) - 806
.route("/sessions/{id}/launch/start", post(start_launch)) - 807
.route("/sessions/{id}/launch/stop", post(stop_launch)) - 808
.route("/sessions/{id}/launch/logs", get(launch_logs)) - 809
.route("/sessions/{id}/run", post(run_prompt)) - 810
.route("/sessions/{id}/steering", post(send_steering)) - 811
.route("/sessions/{id}/cancel", post(cancel_run)) - 812
.route("/sessions/{id}/pause", post(pause_run)) - 813
.route("/sessions/{id}/resume", post(resume_run)) - 814
.route("/sessions/{id}/control-state", get(control_state)) - 815
.route("/sessions/{id}/plan-change", post(plan_change)) - 816
.route("/sessions/{id}/workers", get(list_workers)) - 817
.route("/sessions/{id}/workers/{child}/steer", post(steer_worker)) - 818
.route("/sessions/{id}/workers/{child}/stop", post(stop_worker)) - 819
// Backward-compatible aliases for the old `subagents` route names. - 820
.route("/sessions/{id}/subagents", get(list_workers)) - 821
.route("/sessions/{id}/subagents/{child}/steer", post(steer_worker)) - 822
.route("/sessions/{id}/subagents/{child}/stop", post(stop_worker)) - 823
.route("/sessions/{id}/approvals/{req_id}", post(answer_approval)) - 824
.route("/sessions/{id}/outcome-review", post(record_outcome_review)) - 825
.route("/sessions/{id}/events", get(events_sse)) - 826
.route( - 827
"/sessions/{id}/sandbox/executions", - 828
get(session_sandbox_executions), - 829
) - 830
.route( - 831
"/sessions/{id}/sandbox/records", - 832
get(list_session_sandbox_records), - 833
) - 834
.route( - 835
"/sessions/{id}/sandbox/candidates", - 836
post(export_sandbox_candidate), - 837
) - 838
.route( - 839
"/sessions/{id}/sandbox/candidates/{candidate_id}/files", - 840
get(read_sandbox_candidate_file), - 841
) - 842
.route( - 843
"/sessions/{id}/sandbox/candidates/{candidate_id}/files/raw", - 844
get(read_sandbox_candidate_file_raw), - 845
) - 846
.route( - 847
"/sessions/{id}/sandbox/candidates/{candidate_id}/office-review", - 848
get(read_sandbox_candidate_office_review), - 849
) - 850
.route( - 851
"/sessions/{id}/sandbox/candidates/{candidate_id}/office-narrow", - 852
post(narrow_sandbox_candidate_office), - 853
) - 854
.route( - 855
"/sessions/{id}/sandbox/candidates/{candidate_id}/office", - 856
get(read_sandbox_candidate_office_projection), - 857
) - 858
.route( - 859
"/sessions/{id}/sandbox/candidates/{candidate_id}/comments", - 860
get(list_sandbox_candidate_comments).post(comment_on_sandbox_candidate), - 861
) - 862
.route( - 863
"/sessions/{id}/sandbox/candidates/{candidate_id}/comments/{comment_id}/request-revision", - 864
post(request_revision_from_candidate_comment), - 865
) - 866
.route( - 867
"/sessions/{id}/sandbox/promote", - 868
post(promote_sandbox_candidate), - 869
) - 870
.route( - 871
"/sessions/{id}/sandbox/promotions/{candidate_id}/undo", - 872
post(undo_sandbox_promotion), - 873
) - 874
.route( - 875
"/sessions/{id}/sandbox/promotions/{candidate_id}/checks", - 876
post(run_sandbox_workspace_check), - 877
) - 878
.route("/sessions/{id}/presentation", get(presentation_snapshot)) - 879
.route("/sessions/{id}/results/{result_id}", get(session_result)) - 880
.route( - 881
"/sessions/{id}/presentation/feedback", - 882
post(presentation_feedback), - 883
) - 884
.route( - 885
"/sessions/{id}/presentation/select", - 886
post(select_presentation_for_session), - 887
) - 888
.route( - 889
"/sessions/{id}/presentation/events", - 890
get(presentation_events_sse), - 891
) - 892
.route("/sessions/{id}/transcript", get(transcript)) - 893
.route("/sessions/{id}/transcript.md", get(transcript_markdown)) - 894
.route( - 895
"/sessions/{id}/coworking/invitations", - 896
get(list_coworking_invitations).post(create_coworking_invitation), - 897
) - 898
.route("/sessions/{id}/coworking/me", get(coworking_me)) - 899
.route( - 900
"/sessions/{id}/coworking/presence", - 901
get(coworking_presence), - 902
) - 903
.route( - 904
"/sessions/{id}/coworking/messages", - 905
post(create_coworking_message), - 906
) - 907
.route( - 908
"/sessions/{id}/coworking/approvals", - 909
get(list_coworking_approvals), - 910
) - 911
.route( - 912
"/sessions/{id}/coworking/approvals/{req_id}", - 913
post(answer_coworking_approval), - 914
) - 915
.route( - 916
"/sessions/{id}/coworking/approvals/{req_id}/delegate", - 917
post(delegate_coworking_approval), - 918
) - 919
.route("/sessions/{id}/coworking/updates", get(coworking_updates)) - 920
.route("/sessions/{id}/office-workspaces", get(office_workspace::list).post(office_workspace::create)) - 921
.route("/sessions/{id}/office-workspaces/{room_id}", post(office_workspace::mutate)) - 922
.route("/sessions/{id}/office-workspaces/{room_id}/presence", post(office_workspace::focus)) - 923
.route( - 924
"/sessions/{id}/coworking/invitations/{grant_id}/revoke", - 925
post(revoke_coworking_invitation), - 926
) - 927
.route("/sessions/{id}/side", post(side_chat)) - 928
.route("/sessions/{id}/side/cancel", post(side_cancel_run)) - 929
.route("/sessions/{id}/bestofn", post(start_bestofn)) - 930
.route("/sessions/{id}/keep", post(keep_best_run)) - 931
.route("/sessions/{id}/discard", post(discard_best_run)) - 932
.route("/fs/file", get(read_file).put(write_file)) - 933
.route("/fs/file/raw", get(read_file_raw)) - 934
.route("/fs/office", get(read_office_projection)) - 935
.route( - 936
"/fs/inbox", - 937
post(upload_to_inbox).layer(axum::extract::DefaultBodyLimit::max( - 938
inbox::UPLOAD_MAX_BYTES, - 939
)), - 940
) - 941
.route("/fs/preview/{*path}", get(preview_file)) - 942
.route("/sandbox/records", get(list_sandbox_records)) - 943
.route("/fs/tree", get(fs_tree)) - 944
.route("/config", get(get_config).patch(patch_config)) - 945
.route("/config/intent/evidence", post(patch_evidence_policy)) - 946
.route( - 947
"/config/global", - 948
get(get_global_config_layer).patch(patch_global_config), - 949
) - 950
.route("/config/workspace", get(get_workspace_config_layer)) - 951
.route("/config/project", get(get_workspace_config_layer)) - 952
.route("/config/mode", post(set_permission_mode)) - 953
.route( - 954
"/agent-network/capabilities", - 955
post(agent_network_capability), - 956
) - 957
.route( - 958
"/agent-network/messages", - 959
post(agent_network_send).get(agent_network_receive), - 960
) - 961
.route("/config/mcp", get(get_mcp_servers).put(put_mcp_servers)) - 962
.route( - 963
"/config/mcp/global", - 964
get(get_global_mcp_servers).put(put_global_mcp_servers), - 965
) - 966
.route("/config/integrations", get(get_integration_catalog)) - 967
.route( - 968
"/config/integrations/{id}", - 969
get(get_scoped_integration) - 970
.put(put_scoped_integration) - 971
.delete(delete_scoped_integration), - 972
) - 973
.route("/config/hooks", get(get_hooks).put(put_hooks)) - 974
.route( - 975
"/config/prompts", - 976
get(get_prompt_layer).put(put_prompt_block), - 977
) - 978
.route("/config/prompts/effective", get(get_prompt_effective)) - 979
.route("/config/prompts/preview", post(preview_prompt)) - 980
.route("/config/prompts/roles", get(list_prompt_roles)) - 981
.route("/agents", get(agent_chats::list)) - 982
.route("/agents/{agent}/open", post(agent_chats::open)) - 983
.route("/config/agents", get(get_agents).put(put_agents)) - 984
.route( - 985
"/config/hooks/global", - 986
get(get_global_hooks).put(put_global_hooks), - 987
) - 988
.route( - 989
"/config/key", - 990
put(put_provider_key).delete(delete_provider_key), - 991
) - 992
// Distributed event bus status (vak-bus, docs/design/53). - 993
// Credentials are never returned; only the connection state and - 994
// metrics are exposed. - 995
.route( - 996
"/config/bus", - 997
get(get_bus_config) - 998
.put(put_bus_config) - 999
.delete(delete_bus_config), - 1000
) - 1001
// The approval policy: whether an `Ask` raised on an unattended - 1002
// chat surface reaches a human at all. Read-only everywhere until - 1003
// now, which made `vak_core::reach`'s own printed remedy an action - 1004
// no surface could perform. - 1005
.route( - 1006
"/gateway/approvals", - 1007
get(get_gateway_approvals).put(put_gateway_approvals), - 1008
) - 1009
// The three permission rule lists, as the engine evaluates them. - 1010
.route( - 1011
"/config/permissions", - 1012
get(get_permission_rules).put(put_permission_rules), - 1013
) - 1014
// Multi-bot-per-surface (docs/design/34, multi-bot): a `Bot` is an - 1015
// independent identity, so it gets its own id-addressed routes - 1016
// rather than reusing the one-slot-per-surface ones above. - 1017
.route("/gateway/bots", get(list_bots).post(create_bot)) - 1018
.route( - 1019
"/gateway/bots/{id}", - 1020
axum::routing::patch(update_bot).delete(delete_bot), - 1021
) - 1022
.route( - 1023
"/gateway/bots/{id}/token", - 1024
put(put_bot_id_token).delete(delete_bot_id_token), - 1025
) - 1026
.route("/providers", get(list_providers)) - 1027
.route("/providers/{name}/models", get(discover_models)) - 1028
.route( - 1029
"/providers/{name}/models/availability", - 1030
get(model_availability), - 1031
) - 1032
.route("/providers/{name}/status", get(provider_status)) - 1033
.route("/search", get(search_sessions)) - 1034
.route("/ops/status", get(ops_status)) - 1035
.route("/ops/center", get(operations_center)) - 1036
.route("/ops/actions", get(operations_actions)) - 1037
.route("/ops/incidents", get(operations_incidents)) - 1038
.route("/ops/outbox", get(operations_outbox)) - 1039
.route( - 1040
"/ops/outbox/{job_id}/replay", - 1041
post(replay_operations_outbox), - 1042
) - 1043
.route("/ops/{service}/{action}", post(ops_action)) - 1044
.route("/ops/diagnostics", get(ops_diagnostics)) - 1045
// Activation, explicitly (docs/design/46 D6). Configuration writes - 1046
// never register a service; this is the one action that does. - 1047
.route("/ops/services/activate", post(activate_services)) - 1048
.route("/finops", get(finops_status).patch(patch_finops)) - 1049
.route("/voice/speak", post(voice::voice_speak)) - 1050
.route("/voice/providers", get(voice::voice_providers)) - 1051
.route("/memory", get(list_memory).post(append_memory)) - 1052
.route("/memory/cleanup", post(cleanup_memory)) - 1053
.route("/memory/consolidate", post(consolidate_memory_route)) - 1054
.route( - 1055
"/memory/{note_id}", - 1056
axum::routing::patch(amend_memory_note).delete(forget_memory_note), - 1057
) - 1058
.route( - 1059
"/entities", - 1060
get(list_entities_route).post(upsert_entity_route), - 1061
) - 1062
.route( - 1063
"/entities/{id}", - 1064
get(get_entity_route).delete(delete_entity_route), - 1065
) - 1066
.route("/agents/templates", get(list_agent_templates)) - 1067
.route("/agents/instantiate", post(instantiate_agent_template)) - 1068
.route("/canvas/preview", post(canvas_preview)) - 1069
.route("/intent/explain", get(intent_explain)) - 1070
.route("/intent/policy", get(intent_policy)) - 1071
.route("/commitments", get(list_commitments)) - 1072
.route("/commitments/{id}", get(get_commitment)) - 1073
.route("/commitments/{id}/close", post(close_commitment)) - 1074
.route("/doctor", get(doctor_report)) - 1075
.route("/onboarding", get(onboarding_state)) - 1076
// The composition layer setup needs, and nothing more: every other - 1077
// choice reuses the config, provider, integration, and bot APIs - 1078
// that already exist (doc 46, "API and command design"). - 1079
.route("/onboarding/seed", post(onboarding_seed)) - 1080
.route( - 1081
"/onboarding/workspace-review", - 1082
post(onboarding_workspace_review), - 1083
) - 1084
.route("/onboarding/trust", post(onboarding_trust)) - 1085
.route("/onboarding/first-task", post(onboarding_first_task)) - 1086
// ---- browser auth (docs/design/48-web-client.md §4.3) ----------- - 1087
// - 1088
// ONE login for every browser surface — the workspace client and - 1089
// the operations console share this exchange and this cookie. - 1090
// These replace `/admin/login` and `/admin/logout`, which were - 1091
// removed rather than kept alongside: two endpoints against one - 1092
// cookie is two contracts that must agree forever (invariant 30). - 1093
.route("/auth/login", post(web::login)) - 1094
.route("/auth/logout", post(web::logout)) - 1095
.route("/auth/session", get(web::session_status)) - 1096
// ---- the workspace client's own host surface -------------------- - 1097
.route("/host", get(web::host_info)) - 1098
.route("/stream", get(stream::stream)) - 1099
.route("/workspaces", get(web::list_workspaces)) - 1100
.route("/workspaces/open", post(web::open_workspace)) - 1101
.route("/workspaces/forget", post(web::forget_workspace)) - 1102
.route("/fs/dirs", get(web::list_dirs)) - 1103
.route("/pty", get(web::pty_socket)) - 1104
.route("/voice/session", get(voice::voice_socket)) - 1105
.route("/version", get(web::version)) - 1106
.route("/backup/export", post(backup_export)) - 1107
.route("/backup/import", post(backup_import)) - 1108
.route("/digest", get(digest_report)) - 1109
.route("/inbox", get(inbox_list)) - 1110
.route("/inbox/unread_count", get(inbox_unread_count)) - 1111
.route("/inbox/{id}/ack", post(inbox_ack)) - 1112
.route("/skills/proposals", get(list_proposals_route)) - 1113
.route("/skills/proposals/{id}/promote", post(promote_proposal)) - 1114
.route("/skills/proposals/{id}/reject", post(reject_proposal)) - 1115
.merge(gateway::routes()) - 1116
.merge(feeds::routes()) - 1117
.merge(admin::routes()) - 1118
// Compression is scoped to the three STATIC bundles and nowhere - 1119
// else. These are the big, highly compressible responses — a site - 1120
// page is ~78 KB of inlined CSS and markup, the vendored motion - 1121
// build is 141 KB, and the SPA bundles are larger still — and this - 1122
// product is explicitly built to be reached over a tunnel, where - 1123
// that is the whole first-visit cost. - 1124
// - 1125
// It is NOT applied to the API. `/sessions/:id/events` is - 1126
// server-sent events: a compressor sits between the writer and the - 1127
// socket, and a live transcript that arrives in buffer-sized - 1128
// batches instead of per frame is a worse product than an - 1129
// uncompressed one. Scoping it here rather than at the root is the - 1130
// difference between a smaller page and a laggy agent. - 1131
.merge( - 1132
axum::Router::new() - 1133
.merge(admin_ui::routes()) - 1134
.merge(client_ui::routes()) - 1135
.merge(site::routes()) - 1136
.layer(tower_http::compression::CompressionLayer::new().gzip(true)), - 1137
) - 1138
.with_state(state) - 1139
} - 1140
- 1141
/// Service-control plane over vak-ops: lets TUI/desktop/tray agree on the - 1142
/// same truth (docs/design/28-operations.md). - 1143
fn ops_payload(cfg: &vak_ops::OpsConfig) -> serde_json::Value { - 1144
let st = |svc| vak_ops::status(svc, cfg); - 1145
serde_json::json!({ - 1146
"gateway": { "state": st(vak_ops::Service::Gateway).to_string() }, - 1147
"bridges": { "state": st(vak_ops::Service::Bridges).to_string() }, - 1148
"gateway_healthy": vak_ops::health_ok(cfg), - 1149
}) - 1150
} - 1151
- 1152
async fn ops_status(State(state): State<AppState>) -> Json<serde_json::Value> { - 1153
let mut cfg = vak_ops::OpsConfig::detect(); - 1154
cfg.port = state.ops_port; - 1155
let payload = tokio::task::spawn_blocking(move || ops_payload(&cfg)) - 1156
.await - 1157
.unwrap_or_else(|_| { - 1158
serde_json::json!({ - 1159
"gateway": { "state": "unknown" }, - 1160
"telegram": { "state": "unknown" }, - 1161
"gateway_healthy": false, - 1162
}) - 1163
}); - 1164
Json(payload) - 1165
} - 1166
- 1167
/// Read-only operational projection for desktop/TUI surfaces. This keeps - 1168
/// service, gateway, flow and health state in one refreshable payload without - 1169
/// exposing credentials or implementation paths. - 1170
async fn ops_diagnostics(State(state): State<AppState>) -> Json<serde_json::Value> { - 1171
refresh_control_plane(&state); - 1172
let mut cfg = vak_ops::OpsConfig::detect(); - 1173
cfg.port = state.ops_port; - 1174
let root = state.core.sessions_home().join("flow-runs"); - 1175
let mut flows = Vec::new(); - 1176
if let Ok(entries) = std::fs::read_dir(&root) { - 1177
for entry in entries.flatten().filter(|e| e.path().is_dir()) { - 1178
let name = entry.file_name().to_string_lossy().into_owned(); - 1179
let runs = std::fs::read_dir(entry.path()) - 1180
.map(|items| { - 1181
items - 1182
.flatten() - 1183
.filter(|e| e.path().extension().is_some_and(|x| x == "json")) - 1184
.count() - 1185
}) - 1186
.unwrap_or(0); - 1187
flows.push(serde_json::json!({ "name": name, "runs": runs })); - 1188
} - 1189
} - 1190
flows.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); - 1191
let gateway = state.gateway.snapshot(); - 1192
let services = tokio::task::spawn_blocking(move || ops_payload(&cfg)) - 1193
.await - 1194
.unwrap_or_else(|_| { - 1195
serde_json::json!({ - 1196
"gateway": { "state": "unknown" }, - 1197
"telegram": { "state": "unknown" }, - 1198
"gateway_healthy": false, - 1199
}) - 1200
}); - 1201
Json(serde_json::json!({ - 1202
"health": health_projection(&state), - 1203
"services": services, - 1204
"gateway": { - 1205
"enabled": state.gateway.enabled, - 1206
"bindings": gateway.into_iter().map(|(target, binding)| serde_json::json!({ - 1207
"target": target, - 1208
"session_id": binding.session_id, - 1209
"provider": binding.provider, - 1210
"model": binding.model, - 1211
"workspace": binding.workspace, - 1212
"route_revision": binding.route_revision, - 1213
})).collect::<Vec<_>>(), - 1214
"approvals": { - 1215
"mode": state.gateway.approvals_mode(), - 1216
"approver": state.gateway.approver_target(), - 1217
"pending": state.gateway.pending_approval_count(), - 1218
}, - 1219
}, - 1220
"flows": flows, - 1221
})) - 1222
} - 1223
- 1224
fn operation_services(cfg: &vak_ops::OpsConfig) -> serde_json::Value { - 1225
let gateway = vak_ops::status(vak_ops::Service::Gateway, cfg); - 1226
let bridges = vak_ops::status(vak_ops::Service::Bridges, cfg); - 1227
serde_json::json!({ - 1228
"gateway": { "state": gateway.to_string() }, - 1229
"bridges": { "state": bridges.to_string() }, - 1230
"gateway_healthy": vak_ops::health_ok(cfg), - 1231
}) - 1232
} - 1233
- 1234
fn operation_runs(state: &AppState) -> Vec<serde_json::Value> { - 1235
state - 1236
.live_handles() - 1237
.into_iter() - 1238
.filter_map(|handle| { - 1239
let active = handle - 1240
.session - 1241
.lock() - 1242
.unwrap_or_else(std::sync::PoisonError::into_inner) - 1243
.is_none(); - 1244
let pending = handle - 1245
.pending - 1246
.lock() - 1247
.unwrap_or_else(std::sync::PoisonError::into_inner) - 1248
.values() - 1249
.map(|request| { - 1250
serde_json::json!({ - 1251
"id": request.id, - 1252
"tool": request.tool, - 1253
"reason": request.reason, - 1254
"requested_at": request.requested_at, - 1255
}) - 1256
}) - 1257
.collect::<Vec<_>>(); - 1258
if !active && pending.is_empty() { - 1259
return None; - 1260
} - 1261
let (agent_id, agent_name) = handle - 1262
.core - 1263
.agent_identity() - 1264
.map(|id| (id.id.clone(), id.name.clone())) - 1265
.unwrap_or_else(|| ("vak".to_string(), "Vakyartha".to_string())); - 1266
Some(serde_json::json!({ - 1267
"session_id": handle.id, - 1268
"workspace": handle.cwd, - 1269
"agent_id": agent_id, - 1270
"agent_name": agent_name, - 1271
"state": if !pending.is_empty() { "waiting_approval" } else { "running" }, - 1272
"pending_approvals": pending, - 1273
})) - 1274
}) - 1275
.collect() - 1276
} - 1277
- 1278
fn operation_tasks(state: &AppState) -> Vec<serde_json::Value> { - 1279
let tasks = state - 1280
.tasks - 1281
.lock() - 1282
.unwrap_or_else(std::sync::PoisonError::into_inner); - 1283
let next_fire = state - 1284
.next_fire - 1285
.lock() - 1286
.unwrap_or_else(std::sync::PoisonError::into_inner); - 1287
let inflight = state - 1288
.script_inflight - 1289
.lock() - 1290
.unwrap_or_else(std::sync::PoisonError::into_inner); - 1291
tasks - 1292
.values() - 1293
.map(|task| { - 1294
let mut value = serde_json::to_value(task).unwrap_or_else(|_| serde_json::json!({})); - 1295
if let Some(object) = value.as_object_mut() { - 1296
object.insert( - 1297
"next_fire".into(), - 1298
next_fire - 1299
.get(&task.id) - 1300
.map(|fire| serde_json::Value::String(fire.to_rfc3339())) - 1301
.unwrap_or(serde_json::Value::Null), - 1302
); - 1303
object.insert( - 1304
"running".into(), - 1305
serde_json::Value::Bool(inflight.contains(&task.id)), - 1306
); - 1307
} - 1308
value - 1309
}) - 1310
.collect() - 1311
} - 1312
- 1313
fn operation_outbox(state: &AppState) -> Result<(Vec<serde_json::Value>, usize, usize), String> { - 1314
let records = delivery::outbox_records(&state.core)?; - 1315
let pending = records - 1316
.iter() - 1317
.filter(|record| record.state == vak_delivery::outbox::OutboxState::Pending) - 1318
.count(); - 1319
let dead = records - 1320
.iter() - 1321
.filter(|record| record.state == vak_delivery::outbox::OutboxState::DeadLetter) - 1322
.count(); - 1323
let rows = records - 1324
.into_iter() - 1325
.take(200) - 1326
.map(|record| { - 1327
serde_json::json!({ - 1328
"job_id": record.job.job_id, - 1329
"target": record.job.target, - 1330
"kind": record.job.kind, - 1331
"state": record.state, - 1332
"attempts": record.attempts, - 1333
"created_at_ms": record.created_at_ms, - 1334
"updated_at_ms": record.updated_at_ms, - 1335
"last_error": record.last_error, - 1336
}) - 1337
}) - 1338
.collect(); - 1339
Ok((rows, pending, dead)) - 1340
} - 1341
- 1342
/// Unified, evidence-backed projection for the Operations Center. Every row - 1343
/// is derived from an existing ledger, manager probe, or in-process handle; - 1344
/// unavailable state stays explicit instead of being painted green. - 1345
async fn operations_center(State(state): State<AppState>) -> Json<serde_json::Value> { - 1346
refresh_control_plane(&state); - 1347
load_tasks(&state); - 1348
let health = health_projection(&state); - 1349
let mut service_cfg = vak_ops::OpsConfig::detect(); - 1350
service_cfg.port = state.ops_port; - 1351
let ops_port = service_cfg.port; - 1352
let services = tokio::task::spawn_blocking(move || operation_services(&service_cfg)) - 1353
.await - 1354
.unwrap_or_else(|_| { - 1355
serde_json::json!({ - 1356
"gateway": { "state": "unknown" }, - 1357
"telegram": { "state": "unknown" }, - 1358
"gateway_healthy": false, - 1359
}) - 1360
}); - 1361
let default_route = state.core.effective_route(); - 1362
let gateway = state.gateway.snapshot(); - 1363
let pool = state - 1364
.gateway - 1365
.core_pool - 1366
.snapshot_at(Instant::now()) - 1367
.into_iter() - 1368
.map(|entry| { - 1369
serde_json::json!({ - 1370
"workspace": entry.workspace, - 1371
"is_default": entry.is_default, - 1372
"state": "warm", - 1373
"idle_secs": entry.idle_secs, - 1374
"permission_override": entry.permission_override, - 1375
"effective_permission_mode": entry.effective_permission_mode, - 1376
}) - 1377
}) - 1378
.collect::<Vec<_>>(); - 1379
let allowlist_snapshot = state.gateway.allowlist_snapshot(); - 1380
let allowlist_map: HashMap<String, String> = allowlist_snapshot - 1381
.iter() - 1382
.map(|e| { - 1383
( - 1384
e.key.clone(), - 1385
e.agent_id.clone().unwrap_or_else(|| "vak".to_string()), - 1386
) - 1387
}) - 1388
.collect(); - 1389
let mut bound_targets = std::collections::HashSet::new(); - 1390
let mut bindings = gateway - 1391
.into_iter() - 1392
.map(|(target, binding)| { - 1393
bound_targets.insert(target.clone()); - 1394
let agent_id = allowlist_map - 1395
.get(&target) - 1396
.cloned() - 1397
.unwrap_or_else(|| "vak".to_string()); - 1398
serde_json::json!({ - 1399
"target": target, - 1400
"session_id": binding.session_id, - 1401
"workspace": binding.workspace, - 1402
"agent_id": agent_id, - 1403
"provider": binding - 1404
.provider - 1405
.unwrap_or_else(|| default_route.provider.clone()), - 1406
"model": binding.model.unwrap_or_else(|| default_route.model.clone()), - 1407
"route_revision": binding - 1408
.route_revision - 1409
.unwrap_or_else(|| default_route.revision.clone()), - 1410
}) - 1411
}) - 1412
.collect::<Vec<_>>(); - 1413
for entry in allowlist_snapshot { - 1414
if entry.status != gateway::AllowlistStatus::Allowed || bound_targets.contains(&entry.key) { - 1415
continue; - 1416
} - 1417
bindings.push(serde_json::json!({ - 1418
"target": entry.key, - 1419
"session_id": null, - 1420
"workspace": state.gateway.workspace_for_entry(&state.core, &entry.key), - 1421
"agent_id": entry.agent_id.as_deref().unwrap_or("vak"), - 1422
"provider": entry.route.as_ref().map(|route| route.provider.clone()).unwrap_or_else(|| state.core.effective_provider()), - 1423
"model": entry.route.as_ref().map(|route| route.model.clone()).unwrap_or_else(|| state.core.effective_model()), - 1424
"route_revision": entry.route.as_ref().map(|route| format!("channel:{}:{}", route.provider, route.model)).unwrap_or_else(|| state.core.effective_route().revision), - 1425
"cold": true, - 1426
})); - 1427
} - 1428
let (outbox, outbox_pending, outbox_dead, outbox_error) = match operation_outbox(&state) { - 1429
Ok((rows, pending, dead)) => (rows, pending, dead, None), - 1430
Err(error) => (Vec::new(), 0, 0, Some(error)), - 1431
}; - 1432
let runs = operation_runs(&state); - 1433
let approvals = state - 1434
.live_handles() - 1435
.into_iter() - 1436
.map(|handle| { - 1437
handle - 1438
.pending - 1439
.lock() - 1440
.unwrap_or_else(std::sync::PoisonError::into_inner) - 1441
.len() - 1442
}) - 1443
.sum::<usize>(); - 1444
let security = vak_core::security_events::list(&state.core.sessions_home(), 30) - 1445
.into_iter() - 1446
.map(|event| serde_json::to_value(event).unwrap_or_else(|_| serde_json::json!({}))) - 1447
.collect::<Vec<_>>(); - 1448
let workspace = Some(state.core.cwd().to_string_lossy().into_owned()); - 1449
let mut candidates = Vec::new(); - 1450
if health["failures"].as_u64().unwrap_or(0) > 0 { - 1451
let evidence = health["checks"] - 1452
.as_array() - 1453
.into_iter() - 1454
.flatten() - 1455
.filter(|check| check["status"] == "fail") - 1456
.filter_map(|check| check["label"].as_str().map(str::to_string)) - 1457
.collect(); - 1458
candidates.push(operations::IncidentCandidate { - 1459
fingerprint: "doctor:health-checks".to_string(), - 1460
severity: "critical".to_string(), - 1461
source: "doctor".to_string(), - 1462
title: "Health checks need attention".to_string(), - 1463
detail: format!("{} check(s) failed", health["failures"]), - 1464
workspace: workspace.clone(), - 1465
evidence, - 1466
}); - 1467
} - 1468
if approvals > 0 { - 1469
candidates.push(operations::IncidentCandidate { - 1470
fingerprint: "permission:pending-approvals".to_string(), - 1471
severity: "warning".to_string(), - 1472
source: "permission-engine".to_string(), - 1473
title: "Runs are waiting for approval".to_string(), - 1474
detail: format!("{approvals} approval gate(s) are blocking work"), - 1475
workspace: workspace.clone(), - 1476
evidence: runs - 1477
.iter() - 1478
.filter_map(|run| run["session_id"].as_str().map(|id| format!("session:{id}"))) - 1479
.collect(), - 1480
}); - 1481
} - 1482
if outbox_pending > 0 || outbox_dead > 0 { - 1483
candidates.push(operations::IncidentCandidate { - 1484
fingerprint: "delivery:outbox".to_string(), - 1485
severity: if outbox_dead > 0 { - 1486
"critical" - 1487
} else { - 1488
"warning" - 1489
} - 1490
.to_string(), - 1491
source: "delivery".to_string(), - 1492
title: "Outbound delivery needs attention".to_string(), - 1493
detail: format!("{outbox_pending} pending, {outbox_dead} dead-lettered"), - 1494
workspace: workspace.clone(), - 1495
evidence: outbox - 1496
.iter() - 1497
.filter(|row| row["state"] != "delivered") - 1498
.filter_map(|row| row["job_id"].as_str().map(|id| format!("outbox:{id}"))) - 1499
.collect(), - 1500
}); - 1501
} - 1502
if let Some(error) = &outbox_error { - 1503
candidates.push(operations::IncidentCandidate { - 1504
fingerprint: "delivery:outbox-read".to_string(), - 1505
severity: "critical".to_string(), - 1506
source: "delivery".to_string(), - 1507
title: "Delivery evidence is unavailable".to_string(), - 1508
detail: error.clone(), - 1509
workspace: workspace.clone(), - 1510
evidence: vec!["outbox:read".to_string()], - 1511
}); - 1512
} - 1513
if services["gateway"]["state"] != "running" && state.gateway.enabled { - 1514
candidates.push(operations::IncidentCandidate { - 1515
fingerprint: "service:gateway".to_string(), - 1516
severity: "warning".to_string(), - 1517
source: "service-manager".to_string(), - 1518
title: "Gateway service is not running".to_string(), - 1519
detail: services["gateway"]["state"] - 1520
.as_str() - 1521
.unwrap_or("unknown") - 1522
.to_string(), - 1523
workspace: workspace.clone(), - 1524
evidence: vec!["service:gateway".to_string()], - 1525
}); - 1526
} - 1527
let incidents = operations::reconcile(&state.core.sessions_home(), candidates) - 1528
.into_iter() - 1529
.map(|incident| serde_json::to_value(incident).unwrap_or_else(|_| serde_json::json!({}))) - 1530
.collect::<Vec<_>>(); - 1531
let mut all_agents = vec![serde_json::json!({ - 1532
"id": "vak", - 1533
"name": "Vakyartha", - 1534
"personality": "Codex-grade safety, pi-grade transparency, Claude Code-grade extensibility, opencode-grade simplicity.", - 1535
"lifecycle": "active", - 1536
})]; - 1537
if let Ok(custom) = agents::effective(&state.core) { - 1538
for a in custom { - 1539
all_agents.push(serde_json::json!({ - 1540
"id": a.id, - 1541
"name": a.name, - 1542
"personality": a.personality, - 1543
"lifecycle": a.lifecycle, - 1544
})); - 1545
} - 1546
} - 1547
Json(serde_json::json!({ - 1548
"generated_at": Utc::now(), - 1549
"server": { - 1550
"pid": std::process::id(), - 1551
"version": env!("CARGO_PKG_VERSION"), - 1552
"uptime_secs": state.started_at.elapsed().as_secs(), - 1553
"cwd": state.core.cwd(), - 1554
"posture": health["posture"], - 1555
}, - 1556
"health": health, - 1557
"services": services, - 1558
"agents": all_agents, - 1559
"gateway": { - 1560
"enabled": state.gateway.enabled, - 1561
"approvals": { "pending": approvals, "mode": state.gateway.approvals_mode(), "approver": state.gateway.approver_target() }, - 1562
"bindings": bindings, - 1563
"workspace_catalog": crate::admin::workspace_catalog(&state), - 1564
}, - 1565
"pool": { - 1566
"max": state.core.config().gateway.core_pool_max, - 1567
"idle_secs": state.core.config().gateway.core_pool_idle_secs, - 1568
"entries": pool, - 1569
}, - 1570
"runs": runs, - 1571
"tasks": operation_tasks(&state), - 1572
"outbox": { "pending": outbox_pending, "dead_letter": outbox_dead, "records": outbox, "error": outbox_error }, - 1573
"bus": state.hub.bus_status(), - 1574
"security": security, - 1575
"incidents": incidents, - 1576
"actions": operations::recent_actions(&state.core.sessions_home(), 50), - 1577
"ops_port": ops_port, - 1578
})) - 1579
} - 1580
- 1581
async fn operations_actions(State(state): State<AppState>) -> Json<serde_json::Value> { - 1582
Json(serde_json::json!({ - 1583
"actions": operations::recent_actions(&state.core.sessions_home(), 200), - 1584
})) - 1585
} - 1586
- 1587
async fn operations_incidents(State(state): State<AppState>) -> Json<serde_json::Value> { - 1588
Json(serde_json::json!({ - 1589
"incidents": operations::list(&state.core.sessions_home()), - 1590
})) - 1591
} - 1592
- 1593
async fn operations_outbox(State(state): State<AppState>) -> axum::response::Response { - 1594
match delivery::outbox_records(&state.core) { - 1595
Ok(records) => Json(serde_json::json!({ - 1596
"records": records.into_iter().take(500).map(|record| serde_json::json!({ - 1597
"job_id": record.job.job_id, - 1598
"target": record.job.target, - 1599
"kind": record.job.kind, - 1600
"state": record.state, - 1601
"attempts": record.attempts, - 1602
"created_at_ms": record.created_at_ms, - 1603
"updated_at_ms": record.updated_at_ms, - 1604
"last_error": record.last_error, - 1605
})).collect::<Vec<_>>(), - 1606
})) - 1607
.into_response(), - 1608
Err(error) => ( - 1609
StatusCode::INTERNAL_SERVER_ERROR, - 1610
Json(serde_json::json!({ "error": error })), - 1611
) - 1612
.into_response(), - 1613
} - 1614
} - 1615
- 1616
async fn replay_operations_outbox( - 1617
State(state): State<AppState>, - 1618
Path(job_id): Path<String>, - 1619
) -> axum::response::Response { - 1620
let state_label = |state: vak_delivery::outbox::OutboxState| match state { - 1621
vak_delivery::outbox::OutboxState::Pending => "pending", - 1622
vak_delivery::outbox::OutboxState::Delivered => "delivered", - 1623
vak_delivery::outbox::OutboxState::DeadLetter => "dead_letter", - 1624
}; - 1625
let before = delivery::outbox_records(&state.core) - 1626
.ok() - 1627
.and_then(|records| { - 1628
records - 1629
.into_iter() - 1630
.find(|record| record.job.job_id == job_id) - 1631
}) - 1632
.map(|record| state_label(record.state).to_string()) - 1633
.unwrap_or_else(|| "not found".to_string()); - 1634
let requested_at = Utc::now(); - 1635
match delivery::replay_outbox_job(&state.core, &job_id).await { - 1636
Ok(()) => { - 1637
let after_record = delivery::outbox_records(&state.core) - 1638
.ok() - 1639
.and_then(|records| { - 1640
records - 1641
.into_iter() - 1642
.find(|record| record.job.job_id == job_id) - 1643
}); - 1644
let after = after_record - 1645
.as_ref() - 1646
.map(|record| state_label(record.state).to_string()) - 1647
.unwrap_or_else(|| "not found".to_string()); - 1648
if after == "delivered" - 1649
&& let Some(record) = after_record.as_ref() - 1650
&& let vak_delivery::DeliveryContent::Answer(answer) = &record.job.content - 1651
&& let Some(task_id) = answer.metadata.get("vak_task_id") - 1652
{ - 1653
update_tasks(&state, |tasks| { - 1654
if let Some(task) = tasks.get_mut(task_id) { - 1655
task.last_delivery_state = Some("delivered".into()); - 1656
} - 1657
}); - 1658
} - 1659
let verification_status = if after == "delivered" { - 1660
"verified" - 1661
} else { - 1662
"pending"
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.