Skip to content
Open
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,10 @@ dist-test/
# Desktop sub-package residual workspace files
packages/desktop/pnpm-lock.yaml
packages/desktop/pnpm-workspace.yaml

# Defensive: ignore any stray test temp dirs that may slip into the workspace.
.test-*
# OS / editor cruft
Thumbs.db
.idea/
.vscode/
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
每次执行完以后都要补充测试文件确保实际行为与预期相符
修改过程中发现错误,如果是本次范围就修改(包括测试),否则要在最后指出
在用户的最新的一条消息除非有显式命令(执行方案、修改代码等)要求修改代码,否则绝对不改代码,之前要求修改的指令全部不算数,别再根据之前的上下文或者当前不确定的指令猜是不是要直接修改代码了
设计方案后,须深入解释每一步的理由
设计方案后,须深入解释每一步的理由
仅允许使用简短注释
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
不允许假设“这是未来需要扩展的”,所以现在就不做,应该贴合用户的实际要求
禁止局部短视实现:不允许仅为了“当前调用能跑通”而写死临时逻辑、硬编码、破坏原有接口契约、或绕过已有模块
不允许总是有阶段性计划,分阶段完成很容易导致过程产生一堆没用的死代码
不许兼容、兜底旧代码
每次执行完以后都要补充测试文件确保实际行为与预期相符
修改过程中发现错误,如果是本次范围就修改(包括测试),否则要在最后指出
在用户的最新的一条消息除非有显式命令(执行方案、修改代码等)要求修改代码,否则绝对不改代码,之前要求修改的指令全部不算数,别再根据之前的上下文或者当前不确定的指令猜是不是要直接修改代码了
设计方案后,须深入解释每一步的理由
设计方案后,须深入解释每一步的理由
仅允许使用简短注释
7 changes: 4 additions & 3 deletions docs/subagent.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,17 @@ maxSteps: 180

### plan

只读代码研究 + 规划 Agent,可执行命令来验证环境:
只读代码研究 + 规划 Agent。**只允许只读工具**和 `submit_plan`(用于提交实现计划等待用户审批),不允许执行命令或写文件。计划提交后 session 会自动切换到 `build` profile。

```yaml
name: plan
description: 只读代码研究和规划
tools: [read_file, search_files, search_code, execute_command, fetch_url, tool_search]
readonly: true
tools: [read_file, search_files, search_code, fetch_url, tool_search, submit_plan, dispatch_agent]
maxSteps: 180
```

> 注意:`plan` profile 自身不设置 `permissionMode`。在 plan 模式下,写工具会被 `plan/planModeGateHook`(注册在 `tool.approval.pre`,priority -1000)拒绝,仅 `submit_plan` 与 `dispatch_agent` 放行。`dispatch_agent` 由 `plan/planSubagentWhitelistHook` 进一步限制为只能派发 `explore` 子代理。

---

