@gmickel/gno 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +14 -2
  2. package/assets/skill/SKILL.md +17 -1
  3. package/assets/skill/mcp-reference.md +21 -0
  4. package/package.json +2 -2
  5. package/src/cli/commands/daemon.ts +69 -2
  6. package/src/cli/commands/models/pull.ts +13 -3
  7. package/src/cli/commands/status.ts +2 -0
  8. package/src/cli/detach.ts +37 -20
  9. package/src/cli/program.ts +74 -27
  10. package/src/config/index.ts +3 -0
  11. package/src/config/types.ts +37 -0
  12. package/src/core/job-manager.ts +19 -0
  13. package/src/core/mutation-generations.ts +33 -0
  14. package/src/llm/cache.ts +13 -3
  15. package/src/llm/nodeLlamaCpp/adapter.ts +10 -1
  16. package/src/llm/nodeLlamaCpp/lifecycle.ts +71 -0
  17. package/src/mcp/context.ts +161 -0
  18. package/src/mcp/http-security.ts +477 -0
  19. package/src/mcp/http-session.ts +272 -0
  20. package/src/mcp/http-transport.ts +370 -0
  21. package/src/mcp/resources/index.ts +141 -134
  22. package/src/mcp/server.ts +19 -79
  23. package/src/mcp/tools/add-collection.ts +3 -1
  24. package/src/mcp/tools/capture.ts +3 -0
  25. package/src/mcp/tools/clear-collection-embeddings.ts +2 -0
  26. package/src/mcp/tools/context.ts +9 -8
  27. package/src/mcp/tools/embed.ts +62 -52
  28. package/src/mcp/tools/index-cmd.ts +88 -74
  29. package/src/mcp/tools/index.ts +22 -2
  30. package/src/mcp/tools/remove-collection.ts +2 -0
  31. package/src/mcp/tools/status.ts +11 -0
  32. package/src/mcp/tools/sync.ts +16 -14
  33. package/src/mcp/tools/workspace-write.ts +7 -3
  34. package/src/serve/background-runtime.ts +12 -212
  35. package/src/serve/embed-scheduler.ts +74 -43
  36. package/src/serve/index.ts +9 -0
  37. package/src/serve/jobs.ts +78 -80
  38. package/src/serve/public/components/HealthCenter.tsx +74 -1
  39. package/src/serve/public/globals.built.css +1 -1
  40. package/src/serve/public/pages/Dashboard.tsx +1 -0
  41. package/src/serve/resident-admission.ts +159 -0
  42. package/src/serve/resident-background-work.ts +39 -0
  43. package/src/serve/resident-request.ts +55 -0
  44. package/src/serve/resident-runtime.ts +490 -0
  45. package/src/serve/resident-status.ts +96 -0
  46. package/src/serve/routes/api.ts +263 -167
  47. package/src/serve/routes/mcp.ts +69 -0
  48. package/src/serve/server.ts +191 -37
  49. package/src/serve/status-model.ts +51 -0
  50. package/src/serve/status.ts +5 -0
  51. package/src/store/sqlite/adapter.ts +26 -9
