@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.
- package/README.md +12 -1
- package/browser-extension/artifacts/{gno-browser-clipper-v1.34.6.zip → gno-browser-clipper-v1.35.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.35.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +37 -0
- package/spec/output-schemas/mcp-job-status.schema.json +6 -2
- package/src/config/index.ts +4 -0
- package/src/config/types.ts +14 -0
- package/src/core/path-rules.ts +34 -0
- package/src/ingestion/index.ts +21 -0
- package/src/ingestion/record-container.ts +23 -1
- package/src/ingestion/source-availability/darwin-io.ts +295 -0
- package/src/ingestion/source-availability/darwin-path.ts +58 -0
- package/src/ingestion/source-availability/directory.ts +402 -0
- package/src/ingestion/source-availability/index.ts +74 -0
- package/src/ingestion/source-availability/readers.ts +360 -0
- package/src/ingestion/source-availability/resolve.ts +28 -0
- package/src/ingestion/source-availability/types.ts +170 -0
- package/src/ingestion/sync.ts +197 -24
- package/src/ingestion/types.ts +45 -3
- package/src/ingestion/walker.ts +263 -5
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/watch-reconciliation-fallback-disk.ts +239 -100
- package/src/serve/watch-reconciliation-fallback.ts +35 -5
- package/src/serve/watch-reconciliation-shared.ts +8 -3
- package/src/serve/watch-reconciliation.ts +7 -0
- package/src/serve/watch-service-flush.ts +10 -0
- package/src/serve/watch-service-lifecycle.ts +2 -0
- package/src/serve/watch-service-snapshot.ts +27 -3
- package/src/serve/watch-service.ts +1 -0
- package/src/serve/watch-snapshot-availability.ts +51 -0
- package/src/serve/watch-snapshot-handles.ts +117 -37
- package/src/serve/watch-snapshot-libc.ts +141 -22
- package/src/serve/watch-snapshot-ops.ts +151 -9
- package/src/serve/watch-snapshot-scan.ts +3 -0
- package/src/serve/watch-snapshot-types.ts +45 -3
- 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.
|
|
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
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
c1e214bba1c0a9a6f54bfd334f7a95c8c5e28e84b12b9f91064e7312e0411882 gno-browser-clipper-v1.35.0.zip
|
package/package.json
CHANGED
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": {
|
|
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"
|
package/src/config/index.ts
CHANGED
|
@@ -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,
|
package/src/config/types.ts
CHANGED
|
@@ -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({
|
package/src/core/path-rules.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/ingestion/index.ts
CHANGED
|
@@ -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) =>
|
|
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
|
+
}
|