@gmickel/gno 1.34.6 → 1.35.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 (38) hide show
  1. package/README.md +12 -1
  2. package/browser-extension/artifacts/{gno-browser-clipper-v1.34.6.zip → gno-browser-clipper-v1.35.0.zip} +0 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.35.0.zip.sha256 +1 -0
  4. package/browser-extension/dist/manifest.json +1 -1
  5. package/package.json +1 -1
  6. package/spec/cli.md +37 -0
  7. package/spec/output-schemas/mcp-job-status.schema.json +6 -2
  8. package/src/config/index.ts +4 -0
  9. package/src/config/types.ts +14 -0
  10. package/src/core/path-rules.ts +34 -0
  11. package/src/ingestion/index.ts +21 -0
  12. package/src/ingestion/record-container.ts +23 -1
  13. package/src/ingestion/source-availability/darwin-io.ts +295 -0
  14. package/src/ingestion/source-availability/darwin-path.ts +58 -0
  15. package/src/ingestion/source-availability/directory.ts +402 -0
  16. package/src/ingestion/source-availability/index.ts +74 -0
  17. package/src/ingestion/source-availability/readers.ts +360 -0
  18. package/src/ingestion/source-availability/resolve.ts +28 -0
  19. package/src/ingestion/source-availability/types.ts +170 -0
  20. package/src/ingestion/sync.ts +197 -24
  21. package/src/ingestion/types.ts +45 -3
  22. package/src/ingestion/walker.ts +263 -5
  23. package/src/serve/public/globals.built.css +1 -1
  24. package/src/serve/watch-reconciliation-fallback-disk.ts +239 -100
  25. package/src/serve/watch-reconciliation-fallback.ts +35 -5
  26. package/src/serve/watch-reconciliation-shared.ts +8 -3
  27. package/src/serve/watch-reconciliation.ts +7 -0
  28. package/src/serve/watch-service-flush.ts +10 -0
  29. package/src/serve/watch-service-lifecycle.ts +2 -0
  30. package/src/serve/watch-service-snapshot.ts +27 -3
  31. package/src/serve/watch-service.ts +1 -0
  32. package/src/serve/watch-snapshot-availability.ts +51 -0
  33. package/src/serve/watch-snapshot-handles.ts +117 -37
  34. package/src/serve/watch-snapshot-libc.ts +141 -22
  35. package/src/serve/watch-snapshot-ops.ts +151 -9
  36. package/src/serve/watch-snapshot-scan.ts +3 -0
  37. package/src/serve/watch-snapshot-types.ts +45 -3
  38. package/browser-extension/artifacts/gno-browser-clipper-v1.34.6.zip.sha256 +0 -1
@@ -347,6 +347,7 @@ export class CollectionWatchService {
347
347
  scheduleFlush(buildQueueHost(this.#hostState()), name);
348
348
  }
349
349
  },
350
+ getSyncOptions: () => this.#syncOptions,
350
351
  buildSnapshot: this.#buildSnapshot,
351
352
  },
352
353
  collection
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Source-availability boundaries shared by watcher snapshot operations.
3
+ *
4
+ * @module src/serve/watch-snapshot-availability
5
+ */
6
+
7
+ // node:path — Bun has no path join helper.
8
+ import { join } from "node:path";
9
+
10
+ import type { WatcherSnapshotFs } from "./watch-snapshot-types";
11
+
12
+ import {
13
+ isUnprovenDirectoryResult,
14
+ type DirectoryAvailabilityPort,
15
+ } from "../ingestion/source-availability";
16
+ import { readDirectChildren } from "./watch-snapshot-scan";
17
+
18
+ export async function directoryAllowsDescent(
19
+ rootAbs: string,
20
+ dirRel: string,
21
+ classifier: DirectoryAvailabilityPort | undefined
22
+ ): Promise<boolean> {
23
+ if (!classifier || classifier.mode === "any") {
24
+ return true;
25
+ }
26
+ const absPath = dirRel === "" ? rootAbs : join(rootAbs, dirRel);
27
+ const classified = await classifier.classify(absPath);
28
+ return !isUnprovenDirectoryResult(classified);
29
+ }
30
+
31
+ export async function readAvailableDirectory(
32
+ rootAbs: string,
33
+ dirRel: string,
34
+ fs: WatcherSnapshotFs,
35
+ maxEntries: number,
36
+ classifier: DirectoryAvailabilityPort | undefined
37
+ ): Promise<
38
+ Awaited<ReturnType<typeof readDirectChildren>> | { status: "unproven" }
39
+ > {
40
+ if (!classifier || classifier.mode === "any") {
41
+ return readDirectChildren(rootAbs, dirRel, fs, maxEntries);
42
+ }
43
+ if (!fs.readDirectChildrenSync) {
44
+ return { status: "unproven" };
45
+ }
46
+ const absPath = dirRel === "" ? rootAbs : join(rootAbs, dirRel);
47
+ const read = classifier.readDirectory(absPath, () =>
48
+ fs.readDirectChildrenSync!(rootAbs, dirRel, maxEntries)
49
+ );
50
+ return read.kind === "available" ? read.value : { status: "unproven" };
51
+ }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Production anchored directory handles (openat / fdopendir).
3
3
  *
