@gmickel/gno 2.4.0 → 2.6.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 (136) hide show
  1. package/README.md +69 -6
  2. package/assets/skill/README.md +5 -1
  3. package/assets/skill/SKILL.md +98 -1
  4. package/assets/skill/cli-reference.md +140 -0
  5. package/assets/skill/examples.md +45 -0
  6. package/assets/skill/mcp-reference.md +62 -0
  7. package/assets/skill/recipes/capture-and-file.md +6 -0
  8. package/assets/skill/recipes/memory-file-decision.md +6 -0
  9. package/assets/skill/recipes/memory-supersede-fact.md +5 -0
  10. package/assets/skill/recipes/session-evidence-lookup.md +98 -0
  11. package/assets/spa-production.json.gz +0 -0
  12. package/browser-extension/artifacts/{gno-browser-clipper-v2.4.0.zip → gno-browser-clipper-v2.6.0.zip} +0 -0
  13. package/browser-extension/artifacts/gno-browser-clipper-v2.6.0.zip.sha256 +1 -0
  14. package/browser-extension/dist/manifest.json +1 -1
  15. package/package.json +2 -1
  16. package/spec/cli.md +358 -24
  17. package/spec/compiled-context.md +68 -0
  18. package/spec/mcp.md +223 -2
  19. package/spec/output-schemas/capture-receipt.schema.json +3 -0
  20. package/spec/output-schemas/compiled-context-check.schema.json +44 -0
  21. package/spec/output-schemas/compiled-context-file.schema.json +165 -0
  22. package/spec/output-schemas/compiled-context-preview.schema.json +142 -0
  23. package/spec/output-schemas/mcp-capture-result.schema.json +3 -0
  24. package/spec/output-schemas/memory-remember.schema.json +8 -2
  25. package/spec/output-schemas/request-status.schema.json +113 -0
  26. package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
  27. package/spec/output-schemas/sessions-discovery.schema.json +38 -0
  28. package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
  29. package/spec/output-schemas/sessions-status.schema.json +432 -0
  30. package/src/app/compiled-context-files.ts +361 -0
  31. package/src/app/compiled-context.ts +240 -0
  32. package/src/app/context-surface.ts +7 -2
  33. package/src/cli/commands/ask.ts +14 -2
  34. package/src/cli/commands/audit.ts +4 -0
  35. package/src/cli/commands/capture.ts +55 -96
  36. package/src/cli/commands/context-compiled.ts +136 -0
  37. package/src/cli/commands/daemon.ts +41 -0
  38. package/src/cli/commands/ls.ts +3 -0
  39. package/src/cli/commands/memory.ts +12 -3
  40. package/src/cli/commands/request-status.ts +59 -0
  41. package/src/cli/commands/reset.ts +39 -5
  42. package/src/cli/commands/sessions.ts +713 -0
  43. package/src/cli/commands/shared.ts +14 -1
  44. package/src/cli/errors.ts +10 -3
  45. package/src/cli/program.ts +458 -1
  46. package/src/cli/session-binding.ts +49 -0
  47. package/src/config/types.ts +8 -0
  48. package/src/core/capture-publish.ts +239 -0
  49. package/src/core/capture-sync.ts +3 -0
  50. package/src/core/compiled-context.ts +254 -0
  51. package/src/core/context-budget.ts +2 -10
  52. package/src/core/file-lock.ts +22 -5
  53. package/src/core/folder-setup-planning.ts +2 -1
  54. package/src/core/memory-remember.ts +233 -122
  55. package/src/core/memory-types.ts +11 -0
  56. package/src/core/network-boundary-inventory.ts +16 -0
  57. package/src/core/request-receipts.ts +671 -0
  58. package/src/core/setup-receipt.ts +27 -20
  59. package/src/core/typed-metadata.ts +4 -0
  60. package/src/core/validation.ts +9 -2
  61. package/src/core/windows-private-path.ts +96 -0
  62. package/src/index.ts +11 -2
  63. package/src/ingestion/compiled-context.ts +15 -0
  64. package/src/ingestion/sync.ts +40 -8
  65. package/src/ingestion/walker.ts +5 -4
  66. package/src/llm/nodeLlamaCpp/simulator-install.ts +6 -2
  67. package/src/mcp/context.ts +8 -0
  68. package/src/mcp/http-egress.ts +15 -3
  69. package/src/mcp/http-transport.ts +2 -0
  70. package/src/mcp/retrieval-warnings.ts +24 -0
  71. package/src/mcp/tools/ask.ts +14 -1
  72. package/src/mcp/tools/capture.ts +87 -83
  73. package/src/mcp/tools/context.ts +46 -2
  74. package/src/mcp/tools/index.ts +119 -7
  75. package/src/mcp/tools/memory-remember.ts +7 -0
  76. package/src/mcp/tools/memory-shared.ts +7 -1
  77. package/src/mcp/tools/query.ts +3 -1
  78. package/src/mcp/tools/request-status.ts +73 -0
  79. package/src/mcp/tools/search.ts +3 -1
  80. package/src/mcp/tools/sessions.ts +208 -0
  81. package/src/mcp/tools/vsearch.ts +3 -1
  82. package/src/sdk/client.ts +252 -89
  83. package/src/sdk/index.ts +13 -0
  84. package/src/sdk/types.ts +82 -3
  85. package/src/serve/capture-service.ts +98 -32
  86. package/src/serve/compiled-context.ts +84 -0
  87. package/src/serve/config-sync.ts +3 -2
  88. package/src/serve/public/app.tsx +15 -1
  89. package/src/serve/public/components/CaptureModal.tsx +26 -8
  90. package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
  91. package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
  92. package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
  93. package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
  94. package/src/serve/public/components/sessions/api.ts +40 -0
  95. package/src/serve/public/components/sessions/snippet.tsx +53 -0
  96. package/src/serve/public/globals.built.css +1 -1
  97. package/src/serve/public/hooks/use-api.ts +10 -2
  98. package/src/serve/public/lib/request-intent.ts +69 -0
  99. package/src/serve/public/lib/workspace-actions.ts +12 -1
  100. package/src/serve/public/lib/workspace-tabs.ts +4 -0
  101. package/src/serve/public/pages/CompiledContext.tsx +364 -0
  102. package/src/serve/public/pages/Dashboard.tsx +17 -0
  103. package/src/serve/public/pages/DocView.tsx +15 -1
  104. package/src/serve/public/pages/DocumentEditor.tsx +139 -96
  105. package/src/serve/public/pages/Sessions.tsx +350 -0
  106. package/src/serve/resident-runtime.ts +43 -3
  107. package/src/serve/routes/api.ts +476 -147
  108. package/src/serve/routes/sessions.ts +766 -0
  109. package/src/serve/security.ts +9 -0
  110. package/src/serve/server.ts +248 -3
  111. package/src/serve/session-automation.ts +146 -0
  112. package/src/serve/spa-production-build.ts +6 -5
  113. package/src/sessions/archive.ts +348 -0
  114. package/src/sessions/automation-state.ts +444 -0
  115. package/src/sessions/automation-status.ts +239 -0
  116. package/src/sessions/automation.ts +1169 -0
  117. package/src/sessions/binding.ts +105 -0
  118. package/src/sessions/claude-hook.ts +240 -0
  119. package/src/sessions/config.ts +176 -0
  120. package/src/sessions/format.ts +191 -0
  121. package/src/sessions/import-child-env.ts +8 -0
  122. package/src/sessions/import-child.ts +152 -0
  123. package/src/sessions/parsers/claude-code.ts +259 -0
  124. package/src/sessions/parsers/codex.ts +303 -0
  125. package/src/sessions/parsers/hermes.ts +248 -0
  126. package/src/sessions/parsers/openclaw.ts +496 -0
  127. package/src/sessions/parsers/shared.ts +184 -0
  128. package/src/sessions/sanitize.ts +222 -0
  129. package/src/sessions/service.ts +1533 -0
  130. package/src/sessions/setup.ts +477 -0
  131. package/src/sessions/sources.ts +518 -0
  132. package/src/sessions/state.ts +118 -0
  133. package/src/sessions/types.ts +457 -0
  134. package/src/store/sqlite/adapter.ts +68 -16
  135. package/src/store/sqlite/scoped-index.ts +9 -0
  136. package/browser-extension/artifacts/gno-browser-clipper-v2.4.0.zip.sha256 +0 -1