@@ -0,0 +1,69 @@
1
+ /** Production route adapter for the secured resident MCP endpoint. */
2
+
3
+ import type {
4
+ HttpMcpPeerServer,
5
+ ResolvedHttpGatewayConfig,
6
+ } from "../../mcp/http-security";
7
+ import type { ResidentRuntime } from "../resident-runtime";
8
+
9
+ import { HttpMcpSecurity } from "../../mcp/http-security";
10
+ import { HttpMcpTransport } from "../../mcp/http-transport";
11
+
12
+ export type McpHttpRoute = (
13
+ request: Request,
14
+ server: HttpMcpPeerServer
15
+ ) => Promise<Response>;
16
+
17
+ export interface McpHttpGateway {
18
+ readonly route: McpHttpRoute;
19
+ readonly security: HttpMcpSecurity;
20
+ readonly transport: HttpMcpTransport;
21
+ close(): Promise<void>;
22
+ }
23
+
24
+ /**
25
+ * Build the route only after startup policy and token-file checks succeed.
26
+ * Every method passes through the external boundary before transport dispatch.
27
+ */
28
+ export async function createMcpHttpGateway(
29
+ runtime: ResidentRuntime,
30
+ config: ResolvedHttpGatewayConfig
31
+ ): Promise<McpHttpGateway> {
32
+ runtime.mcpContext.enableWrite = config.enableWrite;
33
+ const transport = new HttpMcpTransport(runtime, {
34
+ enableWrite: config.enableWrite,
35
+ idleTimeoutMs: config.limits.sessionIdleTimeoutMs,
36
+ maxConcurrentRequests: config.limits.maxConcurrentRequests,
37
+ maxQueuedRequests: config.limits.maxQueuedRequests,
38
+ maxSessions: config.limits.maxSessions,
39
+ });
40
+ const security = new HttpMcpSecurity(config, {
41
+ onCredentialsChanged: () => transport.invalidateAuthenticatedSessions(),
42
+ });
43
+ await security.initialize();
44
+ (runtime as Partial<ResidentRuntime>).setTransportStatusProvider?.(() =>
45
+ transport.getStatus()
46
+ );
47
+
48
+ const route: McpHttpRoute = async (request, server) => {
49
+ const authorization = await security.authorize(request, server);
50
+ if (!authorization.ok) return authorization.response;
51
+
52
+ // POST responses and GET streams may both be long-lived SSE responses.
53
+ server.timeout(request, 0);
54
+ return transport.handleRequest(authorization.value.request, {
55
+ identity: authorization.value.identity,
56
+ parsedBody: authorization.value.parsedBody,
57
+ });
58
+ };
59
+
60
+ return {
61
+ route,
62
+ security,
63
+ transport,
64
+ close: async () => {
65
+ await transport.close();
66
+ (runtime as Partial<ResidentRuntime>).setTransportStatusProvider?.(null);
67
+ },
68
+ };
69
+ }
@@ -6,13 +6,20 @@
6
6
  * @module src/serve/server
7
7
  */
8
8
 
9
+ import type { HttpGatewayOverrides } from "../mcp/http-security";
10
+ import type { ResidentRuntime } from "./resident-runtime";
9
11
  import type { ContextHolder } from "./routes/api";
10
12
 
13
+ import {
14
+ isHttpGatewayLoopbackBind,
15
+ resolveHttpGatewayConfig,
16
+ } from "../mcp/http-security";
11
17
  import { startBackgroundRuntime } from "./background-runtime";
12
18
  import { handleContextBuild, handleContextVerify } from "./context-capsule";
13
19
  import { DocumentEventBus } from "./doc-events";
14
20
  // HTML import - Bun handles bundling TSX/CSS automatically via routes
15
21
  import homepage from "./public/index.html";
