Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,15 +172,15 @@ Without an API key, images can still be pasted (generating `[Image #N]` placehol
| `OPENAI_BASE_URL` | OpenAI API base URL |
| `JINA_API_KEY` | Jina AI API key (required for WebSearch/WebFetch tools) |
| `BASH_AGENT_HOME` | Override session storage directory (default: `$HOME`) |
| `BASH_AGENT_BASH_MODE` | Bash tool permissions as 4 octal rwx digits: `system/external/network/workspace`; each digit uses `4=read,2=write,1=execute` (default: `0467`) |
| `BASH_AGENT_BASH_MODE` | Bash and local file tool permissions as 4 octal rwx digits: `system/external/network/workspace`; each digit uses `4=read,2=write,1=execute` (default: `0467`) |
| `DESCRIBE_API_KEY` | Image description API key (defaults to GLM-4V-Flash, free) |
| `DESCRIBE_MODEL` | Image description model name (default: `glm-4v-flash`) |
| `DESCRIBE_BASE_URL` | Image description API base URL (default: `https://open.bigmodel.cn/api/paas/v4`) |
| `THINKING_BUDGET` | Thinking token budget (default: `2048`) |

## Bash Tool Permission Mode
## Bash and Local File Tool Permission Mode

`BASH_AGENT_BASH_MODE` controls what the `Bash` tool is allowed to touch. It is a 4-digit octal string:
`BASH_AGENT_BASH_MODE` controls what the `Bash`, `Read`, `Write`, `Edit`, `Glob`, and `Grep` tools are allowed to touch. It is a 4-digit octal string:

```text
system external network workspace
Expand Down Expand Up @@ -212,6 +212,8 @@ export BASH_AGENT_BASH_MODE=0457 # allow network execute
export BASH_AGENT_BASH_MODE=7777 # fully open, trusted environments only
```

Local file tools derive equivalent access probes from their paths: `Read`, `Grep`, and `Glob` require read access, while `Write` and `Edit` require write access. A missing path is checked against the current workspace. A pathless `Glob` with an absolute pattern or `..` segment fails closed as a system read.

Invalid values fail closed to `0000`. The model is expressed like Linux-style `rwx` permission bits. Full classification rules, recommended settings, and the shared block message are documented in [`docs/bash-tool-policy.md`](docs/bash-tool-policy.md).

DP algorithm environment variables:
Expand Down Expand Up @@ -300,7 +302,7 @@ Scopes: global (`~/.bash-agent/`) and project (current directory).
| Document | Description |
| --- | --- |
| [`docs/architecture.md`](docs/architecture.md) | Architecture, layering, protocols |
| [`docs/bash-tool-policy.md`](docs/bash-tool-policy.md) | Bash tool permission mode, classification rules, recommended settings |
| [`docs/bash-tool-policy.md`](docs/bash-tool-policy.md) | Bash and local file tool permission mode, classification rules, recommended settings |
| [`docs/tools.md`](docs/tools.md) | 13 built-in tool references |
| [`docs/compact-analysis.md`](docs/compact-analysis.md) | Compaction algorithm derivation |
| [`docs/sessions.md`](docs/sessions.md) | Session files and recovery |
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,16 +175,16 @@ tcode goagent -p openai -m gpt-4o
| `OPENAI_BASE_URL` | OpenAI API base URL |
| `JINA_API_KEY` | Jina AI API key(WebSearch/WebFetch 工具需要) |
| `BASH_AGENT_HOME` | 覆盖 session 存储目录(默认 `$HOME`) |
| `BASH_AGENT_BASH_MODE` | Bash 工具权限,4 位八进制 `system/external/network/workspace`;每位 `4=read,2=write,1=execute`(默认 `0467`) |
| `BASH_AGENT_BASH_MODE` | Bash 与本地文件工具权限,4 位八进制 `system/external/network/workspace`;每位 `4=read,2=write,1=execute`(默认 `0467`) |
| `DESCRIBE_API_KEY` | 图片描述 API key(默认使用 GLM-4V-Flash,免费) |
| `DESCRIBE_MODEL` | 图片描述模型名(默认 `glm-4v-flash`) |
| `DESCRIBE_BASE_URL` | 图片描述 API 基础地址(默认 `https://open.bigmodel.cn/api/paas/v4`) |
| `THINKING` | 覆盖思考模式:`enabled` / `disabled`(CLI `--thinking`) |
| `EFFORT` | 覆盖思考力度:`low` / `medium` / `high`(CLI `--effort`) |

## Bash 工具权限模式
## Bash 与本地文件工具权限模式

`BASH_AGENT_BASH_MODE` 控制 `Bash` 工具允许访问的范围。它是一个 4 位八进制字符串:
`BASH_AGENT_BASH_MODE` 控制 `Bash`、`Read`、`Write`、`Edit`、`Glob` 和 `Grep` 工具允许访问的范围。它是一个 4 位八进制字符串:

```text
system external network workspace
Expand Down Expand Up @@ -216,6 +216,8 @@ export BASH_AGENT_BASH_MODE=0457 # 允许 network execute
export BASH_AGENT_BASH_MODE=7777 # 全开,仅建议受信环境
```

本地文件工具按路径生成等价访问探针:`Read`、`Grep`、`Glob` 按读取检查,`Write`、`Edit` 按写入检查;未指定路径时按当前工作区检查。无路径的 `Glob` 若模式为绝对路径或包含 `..`,会失败关闭为系统读取检查。

如果值非法,会 fail-closed 为 `0000`。这个模型的表达方式类似 Linux 文件权限的 `rwx` 位。更完整的分类规则、推荐配置和错误文案见 [`docs/bash-tool-policy.md`](docs/bash-tool-policy.md)。

DP 算法相关环境变量:
Expand Down Expand Up @@ -304,7 +306,7 @@ $$
| 文档 | 说明 |
| --- | --- |
| [`docs/architecture.md`](docs/architecture.md) | 架构设计、分层、协议 |
| [`docs/bash-tool-policy.md`](docs/bash-tool-policy.md) | Bash 工具权限模式、分类规则、推荐配置 |
| [`docs/bash-tool-policy.md`](docs/bash-tool-policy.md) | Bash 与本地文件工具权限模式、分类规则、推荐配置 |
| [`docs/tools.md`](docs/tools.md) | 13 个内置工具详细说明 |
| [`docs/compact-analysis.md`](docs/compact-analysis.md) | 压缩算法完整推导 |
| [`docs/sessions.md`](docs/sessions.md) | Session 文件结构与恢复 |
Expand Down
4 changes: 2 additions & 2 deletions c/agent.c
Original file line number Diff line number Diff line change
Expand Up @@ -2033,7 +2033,7 @@ char *agent_handle_sub_agent(Agent *agent, const char *prompt,
/* 显示启动通知(事件已手动写入,只推显示队列不重复写事件) */
DisplayMessage *dm = malloc(sizeof(DisplayMessage));
*dm = display_msg_sub_agent_start(sub_session_id, description);
if (!store_event_stream_json_enabled()) {
if (!store_event_stream_json_enabled() && agent->display_queue) {
mq_push(agent->display_queue, dm);
} else {
display_message_free(dm);
Expand Down Expand Up @@ -2160,7 +2160,7 @@ int agent_handle_sub_agent_result(Agent *agent, const char *session_id,
DisplayMessage *dm = malloc(sizeof(DisplayMessage));
*dm = display_msg_sub_agent_result(session_id, status, thinking,
text, in_tokens, out_tokens);
if (!store_event_stream_json_enabled()) {
if (!store_event_stream_json_enabled() && agent->display_queue) {
mq_push(agent->display_queue, dm);
} else {
display_message_free(dm);
Expand Down
28 changes: 27 additions & 1 deletion c/cagent.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,27 @@ static void usage(const char *prog) {
fprintf(stderr, " *_API_KEY API key (ANTHROPIC / OPENAI / DEEPSEEK auto-detected)\n");
fprintf(stderr, " *_BASE_URL Override API base URL (ANTHROPIC / OPENAI)\n");
fprintf(stderr, " BASH_AGENT_HOME Override session storage (default: $HOME)\n");
fprintf(stderr, " BASH_AGENT_BASH_MODE Bash tool permissions (default: 0467)\n");
fprintf(stderr, " BASH_AGENT_BASH_MODE Bash and local file tool permissions (default: 0467)\n");
}

static char *g_paste_session_dir = NULL;
static void image_paste_wrapper(char **out, size_t *outlen) {
agent_image_clipboard_paste(g_paste_session_dir, out, outlen);
}

/* 崩溃信号处理:恢复终端 raw mode 后重新发出信号以生成 core dump。
* 仅使用 async-signal-safe 函数。 */
static void crash_handler(int sig) {
linenoiseRestoreTerminal();
/* 输出换行,避免终端残留 prompt 行 */
const char msg[] = "\n\rcagent: fatal signal received, terminal restored.\n\r";
write(STDERR_FILENO, msg, sizeof(msg) - 1);
/* 恢复默认 handler 并重新发送信号,让 OS 生成 core dump */
signal(sig, SIG_DFL);
raise(sig);
_exit(128 + sig); /* 理论上不会到达 */
}

int main(int argc, char *argv[]) {
srand((unsigned int)time(NULL) ^ (unsigned int)getpid());
/* 默认值 */
Expand Down Expand Up @@ -235,6 +248,19 @@ int main(int argc, char *argv[]) {
* 可能触发 SIGPIPE,默认行为是终止整个进程 */
signal(SIGPIPE, SIG_IGN);

/* SIGSEGV / SIGABRT handler:进程崩溃时恢复终端 raw mode,避免
* linenoise 设置的 raw mode 残留导致终端排版错乱。
* handler 内仅调用 async-signal-safe 函数(tcsetattr / write / _exit)。 */
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = crash_handler;
sa.sa_flags = SA_RESETHAND; /* 恢复默认行为,允许 core dump */
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGABRT, &sa, NULL);
sigaction(SIGBUS, &sa, NULL);
sigaction(SIGFPE, &sa, NULL);

/* 初始化 curl */
curl_global_init(CURL_GLOBAL_DEFAULT);

Expand Down
1 change: 1 addition & 0 deletions c/json.c
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ JsonParse json_parse(const char *src, size_t *pos) {
}

JsonParse json_parse_root(const char *src) {
if (!src) return make_err("null input");
size_t pos = 0;
JsonParse p = json_parse_internal(src, &pos);
if (p.error) return p;
Expand Down
3 changes: 3 additions & 0 deletions c/msgqueue.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ void mq_destroy(MsgQueue *q) {
}

int mq_push(MsgQueue *q, void *data) {
if (!q || !data) return -1;
MQNode *node = malloc(sizeof(MQNode));
if (!node) return -1;
node->data = data;
Expand All @@ -62,6 +63,7 @@ int mq_push(MsgQueue *q, void *data) {
}

int mq_pop(MsgQueue *q, void **data_out) {
if (!q || !data_out) return -1;
pthread_mutex_lock(&q->mutex);
while (!q->head && !q->closed) {
pthread_cond_wait(&q->cond, &q->mutex);
Expand All @@ -83,6 +85,7 @@ int mq_pop(MsgQueue *q, void **data_out) {
}

int mq_try_pop(MsgQueue *q, void **data_out) {
if (!q || !data_out) return -1;
pthread_mutex_lock(&q->mutex);
if (!q->head) {
pthread_mutex_unlock(&q->mutex);
Expand Down
82 changes: 80 additions & 2 deletions c/test_classify.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@

extern void bash_classify_required_mode(const char *cmd, char out[5]);

typedef struct {
char *output;
int exit_code;
} ToolResult;
extern ToolResult native_file_mode_guard(const char *name, const char *input_json);
extern void tool_result_free(ToolResult *r);

static int g_pass = 0, g_fail = 0;

static void t(const char *label, const char *cmd, const char *expect) {
Expand All @@ -32,13 +39,15 @@ static void t(const char *label, const char *cmd, const char *expect) {
#define T(cmd, expect) t(cmd, cmd, expect)

int main(void) {
char cwd[1024], b1[2048], b2[2048], b3[2048], b4[2048], b5[2048];
char cwd[1024], home[1024], b1[2048], b2[2048], b3[2048], b4[2048], b5[2048];

if (getcwd(cwd, sizeof(cwd))) {
for (char *p = cwd; *p; p++) *p = tolower((unsigned char)*p);
} else {
cwd[0] = '\0';
}
snprintf(home, sizeof(home), "/bash-agent-test-home");
setenv("BASH_AGENT_HOME", home, 1);

printf("=== workspace absolute path ===\n");
snprintf(b1, sizeof(b1), "ls %s/src/agent.sh", cwd);
Expand All @@ -64,8 +73,13 @@ int main(void) {
printf("\n=== external ===\n");
T("echo hi > ~/note.txt", "0200");

printf("\n=== /tmp whitelist ===\n");
printf("\n=== trusted internal paths ===\n");
T("cat > /tmp/test.go << EOF", "0004");
T("cat /tmp-other/file", "0400");
snprintf(b1, sizeof(b1), "cat %s/.bash-agent/projects/session/file", home);
t("project storage read", b1, "0004");
snprintf(b1, sizeof(b1), "cat %s/.bash-agent/projects-copy/session/file", home);
t("project storage adjacent path", b1, "0400");

printf("\n=== /dev/null ===\n");
T("echo hi >/dev/null", "0004");
Expand Down Expand Up @@ -99,6 +113,70 @@ int main(void) {
T("cat > /tmp/test.sh << 'EOF' && bash /tmp/test.sh", "0001");
T("git add -A && git commit -m fix && git push", "0023");

printf("\n=== native file guards ===\n");
setenv("BASH_AGENT_BASH_MODE", "0467", 1);
ToolResult guard = native_file_mode_guard("Read", "{\"path\":\"src/agent.sh\"}");
if (!guard.output) { printf("\033[32m✓ PASS\033[0m: workspace Read allowed\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: workspace Read allowed: %s\n", guard.output); g_fail++; }
tool_result_free(&guard);
guard = native_file_mode_guard("Read", "{\"path\":\"/etc/hosts\"}");
if (guard.output && guard.exit_code == 1) { printf("\033[32m✓ PASS\033[0m: system Read blocked\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: system Read blocked\n"); g_fail++; }
tool_result_free(&guard);
guard = native_file_mode_guard("Write", "{\"path\":\"~/note.txt\"}");
if (guard.output && guard.exit_code == 1) { printf("\033[32m✓ PASS\033[0m: external Write blocked\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: external Write blocked\n"); g_fail++; }
tool_result_free(&guard);
setenv("BASH_AGENT_BASH_MODE", "0004", 1);
guard = native_file_mode_guard("Read", "{\"path\":\"/tmp/native-file-mode-test\"}");
if (!guard.output) { printf("\033[32m✓ PASS\033[0m: tmp Read allowed\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: tmp Read allowed: %s\n", guard.output); g_fail++; }
tool_result_free(&guard);
guard = native_file_mode_guard("Write", "{\"path\":\"/tmp/native-file-mode-test\"}");
if (!guard.output) { printf("\033[32m✓ PASS\033[0m: tmp Write allowed\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: tmp Write allowed: %s\n", guard.output); g_fail++; }
tool_result_free(&guard);
guard = native_file_mode_guard("Edit", "{\"path\":\"/tmp/native-file-mode-test\",\"old_string\":\"a\",\"new_string\":\"b\"}");
if (!guard.output) { printf("\033[32m✓ PASS\033[0m: tmp Edit allowed\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: tmp Edit allowed: %s\n", guard.output); g_fail++; }
tool_result_free(&guard);
guard = native_file_mode_guard("Grep", "{\"pattern\":\"needle\",\"path\":\"/tmp\"}");
if (!guard.output) { printf("\033[32m✓ PASS\033[0m: tmp Grep allowed\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: tmp Grep allowed: %s\n", guard.output); g_fail++; }
tool_result_free(&guard);
guard = native_file_mode_guard("Glob", "{\"pattern\":\"*.txt\",\"path\":\"/tmp\"}");
if (!guard.output) { printf("\033[32m✓ PASS\033[0m: tmp Glob allowed\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: tmp Glob allowed: %s\n", guard.output); g_fail++; }
tool_result_free(&guard);
guard = native_file_mode_guard("Read", "{\"path\":\"/tmp-other/native-file-mode-test\"}");
if (guard.output && guard.exit_code == 1) { printf("\033[32m✓ PASS\033[0m: tmp adjacent blocked\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: tmp adjacent blocked\n"); g_fail++; }
tool_result_free(&guard);
setenv("BASH_AGENT_BASH_MODE", "0465", 1);
guard = native_file_mode_guard("Edit", "{\"path\":\"src/agent.sh\"}");
if (guard.output && guard.exit_code == 1) { printf("\033[32m✓ PASS\033[0m: workspace Edit without write blocked\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: workspace Edit without write blocked\n"); g_fail++; }
tool_result_free(&guard);
setenv("BASH_AGENT_BASH_MODE", "0467", 1);
guard = native_file_mode_guard("Grep", "{\"pattern\":\"needle\"}");
if (!guard.output) { printf("\033[32m✓ PASS\033[0m: default Grep allowed\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: default Grep allowed: %s\n", guard.output); g_fail++; }
tool_result_free(&guard);
guard = native_file_mode_guard("Glob", "{\"pattern\":\"/etc/*\"}");
if (guard.output && guard.exit_code == 1) { printf("\033[32m✓ PASS\033[0m: absolute default Glob blocked\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: absolute default Glob blocked\n"); g_fail++; }
tool_result_free(&guard);
guard = native_file_mode_guard("Glob", "{\"pattern\":\"../*\"}");
if (guard.output && guard.exit_code == 1) { printf("\033[32m✓ PASS\033[0m: parent default Glob blocked\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: parent default Glob blocked\n"); g_fail++; }
tool_result_free(&guard);
setenv("BASH_AGENT_BASH_MODE", "invalid", 1);
guard = native_file_mode_guard("Read", "{\"path\":\"src/agent.sh\"}");
if (guard.output && guard.exit_code == 1) { printf("\033[32m✓ PASS\033[0m: invalid mode fails closed\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: invalid mode fails closed\n"); g_fail++; }
tool_result_free(&guard);
unsetenv("BASH_AGENT_BASH_MODE");

printf("\n==============================\n");
printf("Results: \033[32m%d passed\033[0m, \033[31m%d failed\033[0m\n", g_pass, g_fail);
printf("==============================\n");
Expand Down
Loading
Loading