@gmickel/gno 2.3.3 → 2.5.1

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 (117) hide show
  1. package/README.md +22 -3
  2. package/assets/skill/SKILL.md +53 -0
  3. package/assets/skill/cli-reference.md +29 -0
  4. package/assets/skill/examples.md +30 -0
  5. package/assets/skill/mcp-reference.md +26 -0
  6. package/assets/spa-production.json.gz +0 -0
  7. package/browser-extension/artifacts/{gno-browser-clipper-v2.3.3.zip → gno-browser-clipper-v2.5.1.zip} +0 -0
  8. package/browser-extension/artifacts/gno-browser-clipper-v2.5.1.zip.sha256 +1 -0
  9. package/browser-extension/dist/manifest.json +1 -1
  10. package/package.json +1 -1
  11. package/spec/cli.md +41 -0
  12. package/spec/compiled-context.md +68 -0
  13. package/spec/db/schema.sql +2 -0
  14. package/spec/evals.md +48 -0
  15. package/spec/mcp.md +37 -0
  16. package/spec/output-schemas/ask.schema.json +12 -0
  17. package/spec/output-schemas/compiled-context-check.schema.json +44 -0
  18. package/spec/output-schemas/compiled-context-file.schema.json +165 -0
  19. package/spec/output-schemas/compiled-context-preview.schema.json +142 -0
  20. package/spec/output-schemas/context-capsule-v1.schema.json +205 -32
  21. package/spec/output-schemas/context-capsule-verification.schema.json +32 -9
  22. package/spec/output-schemas/evidence-fixtures.schema.json +176 -0
  23. package/spec/output-schemas/evidence-report.schema.json +322 -0
  24. package/spec/output-schemas/evidence-run.schema.json +277 -0
  25. package/spec/output-schemas/metadata-predicate.schema.json +460 -0
  26. package/spec/output-schemas/retrieval-trace-filters.schema.json +13 -4
  27. package/spec/output-schemas/search-results.schema.json +12 -0
  28. package/spec/output-schemas/status.schema.json +37 -6
  29. package/src/app/compiled-context-files.ts +361 -0
  30. package/src/app/compiled-context.ts +240 -0
  31. package/src/app/context-runtime-contract.ts +33 -2
  32. package/src/app/context-runtime-input.ts +17 -0
  33. package/src/app/context-runtime-types.ts +2 -0
  34. package/src/app/context-runtime.ts +27 -3
  35. package/src/app/context-surface.ts +7 -0
  36. package/src/app/verified-ask.ts +1 -0
  37. package/src/cli/commands/ask.ts +1 -0
  38. package/src/cli/commands/audit.ts +4 -0
  39. package/src/cli/commands/context-build.ts +1 -0
  40. package/src/cli/commands/context-compiled.ts +136 -0
  41. package/src/cli/commands/search.ts +1 -0
  42. package/src/cli/commands/status.ts +10 -0
  43. package/src/cli/errors.ts +10 -3
  44. package/src/cli/options.ts +19 -0
  45. package/src/cli/program.ts +82 -0
  46. package/src/converters/types.ts +2 -0
  47. package/src/core/compiled-context.ts +254 -0
  48. package/src/core/context-budget.ts +2 -10
  49. package/src/core/context-capsule-retrieval-schema.ts +8 -0
  50. package/src/core/context-capsule-schema.ts +47 -3
  51. package/src/core/context-capsule-validation.ts +14 -2
  52. package/src/core/context-capsule-verification.ts +1 -0
  53. package/src/core/context-capsule.ts +16 -2
  54. package/src/core/context-compiler.ts +3 -0
  55. package/src/core/file-lock.ts +22 -5
  56. package/src/core/folder-setup-planning.ts +2 -1
  57. package/src/core/network-boundary-inventory.ts +8 -0
  58. package/src/core/record-metadata.ts +1 -1
  59. package/src/core/retrieval-replay-candidate.ts +5 -0
  60. package/src/core/retrieval-trace-filter-normalization.ts +4 -0
  61. package/src/core/retrieval-trace-request.ts +1 -0
  62. package/src/core/retrieval-trace-session.ts +5 -1
  63. package/src/core/retrieval-trace.ts +2 -0
  64. package/src/core/setup-receipt.ts +27 -20
  65. package/src/core/typed-metadata.ts +228 -0
  66. package/src/core/validation.ts +9 -2
  67. package/src/core/windows-private-path.ts +96 -0
  68. package/src/index.ts +2 -2
  69. package/src/ingestion/compiled-context.ts +15 -0
  70. package/src/ingestion/record-adapter-canonical.ts +13 -2
  71. package/src/ingestion/record-container.ts +9 -0
  72. package/src/ingestion/sync.ts +51 -9
  73. package/src/ingestion/typed-metadata.ts +55 -0
  74. package/src/ingestion/walker.ts +5 -4
  75. package/src/llm/nodeLlamaCpp/simulator-install.ts +6 -2
  76. package/src/mcp/http-egress.ts +11 -3
  77. package/src/mcp/retrieval-warnings.ts +24 -0
  78. package/src/mcp/tools/ask.ts +23 -2
  79. package/src/mcp/tools/context.ts +47 -2
  80. package/src/mcp/tools/index.ts +58 -1
  81. package/src/mcp/tools/query.ts +16 -1
  82. package/src/mcp/tools/search.ts +12 -1
  83. package/src/mcp/tools/status.ts +4 -0
  84. package/src/mcp/tools/vsearch.ts +12 -1
  85. package/src/pipeline/diagnose.ts +6 -0
  86. package/src/pipeline/filters.ts +65 -0
  87. package/src/pipeline/graph-retrieval.ts +9 -1
  88. package/src/pipeline/hybrid.ts +22 -2
  89. package/src/pipeline/search.ts +11 -0
  90. package/src/pipeline/types.ts +7 -1
  91. package/src/pipeline/vsearch.ts +21 -1
  92. package/src/sdk/client.ts +98 -3
  93. package/src/sdk/index.ts +11 -0
  94. package/src/sdk/types.ts +35 -1
  95. package/src/serve/compiled-context.ts +84 -0
  96. package/src/serve/context-capsule.ts +1 -0
  97. package/src/serve/public/app.tsx +12 -1
  98. package/src/serve/public/components/MetadataFilter.tsx +186 -0
  99. package/src/serve/public/globals.built.css +1 -1
  100. package/src/serve/public/lib/metadata-filter.ts +28 -0
  101. package/src/serve/public/lib/retrieval-filters.ts +3 -0
  102. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  103. package/src/serve/public/pages/Ask.tsx +48 -3
  104. package/src/serve/public/pages/CompiledContext.tsx +364 -0
  105. package/src/serve/public/pages/Dashboard.tsx +7 -0
  106. package/src/serve/public/pages/DocView.tsx +33 -0
  107. package/src/serve/public/pages/Search.tsx +44 -1
  108. package/src/serve/routes/api.ts +50 -0
  109. package/src/serve/server.ts +43 -2
  110. package/src/serve/spa-production-build.ts +6 -5
  111. package/src/store/migrations/030-typed-metadata.ts +13 -0
  112. package/src/store/migrations/index.ts +2 -0
  113. package/src/store/sqlite/adapter.ts +69 -5
  114. package/src/store/sqlite/eligibility.ts +12 -0
  115. package/src/store/sqlite/metadata-predicate.ts +71 -0
  116. package/src/store/types.ts +11 -0
  117. package/browser-extension/artifacts/gno-browser-clipper-v2.3.3.zip.sha256 +0 -1
