@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
package/README.md CHANGED
@@ -117,7 +117,7 @@ gno daemon --detach # headless indexing + resident MCP gateway
117
117
 
118
118
  <!-- public-truth:current-version -->
119
119
 
120
- > Current release: **v1.34.5** — see [CHANGELOG.md](./CHANGELOG.md)
120
+ > Current release: **v1.35.0** — see [CHANGELOG.md](./CHANGELOG.md)
121
121
 
122
122
  <!-- /public-truth -->
123
123
 
@@ -174,6 +174,17 @@ gno daemon --detach # headless indexing + resident MCP gateway
174
174
  `local_only`, `lan`, or `remote` boundary that follows mixed and derived
175
175
  evidence through resident serving, inference, publishing, exports, Capsules,
176
176
  and traces. Authentication never overrides policy.
177
+ - **Source availability (`any` | `local`)**: opt-in `local` refuses
178
+ cloud-placeholder materialization on the macOS File Provider layouts covered
179
+ by physical evidence (Google Drive, iCloud Drive, and OneDrive for both
180
+ validated immediate SharePoint library roots). Hierarchical directory
181
+ classification + guarded content recheck; skips are not conversion errors;
182
+ unproven prefixes preserve indexed descendants. Distinct from egress.
183
+ Unsupported platforms fail closed; no claim of zero provider activity or
184
+ universal provider support. On the controlled 5,000-file all-local corpus,
185
+ production `any` regressed -1.1280% and hierarchical `local` added 1.1841%
186
+ median traversal overhead (2 warmups, 9 samples per lane; same corpus and
187
+ interleaved pre-implementation/current production walkers).
177
188
  - **Read-only integrity audits**: `gno audit` and MCP `gno_audit` inspect local
178
189
  links, declared provenance, and source/index freshness offline. Stable,
179
190
  bounded findings distinguish complete, partial, unavailable, and changing
@@ -0,0 +1 @@
1
+ c1e214bba1c0a9a6f54bfd334f7a95c8c5e28e84b12b9f91064e7312e0411882 gno-browser-clipper-v1.35.0.zip
@@ -21,5 +21,5 @@
21
21
  "content_security_policy": {
22
22
  "extension_pages": "script-src 'self'; object-src 'none'; connect-src http://127.0.0.1:*"
23
23
  },
24
- "version": "1.34.6"
24
+ "version": "1.35.0"
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "1.34.6",
3
+ "version": "1.35.0",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
package/spec/cli.md CHANGED
@@ -411,6 +411,10 @@ collections:
411
411
  exclude: [.git, node_modules]
412
412
  updateCmd: git pull
413
413
  languageHint: en
414
+ # Source content availability (distinct from egressPolicy):
415
+ # any (default) = legacy reads; local = opt-in no-materialization guard
416
+ # (macOS File Provider). Unsupported platforms fail closed under local.
417
+ sourceAvailability: any
414
418
  models:
415
419
  embed: file:/models/embed.gguf
416
420
  contexts:
@@ -446,6 +450,39 @@ protection remain authoritative.
446
450
  `graphHints` is active: ordered hints type
447
451
  projected wiki/markdown edges and surface in graph traversal/diagnose metadata.
448
452
 