22
+ import { handleResidentRead } from "./resident-request";
16
23
  import {
17
24
  handleActiveJob,
18
25
  handleAsk,
@@ -49,6 +56,7 @@ import {
49
56
  handleQuery,
50
57
  handleQueryDiagnose,
51
58
  handleRefactorPlan,
59
+ handleResidentStatus,
52
60
  handleRenameDoc,
53
61
  handleRevealDoc,
54
62
  handleSearch,
@@ -67,9 +75,10 @@ import {
67
75
  handleDocLinks,
68
76
  handleDocSimilar,
69
77
  } from "./routes/links";
78
+ import { createMcpHttpGateway } from "./routes/mcp";
70
79
  import { forbiddenResponse, isRequestAllowed } from "./security";
71
80
 
72
- export interface ServeOptions {
81
+ export interface ServeOptions extends HttpGatewayOverrides {
73
82
  /** Port to listen on (default: 3000) */
74
83
  port?: number;
75
84
  /** Config path override */
@@ -85,8 +94,14 @@ export interface ServeResult {
85
94
 
86
95
  interface StartServerDependencies {
87
96
  startBackgroundRuntime?: typeof startBackgroundRuntime;
97
+ createMcpHttpGateway?: typeof createMcpHttpGateway;
88
98
  serve?: typeof Bun.serve;
89
99
  handleInstallConnector?: typeof handleInstallConnector;
100
+ handleDocs?: typeof handleDocs;
101
+ handleVerifyConnector?: typeof handleVerifyConnector;
102
+ handleImportPreview?: typeof handleImportPreview;
103
+ handlePublishExport?: typeof handlePublishExport;
104
+ handleRefactorPlan?: typeof handleRefactorPlan;
90
105
  waitForShutdown?: (signal: AbortSignal) => Promise<void>;
91
106
  }
92
107
 
@@ -152,6 +167,7 @@ export async function startServer(
152
167
  const runtimeResult = await (
153
168
  dependencies.startBackgroundRuntime ?? startBackgroundRuntime
154
169
  )({
170
+ mode: "serve",
155
171
  configPath: options.configPath,
156
172
  index: options.index,
157
173
  requireCollections: false,
@@ -163,6 +179,35 @@ export async function startServer(
163
179
  const runtime = runtimeResult.runtime;
164
180
  const store = runtime.store;
165
181
  const ctxHolder: ContextHolder = runtime.ctxHolder;
182
+ const gatewayConfig = resolveHttpGatewayConfig(runtime.config.gateway, {
183
+ host: options.host,
184
+ port,
185
+ tokenFile: options.tokenFile,
186
+ allowedHosts: options.allowedHosts,
187
+ allowedOrigins: options.allowedOrigins,
188
+ enableWrite: options.enableWrite,
189
+ });
190
+ if (!isHttpGatewayLoopbackBind(gatewayConfig.host)) {
191
+ await runtime.dispose();
192
+ return {
193
+ success: false,
194
+ error:
195
+ "gno serve remains loopback-only because Web and REST share its listener; use gno daemon for authenticated non-loopback MCP",
196
+ };
197
+ }
198
+ let gateway: Awaited<ReturnType<typeof createMcpHttpGateway>>;
199
+ try {
200
+ gateway = await (dependencies.createMcpHttpGateway ?? createMcpHttpGateway)(
201
+ runtime as ResidentRuntime,
202
+ gatewayConfig
203
+ );
204
+ } catch (error) {
205
+ await runtime.dispose();
206
+ return {
207
+ success: false,
208
+ error: error instanceof Error ? error.message : String(error),
209
+ };
210
+ }
166
211
 
167
212
  // Shutdown controller for clean lifecycle
168
213
  const shutdownController = new AbortController();
@@ -185,13 +230,14 @@ export async function startServer(
185
230
  try {
186
231
  server = (dependencies.serve ?? Bun.serve)({
187
232
  port,
188
- hostname: "127.0.0.1", // Loopback only - no LAN exposure
233
+ hostname: gatewayConfig.host,
189
234
 
190
235
  // Enable development mode for HMR and console logging
191
236
  development: isDev,
192
237
 
193
238
  // Static routes - Bun handles HTML bundling and /_bun/* assets automatically
194
239
  routes: {
240
+ "/mcp": gateway.route,
195
241
  // SPA routes - all serve the same React app
196
242
  "/": homepage,
197
243
  "/search": homepage,
@@ -208,8 +254,25 @@ export async function startServer(
208
254
  GET: () => withSecurityHeaders(handleHealth(), isDev),
209
255
  },
210
256
  "/api/status": {
211
- GET: async () =>
212
- withSecurityHeaders(await handleStatus(ctxHolder.current), isDev),
257
+ GET: async (req: Request) =>
258
+ withSecurityHeaders(
259
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
260
+ handleStatus(ctxHolder.current, {
261
+ getResidentStatus: () =>
262
+ (runtime as ResidentRuntime).getStatus(),
263
+ })
264
+ ),
265
+ isDev
266
+ ),
267
+ },
268
+ "/api/resident/status": {
269
+ GET: () =>
270
+ withSecurityHeaders(
271
+ handleResidentStatus(() =>
272
+ (runtime as ResidentRuntime).getStatus()
273
+ ),
274
+ isDev
275
+ ),
213
276
  },
214
277
  "/api/collections": {
215
278
  GET: async () =>
@@ -228,9 +291,11 @@ export async function startServer(
228
291
  },
229
292
  },
230
293
  "/api/connectors": {
231
- GET: async () =>
294
+ GET: async (req: Request) =>
232
295
  withSecurityHeaders(
233
- await handleConnectors(ctxHolder.config),
296
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
297
+ handleConnectors(ctxHolder.config)
298
+ ),
234
299
  isDev
235
300
  ),
236
301
  },
@@ -256,7 +321,13 @@ export async function startServer(
256
321
  return withSecurityHeaders(forbiddenResponse(), isDev);
257
322
  }
258
323
  return withSecurityHeaders(
259
- await handleVerifyConnector(ctxHolder.config, store, req),
324
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
325
+ (dependencies.handleVerifyConnector ?? handleVerifyConnector)(
326
+ ctxHolder.config,
327
+ store,
328
+ req
329
+ )
330
+ ),
260
331
  isDev
261
332
  );
262
333
  },
@@ -267,7 +338,12 @@ export async function startServer(
267
338
  return withSecurityHeaders(forbiddenResponse(), isDev);
268
339
  }
269
340
  return withSecurityHeaders(
270
- await handleImportPreview(ctxHolder, req),
341
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
342
+ (dependencies.handleImportPreview ?? handleImportPreview)(
343
+ ctxHolder,
344
+ req
345
+ )
346
+ ),
271
347
  isDev
272
348
  );
273
349
  },
@@ -278,7 +354,13 @@ export async function startServer(
278
354
  return withSecurityHeaders(forbiddenResponse(), isDev);
279
355
  }
280
356
  return withSecurityHeaders(
281
- await handlePublishExport(ctxHolder.config, store, req),
357
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
358
+ (dependencies.handlePublishExport ?? handlePublishExport)(
359
+ ctxHolder.config,
360
+ store,
361
+ req
362
+ )
363
+ ),
282
364
  isDev
283
365
  );
284
366
  },
@@ -308,7 +390,12 @@ export async function startServer(
308
390
  "/api/docs": {
309
391
  GET: async (req: Request) => {
310
392
  const url = new URL(req.url);
311
- return withSecurityHeaders(await handleDocs(store, url), isDev);
393
+ return withSecurityHeaders(
394
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
395
+ (dependencies.handleDocs ?? handleDocs)(store, url)
396
+ ),
397
+ isDev
398
+ );
312
399
  },
313
400
  POST: async (req: Request) => {
314
401
  if (!isRequestAllowed(req, port)) {
@@ -324,7 +411,9 @@ export async function startServer(
324
411
  GET: async (req: Request) => {
325
412
  const url = new URL(req.url);
326
413
  return withSecurityHeaders(
327
- await handleDocsAutocomplete(store, url),
414
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
415
+ handleDocsAutocomplete(store, url)
416
+ ),
328
417
  isDev
329
418
  );
330
419
  },
@@ -345,8 +434,13 @@ export async function startServer(
345
434
  },
346
435
  },
347
436
  "/api/browse/tree": {
348
- GET: async () =>
349
- withSecurityHeaders(await handleBrowseTree(store), isDev),
437
+ GET: async (req: Request) =>
438
+ withSecurityHeaders(
439
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
440
+ handleBrowseTree(store)
441
+ ),
442
+ isDev
443
+ ),
350
444
  },
351
445
  "/api/docs/:id/deactivate": {
352
446
  POST: async (req: Request) => {
@@ -358,7 +452,7 @@ export async function startServer(
358
452
  const parts = url.pathname.split("/");
359
453
  const id = decodeURIComponent(parts[3] || "");
360
454
  return withSecurityHeaders(
361
- await handleDeactivateDoc(store, id, req),
455
+ await handleDeactivateDoc(ctxHolder, store, id, req),
362
456
  isDev
363
457
  );
364
458
  },
@@ -414,7 +508,14 @@ export async function startServer(
414
508
  const parts = url.pathname.split("/");
415
509
  const id = decodeURIComponent(parts[3] || "");
416
510
  return withSecurityHeaders(
417
- await handleRefactorPlan(ctxHolder, store, id, req),
511
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
512
+ (dependencies.handleRefactorPlan ?? handleRefactorPlan)(
513
+ ctxHolder,
514
+ store,
515
+ id,
516
+ req
517
+ )
518
+ ),
418
519
  isDev
419
520
  );
420
521
  },
@@ -479,7 +580,9 @@ export async function startServer(
479
580
  GET: async (req: Request) => {
480
581
  const url = new URL(req.url);
481
582
  return withSecurityHeaders(
482
- await handleDoc(store, ctxHolder.config, url),
583
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
584
+ handleDoc(store, ctxHolder.config, url)
585
+ ),
483
586
  isDev
484
587
  );
485
588
  },
@@ -488,7 +591,9 @@ export async function startServer(
488
591
  GET: async (req: Request) => {
489
592
  const url = new URL(req.url);
490
593
  return withSecurityHeaders(
491
- await handleDocAsset(store, ctxHolder.config, url),
594
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
595
+ handleDocAsset(store, ctxHolder.config, url)
596
+ ),
492
597
  isDev
493
598
  );
494
599
  },
@@ -504,7 +609,12 @@ export async function startServer(
504
609
  "/api/tags": {
505
610
  GET: async (req: Request) => {
506
611
  const url = new URL(req.url);
507
- return withSecurityHeaders(await handleTags(store, url), isDev);
612
+ return withSecurityHeaders(
613
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
614
+ handleTags(store, url)
615
+ ),
616
+ isDev
617
+ );
508
618
  },
509
619
  },
510
620
  "/api/search": {
@@ -512,7 +622,12 @@ export async function startServer(
512
622
  if (!isRequestAllowed(req, port)) {
513
623
  return withSecurityHeaders(forbiddenResponse(), isDev);
514
624
  }
515
- return withSecurityHeaders(await handleSearch(store, req), isDev);
625
+ return withSecurityHeaders(
626
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
627
+ handleSearch(store, req)
628
+ ),
629
+ isDev
630
+ );
516
631
  },
517
632
  },
518
633
  "/api/query": {
@@ -521,7 +636,9 @@ export async function startServer(
521
636
  return withSecurityHeaders(forbiddenResponse(), isDev);
522
637
  }
523
638
  return withSecurityHeaders(
524
- await handleQuery(ctxHolder.current, req),
639
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
640
+ handleQuery(ctxHolder.current, req)
641
+ ),
525
642
  isDev
526
643
  );
527
644
  },
@@ -532,7 +649,9 @@ export async function startServer(
532
649
  return withSecurityHeaders(forbiddenResponse(), isDev);
533
650
  }
534
651
  return withSecurityHeaders(
535
- await handleContextBuild(ctxHolder.current, req),
652
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
653
+ handleContextBuild(ctxHolder.current, req)
654
+ ),
536
655
  isDev
537
656
  );
538
657
  },
@@ -543,7 +662,9 @@ export async function startServer(
543
662
  return withSecurityHeaders(forbiddenResponse(), isDev);
544
663
  }
545
664
  return withSecurityHeaders(
546
- await handleContextVerify(ctxHolder.current, req),
665
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
666
+ handleContextVerify(ctxHolder.current, req)
667
+ ),
547
668
  isDev
548
669
  );
549
670
  },
@@ -554,7 +675,9 @@ export async function startServer(
554
675
  return withSecurityHeaders(forbiddenResponse(), isDev);
555
676
  }
556
677
  return withSecurityHeaders(
557
- await handleQueryDiagnose(ctxHolder.current, req),
678
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
679
+ handleQueryDiagnose(ctxHolder.current, req)
680
+ ),
558
681
  isDev
559
682
  );
560
683
  },
