Skip to content
Merged
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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ test-go-e2e: build-go

test-rust:
cd rust && cargo check
cargo test --manifest-path rust/Cargo.toml native_file_mode_guard_matches_bash

test-rust-e2e: build-rust
AGENT=./dist/rustagent bash tests/test.sh
Expand Down
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. Paths under `/tmp` and `$BASH_AGENT_HOME/.bash-agent/projects` that do not contain `..` are trusted internal paths, but any path containing `..` is checked as a system path to prevent traversal bypasses such as `/tmp/../...`. 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` 按写入检查;未指定路径时按当前工作区检查。`/tmp` 与 `$BASH_AGENT_HOME/.bash-agent/projects` 下不含 `..` 的路径是可信内部目录,但含 `..` 的路径一律按系统路径检查,防止借由 `/tmp/../...` 穿越绕过权限。无路径的 `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
2 changes: 1 addition & 1 deletion c/cagent.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ 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;
Expand Down
96 changes: 94 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,15 @@ 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");
T("cat /tmp/../etc/hosts", "4000");
T("echo hi > /tmp/../etc/native-file-mode-test", "2000");
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 +115,82 @@ 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);
guard = native_file_mode_guard("Read", "{\"path\":\"/tmp/../etc/hosts\"}");
if (guard.output && guard.exit_code == 1) { printf("\033[32m✓ PASS\033[0m: tmp traversal Read blocked\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: tmp traversal Read blocked\n"); g_fail++; }
tool_result_free(&guard);
guard = native_file_mode_guard("Write", "{\"path\":\"/tmp/../etc/native-file-mode-test\"}");
if (guard.output && guard.exit_code == 1) { printf("\033[32m✓ PASS\033[0m: tmp traversal Write blocked\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: tmp traversal Write blocked\n"); g_fail++; }
tool_result_free(&guard);
guard = native_file_mode_guard("Edit", "{\"path\":\"/tmp/../etc/hosts\"}");
if (guard.output && guard.exit_code == 1) { printf("\033[32m✓ PASS\033[0m: tmp traversal Edit blocked\n"); g_pass++; }
else { printf("\033[31m✗ FAIL\033[0m: tmp traversal Edit 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