453
+ `collections[].sourceAvailability` is optional; omitted means `any`. Exact
454
+ values: `any` | `local`. There is no separate public knob beyond these two
455
+ modes and no claim that availability is egress policy. `any` preserves
456
+ historical source reads. `local` is opt-in and establishes a platform-aware
457
+ no-materialization content-read boundary (currently macOS File Provider via
458
+ process-scoped `IOPOL_TYPE_VFS_MATERIALIZE_DATALESS_FILES` plus hierarchical
459
+ memoized per-directory availability classification and a content-boundary
460
+ recheck shared by full/targeted/watch ingestion, sniff/hash/conversion, and
461
+ record import). Unsupported platforms/filesystems and policy-setup failure fail
462
+ closed with distinct codes (`SOURCE_AVAILABILITY_UNSUPPORTED`,
463
+ `SOURCE_AVAILABILITY_POLICY_FAILED`, `SOURCE_AVAILABILITY_UNKNOWN`).
464
+ Cloud-placeholder refusal (`EDEADLK`) surfaces as a skipped file with
465
+ `CLOUD_PLACEHOLDER` / `CLOUD_PARTIAL`, not a conversion error. Local mode also
466
+ refuses descent into dataless or availability-unknown directories
467
+ (`DATALESS_DIRECTORY` skip, or the fail-closed codes above) and preserves
468
+ previously indexed descendants under those unproven prefixes rather than
469
+ marking them inactive. Eligible files have no availability `errorCode`.
470
+ Evidence-qualified scope: Google Drive, iCloud Drive, and OneDrive only for the
471
+ tested OS/provider configuration; OneDrive only for both validated immediate
472
+ SharePoint library roots. No Windows/Linux guarantee; metadata/provider
473
+ bookkeeping may occur; GNO does not pin/evict/download as product behavior.
474
+ Source availability is distinct from `egressPolicy` (where derived content may
475
+ travel).
476
+
477
+ The fixture-scoped post-implementation performance contract is recorded by
478
+ `benchmark-local`: at least 2 warmups and 9 retained samples per lane, raw
479
+ samples plus median/p95/min/max/stddev, explicit contamination, and separate
480
+ discovery/traversal, availability metadata, sniff/read/hash, conversion, and
481
+ embedding phases. The controlled 5,000-file all-local production comparison
482
+ used the same corpus and interleaved pre-implementation/current walkers; it
483
+ passed the current-`any` ≤3% gate at -1.1280% and the hierarchical-`local` ≤10%
484
+ gate at +1.1841%. It is not a provider-latency guarantee.
485
+
449
486
  ---
450
487
 
451
488
  ### gno setup
