@gmickel/gno 1.16.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 (86) hide show
  1. package/README.md +34 -18
  2. package/assets/skill/SKILL.md +25 -6
  3. package/assets/skill/mcp-reference.md +21 -0
  4. package/package.json +2 -2
  5. package/src/app/context-agent-projection.ts +303 -0
  6. package/src/app/context-format.ts +249 -0
  7. package/src/app/context-runtime-contract.ts +325 -0
  8. package/src/app/context-runtime-input.ts +362 -0
  9. package/src/app/context-runtime-types.ts +65 -0
  10. package/src/app/context-runtime.ts +170 -0
  11. package/src/app/context-surface.ts +145 -0
  12. package/src/cli/commands/context-build.ts +149 -0
  13. package/src/cli/commands/context-verify.ts +90 -0
  14. package/src/cli/commands/daemon.ts +69 -2
  15. package/src/cli/commands/models/pull.ts +13 -3
  16. package/src/cli/commands/status.ts +2 -0
  17. package/src/cli/detach.ts +37 -20
  18. package/src/cli/options.ts +4 -0
  19. package/src/cli/program.ts +252 -27
  20. package/src/config/index.ts +3 -0
  21. package/src/config/types.ts +37 -0
  22. package/src/core/context-budget.ts +461 -0
  23. package/src/core/context-capsule-index-schema.ts +15 -0
  24. package/src/core/context-capsule-retrieval-schema.ts +81 -0
  25. package/src/core/context-capsule-schema.ts +473 -0
  26. package/src/core/context-capsule-validation.ts +416 -0
  27. package/src/core/context-capsule-verification.ts +218 -0
  28. package/src/core/context-capsule.ts +439 -0
  29. package/src/core/context-compiler.ts +513 -0
  30. package/src/core/context-evidence-metadata.ts +33 -0
  31. package/src/core/context-evidence.ts +495 -0
  32. package/src/core/context-facets.ts +163 -0
  33. package/src/core/context-guidance.ts +69 -0
  34. package/src/core/context-scope.ts +32 -0
  35. package/src/core/context-verifier-canonical.ts +90 -0
  36. package/src/core/context-verifier-input.ts +66 -0
  37. package/src/core/context-verifier.ts +447 -0
  38. package/src/core/job-manager.ts +19 -0
  39. package/src/core/mutation-generations.ts +33 -0
  40. package/src/core/sections.ts +63 -0
  41. package/src/llm/cache.ts +13 -3
  42. package/src/llm/nodeLlamaCpp/adapter.ts +10 -1
  43. package/src/llm/nodeLlamaCpp/lifecycle.ts +71 -0
  44. package/src/mcp/context.ts +161 -0
  45. package/src/mcp/http-security.ts +477 -0
  46. package/src/mcp/http-session.ts +272 -0
  47. package/src/mcp/http-transport.ts +370 -0
  48. package/src/mcp/resources/index.ts +141 -134
  49. package/src/mcp/server.ts +28 -82
  50. package/src/mcp/tools/add-collection.ts +3 -1
  51. package/src/mcp/tools/capture.ts +3 -0
  52. package/src/mcp/tools/clear-collection-embeddings.ts +2 -0
  53. package/src/mcp/tools/context.ts +230 -0
  54. package/src/mcp/tools/embed.ts +62 -52
  55. package/src/mcp/tools/index-cmd.ts +88 -74
  56. package/src/mcp/tools/index.ts +49 -2
  57. package/src/mcp/tools/remove-collection.ts +2 -0
  58. package/src/mcp/tools/status.ts +11 -0
  59. package/src/mcp/tools/sync.ts +16 -14
  60. package/src/mcp/tools/workspace-write.ts +7 -3
  61. package/src/pipeline/chunk-lookup.ts +33 -0
  62. package/src/pipeline/hybrid.ts +79 -57
  63. package/src/pipeline/types.ts +14 -0
  64. package/src/sdk/client.ts +68 -6
  65. package/src/sdk/index.ts +21 -0
  66. package/src/sdk/types.ts +24 -0
  67. package/src/serve/background-runtime.ts +12 -211
  68. package/src/serve/context-capsule.ts +136 -0
  69. package/src/serve/context.ts +10 -1
  70. package/src/serve/embed-scheduler.ts +74 -43
  71. package/src/serve/index.ts +9 -0
  72. package/src/serve/jobs.ts +78 -80
  73. package/src/serve/public/components/HealthCenter.tsx +74 -1
  74. package/src/serve/public/globals.built.css +1 -1
  75. package/src/serve/public/pages/Dashboard.tsx +1 -0
  76. package/src/serve/resident-admission.ts +159 -0
  77. package/src/serve/resident-background-work.ts +39 -0
  78. package/src/serve/resident-request.ts +55 -0
  79. package/src/serve/resident-runtime.ts +490 -0
  80. package/src/serve/resident-status.ts +96 -0
  81. package/src/serve/routes/api.ts +265 -167
  82. package/src/serve/routes/mcp.ts +69 -0
  83. package/src/serve/server.ts +212 -35
  84. package/src/serve/status-model.ts +51 -0
  85. package/src/serve/status.ts +5 -0
  86. package/src/store/sqlite/adapter.ts +64 -29
