@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
@@ -98,6 +98,7 @@ export default defineRuntimePlugin((context) => ({
98
98
  root.commandDirectory,
99
99
  maxResults,
100
100
  executionContext?.abortSignal,
101
+ root.directoryAuthority,
101
102
  );
102
103
  sourceTruncated ||= names.truncated;
103
104
  for (const rawPath of names.items) {
@@ -132,6 +133,7 @@ export default defineRuntimePlugin((context) => ({
132
133
  remaining,
133
134
  executionContext?.abortSignal,
134
135
  root.stdinFd,
136
+ root.directoryAuthority,
135
137
  );
136
138
  sourceTruncated ||= content.truncated;
137
139
  for (const match of content.items) {
@@ -9,6 +9,7 @@ import type {
9
9
  import { resolvePluginPath } from "../../../src/plugin-sdk/index.js";
10
10
 
11
11
  import { LocalSearchError } from "./errors.js";
12
+ import type { SearchDirectoryAuthority } from "./ripgrep-process.js";
12
13
 
13
14
  export type SearchRoot = Readonly<{
14
15
  target: ResolvedRuntimeToolPath;
@@ -16,6 +17,7 @@ export type SearchRoot = Readonly<{
16
17
  commandDirectory: string;
17
18
  commandTarget: string;
18
19
  stdinFd?: number;
20
+ directoryAuthority?: SearchDirectoryAuthority;
19
21
  close(): Promise<void>;
20
22
  }>;
21
23
 
@@ -54,11 +56,14 @@ function authorityUnavailable(): LocalSearchError {
54
56
  async function openSearchAuthority(
55
57
  target: ResolvedRuntimeToolPath,
56
58
  ): Promise<SearchRoot> {
57
- if (
58
- process.platform !== "linux" ||
59
- !Number.isInteger(constants.O_NOFOLLOW) ||
60
- !Number.isInteger(constants.O_NONBLOCK)
61
- ) {
59
+ const supportsSearchAuthority = ["linux", "darwin"].includes(
60
+ process.platform,
61
+ );
62
+ if (!supportsSearchAuthority) throw authorityUnavailable();
63
+ const supportsAuthorityOpenFlags =
64
+ Number.isInteger(constants.O_NOFOLLOW) &&
65
+ Number.isInteger(constants.O_NONBLOCK);
66
+ if (!supportsAuthorityOpenFlags) {
62
67
  throw authorityUnavailable();
63
68
  }
64
69
 
@@ -92,7 +97,9 @@ async function openSearchAuthority(
92
97
 
93
98
  const [pathIdentity, descriptorIdentity] = await Promise.all([
94
99
  stat(canonicalTarget, { bigint: true }),
95
- stat(`/proc/self/fd/${handle.fd}`, { bigint: true }),
100
+ process.platform === "linux"
101
+ ? stat(`/proc/self/fd/${handle.fd}`, { bigint: true })
102
+ : handle.stat({ bigint: true }),
96
103
  ]);
97
104
  if (
98
105
  !sameIdentity(observed, pathIdentity) ||
@@ -103,14 +110,30 @@ async function openSearchAuthority(
103
110
 
104
111
  const authority = handle;
105
112
  const isFile = observed.isFile();
113
+ const needsDirectoryBridge = !isFile && process.platform === "darwin";
114
+ let commandDirectory = "/";
115
+ if (!isFile) {
116
+ commandDirectory =
117
+ process.platform === "linux"
118
+ ? `/proc/self/fd/${authority.fd}`
119
+ : canonicalTarget;
120
+ }
106
121
  let closed = false;
107
122
  handle = undefined;
108
123
  return Object.freeze({
109
124
  target,
110
125
  isFile,
111
- commandDirectory: isFile ? "/" : `/proc/self/fd/${authority.fd}`,
126
+ commandDirectory,
112
127
  commandTarget: isFile ? "-" : ".",
113
128
  ...(isFile ? { stdinFd: authority.fd } : {}),
129
+ ...(needsDirectoryBridge
130
+ ? {
131
+ directoryAuthority: {
132
+ directoryPath: canonicalTarget,
133
+ directoryFd: authority.fd,
134
+ },
135
+ }
136
+ : {}),
114
137
  close: async () => {
115
138
  if (closed) return;
116
139
  closed = true;
@@ -0,0 +1,69 @@
1
+ import { spawn, type ChildProcessByStdio } from "node:child_process";
2
+ import type { Readable } from "node:stream";
3
+
4
+ import { rgPath } from "@vscode/ripgrep";
5
+
6
+ import { spawnDirectoryAuthorityCommand } from "../../../src/shared/directory-authority/index.js";
7
+
8
+ export type SearchDirectoryAuthority = Readonly<{
9
+ directoryPath: string;
10
+ directoryFd: number;
11
+ }>;
12
+
13
+ function controlledRipgrepEnvironment(): NodeJS.ProcessEnv {
14
+ const environment = { ...process.env };
15
+ for (const name of Object.keys(environment)) {
16
+ if (name.toUpperCase() === "RIPGREP_CONFIG_PATH") {
17
+ delete environment[name];
18
+ }
19
+ }
20
+ return environment;
21
+ }
22
+
23
+ function terminateAuthorityGroup(
24
+ child: ChildProcessByStdio<null, Readable, Readable>,
25
+ ): void {
26
+ if (!child.pid) {
27
+ child.kill("SIGKILL");
28
+ return;
29
+ }
30
+ try {
31
+ process.kill(-child.pid, "SIGKILL");
32
+ } catch (error) {
33
+ const groupAlreadyExited =
34
+ (error as NodeJS.ErrnoException).code === "ESRCH";
35
+ if (groupAlreadyExited) return;
36
+ throw error;
37
+ }
38
+ }
39
+
40
+ export function spawnRipgrepProcess(
41
+ args: readonly string[],
42
+ cwd: string,
43
+ stdinFd?: number,
44
+ directoryAuthority?: SearchDirectoryAuthority,
45
+ ) {
46
+ const commandArgs = ["--no-config", ...args];
47
+ const env = controlledRipgrepEnvironment();
48
+ if (directoryAuthority) {
49
+ const child = spawnDirectoryAuthorityCommand({
50
+ ...directoryAuthority,
51
+ command: rgPath,
52
+ args: commandArgs,
53
+ env,
54
+ });
55
+ return { child, terminate: () => terminateAuthorityGroup(child) };
56
+ }
57
+ const child = spawn(rgPath, commandArgs, {
58
+ cwd,
59
+ env,
60
+ stdio: [stdinFd ?? "ignore", "pipe", "pipe"],
61
+ windowsHide: true,
62
+ }) as ChildProcessByStdio<null, Readable, Readable>;
63
+ return {
64
+ child,
65
+ terminate: () => {
66
+ child.kill("SIGKILL");
67
+ },
68
+ };
69
+ }
@@ -1,10 +1,10 @@
1
- import { spawn, type ChildProcessByStdio } from "node:child_process";
2
- import type { Readable } from "node:stream";
3
1
  import { TextDecoder } from "node:util";
4
2
 
5
- import { rgPath } from "@vscode/ripgrep";
6
-
7
3
  import { LocalSearchError } from "./errors.js";
4
+ import {
5
+ spawnRipgrepProcess,
6
+ type SearchDirectoryAuthority,
7
+ } from "./ripgrep-process.js";
8
8
 
9
9
  const SEARCH_TIMEOUT_MS = 10_000;
10
10
  const MAX_PATH_RECORD_BYTES = 64 * 1024;
@@ -45,16 +45,6 @@ export function escapeRgGlobLiteral(value: string): string {
45
45
  .join("");
46
46
  }
47
47
 
48
- function controlledRipgrepEnvironment(): NodeJS.ProcessEnv {
49
- const environment = { ...process.env };
50
- for (const name of Object.keys(environment)) {
51
- if (name.toUpperCase() === "RIPGREP_CONFIG_PATH") {
52
- delete environment[name];
53
- }
54
- }
55
- return environment;
56
- }
57
-
58
48
  function decodeUtf8Record(bytes: Buffer, kind: "path" | "result"): string {
59
49
  let value: string;
60
50
  try {
@@ -129,6 +119,7 @@ async function runRipgrepBounded<T>(
129
119
  parser: RipgrepStreamParser<T>,
130
120
  signal?: AbortSignal,
131
121
  stdinFd?: number,
122
+ directoryAuthority?: SearchDirectoryAuthority,
132
123
  ): Promise<BoundedRipgrepResult<T>> {
133
124
  if (signal?.aborted) {
134
125
  throw new LocalSearchError(
@@ -143,12 +134,12 @@ async function runRipgrepBounded<T>(
143
134
  let timedOut = false;
144
135
  let aborted = false;
145
136
  let parserError: unknown;
146
- const child = spawn(rgPath, ["--no-config", ...args], {
137
+ const { child, terminate } = spawnRipgrepProcess(
138
+ args,
147
139
  cwd,
148
- env: controlledRipgrepEnvironment(),
149
- stdio: [stdinFd ?? "ignore", "pipe", "pipe"],
150
- windowsHide: true,
151
- }) as ChildProcessByStdio<null, Readable, Readable>;
140
+ stdinFd,
141
+ directoryAuthority,
142
+ );
152
143
 
153
144
  const cleanup = (): void => {
154
145
  clearTimeout(timeout);
@@ -177,17 +168,17 @@ async function runRipgrepBounded<T>(
177
168
  stoppedAfterExtraResult = true;
178
169
  if (mayStopProcess) {
179
170
  child.stdout.pause();
180
- child.kill("SIGKILL");
171
+ terminate();
181
172
  }
182
173
  return false;
183
174
  };
184
175
  const abortListener = (): void => {
185
176
  aborted = true;
186
- child.kill("SIGKILL");
177
+ terminate();
187
178
  };
188
179
  const timeout = setTimeout(() => {
189
180
  timedOut = true;
190
- child.kill("SIGKILL");
181
+ terminate();
191
182
  }, SEARCH_TIMEOUT_MS);
192
183
  timeout.unref?.();
193
184
 
@@ -198,7 +189,7 @@ async function runRipgrepBounded<T>(
198
189
  } catch (error) {
199
190
  parserError = error;
200
191
  child.stdout.pause();
201
- child.kill("SIGKILL");
192
+ terminate();
202
193
  }
203
194
  });
204
195
  // Always drain stderr so the child cannot block on its diagnostic pipe.
@@ -313,6 +304,7 @@ export function runRipgrepPathSearch(
313
304
  cwd: string,
314
305
  maxResults: number,
315
306
  signal?: AbortSignal,
307
+ directoryAuthority?: SearchDirectoryAuthority,
316
308
  ): Promise<BoundedRipgrepResult<string>> {
317
309
  return runRipgrepBounded(
318
310
  args,
@@ -324,6 +316,8 @@ export function runRipgrepPathSearch(
324
316
  parse: (record) => decodeUtf8Record(record, "path"),
325
317
  }),
326
318
  signal,
319
+ undefined,
320
+ directoryAuthority,
327
321
  );
328
322
  }
329
323
 
@@ -333,6 +327,7 @@ export function runRipgrepContentSearch(
333
327
  maxResults: number,
334
328
  signal?: AbortSignal,
335
329
  stdinFd?: number,
330
+ directoryAuthority?: SearchDirectoryAuthority,
336
331
  ): Promise<BoundedRipgrepResult<ContentMatch>> {
337
332
  return runRipgrepBounded(
338
333
  args,
@@ -349,5 +344,6 @@ export function runRipgrepContentSearch(
349
344
  }),
350
345
  signal,
351
346
  stdinFd,
347
+ directoryAuthority,
352
348
  );
353
349
  }