4
- * Uses Bun FFI for fd-relative open/enumerate and node:fs structure ops for
5
- * open(O_DIRECTORY|O_NOFOLLOW) + fstat bigint metadata. No new dependencies.
4
+ * Uses Bun FFI for fd-relative open/enumerate/no-follow metadata and node:fs
5
+ * structure ops for directory open + fstat confirmation. No new dependencies.
6
6
  *
7
7
  * Windows and runtimes without a safe anchored path report
8
8
  * `supportsAnchoredHandles: false` so callers fall back rather than claiming
@@ -18,13 +18,19 @@ import { join } from "node:path";
18
18
 
19
19
  import type { LoadedLibc } from "./watch-snapshot-libc";
20
20
  import type {
21
+ SnapshotEntryFingerprint,
21
22
  WatcherDirHandle,
22
23
  WatcherSnapshotFs,
23
24
  WatcherSnapshotStat,
24
25
  } from "./watch-snapshot-types";
25
26
 
26
- import { loadLibc, openatOrThrow, readDirNames } from "./watch-snapshot-libc";
27
- import { isMissingFsError } from "./watch-snapshot-types";
27
+ import {
28
+ loadLibc,
29
+ openatOrThrow,
30
+ readDirNames,
31
+ statatNoFollowOrThrow,
32
+ } from "./watch-snapshot-libc";
33
+ import { fingerprintFromStat, isMissingFsError } from "./watch-snapshot-types";
28
34
 