@@ -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,12 +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";
18
+ import { handleContextBuild, handleContextVerify } from "./context-capsule";
12
19
  import { DocumentEventBus } from "./doc-events";
13
20
  // HTML import - Bun handles bundling TSX/CSS automatically via routes
14
21
  import homepage from "./public/index.html";
22
+ import { handleResidentRead } from "./resident-request";
15
23
  import {
16
24
  handleActiveJob,
17
25
  handleAsk,
@@ -48,6 +56,7 @@ import {
48
56
  handleQuery,
49
57
  handleQueryDiagnose,
50
58
  handleRefactorPlan,
59
+ handleResidentStatus,
51
60
  handleRenameDoc,
52
61
  handleRevealDoc,
53
62
  handleSearch,
@@ -66,9 +75,10 @@ import {
66
75
  handleDocLinks,
67
76
  handleDocSimilar,
68
77
  } from "./routes/links";
78
+ import { createMcpHttpGateway } from "./routes/mcp";
69
79
  import { forbiddenResponse, isRequestAllowed } from "./security";
70
80
 
71
- export interface ServeOptions {
81
+ export interface ServeOptions extends HttpGatewayOverrides {
72
82
  /** Port to listen on (default: 3000) */
73
83
  port?: number;
74
84
  /** Config path override */
@@ -84,8 +94,14 @@ export interface ServeResult {
84
94
 
85
95
  interface StartServerDependencies {
86
96
  startBackgroundRuntime?: typeof startBackgroundRuntime;
97
+ createMcpHttpGateway?: typeof createMcpHttpGateway;
87
98
  serve?: typeof Bun.serve;
88
99
  handleInstallConnector?: typeof handleInstallConnector;
100
+ handleDocs?: typeof handleDocs;
101
+ handleVerifyConnector?: typeof handleVerifyConnector;
102
+ handleImportPreview?: typeof handleImportPreview;
103
+ handlePublishExport?: typeof handlePublishExport;
104
+ handleRefactorPlan?: typeof handleRefactorPlan;
89
105
  waitForShutdown?: (signal: AbortSignal) => Promise<void>;
90
106
  }
91
107
 
@@ -151,6 +167,7 @@ export async function startServer(
151
167
  const runtimeResult = await (
152
168
  dependencies.startBackgroundRuntime ?? startBackgroundRuntime
153
169
  )({
170
+ mode: "serve",
154
171
  configPath: options.configPath,
155
172
  index: options.index,
156
173
  requireCollections: false,
@@ -162,6 +179,35 @@ export async function startServer(
162
179
  const runtime = runtimeResult.runtime;
163
180
  const store = runtime.store;
164
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
+ }
165
211
 
166
212
  // Shutdown controller for clean lifecycle
167
213
  const shutdownController = new AbortController();
@@ -184,13 +230,14 @@ export async function startServer(
184
230
  try {
185
231
  server = (dependencies.serve ?? Bun.serve)({
186
232
  port,
187
- hostname: "127.0.0.1", // Loopback only - no LAN exposure
233
+ hostname: gatewayConfig.host,
188
234
 
189
235
  // Enable development mode for HMR and console logging
190
236
  development: isDev,
191
237
 
192
238
  // Static routes - Bun handles HTML bundling and /_bun/* assets automatically
193
239
  routes: {
240
+ "/mcp": gateway.route,
194
241
  // SPA routes - all serve the same React app
195
242
  "/": homepage,
196
243
  "/search": homepage,
@@ -207,8 +254,25 @@ export async function startServer(
207
254
  GET: () => withSecurityHeaders(handleHealth(), isDev),
208
255
  },
209
256
  "/api/status": {
210
- GET: async () =>
211
- 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
+ ),
212
276
  },
213
277
  "/api/collections": {
214
278
  GET: async () =>
@@ -227,9 +291,11 @@ export async function startServer(
227
291
  },
228
292
  },
229
293
  "/api/connectors": {
230
- GET: async () =>
294
+ GET: async (req: Request) =>
231
295
  withSecurityHeaders(
232
- await handleConnectors(ctxHolder.config),
296
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
297
+ handleConnectors(ctxHolder.config)
298
+ ),
233
299
  isDev
234
300
  ),
235
301
  },
@@ -255,7 +321,13 @@ export async function startServer(
255
321
  return withSecurityHeaders(forbiddenResponse(), isDev);
256
322
  }
257
323
  return withSecurityHeaders(
258
- 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
+ ),
259
331
  isDev
260
332
  );
261
333
  },
@@ -266,7 +338,12 @@ export async function startServer(
266
338
  return withSecurityHeaders(forbiddenResponse(), isDev);
267
339
  }
268
340
  return withSecurityHeaders(
269
- await handleImportPreview(ctxHolder, req),
341
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
342
+ (dependencies.handleImportPreview ?? handleImportPreview)(
343
+ ctxHolder,
344
+ req
345
+ )
346
+ ),
270
347
  isDev
271
348
  );
272
349
  },
@@ -277,7 +354,13 @@ export async function startServer(
277
354
  return withSecurityHeaders(forbiddenResponse(), isDev);
278
355
  }
279
356
  return withSecurityHeaders(
280
- 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
+ ),
281
364
  isDev
282
365
  );
283
366
  },
@@ -307,7 +390,12 @@ export async function startServer(
307
390
  "/api/docs": {
308
391
  GET: async (req: Request) => {
309
392
  const url = new URL(req.url);
310
- 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
+ );
311
399
  },
312
400
  POST: async (req: Request) => {
313
401
  if (!isRequestAllowed(req, port)) {
@@ -323,7 +411,9 @@ export async function startServer(
323
411
  GET: async (req: Request) => {
324
412
  const url = new URL(req.url);
325
413
  return withSecurityHeaders(
326
- await handleDocsAutocomplete(store, url),
414
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
415
+ handleDocsAutocomplete(store, url)
416
+ ),
327
417
  isDev
328
418
  );
329
419
  },
@@ -344,8 +434,13 @@ export async function startServer(
344
434
  },
345
435
  },
346
436
  "/api/browse/tree": {
347
- GET: async () =>
348
- 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
+ ),
349
444
  },
350
445
  "/api/docs/:id/deactivate": {
351
446
  POST: async (req: Request) => {
@@ -357,7 +452,7 @@ export async function startServer(
357
452
  const parts = url.pathname.split("/");
358
453
  const id = decodeURIComponent(parts[3] || "");
359
454
  return withSecurityHeaders(
360
- await handleDeactivateDoc(store, id, req),
455
+ await handleDeactivateDoc(ctxHolder, store, id, req),
361
456
  isDev
362
457
  );
363
458
  },
@@ -413,7 +508,14 @@ export async function startServer(
413
508
  const parts = url.pathname.split("/");
414
509
  const id = decodeURIComponent(parts[3] || "");
415
510
  return withSecurityHeaders(
416
- 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
+ ),
417
519
  isDev
418
520
  );
419
521
  },
@@ -478,7 +580,9 @@ export async function startServer(
478
580
  GET: async (req: Request) => {
479
581
  const url = new URL(req.url);
480
582
  return withSecurityHeaders(
481
- await handleDoc(store, ctxHolder.config, url),
583
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
584
+ handleDoc(store, ctxHolder.config, url)
585
+ ),
482
586
  isDev
483
587
  );
484
588
  },
@@ -487,7 +591,9 @@ export async function startServer(
487
591
  GET: async (req: Request) => {
488
592
  const url = new URL(req.url);
489
593
  return withSecurityHeaders(
490
- await handleDocAsset(store, ctxHolder.config, url),
594
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
595
+ handleDocAsset(store, ctxHolder.config, url)
596
+ ),
491
597
  isDev
492
598
  );
493
599
  },
@@ -503,7 +609,12 @@ export async function startServer(
503
609
  "/api/tags": {
504
610
  GET: async (req: Request) => {
505
611
  const url = new URL(req.url);
506
- 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
+ );
507
618
  },
508
619
  },
509
620
  "/api/search": {
@@ -511,7 +622,12 @@ export async function startServer(
511
622
  if (!isRequestAllowed(req, port)) {
512
623
  return withSecurityHeaders(forbiddenResponse(), isDev);
513
624
  }
514
- 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
+ );
515
631
  },
516
632
  },
517
633
  "/api/query": {
@@ -520,7 +636,35 @@ export async function startServer(
520
636
  return withSecurityHeaders(forbiddenResponse(), isDev);
521
637
  }
522
638
  return withSecurityHeaders(
523
- await handleQuery(ctxHolder.current, req),
639
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
640
+ handleQuery(ctxHolder.current, req)
641
+ ),
642
+ isDev
643
+ );
644
+ },
645
+ },
646
+ "/api/context": {
647
+ POST: async (req: Request) => {
648
+ if (!isRequestAllowed(req, port)) {
649
+ return withSecurityHeaders(forbiddenResponse(), isDev);
650
+ }
651
+ return withSecurityHeaders(
652
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
653
+ handleContextBuild(ctxHolder.current, req)
654
+ ),
655
+ isDev
656
+ );
657
+ },
658
+ },
659
+ "/api/context/verify": {
660
+ POST: async (req: Request) => {
661
+ if (!isRequestAllowed(req, port)) {
662
+ return withSecurityHeaders(forbiddenResponse(), isDev);
663
+ }
664
+ return withSecurityHeaders(
665
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
666
+ handleContextVerify(ctxHolder.current, req)
667
+ ),
524
668
  isDev
525
669
  );
526
670
  },
@@ -531,7 +675,9 @@ export async function startServer(
531
675
  return withSecurityHeaders(forbiddenResponse(), isDev);
532
676
  }
533
677
  return withSecurityHeaders(
534
- await handleQueryDiagnose(ctxHolder.current, req),
678
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
679
+ handleQueryDiagnose(ctxHolder.current, req)
680
+ ),
535
681
  isDev
536
682
  );
537
683
  },
@@ -542,7 +688,9 @@ export async function startServer(
542
688
  return withSecurityHeaders(forbiddenResponse(), isDev);
543
689
  }
544
690
  return withSecurityHeaders(
545
- await handleAsk(ctxHolder.current, req),
691
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
692
+ handleAsk(ctxHolder.current, req)
693
+ ),
546
694
  isDev
547
695
  );