@@ -90,7 +90,8 @@
90
90
  "properties": {
91
91
  "relPath": { "type": "string" },
92
92
  "status": {
93
- "enum": ["added", "updated", "unchanged", "error", "skipped"]
93
+ "enum": ["added", "updated", "unchanged", "error", "skipped"],
94
+ "description": "skipped is used for eligibility refusals (e.g. cloud placeholders, dataless directories) and other non-error skips; error is for fail-closed and I/O failures"
94
95
  },
95
96
  "docid": { "type": "string" },
96
97
  "mirrorHash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
@@ -98,7 +99,10 @@
98
99
  "contentTypeSource": {
99
100
  "enum": ["frontmatter-type", "prefix", "path-ext", "fallback"]
100
101
  },
101
- "errorCode": { "type": "string" },
102
+ "errorCode": {
103
+ "type": "string",
104
+ "description": "Machine code for skipped/error outcomes. Source-availability codes include CLOUD_PLACEHOLDER, CLOUD_PARTIAL, DATALESS_DIRECTORY (status=skipped), and SOURCE_AVAILABILITY_UNSUPPORTED, SOURCE_AVAILABILITY_POLICY_FAILED, SOURCE_AVAILABILITY_UNKNOWN, PERMISSION, NOT_FOUND, NOT_FILE, IO_ERROR (fail-closed; unproven prefixes preserve indexed descendants). Distinct from collection egressPolicy."
105
+ },
102
106
  "errorMessage": { "type": "string" },
103
107
  "recordImport": {
104
108
  "$ref": "gno://schemas/record-import@1.0"
@@ -60,6 +60,7 @@ export {
60
60
  type Config,
61
61
  ConfigSchema,
62
62
  DEFAULT_EGRESS_POLICY,
63
+ DEFAULT_SOURCE_AVAILABILITY,
63
64
  HttpGatewayConfigSchema,
64
65
  HttpGatewayLimitsSchema,
65
66
  CONTENT_TYPE_GRAPH_HINTS,
@@ -84,6 +85,9 @@ export {
84
85
  isValidLanguageHint,
85
86
  parseScope,
86
87
  resolveConfiguredEgressPolicy,
88
+ SOURCE_AVAILABILITY_MODES,
89
+ type SourceAvailabilityMode,
90
+ SourceAvailabilitySchema,
87
91
  type ProjectProfileBinding,
88
92
  ProjectProfileBindingSchema,
89
93
  type ScopeType,
@@ -56,6 +56,12 @@ export type EgressPolicy = z.infer<typeof EgressPolicySchema>;
56
56
  /** Missing policy is always interpreted as the fail-closed local-only default. */
57
57
  export const DEFAULT_EGRESS_POLICY: EgressPolicy = "local_only";
58
58
 
59
+ /** Source byte materialization boundary for collection indexing. */
60
+ export const SOURCE_AVAILABILITY_MODES = ["any", "local"] as const;
61
+ export const SourceAvailabilitySchema = z.enum(SOURCE_AVAILABILITY_MODES);
62
+ export type SourceAvailabilityMode = z.infer<typeof SourceAvailabilitySchema>;
63
+ export const DEFAULT_SOURCE_AVAILABILITY: SourceAvailabilityMode = "any";
64
+
59
65
  /** Provenance for an effective collection egress policy. */
60
66
  export const EGRESS_POLICY_SOURCES = [
61
67
  "explicit",
@@ -138,6 +144,14 @@ export const CollectionSchema = z.object({
138
144
  */
139
145
  egressPolicyRevision: z.number().int().nonnegative().optional(),
140
146
 
147
+ /**
148
+ * Source content availability policy for indexing. Distinct from egress:
149
+ * controls whether cloud placeholders may be materialized, not where data
150
+ * may leave the machine. Omitted / unset means `any` (legacy read behavior);
151
+ * `local` is opt-in and fails closed where the platform guard is unavailable.
152
+ */
153
+ sourceAvailability: SourceAvailabilitySchema.optional(),
154
+
141
155
  /** Optional per-collection model overrides */
142
156
  models: z
143
157
  .object({
@@ -51,3 +51,37 @@ export function matchesCollectionExclusion(
51
51
  }
52
52
  return false;
53
53
  }
54
+
55
+ /**
56
+ * Whether a known directory is the root of a fully excluded subtree.
57
+ * A trailing `/**` excludes every descendant even though Bun.Glob does not
58
+ * match the directory root itself. Other partial glob shapes must still be
59
+ * traversed because they may leave eligible descendants.
60
+ */
61
+ export function matchesCollectionSubtreeExclusion(
62
+ relPath: string,
63
+ excludes: readonly string[]
64
+ ): boolean {
65
+ const normalizedPath = relPath.replaceAll("\\", "/");
66
+ if (matchesCollectionExclusion(normalizedPath, excludes)) {
67
+ return true;
68
+ }
69
+
70
+ for (const rawPattern of excludes) {
71
+ const pattern = rawPattern.replaceAll("\\", "/");
72
+ if (pattern === "**") {
73
+ return true;
74
+ }
75
+ if (!pattern.endsWith("/**")) {
76
+ continue;
77
+ }
78
+ const subtreeRootPattern = pattern.slice(0, -3);
79
+ if (
80
+ subtreeRootPattern.length > 0 &&
81
+ new Bun.Glob(subtreeRootPattern).match(normalizedPath)
82
+ ) {
83
+ return true;
84
+ }
85
+ }
86
+ return false;
87
+ }
@@ -11,6 +11,27 @@ export { defaultLanguageDetector, SimpleLanguageDetector } from "./language";
11
11
  // Sync service
12
12
  export { defaultSyncService, SyncService } from "./sync";
13
13
  export { resolveContentTypeRules, withContentTypeRules } from "./sync-options";
14
+ // Source availability (content-boundary guard)
15
+ export {
16
+ createDirectoryAvailability,
17
+ createSourceContentReader,
18
+ DEFAULT_SOURCE_AVAILABILITY,
19
+ findUnprovenAvailabilityPrefix,
20
+ isSourceAvailabilitySkip,
21
+ isUnprovenAbsenceCode,
22
+ memoizeDirectoryAvailability,
23
+ relPathUnderAnyPrefix,
24
+ resolveSourceAvailability,
25
+ SOURCE_AVAILABILITY_MODES,
26
+ } from "./source-availability";
27
+ export type {
28
+ DirectoryAvailabilityPort,
29
+ DirectoryAvailabilityResult,
30
+ SourceAvailabilityCode,
31
+ SourceAvailabilityMode,
32
+ SourceContentReaderPort,
33
+ SourceReadResult,
34
+ } from "./source-availability";
14
35
  // Types
15
36
  export type {
16
37
  ChunkerPort,
@@ -58,6 +58,11 @@ interface RecordContainerInput {
58
58
  sourceMtime: string;
59
59
  sourceSize: number;
60
60
  store: StorePort;
61
+ /**
62
+ * Optional pre-read source bytes from the guarded content boundary.
63
+ * When set, record import streams from this buffer and never reopens the path.
64
+ */
65
+ sourceBytes?: Uint8Array;
61
66
  }
62
67
 
63
68
  interface AppliedRecordReconciliation {
@@ -283,6 +288,20 @@ const sourceStream = (
283
288
  },
284
289
  });
285
290
 
291
+ const bytesStream = (
292
+ bytes: Uint8Array,
293
+ signal?: AbortSignal
294
+ ): AsyncIterable<Uint8Array> => ({
295
+ async *[Symbol.asyncIterator]() {
296
+ if (signal?.aborted) {
297
+ throw new Error("record adapter aborted");
298
+ }
299
+ if (bytes.byteLength > 0) {
300
+ yield bytes;
301
+ }
302
+ },
303
+ });
304
+
286
305
  const loadPreviousStructure = async (
287
306
  store: StorePort,
288
307
  existing: DocumentRow | undefined
@@ -443,7 +462,10 @@ export async function processRecordContainer(
443
462
  collection: input.collection.name,
444
463
  mime: input.mime,
445
464
  ext: input.ext,
446
- open: (signal) => sourceStream(input.entry.absPath, signal),
465
+ open: (signal) =>
466
+ input.sourceBytes
467
+ ? bytesStream(input.sourceBytes, signal)
468
+ : sourceStream(input.entry.absPath, signal),
447
469
  limits: {
448
470
  ...DEFAULT_RECORD_ADAPTER_LIMITS,
449
471
  timeoutMs: Math.min(
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Darwin FFI backend for no-materialization I/O policy and guarded content reads.
3
+ * Provider-neutral; reuses the TN3150 mechanism proven in
4
+ * scripts/macos-file-provider-smoke.ts (IOPOL_TYPE_VFS_MATERIALIZE_DATALESS_FILES).
5
+ * No provider SDK; no pin/evict/download/availability mutation.
6
+ *
7
+ * @module src/ingestion/source-availability/darwin-io
8
+ */
9
+
10
+ // bun:ffi — getiopolicy_np/setiopolicy_np/open/read require libc FFI; no Bun high-level equivalent
11
+ import { dlopen, FFIType, ptr, toArrayBuffer } from "bun:ffi";
12
+ // node:fs constants expose Darwin O_NOFOLLOW; Bun has no file-open flag API.
13
+ import { constants as fsConstants } from "node:fs";
14
+
15
+ /** TN3150 / File Provider constants (from smoke harness evidence). */
16
+ export const SF_DATALESS = 0x4000_0000;
17
+ export const IOPOL_TYPE_VFS_MATERIALIZE_DATALESS_FILES = 3;
18
+ export const IOPOL_SCOPE_PROCESS = 0;
19
+ export const IOPOL_MATERIALIZE_DATALESS_FILES_OFF = 1;
20
+ /** Darwin errno for guarded dataless materialization refusal. */
21
+ export const DARWIN_EDEADLK = 11;
22
+ export const DARWIN_EACCES = 13;
23
+ export const DARWIN_EPERM = 1;
24
+ export const DARWIN_ENOENT = 2;
25
+ export const DARWIN_EISDIR = 21;
26
+ export const DARWIN_ELOOP = 62;
27
+ export const DARWIN_EIO = 5;
28
+ const OPEN_RDONLY_NOFOLLOW = fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW;
29
+
30
+ export type DarwinIoPolicyPort = {
31
+ get: (type: number, scope: number) => number;
32
+ set: (type: number, scope: number, policy: number) => number;
33
+ readErrno: () => number;
34
+ };
35
+
36
+ export type DarwinFileIoPort = {
37
+ open: (absPath: string, flags: number) => number;
38
+ read: (fd: number, buf: Uint8Array) => number;
39
+ close: (fd: number) => number;
40
+ readErrno: () => number;
41
+ };
42
+
43
+ /**
44
+ * No-follow lstat of `st_flags` for SF_DATALESS directory classification.
45
+ * Distinct from content open/read — used only at directory boundaries.
46
+ */
47
+ export type DarwinStatPort = {
48
+ lstatFlags: (
49
+ absPath: string
50
+ ) => { ok: true; stFlags: number } | { ok: false; errno: number };
51
+ };
52
+
53
+ export type DarwinIoBundle = {
54
+ policy: DarwinIoPolicyPort;
55
+ file: DarwinFileIoPort;
56
+ stat: DarwinStatPort;
57
+ };
58
+
59
+ /** Darwin `struct stat` layout used by the smoke harness (arm64/x86_64). */
60
+ export const DARWIN_STAT_BUF_SIZE = 144;
61
+ export const DARWIN_ST_FLAGS_OFFSET = 116;
62
+
63
+ type LibSymbols = {
64
+ getiopolicy_np: (type: number, scope: number) => number;
65
+ setiopolicy_np: (type: number, scope: number, policy: number) => number;
66
+ open: (path: ReturnType<typeof ptr>, flags: number) => number;
67
+ close: (fd: number) => number;
68
+ read: (fd: number, buf: ReturnType<typeof ptr>, n: bigint) => bigint;
69
+ lstat: (path: ReturnType<typeof ptr>, buf: ReturnType<typeof ptr>) => number;
70
+ __error: () => ReturnType<typeof ptr> | null;
71
+ };
72
+
73
+ let cachedBundle: DarwinIoBundle | null | undefined;
74
+
75
+ /** Test-only: clear FFI caches between cases. */
76
+ export function resetDarwinIoCachesForTests(): void {
77
+ cachedBundle = undefined;
78
+ }
79
+
80
+ function asFn<T extends (...args: never[]) => unknown>(
81
+ value: unknown
82
+ ): T | null {
83
+ return typeof value === "function" ? (value as T) : null;
84
+ }
85
+
86
+ function cstr(value: string): Uint8Array {
87
+ return Buffer.from(`${value}\0`);
88
+ }
89
+
90
+ function readErrnoFrom(symbols: LibSymbols): number {
91
+ const p = symbols.__error();
92
+ if (!p) {
93
+ return 0;
94
+ }
95
+ return new Int32Array(toArrayBuffer(p, 0, 4))[0] ?? 0;
96
+ }
97
+
98
+ function loadLibSystem(): {
99
+ library: ReturnType<typeof dlopen>;
100
+ symbols: LibSymbols;
101
+ } | null {
102
+ if (process.platform !== "darwin") {
103
+ return null;
104
+ }
105
+ for (const soname of ["libSystem.B.dylib", "libSystem.dylib"]) {
106
+ try {
107
+ const library = dlopen(soname, {
108
+ getiopolicy_np: {
109
+ args: [FFIType.i32, FFIType.i32],
110
+ returns: FFIType.i32,
111
+ },
112
+ setiopolicy_np: {
113
+ args: [FFIType.i32, FFIType.i32, FFIType.i32],
114
+ returns: FFIType.i32,
115
+ },
116
+ open: { args: [FFIType.ptr, FFIType.i32], returns: FFIType.i32 },
117
+ close: { args: [FFIType.i32], returns: FFIType.i32 },
118
+ read: {
119
+ args: [FFIType.i32, FFIType.ptr, FFIType.u64],
120
+ returns: FFIType.i64,
121
+ },
122
+ lstat: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
123
+ __error: { args: [], returns: FFIType.ptr },
124
+ });
125
+ const raw = library.symbols as Record<string, unknown>;
126
+ const getiopolicy_np = asFn<LibSymbols["getiopolicy_np"]>(
127
+ raw.getiopolicy_np
128
+ );
129
+ const setiopolicy_np = asFn<LibSymbols["setiopolicy_np"]>(
130
+ raw.setiopolicy_np
131
+ );
132
+ const openFn = asFn<LibSymbols["open"]>(raw.open);
133
+ const closeFn = asFn<LibSymbols["close"]>(raw.close);
134
+ const readFn = asFn<LibSymbols["read"]>(raw.read);
135
+ const lstatFn = asFn<LibSymbols["lstat"]>(raw.lstat);
136
+ const errorFn = asFn<LibSymbols["__error"]>(raw.__error);
137
+ if (
138
+ !getiopolicy_np ||
139
+ !setiopolicy_np ||
140
+ !openFn ||
141
+ !closeFn ||
142
+ !readFn ||
143
+ !lstatFn ||
144
+ !errorFn
145
+ ) {
146
+ continue;
147
+ }
148
+ return {
149
+ library,
150
+ symbols: {
151
+ getiopolicy_np,
152
+ setiopolicy_np,
153
+ open: openFn,
154
+ close: closeFn,
155
+ read: readFn,
156
+ lstat: lstatFn,
157
+ __error: errorFn,
158
+ },
159
+ };
160
+ } catch {
161
+ // try next soname
162
+ }
163
+ }
164
+ return null;
165
+ }
166
+
167
+ /** Load Darwin policy + file I/O ports; null when unavailable. */
168
+ export function loadDarwinIo(): DarwinIoBundle | null {
169
+ if (cachedBundle !== undefined) {
170
+ return cachedBundle;
171
+ }
172
+ const loaded = loadLibSystem();
173
+ if (!loaded) {
174
+ cachedBundle = null;
175
+ return null;
176
+ }
177
+ const { symbols } = loaded;
178
+ const readErrno = (): number => readErrnoFrom(symbols);
179
+ cachedBundle = {
180
+ policy: {
181
+ get: symbols.getiopolicy_np,
182
+ set: symbols.setiopolicy_np,
183
+ readErrno,
184
+ },
185
+ file: {
186
+ open: (absPath: string, flags: number): number =>
187
+ symbols.open(ptr(cstr(absPath)), flags),
188
+ read: (fd: number, buf: Uint8Array): number =>
189
+ Number(symbols.read(fd, ptr(buf), BigInt(buf.byteLength))),
190
+ close: symbols.close,
191
+ readErrno,
192
+ },
193
+ stat: {
194
+ lstatFlags: (absPath: string) => {
195
+ const buf = new Uint8Array(DARWIN_STAT_BUF_SIZE);
196
+ const rc = symbols.lstat(ptr(cstr(absPath)), ptr(buf));
197
+ if (rc !== 0) {
198
+ return { ok: false as const, errno: readErrno() };
199
+ }
200
+ const stFlags = new DataView(
201
+ buf.buffer,
202
+ buf.byteOffset,
203
+ buf.byteLength
204
+ ).getUint32(DARWIN_ST_FLAGS_OFFSET, true);
205
+ return { ok: true as const, stFlags };
206
+ },
207
+ },
208
+ };
209
+ // Keep library strongly referenced via symbols closure for process lifetime.
210
+ void loaded.library;
211
+ return cachedBundle;
212
+ }
213
+
214
+ /**
215
+ * Run `fn` under process-scoped IOPOL_MATERIALIZE_DATALESS_FILES_OFF.
216
+ * Always restores prior policy. Fail-closed on setup/restore failure.
217
+ */
218
+ export function withNoMaterializePolicy<T>(
219
+ run: () => T,
220
+ port: DarwinIoPolicyPort
221
+ ):
222
+ | { ok: true; value: T }
223
+ | {
224
+ ok: false;
225
+ error:
226
+ | "policy_get_failed"
227
+ | "policy_set_failed"
228
+ | "policy_restore_failed";
229
+ } {
230
+ let prior: number;
231
+ try {
232
+ prior = port.get(
233
+ IOPOL_TYPE_VFS_MATERIALIZE_DATALESS_FILES,
234
+ IOPOL_SCOPE_PROCESS
235
+ );
236
+ } catch {
237
+ return { ok: false, error: "policy_get_failed" };
238
+ }
239
+ if (prior < 0) {
240
+ return { ok: false, error: "policy_get_failed" };
241
+ }
242
+ let setupResult: number;
243
+ try {
244
+ setupResult = port.set(
245
+ IOPOL_TYPE_VFS_MATERIALIZE_DATALESS_FILES,
246
+ IOPOL_SCOPE_PROCESS,
247
+ IOPOL_MATERIALIZE_DATALESS_FILES_OFF
248
+ );
249
+ } catch {
250
+ return { ok: false, error: "policy_set_failed" };
251
+ }
252
+ if (setupResult !== 0) {
253
+ return { ok: false, error: "policy_set_failed" };
254
+ }
255
+ let value: T;
256
+ let thrown: unknown;
257
+ try {
258
+ value = run();
259
+ } catch (error) {
260
+ thrown = error;
261
+ }
262
+ let restoreResult: number;
263
+ try {
264
+ restoreResult = port.set(
265
+ IOPOL_TYPE_VFS_MATERIALIZE_DATALESS_FILES,
266
+ IOPOL_SCOPE_PROCESS,
267
+ prior
268
+ );
269
+ } catch {
270
+ return { ok: false, error: "policy_restore_failed" };
271
+ }
272
+ if (restoreResult !== 0) {
273
+ return { ok: false, error: "policy_restore_failed" };
274
+ }
275
+ if (thrown !== undefined) {
276
+ throw thrown;
277
+ }
278
+ return { ok: true, value: value! };
279
+ }
280
+
281
+ export function guardedOpenFlags(): number {
282
+ return OPEN_RDONLY_NOFOLLOW;
283
+ }
284
+
285
+ export function classifyGuardedReadErrno(
286
+ errno: number
287
+ ): "EDEADLK" | "EACCES" | "EPERM" | "ENOENT" | "EISDIR" | "ELOOP" | "OTHER" {
288
+ if (errno === DARWIN_EDEADLK) return "EDEADLK";
289
+ if (errno === DARWIN_EACCES) return "EACCES";
290
+ if (errno === DARWIN_EPERM) return "EPERM";
291
+ if (errno === DARWIN_ENOENT) return "ENOENT";
292
+ if (errno === DARWIN_EISDIR) return "EISDIR";
293
+ if (errno === DARWIN_ELOOP) return "ELOOP";
294
+ return "OTHER";
295
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Metadata-free recognition of the macOS File Provider layouts covered by the
3
+ * physical fn-118 evidence. This is intentionally narrower than all paths on
4
+ * Darwin: unknown storage must not inherit a no-materialization guarantee.
5
+ */
6
+
7
+ // node:os — Bun has no home-directory helper.
8
+ import { homedir } from "node:os";
9
+ // node:path — Bun has no path normalization/relative helpers.
10
+ import { isAbsolute, relative, resolve, sep } from "node:path";
11
+
12
+ export type DarwinFileProviderPathSupport =
13
+ | "google-drive"
14
+ | "icloud-drive"
15
+ | "onedrive-sharepoint"
16
+ | "unsupported"
17
+ | "unknown";
18
+
19
+ export function classifyDarwinFileProviderPath(
20
+ absPath: string,
21
+ home: string = homedir()
22
+ ): DarwinFileProviderPathSupport {
23
+ if (!isAbsolute(absPath) || !isAbsolute(home)) {
24
+ return "unknown";
25
+ }
26
+ const relativeHomePath = relative(resolve(home), resolve(absPath));
27
+ if (
28
+ relativeHomePath === ".." ||
29
+ relativeHomePath.startsWith(`..${sep}`) ||
30
+ isAbsolute(relativeHomePath)
31
+ ) {
32
+ return "unsupported";
33
+ }
34
+ const parts = relativeHomePath.split(sep);
35
+ if (
36
+ parts[0] === "Library" &&
37
+ parts[1] === "Mobile Documents" &&
38
+ parts[2] === "com~apple~CloudDocs"
39
+ ) {
40
+ return "icloud-drive";
41
+ }
42
+ if (parts[0] !== "Library" || parts[1] !== "CloudStorage") {
43
+ return "unsupported";
44
+ }
45
+ const domain = parts[2];
46
+ if (domain?.startsWith("GoogleDrive-") && parts[3] === "My Drive") {
47
+ return "google-drive";
48
+ }
49
+ if (
50
+ domain?.startsWith("OneDrive-") &&
51
+ domain.includes("SharedLibraries") &&
52
+ typeof parts[3] === "string" &&
53
+ parts[3].length > 0
54
+ ) {
55
+ return "onedrive-sharepoint";
56
+ }
57
+ return "unsupported";
58
+ }