@@ -12,6 +12,7 @@ import { dirname, join } from "node:path";
12
12
  import type { ActivationVerificationReceipt } from "../store/types";
13
13
 
14
14
  import { canonicalizeIndexName } from "../app/index-name";
15
+ import { withWriteLock } from "./file-lock";
15
16
 
16
17
  export const SETUP_RECEIPT_SCHEMA_VERSION = "1.0" as const;
17
18
 
@@ -278,27 +279,33 @@ export async function persistSetupReceipt(
278
279
  ): Promise<void> {
279
280
  const receiptDir = dirname(receipt.paths.receipt);
280
281
  await mkdir(receiptDir, { recursive: true, mode: 0o700 });
281
- await chmod(receiptDir, 0o700);
282
+ // Windows cannot reliably replace the same destination concurrently.
283
+ // Reuse the process-shared writer lock; each completed receipt stays atomic.
284
+ await withWriteLock(`${receipt.paths.receipt}.lock`, async () => {
285
+ await chmod(receiptDir, 0o700);
282
286
 
283
- const tempPath = `${receipt.paths.receipt}.tmp.${crypto.randomUUID()}`;
284
- let tempFile: Awaited<ReturnType<typeof open>> | null = null;
285
- try {
286
- tempFile = await open(tempPath, "wx", 0o600);
287
- await tempFile.writeFile(serializeSetupReceipt(receipt), "utf8");
288
- await tempFile.sync();
289
- await tempFile.close();
290
- tempFile = null;
291
- await rename(tempPath, receipt.paths.receipt);
292
- await chmod(receipt.paths.receipt, 0o600);
293
- } catch (error) {
294
- await tempFile?.close().catch(() => {
295
- /* best-effort temporary receipt handle cleanup */
296
- });
297
- await unlink(tempPath).catch(() => {
298
- /* best-effort temporary receipt cleanup */
299
- });
300
- throw error;
301
- }
287
+ const tempPath = `${receipt.paths.receipt}.tmp.${crypto.randomUUID()}`;
288
+ let tempFile: Awaited<ReturnType<typeof open>> | null = null;
289
+ try {
290
+ tempFile = await open(tempPath, "wx", 0o600);
291
+ await tempFile.writeFile(serializeSetupReceipt(receipt), "utf8");
292
+ // Set permissions before publication. Opening the shared destination for
293
+ // chmod can prevent a concurrent atomic replacement on Windows.
294
+ await tempFile.chmod(0o600);
295
+ await tempFile.sync();
296
+ await tempFile.close();
297
+ tempFile = null;
298
+ await rename(tempPath, receipt.paths.receipt);
299
+ } catch (error) {
300
+ await tempFile?.close().catch(() => {
301
+ /* best-effort temporary receipt handle cleanup */
302
+ });
303
+ await unlink(tempPath).catch(() => {
304
+ /* best-effort temporary receipt cleanup */
305
+ });
306
+ throw error;
307
+ }
308
+ });
302
309
  }
