@abot-ai/runtime 1.3.0 → 1.3.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 (35) hide show
  1. package/dist/scripts/init-runtime.js +4 -13
  2. package/dist/scripts/runtime-setup-directories.d.ts +2 -0
  3. package/dist/scripts/runtime-setup-directories.js +38 -0
  4. package/dist/src/shared/directory-authority/bootstrap.d.ts +1 -0
  5. package/dist/src/shared/directory-authority/bootstrap.js +118 -0
  6. package/dist/src/shared/directory-authority/command.d.ts +8 -0
  7. package/dist/src/shared/directory-authority/command.js +27 -0
  8. package/dist/src/shared/directory-authority/errors.d.ts +5 -0
  9. package/dist/src/shared/directory-authority/errors.js +10 -0
  10. package/dist/src/shared/directory-authority/index.d.ts +3 -0
  11. package/dist/src/shared/directory-authority/index.js +3 -0
  12. package/dist/src/shared/directory-authority/protocol.d.ts +11 -0
  13. package/dist/src/shared/directory-authority/protocol.js +62 -0
  14. package/dist/src/shared/directory-authority/task.d.ts +7 -0
  15. package/dist/src/shared/directory-authority/task.js +97 -0
  16. package/docs/plugins.md +15 -9
  17. package/package.json +1 -1
  18. package/plugins/exec/plugin.json +3 -3
  19. package/plugins/exec/skills/exec_skill/SKILL.md +1 -1
  20. package/plugins/exec/source/process-manager.ts +1 -20
  21. package/plugins/exec/source/shell-platform.ts +29 -0
  22. package/plugins/exec/src/index.cjs +25 -18
  23. package/plugins/filesystem/source/atomic-write.ts +9 -0
  24. package/plugins/filesystem/source/bounded-io.ts +5 -99
  25. package/plugins/filesystem/source/directory-authority-write.ts +64 -0
  26. package/plugins/filesystem/source/directory-sample-task.ts +23 -0
  27. package/plugins/filesystem/source/directory-sample.ts +132 -0
  28. package/plugins/filesystem/source/directory-write-task.ts +160 -0
  29. package/plugins/filesystem/source/mutation-parent.ts +11 -0
  30. package/plugins/filesystem/src/index.cjs +693 -118
  31. package/plugins/local-search/source/index.ts +2 -0
  32. package/plugins/local-search/source/paths.ts +30 -7
  33. package/plugins/local-search/source/ripgrep-process.ts +69 -0
  34. package/plugins/local-search/source/ripgrep.ts +19 -23
  35. package/plugins/local-search/src/index.cjs +295 -32
@@ -558,11 +558,34 @@ function resolveExecScopedPath(pluginContext, executionContext, rawPath, cwd) {
558
558
 
559
559
  // plugins/exec/source/process-manager.ts
560
560
  var import_node_child_process = require("node:child_process");
561
- var import_node_fs2 = require("node:fs");
562
- var import_promises3 = require("node:fs/promises");
563
561
  var import_node_crypto2 = require("node:crypto");
564
562
  var import_node_string_decoder = require("node:string_decoder");
563
+
564
+ // plugins/exec/source/shell-platform.ts
565
+ var import_node_fs2 = require("node:fs");
566
+ var import_promises3 = require("node:fs/promises");
565
567
  var EXEC_SHELL = "/bin/bash";
568
+ function isSupportedExecPlatform(platform) {
569
+ return platform === "linux" || platform === "darwin";
570
+ }
571
+ async function assertSupportedShell(platform = process.platform) {
572
+ if (!isSupportedExecPlatform(platform)) {
573
+ throw new ExecPluginError(
574
+ "exec_platform_unsupported",
575
+ "The exec plugin requires Linux or macOS and executable /bin/bash."
576
+ );
577
+ }
578
+ try {
579
+ await (0, import_promises3.access)(EXEC_SHELL, import_node_fs2.constants.X_OK);
580
+ } catch {
581
+ throw new ExecPluginError(
582
+ "exec_shell_unavailable",
583
+ "The exec plugin cannot start because executable /bin/bash is unavailable."
584
+ );
585
+ }
586
+ }
587
+
588
+ // plugins/exec/source/process-manager.ts
566
589
  var COMPLETED_PROCESS_RETENTION_MS = 5 * 6e4;
567
590
  var MAX_ACTIVE_PROCESSES_PER_SCOPE = 4;
568
591
  var CappedTextBuffer = class {
@@ -602,22 +625,6 @@ var CappedTextBuffer = class {
602
625
  });
603
626
  }