548
696
  },
@@ -568,21 +716,30 @@ export async function startServer(
568
716
  GET: () => withSecurityHeaders(handleModelStatus(), isDev),
569
717
  },
570
718
  "/api/models/pull": {
571
- POST: (req: Request) => {
719
+ POST: async (req: Request) => {
572
720
  if (!isRequestAllowed(req, port)) {
573
721
  return withSecurityHeaders(forbiddenResponse(), isDev);
574
722
  }
575
- return withSecurityHeaders(handleModelPull(ctxHolder), isDev);
723
+ return withSecurityHeaders(
724
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
725
+ handleModelPull(ctxHolder)
726
+ ),
727
+ isDev
728
+ );
576
729
  },
577
730
  },
578
731
  "/api/jobs/active": {
579
- GET: () => withSecurityHeaders(handleActiveJob(), isDev),
732
+ GET: () =>
733
+ withSecurityHeaders(handleActiveJob(ctxHolder.jobManager), isDev),
580
734
  },
581
735
  "/api/jobs/:id": {
582
736
  GET: (req: Request) => {
583
737
  const url = new URL(req.url);
584
738
  const id = decodeURIComponent(url.pathname.split("/").pop() || "");
585
- return withSecurityHeaders(handleJob(id), isDev);
739
+ return withSecurityHeaders(
740
+ handleJob(id, ctxHolder.jobManager),
741
+ isDev
742
+ );
586
743
  },
587
744
  },