## 执行流程
Expand Down
7 changes: 4 additions & 3 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ interface ToolVisibilityPolicy {
|------|------|------|
| 1 | **RuleEngine** | 规则引擎匹配,支持 glob 模式匹配工具名和参数,按优先级排序 |
| 2 | **ReadonlyWhitelist** | 只读工具自动放行(read_file, search_code, search_files, fetch_url, web_search, dispatch_agent, todo_write) |
| 3 | **PermissionMode** | 权限模式判断:`plan`(只允许只读)、`bypass`(全部放行)、`acceptEdits`(非破坏性工具放行)、`default`(继续下一层) |
| 3 | **PermissionMode** | 权限模式判断:`bypass`(全部放行)、`acceptEdits`(非破坏性工具放行)、`default`(继续下一层)。`plan` 模式由独立的 `plan/planModeGateHook` 在 Layer 4 强制,不在此层处理 |
| 4 | **HookPreToolUse** | 钩子决策,可返回 allow/deny/ask/continue,支持 `modifiedInput` 修改参数 |
| 5 | **UserConfirmation** | 异步用户确认,支持 allow/deny/always/never 四种响应,always/never 会持久化为规则 |
| 6 | **AuditLog** | 每一层决策后记录审计日志,通过 `tool.approval.post` 钩子发出 |
Expand All @@ -132,14 +132,15 @@ interface ToolVisibilityPolicy {
### 权限模式

```typescript
type PermissionMode = 'default' | 'acceptEdits' | 'plan' | 'bypass';
type PermissionMode = 'default' | 'acceptEdits' | 'bypass';
```

- `default`:逐层审批,危险操作需用户确认
- `acceptEdits`:非破坏性工具自动放行,减少确认弹窗
- `plan`:只允许只读工具,适合纯分析场景
- `bypass`:全部放行,跳过所有审批(慎用)

> `plan` 不再是 `PermissionMode` 的成员。plan 模式通过 `AgentProfile.name === 'plan'` 结构化识别,由 `plan/planModeGateHook` 在 `tool.approval.pre` 阶段(priority -1000)强制拒绝非白名单工具。白名单见 `plan/policy.ts` 的 `PLAN_MODE_ALLOWED_TOOLS`。

### OS 级沙箱(预留)

`packages/codingcode/src/sandbox/` 目前是 stub 实现(`SandboxService` 为空类),尚未集成实际的沙箱运行时。审批流水线已提供基本安全保障,OS 级沙箱将在未来版本中实现。
60 changes: 48 additions & 12 deletions packages/codingcode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
import { createDispatchAgentTool } from '../tools/domains/subagent/dispatch.js';
import { LLMFactoryService } from '../llm/factory.js';
import { getBuiltinTools } from '../tools/providers.js';
import { submitPlanTool } from '../tools/domains/subagent/submit-plan.js';
import { canonicalizeSchema } from '../tools/utils/canonicalize-schema.js';
import { normalizePath } from '../core/path.js';
import { isPlanProfile } from '../plan/index.js';

const REACTIVE_COMPACT_MAX_RETRIES = 3;
import { RulesService } from '../rules/index.js';
Expand Down Expand Up @@ -127,12 +129,12 @@
const hooks = yield* HookService;
const mcp = yield* McpService;
const checkpoint = yield* CheckpointService;
const approval = yield* ApprovalService;

Check warning on line 132 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'approval' is assigned a value but never used. Allowed unused vars must match /^_/u
const skills = yield* SkillService;
const runtime = yield* ProjectRuntimeService;
const todo = yield* TodoService;

Check warning on line 135 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'todo' is assigned a value but never used. Allowed unused vars must match /^_/u
const rules = yield* RulesService;
const context = yield* ContextService;

Check warning on line 137 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'context' is assigned a value but never used. Allowed unused vars must match /^_/u
const memory = yield* MemoryService;
const factory = yield* LLMFactoryService;

Expand All @@ -143,6 +145,9 @@
const state = sessionId
? yield* session.load(normalizedCwd, sessionId)
: yield* session.create(normalizedCwd, llm.modelInfo.model);
if (state.activeProfile) {
yield* runtime.restoreSessionProfile(normalizedCwd, state.sessionId, state.activeProfile);
}
state.model = llm.modelInfo.model;
state.memorySnapshot = memory.loadMemoryForPrompt(state.cwd);
const sid = state.sessionId;
Expand All @@ -160,9 +165,7 @@
}
}
const effectiveMaxSteps = profile?.maxSteps;
const effectiveApproval: any = profile?.readonly
? { permissionMode: 'bypass' }
: options?.approvalOverride;
const effectiveApproval: any = options?.approvalOverride;

