autoclaw 1.3.3 → 1.3.4
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 +2 -0
- package/README.zh-CN.md +2 -0
- package/dist/agent.js +58 -6
- package/dist/batch.js +2 -0
- package/dist/index.js +5 -4
- package/dist/tools/core.js +27 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -194,6 +194,7 @@ AutoClaw uses a hierarchical configuration system.
|
|
|
194
194
|
- `model`: Default model to use.
|
|
195
195
|
- `maxSteps`: Max LLM turns per task before the agent stops (default: `25`).
|
|
196
196
|
- `shellTimeout`: Shell command timeout in milliseconds (default: `120000`).
|
|
197
|
+
- `taskTimeoutMs`: Whole-task wall-clock timeout in milliseconds (off by default; aborts in-flight API calls and stops with `timeout` status).
|
|
197
198
|
- `shell`: Force a shell for `execute_shell_command` (`bash`, `powershell`, `cmd`, `sh`; default: auto-detect — Git Bash > PowerShell > cmd on Windows).
|
|
198
199
|
- `tavilyApiKey`: API Key for Tavily Web Search.
|
|
199
200
|
- `smtpHost`, `smtpPort`, `smtpUser`, `smtpPass`, `smtpFrom`: SMTP Email settings.
|
|
@@ -214,6 +215,7 @@ Create a file at `.autoclaw/setting.json`:
|
|
|
214
215
|
- `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`: main LLM settings.
|
|
215
216
|
- `AUTOCLOW_PROVIDER`: provider preset used when `-P` is not passed.
|
|
216
217
|
- `AUTOCLOW_MAX_STEPS`, `AUTOCLOW_SHELL_TIMEOUT`: reliability limits (max LLM turns per task; shell timeout in ms).
|
|
218
|
+
- `AUTOCLOW_TASK_TIMEOUT_MS`: whole-task wall-clock timeout in ms.
|
|
217
219
|
- `AUTOCLOW_SHELL`: force the shell for shell commands (`bash`, `powershell`, `cmd`, `sh`).
|
|
218
220
|
- `AUTOCLOW_INCLUDE_USAGE`: set to `1`/`true` to request token usage from the API (opt-in).
|
|
219
221
|
- `TAVILY_API_KEY`, `SMTP_HOST`/`SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`, `FEISHU_WEBHOOK`/`FEISHU_KEYWORD`, `DINGTALK_WEBHOOK`/`DINGTALK_KEYWORD`, `WECOM_WEBHOOK`/`WECOM_KEYWORD`: tool credentials as an alternative to setup.
|
package/README.zh-CN.md
CHANGED
|
@@ -196,6 +196,7 @@ AutoClaw 使用层级配置系统。
|
|
|
196
196
|
- `model`: 默认使用的模型。
|
|
197
197
|
- `maxSteps`: 单任务最大 LLM 轮数,超出后自动停止 (默认: `25`)。
|
|
198
198
|
- `shellTimeout`: Shell 命令超时时间(毫秒)(默认: `120000`)。
|
|
199
|
+
- `taskTimeoutMs`: 单任务整体墙钟超时(毫秒,默认关闭;会中断进行中的 API 调用并以 `timeout` 状态停止)。
|
|
199
200
|
- `shell`: 强制 `execute_shell_command` 使用的 shell (`bash`、`powershell`、`cmd`、`sh`;默认自动检测——Windows 上优先 Git Bash > PowerShell > cmd)。
|
|
200
201
|
- `tavilyApiKey`: Tavily 网页搜索的 API 密钥。
|
|
201
202
|
- `smtpHost`, `smtpPort`, `smtpUser`, `smtpPass`, `smtpFrom`: SMTP 邮件设置。
|
|
@@ -216,6 +217,7 @@ AutoClaw 使用层级配置系统。
|
|
|
216
217
|
- `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`: 主模型设置。
|
|
217
218
|
- `AUTOCLOW_PROVIDER`: 未传 `-P` 时使用的 provider 预设。
|
|
218
219
|
- `AUTOCLOW_MAX_STEPS`, `AUTOCLOW_SHELL_TIMEOUT`: 稳定性限制(单任务最大轮数;Shell 超时毫秒数)。
|
|
220
|
+
- `AUTOCLOW_TASK_TIMEOUT_MS`: 单任务整体墙钟超时(毫秒)。
|
|
219
221
|
- `AUTOCLOW_SHELL`: 强制 shell 命令使用的 shell (`bash`、`powershell`、`cmd`、`sh`)。
|
|
220
222
|
- `AUTOCLOW_INCLUDE_USAGE`: 设为 `1`/`true` 时向 API 请求 token 用量(可选开启)。
|
|
221
223
|
- `TAVILY_API_KEY`, `SMTP_HOST`/`SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`, `FEISHU_WEBHOOK`/`FEISHU_KEYWORD`, `DINGTALK_WEBHOOK`/`DINGTALK_KEYWORD`, `WECOM_WEBHOOK`/`WECOM_KEYWORD`: 工具凭据,可作为 setup 的替代方式。
|
package/dist/agent.js
CHANGED
|
@@ -104,6 +104,13 @@ RULES OF ENGAGEMENT:
|
|
|
104
104
|
async chat(userInput) {
|
|
105
105
|
this.messages.push({ role: "user", content: userInput });
|
|
106
106
|
const maxSteps = Number(this.config?.maxSteps || process.env.AUTOCLOW_MAX_STEPS || DEFAULT_MAX_STEPS);
|
|
107
|
+
const taskTimeoutMs = Number(this.config?.taskTimeoutMs || process.env.AUTOCLOW_TASK_TIMEOUT_MS || 0);
|
|
108
|
+
const deadline = taskTimeoutMs > 0 ? Date.now() + taskTimeoutMs : Number.POSITIVE_INFINITY;
|
|
109
|
+
const abortController = new AbortController();
|
|
110
|
+
const abortTimer = taskTimeoutMs > 0
|
|
111
|
+
? setTimeout(() => abortController.abort(new Error(`task wall-clock timeout after ${taskTimeoutMs}ms`)), taskTimeoutMs)
|
|
112
|
+
: null;
|
|
113
|
+
const startedAt = Date.now();
|
|
107
114
|
let active = true;
|
|
108
115
|
let step = 0;
|
|
109
116
|
let status = 'completed';
|
|
@@ -120,6 +127,13 @@ RULES OF ENGAGEMENT:
|
|
|
120
127
|
}
|
|
121
128
|
break;
|
|
122
129
|
}
|
|
130
|
+
if (Date.now() > deadline) {
|
|
131
|
+
status = 'timeout';
|
|
132
|
+
if (!this.jsonMode) {
|
|
133
|
+
console.log(chalk.yellow(`\n[TaskTimeout] Wall-clock limit of ${taskTimeoutMs}ms reached; stopping.`));
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
123
137
|
this.trimOldToolResults();
|
|
124
138
|
step++;
|
|
125
139
|
const spinner = this.jsonMode
|
|
@@ -133,13 +147,13 @@ RULES OF ENGAGEMENT:
|
|
|
133
147
|
stream = await withRetry(async () => this.client.chat.completions.create({
|
|
134
148
|
model: this.model,
|
|
135
149
|
messages: this.messages,
|
|
136
|
-
tools: getToolDefinitions(),
|
|
150
|
+
tools: getToolDefinitions(this.config),
|
|
137
151
|
tool_choice: "auto",
|
|
138
152
|
stream: true,
|
|
139
153
|
// Not every OpenAI-compatible provider accepts stream_options;
|
|
140
154
|
// usage tracking is therefore opt-in only.
|
|
141
155
|
...(this.config?.includeUsage ? { stream_options: { include_usage: true } } : {})
|
|
142
|
-
}), {
|
|
156
|
+
}, { signal: abortController.signal }), {
|
|
143
157
|
onRetry: (err, nextAttempt, delayMs) => {
|
|
144
158
|
spinner.text = `API error (${err.message}); retrying in ${Math.round(delayMs / 1000)}s (attempt ${nextAttempt})...`;
|
|
145
159
|
}
|
|
@@ -149,8 +163,14 @@ RULES OF ENGAGEMENT:
|
|
|
149
163
|
spinner.fail('Error during processing');
|
|
150
164
|
if (!this.jsonMode)
|
|
151
165
|
console.error(chalk.red(error.message));
|
|
152
|
-
|
|
153
|
-
|
|
166
|
+
if (abortController.signal.aborted) {
|
|
167
|
+
status = 'timeout';
|
|
168
|
+
errorMessage = `task wall-clock timeout after ${taskTimeoutMs}ms`;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
status = 'error';
|
|
172
|
+
errorMessage = error.message;
|
|
173
|
+
}
|
|
154
174
|
active = false;
|
|
155
175
|
break;
|
|
156
176
|
}
|
|
@@ -231,8 +251,14 @@ RULES OF ENGAGEMENT:
|
|
|
231
251
|
spinner.fail('Error during processing');
|
|
232
252
|
if (!this.jsonMode)
|
|
233
253
|
console.error(chalk.red(error.message));
|
|
234
|
-
|
|
235
|
-
|
|
254
|
+
if (abortController.signal.aborted) {
|
|
255
|
+
status = 'timeout';
|
|
256
|
+
errorMessage = `task wall-clock timeout after ${taskTimeoutMs}ms`;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
status = 'error';
|
|
260
|
+
errorMessage = error.message;
|
|
261
|
+
}
|
|
236
262
|
active = false;
|
|
237
263
|
break;
|
|
238
264
|
}
|
|
@@ -351,6 +377,8 @@ RULES OF ENGAGEMENT:
|
|
|
351
377
|
this.emitEvent({ event: 'usage', step, ...totalUsage });
|
|
352
378
|
}
|
|
353
379
|
}
|
|
380
|
+
if (abortTimer)
|
|
381
|
+
clearTimeout(abortTimer);
|
|
354
382
|
const result = {
|
|
355
383
|
status,
|
|
356
384
|
steps: step,
|
|
@@ -358,9 +386,33 @@ RULES OF ENGAGEMENT:
|
|
|
358
386
|
...(errorMessage ? { error: errorMessage } : {}),
|
|
359
387
|
...(sawUsage ? { usage: totalUsage } : {})
|
|
360
388
|
};
|
|
389
|
+
this.appendRunLog(userInput, result, startedAt);
|
|
361
390
|
this.emitEvent({ event: 'run_end', ...result });
|
|
362
391
|
return result;
|
|
363
392
|
}
|
|
393
|
+
// Best-effort local run history: ~/.autoclaw/logs/runs.jsonl, one line
|
|
394
|
+
// per run, for post-hoc debugging of unattended batches. Logging must
|
|
395
|
+
// never fail a run.
|
|
396
|
+
appendRunLog(userInput, result, startedAt) {
|
|
397
|
+
try {
|
|
398
|
+
const dir = path.join(os.homedir(), '.autoclaw', 'logs');
|
|
399
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
400
|
+
const line = JSON.stringify({
|
|
401
|
+
time: new Date().toISOString(),
|
|
402
|
+
model: this.model,
|
|
403
|
+
task: String(userInput).slice(0, 200),
|
|
404
|
+
status: result.status,
|
|
405
|
+
steps: result.steps,
|
|
406
|
+
...(result.error ? { error: result.error.slice(0, 300) } : {}),
|
|
407
|
+
...(result.usage ? { usage: result.usage } : {}),
|
|
408
|
+
durationMs: Date.now() - startedAt
|
|
409
|
+
});
|
|
410
|
+
fs.appendFileSync(path.join(dir, 'runs.jsonl'), line + '\n');
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
// ignore
|
|
414
|
+
}
|
|
415
|
+
}
|
|
364
416
|
// Every turn resends the full history, so early large tool results
|
|
365
417
|
// dominate context growth. Keep the most recent results intact and bound
|
|
366
418
|
// older ones to a short excerpt (full output stays on disk via /view when
|
package/dist/batch.js
CHANGED
|
@@ -25,6 +25,8 @@ export function parseManifest(raw) {
|
|
|
25
25
|
const entry = { lineNo, id, task: parsed.task.trim() };
|
|
26
26
|
if (typeof parsed.maxSteps === 'number' && parsed.maxSteps > 0)
|
|
27
27
|
entry.maxSteps = parsed.maxSteps;
|
|
28
|
+
if (typeof parsed.taskTimeoutMs === 'number' && parsed.taskTimeoutMs > 0)
|
|
29
|
+
entry.taskTimeoutMs = parsed.taskTimeoutMs;
|
|
28
30
|
if (typeof parsed.model === 'string' && parsed.model.trim())
|
|
29
31
|
entry.model = parsed.model.trim();
|
|
30
32
|
if (typeof parsed.provider === 'string' && parsed.provider.trim())
|
package/dist/index.js
CHANGED
|
@@ -46,7 +46,7 @@ dotenv.config({ path: GLOBAL_ENV_FILE });
|
|
|
46
46
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
47
47
|
// In dist/index.js, package.json is usually up one level in the root
|
|
48
48
|
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
49
|
-
let version = '1.3.
|
|
49
|
+
let version = '1.3.4';
|
|
50
50
|
try {
|
|
51
51
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
52
52
|
version = pkg.version;
|
|
@@ -615,7 +615,8 @@ async function runBatchCommand(manifestPath, globalOptions, cmdOptions) {
|
|
|
615
615
|
...fullConfig,
|
|
616
616
|
// The results file is the machine-readable contract in batch mode.
|
|
617
617
|
jsonMode: false,
|
|
618
|
-
maxSteps: entry.maxSteps ?? fullConfig.maxSteps
|
|
618
|
+
maxSteps: entry.maxSteps ?? fullConfig.maxSteps,
|
|
619
|
+
taskTimeoutMs: entry.taskTimeoutMs ?? fullConfig.taskTimeoutMs
|
|
619
620
|
};
|
|
620
621
|
const agent = new Agent(apiKey, taskBaseURL, taskModel, taskConfig);
|
|
621
622
|
const start = Date.now();
|
|
@@ -667,9 +668,9 @@ async function runChat(queryParts, options) {
|
|
|
667
668
|
}
|
|
668
669
|
const result = await agent.chat(initialQuery);
|
|
669
670
|
// Headless mode exit — the exit code is the orchestrator-facing outcome:
|
|
670
|
-
// 0 completed, 1 hard failure, 2 step cap
|
|
671
|
+
// 0 completed, 1 hard failure, 2 unfinished (step cap or wall-clock timeout).
|
|
671
672
|
if (!options.interactive) {
|
|
672
|
-
process.exit(result.status === 'completed' ? 0 : result.status === 'max_steps' ? 2 : 1);
|
|
673
|
+
process.exit(result.status === 'completed' ? 0 : result.status === 'max_steps' || result.status === 'timeout' ? 2 : 1);
|
|
673
674
|
}
|
|
674
675
|
}
|
|
675
676
|
// Main chat loop
|
package/dist/tools/core.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from 'path';
|
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { execShellCommand } from '../shell.js';
|
|
6
|
+
import * as os from 'os';
|
|
6
7
|
const DEFAULT_SHELL_TIMEOUT_MS = 120000;
|
|
7
8
|
const SHELL_MAX_BUFFER = 10 * 1024 * 1024;
|
|
8
9
|
// Bounded reads keep a huge file from exhausting memory or the model context;
|
|
@@ -27,6 +28,24 @@ export function matchDangerousPattern(command) {
|
|
|
27
28
|
const hit = DANGEROUS_PATTERNS.find(p => p.re.test(command));
|
|
28
29
|
return hit ? hit.label : null;
|
|
29
30
|
}
|
|
31
|
+
// An unattended agent reads web pages and repos; a prompt injection must not
|
|
32
|
+
// end up reading AutoClaw's own credential stores (or classic .env files)
|
|
33
|
+
// and exfiltrating them. Blocked for read_file/write_file unless
|
|
34
|
+
// --allow-dangerous. execute_shell_command is not pattern-restricted here.
|
|
35
|
+
export function isSensitivePath(p) {
|
|
36
|
+
const norm = path.normalize(String(p)).toLowerCase();
|
|
37
|
+
const protectedFiles = [
|
|
38
|
+
path.join(os.homedir(), '.autoclaw', 'setting.json').toLowerCase(),
|
|
39
|
+
path.join(os.homedir(), '.autoclaw', '.env').toLowerCase(),
|
|
40
|
+
path.join(process.cwd(), '.autoclaw', 'setting.json').toLowerCase()
|
|
41
|
+
];
|
|
42
|
+
if (protectedFiles.includes(norm))
|
|
43
|
+
return true;
|
|
44
|
+
return path.basename(norm).startsWith('.env');
|
|
45
|
+
}
|
|
46
|
+
function sensitivePathBlock(p, verb) {
|
|
47
|
+
return `Error: ${p} is blocked by AutoClaw safety policy (credential/secret store). It was NOT ${verb === 'read' ? 'read' : 'written'}. If this task genuinely requires it, restart AutoClaw with --allow-dangerous.`;
|
|
48
|
+
}
|
|
30
49
|
export const ShellTool = {
|
|
31
50
|
name: "Shell Execution",
|
|
32
51
|
definition: {
|
|
@@ -107,7 +126,10 @@ export const ReadFileTool = {
|
|
|
107
126
|
}
|
|
108
127
|
}
|
|
109
128
|
},
|
|
110
|
-
handler: async (args) => {
|
|
129
|
+
handler: async (args, config) => {
|
|
130
|
+
if (!config?.allowDangerous && isSensitivePath(args.path)) {
|
|
131
|
+
return sensitivePathBlock(args.path, 'read');
|
|
132
|
+
}
|
|
111
133
|
let fh;
|
|
112
134
|
try {
|
|
113
135
|
fh = await fs.open(args.path, 'r');
|
|
@@ -151,7 +173,10 @@ export const WriteFileTool = {
|
|
|
151
173
|
}
|
|
152
174
|
}
|
|
153
175
|
},
|
|
154
|
-
handler: async (args) => {
|
|
176
|
+
handler: async (args, config) => {
|
|
177
|
+
if (!config?.allowDangerous && isSensitivePath(args.path)) {
|
|
178
|
+
return sensitivePathBlock(args.path, 'write');
|
|
179
|
+
}
|
|
155
180
|
try {
|
|
156
181
|
await fs.mkdir(path.dirname(args.path), { recursive: true });
|
|
157
182
|
await fs.writeFile(args.path, args.content, 'utf-8');
|