588
745
  "/api/embed": {
@@ -654,7 +811,9 @@ export async function startServer(
654
811
  const parts = url.pathname.split("/");
655
812
  const id = decodeURIComponent(parts[3] || "");
656
813
  return withSecurityHeaders(
657
- await handleDocLinks(store, id, url),
814
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
815
+ handleDocLinks(store, id, url)
816
+ ),
658
817
  isDev
659
818
  );
660
819
  },
@@ -665,7 +824,9 @@ export async function startServer(
665
824
  const parts = url.pathname.split("/");
666
825
  const id = decodeURIComponent(parts[3] || "");
667
826
  return withSecurityHeaders(
668
- await handleDocSections(store, id, req),
827
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
828
+ handleDocSections(store, id, req)
829
+ ),
669
830
  isDev
670
831
  );
671
832
  },
@@ -677,7 +838,9 @@ export async function startServer(
677
838
  const parts = url.pathname.split("/");
678
839
  const id = decodeURIComponent(parts[3] || "");
679
840
  return withSecurityHeaders(
680
- await handleDocBacklinks(store, id),
841
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
842
+ handleDocBacklinks(store, id)
843
+ ),
681
844
  isDev
682
845
  );
683
846
  },
@@ -689,7 +852,9 @@ export async function startServer(
689
852
  const parts = url.pathname.split("/");
690
853
  const id = decodeURIComponent(parts[3] || "");
691
854
  return withSecurityHeaders(
692
- await handleDocSimilar(ctxHolder.current, id, url),
855
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
856
+ handleDocSimilar(ctxHolder.current, id, url)
857
+ ),
693
858
  isDev
694
859
  );