@@ -565,7 +688,9 @@ export async function startServer(
565
688
  return withSecurityHeaders(forbiddenResponse(), isDev);
566
689
  }
567
690
  return withSecurityHeaders(
568
- await handleAsk(ctxHolder.current, req),
691
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
692
+ handleAsk(ctxHolder.current, req)
693
+ ),
569
694
  isDev
570
695
  );
571
696
  },
@@ -591,21 +716,30 @@ export async function startServer(
591
716
  GET: () => withSecurityHeaders(handleModelStatus(), isDev),
592
717
  },
593
718
  "/api/models/pull": {
594
- POST: (req: Request) => {
719
+ POST: async (req: Request) => {
595
720
  if (!isRequestAllowed(req, port)) {
596
721
  return withSecurityHeaders(forbiddenResponse(), isDev);
597
722
  }
598
- return withSecurityHeaders(handleModelPull(ctxHolder), isDev);
723
+ return withSecurityHeaders(
724
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
725
+ handleModelPull(ctxHolder)
726
+ ),
727
+ isDev
728
+ );
599
729
  },
600
730
  },
601
731
  "/api/jobs/active": {
602
- GET: () => withSecurityHeaders(handleActiveJob(), isDev),
732
+ GET: () =>
733
+ withSecurityHeaders(handleActiveJob(ctxHolder.jobManager), isDev),
603
734
  },
