@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.
- package/dist/scripts/init-runtime.js +4 -13
- package/dist/scripts/runtime-setup-directories.d.ts +2 -0
- package/dist/scripts/runtime-setup-directories.js +38 -0
- package/dist/src/shared/directory-authority/bootstrap.d.ts +1 -0
- package/dist/src/shared/directory-authority/bootstrap.js +118 -0
- package/dist/src/shared/directory-authority/command.d.ts +8 -0
- package/dist/src/shared/directory-authority/command.js +27 -0
- package/dist/src/shared/directory-authority/errors.d.ts +5 -0
- package/dist/src/shared/directory-authority/errors.js +10 -0
- package/dist/src/shared/directory-authority/index.d.ts +3 -0
- package/dist/src/shared/directory-authority/index.js +3 -0
- package/dist/src/shared/directory-authority/protocol.d.ts +11 -0
- package/dist/src/shared/directory-authority/protocol.js +62 -0
- package/dist/src/shared/directory-authority/task.d.ts +7 -0
- package/dist/src/shared/directory-authority/task.js +97 -0
- package/docs/plugins.md +15 -9
- package/package.json +1 -1
- package/plugins/exec/plugin.json +3 -3
- package/plugins/exec/skills/exec_skill/SKILL.md +1 -1
- package/plugins/exec/source/process-manager.ts +1 -20
- package/plugins/exec/source/shell-platform.ts +29 -0
- package/plugins/exec/src/index.cjs +25 -18
- package/plugins/filesystem/source/atomic-write.ts +9 -0
- package/plugins/filesystem/source/bounded-io.ts +5 -99
- package/plugins/filesystem/source/directory-authority-write.ts +64 -0
- package/plugins/filesystem/source/directory-sample-task.ts +23 -0
- package/plugins/filesystem/source/directory-sample.ts +132 -0
- package/plugins/filesystem/source/directory-write-task.ts +160 -0
- package/plugins/filesystem/source/mutation-parent.ts +11 -0
- package/plugins/filesystem/src/index.cjs +693 -118
- package/plugins/local-search/source/index.ts +2 -0
- package/plugins/local-search/source/paths.ts +30 -7
- package/plugins/local-search/source/ripgrep-process.ts +69 -0
- package/plugins/local-search/source/ripgrep.ts +19 -23
- package/plugins/local-search/src/index.cjs +295 -32
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
2
|
import { access, copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { initializeConfiguredRuntimeDirectories } from "./runtime-setup-directories.js";
|
|
5
6
|
import { buildModelConfig, buildProviderConfig, DEFAULT_ROOT_RESPONSE_METHODOLOGY_FILES, isRecord, parseBaseUrl, parseProvider, readJsonObject, resolveRuntimePackageRoot, writeJsonObject, } from "./runtime-setup-files.js";
|
|
6
7
|
import { getInitNextSteps, getInitUsage, } from "./runtime-setup-presentation.js";
|
|
7
8
|
const ENV_FILE = ".env";
|
|
@@ -13,16 +14,6 @@ const CONFIG_EXAMPLE_FILE = join("examples", "runtime.config.example.json");
|
|
|
13
14
|
const REQUEST_RUNNER_CONFIG_EXAMPLE_FILE = join("examples", "request-runner.config.example.json");
|
|
14
15
|
const MODEL_CONFIG_EXAMPLE_FILE = join("examples", "models", "default.config.json");
|
|
15
16
|
const REQUIRED_ENV_LINE = "LLM_RUNTIME_CONFIG_FILE=local/runtime.config.json";
|
|
16
|
-
const RUNTIME_DIRS = [
|
|
17
|
-
join(".runtime", "compiled"),
|
|
18
|
-
join(".runtime", "shared", "logs"),
|
|
19
|
-
join(".runtime", "prod"),
|
|
20
|
-
join(".runtime", "prod", "sandbox"),
|
|
21
|
-
join(".runtime", "prod", "sessions"),
|
|
22
|
-
join(".runtime", "dev"),
|
|
23
|
-
join(".runtime", "dev", "sandbox"),
|
|
24
|
-
join(".runtime", "dev", "sessions"),
|
|
25
|
-
];
|
|
26
17
|
function requireValue(argv, index, flag) {
|
|
27
18
|
const value = argv[index + 1]?.trim();
|
|
28
19
|
if (!value || value.startsWith("--")) {
|
|
@@ -238,7 +229,7 @@ export async function runInitRuntime(argv = process.argv.slice(2), runOptions =
|
|
|
238
229
|
if (modelConfigStatus !== "kept") {
|
|
239
230
|
await configureModelProfile(modelConfigPath, options.provider, options.model);
|
|
240
231
|
}
|
|
241
|
-
|
|
232
|
+
const runtimeDirectories = await initializeConfiguredRuntimeDirectories(options.rootDir, configPath);
|
|
242
233
|
console.log([
|
|
243
234
|
"abot initialized",
|
|
244
235
|
`root: ${options.rootDir}`,
|
|
@@ -250,7 +241,7 @@ export async function runInitRuntime(argv = process.argv.slice(2), runOptions =
|
|
|
250
241
|
`${LOCAL_REQUEST_RUNNER_CONFIG_FILE}: ${requestRunnerConfigStatus}`,
|
|
251
242
|
`${LOCAL_MODEL_CONFIG_FILE}: ${modelConfigStatus}`,
|
|
252
243
|
...methodologyStatuses.map(({ relativePath, status }) => `${relativePath}: ${status}`),
|
|
253
|
-
|
|
244
|
+
`initialized runtime directories: ${runtimeDirectories.map((directory) => relative(options.rootDir, directory) || ".").join(", ")}`,
|
|
254
245
|
"",
|
|
255
246
|
...getInitNextSteps(commandMode),
|
|
256
247
|
].join("\n"));
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { loadRuntimeConfig } from "../src/runtime/config.js";
|
|
4
|
+
import { RUNTIME_LOGS_DIR_NAME } from "../src/runtime/config/layout.js";
|
|
5
|
+
import { isRecord, readJsonObject, } from "./runtime-setup-files.js";
|
|
6
|
+
function configuredEnvironmentIds(config) {
|
|
7
|
+
if (!isRecord(config.environment))
|
|
8
|
+
return [undefined];
|
|
9
|
+
if (!isRecord(config.environment.profiles))
|
|
10
|
+
return [undefined];
|
|
11
|
+
const profileIds = Object.keys(config.environment.profiles);
|
|
12
|
+
const hasConfiguredEnvironments = profileIds.length > 0;
|
|
13
|
+
return hasConfiguredEnvironments ? profileIds : [undefined];
|
|
14
|
+
}
|
|
15
|
+
function runtimeOwnedDirectories(paths) {
|
|
16
|
+
return [
|
|
17
|
+
paths.runtimeDir,
|
|
18
|
+
paths.agentWorkDir,
|
|
19
|
+
paths.sessionsDir,
|
|
20
|
+
paths.attachmentsDir,
|
|
21
|
+
paths.sharedDir,
|
|
22
|
+
paths.compiledDir,
|
|
23
|
+
dirname(paths.traceFile),
|
|
24
|
+
join(paths.sharedDir, RUNTIME_LOGS_DIR_NAME),
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
/** Initialize the preserved configuration's output roots without moving existing state. */
|
|
28
|
+
export async function initializeConfiguredRuntimeDirectories(rootDir, configPath) {
|
|
29
|
+
const config = await readJsonObject(configPath);
|
|
30
|
+
// Resolve every environment before creating any directories. This preserves the
|
|
31
|
+
// canonical guard against silently reassigning existing shared runtime state.
|
|
32
|
+
const configurations = configuredEnvironmentIds(config).map((profileId) => loadRuntimeConfig({ rootDir, configPath, profileId }));
|
|
33
|
+
const directories = [
|
|
34
|
+
...new Set(configurations.flatMap(({ paths }) => runtimeOwnedDirectories(paths))),
|
|
35
|
+
];
|
|
36
|
+
await Promise.all(directories.map((directory) => mkdir(directory, { recursive: true })));
|
|
37
|
+
return directories;
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function directoryAuthorityScript(mode: "task" | "command", messageLimit: number, task?: (input: never) => unknown): string;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This function is serialized into a fresh Node process. It must remain
|
|
3
|
+
* closure-free and use only node: built-ins; the starting cwd is untrusted
|
|
4
|
+
* until its identity has been compared with the inherited directory handle.
|
|
5
|
+
*/
|
|
6
|
+
function directoryAuthorityBootstrap(mode, messageLimit, task) {
|
|
7
|
+
const fs = require("node:fs");
|
|
8
|
+
function fail(code, message) {
|
|
9
|
+
throw Object.assign(new Error(message), { code });
|
|
10
|
+
}
|
|
11
|
+
function verifyDirectoryAuthority() {
|
|
12
|
+
const expected = fs.fstatSync(3, { bigint: true });
|
|
13
|
+
const actual = fs.statSync(".", { bigint: true });
|
|
14
|
+
const matchesHeldDirectory = expected.isDirectory() &&
|
|
15
|
+
actual.isDirectory() &&
|
|
16
|
+
expected.dev === actual.dev &&
|
|
17
|
+
expected.ino === actual.ino;
|
|
18
|
+
if (matchesHeldDirectory)
|
|
19
|
+
return;
|
|
20
|
+
fail("directory_authority_changed", "The directory changed before the operation could establish its authority.");
|
|
21
|
+
}
|
|
22
|
+
function readRequest() {
|
|
23
|
+
const fd = mode === "task" ? 0 : 4;
|
|
24
|
+
const chunk = Buffer.alloc(64 * 1024);
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let total = 0;
|
|
27
|
+
for (;;) {
|
|
28
|
+
const length = fs.readSync(fd, chunk, 0, chunk.length, null);
|
|
29
|
+
if (length === 0)
|
|
30
|
+
break;
|
|
31
|
+
total += length;
|
|
32
|
+
if (total > messageLimit) {
|
|
33
|
+
fail("directory_authority_request_too_large", "The directory operation input exceeds the bridge byte limit.");
|
|
34
|
+
}
|
|
35
|
+
chunks.push(Buffer.from(chunk.subarray(0, length)));
|
|
36
|
+
}
|
|
37
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
38
|
+
}
|
|
39
|
+
function reportFailure(error) {
|
|
40
|
+
const detail = error;
|
|
41
|
+
const code = typeof detail?.code === "string"
|
|
42
|
+
? detail.code.slice(0, 256)
|
|
43
|
+
: "directory_authority_failed";
|
|
44
|
+
const message = typeof detail?.message === "string"
|
|
45
|
+
? detail.message.slice(0, 4096)
|
|
46
|
+
: "The directory operation failed.";
|
|
47
|
+
let encoded;
|
|
48
|
+
try {
|
|
49
|
+
encoded = JSON.stringify({
|
|
50
|
+
ok: false,
|
|
51
|
+
error: { code, message, data: detail?.data },
|
|
52
|
+
});
|
|
53
|
+
if (Buffer.byteLength(encoded) > messageLimit)
|
|
54
|
+
throw new Error();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
encoded = JSON.stringify({ ok: false, error: { code, message } });
|
|
58
|
+
}
|
|
59
|
+
writeResponse(mode === "task" ? 1 : 2, encoded);
|
|
60
|
+
process.exitCode = mode === "task" ? 0 : 125;
|
|
61
|
+
}
|
|
62
|
+
function writeResponse(fd, encoded) {
|
|
63
|
+
const bytes = Buffer.from(encoded);
|
|
64
|
+
let offset = 0;
|
|
65
|
+
while (offset < bytes.length) {
|
|
66
|
+
offset += fs.writeSync(fd, bytes, offset, bytes.length - offset);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function runTask(input) {
|
|
70
|
+
if (!task)
|
|
71
|
+
fail("directory_authority_failed", "The directory task is missing.");
|
|
72
|
+
const result = await task(input);
|
|
73
|
+
const encoded = JSON.stringify({ ok: true, result });
|
|
74
|
+
if (Buffer.byteLength(encoded) > messageLimit) {
|
|
75
|
+
fail("directory_authority_result_too_large", "The directory operation result exceeds the bridge byte limit.");
|
|
76
|
+
}
|
|
77
|
+
writeResponse(1, encoded);
|
|
78
|
+
}
|
|
79
|
+
function runCommand(input) {
|
|
80
|
+
const { spawn } = require("node:child_process");
|
|
81
|
+
const command = input;
|
|
82
|
+
// Omitting cwd inherits the kernel-pinned authority. Resolving cwd() back
|
|
83
|
+
// into a pathname here would reopen the race this bridge prevents.
|
|
84
|
+
const child = spawn(command.command, command.args, {
|
|
85
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
86
|
+
env: process.env,
|
|
87
|
+
});
|
|
88
|
+
child.once("error", reportFailure);
|
|
89
|
+
child.once("exit", (code, signal) => {
|
|
90
|
+
if (signal) {
|
|
91
|
+
process.kill(process.pid, signal);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (typeof code === "number")
|
|
95
|
+
process.exitCode = code;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
verifyDirectoryAuthority();
|
|
100
|
+
const input = readRequest();
|
|
101
|
+
if (mode === "command") {
|
|
102
|
+
runCommand(input);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
void runTask(input).catch(reportFailure);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
reportFailure(error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function directoryAuthorityScript(mode, messageLimit, task) {
|
|
112
|
+
const taskSource = task ? `(${task.toString()})` : "undefined";
|
|
113
|
+
// tsx preserves nested function names with esbuild's __name annotation.
|
|
114
|
+
// Include that annotation's definition so the same trusted task works in
|
|
115
|
+
// development and in the production bundle without rewriting its source.
|
|
116
|
+
const preserveFunctionName = "const __name=(target,value)=>Object.defineProperty(target,'name',{value,configurable:true});";
|
|
117
|
+
return `${preserveFunctionName}(${directoryAuthorityBootstrap.toString()})(${JSON.stringify(mode)},${messageLimit},${taskSource});`;
|
|
118
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ChildProcessByStdio } from "node:child_process";
|
|
2
|
+
import type { Readable } from "node:stream";
|
|
3
|
+
import { type DirectoryAuthorityLocation } from "./protocol.js";
|
|
4
|
+
export declare function spawnDirectoryAuthorityCommand(input: DirectoryAuthorityLocation & Readonly<{
|
|
5
|
+
command: string;
|
|
6
|
+
args: readonly string[];
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
}>): ChildProcessByStdio<null, Readable, Readable>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { directoryAuthorityScript } from "./bootstrap.js";
|
|
3
|
+
import { assertDirectoryAuthorityLocation, authorityEnvironment, AUTHORITY_MESSAGE_LIMIT, encodeAuthorityRequest, } from "./protocol.js";
|
|
4
|
+
export function spawnDirectoryAuthorityCommand(input) {
|
|
5
|
+
assertDirectoryAuthorityLocation(input);
|
|
6
|
+
const request = encodeAuthorityRequest({
|
|
7
|
+
command: input.command,
|
|
8
|
+
args: input.args,
|
|
9
|
+
});
|
|
10
|
+
const child = spawn(process.execPath, [
|
|
11
|
+
"--input-type=commonjs",
|
|
12
|
+
"-e",
|
|
13
|
+
directoryAuthorityScript("command", AUTHORITY_MESSAGE_LIMIT),
|
|
14
|
+
], {
|
|
15
|
+
cwd: input.directoryPath,
|
|
16
|
+
env: authorityEnvironment(input.env),
|
|
17
|
+
detached: true,
|
|
18
|
+
windowsHide: true,
|
|
19
|
+
stdio: ["ignore", "pipe", "pipe", input.directoryFd, "pipe"],
|
|
20
|
+
});
|
|
21
|
+
const requestPipe = child.stdio[4];
|
|
22
|
+
// An early authority failure may close the request pipe before the parent
|
|
23
|
+
// finishes writing. The process result remains the authoritative error.
|
|
24
|
+
requestPipe?.on("error", () => undefined);
|
|
25
|
+
requestPipe?.end(request);
|
|
26
|
+
return child;
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const AUTHORITY_MESSAGE_LIMIT: number;
|
|
2
|
+
export declare const AUTHORITY_STDERR_LIMIT: number;
|
|
3
|
+
export declare const AUTHORITY_TASK_TIMEOUT_MS = 30000;
|
|
4
|
+
export type DirectoryAuthorityLocation = Readonly<{
|
|
5
|
+
directoryPath: string;
|
|
6
|
+
directoryFd: number;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function assertDirectoryAuthorityLocation(location: DirectoryAuthorityLocation): void;
|
|
9
|
+
export declare function authorityEnvironment(environment?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
10
|
+
export declare function encodeAuthorityRequest(input: unknown): string;
|
|
11
|
+
export declare function decodeAuthorityResult<Result>(output: string): Result;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { DirectoryAuthorityError } from "./errors.js";
|
|
3
|
+
export const AUTHORITY_MESSAGE_LIMIT = 8 * 1024 * 1024;
|
|
4
|
+
export const AUTHORITY_STDERR_LIMIT = 16 * 1024;
|
|
5
|
+
export const AUTHORITY_TASK_TIMEOUT_MS = 30_000;
|
|
6
|
+
export function assertDirectoryAuthorityLocation(location) {
|
|
7
|
+
if (!isAbsolute(location.directoryPath)) {
|
|
8
|
+
throw new DirectoryAuthorityError("directory_authority_invalid_path", "The directory authority requires an absolute starting path.");
|
|
9
|
+
}
|
|
10
|
+
const hasOpenDescriptorNumber = Number.isInteger(location.directoryFd) && location.directoryFd >= 0;
|
|
11
|
+
if (hasOpenDescriptorNumber)
|
|
12
|
+
return;
|
|
13
|
+
throw new DirectoryAuthorityError("directory_authority_invalid_fd", "The directory authority requires an open directory descriptor.");
|
|
14
|
+
}
|
|
15
|
+
export function authorityEnvironment(environment = process.env) {
|
|
16
|
+
const sanitized = { ...environment };
|
|
17
|
+
delete sanitized.NODE_OPTIONS;
|
|
18
|
+
delete sanitized.NODE_PATH;
|
|
19
|
+
return sanitized;
|
|
20
|
+
}
|
|
21
|
+
export function encodeAuthorityRequest(input) {
|
|
22
|
+
let encoded;
|
|
23
|
+
try {
|
|
24
|
+
encoded = JSON.stringify(input);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new DirectoryAuthorityError("directory_authority_invalid_request", "The directory operation input must be JSON serializable.");
|
|
28
|
+
}
|
|
29
|
+
if (encoded === undefined) {
|
|
30
|
+
throw new DirectoryAuthorityError("directory_authority_invalid_request", "The directory operation input must be JSON serializable.");
|
|
31
|
+
}
|
|
32
|
+
if (Buffer.byteLength(encoded) <= AUTHORITY_MESSAGE_LIMIT)
|
|
33
|
+
return encoded;
|
|
34
|
+
throw new DirectoryAuthorityError("directory_authority_request_too_large", "The directory operation input exceeds the bridge byte limit.");
|
|
35
|
+
}
|
|
36
|
+
function isAuthorityMessageObject(value) {
|
|
37
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
export function decodeAuthorityResult(output) {
|
|
40
|
+
let message;
|
|
41
|
+
try {
|
|
42
|
+
message = JSON.parse(output);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new DirectoryAuthorityError("directory_authority_protocol_error", "The directory operation returned an invalid response.");
|
|
46
|
+
}
|
|
47
|
+
if (!isAuthorityMessageObject(message)) {
|
|
48
|
+
throw new DirectoryAuthorityError("directory_authority_protocol_error", "The directory operation returned an invalid response.");
|
|
49
|
+
}
|
|
50
|
+
if (message.ok === true)
|
|
51
|
+
return message.result;
|
|
52
|
+
const hasAuthorityError = message.ok === false && isAuthorityMessageObject(message.error);
|
|
53
|
+
if (!hasAuthorityError) {
|
|
54
|
+
throw new DirectoryAuthorityError("directory_authority_protocol_error", "The directory operation returned an invalid response.");
|
|
55
|
+
}
|
|
56
|
+
const { code, message: explanation, data, } = message.error;
|
|
57
|
+
const hasErrorDescription = typeof code === "string" && typeof explanation === "string";
|
|
58
|
+
if (!hasErrorDescription) {
|
|
59
|
+
throw new DirectoryAuthorityError("directory_authority_protocol_error", "The directory operation returned an invalid error response.");
|
|
60
|
+
}
|
|
61
|
+
throw new DirectoryAuthorityError(code, explanation, isAuthorityMessageObject(data) ? data : undefined);
|
|
62
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type DirectoryAuthorityLocation } from "./protocol.js";
|
|
2
|
+
export declare function runDirectoryAuthorityTask<Input, Result>(input: DirectoryAuthorityLocation & Readonly<{
|
|
3
|
+
input: Input;
|
|
4
|
+
task: (input: Input) => Result | Promise<Result>;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}>): Promise<Result>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { directoryAuthorityScript } from "./bootstrap.js";
|
|
3
|
+
import { DirectoryAuthorityError } from "./errors.js";
|
|
4
|
+
import { assertDirectoryAuthorityLocation, authorityEnvironment, AUTHORITY_MESSAGE_LIMIT, AUTHORITY_STDERR_LIMIT, AUTHORITY_TASK_TIMEOUT_MS, decodeAuthorityResult, encodeAuthorityRequest, } from "./protocol.js";
|
|
5
|
+
export async function runDirectoryAuthorityTask(input) {
|
|
6
|
+
assertDirectoryAuthorityLocation(input);
|
|
7
|
+
const request = encodeAuthorityRequest(input.input);
|
|
8
|
+
const timeoutMs = input.timeoutMs ?? AUTHORITY_TASK_TIMEOUT_MS;
|
|
9
|
+
const hasPositiveDeadline = Number.isFinite(timeoutMs) && timeoutMs > 0;
|
|
10
|
+
if (!hasPositiveDeadline) {
|
|
11
|
+
throw new DirectoryAuthorityError("directory_authority_invalid_timeout", "The directory operation deadline must be a positive finite duration.");
|
|
12
|
+
}
|
|
13
|
+
if (input.signal?.aborted) {
|
|
14
|
+
throw new DirectoryAuthorityError("directory_authority_aborted", "The directory operation was cancelled before it started.");
|
|
15
|
+
}
|
|
16
|
+
const child = spawn(process.execPath, [
|
|
17
|
+
"--input-type=commonjs",
|
|
18
|
+
"-e",
|
|
19
|
+
directoryAuthorityScript("task", AUTHORITY_MESSAGE_LIMIT, input.task),
|
|
20
|
+
], {
|
|
21
|
+
cwd: input.directoryPath,
|
|
22
|
+
env: authorityEnvironment(),
|
|
23
|
+
detached: true,
|
|
24
|
+
windowsHide: true,
|
|
25
|
+
stdio: ["pipe", "pipe", "pipe", input.directoryFd],
|
|
26
|
+
});
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const chunks = [];
|
|
29
|
+
const stderrChunks = [];
|
|
30
|
+
let bytes = 0;
|
|
31
|
+
let stderrBytes = 0;
|
|
32
|
+
let failure;
|
|
33
|
+
const stop = (error) => {
|
|
34
|
+
if (failure)
|
|
35
|
+
return;
|
|
36
|
+
failure = error;
|
|
37
|
+
if (typeof child.pid === "number") {
|
|
38
|
+
try {
|
|
39
|
+
process.kill(-child.pid, "SIGKILL");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// A process which has already exited may no longer own its group.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
child.kill("SIGKILL");
|
|
47
|
+
};
|
|
48
|
+
const onAbort = () => stop(new DirectoryAuthorityError("directory_authority_aborted", "The directory operation was cancelled."));
|
|
49
|
+
const timer = setTimeout(() => stop(new DirectoryAuthorityError("directory_authority_timeout", "The directory operation exceeded its deadline.")), timeoutMs);
|
|
50
|
+
timer.unref();
|
|
51
|
+
input.signal?.addEventListener("abort", onAbort, { once: true });
|
|
52
|
+
if (input.signal?.aborted)
|
|
53
|
+
onAbort();
|
|
54
|
+
child.stdout.on("data", (chunk) => {
|
|
55
|
+
bytes += chunk.length;
|
|
56
|
+
if (bytes > AUTHORITY_MESSAGE_LIMIT) {
|
|
57
|
+
stop(new DirectoryAuthorityError("directory_authority_result_too_large", "The directory operation result exceeds the bridge byte limit."));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
chunks.push(chunk);
|
|
61
|
+
});
|
|
62
|
+
child.stderr.on("data", (chunk) => {
|
|
63
|
+
const remaining = Math.max(AUTHORITY_STDERR_LIMIT - stderrBytes, 0);
|
|
64
|
+
if (remaining === 0)
|
|
65
|
+
return;
|
|
66
|
+
stderrChunks.push(chunk.subarray(0, remaining));
|
|
67
|
+
stderrBytes += Math.min(chunk.length, remaining);
|
|
68
|
+
});
|
|
69
|
+
child.stdin.on("error", () => undefined);
|
|
70
|
+
child.once("error", (error) => {
|
|
71
|
+
failure ??= new DirectoryAuthorityError("directory_authority_unavailable", `The directory operation could not start: ${error.message}`);
|
|
72
|
+
});
|
|
73
|
+
child.once("close", (code, signal) => {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
input.signal?.removeEventListener("abort", onAbort);
|
|
76
|
+
if (failure) {
|
|
77
|
+
reject(failure);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (code !== 0) {
|
|
81
|
+
reject(new DirectoryAuthorityError("directory_authority_failed", "The directory operation process did not complete successfully.", {
|
|
82
|
+
exitCode: code,
|
|
83
|
+
signal,
|
|
84
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8"),
|
|
85
|
+
}));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
resolve(decodeAuthorityResult(Buffer.concat(chunks).toString("utf8")));
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
reject(error);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
child.stdin.end(request);
|
|
96
|
+
});
|
|
97
|
+
}
|
package/docs/plugins.md
CHANGED
|
@@ -300,13 +300,19 @@ Node filesystem APIs do not provide inode compare-and-swap, so a completely
|
|
|
300
300
|
independent writer that ignores this coordination can still race in the final
|
|
301
301
|
existing-file commit window. Consumers that require stronger multi-process
|
|
302
302
|
coordination must place the work root behind a versioned or transactional store.
|
|
303
|
-
Mutation path traversal requires
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
303
|
+
Mutation path traversal requires no-follow directory handles. On Linux the
|
|
304
|
+
plugin anchors operations through procfs. On macOS an isolated Node child
|
|
305
|
+
inherits the held directory handle, verifies its current directory against
|
|
306
|
+
that handle, and performs basename-relative operations from the pinned current
|
|
307
|
+
directory. Each descendant transition is verified before mutation. The parent
|
|
308
|
+
Runtime never changes its current directory, and unavailable authority fails
|
|
309
|
+
closed. Directory inspection uses the same platform-specific authority.
|
|
310
|
+
|
|
311
|
+
The bundled `local-search` plugin holds the selected file or directory through
|
|
312
|
+
a stable descriptor for the complete ripgrep invocation. Linux directory
|
|
313
|
+
searches use procfs; macOS searches inherit the verified current directory of
|
|
314
|
+
an isolated Node child. File content searches use the held input descriptor on
|
|
315
|
+
both platforms. A search fails closed when its authority cannot be established.
|
|
310
316
|
|
|
311
317
|
All output and structured data must be bounded at the producer. The shared SDK
|
|
312
318
|
reserves wrapper headroom by rejecting plugin results above 128 KiB before they
|
|
@@ -316,8 +322,8 @@ and rendered text. A truncated success reports machine-readable truncation
|
|
|
316
322
|
metadata; an operation that cannot produce a truthful bounded result fails
|
|
317
323
|
explicitly.
|
|
318
324
|
|
|
319
|
-
The bundled `exec` plugin
|
|
320
|
-
|
|
325
|
+
The bundled `exec` plugin supports Linux and macOS with non-interactive
|
|
326
|
+
`/bin/bash`. Commands must use utilities available on the host. Its configured working directory is constrained to
|
|
321
327
|
agent-work or workspace roots, but the shell runs with the Runtime process
|
|
322
328
|
permissions and is not an operating-system sandbox.
|
|
323
329
|
|
package/package.json
CHANGED
package/plugins/exec/plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "exec",
|
|
4
4
|
"version": "1.0.0",
|
|
5
|
-
"description": "Execute, observe, and cancel bounded non-interactive
|
|
5
|
+
"description": "Execute, observe, and cancel bounded non-interactive /bin/bash processes on Linux and macOS.",
|
|
6
6
|
"extensions": {
|
|
7
7
|
"ai.abot.runtime": {
|
|
8
8
|
"version": 1,
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"capabilities": {
|
|
20
20
|
"exec": {
|
|
21
21
|
"catalogGroups": ["read", "write", "exec"],
|
|
22
|
-
"description": "Execute one bounded non-interactive
|
|
22
|
+
"description": "Execute one bounded non-interactive /bin/bash command on Linux or macOS. The explicit cwd must resolve inside agent work or the configured workspace; `.` means the agent-work root and `workspace/...` selects the workspace. The shell itself runs with the runtime process permissions and is not an operating-system sandbox. A command still running after the foreground window yields a process ID and cursor that exec_wait can resume without restarting it. Prefer dedicated inspection and file-mutation capabilities when they can express the task.",
|
|
23
23
|
"routingCapability": "filesystem_inspection",
|
|
24
24
|
"developmentRoles": ["inspect", "establish", "verify", "auxiliary"],
|
|
25
25
|
"eventPresentation": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"skills": ["exec_skill"],
|
|
32
32
|
"operations": {
|
|
33
33
|
"execute_command": {
|
|
34
|
-
"summary": "Execute one bounded non-interactive
|
|
34
|
+
"summary": "Execute one bounded non-interactive /bin/bash command on Linux or macOS in an explicit existing agent-work or workspace directory. The cwd boundary is not an OS sandbox. Full-access availability does not authorize servers, GUI automation, installs, service changes, background work, or other sensitive effects.",
|
|
35
35
|
"input": {
|
|
36
36
|
"type": "object",
|
|
37
37
|
"additionalProperties": false,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Exec Skill
|
|
2
2
|
|
|
3
3
|
- Use `exec` for narrow shell-native discovery or verification when a dedicated tool is insufficient.
|
|
4
|
-
- `exec`
|
|
4
|
+
- `exec` is available on Linux and macOS with executable `/bin/bash`; it does not select a Windows shell or another fallback shell. Commands must use the utilities and options available on the host.
|
|
5
5
|
- Resolve `.` and ordinary relative `cwd` values from the configured agent work directory. They are never rewritten to the active Worker working directory. Use `workspace/...` only when the configured workspace is explicitly intended.
|
|
6
6
|
- Always provide an explicit existing `cwd`. For an existing project, use its project path. To create a new project, use an existing parent `cwd` (normally `.`) and create the project path in the command.
|
|
7
7
|
- The cwd is restricted to configured roots, but the shell runs with the runtime process permissions and is not an operating-system sandbox.
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { spawn, type ChildProcessByStdio } from "node:child_process";
|
|
2
|
-
import { constants } from "node:fs";
|
|
3
|
-
import { access } from "node:fs/promises";
|
|
4
2
|
import { randomUUID } from "node:crypto";
|
|
5
3
|
import type { Readable } from "node:stream";
|
|
6
4
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -8,13 +6,13 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
8
6
|
import { sanitizeJsonText } from "../../../src/plugin-sdk/index.js";
|
|
9
7
|
|
|
10
8
|
import { ExecPluginError } from "./errors.js";
|
|
9
|
+
import { assertSupportedShell, EXEC_SHELL } from "./shell-platform.js";
|
|
11
10
|
import type {
|
|
12
11
|
ExecProcessSnapshot,
|
|
13
12
|
ExecStreamSnapshot,
|
|
14
13
|
ExecTerminationReason,
|
|
15
14
|
} from "./types.js";
|
|
16
15
|
|
|
17
|
-
const EXEC_SHELL = "/bin/bash";
|
|
18
16
|
const COMPLETED_PROCESS_RETENTION_MS = 5 * 60_000;
|
|
19
17
|
const MAX_ACTIVE_PROCESSES_PER_SCOPE = 4;
|
|
20
18
|
|
|
@@ -115,23 +113,6 @@ export type ExecProcessManager = Readonly<{
|
|
|
115
113
|
release(processId: string, scope: string): Promise<void>;
|
|
116
114
|
}>;
|
|
117
115
|
|
|
118
|
-
async function assertSupportedShell(): Promise<void> {
|
|
119
|
-
if (process.platform !== "linux") {
|
|
120
|
-
throw new ExecPluginError(
|
|
121
|
-
"exec_platform_unsupported",
|
|
122
|
-
"The exec plugin v1 requires Linux and /bin/bash.",
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
try {
|
|
126
|
-
await access(EXEC_SHELL, constants.X_OK);
|
|
127
|
-
} catch {
|
|
128
|
-
throw new ExecPluginError(
|
|
129
|
-
"exec_shell_unavailable",
|
|
130
|
-
"The exec plugin cannot start because executable /bin/bash is unavailable.",
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
116
|
export function createExecProcessManager(): ExecProcessManager {
|
|
136
117
|
const processes = new Map<string, ManagedExecProcess>();
|
|
137
118
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import { ExecPluginError } from "./errors.js";
|
|
5
|
+
|
|
6
|
+
export const EXEC_SHELL = "/bin/bash";
|
|
7
|
+
|
|
8
|
+
function isSupportedExecPlatform(platform: NodeJS.Platform): boolean {
|
|
9
|
+
return platform === "linux" || platform === "darwin";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function assertSupportedShell(
|
|
13
|
+
platform: NodeJS.Platform = process.platform,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
if (!isSupportedExecPlatform(platform)) {
|
|
16
|
+
throw new ExecPluginError(
|
|
17
|
+
"exec_platform_unsupported",
|
|
18
|
+
"The exec plugin requires Linux or macOS and executable /bin/bash.",
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
await access(EXEC_SHELL, constants.X_OK);
|
|
23
|
+
} catch {
|
|
24
|
+
throw new ExecPluginError(
|
|
25
|
+
"exec_shell_unavailable",
|
|
26
|
+
"The exec plugin cannot start because executable /bin/bash is unavailable.",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|