695
860
  },
@@ -697,7 +862,12 @@ export async function startServer(
697
862
  "/api/graph": {
698
863
  GET: async (req: Request) => {
699
864
  const url = new URL(req.url);
700
- 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
+ );
701
871
  },
702
872
  },
703
873
  "/api/graph/query": {
@@ -706,7 +876,9 @@ export async function startServer(
706
876
  return withSecurityHeaders(forbiddenResponse(), isDev);
707
877
  }
708
878
  return withSecurityHeaders(
709
- await handleGraphQuery(store, ctxHolder.config, req),
879
+ await handleResidentRead(runtime as ResidentRuntime, req, () =>
880
+ handleGraphQuery(store, ctxHolder.config, req)
881
+ ),
710
882
  isDev
711
883
  );
712
884
  },
@@ -715,14 +887,18 @@ export async function startServer(
715
887
  });
716
888
  } catch (e) {
717
889
  removeShutdownHandlers();
718
- await runtime.dispose();
890
+ await Promise.allSettled([gateway.close()]);
891
+ await Promise.allSettled([runtime.dispose()]);
719
892
  return {
720
893
  success: false,
721
894
  error: e instanceof Error ? e.message : String(e),
722
895
  };
723
896
  }
897
+ (runtime as Partial<ResidentRuntime>).setListenerPort?.(server.port ?? port);
724
898
 
725
- console.log(`GNO server running at http://localhost:${server.port}`);
899
+ console.log(
900
+ `GNO server running at http://${gatewayConfig.host}:${server.port}`
901
+ );
726
902
  console.log("Press Ctrl+C to stop");
727
903
 
728
904
  // Block until shutdown signal
@@ -740,7 +916,8 @@ export async function startServer(
740
916
  try {
741
917
  await server.stop(true);
742
918
  } finally {
743
- await runtime.dispose();
919
+ await Promise.allSettled([gateway.close()]);
920
+ await Promise.allSettled([runtime.dispose()]);
744
921
  }
745
922
  return { success: true };
746
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,