604
735
  "/api/jobs/:id": {
605
736
  GET: (req: Request) => {
606
737
  const url = new URL(req.url);
607
738
  const id = decodeURIComponent(url.pathname.split("/").pop() || "");
608
- return withSecurityHeaders(handleJob(id), isDev);
739
+ return withSecurityHeaders(
740
+ handleJob(id, ctxHolder.jobManager),
741
+ isDev
742
+ );
609
743
  },
610
744
  },
611
745
  "/api/embed": {
@@ -677,7 +811,9 @@ export async function startServer(
677
811
  const parts = url.pathname.split("/");
678
812
  const id = decodeURIComponent(parts[3] || "");
679
813
  return withSecurityHeaders(
680
- await handleDocLinks(store, id, url),
814
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
815
+ handleDocLinks(store, id, url)
816
+ ),
681
817
  isDev
682
818
  );
683
819
  },
@@ -688,7 +824,9 @@ export async function startServer(
688
824
  const parts = url.pathname.split("/");
689
825
  const id = decodeURIComponent(parts[3] || "");
690
826
  return withSecurityHeaders(
691
- await handleDocSections(store, id, req),
827
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
828
+ handleDocSections(store, id, req)
829
+ ),
692
830
  isDev
693
831
  );
694
832
  },
@@ -700,7 +838,9 @@ export async function startServer(
700
838
  const parts = url.pathname.split("/");
701
839
  const id = decodeURIComponent(parts[3] || "");
702
840
  return withSecurityHeaders(
703
- await handleDocBacklinks(store, id),
841
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
842
+ handleDocBacklinks(store, id)
843
+ ),
704
844
  isDev
705
845
  );
