main.vala 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. using Astralis;
  2. using Inversion;
  3. using InvercargillSqlInversion;
  4. using Spry;
  5. using Spry.Authentication;
  6. using Statum;
  7. using ClankerSaddle;
  8. using ClankerSaddle.Api;
  9. using ClankerSaddle.Data;
  10. using ClankerSaddle.Services;
  11. int main(string[] args) {
  12. int port = args.length > 1 ? int.parse(args[1]) : 8080;
  13. print("ClankerSaddle - LLM task orchestration platform\n");
  14. print(@"Website starting on port $(port)\n");
  15. try {
  16. var application = new WebApplication(port);
  17. application.use_compression();
  18. application.add_module<StatumModule>();
  19. application.add_module<SpryModule>();
  20. var db_path = Environment.get_variable("SPRY_DB_PATH") ?? "";
  21. if (db_path.strip().length == 0) {
  22. db_path = "./clanker-saddle.sqlite";
  23. }
  24. var db = application.container.configure_with<DatabaseConfigurator>();
  25. db.register_connection(@"sqlite://$(db_path)");
  26. // SQLite serialisation posture (the flake family where a SELECT
  27. // stepping against a concurrent write lock — the test harness's
  28. // fixture writes share this file — returned SQLITE_BUSY, which
  29. // the ORM's lazy enumerator reports as end-of-rows: an EMPTY
  30. // result for data that exists): WAL readers never block on (and
  31. // are never blocked by) writers, and the busy timeout absorbs
  32. // the remaining writer-writer window.
  33. try {
  34. var connection_scope = application.container.create_scope();
  35. var connection = connection_scope.resolve<InvercargillSql.Connection>();
  36. connection.execute("PRAGMA journal_mode = WAL");
  37. connection.execute("PRAGMA busy_timeout = 5000");
  38. connection_scope.release();
  39. } catch (Error pragma_error) {
  40. print(@"[ClankerSaddle] sqlite pragmas failed: $(pragma_error.message)\n");
  41. }
  42. db.add_migration<ClankerSaddle.Data.Migrations.M0001_Initial>();
  43. db.add_migration<ClankerSaddle.Data.Migrations.M0002_BatchImprovements>();
  44. db.add_migration<ClankerSaddle.Data.Migrations.M0003_EventIndexes>();
  45. db.add_migration<ClankerSaddle.Data.Migrations.M0004_ProjectUnderstandings>();
  46. db.add_migration<ClankerSaddle.Data.Migrations.M0005_Discoveries>();
  47. db.add_migration<ClankerSaddle.Data.Migrations.M0006_Advisor>();
  48. db.add_migration<ClankerSaddle.Data.Migrations.M0007_ParkingGuard>();
  49. db.add_migration<ClankerSaddle.Data.Migrations.M0008_AnswerWake>();
  50. db.add_migration<ClankerSaddle.Data.Migrations.M0009_TurnCapPark>();
  51. db.add_migration<ClankerSaddle.Data.Migrations.M0010_ModelConfig>();
  52. db.add_entity<Project>(Project.entity_mapping);
  53. db.add_entity<ProjectLink>(ProjectLink.entity_mapping);
  54. db.add_entity<TaskEntity>(TaskEntity.entity_mapping);
  55. db.add_entity<TaskComment>(TaskComment.entity_mapping);
  56. db.add_entity<TaskBlock>(TaskBlock.entity_mapping);
  57. db.add_entity<EventRecord>(EventRecord.entity_mapping);
  58. db.add_entity<Saddle>(Saddle.entity_mapping);
  59. db.add_entity<Session>(Session.entity_mapping);
  60. db.add_entity<Rule>(Rule.entity_mapping);
  61. db.add_entity<RuleProject>(RuleProject.entity_mapping);
  62. db.add_entity<TaskRule>(TaskRule.entity_mapping);
  63. db.add_entity<ProjectNote>(ProjectNote.entity_mapping);
  64. db.add_entity<ProjectUnderstanding>(ProjectUnderstanding.entity_mapping);
  65. db.add_entity<Discovery>(Discovery.entity_mapping);
  66. db.add_entity<Capability>(Capability.entity_mapping);
  67. db.add_entity<SaddleCapability>(SaddleCapability.entity_mapping);
  68. db.add_entity<TaskCapability>(TaskCapability.entity_mapping);
  69. db.add_entity<PushSubscription>(PushSubscription.entity_mapping);
  70. db.add_entity<TaskNotifyPref>(TaskNotifyPref.entity_mapping);
  71. db.add_entity<Goal>(Goal.entity_mapping);
  72. db.add_entity<GoalLink>(GoalLink.entity_mapping);
  73. db.add_entity<AdvisorChat>(AdvisorChat.entity_mapping);
  74. db.add_entity<AdvisorMessage>(AdvisorMessage.entity_mapping);
  75. db.add_entity<AdvisorSuggestion>(AdvisorSuggestion.entity_mapping);
  76. db.add_entity<Stack>(Stack.entity_mapping);
  77. db.add_entity<StackProject>(StackProject.entity_mapping);
  78. db.add_entity<Note>(Note.entity_mapping);
  79. db.add_entity<ModelConfig>(ModelConfig.entity_mapping);
  80. db.migrate_on_startup();
  81. application.add_scoped<ProjectService>();
  82. application.add_scoped<TaskService>();
  83. application.add_scoped<EventService>();
  84. application.add_scoped<GoalService>();
  85. application.add_scoped<StackService>();
  86. application.add_scoped<NoteService>();
  87. application.add_scoped<UnderstandingService>();
  88. application.add_scoped<DiscoveryService>();
  89. application.add_scoped<SaddleService>();
  90. application.add_scoped<LifecycleService>();
  91. application.add_scoped<RuleService>();
  92. application.add_scoped<CapabilityService>();
  93. application.add_scoped<AllocationService>();
  94. application.add_scoped<TimelineService>();
  95. application.add_scoped<ChatService>();
  96. application.add_singleton<SaddlePollState>();
  97. application.add_singleton<AnswerDeliveryHub>();
  98. application.add_singleton<StatePulse>();
  99. application.add_singleton<PushService>();
  100. application.add_singleton<Dispatcher>();
  101. application.add_singleton<ModelConfigService>();
  102. application.add_singleton<AdvisorService>();
  103. application.add_singleton<AdvisorCallback>();
  104. // Web Push (plan 11 task E): the VAPID keypair loads or
  105. // generates + persists BEFORE the server accepts requests, so
  106. // every subscriber sees the same public key from the first
  107. // page render.
  108. var key_scope = application.container.create_scope();
  109. key_scope.resolve<PushService>().ensure_keys_from_config();
  110. key_scope.release();
  111. var statum = application.configure_with<StatumConfigurator>();
  112. statum.global<ClankerSaddle.Services.PulsePublic>(StatePulse.SLOT_TYPE,
  113. new ClankerSaddle.Services.PulsePublic());
  114. // spry:pages-begin
  115. statum.add_page<HomePage, HomeEntrypoint>();
  116. statum.add_page<LoginPage, LoginEntrypoint>();
  117. statum.add_page<ProjectsPage, ProjectsEntrypoint>();
  118. statum.add_page<ProjectDetailPage, ProjectDetailEntrypoint>();
  119. statum.add_page<TasksPage, TasksEntrypoint>();
  120. statum.add_page<TaskDetailPage, TaskDetailEntrypoint>();
  121. statum.add_page<TaskLogPage, TaskLogEntrypoint>();
  122. statum.add_page<GoalsPage, GoalsEntrypoint>();
  123. statum.add_page<GoalDetailPage, GoalDetailEntrypoint>();
  124. statum.add_page<SaddlesPage, SaddlesEntrypoint>();
  125. statum.add_page<RulesPage, RulesEntrypoint>();
  126. statum.add_page<DiscoveriesPage, DiscoveriesEntrypoint>();
  127. statum.add_page<ChatsPage, ChatsEntrypoint>();
  128. statum.add_page<ChatDetailPage, ChatDetailEntrypoint>();
  129. statum.add_page<StacksPage, StacksEntrypoint>();
  130. statum.add_page<StackDetailPage, StackDetailEntrypoint>();
  131. statum.add_page<UsersPage, UsersEntrypoint>();
  132. statum.add_page<ModelSettingsPage, ModelSettingsEntrypoint>();
  133. statum.add_page<ProfilePage, ProfileEntrypoint>();
  134. // spry:pages-end
  135. // spry:actions-begin
  136. statum.action<LoginAction>();
  137. statum.action<Spry.Actions.LogoutAction>();
  138. statum.action<CreateProjectAction>();
  139. statum.action<UpdateProjectAction>();
  140. statum.action<AddProjectLinkAction>();
  141. statum.action<RemoveProjectLinkAction>();
  142. statum.action<DeleteProjectAction>();
  143. statum.action<CreateTaskAction>();
  144. statum.action<UpdateTaskAction>();
  145. statum.action<AddCommentAction>();
  146. statum.action<AnswerBlockAction>();
  147. statum.action<EnqueueTaskAction>();
  148. statum.action<DeleteTaskAction>();
  149. statum.action<CancelTaskAction>();
  150. statum.action<RetryTaskAction>();
  151. statum.action<ResumeTaskAction>();
  152. statum.action<TaskForceWakeAction>();
  153. statum.action<RefreshTasksDataAction>();
  154. statum.action<CreateGoalAction>();
  155. statum.action<StartGoalAction>();
  156. statum.action<UpdateDraftGoalAction>();
  157. statum.action<PauseGoalAction>();
  158. statum.action<ResumeGoalAction>();
  159. statum.action<GoalForceWakeAction>();
  160. statum.action<CancelGoalAction>();
  161. statum.action<RetryGoalRunAction>();
  162. statum.action<RequeueGoalRunAction>();
  163. statum.action<GoalCommentAction>();
  164. statum.action<AnswerGoalBlockAction>();
  165. statum.action<AddGoalLinkAction>();
  166. statum.action<RemoveGoalLinkAction>();
  167. statum.action<RefreshGoalDataAction>();
  168. statum.action<PushSubscribeAction>();
  169. statum.action<PushUnsubscribeAction>();
  170. statum.action<PushTestAction>();
  171. statum.action<ToggleTaskNotifyAction>();
  172. statum.action<RefreshTaskDataAction>();
  173. statum.action<RefreshDashboardAction>();
  174. statum.action<RegisterSaddleAction>();
  175. statum.action<UnregisterSaddleAction>();
  176. statum.action<RefreshSaddleStatusAction>();
  177. statum.action<UpdateSaddleIdentityAction>();
  178. statum.action<UpdateSaddlePromptNoteAction>();
  179. statum.action<CreateRuleAction>();
  180. statum.action<UpdateRuleAction>();
  181. statum.action<DeleteRuleAction>();
  182. statum.action<AttachProjectRuleAction>();
  183. statum.action<DetachProjectRuleAction>();
  184. statum.action<AttachTaskRuleAction>();
  185. statum.action<DetachTaskRuleAction>();
  186. statum.action<DeleteProjectNoteAction>();
  187. statum.action<RefreshUnderstandingAction>();
  188. statum.action<UpdateDiscoveryAction>();
  189. statum.action<DeleteDiscoveryAction>();
  190. statum.action<StartAdvisorChatAction>();
  191. statum.action<SendAdvisorMessageAction>();
  192. statum.action<AnswerAdvisorQuestionAction>();
  193. statum.action<DeleteAdvisorChatAction>();
  194. statum.action<RefreshAdvisorChatAction>();
  195. statum.action<DecideAdvisorSuggestionAction>();
  196. statum.action<CreateStackAction>();
  197. statum.action<UpdateStackAction>();
  198. statum.action<DeleteStackAction>();
  199. statum.action<AddStackMemberAction>();
  200. statum.action<RemoveStackMemberAction>();
  201. statum.action<ApplyStackToProjectAction>();
  202. statum.action<ApplyStackToGoalAction>();
  203. statum.action<AddNoteAction>();
  204. statum.action<ToggleNoteAction>();
  205. statum.action<DeleteNoteAction>();
  206. statum.action<PromoteNoteAction>();
  207. statum.action<CreateCapabilityAction>();
  208. statum.action<DeleteCapabilityAction>();
  209. statum.action<AttachSaddleCapabilityAction>();
  210. statum.action<DetachSaddleCapabilityAction>();
  211. statum.action<AttachTaskCapabilityAction>();
  212. statum.action<DetachTaskCapabilityAction>();
  213. statum.action<CreateUserAction>();
  214. statum.action<AlterManagedUserAction>();
  215. statum.action<SetManagedUserEnabledAction>();
  216. statum.action<GrantUserAdminAction>();
  217. statum.action<RevokeUserAdminAction>();
  218. statum.action<DeleteManagedUserAction>();
  219. statum.action<ChangeMyPasswordAction>();
  220. statum.action<SaveModelConfigAction>();
  221. // spry:actions-end
  222. // spry:resources-begin
  223. statum.add_resource<ClankerSaddle.AppCss>();
  224. statum.add_resource<ClankerSaddle.AppJs>();
  225. statum.add_resource<ClankerSaddle.TaskLogJs>();
  226. statum.add_resource<ClankerSaddle.FaviconResource>();
  227. statum.add_resource<ClankerSaddle.EmblemResource>();
  228. // spry:resources-end
  229. // spry:auth-begin
  230. // spry:auth-end
  231. application.add_endpoint<SaddleNextTaskEndpoint>(
  232. new EndpointRoute("/api/saddle/next-task", Method.POST));
  233. application.add_endpoint<SaddleChunksEndpoint>(
  234. new EndpointRoute("/api/saddle/chunks", Method.POST));
  235. application.add_endpoint<SaddleResultsEndpoint>(
  236. new EndpointRoute("/api/saddle/results", Method.POST));
  237. application.add_endpoint<SaddleNoteEndpoint>(
  238. new EndpointRoute("/api/saddle/note", Method.POST));
  239. application.add_endpoint<SaddleUnderstandingEndpoint>(
  240. new EndpointRoute("/api/saddle/understanding", Method.POST));
  241. application.add_endpoint<SaddleUnderstandingEndpoint>(
  242. new EndpointRoute("/api/saddle/understanding", Method.GET));
  243. application.add_endpoint<SaddleDiscoveryEndpoint>(
  244. new EndpointRoute("/api/saddle/discovery", Method.POST));
  245. application.add_endpoint<SaddleDiscoveryEndpoint>(
  246. new EndpointRoute("/api/saddle/discoveries", Method.GET));
  247. application.add_endpoint<SaddleDiscoveryUpdateEndpoint>(
  248. new EndpointRoute("/api/saddle/discovery/update", Method.POST));
  249. application.add_endpoint<SaddleTaskCreateEndpoint>(
  250. new EndpointRoute("/api/saddle/task/create", Method.POST));
  251. application.add_endpoint<SaddleTaskDeleteEndpoint>(
  252. new EndpointRoute("/api/saddle/task/delete", Method.POST));
  253. application.add_endpoint<SaddleTaskEnqueueEndpoint>(
  254. new EndpointRoute("/api/saddle/task/enqueue", Method.POST));
  255. application.add_endpoint<SaddleTaskListEndpoint>(
  256. new EndpointRoute("/api/saddle/tasks", Method.GET));
  257. application.add_endpoint<SaddleCanonListEndpoint>(
  258. new EndpointRoute("/api/saddle/canon", Method.GET));
  259. application.add_endpoint<SaddleCanonTextEndpoint>(
  260. new EndpointRoute("/api/saddle/canon/{slug}", Method.GET));
  261. application.add_endpoint<SaddleEthosEndpoint>(
  262. new EndpointRoute("/api/saddle/ethos", Method.GET));
  263. application.add_endpoint<SaddleTaskDetailEndpoint>(
  264. new EndpointRoute("/api/saddle/task/{id}", Method.GET));
  265. application.add_endpoint<SaddleCapabilitiesEndpoint>(
  266. new EndpointRoute("/api/saddle/capabilities", Method.GET));
  267. application.add_endpoint<SaddleGoalsEndpoint>(
  268. new EndpointRoute("/api/saddle/goals", Method.GET));
  269. application.add_endpoint<ClankerSaddle.Api.TaskEventsEndpoint>(
  270. new EndpointRoute("/tasks/{task_id}/events", Method.GET));
  271. application.add_endpoint<ClankerSaddle.Api.EventStreamSseEndpoint>(
  272. new EndpointRoute("/tasks/{task_id}/events/stream", Method.GET));
  273. application.add_endpoint<ClankerSaddle.Api.ServiceWorkerEndpoint>(
  274. new EndpointRoute("/sw.js", Method.GET));
  275. application.add_endpoint<ClankerSaddle.Api.AdvisorChatEndpoint>(
  276. new EndpointRoute("/api/advisor/chat", Method.POST));
  277. application.add_endpoint<ClankerSaddle.Api.AdvisorChatEndpoint>(
  278. new EndpointRoute("/api/advisor/turn", Method.POST));
  279. application.add_endpoint<ClankerSaddle.Api.AdvisorChatEndpoint>(
  280. new EndpointRoute("/api/advisor/answer", Method.POST));
  281. application.add_endpoint<ClankerSaddle.Api.AdvisorChatEndpoint>(
  282. new EndpointRoute("/api/advisor/chat/delete", Method.POST));
  283. application.add_endpoint<ClankerSaddle.Api.AdvisorChatDetailEndpoint>(
  284. new EndpointRoute("/api/advisor/chat/{id}", Method.GET));
  285. application.add_endpoint<ClankerSaddle.Api.ProjectDeleteEndpoint>(
  286. new EndpointRoute("/api/projects/{id}", Method.DELETE));
  287. GLib.Idle.add(() => {
  288. seed_admin.begin(application.container);
  289. start_dispatcher.begin(application.container);
  290. start_advisor_callback.begin(application.container);
  291. return false;
  292. });
  293. log_canon_health();
  294. application.run();
  295. return 0;
  296. } catch (Error e) {
  297. printerr("Error: %s\n", e.message);
  298. return 1;
  299. }
  300. }
  301. /**
  302. * Seeds the admin account from SPRY_SEED_ADMIN_USERNAME and
  303. * SPRY_SEED_ADMIN_PASSWORD, defaulting both to admin/change-me when unset.
  304. * The password is never logged.
  305. */
  306. private async void seed_admin(Container container) {
  307. var username = Environment.get_variable("SPRY_SEED_ADMIN_USERNAME") ?? "admin";
  308. var password = Environment.get_variable("SPRY_SEED_ADMIN_PASSWORD") ?? "change-me";
  309. if (username.length == 0 || password.length == 0) {
  310. return;
  311. }
  312. try {
  313. var scope = container.create_scope();
  314. yield SpryAuth.ensure_admin(scope.resolve<UserService>(),
  315. username, @"$(username)@example.com", password);
  316. scope.release();
  317. print("[ClankerSaddle] Seeded admin account: %s\n", username);
  318. } catch (Error e) {
  319. printerr("[ClankerSaddle] Admin seeding failed: %s\n", e.message);
  320. }
  321. }
  322. private async void start_dispatcher(Container container) {
  323. try {
  324. var scope = container.create_scope();
  325. scope.resolve<Dispatcher>().start();
  326. scope.release();
  327. } catch (Error e) {
  328. printerr("[ClankerSaddle] Dispatcher startup failed: %s\n", e.message);
  329. }
  330. }
  331. /**
  332. * Starts the advisor completion callback's periodic pass alongside
  333. * the dispatcher (goal 27): every ADVISOR_CALLBACK_TICK_S (15s) it
  334. * delivers terminal task outcomes into the chats that suggested
  335. * them.
  336. */
  337. private async void start_advisor_callback(Container container) {
  338. try {
  339. var scope = container.create_scope();
  340. scope.resolve<AdvisorCallback>().start();
  341. scope.release();
  342. } catch (Error e) {
  343. printerr("[ClankerSaddle] Advisor callback startup failed: %s\n",
  344. e.message);
  345. }
  346. }
  347. /**
  348. * The canon health line (goal 27 deploy hardening): the resolved
  349. * directory and the text count at startup, or — when nothing
  350. * resolves — a LOUD warning naming every candidate path tried and
  351. * the CLANKER_CANON_DIR override, so an empty canon shelf is
  352. * visible in the container logs without asking the bot. The same
  353. * fact rides the dashboard's public data as `canon_texts`.
  354. */
  355. private void log_canon_health() {
  356. var canon = new ClankerSaddle.Services.Canon();
  357. var directory = canon.directory();
  358. if (directory.length > 0) {
  359. print("[ClankerSaddle] Canon: %d texts from %s\n",
  360. canon.texts().length, directory);
  361. var errors = canon.errors();
  362. if (errors.length > 0) {
  363. printerr("[ClankerSaddle] WARNING: canon at %s carries %d "
  364. + "unparseable file(s): %s\n", directory, errors.length,
  365. string.joinv("; ", errors));
  366. }
  367. return;
  368. }
  369. var candidates = canon.candidates();
  370. printerr("[ClankerSaddle] WARNING: no canon directory resolved — the "
  371. + "canonical texts will be ABSENT (/api/saddle/canon answers an "
  372. + "empty library and the ethos has nothing to point at). Tried:\n");
  373. foreach (var candidate in candidates) {
  374. printerr("[ClankerSaddle] %s\n", candidate);
  375. }
  376. printerr("[ClankerSaddle] Set CLANKER_CANON_DIR to the directory "
  377. + "holding the canon *.md files (or install the "
  378. + "share/clanker-saddle/canon package files).\n");
  379. }