mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-11 15:17:24 +08:00
fix: bypass GateGuard file gates in subagents (#1710)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Everything Claude Code (ECC) — Agent Instructions
|
||||
|
||||
This is a **production-ready AI coding plugin** providing 48 specialized agents, 182 skills, 68 commands, and automated hook workflows for software development.
|
||||
This is a **production-ready AI coding plugin** providing 48 specialized agents, 185 skills, 68 commands, and automated hook workflows for software development.
|
||||
|
||||
**Version:** 2.0.0-rc.1
|
||||
|
||||
@@ -146,7 +146,7 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
|
||||
|
||||
```
|
||||
agents/ — 48 specialized subagents
|
||||
skills/ — 182 workflow skills and domain knowledge
|
||||
skills/ — 185 workflow skills and domain knowledge
|
||||
commands/ — 68 slash commands
|
||||
hooks/ — Trigger-based automations
|
||||
rules/ — Always-follow guidelines (common + per-language)
|
||||
|
||||
@@ -350,7 +350,7 @@ If you stacked methods, clean up in this order:
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
**That's it!** You now have access to 48 agents, 182 skills, and 68 legacy command shims.
|
||||
**That's it!** You now have access to 48 agents, 185 skills, and 68 legacy command shims.
|
||||
|
||||
### Dashboard GUI
|
||||
|
||||
@@ -1338,7 +1338,7 @@ The configuration is automatically detected from `.opencode/opencode.json`.
|
||||
|---------|-------------|----------|--------|
|
||||
| Agents | PASS: 48 agents | PASS: 12 agents | **Claude Code leads** |
|
||||
| Commands | PASS: 68 commands | PASS: 31 commands | **Claude Code leads** |
|
||||
| Skills | PASS: 182 skills | PASS: 37 skills | **Claude Code leads** |
|
||||
| Skills | PASS: 185 skills | PASS: 37 skills | **Claude Code leads** |
|
||||
| Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** |
|
||||
| Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** |
|
||||
| MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** |
|
||||
@@ -1443,7 +1443,7 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e
|
||||
|---------|------------|------------|-----------|----------|
|
||||
| **Agents** | 48 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |
|
||||
| **Commands** | 68 | Shared | Instruction-based | 31 |
|
||||
| **Skills** | 182 | Shared | 10 (native format) | 37 |
|
||||
| **Skills** | 185 | Shared | 10 (native format) | 37 |
|
||||
| **Hook Events** | 8 types | 15 types | None yet | 11 types |
|
||||
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks |
|
||||
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions |
|
||||
|
||||
@@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
**完成!** 你现在可以使用 48 个代理、182 个技能和 68 个命令。
|
||||
**完成!** 你现在可以使用 48 个代理、185 个技能和 68 个命令。
|
||||
|
||||
### multi-* 命令需要额外配置
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Everything Claude Code (ECC) — 智能体指令
|
||||
|
||||
这是一个**生产就绪的 AI 编码插件**,提供 48 个专业代理、182 项技能、68 条命令以及自动化钩子工作流,用于软件开发。
|
||||
这是一个**生产就绪的 AI 编码插件**,提供 48 个专业代理、185 项技能、68 条命令以及自动化钩子工作流,用于软件开发。
|
||||
|
||||
**版本:** 2.0.0-rc.1
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
|
||||
```
|
||||
agents/ — 48 个专业子代理
|
||||
skills/ — 182 个工作流技能和领域知识
|
||||
skills/ — 185 个工作流技能和领域知识
|
||||
commands/ — 68 个斜杠命令
|
||||
hooks/ — 基于触发的自动化
|
||||
rules/ — 始终遵循的指导方针(通用 + 每种语言)
|
||||
|
||||
@@ -224,7 +224,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
**搞定!** 你现在可以使用 48 个智能体、182 项技能和 68 个命令了。
|
||||
**搞定!** 你现在可以使用 48 个智能体、185 项技能和 68 个命令了。
|
||||
|
||||
***
|
||||
|
||||
@@ -1134,7 +1134,7 @@ opencode
|
||||
|---------|-------------|----------|--------|
|
||||
| 智能体 | PASS: 48 个 | PASS: 12 个 | **Claude Code 领先** |
|
||||
| 命令 | PASS: 68 个 | PASS: 31 个 | **Claude Code 领先** |
|
||||
| 技能 | PASS: 182 项 | PASS: 37 项 | **Claude Code 领先** |
|
||||
| 技能 | PASS: 185 项 | PASS: 37 项 | **Claude Code 领先** |
|
||||
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
|
||||
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
|
||||
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
|
||||
@@ -1242,7 +1242,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
|
||||
|---------|------------|------------|-----------|----------|
|
||||
| **智能体** | 48 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
|
||||
| **命令** | 68 | 共享 | 基于指令 | 31 |
|
||||
| **技能** | 182 | 共享 | 10 (原生格式) | 37 |
|
||||
| **技能** | 185 | 共享 | 10 (原生格式) | 37 |
|
||||
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
|
||||
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |
|
||||
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
|
||||
|
||||
@@ -376,6 +376,21 @@ function withRecoveryHint(message, hookIds = [EDIT_WRITE_HOOK_ID]) {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function isSubagentInvocation(data) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
data.agent_id,
|
||||
data.agentId,
|
||||
data.parent_tool_use_id,
|
||||
data.parentToolUseId
|
||||
];
|
||||
|
||||
return candidates.some(candidate => typeof candidate === 'string' && candidate.trim());
|
||||
}
|
||||
|
||||
// --- Deny helper ---
|
||||
|
||||
function denyResult(reason, options = {}) {
|
||||
@@ -422,6 +437,7 @@ function run(rawInput) {
|
||||
// Normalize: case-insensitive matching via lookup map
|
||||
const TOOL_MAP = { edit: 'Edit', write: 'Write', multiedit: 'MultiEdit', bash: 'Bash' };
|
||||
const toolName = TOOL_MAP[rawToolName.toLowerCase()] || rawToolName;
|
||||
const inSubagent = isSubagentInvocation(data);
|
||||
|
||||
if (toolName === 'Edit' || toolName === 'Write') {
|
||||
const filePath = toolInput.file_path || '';
|
||||
@@ -429,6 +445,10 @@ function run(rawInput) {
|
||||
return rawInput; // allow
|
||||
}
|
||||
|
||||
if (inSubagent) {
|
||||
return rawInput; // parent session already passed the first-touch file gate
|
||||
}
|
||||
|
||||
if (!isChecked(filePath)) {
|
||||
if (!markChecked(filePath)) {
|
||||
return allowWithStateWarning();
|
||||
@@ -440,6 +460,10 @@ function run(rawInput) {
|
||||
}
|
||||
|
||||
if (toolName === 'MultiEdit') {
|
||||
if (inSubagent) {
|
||||
return rawInput; // parent session already passed the first-touch file gate
|
||||
}
|
||||
|
||||
const edits = toolInput.edits || [];
|
||||
for (const edit of edits) {
|
||||
const filePath = edit.file_path || '';
|
||||
|
||||
@@ -32,7 +32,6 @@ Flox environments are defined in `.flox/env/manifest.toml` and activated with `f
|
||||
- `$FLOX_ENV_CACHE` — Persistent local storage for caches, venvs, data (survives rebuilds)
|
||||
- `$FLOX_ENV_PROJECT` — Project root directory (where `.flox/` lives)
|
||||
|
||||
|
||||
## Essential Commands
|
||||
|
||||
```bash
|
||||
|
||||
@@ -131,7 +131,7 @@ Options: `--size <pt>` (default: 68), `--color <hex>` (default: 8E8E93), `--weig
|
||||
| `carbon` | Carbon | 2400+ | IBM design language |
|
||||
| `heroicons` | HeroIcons | 1200+ | Tailwind CSS companion |
|
||||
|
||||
Browse all: https://icon-sets.iconify.design/
|
||||
Browse all: <https://icon-sets.iconify.design/>
|
||||
|
||||
## Scripts Reference
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Architecture and implementation patterns for building modules with the **tinystr
|
||||
|
||||
## How It Works
|
||||
|
||||
The tinystruct framework treats any method annotated with `@Action` as a routable endpoint for both terminal and web environments. Applications are created by extending `AbstractApplication`, which provides core lifecycle hooks like `init()` and access to the request `Context`.
|
||||
The tinystruct framework treats any method annotated with `@Action` as a routable endpoint for both terminal and web environments. Applications are created by extending `AbstractApplication`, which provides core lifecycle hooks like `init()` and access to the request `Context`.
|
||||
|
||||
Routing is handled by the `ActionRegistry`, which automatically maps path segments to method arguments and injects dependencies. For data-only services, the native `Builder` component should be used for JSON serialization to maintain a zero-dependency footprint. The framework also includes a utility in `ApplicationManager` to bootstrap the project's execution environment by generating the `bin/dispatcher` script.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Use the testing patterns described here when writing units tests for your tinyst
|
||||
|
||||
## How It Works
|
||||
|
||||
Testing tinystruct applications requires a specific setup to ensure framework-level features like annotation processing and configuration management are active. By creating a new instance of your application and passing it a `Settings` object in the `setUp()` method, you trigger the `init()` lifecycle. This ensures all `@Action` methods are discovered and registered.
|
||||
Testing tinystruct applications requires a specific setup to ensure framework-level features like annotation processing and configuration management are active. By creating a new instance of your application and passing it a `Settings` object in the `setUp()` method, you trigger the `init()` lifecycle. This ensures all `@Action` methods are discovered and registered.
|
||||
|
||||
Because the `ActionRegistry` is a singleton, it is critical to maintain isolation between tests by properly initializing your application state before each test execution, preventing side effects from leaking across the test suite.
|
||||
|
||||
|
||||
@@ -981,6 +981,168 @@ function runTests() {
|
||||
assert.ok(fs.existsSync(freshState), 'fresh state file should remain');
|
||||
})) passed++; else failed++;
|
||||
|
||||
function runFreshSessionEdit(filePath, extra = {}) {
|
||||
return runHook({
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: filePath, old_string: 'a', new_string: 'b' },
|
||||
session_id: 'subagent-fresh-session',
|
||||
...extra
|
||||
}, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' });
|
||||
}
|
||||
|
||||
function runFreshSessionBash(command, extra = {}) {
|
||||
return runBashHook({
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command },
|
||||
session_id: 'subagent-fresh-session',
|
||||
...extra
|
||||
}, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' });
|
||||
}
|
||||
|
||||
// --- Test 30: top-level Edit denies; subagent Edit allows ---
|
||||
clearState();
|
||||
if (test('A/B: same Edit denies at top level and allows with agent_id', () => {
|
||||
const topLevel = runFreshSessionEdit('/src/subagent-edit.js');
|
||||
const topOut = parseOutput(topLevel.stdout);
|
||||
assert.ok(topOut, 'top-level edit should produce JSON output');
|
||||
assert.strictEqual(topOut.hookSpecificOutput.permissionDecision, 'deny');
|
||||
|
||||
clearState();
|
||||
const subagent = runFreshSessionEdit('/src/subagent-edit.js', { agent_id: 'agent-abc-123' });
|
||||
const subOut = parseOutput(subagent.stdout);
|
||||
assert.ok(subOut, 'subagent edit should produce JSON output');
|
||||
assert.ok(!subOut.hookSpecificOutput || subOut.hookSpecificOutput.permissionDecision !== 'deny',
|
||||
'subagent edit should bypass the first-touch file gate');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 31: top-level Write denies; subagent Write allows ---
|
||||
clearState();
|
||||
if (test('A/B: same Write denies at top level and allows with agent_id', () => {
|
||||
const topLevel = runHook({
|
||||
tool_name: 'Write',
|
||||
tool_input: { file_path: '/src/subagent-write.js', content: 'module.exports = {};' },
|
||||
session_id: 'subagent-fresh-session'
|
||||
}, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' });
|
||||
const topOut = parseOutput(topLevel.stdout);
|
||||
assert.ok(topOut, 'top-level write should produce JSON output');
|
||||
assert.strictEqual(topOut.hookSpecificOutput.permissionDecision, 'deny');
|
||||
|
||||
clearState();
|
||||
const subagent = runHook({
|
||||
tool_name: 'Write',
|
||||
tool_input: { file_path: '/src/subagent-write.js', content: 'module.exports = {};' },
|
||||
session_id: 'subagent-fresh-session',
|
||||
agent_id: 'agent-abc-123'
|
||||
}, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' });
|
||||
const subOut = parseOutput(subagent.stdout);
|
||||
assert.ok(subOut, 'subagent write should produce JSON output');
|
||||
assert.ok(!subOut.hookSpecificOutput || subOut.hookSpecificOutput.permissionDecision !== 'deny',
|
||||
'subagent write should bypass the first-touch file gate');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 32: top-level MultiEdit denies; subagent MultiEdit allows ---
|
||||
clearState();
|
||||
if (test('A/B: same MultiEdit denies at top level and allows with agent_id', () => {
|
||||
const edits = [
|
||||
{ file_path: '/src/subagent-multi-a.js', old_string: 'a', new_string: 'b' },
|
||||
{ file_path: '/src/subagent-multi-b.js', old_string: 'c', new_string: 'd' }
|
||||
];
|
||||
|
||||
const topLevel = runHook({
|
||||
tool_name: 'MultiEdit',
|
||||
tool_input: { edits },
|
||||
session_id: 'subagent-fresh-session'
|
||||
}, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' });
|
||||
const topOut = parseOutput(topLevel.stdout);
|
||||
assert.ok(topOut, 'top-level MultiEdit should produce JSON output');
|
||||
assert.strictEqual(topOut.hookSpecificOutput.permissionDecision, 'deny');
|
||||
|
||||
clearState();
|
||||
const subagent = runHook({
|
||||
tool_name: 'MultiEdit',
|
||||
tool_input: { edits },
|
||||
session_id: 'subagent-fresh-session',
|
||||
agent_id: 'agent-abc-123'
|
||||
}, { CLAUDE_SESSION_ID: '', ECC_SESSION_ID: '' });
|
||||
const subOut = parseOutput(subagent.stdout);
|
||||
assert.ok(subOut, 'subagent MultiEdit should produce JSON output');
|
||||
assert.ok(!subOut.hookSpecificOutput || subOut.hookSpecificOutput.permissionDecision !== 'deny',
|
||||
'subagent MultiEdit should bypass the first-touch file gate');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 33: Bash stays gated inside subagents ---
|
||||
clearState();
|
||||
if (test('routine Bash remains gated in subagent context', () => {
|
||||
const result = runFreshSessionBash('pwd', { agent_id: 'agent-abc-123' });
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, 'subagent Bash should produce JSON output');
|
||||
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('current user request'));
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 34: destructive Bash stays gated inside subagents ---
|
||||
clearState();
|
||||
if (test('destructive Bash remains gated in subagent context', () => {
|
||||
const result = runFreshSessionBash('rm -rf /tmp/demo-path', { agent_id: 'agent-abc-123' });
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, 'subagent destructive Bash should produce JSON output');
|
||||
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('Destructive command detected'));
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 35: parent tool IDs also mark subagent context ---
|
||||
clearState();
|
||||
if (test('parent_tool_use_id and parentToolUseId mark subagent file edits', () => {
|
||||
const snake = runFreshSessionEdit('/src/subagent-parent-snake.js', { parent_tool_use_id: 'toolu_parent_01' });
|
||||
const snakeOut = parseOutput(snake.stdout);
|
||||
assert.ok(snakeOut, 'snake-case parent marker should produce JSON output');
|
||||
assert.ok(!snakeOut.hookSpecificOutput || snakeOut.hookSpecificOutput.permissionDecision !== 'deny',
|
||||
'parent_tool_use_id should bypass the first-touch file gate');
|
||||
|
||||
clearState();
|
||||
const camel = runFreshSessionEdit('/src/subagent-parent-camel.js', { parentToolUseId: 'toolu_parent_02' });
|
||||
const camelOut = parseOutput(camel.stdout);
|
||||
assert.ok(camelOut, 'camel-case parent marker should produce JSON output');
|
||||
assert.ok(!camelOut.hookSpecificOutput || camelOut.hookSpecificOutput.permissionDecision !== 'deny',
|
||||
'parentToolUseId should bypass the first-touch file gate');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 36: only non-empty string markers count ---
|
||||
clearState();
|
||||
if (test('empty and non-string subagent markers do not bypass file gates', () => {
|
||||
const cases = [
|
||||
['empty', { agent_id: '' }],
|
||||
['whitespace', { agent_id: ' ' }],
|
||||
['numeric', { agent_id: 12345 }],
|
||||
['null', { agent_id: null }]
|
||||
];
|
||||
|
||||
for (const [name, extra] of cases) {
|
||||
clearState();
|
||||
const result = runFreshSessionEdit(`/src/subagent-marker-${name}.js`, extra);
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, `${name} marker should produce JSON output`);
|
||||
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny',
|
||||
`${name} marker should not bypass the first-touch file gate`);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 37: two sequential subagent Edits on different files pass ---
|
||||
clearState();
|
||||
if (test('two sequential subagent Edits on different files both pass', () => {
|
||||
const first = runFreshSessionEdit('/src/subagent-seq-a.js', { agent_id: 'agent-seq' });
|
||||
const firstOut = parseOutput(first.stdout);
|
||||
assert.ok(firstOut, 'first subagent edit should produce JSON output');
|
||||
assert.ok(!firstOut.hookSpecificOutput || firstOut.hookSpecificOutput.permissionDecision !== 'deny',
|
||||
'first subagent edit should pass');
|
||||
|
||||
const second = runFreshSessionEdit('/src/subagent-seq-b.js', { agent_id: 'agent-seq' });
|
||||
const secondOut = parseOutput(second.stdout);
|
||||
assert.ok(secondOut, 'second subagent edit should produce JSON output');
|
||||
assert.ok(!secondOut.hookSpecificOutput || secondOut.hookSpecificOutput.permissionDecision !== 'deny',
|
||||
'second subagent edit should pass even on a new file');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Cleanup only the temp directory created by this test file.
|
||||
try {
|
||||
if (fs.existsSync(stateDir)) {
|
||||
|
||||
Reference in New Issue
Block a user