706
846
  },
@@ -712,7 +852,9 @@ export async function startServer(
712
852
  const parts = url.pathname.split("/");
713
853
  const id = decodeURIComponent(parts[3] || "");
714
854
  return withSecurityHeaders(
715
- await handleDocSimilar(ctxHolder.current, id, url),
855
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
856
+ handleDocSimilar(ctxHolder.current, id, url)
857
+ ),
716
858
  isDev
717
859
  );
718
860
  },
@@ -720,7 +862,12 @@ export async function startServer(
720
862
  "/api/graph": {
721
863
  GET: async (req: Request) => {
722
864
  const url = new URL(req.url);
723
- return withSecurityHeaders(await handleGraph(store, url), isDev);
865
+ return withSecurityHeaders(
866
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
867
+ handleGraph(store, url)
868
+ ),
869
+ isDev
870
+ );
724
871
  },
725
872
  },
726
873
  "/api/graph/query": {
@@ -729,7 +876,9 @@ export async function startServer(
729
876
  return withSecurityHeaders(forbiddenResponse(), isDev);
730
877
  }
731
878
  return withSecurityHeaders(
732
- await handleGraphQuery(store, ctxHolder.config, req),
879
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
880
+ handleGraphQuery(store, ctxHolder.config, req)
881
+ ),
733
882
  isDev
734
883
  );
735
884
  },
@@ -738,14 +887,18 @@ export async function startServer(
738
887
  });
739
888
  } catch (e) {
740
889
  removeShutdownHandlers();
741
- await runtime.dispose();
890
+ await Promise.allSettled([gateway.close()]);
891
+ await Promise.allSettled([runtime.dispose()]);
742
892
  return {
743
893
  success: false,
744
894
  error: e instanceof Error ? e.message : String(e),
745
895
  };
746
896
  }