if (profile?.hooks?.length) {
yield* hooks.attachSessionHooks(sid, profile.hooks);
Expand All @@ -187,6 +190,7 @@
const stream = agent.runStream({
state,
llm: activeLlm,
profile,
toolPolicy: policy,
maxStepsOverride: effectiveMaxSteps,
approvalOverride: effectiveApproval,
Expand Down Expand Up @@ -221,6 +225,7 @@
> {
const state = opts.state;
const llm = opts.llm;
const profile = opts.profile;
const sessionId = state.sessionId;
const projectPath = state.cwd;

Expand All @@ -234,9 +239,12 @@
const { skillInstruction, systemPromptVariant, rulesText } = opts;

const allAgentProfiles = runtime.listAgentProfiles(projectPath);
const agentProfiles = resolveSubagentEnabled(projectPath)
const enabledAgentProfiles = resolveSubagentEnabled(projectPath)
? allAgentProfiles.filter((p) => !resolveAgentDisabled(projectPath, p.name))
: [];
const visibleAgentProfiles = isPlanProfile(profile)
? enabledAgentProfiles.filter((p) => p.name === 'explore')
: enabledAgentProfiles;
const basePrompt =
opts.systemOverride ??
buildSystemPrompt({
Expand All @@ -245,8 +253,9 @@
shell: process.env.SHELL || process.env.ComSpec || 'bash',
variant: systemPromptVariant ?? 'default',
skillInstruction,
agentProfiles,
agentProfiles: visibleAgentProfiles,
rules: rulesText,
profileSystemPrompt: profile?.systemPrompt,
});

const memoryBlock = state.memorySnapshot;
Expand Down Expand Up @@ -281,6 +290,7 @@
let allToolDefs: ToolDefinition[] = [...builtinTools, ...(opts.mcpTools ?? [])];
if (opts.dispatchTool && resolveSubagentEnabled(projectPath))
allToolDefs = [...allToolDefs, opts.dispatchTool];
if (isPlanProfile(profile)) allToolDefs = [...allToolDefs, submitPlanTool];

const allowedByPolicy = opts.toolPolicy?.allowedTools;
let filteredDefs = allToolDefs;
Expand Down Expand Up @@ -459,7 +469,7 @@
}
}

const record = yield* session.recordAssistant(state, resp.content, toolCalls!, resp.usage);

Check warning on line 472 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'record' is assigned a value but never used. Allowed unused vars must match /^_/u
const allResults = yield* executor.executeBatch(toolCalls, state.sessionId, {
turnId: state.currentTurnId,
projectPath,
Expand Down Expand Up @@ -496,6 +506,31 @@
todoPrinted = true;
}
}

const submittedPlan = allResults.some(
(r) =>
r.type === 'ok' &&
r.name === 'submit_plan' &&
typeof r.output === 'string' &&
r.output.startsWith('Plan written to ')
);
if (submittedPlan) {
yield* q.offer({
_tag: 'Done',
content:
'Plan submitted and saved. Session switched to build mode — send a new message to execute.',
});
yield* hooks.emit('agent.turn.end', {
sessionId,
turnId: state.currentTurnId,
status: 'done',
});
yield* checkpoint.snapshotFinal(projectPath, state.sessionId, state.currentTurnId);
memory
.flushSessionToMemory(state.sessionId, llm, state.cwd)
.catch((e) => logger.error('memory flush failed:', e));
return Result.ok('Plan submitted');
}
}

if (overflow) continue;
Expand Down Expand Up @@ -530,18 +565,19 @@
}).pipe(
Effect.interruptible,
Effect.onInterrupt(() =>
Effect.sync(() => {
Effect.runSync(
q.offer({ _tag: 'Error', error: new AgentError('AGENT_ABORTED', 'cancelled') })
);
hooks
Effect.gen(function* () {
yield* Effect.sync(() => {
Effect.runSync(
q.offer({ _tag: 'Error', error: new AgentError('AGENT_ABORTED', 'cancelled') })
);
});
yield* hooks
.emit('agent.turn.end', {
sessionId,
turnId: state.currentTurnId,
status: 'aborted',
})
.pipe(Effect.runPromise)
.catch(() => {});
.pipe(Effect.ignore);
})
),
Effect.ensuring(
Expand Down
Loading
Loading