303
310
 
304
311
  export async function loadSetupReceipt(
@@ -12,6 +12,10 @@ export type MetadataPredicate =
12
12
  | { op: "in" | "nin" | "all"; key: string; values: MetadataScalar[] }
13
13
  | { op: "exists"; key: string; value: boolean };
14
14
 
15
+ export const METADATA_COVERAGE_GUIDANCE =
16
+ "Incomplete coverage is not proof of absence or of matching documents. Warnings describe the query scope, not a specific target. Keep requested filters unchanged. If an expected target is known, diagnose it with the same filter; otherwise report the coverage limit without inventing a target or broadening the search.";
17
+ export const METADATA_FILTER_DESCRIPTION = `Typed custom metadata predicate; intersects existing scope. ${METADATA_COVERAGE_GUIDANCE}`;
18
+
15
19
  export const TYPED_METADATA_INGEST_VERSION = 7;
16
20
  export const METADATA_LIMITS = {
17
21
  keys: 64,
@@ -9,7 +9,14 @@ import { realpath } from "node:fs/promises";
9
9
  // node:os for homedir (no Bun os utils)
10
10
  import { homedir } from "node:os";
11
11
  // node:path for path utils (no Bun path utils)
12
- import { isAbsolute, join, posix as pathPosix, relative, sep } from "node:path";
12
+ import {
13
+ isAbsolute,
14
+ join,
15
+ parse,
16
+ posix as pathPosix,
17
+ relative,
18
+ sep,
19
+ } from "node:path";
13
20
 
14
21
  import { toAbsolutePath } from "../config/paths";
15
22
 
@@ -87,7 +94,7 @@ export async function validateCollectionRoot(
87
94
  DANGEROUS_ROOT_PATTERNS.map((p) => resolveRealPathSafe(p))
88
95
  );
89
96
 
90
- if (dangerousRoots.includes(realPath)) {
97
+ if (realPath === parse(realPath).root || dangerousRoots.includes(realPath)) {
91
98
  throw new Error(`Cannot add ${inputPath}: resolves to dangerous root`);
92
99
  }
93
100
 
@@ -0,0 +1,96 @@
1
+ /** Windows owner-only evidence storage. Paths are data, never interpolated script.
2
+ * Fresh Windows objects may use the token default Owner instead of its User.
3
+ * Only those exact owner SIDs are accepted; allowed DACL entries remain User-only. */
4
+ // Use framework APIs directly: module auto-discovery depends on profile paths
5
+ // deliberately absent from isolated native workers.
6
+ const ACL = `
7
+ $ErrorActionPreference='Stop'
8
+ $p=$env:GNO_PRIVATE_PATH
9
+ $attributes=[IO.File]::GetAttributes($p)
10
+ $isDirectory=($attributes -band [IO.FileAttributes]::Directory) -ne 0
11
+ if (($attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'Reparse private path' }
12
+ $identity=[Security.Principal.WindowsIdentity]::GetCurrent()
13
+ $sid=$identity.User
14
+ $defaultOwner=$identity.Owner
15
+ $acl=if ($isDirectory) { [IO.Directory]::GetAccessControl($p) } else { [IO.File]::GetAccessControl($p) }
16
+ $owner=$acl.GetOwner([Security.Principal.SecurityIdentifier])
17
+ if ($owner.Value -ne $sid.Value -and $owner.Value -ne $defaultOwner.Value) { throw 'Foreign private owner' }
18
+ if ($env:GNO_PRIVATE_CREATE -eq '1') {
19
+ if (-not $isDirectory) { throw 'Private directory required' }
20
+ $acl=[Security.AccessControl.DirectorySecurity]::new()
21
+ $acl.SetOwner($sid)
22
+ $acl.SetAccessRuleProtection($true,$false)
23
+ $rule=[Security.AccessControl.FileSystemAccessRule]::new($sid,'FullControl','ContainerInherit,ObjectInherit','None','Allow')
24
+ $acl.AddAccessRule($rule)
25
+ [IO.Directory]::SetAccessControl($p,$acl)
26
+ if ([IO.Directory]::GetFileSystemEntries($p).Length -ne 0) { throw 'Private directory must be empty' }
27
+ }
28
+ $acl=if ($isDirectory) { [IO.Directory]::GetAccessControl($p) } else { [IO.File]::GetAccessControl($p) }
29
+ $owner=$acl.GetOwner([Security.Principal.SecurityIdentifier])
30
+ if ($owner.Value -ne $sid.Value -and $owner.Value -ne $defaultOwner.Value) { throw 'Foreign private owner' }
31
+ $rules=$acl.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier])
32
+ $allowed=$false
33
+ foreach ($rule in $rules) {
34
+ if ($rule.AccessControlType -eq 'Allow') {
35
+ if ($rule.IdentityReference.Value -ne $sid.Value) { throw 'Private ACL permits another principal' }
36
+ if (($rule.FileSystemRights -band [Security.AccessControl.FileSystemRights]::FullControl) -eq [Security.AccessControl.FileSystemRights]::FullControl) { $allowed=$true }
37
+ }
38
+ }
39
+ if (-not $allowed) { throw 'Private owner access unavailable' }
40
+ `;
41
+
42
+ export async function windowsPrivatePath(
43
+ path: string,
44
+ create = false
45
+ ): Promise<void> {
46
+ if (process.platform !== "win32")
47
+ throw new Error("Windows ACL operation requires Windows");
48
+ const started = performance.now();
49
+ const child = Bun.spawn(
50
+ [
51
+ "powershell.exe",
52
+ "-NoLogo",
53
+ "-NoProfile",
54
+ "-NonInteractive",
55
+ "-EncodedCommand",
56
+ Buffer.from(ACL, "utf16le").toString("base64"),
57
+ ],
58
+ {
59
+ env: {
60
+ ...process.env,
61
+ GNO_PRIVATE_PATH: path,
62
+ GNO_PRIVATE_CREATE: create ? "1" : "0",
63
+ },
64
+ stdin: "ignore",
65
+ // ACL checks return no data; do not allocate an unused stdout pipe.
66
+ stdout: "ignore",
67
+ stderr: "pipe",
68
+ timeout: 10000,
69
+ }
70
+ );
71
+ const reader = child.stderr.getReader();
72
+ try {
73
+ const chunks: Uint8Array[] = [];
74
+ let size = 0;
75
+ while (true) {
76
+ const { done, value } = await reader.read();
77
+ if (done) break;
78
+ size += value.byteLength;
79
+ if (size > 65536)
80
+ throw new Error("Private path ACL stderr exceeded 64 KiB");
81
+ chunks.push(value);
82
+ }
83
+ const exitCode = await child.exited;
84
+ if (exitCode !== 0) {
85
+ const stderr = new TextDecoder().decode(Buffer.concat(chunks)).trim();
86
+ throw new Error(
87
+ `Private path ACL unavailable (exit=${String(exitCode)}, signal=${String(child.signalCode)}, elapsedMs=${Math.round(performance.now() - started)}): ${stderr || "no stderr"}`
88
+ );
89
+ }
90
+ } finally {
91
+ reader.releaseLock();
92
+ if (child.exitCode === null && child.signalCode === null)
93
+ child.kill("SIGKILL");
94
+ await child.exited;
95
+ }
96
+ }
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { runCli } from "./cli/run";
10
10
  import { resetModelManager } from "./llm/nodeLlamaCpp/lifecycle";
11
+ import { IMPORT_CHILD_ENV } from "./sessions/import-child-env";
11
12
 
12
13
  /**
13
14
  * Cleanup models and exit.
@@ -37,8 +38,16 @@ process.on("SIGINT", () => {
37
38
  });
38
39
  });
39
40
 
40
- // Run CLI and exit
41
- runCli(process.argv)
41
+ // A compiled executable re-run as the session import child (see
42
+ // src/sessions/import-child.ts) serves that one request instead of the CLI.
43
+ if (process.env[IMPORT_CHILD_ENV] === "1") {
44
+ const { runImportChild } = await import("./sessions/import-child");
45
+ await runImportChild();
46
+ await cleanupAndExit(0);
47
+ }
48
+
49
+ // Await module completion so pending piped stdin keeps Windows Bun alive.
50
+ await runCli(process.argv)
42
51
  .then((code) => cleanupAndExit(interruptExitCode || code))
43
52
  .catch((err) => {
44
53
  process.stderr.write(
@@ -0,0 +1,15 @@
1
+ /** Generated context is derived evidence and must not feed recursive retrieval. */
2
+ const SIDECAR_MARKER = /"artifactKind"\s*:\s*"gno_compiled_context_sidecar"/;
3
+ export function isCompiledContextPath(path: string): boolean {
4
+ return path
5
+ .toLowerCase()
6
+ .split(/[\\/]/)
7
+ .some((part) => part.includes(".gno-context."));
8
+ }
9
+ export function isCompiledContextContent(prefix: Uint8Array): boolean {
10
+ const text = new TextDecoder().decode(prefix).trimStart();
11
+ return (
12
+ text.startsWith("<!-- gno:compiled-context ") ||
13
+ (text.startsWith("{") && SIDECAR_MARKER.test(text))
14
+ );
15
+ }
@@ -1,12 +1,11 @@
1
+ // node:fs/promises for realpath/stat (no Bun equivalent for canonical paths or file stats)
2
+ import { realpath, stat } from "node:fs/promises";
1
3
  /**
2
4
  * Sync service - orchestrates file ingestion.
3
5
  * Walks collections, converts files, chunks content, updates store.
4
6
  *
5
7
  * @module src/ingestion/sync
6
8
  */
7
-
8
- // node:fs/promises for realpath/stat (no Bun equivalent for canonical paths or file stats)
9
- import { realpath, stat } from "node:fs/promises";
10
9
  // node:path for join (no Bun path utils)
11
10
  import { isAbsolute, join, relative, sep } from "node:path";
12
11
 
@@ -61,6 +60,10 @@ import { normalizeTag, validateTag } from "../core/tags";
61
60
  import { TYPED_METADATA_INGEST_VERSION } from "../core/typed-metadata";
62
61
  import { defaultChunker } from "./chunker";
63
62
  import { persistChunkLayout, prepareChunking } from "./chunking";
63
+ import {
64
+ isCompiledContextPath,
65
+ isCompiledContextContent,
66
+ } from "./compiled-context";
64
67
  import {
65
68
  extractHashtags,
66
69
  parseFrontmatter,
@@ -642,6 +645,14 @@ export class SyncService {
642
645
  store: StorePort,
643
646
  options: SyncOptions
644
647
  ): Promise<FileSyncResult> {
648
+ const generatedResult: FileSyncResult = {
649
+ relPath: entry.relPath,
650
+ status: "skipped",
651
+ errorCode: "GENERATED_CONTEXT",
652
+ errorMessage:
653
+ "Compiled context is derived material; index original sources instead",
654
+ };
655
+ if (isCompiledContextPath(entry.relPath)) return generatedResult;
645
656
  const limits = {
646
657
  maxBytes: options.limits?.maxBytes ?? DEFAULT_LIMITS.maxBytes,
647
658
  timeoutMs: options.limits?.timeoutMs ?? DEFAULT_LIMITS.timeoutMs,
@@ -733,6 +744,13 @@ export class SyncService {
733
744
  guardedBytes = sourceRead.bytes;
734
745
  }
735
746
 
747
+ const generatedPrefix =
748
+ guardedBytes?.subarray(0, 4096) ??
749
+ new Uint8Array(
750
+ await Bun.file(entry.absPath).slice(0, 4096).arrayBuffer()
751
+ );
752
+ if (isCompiledContextContent(generatedPrefix)) return generatedResult;
753
+
736
754
  if (recordAdapter) {
737
755
  return await processRecordContainer({
738
756
  adapter: recordAdapter,
@@ -754,11 +772,7 @@ export class SyncService {
754
772
  });
755
773
  }
756
774
 
757
- const sniffBytes = guardedBytes
758
- ? guardedBytes.subarray(0, Math.min(512, guardedBytes.byteLength))
759
- : new Uint8Array(
760
- await Bun.file(entry.absPath).slice(0, 512).arrayBuffer()
761
- );
775
+ const sniffBytes = generatedPrefix.subarray(0, 512);
762
776
  const mime = this.mimeDetector.detect(entry.relPath, sniffBytes);
763
777
 
764
778
  const priorRecordDocuments = mustOk(
@@ -1447,6 +1461,20 @@ export class SyncService {
1447
1461
  store,
1448
1462
  syncOptions
1449
1463
  );
1464
+ if (result.errorCode === "GENERATED_CONTEXT") {
1465
+ const inactive = await this.inactivateOneAbsentSource(
1466
+ collection,
1467
+ store,
1468
+ relPath,
1469
+ projectionSourceIds,
1470
+ { existingDoc, recordDocuments }
1471
+ );
1472
+ markedInactive += inactive.markedInactive;
1473
+ results.push(
1474
+ inactive.result.status === "error" ? inactive.result : result
1475
+ );
1476
+ continue;
1477
+ }
1450
1478
  results.push(result);
1451
1479
  if (result.status === "error" || result.status === "skipped") {
1452
1480
  continue;
@@ -1872,6 +1900,8 @@ export class SyncService {
1872
1900
  store,
1873
1901
  syncOptions
1874
1902
  );
1903
+ if (result.errorCode === "GENERATED_CONTEXT")
1904
+ seenPaths.delete(entry.relPath);
1875
1905
  fileResults.push(result);
1876
1906
  switch (result.status) {
1877
1907
  case "added":
@@ -1938,6 +1968,8 @@ export class SyncService {
1938
1968
  store,
1939
1969
  syncOptions
1940
1970
  );
1971
+ if (result.errorCode === "GENERATED_CONTEXT")
1972
+ seenPaths.delete(entry.relPath);
1941
1973
  fileResults.push(result);
1942
1974
  results.push(result);
1943
1975
  } finally {
@@ -1,3 +1,5 @@
1
+ // node:fs - Bun has no synchronous Dirent enumeration for the guarded local walk.
2
+ import { readdirSync } from "node:fs";
1
3
  /**
2
4
  * File walker implementation.
3
5
  * Walks collection directories using Bun.Glob (`any`) or hierarchical
@@ -5,9 +7,6 @@
5
7
  *
6
8
  * @module src/ingestion/walker
7
9
  */
8
-
9
- // node:fs - Bun has no synchronous Dirent enumeration for the guarded local walk.
10
- import { readdirSync } from "node:fs";
11
10
  // node:fs/promises - Bun has no realpath equivalent for symlink-safe containment.
12
11
  import { lstat, realpath } from "node:fs/promises";
13
12
  // node:path - Bun has no path manipulation module
@@ -28,6 +27,7 @@ import {
28
27
  matchesCollectionExclusion,
29
28
  matchesCollectionSubtreeExclusion,
30
29
  } from "../core/path-rules";
30
+ import { isCompiledContextPath } from "./compiled-context";
31
31
  import { isRecordVirtualPath } from "./record-path";
32
32
  import {
33
33
  createDirectoryAvailability,
@@ -204,7 +204,8 @@ export function matchesWalkPath(
204
204
  if (
205
205
  isAbsolute(normalizedPath) ||
206
206
  DANGEROUS_PATTERN_REGEX.test(normalizedPath) ||
207
- isRecordVirtualPath(normalizedPath)
207
+ isRecordVirtualPath(normalizedPath) ||
208
+ isCompiledContextPath(normalizedPath)
208
209
  ) {
209
210
  return false;
210
211
  }
@@ -1,4 +1,7 @@
1
1
  /** Install the pinned simulator guard in memory, including npm/nested installs. */
2
+ // node:url — filesystem conversion of file URLs has no Bun equivalent.
3
+ import { fileURLToPath } from "node:url";
4
+
2
5
  import type {
3
6
  SimulatorBackend,
4
7
  SimulatorDependencies,
@@ -52,14 +55,15 @@ export async function verifySimulatorPackage(
52
55
  export async function loadSimulatorDependencies(
53
56
  entry: string
54
57
  ): Promise<SimulatorDependencies> {
58
+ const parent = fileURLToPath(entry);
55
59
  const [guards, cache, locks, binding, tensors, byteModule] =
56
60
  await Promise.all([
57
61
  import(new URL("./utils/DisposeGuard.js", entry).href),
58
62
  import(new URL("./utils/LruCache.js", entry).href),
59
- import(import.meta.resolve("lifecycle-utils", entry)),
63
+ import(import.meta.resolve("lifecycle-utils", parent)),
60
64
  import(new URL("./bindings/types.js", entry).href),
61
65
  import(new URL("./gguf/types/GgufTensorInfoTypes.js", entry).href),
62
- import(import.meta.resolve("bytes", entry)),
66
+ import(import.meta.resolve("bytes", parent)),
63
67
  ]);
64
68
  return {
65
69
  DisposeGuard: guards.DisposeGuard,
@@ -66,11 +66,14 @@ export interface ToolContextSnapshot {
66
66
  * session id, so this is what keeps two modern callers apart.
67
67
  */
68
68
  requestIdentity?: string;
69
+ /** Stable request-receipt namespace of the authorized HTTP identity. */
70
+ requestNamespace?: string;
69
71
  }
70
72
 
71
73
  export interface RequestScope {
72
74
  egress: NonNullable<ToolContextSnapshot["egress"]>;
73
75
  requestIdentity?: string;
76
+ requestNamespace?: string;
74
77
  }
75
78
 
76
79
  export interface ToolContext {
@@ -107,6 +110,8 @@ export interface ToolContext {
107
110
  getEgressContext?: () => ToolContextSnapshot["egress"];
108
111
  /** Per-caller identity of the current request; absent on stdio. */
109
112
  getRequestIdentity?: () => string | undefined;
113
+ /** Request-receipt namespace of the current HTTP caller; absent on stdio. */
114
+ getRequestNamespace?: () => string | undefined;
110
115
  runWithEgressContext?<T>(
111
116
  egress: NonNullable<ToolContextSnapshot["egress"]>,
112
117
  operation: () => Promise<T>,
@@ -216,6 +221,7 @@ export function createToolContext(
216
221
  requestSnapshot.getStore()?.egress?.authorizationEpoch?.value,
217
222
  getEgressContext: () => requestSnapshot.getStore()?.egress,
218
223
  getRequestIdentity: () => requestSnapshot.getStore()?.requestIdentity,
224
+ getRequestNamespace: () => requestSnapshot.getStore()?.requestNamespace,
219
225
  runWithEgressContext<T>(
220
226
  egress: NonNullable<ToolContextSnapshot["egress"]>,
221
227
  operation: () => Promise<T>,
@@ -228,6 +234,7 @@ export function createToolContext(
228
234
  collections: config.collections,
229
235
  egress,
230
236
  requestIdentity: scope?.requestIdentity,
237
+ requestNamespace: scope?.requestNamespace,
231
238
  },
232
239
  operation
233
240
  );
@@ -241,6 +248,7 @@ export function createToolContext(
241
248
  collections: config.collections,
242
249
  egress: current?.egress,
243
250
  requestIdentity: current?.requestIdentity,
251
+ requestNamespace: current?.requestNamespace,
244
252
  },
245
253
  operation
246
254
  );
@@ -23,6 +23,8 @@ export const MCP_HTTP_EGRESS_TOOLS = {
23
23
  gno_changes: "metadata",
24
24
  gno_clear_collection_embeddings: "metadata",
25
25
  gno_context: "capsule",
26
+ gno_context_compiled_preview: "capsule",
27
+ gno_context_compiled_check: "capsule",
26
28
  gno_context_verify: "capsule",
27
29
  gno_create_folder: "metadata",
28
30
  gno_diff: "metadata",
@@ -55,8 +57,12 @@ export const MCP_HTTP_EGRESS_TOOLS = {
55
57
  gno_remember: "source",
56
58
  gno_remove_collection: "metadata",
57
59
  gno_rename_note: "metadata",
60
+ gno_request_status: "metadata",
58
61
  gno_search: "snippet",
59
62
  gno_section: "metadata",
63
+ gno_sessions_automation_run: "metadata",
64
+ gno_sessions_import: "metadata",
65
+ gno_sessions_status: "metadata",
60
66
  gno_similar: "snippet",
61
67
  gno_peek: "metadata",
62
68
  gno_status: "metadata",
@@ -159,9 +165,15 @@ const enforceMessage = (
159
165
  const params = asRecord(message.params);
160
166
  const name = params?.name;
161
167
  if (typeof name !== "string" || !(name in MCP_HTTP_EGRESS_TOOLS)) return;
162
- // Trace export is authorized inside RetrievalTraceManagementService after
163
- // exact trace IDs resolve to their immutable collection lineage.
164
- if (name === "gno_trace_export") return;
168
+ // Derived exports authorize exact current lineage inside their shared
169
+ // runtime. Broad transport scoping would deny eligible subsets merely
170
+ // because another configured collection is private.
171
+ if (
172
+ name === "gno_trace_export" ||
173
+ name === "gno_context_compiled_preview" ||
174
+ name === "gno_context_compiled_check"
175
+ )
176
+ return;
165
177
  contentClass =
166
178
  MCP_HTTP_EGRESS_TOOLS[name as keyof typeof MCP_HTTP_EGRESS_TOOLS];
167
179
  } else if (
@@ -15,6 +15,7 @@ import type {
15
15
 
16
16
  import { MCP_SERVER_NAME, VERSION } from "../app/constants";
17
17
  import { EgressDeniedError } from "../core/egress-enforcement";
18
+ import { httpMcpRequestNamespace } from "../core/request-receipts";
18
19
  import { withInferenceScope } from "../llm/inference-scope";
19
20
  import { createMcpServerSurface, type ToolContext } from "./context";
20
21
  import {
@@ -445,6 +446,7 @@ export class HttpMcpTransport {
445
446
  this.#runtime.mcpContext.serverInstanceId,
446
447
  context.identity
447
448
  ),
449
+ requestNamespace: httpMcpRequestNamespace(context.identity),
448
450
  }
449
451
  )
450
452
  : await handle();
@@ -0,0 +1,24 @@
1
+ import type { SearchMeta } from "../pipeline/types";
2
+
3
+ import { METADATA_COVERAGE_GUIDANCE } from "../core/typed-metadata";
4
+
5
+ /** Text-only MCP clients need the same warnings as structured clients. */
6
+ export function appendRetrievalWarnings(
7
+ text: string,
8
+ warnings: SearchMeta["warnings"]
9
+ ): string {
10
+ if (!warnings?.length) return text;
11
+ const lines = warnings.map(
12
+ ({ code, message }) => `Warning [${code}]: ${message}`
13
+ );
14
+ if (
15
+ warnings.some(
16
+ ({ code }) =>
17
+ code === "METADATA_COVERAGE_INCOMPLETE" ||
18
+ code === "METADATA_COVERAGE_UNKNOWN"
19
+ )
20
+ ) {
21
+ lines.push(METADATA_COVERAGE_GUIDANCE);
22
+ }
23
+ return `${text}\n\n${lines.join("\n")}`;
24
+ }
@@ -17,6 +17,8 @@ import { attachRetrievalTraceMetadata } from "../../core/retrieval-trace-session
17
17
  import { normalizeStructuredQueryInput } from "../../core/structured-query";
18
18
  import {
19
19
  metadataPredicateSchema,
20
+ METADATA_FILTER_DESCRIPTION,
21
+ METADATA_COVERAGE_GUIDANCE,
20
22
  normalizeMetadataPredicate,
21
23
  } from "../../core/typed-metadata";
22
24
  import { resolveModelUri } from "../../llm/registry";
@@ -44,7 +46,9 @@ export const askInputSchema = z
44
46
  candidateLimit: z.number().int().min(1).max(100).optional(),
45
47
  exclude: z.array(z.string()).optional(),
46
48
  queryModes: z.array(queryModeSchema).optional(),
47
- filter: metadataPredicateSchema.optional(),
49
+ filter: metadataPredicateSchema
50
+ .optional()
51
+ .describe(METADATA_FILTER_DESCRIPTION),
48
52
  tagsAll: z.array(z.string()).optional(),
49
53
  tagsAny: z.array(z.string()).optional(),
50
54
  since: z.string().optional(),
@@ -111,6 +115,15 @@ export const formatVerifiedAskReadable = (
111
115
  );
112
116
  }
113
117
  }
118
+ if (
119
+ result.verification?.capsule.warnings.some(
120
+ (warning) => warning.code === "metadata_coverage_incomplete"
121
+ )
122
+ ) {
123
+ lines.push(
124
+ "Warning [metadata_coverage_incomplete]: " + METADATA_COVERAGE_GUIDANCE
125
+ );
126
+ }
114
127
  for (const gap of result.verification?.capsule.coverage.gaps ?? []) {
115
128
  lines.push(`Gap: ${gap.facet} (${gap.code})`);
116
129
  }