897
+ (runtime as Partial<ResidentRuntime>).setListenerPort?.(server.port ?? port);
747
898
 
748
- console.log(`GNO server running at http://localhost:${server.port}`);
899
+ console.log(
900
+ `GNO server running at http://${gatewayConfig.host}:${server.port}`
901
+ );
749
902
  console.log("Press Ctrl+C to stop");
750
903
 
751
904
  // Block until shutdown signal
@@ -763,7 +916,8 @@ export async function startServer(
763
916
  try {
764
917
  await server.stop(true);
765
918
  } finally {
766
- await runtime.dispose();
919
+ await Promise.allSettled([gateway.close()]);
920
+ await Promise.allSettled([runtime.dispose()]);
767
921
  }
768
922
  return { success: true };
769
923
  }
@@ -59,6 +59,56 @@ export interface HealthCenterState {
59
59
  checks: HealthCheck[];
60
60
  }
61
61
 
62
+ export type RuntimeMode = "serve" | "daemon" | "stdio" | "direct-cli";
63
+
64
+ export interface ResidentStatus {
65
+ schemaVersion: "1.0";
66
+ mode: RuntimeMode;
67
+ resident: boolean;
68
+ uptimeSeconds: number | null;
69
+ listenerPort: number | null;
70
+ admission: {
71
+ state: "accepting" | "draining" | "closed";
72
+ activeRequests: number;
73
+ };
74
+ shutdown: {
75
+ state: "none" | "graceful" | "deadline";
76
+ };
77
+ transport: {
78
+ activeRequests: number;
79
+ activeSessions: number;
80
+ queuedRequests: number;
81
+ maxConcurrentRequests: number;
82
+ maxQueuedRequests: number;
83
+ maxSessions: number;
84
+ };
85
+ readers: {
86
+ active: number;
87
+ queued: number;
88
+ limit: number;
89
+ maxQueued: number;
90
+ };
91
+ models: {
92
+ activeLeases: number;
93
+ leaseAcquisitions: number;
94
+ leaseReleases: number;
95
+ loadedModels: number;
96
+ loadAttempts: number;
97
+ loadSuccesses: number;
98
+ loadFailures: number;
99
+ inflightLoads: number;
100
+ };
101
+ jobs: {
102
+ active: number;
103
+ recent: number;
104
+ failed: number;
105
+ };
106
+ generations: {
107
+ content: number;
108
+ index: number;
109
+ };
110
+ }
111
+
62
112
  export interface BackgroundServiceState {
63
113
  watcher: {
64
114
  expectedCollections: string[];
@@ -125,6 +175,7 @@ export interface BootstrapState {
125
175
  }
126
176
 
127
177
  export interface AppStatusResponse {
178
+ resident: ResidentStatus;
128
179
  indexName: string;
129
180
  configPath: string;
130
181
  dbPath: string;
@@ -26,6 +26,7 @@ import {
26
26
  } from "./activation-health";
27
27
  import { getConnectorVerificationTargets } from "./connectors";
28
28
  import { downloadState, type ServerContext } from "./context";
29
+ import { createStandaloneResidentStatus } from "./resident-status";
29
30
 
30
31
  const GIGABYTE = 1024 * 1024 * 1024;
31
32
  const DISK_WARN_BYTES = 4 * GIGABYTE;
@@ -71,6 +72,7 @@ export interface StatusBuildDeps {
71
72
  listSuggestedCollections?: () => Promise<SuggestedCollection[]>;
72
73
  buildActivation?: typeof buildActivationStatus;
73
74
  listConnectorTargets?: typeof getConnectorVerificationTargets;
75
+ getResidentStatus?: () => AppStatusResponse["resident"];
74
76
  }
75
77
 
76
78
  function formatBytes(bytes: number): string {
@@ -714,6 +716,9 @@ export async function buildAppStatus(
714
716
  : "GNO works, but a few issues still need attention before it feels reliable.";
715
717
 
716
718
  return {
719
+ resident:
720
+ deps.getResidentStatus?.() ??
721
+ createStandaloneResidentStatus("direct-cli"),
717
722
  indexName: status.indexName,
718
723
  configPath: status.configPath,
719
724
  dbPath: status.dbPath,