29
35
  type NativeDir = {
30
36
  fd: number;
@@ -48,32 +54,118 @@ function requireNative(handle: WatcherDirHandle): NativeDir {
48
54
  return native;
49
55
  }
50
56
 
51
- function mapStats(stat: {
52
- isFile(): boolean;
53
- isDirectory(): boolean;
54
- isSymbolicLink(): boolean;
55
- dev: bigint | number;
56
- ino: bigint | number;
57
- size: bigint | number;
58
- mtimeNs?: bigint | number;
59
- ctimeNs?: bigint | number;
60
- }): WatcherSnapshotStat {
61
- return {
62
- isFile: () => stat.isFile(),
63
- isDirectory: () => stat.isDirectory(),
64
- isSymbolicLink: () => stat.isSymbolicLink(),
65
- dev: stat.dev,
66
- ino: stat.ino,
67
- size: stat.size,
68
- mtimeNs: stat.mtimeNs,
69
- ctimeNs: stat.ctimeNs,
70
- };
57
+ function openNativeDirByRel(
58
+ libc: LoadedLibc,
59
+ rootAbs: string,
60
+ dirRel: string
61
+ ): number {
62
+ let fd = openSync(rootAbs, libc.openDirFlags);
63
+ try {
64
+ for (const segment of dirRel.split("/").filter(Boolean)) {
65
+ const child = openatOrThrow(
66
+ libc,
67
+ fd,
68
+ segment,
69
+ libc.openChildFlags,
70
+ "openat"
71
+ );
72
+ closeSync(fd);
73
+ fd = child;
74
+ }
75
+ return fd;
76
+ } catch (cause) {
77
+ closeSync(fd);
78
+ throw cause;
79
+ }
80
+ }
81
+
82
+ function readDirectChildrenNative(
83
+ libc: LoadedLibc,
84
+ rootAbs: string,
85
+ dirRel: string,
86
+ maxEntries: number
87
+ ): ReturnType<NonNullable<WatcherSnapshotFs["readDirectChildrenSync"]>> {
88
+ if (!Number.isInteger(maxEntries) || maxEntries < 0) {
89
+ return {
90
+ status: "scan_failed",
91
+ cause: new Error("maxEntries must be a non-negative integer"),
92
+ };
93
+ }
94
+
95
+ let fd: number;
96
+ try {
97
+ fd = openNativeDirByRel(libc, rootAbs, dirRel);
98
+ } catch (cause) {
99
+ return isMissingFsError(cause)
100
+ ? { status: "missing" }
101
+ : { status: "scan_failed", cause };
102
+ }
103
+
104
+ try {
105
+ const listed = readDirNames(libc, fd, maxEntries);
106
+ if (listed.status === "overflow") {
107
+ return listed;
108
+ }
109
+ const entries = new Map<string, SnapshotEntryFingerprint>();
110
+ listed.names.sort((left, right) =>
111
+ left < right ? -1 : left > right ? 1 : 0
112
+ );
113
+ for (const name of listed.names) {
114
+ if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
115
+ return {
116
+ status: "scan_failed",
117
+ cause: new Error(`Invalid directory entry name: ${name}`),
118
+ };
119
+ }
120
+ if (entries.size >= maxEntries) {
121
+ return { status: "overflow" };
122
+ }
123
+ try {
124
+ const fingerprinted = fingerprintFromStat(
125
+ statatNoFollowOrThrow(libc, fd, name)
126
+ );
127
+ if (!fingerprinted.ok) {
128
+ return { status: "unreliable_metadata" };
129
+ }
130
+ entries.set(name, fingerprinted.fingerprint);
131
+ } catch (cause) {
132
+ return { status: "scan_failed", cause };
133
+ }
134
+ }
135
+ return { status: "present", entries };
136
+ } catch (cause) {
137
+ return isMissingFsError(cause)
138
+ ? { status: "missing" }
139
+ : { status: "scan_failed", cause };
140
+ } finally {
141
+ closeSync(fd);
142
+ }
143
+ }
144
+
145
+ function lstatChildByRelNative(
146
+ libc: LoadedLibc,
147
+ rootAbs: string,
148
+ parentRel: string,
149
+ name: string
150
+ ): WatcherSnapshotStat {
151
+ const parentFd = openNativeDirByRel(libc, rootAbs, parentRel);
152
+ try {
153
+ return statatNoFollowOrThrow(libc, parentFd, name);
154
+ } finally {
155
+ closeSync(parentFd);
156
+ }
71
157
  }
72
158
 
73
159
  function createNativeAnchoredFs(libc: LoadedLibc): WatcherSnapshotFs {
74
160
  return {
75
161
  supportsAnchoredHandles: true,
76
162
 
163
+ readDirectChildrenSync: (rootAbs, dirRel, maxEntries) =>
164
+ readDirectChildrenNative(libc, rootAbs, dirRel, maxEntries),
165
+
166
+ lstatChildByRelSync: (rootAbs, parentRel, name) =>
167
+ lstatChildByRelNative(libc, rootAbs, parentRel, name),
168
+
77
169
  async openDir(absPath: string): Promise<WatcherDirHandle> {
78
170
  const fd = openSync(absPath, libc.openDirFlags);
79
171
  return asHandle({ fd });
@@ -89,19 +181,7 @@ function createNativeAnchoredFs(libc: LoadedLibc): WatcherSnapshotFs {
89
181
  name: string
90
182
  ): Promise<WatcherSnapshotStat> {
91
183
  const native = requireNative(handle);
92
- const fd = openatOrThrow(
93
- libc,
94
- native.fd,
95
- name,
96
- libc.openLstatFlags,
97
- "openat"
98
- );
99
- try {
100
- const stat = fstatSync(fd, { bigint: true });
101
- return mapStats(stat);
102
- } finally {
103
- closeSync(fd);
104
- }
184
+ return statatNoFollowOrThrow(libc, native.fd, name);
105
185
  },
106
186
 
107
187
  async openChildDir(
@@ -14,6 +14,12 @@ import { constants as fsConstants } from "node:fs";
14
14
 
15
15
  export type LibcSymbols = {
16
16
  openat: (dirfd: number, path: Pointer, flags: number) => number;
17
+ fstatat: (
18
+ dirfd: number,
19
+ path: Pointer,
20
+ statBuffer: Pointer,
21
+ flags: number
22
+ ) => number;
17
23
  dup: (fd: number) => number;
18
24
  close: (fd: number) => number;
19
25
  fdopendir: (fd: number) => Pointer | null;
@@ -42,7 +48,33 @@ export type LoadedLibc = {
42
48
  dirent: DirentLayout;
43
49
  openChildFlags: number;
44
50
  openDirFlags: number;
45
- openLstatFlags: number;
51
+ atSymlinkNoFollow: number;
52
+ statLayout: NativeStatLayout;
53
+ };
54
+
55
+ type NativeStatLayout = {
56
+ size: number;
57
+ devOffset: number;
58
+ devBytes: 4 | 8;
59
+ modeOffset: number;
60
+ modeBytes: 2 | 4;
61
+ inoOffset: number;
62
+ sizeOffset: number;
63
+ mtimeSecOffset: number;
64
+ mtimeNsecOffset: number;
65
+ ctimeSecOffset: number;
66
+ ctimeNsecOffset: number;
67
+ };
68
+
69
+ export type FdRelativeStat = {
70
+ isFile(): boolean;
71
+ isDirectory(): boolean;
72
+ isSymbolicLink(): boolean;
73
+ dev: bigint | number;
74
+ ino: bigint;
75
+ size: bigint;
76
+ mtimeNs: bigint;
77
+ ctimeNs: bigint;
46
78
  };
47
79
 
48
80
  let cachedLibc: LoadedLibc | null | undefined;
@@ -82,6 +114,10 @@ function openLibcSymbols(
82
114
  args: [FFIType.i32, FFIType.cstring, FFIType.i32],
83
115
  returns: FFIType.i32,
84
116
  },
117
+ fstatat: {
118
+ args: [FFIType.i32, FFIType.cstring, FFIType.ptr, FFIType.i32],
119
+ returns: FFIType.i32,
120
+ },
85
121
  dup: { args: [FFIType.i32], returns: FFIType.i32 },
86
122
  close: { args: [FFIType.i32], returns: FFIType.i32 },
87
123
  fdopendir: { args: [FFIType.i32], returns: FFIType.ptr },
@@ -121,6 +157,7 @@ export function loadLibc(): LoadedLibc | null {
121
157
 
122
158
  const raw = library.symbols as Record<string, unknown>;
123
159
  const openat = asLibcFn<LibcSymbols["openat"]>(raw.openat);
160
+ const fstatat = asLibcFn<LibcSymbols["fstatat"]>(raw.fstatat);
124
161
  const dup = asLibcFn<LibcSymbols["dup"]>(raw.dup);
125
162
  const close = asLibcFn<LibcSymbols["close"]>(raw.close);
126
163
  const fdopendir = asLibcFn<LibcSymbols["fdopendir"]>(raw.fdopendir);
@@ -129,6 +166,7 @@ export function loadLibc(): LoadedLibc | null {
129
166
  const errnoFn = asLibcFn<LibcSymbols["errnoPtr"]>(raw[errnoName]);
130
167
  if (
131
168
  !openat ||
169
+ !fstatat ||
132
170
  !dup ||
133
171
  !close ||
134
172
  !fdopendir ||
@@ -144,32 +182,15 @@ export function loadLibc(): LoadedLibc | null {
144
182
  const O_DIRECTORY = fsConstants.O_DIRECTORY;
145
183
  const O_NOFOLLOW = fsConstants.O_NOFOLLOW;
146
184
 
147
- let openLstatFlags: number;
148
- if (platform === "darwin") {
149
- // Darwin O_SYMLINK opens the symlink inode itself (lstat-like).
150
- // Without it, O_RDONLY would follow — refuse rather than silent follow.
151
- // O_NONBLOCK is required: plain O_RDONLY|O_SYMLINK can block forever on a
152
- // FIFO with no writer (and similarly hang on some device nodes).
153
- const O_SYMLINK = (fsConstants as { O_SYMLINK?: number }).O_SYMLINK;
154
- if (O_SYMLINK === undefined || O_SYMLINK === 0) {
155
- cachedLibc = null;
156
- return null;
157
- }
158
- const O_NONBLOCK = fsConstants.O_NONBLOCK ?? 0;
159
- openLstatFlags = O_RDONLY | O_SYMLINK | O_NONBLOCK;
160
- } else {
161
- // Linux O_PATH|O_NOFOLLOW is the portable no-follow open for any type.
162
- // O_PATH does not block on FIFOs/sockets; no extra O_NONBLOCK needed.
163
- const O_PATH = (fsConstants as { O_PATH?: number }).O_PATH ?? 0o10_000_000;
164
- openLstatFlags = O_RDONLY | O_PATH | O_NOFOLLOW;
165
- }
166
-
167
185
  const openDirFlags = O_RDONLY | O_DIRECTORY | O_NOFOLLOW;
186
+ const atSymlinkNoFollow = platform === "darwin" ? 0x20 : 0x100;
187
+ const statLayout = nativeStatLayout(platform, process.arch);
168
188
 
169
189
  cachedLibc = {
170
190
  library,
171
191
  symbols: {
172
192
  openat,
193
+ fstatat,
173
194
  dup,
174
195
  close,
175
196
  fdopendir,
@@ -180,11 +201,43 @@ export function loadLibc(): LoadedLibc | null {
180
201
  dirent: direntLayoutFor(platform),
181
202
  openChildFlags: openDirFlags,
182
203
  openDirFlags,
183
- openLstatFlags,
204
+ atSymlinkNoFollow,
205
+ statLayout,
184
206
  };
185
207
  return cachedLibc;
186
208
  }
187
209
 
210
+ function nativeStatLayout(platform: string, arch: string): NativeStatLayout {
211
+ if (platform === "darwin") {
212
+ return {
213
+ size: 144,
214
+ devOffset: 0,
215
+ devBytes: 4,
216
+ modeOffset: 4,
217
+ modeBytes: 2,
218
+ inoOffset: 8,
219
+ sizeOffset: 96,
220
+ mtimeSecOffset: 48,
221
+ mtimeNsecOffset: 56,
222
+ ctimeSecOffset: 64,
223
+ ctimeNsecOffset: 72,
224
+ };
225
+ }
226
+ return {
227
+ size: 144,
228
+ devOffset: 0,
229
+ devBytes: 8,
230
+ modeOffset: arch === "x64" ? 24 : 16,
231
+ modeBytes: 4,
232
+ inoOffset: 8,
233
+ sizeOffset: 48,
234
+ mtimeSecOffset: 88,
235
+ mtimeNsecOffset: 96,
236
+ ctimeSecOffset: 104,
237
+ ctimeNsecOffset: 112,
238
+ };
239
+ }
240
+
188
241
  function asLibcFn<T extends (...args: never[]) => unknown>(
189
242
  value: unknown
190
243
  ): T | null {
@@ -389,3 +442,69 @@ export function openatOrThrow(
389
442
  }
390
443
  return fd;
391
444
  }
445
+
446
+ /**
447
+ * Read no-follow metadata for one direct child without opening its content.
448
+ * This avoids materializing dataless File Provider files while retaining the
449
+ * parent-fd anchoring and symlink identity required by watcher snapshots.
450
+ */
451
+ export function statatNoFollowOrThrow(
452
+ libc: LoadedLibc,
453
+ dirfd: number,
454
+ name: string
455
+ ): FdRelativeStat {
456
+ if (name.includes("\0") || name.includes("/") || name.includes("\\")) {
457
+ throw Object.assign(new Error(`Invalid directory entry name: ${name}`), {
458
+ code: "EINVAL",
459
+ });
460
+ }
461
+ const nameBuffer = Buffer.from(`${name}\0`);
462
+ const statBuffer = Buffer.alloc(libc.statLayout.size);
463
+ for (let attempt = 0; attempt < 2; attempt += 1) {
464
+ const result = libc.symbols.fstatat(
465
+ dirfd,
466
+ ptr(nameBuffer),
467
+ ptr(statBuffer),
468
+ libc.atSymlinkNoFollow
469
+ );
470
+ if (result >= 0) {
471
+ break;
472
+ }
473
+ const errno = readErrno(libc);
474
+ // Darwin/Bun can transiently surface ENOENT during large fd-relative stat
475
+ // batches. Retry the same anchored lookup once. A child that truly
476
+ // disappeared still fails on the second probe, so callers retain the
477
+ // scan-failed authority boundary instead of accepting a partial image.
478
+ if (errno !== 2 || attempt > 0) {
479
+ throw errnoError(errno, "fstatat", name);
480
+ }
481
+ }
482
+
483
+ const layout = libc.statLayout;
484
+ const mode =
485
+ layout.modeBytes === 2
486
+ ? statBuffer.readUInt16LE(layout.modeOffset)
487
+ : statBuffer.readUInt32LE(layout.modeOffset);
488
+ const fileType = mode & 0o170_000;
489
+ const dev =
490
+ layout.devBytes === 4
491
+ ? statBuffer.readUInt32LE(layout.devOffset)
492
+ : statBuffer.readBigUInt64LE(layout.devOffset);
493
+ const mtimeNs =
494
+ statBuffer.readBigInt64LE(layout.mtimeSecOffset) * 1_000_000_000n +
495
+ statBuffer.readBigInt64LE(layout.mtimeNsecOffset);
496
+ const ctimeNs =
497
+ statBuffer.readBigInt64LE(layout.ctimeSecOffset) * 1_000_000_000n +
498
+ statBuffer.readBigInt64LE(layout.ctimeNsecOffset);
499
+
500
+ return {
501
+ isFile: () => fileType === 0o100_000,
502
+ isDirectory: () => fileType === 0o040_000,
503
+ isSymbolicLink: () => fileType === 0o120_000,
504
+ dev,
505
+ ino: statBuffer.readBigUInt64LE(layout.inoOffset),
506
+ size: statBuffer.readBigInt64LE(layout.sizeOffset),
507
+ mtimeNs,
508
+ ctimeNs,
509
+ };
510
+ }