@@ -29,6 +29,7 @@ import {
29
29
  legacyLocalOnlyEgressLineage,
30
30
  } from "./egress-provenance";
31
31
  import { canonicalizeRetrievalTraceFilters } from "./retrieval-trace-filter-normalization";
32
+ import { metadataPredicateSchema } from "./typed-metadata";
32
33
 
33
34
  const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
34
35
  const gnoUriSchema = z
@@ -57,6 +58,7 @@ export const traceFiltersSchema = z
57
58
  since: z.string().max(128).optional(),
58
59
  until: z.string().max(128).optional(),
59
60
  categories: z.array(z.string().max(512)).max(1000).optional(),
61
+ filter: metadataPredicateSchema.optional(),
60
62
  author: z.string().max(1024).optional(),
61
63
  intent: z.string().max(8192).optional(),
62
64
  exclude: z.array(z.string().max(1024)).max(1000).optional(),
@@ -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(
@@ -0,0 +1,228 @@
1
+ /** Bounded custom metadata and the shared, type-strict retrieval predicate. */
2
+ import { z } from "zod";
3
+
4
+ export type MetadataScalar = string | number | boolean;
5
+ export type TypedValue = MetadataScalar | MetadataScalar[];
6
+ export type TypedMetadata = Record<string, TypedValue>;
7
+ export type MetadataPredicate =
8
+ | { op: "and" | "or"; predicates: MetadataPredicate[] }
9
+ | { op: "not"; predicate: MetadataPredicate }
10
+ | { op: "eq" | "ne"; key: string; value: MetadataScalar }
11
+ | { op: "gt" | "gte" | "lt" | "lte"; key: string; value: number }
12
+ | { op: "in" | "nin" | "all"; key: string; values: MetadataScalar[] }
13
+ | { op: "exists"; key: string; value: boolean };
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
+
19
+ export const TYPED_METADATA_INGEST_VERSION = 7;
20
+ export const METADATA_LIMITS = {
21
+ keys: 64,
22
+ keyLength: 128,
23
+ stringLength: 4096,
24
+ members: 128,
25
+ bytes: 65_536,
26
+ nodes: 128,
27
+ depth: 8,
28
+ } as const;
29
+ const reserved = new Set(["__proto__", "prototype", "constructor"]);
30
+ const keySchema = z
31
+ .string()
32
+ .min(1)
33
+ .max(METADATA_LIMITS.keyLength)
34
+ .refine((key) => !reserved.has(key), "Reserved metadata key");
35
+ const scalarSchema = z.union([
36
+ z.string().max(METADATA_LIMITS.stringLength),
37
+ z.number().finite(),
38
+ z.boolean(),
39
+ ]);
40
+ const membersSchema = z
41
+ .array(scalarSchema)
42
+ .max(METADATA_LIMITS.members)
43
+ .refine(
44
+ (values) => values.every((value) => typeof value === typeof values[0]),
45
+ "Array members must have one scalar type"
46
+ );
47
+ export const typedMetadataSchema = z
48
+ .unknown()
49
+ .superRefine((value, ctx) => {
50
+ if (
51
+ value &&
52
+ typeof value === "object" &&
53
+ Object.keys(value).some((key) => reserved.has(key))
54
+ ) {
55
+ ctx.addIssue({ code: "custom", message: "Reserved metadata key" });
56
+ }
57
+ })
58
+ .pipe(
59
+ z
60
+ .record(keySchema, z.union([scalarSchema, membersSchema]))
61
+ .refine(
62
+ (value) => Object.keys(value).length <= METADATA_LIMITS.keys,
63
+ "Too many metadata keys"
64
+ )
65
+ .refine(
66
+ (value) =>
67
+ new TextEncoder().encode(JSON.stringify(value)).length <=
68
+ METADATA_LIMITS.bytes,
69
+ "Metadata exceeds 64 KiB"
70
+ )
71
+ );
72
+
73
+ const leafSchemas = [
74
+ z
75
+ .object({ op: z.enum(["eq", "ne"]), key: keySchema, value: scalarSchema })
76
+ .strict(),
77
+ z
78
+ .object({
79
+ op: z.enum(["gt", "gte", "lt", "lte"]),
80
+ key: keySchema,
81
+ value: z.number().finite(),
82
+ })
83
+ .strict(),
84
+ z
85
+ .object({
86
+ op: z.enum(["in", "nin", "all"]),
87
+ key: keySchema,
88
+ values: membersSchema.refine(
89
+ (v) => v.length > 0,
90
+ "Membership requires values"
91
+ ),
92
+ })
93
+ .strict(),
94
+ z
95
+ .object({ op: z.literal("exists"), key: keySchema, value: z.boolean() })
96
+ .strict(),
97
+ ] as const;
98
+
99
+ /** Finite schema recursion rejects deep/cyclic SDK values before descending.
100
+ * Each level shares its child schema; MCP can export the real input contract. */
101
+ function predicateAtDepth(depth: number): z.ZodType<MetadataPredicate> {
102
+ if (depth === METADATA_LIMITS.depth)
103
+ return z
104
+ .discriminatedUnion("op", leafSchemas)
105
+ .meta({ id: "gnoMetadataPredicateDepth8" });
106
+ const child = predicateAtDepth(depth + 1);
107
+ return z
108
+ .discriminatedUnion("op", [
109
+ ...leafSchemas,
110
+ z
111
+ .object({
112
+ op: z.enum(["and", "or"]),
113
+ predicates: z.array(child).min(1).max(128),
114
+ })
115
+ .strict(),
116
+ z.object({ op: z.literal("not"), predicate: child }).strict(),
117
+ ])
118
+ .meta({ id: `gnoMetadataPredicateDepth${depth}` });
119
+ }
120
+
121
+ export const metadataPredicateSchema = predicateAtDepth(1).superRefine(
122
+ (input, ctx) => {
123
+ const queue: MetadataPredicate[] = [input];
124
+ let nodes = 0;
125
+ while (queue.length) {
126
+ const node = queue.pop();
127
+ if (!node) break;
128
+ if (++nodes > METADATA_LIMITS.nodes) {
129
+ ctx.addIssue({ code: "custom", message: "Filter exceeds 128 nodes" });
130
+ return;
131
+ }
132
+ if ("predicates" in node) queue.push(...node.predicates);
133
+ if ("predicate" in node) queue.push(node.predicate);
134
+ }
135
+ if (
136
+ new TextEncoder().encode(JSON.stringify(input)).length >
137
+ METADATA_LIMITS.bytes
138
+ ) {
139
+ ctx.addIssue({ code: "custom", message: "Filter exceeds 64 KiB" });
140
+ }
141
+ }
142
+ );
143
+
144
+ export function normalizeMetadataPredicate(input: unknown): MetadataPredicate {
145
+ const predicate = metadataPredicateSchema.parse(input);
146
+ const normalize = (node: MetadataPredicate): MetadataPredicate => {
147
+ if ("predicates" in node) {
148
+ const unique = new Map(
149
+ node.predicates.map((child) => {
150
+ const value = normalize(child);
151
+ return [JSON.stringify(value), value];
152
+ })
153
+ );
154
+ return {
155
+ op: node.op,
156
+ predicates: [...unique.entries()]
157
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
158
+ .map(([, value]) => value),
159
+ };
160
+ }
161
+ if (node.op === "not")
162
+ return { op: "not", predicate: normalize(node.predicate) };
163
+ if ("values" in node)
164
+ return {
165
+ op: node.op,
166
+ key: node.key,
167
+ values: [...new Set(node.values)].sort((a, b) =>
168
+ JSON.stringify(a) < JSON.stringify(b)
169
+ ? -1
170
+ : JSON.stringify(a) > JSON.stringify(b)
171
+ ? 1
172
+ : 0
173
+ ),
174
+ };
175
+ return node;
176
+ };
177
+ return normalize(predicate);
178
+ }
179
+
180
+ export function matchesMetadataPredicate(
181
+ metadata: TypedMetadata,
182
+ predicate: MetadataPredicate
183
+ ): boolean {
184
+ if (predicate.op === "and")
185
+ return predicate.predicates.every((p) =>
186
+ matchesMetadataPredicate(metadata, p)
187
+ );
188
+ if (predicate.op === "or")
189
+ return predicate.predicates.some((p) =>
190
+ matchesMetadataPredicate(metadata, p)
191
+ );
192
+ if (predicate.op === "not")
193
+ return !matchesMetadataPredicate(metadata, predicate.predicate);
194
+ if (!("key" in predicate)) return false;
195
+ const present = Object.hasOwn(metadata, predicate.key);
196
+ if (predicate.op === "exists") return present === predicate.value;
197
+ if (!present) return false;
198
+ const field = metadata[predicate.key];
199
+ if ("values" in predicate) {
200
+ const values = Array.isArray(field) ? field : [field];
201
+ if (predicate.op === "all")
202
+ return (
203
+ Array.isArray(field) && predicate.values.every((v) => field.includes(v))
204
+ );
205
+ const match = values.some((v) =>
206
+ predicate.values.some((wanted) => wanted === v)
207
+ );
208
+ return predicate.op === "in" ? match : !match;
209
+ }
210
+ if (Array.isArray(field)) return false;
211
+ if (predicate.op === "eq") return field === predicate.value;
212
+ if (predicate.op === "ne")
213
+ return typeof field === typeof predicate.value && field !== predicate.value;
214
+ if (typeof field !== "number" || typeof predicate.value !== "number")
215
+ return false;
216
+ switch (predicate.op) {
217
+ case "gt":
218
+ return field > predicate.value;
219
+ case "gte":
220
+ return field >= predicate.value;
221
+ case "lt":
222
+ return field < predicate.value;
223
+ case "lte":
224
+ return field <= predicate.value;
225
+ default:
226
+ return false;
227
+ }
228
+ }
@@ -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
@@ -37,8 +37,8 @@ process.on("SIGINT", () => {
37
37
  });
38
38
  });
