wtagent 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +83 -0
- package/docs/technical-design.md +838 -0
- package/package.json +55 -0
- package/src/browser/cdp-browser.js +170 -0
- package/src/browser/chatgpt-web-adapter.js +717 -0
- package/src/browser/fake-web-model-adapter.js +86 -0
- package/src/browser/mode-selection.js +179 -0
- package/src/browser/native-login.js +49 -0
- package/src/cli/at-files.js +93 -0
- package/src/cli/main.js +629 -0
- package/src/cli/render-events.js +367 -0
- package/src/platform/chrome-discovery.js +94 -0
- package/src/platform/paths.js +73 -0
- package/src/policy/path-guard.js +114 -0
- package/src/policy/policy-engine.js +277 -0
- package/src/protocol/markers.js +64 -0
- package/src/protocol/prompt-builder.js +152 -0
- package/src/protocol/xml-protocol.js +333 -0
- package/src/runtime/agent-runtime.js +636 -0
- package/src/session/agent-session.js +569 -0
- package/src/session/canonical-transcript.js +145 -0
- package/src/session/session-export.js +202 -0
- package/src/session/task-session.js +5 -0
- package/src/shared/errors.js +52 -0
- package/src/shared/limits.js +31 -0
- package/src/tools/default-tools.js +555 -0
- package/src/tools/process-manager.js +116 -0
- package/src/tools/process-utils.js +31 -0
- package/src/tools/registry.js +129 -0
- package/src/tools/safe-env.js +67 -0
- package/src/tools/terminal-exec.js +118 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ToolValidationError } from "../shared/errors.js";
|
|
2
|
+
|
|
3
|
+
export class ToolRegistry {
|
|
4
|
+
#definitions = new Map();
|
|
5
|
+
|
|
6
|
+
register(definition) {
|
|
7
|
+
if (!definition?.name || typeof definition.execute !== "function") {
|
|
8
|
+
throw new Error("Tool definition requires name and execute.");
|
|
9
|
+
}
|
|
10
|
+
if (this.#definitions.has(definition.name)) {
|
|
11
|
+
throw new Error(`Duplicate tool: ${definition.name}`);
|
|
12
|
+
}
|
|
13
|
+
this.#definitions.set(definition.name, definition);
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
list() {
|
|
18
|
+
return [...this.#definitions.values()].map((definition) => ({
|
|
19
|
+
name: definition.name,
|
|
20
|
+
description: definition.description,
|
|
21
|
+
inputDescription: definition.inputDescription,
|
|
22
|
+
risk: definition.risk ?? "execute",
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
validate(toolCall) {
|
|
27
|
+
const definition = this.#definitions.get(toolCall.name);
|
|
28
|
+
if (!definition) {
|
|
29
|
+
throw new ToolValidationError(`Unknown tool: ${toolCall.name}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const result = definition.inputSchema.safeParse(toolCall.args ?? {});
|
|
33
|
+
if (!result.success) {
|
|
34
|
+
throw new ToolValidationError(
|
|
35
|
+
`Invalid arguments for ${toolCall.name}: ${result.error.message}`,
|
|
36
|
+
{ details: result.error.issues },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
...toolCall,
|
|
42
|
+
args: result.data,
|
|
43
|
+
definition,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async execute(preparedCall, context) {
|
|
48
|
+
const execution = Promise.resolve().then(
|
|
49
|
+
() => preparedCall.definition.execute(preparedCall.args, context),
|
|
50
|
+
);
|
|
51
|
+
const timeoutMs = preparedCall.definition.managesTimeout
|
|
52
|
+
? null
|
|
53
|
+
: Number(context.toolTimeoutMs);
|
|
54
|
+
let timer = null;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
let outcome;
|
|
58
|
+
if (Number.isSafeInteger(timeoutMs) && timeoutMs > 0) {
|
|
59
|
+
outcome = await Promise.race([
|
|
60
|
+
execution.then(
|
|
61
|
+
(result) => ({ type: "result", result }),
|
|
62
|
+
(error) => ({ type: "error", error }),
|
|
63
|
+
),
|
|
64
|
+
new Promise((resolve) => {
|
|
65
|
+
timer = setTimeout(
|
|
66
|
+
() => resolve({ type: "timeout" }),
|
|
67
|
+
timeoutMs,
|
|
68
|
+
);
|
|
69
|
+
timer.unref?.();
|
|
70
|
+
}),
|
|
71
|
+
]);
|
|
72
|
+
} else {
|
|
73
|
+
outcome = await execution.then(
|
|
74
|
+
(result) => ({ type: "result", result }),
|
|
75
|
+
(error) => ({ type: "error", error }),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (outcome.type === "timeout") {
|
|
80
|
+
execution.catch(() => undefined);
|
|
81
|
+
const completionUnknown = preparedCall.definition.risk !== "read";
|
|
82
|
+
return {
|
|
83
|
+
callId: preparedCall.id,
|
|
84
|
+
name: preparedCall.name,
|
|
85
|
+
ok: false,
|
|
86
|
+
message: completionUnknown
|
|
87
|
+
? (
|
|
88
|
+
`Tool exceeded the ${timeoutMs}ms runtime guard. `
|
|
89
|
+
+ "The operation may still complete because it does not support "
|
|
90
|
+
+ "hard cancellation; completion is unknown."
|
|
91
|
+
)
|
|
92
|
+
: `Tool exceeded the ${timeoutMs}ms runtime guard.`,
|
|
93
|
+
meta: {
|
|
94
|
+
timedOut: true,
|
|
95
|
+
completionUnknown,
|
|
96
|
+
recoverable: true,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (outcome.type === "error") {
|
|
102
|
+
throw outcome.error;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const result = outcome.result;
|
|
106
|
+
return {
|
|
107
|
+
callId: preparedCall.id,
|
|
108
|
+
name: preparedCall.name,
|
|
109
|
+
ok: Boolean(result.ok),
|
|
110
|
+
message: result.message ?? "",
|
|
111
|
+
data: result.data,
|
|
112
|
+
stdout: result.stdout,
|
|
113
|
+
stderr: result.stderr,
|
|
114
|
+
meta: result.meta,
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return {
|
|
118
|
+
callId: preparedCall.id,
|
|
119
|
+
name: preparedCall.name,
|
|
120
|
+
ok: false,
|
|
121
|
+
message: error.message,
|
|
122
|
+
stderr: error.stack,
|
|
123
|
+
};
|
|
124
|
+
} finally {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const SAFE_ENV_NAMES = new Set([
|
|
2
|
+
"PATH",
|
|
3
|
+
"HOME",
|
|
4
|
+
"USER",
|
|
5
|
+
"LOGNAME",
|
|
6
|
+
"SHELL",
|
|
7
|
+
"LANG",
|
|
8
|
+
"LC_ALL",
|
|
9
|
+
"LC_CTYPE",
|
|
10
|
+
"TMP",
|
|
11
|
+
"TEMP",
|
|
12
|
+
"TMPDIR",
|
|
13
|
+
"SYSTEMROOT",
|
|
14
|
+
"COMSPEC",
|
|
15
|
+
"PATHEXT",
|
|
16
|
+
"WINDIR",
|
|
17
|
+
"TERM",
|
|
18
|
+
"COLORTERM",
|
|
19
|
+
"CI",
|
|
20
|
+
"NODE_ENV",
|
|
21
|
+
"NO_COLOR",
|
|
22
|
+
"FORCE_COLOR",
|
|
23
|
+
// Windows-specific locations that common tools (npm, git, node, ...)
|
|
24
|
+
// need to locate config, caches, and the user profile.
|
|
25
|
+
"APPDATA",
|
|
26
|
+
"LOCALAPPDATA",
|
|
27
|
+
"USERPROFILE",
|
|
28
|
+
"PROGRAMDATA",
|
|
29
|
+
"PROGRAMFILES",
|
|
30
|
+
"PROGRAMFILES(X86)",
|
|
31
|
+
"PROGRAMW6432",
|
|
32
|
+
"PUBLIC",
|
|
33
|
+
"HOMEDRIVE",
|
|
34
|
+
"HOMEPATH",
|
|
35
|
+
"NUMBER_OF_PROCESSORS",
|
|
36
|
+
"OS",
|
|
37
|
+
"PROCESSOR_ARCHITECTURE",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const SENSITIVE_NAME = /(TOKEN|SECRET|PASSWORD|PASSWD|API[_-]?KEY|PRIVATE[_-]?KEY|COOKIE|CREDENTIAL|AUTH)/i;
|
|
41
|
+
|
|
42
|
+
export function buildToolEnvironment({ inheritSensitive = false } = {}) {
|
|
43
|
+
if (inheritSensitive) {
|
|
44
|
+
return { ...process.env };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const environment = {};
|
|
48
|
+
for (const [name, value] of Object.entries(process.env)) {
|
|
49
|
+
if (value == null || SENSITIVE_NAME.test(name)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// Environment variable names are case-insensitive on Windows; normalize
|
|
53
|
+
// to upper case for the allow-list lookup while preserving the original
|
|
54
|
+
// key in the returned object.
|
|
55
|
+
const upper = name.toUpperCase();
|
|
56
|
+
if (
|
|
57
|
+
SAFE_ENV_NAMES.has(upper)
|
|
58
|
+
|| name.startsWith("npm_")
|
|
59
|
+
|| name.startsWith("NPM_")
|
|
60
|
+
|| name.startsWith("XDG_")
|
|
61
|
+
) {
|
|
62
|
+
environment[name] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return environment;
|
|
66
|
+
}
|
|
67
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { buildToolEnvironment } from "./safe-env.js";
|
|
3
|
+
import { killProcessTree } from "./process-utils.js";
|
|
4
|
+
|
|
5
|
+
function appendLimited(current, chunk, maxBytes) {
|
|
6
|
+
const next = current + chunk.toString("utf8");
|
|
7
|
+
if (Buffer.byteLength(next, "utf8") <= maxBytes) {
|
|
8
|
+
return { text: next, truncated: false };
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
text: next.slice(0, maxBytes),
|
|
12
|
+
truncated: true,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function runProgram({
|
|
17
|
+
program,
|
|
18
|
+
argv = [],
|
|
19
|
+
cwd,
|
|
20
|
+
timeoutMs,
|
|
21
|
+
maxOutputBytes,
|
|
22
|
+
inheritSensitiveEnv = false,
|
|
23
|
+
onOutput,
|
|
24
|
+
}) {
|
|
25
|
+
return await new Promise((resolve) => {
|
|
26
|
+
const startedAt = Date.now();
|
|
27
|
+
let stdout = "";
|
|
28
|
+
let stderr = "";
|
|
29
|
+
let stdoutTruncated = false;
|
|
30
|
+
let stderrTruncated = false;
|
|
31
|
+
let timedOut = false;
|
|
32
|
+
let settled = false;
|
|
33
|
+
let terminationGuard = null;
|
|
34
|
+
let outputQueue = Promise.resolve();
|
|
35
|
+
|
|
36
|
+
const child = spawn(program, argv, {
|
|
37
|
+
cwd,
|
|
38
|
+
env: buildToolEnvironment({ inheritSensitive: inheritSensitiveEnv }),
|
|
39
|
+
shell: false,
|
|
40
|
+
detached: process.platform !== "win32",
|
|
41
|
+
windowsHide: false,
|
|
42
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const finish = async (result) => {
|
|
46
|
+
if (settled) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
settled = true;
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
clearTimeout(terminationGuard);
|
|
52
|
+
await outputQueue;
|
|
53
|
+
resolve({
|
|
54
|
+
...result,
|
|
55
|
+
stdout,
|
|
56
|
+
stderr,
|
|
57
|
+
timedOut,
|
|
58
|
+
truncated: stdoutTruncated || stderrTruncated,
|
|
59
|
+
durationMs: Date.now() - startedAt,
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const enqueueOutput = (output) => {
|
|
64
|
+
outputQueue = outputQueue
|
|
65
|
+
.then(() => onOutput?.(output))
|
|
66
|
+
.catch(() => undefined);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
child.stdout.on("data", (chunk) => {
|
|
70
|
+
const appended = appendLimited(stdout, chunk, maxOutputBytes);
|
|
71
|
+
stdout = appended.text;
|
|
72
|
+
stdoutTruncated ||= appended.truncated;
|
|
73
|
+
enqueueOutput({ stream: "stdout", chunk: chunk.toString("utf8") });
|
|
74
|
+
});
|
|
75
|
+
child.stderr.on("data", (chunk) => {
|
|
76
|
+
const appended = appendLimited(stderr, chunk, maxOutputBytes);
|
|
77
|
+
stderr = appended.text;
|
|
78
|
+
stderrTruncated ||= appended.truncated;
|
|
79
|
+
enqueueOutput({ stream: "stderr", chunk: chunk.toString("utf8") });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
child.once("error", (error) => {
|
|
83
|
+
void finish({
|
|
84
|
+
ok: false,
|
|
85
|
+
exitCode: null,
|
|
86
|
+
signal: null,
|
|
87
|
+
error,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
child.once("close", (exitCode, signal) => {
|
|
91
|
+
void finish({
|
|
92
|
+
ok: !timedOut && exitCode === 0,
|
|
93
|
+
exitCode,
|
|
94
|
+
signal,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const timer = setTimeout(async () => {
|
|
99
|
+
timedOut = true;
|
|
100
|
+
await killProcessTree(child.pid);
|
|
101
|
+
|
|
102
|
+
if (settled) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
terminationGuard = setTimeout(() => {
|
|
107
|
+
void finish({
|
|
108
|
+
ok: false,
|
|
109
|
+
exitCode: null,
|
|
110
|
+
signal: null,
|
|
111
|
+
completionUnknown: true,
|
|
112
|
+
});
|
|
113
|
+
}, 1_000);
|
|
114
|
+
terminationGuard.unref?.();
|
|
115
|
+
}, timeoutMs);
|
|
116
|
+
timer.unref?.();
|
|
117
|
+
});
|
|
118
|
+
}
|