604
627
  };
605
- async function assertSupportedShell() {
606
- if (process.platform !== "linux") {
607
- throw new ExecPluginError(
608
- "exec_platform_unsupported",
609
- "The exec plugin v1 requires Linux and /bin/bash."
610
- );
611
- }
612
- try {
613
- await (0, import_promises3.access)(EXEC_SHELL, import_node_fs2.constants.X_OK);
614
- } catch {
615
- throw new ExecPluginError(
616
- "exec_shell_unavailable",
617
- "The exec plugin cannot start because executable /bin/bash is unavailable."
618
- );
619
- }
620
- }
621
628
  function createExecProcessManager() {
622
629
  const processes = /* @__PURE__ */ new Map();
623
630
  const unknownProcess = () => new ExecPluginError(
@@ -11,6 +11,7 @@ import {
11
11
  } from "./bounded-io.js";
12
12
  import { fail, isNodeErrorCode, rethrowFilesystemError } from "./errors.js";
13
13
  import { openMutationParent } from "./mutation-parent.js";
14
+ import { writeWithDirectoryAuthority } from "./directory-authority-write.js";
14
15
 
15
16
  export async function atomicWriteText(
16
17
  input: Readonly<{
@@ -20,6 +21,10 @@ export async function atomicWriteText(
20
21
  }>,
21
22
  ): Promise<void> {
22
23
  assertMutationContentSize(input.content);
24
+ if (requiresIsolatedDirectoryAuthority()) {
25
+ await writeWithDirectoryAuthority(input);
26
+ return;
27
+ }
23
28
  const parent = await openMutationParent(input.target);
24
29
  const targetPath = `${parent.procPath}/${parent.targetName}`;
25
30
  const anchoredTarget = Object.freeze({
@@ -113,3 +118,7 @@ async function syncDirectoryBestEffort(
113
118
  // The prepared file itself is always synced before the atomic install.
114
119
  }
115
120
  }
121
+
122
+ function requiresIsolatedDirectoryAuthority(): boolean {
123
+ return process.platform === "darwin";
124
+ }
@@ -1,12 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { constants, type BigIntStats } from "node:fs";
3
- import {
4
- opendir,
5
- open,
6
- realpath,
7
- stat,
8
- type FileHandle,
9
- } from "node:fs/promises";
3
+ import { open, realpath, stat, type FileHandle } from "node:fs/promises";
10
4
  import { relative, sep } from "node:path";
11
5
  import { StringDecoder } from "node:string_decoder";
12
6
  import { TextDecoder } from "node:util";
@@ -19,7 +13,10 @@ export const MAX_MUTATION_BYTES = 1_048_576;
19
13
  export const READ_HEAD_BYTES = 24_576;
20
14
  export const READ_TAIL_BYTES = 12_288;
21
15
  export const DEV_SCAN_BYTES = 1_048_576;
22
- export const DIRECTORY_ENTRY_LIMIT = 160;
16
+ export {
17
+ readDirectorySample,
18
+ DIRECTORY_ENTRY_LIMIT,
19
+ } from "./directory-sample.js";
23
20
 
24
21
  export type BoundedText = Readonly<{
25
22
  text: string;
@@ -350,97 +347,6 @@ export function assertMutationContentSize(content: string): number {
350
347
  return byteCount;
351
348
  }
352
349
 
353
- export async function readDirectorySample(
354
- target: ResolvedRuntimeToolPath,
355
- ): Promise<Readonly<{ entries: readonly string[]; truncated: boolean }>> {
356
- const entries: string[] = [];
357
- let handle: FileHandle | undefined;
358
- try {
359
- if (
360
- typeof constants.O_DIRECTORY !== "number" ||
361
- typeof constants.O_NOFOLLOW !== "number"
362
- ) {
363
- fail(
364
- "filesystem_safe_io_unsupported",
365
- "This platform does not provide the no-follow directory operation required for a safe read.",
366
- );
367
- }
368
- handle = await open(
369
- target.absolutePath,
370
- constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
371
- );
372
- const before = await handle.stat({ bigint: true });
373
- if (!before.isDirectory()) {
374
- fail("not_a_directory", `Expected a directory: ${target.logicalPath}`);
375
- }
376
- const [canonicalRoot, canonicalTarget] = await Promise.all([
377
- realpath(target.rootPath),
378
- realpath(target.absolutePath),
379
- ]);
380
- const rootRelative = relative(canonicalRoot, canonicalTarget);
381
- if (
382
- rootRelative === ".." ||
383
- rootRelative.startsWith(`..${sep}`) ||
384
- rootRelative.startsWith(sep)
385
- ) {
386
- fail(
387
- "filesystem_path_changed",
388
- `Directory changed outside its configured root: ${target.logicalPath}`,
389
- );
390
- }
391
- const pathIdentity = await stat(canonicalTarget, { bigint: true });
392
- if (
393
- !pathIdentity.isDirectory() ||
394
- pathIdentity.dev !== before.dev ||
395
- pathIdentity.ino !== before.ino
396
- ) {
397
- fail(
398
- "filesystem_path_changed",
399
- `Directory changed while it was being opened: ${target.logicalPath}`,
400
- );
401
- }
402
- const directory = await opendir(`/proc/self/fd/${handle.fd}`);
403
- for await (const entry of directory) {
404
- if (entries.length >= DIRECTORY_ENTRY_LIMIT) {
405
- await assertDirectoryStable(handle, before, target.logicalPath);
406
- entries.sort((left, right) => left.localeCompare(right));
407
- return Object.freeze({
408
- entries: Object.freeze(entries),
409
- truncated: true,
410
- });
411
- }
412
- entries.push(`${entry.name}${entry.isDirectory() ? "/" : ""}`);
413
- }
414
- await assertDirectoryStable(handle, before, target.logicalPath);
415
- entries.sort((left, right) => left.localeCompare(right));
416
- return Object.freeze({ entries: Object.freeze(entries), truncated: false });
417
- } catch (error: unknown) {
418
- return rethrowFilesystemError(error, "inspect", target.logicalPath);
419
- } finally {
420
- await handle?.close().catch(() => undefined);
421
- }
422
- }
423
-
424
- async function assertDirectoryStable(
425
- handle: FileHandle,
426
- before: BigIntStats,
427
- logicalPath: string,
428
- ): Promise<void> {
429
- const after = await handle.stat({ bigint: true });
430
- if (
431
- !after.isDirectory() ||
432
- after.dev !== before.dev ||
433
- after.ino !== before.ino ||
434
- after.mtimeNs !== before.mtimeNs ||
435
- after.ctimeNs !== before.ctimeNs
436
- ) {
437
- fail(
438
- "filesystem_read_changed",
439
- `Directory changed while it was being read: ${logicalPath}`,
440
- );
441
- }
442
- }
443
-
444
350
  function rejectBinary(buffer: Buffer, logicalPath: string): void {
445
351
  if (buffer.includes(0)) {
446
352
  fail(
@@ -0,0 +1,64 @@
1
+ import type { ResolvedRuntimeToolPath } from "../../../src/plugin-sdk/index.js";
2
+ import {
3
+ DirectoryAuthorityError,
4
+ runDirectoryAuthorityTask,
5
+ } from "../../../src/shared/directory-authority/index.js";
6
+ import type { MutationTargetVersion } from "./bounded-io.js";
7
+ import { fail, rethrowFilesystemError } from "./errors.js";
8
+ import { openMutationRoot } from "./mutation-parent.js";
9
+ import {
10
+ commitDirectoryWrite,
11
+ type DirectoryWriteInput,
12
+ } from "./directory-write-task.js";
13
+
14
+ export async function writeWithDirectoryAuthority(
15
+ input: Readonly<{
16
+ target: ResolvedRuntimeToolPath;
17
+ content: string;
18
+ expectedVersion: MutationTargetVersion;
19
+ }>,
20
+ ): Promise<void> {
21
+ const root = await openMutationRoot(input.target);
22
+ const expected = input.expectedVersion;
23
+ const expectedVersion: DirectoryWriteInput["expectedVersion"] =
24
+ expected.kind === "absent"
25
+ ? { kind: "absent" }
26
+ : {
27
+ ...expected,
28
+ device: String(expected.device),
29
+ inode: String(expected.inode),
30
+ modifiedNs: String(expected.modifiedNs),
31
+ changedNs: String(expected.changedNs),
32
+ };
33
+ try {
34
+ await runDirectoryAuthorityTask({
35
+ directoryPath: input.target.rootPath,
36
+ directoryFd: root.fd,
37
+ input: {
38
+ relativePath: input.target.relativePath,
39
+ content: input.content,
40
+ expectedVersion,
41
+ },
42
+ task: commitDirectoryWrite,
43
+ });
44
+ } catch (error) {
45
+ if (error instanceof DirectoryAuthorityError) {
46
+ if (error.code.startsWith("filesystem_")) {
47
+ fail(
48
+ error.code,
49
+ `Safe write failed: ${input.target.logicalPath}`,
50
+ error.data,
51
+ );
52
+ }
53
+ if (error.code.startsWith("directory_authority_")) {
54
+ fail(
55
+ "filesystem_path_changed",
56
+ `Directory authority was lost: ${input.target.logicalPath}`,
57
+ );
58
+ }
59
+ }
60
+ rethrowFilesystemError(error, "write", input.target.logicalPath);
61
+ } finally {
62
+ await root.close().catch(() => undefined);
63
+ }
64
+ }
@@ -0,0 +1,23 @@
1
+ /** Self-contained trusted task, run with an already verified directory cwd. */
2
+ export async function sampleDirectoryTask(
3
+ input: Readonly<{ maxEntries: number }>,
4
+ ): Promise<
5
+ Readonly<{
6
+ entries: readonly string[];
7
+ truncated: boolean;
8
+ }>
9
+ > {
10
+ const { opendir } =
11
+ require("node:fs/promises") as typeof import("node:fs/promises");
12
+ const entries: string[] = [];
13
+ const directory = await opendir(".");
14
+ for await (const entry of directory) {
15
+ if (entries.length >= input.maxEntries) {
16
+ entries.sort((left, right) => left.localeCompare(right));
17
+ return { entries, truncated: true };
18
+ }
19
+ entries.push(`${entry.name}${entry.isDirectory() ? "/" : ""}`);
20
+ }
21
+ entries.sort((left, right) => left.localeCompare(right));
22
+ return { entries, truncated: false };
23
+ }
@@ -0,0 +1,132 @@
1
+ import { constants, type BigIntStats } from "node:fs";
2
+ import {
3
+ open,
4
+ opendir,
5
+ realpath,
6
+ stat,
7
+ type FileHandle,
8
+ } from "node:fs/promises";
9
+ import { relative, sep } from "node:path";
10
+ import type { ResolvedRuntimeToolPath } from "../../../src/plugin-sdk/index.js";
11
+ import {
12
+ DirectoryAuthorityError,
13
+ runDirectoryAuthorityTask,
14
+ } from "../../../src/shared/directory-authority/index.js";
15
+ import { fail, rethrowFilesystemError } from "./errors.js";
16
+ import { sampleDirectoryTask } from "./directory-sample-task.js";
17
+
18
+ export const DIRECTORY_ENTRY_LIMIT = 160;
19
+
20
+ export async function readDirectorySample(
21
+ target: ResolvedRuntimeToolPath,
22
+ ): Promise<Readonly<{ entries: readonly string[]; truncated: boolean }>> {
23
+ const entries: string[] = [];
24
+ let handle: FileHandle | undefined;
25
+ try {
26
+ if (
27
+ typeof constants.O_DIRECTORY !== "number" ||
28
+ typeof constants.O_NOFOLLOW !== "number"
29
+ ) {
30
+ fail(
31
+ "filesystem_safe_io_unsupported",
32
+ "This platform does not provide the no-follow directory operation required for a safe read.",
33
+ );
34
+ }
35
+ handle = await open(
36
+ target.absolutePath,
37
+ constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
38
+ );
39
+ const before = await handle.stat({ bigint: true });
40
+ if (!before.isDirectory()) {
41
+ fail("not_a_directory", `Expected a directory: ${target.logicalPath}`);
42
+ }
43
+ const [canonicalRoot, canonicalTarget] = await Promise.all([
44
+ realpath(target.rootPath),
45
+ realpath(target.absolutePath),
46
+ ]);
47
+ const rootRelative = relative(canonicalRoot, canonicalTarget);
48
+ if (
49
+ rootRelative === ".." ||
50
+ rootRelative.startsWith(`..${sep}`) ||
51
+ rootRelative.startsWith(sep)
52
+ ) {
53
+ fail(
54
+ "filesystem_path_changed",
55
+ `Directory changed outside its configured root: ${target.logicalPath}`,
56
+ );
57
+ }
58
+ const pathIdentity = await stat(canonicalTarget, { bigint: true });
59
+ if (
60
+ !pathIdentity.isDirectory() ||
61
+ pathIdentity.dev !== before.dev ||
62
+ pathIdentity.ino !== before.ino
63
+ ) {
64
+ fail(
65
+ "filesystem_path_changed",
66
+ `Directory changed while it was being opened: ${target.logicalPath}`,
67
+ );
68
+ }
69
+ if (requiresIsolatedDirectorySample()) {
70
+ const sample = await runDirectoryAuthorityTask({
71
+ directoryPath: target.absolutePath,
72
+ directoryFd: handle.fd,
73
+ input: { maxEntries: DIRECTORY_ENTRY_LIMIT },
74
+ task: sampleDirectoryTask,
75
+ });
76
+ await assertDirectoryStable(handle, before, target.logicalPath);
77
+ return sample;
78
+ }
79
+ const directory = await opendir(`/proc/self/fd/${handle.fd}`);
80
+ for await (const entry of directory) {
81
+ if (entries.length >= DIRECTORY_ENTRY_LIMIT) {
82
+ await assertDirectoryStable(handle, before, target.logicalPath);
83
+ entries.sort((left, right) => left.localeCompare(right));
84
+ return Object.freeze({
85
+ entries: Object.freeze(entries),
86
+ truncated: true,
87
+ });
88
+ }
89
+ entries.push(`${entry.name}${entry.isDirectory() ? "/" : ""}`);
90
+ }
91
+ await assertDirectoryStable(handle, before, target.logicalPath);
92
+ entries.sort((left, right) => left.localeCompare(right));
93
+ return Object.freeze({ entries: Object.freeze(entries), truncated: false });
94
+ } catch (error: unknown) {
95
+ if (
96
+ error instanceof DirectoryAuthorityError &&
97
+ error.code.startsWith("directory_authority_")
98
+ ) {
99
+ fail(
100
+ "filesystem_path_changed",
101
+ `Directory authority was lost: ${target.logicalPath}`,
102
+ );
103
+ }
104
+ return rethrowFilesystemError(error, "inspect", target.logicalPath);
105
+ } finally {
106
+ await handle?.close().catch(() => undefined);
107
+ }
108
+ }
109
+
110
+ async function assertDirectoryStable(
111
+ handle: FileHandle,
112
+ before: BigIntStats,
113
+ logicalPath: string,
114
+ ): Promise<void> {
115
+ const after = await handle.stat({ bigint: true });
116
+ if (
117
+ !after.isDirectory() ||
118
+ after.dev !== before.dev ||
119
+ after.ino !== before.ino ||
120
+ after.mtimeNs !== before.mtimeNs ||
121
+ after.ctimeNs !== before.ctimeNs
122
+ ) {
123
+ fail(
124
+ "filesystem_read_changed",
125
+ `Directory changed while it was being read: ${logicalPath}`,
126
+ );
127
+ }
128
+ }
129
+
130
+ function requiresIsolatedDirectorySample(): boolean {
131
+ return process.platform === "darwin";
132
+ }
@@ -0,0 +1,160 @@
1
+ export type DirectoryWriteInput = Readonly<{
2
+ relativePath: string;
3
+ content: string;
4
+ expectedVersion:
5
+ | Readonly<{ kind: "absent" }>
6
+ | Readonly<{
7
+ kind: "file";
8
+ device: string;
9
+ inode: string;
10
+ size: number;
11
+ mode: number;
12
+ modifiedNs: string;
13
+ changedNs: string;
14
+ digest: string;
15
+ }>;
16
+ }>;
17
+
18
+ /** Self-contained trusted task: serialized into an isolated Node process. */
19
+ export async function commitDirectoryWrite(
20
+ input: DirectoryWriteInput,
21
+ ): Promise<void> {
22
+ const fs = require("node:fs/promises") as typeof import("node:fs/promises");
23
+ const { constants } = require("node:fs") as typeof import("node:fs");
24
+ const { createHash, randomUUID } =
25
+ require("node:crypto") as typeof import("node:crypto");
26
+ const expected = input.expectedVersion;
27
+
28
+ function failWrite(code: string): never {
29
+ throw Object.assign(new Error(code), { code });
30
+ }
31
+ function hasNodeCode(error: unknown, code: string): boolean {
32
+ return error instanceof Error && "code" in error && error.code === code;
33
+ }
34
+ function isSingleComponent(value: string): boolean {
35
+ if (!value || value === "." || value === "..") return false;
36
+ if (value.includes("/") || value.includes("\\")) return false;
37
+ return !value.includes("\0");
38
+ }
39
+ const segments = input.relativePath.split("/");
40
+ if (!segments.every(isSingleComponent)) failWrite("filesystem_path_changed");
41
+ const targetName = segments.pop()!;
42
+ if (Buffer.byteLength(input.content, "utf8") > 1_048_576) {
43
+ failWrite("filesystem_file_too_large");
44
+ }
45
+
46
+ async function enterChildDirectory(name: string): Promise<void> {
47
+ let handle: Awaited<ReturnType<typeof fs.open>>;
48
+ const flags =
49
+ constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;
50
+ try {
51
+ handle = await fs.open(name, flags);
52
+ } catch (error) {
53
+ if (!hasNodeCode(error, "ENOENT")) throw error;
54
+ try {
55
+ await fs.mkdir(name);
56
+ } catch (creationError) {
57
+ if (!hasNodeCode(creationError, "EEXIST")) throw creationError;
58
+ }
59
+ handle = await fs.open(name, flags);
60
+ }
61
+ try {
62
+ const held = await handle.stat({ bigint: true });
63
+ if (!held.isDirectory()) failWrite("filesystem_path_changed");
64
+ process.chdir(name);
65
+ const entered = await fs.stat(".", { bigint: true });
66
+ if (held.dev !== entered.dev || held.ino !== entered.ino) {
67
+ failWrite("filesystem_path_changed");
68
+ }
69
+ } finally {
70
+ await handle.close();
71
+ }
72
+ }
73
+ for (const segment of segments) await enterChildDirectory(segment);
74
+
75
+ function hasExpectedMetadata(info: import("node:fs").BigIntStats): boolean {
76
+ if (expected.kind !== "file" || !info.isFile()) return false;
77
+ if (String(info.dev) !== expected.device) return false;
78
+ if (String(info.ino) !== expected.inode) return false;
79
+ if (Number(info.size) !== expected.size) return false;
80
+ if ((Number(info.mode) & 0o7777) !== expected.mode) return false;
81
+ if (String(info.mtimeNs) !== expected.modifiedNs) return false;
82
+ return String(info.ctimeNs) === expected.changedNs;
83
+ }
84
+ async function assertExpectedTarget(): Promise<void> {
85
+ let target: Awaited<ReturnType<typeof fs.open>>;
86
+ try {
87
+ target = await fs.open(
88
+ targetName,
89
+ constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
90
+ );
91
+ } catch (error) {
92
+ if (hasNodeCode(error, "ENOENT") && expected.kind === "absent") return;
93
+ failWrite("filesystem_target_changed");
94
+ }
95
+ try {
96
+ const before = await target.stat({ bigint: true });
97
+ if (!hasExpectedMetadata(before)) failWrite("filesystem_target_changed");
98
+ if (expected.kind !== "file") failWrite("filesystem_target_changed");
99
+ const bytes = Buffer.alloc(expected.size);
100
+ let offset = 0;
101
+ while (offset < bytes.length) {
102
+ const { bytesRead } = await target.read(
103
+ bytes,
104
+ offset,
105
+ bytes.length - offset,
106
+ offset,
107
+ );
108
+ if (bytesRead <= 0) failWrite("filesystem_target_changed");
109
+ offset += bytesRead;
110
+ }
111
+ const after = await target.stat({ bigint: true });
112
+ if (!hasExpectedMetadata(after)) failWrite("filesystem_target_changed");
113
+ const digest = createHash("sha256").update(bytes).digest("hex");
114
+ if (digest !== expected.digest) failWrite("filesystem_target_changed");
115
+ } finally {
116
+ await target.close();
117
+ }
118
+ }
119
+
120
+ const parent = await fs.open(
121
+ ".",
122
+ constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
123
+ );
124
+ const temporaryName = `.abot-${targetName}-${randomUUID()}.tmp`;
125
+ let temporaryCreated = false;
126
+ try {
127
+ const temporary = await fs.open(temporaryName, "wx");
128
+ temporaryCreated = true;
129
+ try {
130
+ await temporary.writeFile(input.content, "utf8");
131
+ if (expected.kind === "file") await temporary.chmod(expected.mode);
132
+ await temporary.sync();
133
+ } finally {
134
+ await temporary.close();
135
+ }
136
+ await assertExpectedTarget();
137
+ if (expected.kind === "file") {
138
+ await fs.rename(temporaryName, targetName);
139
+ temporaryCreated = false;
140
+ } else {
141
+ try {
142
+ await fs.link(temporaryName, targetName);
143
+ } catch (error) {
144
+ if (hasNodeCode(error, "EEXIST"))
145
+ failWrite("filesystem_target_changed");
146
+ throw error;
147
+ }
148
+ const removed = await fs
149
+ .rm(temporaryName, { force: true })
150
+ .then(() => true)
151
+ .catch(() => false);
152
+ temporaryCreated = !removed;
153
+ }
154
+ await parent.sync().catch(() => undefined);
155
+ } finally {
156
+ if (temporaryCreated)
157
+ await fs.rm(temporaryName, { force: true }).catch(() => undefined);
158
+ await parent.close().catch(() => undefined);
159
+ }
160
+ }
@@ -66,6 +66,7 @@ async function openDirectory(
66
66
  `Directory changed while it was being opened: ${logicalPath}`,
67
67
  );
68
68
  }
69
+ if (usesIsolatedDirectoryAuthority()) return handle;
69
70
  const procPath = `/proc/self/fd/${handle.fd}`;
70
71
  const procIdentity = await stat(procPath, { bigint: true });
71
72
  if (!procIdentity.isDirectory() || !sameIdentity(opened, procIdentity)) {
@@ -162,3 +163,13 @@ export async function openMutationParent(
162
163
  throw error;
163
164
  }
164
165
  }
166
+
167
+ function usesIsolatedDirectoryAuthority(): boolean {
168
+ return process.platform === "darwin";
169
+ }
170
+
171
+ export function openMutationRoot(
172
+ target: ResolvedRuntimeToolPath,
173
+ ): Promise<FileHandle> {
174
+ return openDirectory(target.rootPath, target.rootPath, target.logicalPath);
175
+ }