39
39
 
40
- // Run CLI and exit
41
- runCli(process.argv)
40
+ // Await module completion so pending piped stdin keeps Windows Bun alive.
41
+ await runCli(process.argv)
42
42
  .then((code) => cleanupAndExit(interruptExitCode || code))
43
43
  .catch((err) => {
44
44
  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
+ }
@@ -7,9 +7,11 @@ import type {
7
7
  RecordAnchor,
8
8
  RecordMetadata,
9
9
  } from "../converters/types";
10
+ import type { TypedMetadata } from "../core/typed-metadata";
10
11
 
11
12
  import { canonicalize, mirrorHash } from "../converters/canonicalize";
12
13
  import { RECORD_METADATA_LIMITS } from "../converters/types";
14
+ import { validateRecordMetadata } from "./typed-metadata";
13
15
 
14
16
  const HASH_PATTERN = /^[a-f\d]{64}$/;
15
17
  const CONTROL_PATTERN = new RegExp(
@@ -33,6 +35,8 @@ const RECORD_ATTACHMENT_DISPOSITIONS = new Set<
33
35
  >(["inline", "attachment"]);
34
36
 
35
37
  export interface CanonicalRecord {
38
+ typedMetadata?: TypedMetadata;
39
+ metadataError?: string;
36
40
  recordKey: string;
37
41
  stableId: string;
38
42
  sourceLocator: string;
@@ -44,7 +48,7 @@ export interface CanonicalRecord {
44
48
  adapterFingerprint: string;
45
49
  title?: string;
46
50
  languageHint?: string;
47
- metadata?: RecordMetadata;
51
+ metadata?: Omit<RecordMetadata, "custom">;
48
52
  anchors?: RecordAnchor[];
49
53
  }
50
54
 
@@ -187,7 +191,7 @@ const normalizeFailureReference = (
187
191
 
188
192
  const normalizeMetadata = (
189
193
  metadata: RecordMetadata | undefined
190
- ): RecordMetadata | undefined => {
194
+ ): Omit<RecordMetadata, "custom"> | undefined => {
191
195
  if (!metadata) return undefined;
192
196
  const normalizeList = (
193
197
  values: string[] | undefined,
@@ -348,6 +352,10 @@ export const canonicalRecord = (
348
352
  const sourceLocator = normalizeLocator(record.sourceLocator);
349
353
  const markdown = canonicalize(record.markdown);
350
354
  const metadata = normalizeMetadata(record.metadata);
355
+ const custom =
356
+ record.metadata && Object.hasOwn(record.metadata, "custom")
357
+ ? validateRecordMetadata(record.metadata.custom, "metadata.custom")
358
+ : undefined;
351
359
  const anchors = normalizeAnchors(record.anchors);
352
360
  const title = boundedText(record.title, RECORD_METADATA_LIMITS.maxTitleChars);
353
361
  const languageHint = boundedText(
@@ -360,6 +368,7 @@ export const canonicalRecord = (
360
368
  languageHint,
361
369
  markdown,
362
370
  metadata,
371
+ ...custom,
363
372
  sourceLocator,
364
373
  stableId,
365
374
  title,
@@ -370,6 +379,7 @@ export const canonicalRecord = (
370
379
  anchors,
371
380
  languageHint,
372
381
  metadata,
382
+ ...custom,
373
383
  sourceLocator,
374
384
  stableId,
375
385
  title,
@@ -395,6 +405,7 @@ export const canonicalRecord = (
395
405
  title,
396
406
  languageHint,
397
407
  metadata,
408
+ ...custom,
398
409
  anchors,
399
410
  accountingChars: derivedSource.length,
400
411
  metadataChars,
@@ -1,6 +1,7 @@
1
1
  import type { NormalizedContentTypeRule } from "../config";
2
2
  import type { Collection } from "../config/types";
3
3
  import type { RecordAdapter, RecordMetadata } from "../converters/types";
4
+ import type { TypedMetadata } from "../core/typed-metadata";
4
5
  import type {
5
6
  ChunkInput,
6
7
  DocumentRow,
@@ -36,6 +37,8 @@ interface RecordDocumentMetadata {
36
37
  author?: string;
37
38
  frontmatterDate?: string;
38
39
  dateFields?: Record<string, string>;
40
+ typedMetadata?: TypedMetadata;
41
+ metadataError?: string;
39
42
  }
40
43
 
41
44
  interface RecordContainerInput {
@@ -376,6 +379,12 @@ const persistRecord = async (
376
379
  author: record.metadata?.author ?? inferred.author,
377
380
  frontmatterDate: primaryDate(dateFields) ?? inferred.frontmatterDate,
378
381
  dateFields,
382
+ typedMetadata: record.metadataError
383
+ ? undefined
384
+ : (record.typedMetadata ?? inferred.typedMetadata),
385
+ metadataError:
386
+ record.metadataError ??
387
+ (record.typedMetadata ? undefined : inferred.metadataError),
379
388
  recordKey: record.recordKey,
380
389
  recordSourcePath: input.entry.relPath,
381
390
  recordSourceLocator: record.sourceLocator,