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
14 changes: 14 additions & 0 deletions assets/agents/dev-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ name: dev-lifecycle
description: 全流程开发编排器 — 需求确认后自动完成设计→任务拆解→并行实现→审查→自动合并
mode: primary
color: '#00bcd4'
permission:
bash:
"*": "allow"
"git push*": "deny"
"git worktree remove*": "deny"
"git worktree add*": "deny"
"git checkout -b*": "deny"
"git tag*": "deny"
"gh pr create*": "deny"
"gh pr merge*": "deny"
"gh issue close*": "deny"
"gh issue create*": "deny"
"gh release create*": "deny"
"npm publish*": "deny"
---

<system-reminder>
Expand Down
29 changes: 14 additions & 15 deletions assets/agents/team/backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,23 @@ name: backend
description: 负责后端代码实现、接口开发、数据库设计和业务逻辑
mode: subagent
color: '#4caf50'
tools:
read: true
bash: true
write: true
edit: true
capabilities:
create_pr: false
merge_pr: false
modify_files: true
run_tests: true
push_branch: true
approve_review: false
complete_goal: false
permission:
bash:
"*": "deny"
"npm *": "allow"
"git *": "allow"
"<profile-test-command>*": "allow"
"git status*": "allow"
"git diff*": "allow"
"git log*": "allow"
"git show*": "allow"
"git rev-parse*": "allow"
"git ls-files*": "allow"
"git branch --merged*": "allow"
"git add*": "allow"
"git commit*": "allow"
"git push*": "deny"
"git worktree*": "deny"
"git checkout -b*": "deny"
"git tag*": "deny"
edit:
"*": "deny"
".worktree/**": "allow"
Expand Down
29 changes: 14 additions & 15 deletions assets/agents/team/frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,23 @@ name: frontend
description: 负责前端页面开发、交互实现、组件封装和接口对接
mode: subagent
color: '#2196f3'
tools:
read: true
bash: true
write: true
edit: true
capabilities:
create_pr: false
merge_pr: false
modify_files: true
run_tests: true
push_branch: true
approve_review: false
complete_goal: false
permission:
bash:
"*": "deny"
"npm *": "allow"
"git *": "allow"
"<profile-test-command>*": "allow"
"git status*": "allow"
"git diff*": "allow"
"git log*": "allow"
"git show*": "allow"
"git rev-parse*": "allow"
"git ls-files*": "allow"
"git branch --merged*": "allow"
"git add*": "allow"
"git commit*": "allow"
"git push*": "deny"
"git worktree*": "deny"
"git checkout -b*": "deny"
"git tag*": "deny"
edit:
"*": "deny"
".worktree/**": "allow"
Expand Down
2 changes: 2 additions & 0 deletions assets/agents/team/reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ permission:
"*": "deny"
"gh pr view": "allow"
"gh pr view *": "allow"
"gh pr diff": "allow"
"gh pr diff *": "allow"
"diff": "allow"
"diff *": "allow"
"gh pr checks": "allow"
Expand Down
49 changes: 49 additions & 0 deletions src/kernel/caller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,40 @@

export type CallerRole = "primary" | "developer" | "architect" | "reviewer" | "goal-verify"

/** 5 个生命周期工具(spec §2.2 / PRD R4) */
export const LIFECYCLE_TOOLS = [
"setup_control",
"flow_control",
"task_control",
"tdd_checkpoint",
"release_control",
] as const

/** 工具默认允许角色矩阵:primary 全量;developer 仅 tdd_checkpoint;其余角色默认无工具 */
const TOOL_ROLES: Record<string, CallerRole[]> = {
setup_control: ["primary"],
flow_control: ["primary"],
task_control: ["primary"],
tdd_checkpoint: ["primary", "developer"],
release_control: ["primary"],
}

/** op 级覆盖(最后匹配优先):architect 仅 flow_control.status 只读;goal-verify 仅 complete-flow */
const OP_ROLES: Record<string, Record<string, CallerRole[]>> = {
flow_control: {
"complete-flow": ["goal-verify"],
status: ["primary", "architect"],
},
}

/**
* 解析工具(+op)允许的角色集。op 级覆盖优先;未知工具保守返回空集(fail-closed)。
*/
export function rolesForTool(tool: string, op?: string): CallerRole[] {
if (op && OP_ROLES[tool]?.[op]) return OP_ROLES[tool][op]
return TOOL_ROLES[tool] ?? []
}

/** requireCaller 拒绝时的错误码 */
export const CALLER_NOT_AUTHORIZED = "CALLER_NOT_AUTHORIZED"

Expand Down Expand Up @@ -62,3 +96,18 @@ export async function requireCaller(
if (allowedRoles.includes(role)) return null
return `${CALLER_NOT_AUTHORIZED}: caller "${ctx.agent}" resolved as "${role}" is not allowed to call "${op}" (allowed: ${allowedRoles.join(", ")})`
}

/**
* 工具级 caller 门禁:按工具(+op)矩阵解析允许角色后校验。
* 供各生命周期工具 execute 内调用(spec §2.2 第一道门;config 层为第二道门)。
*/
export async function requireToolCaller(
ctx: CallerContext,
tool: string,
op: string | undefined,
client: CallerSessionClient,
): Promise<string | null> {
const allowed = rolesForTool(tool, op)
const label = op ? `${tool}:{op:"${op}"}` : tool
return requireCaller(ctx, allowed, label, client)
}
38 changes: 38 additions & 0 deletions src/kernel/permission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* OpenCode permission 匹配纯函数(spec §7.2)。
*
* 语义与 OpenCode 当前实现对齐:
* - bash 模式规则:pattern 支持 `*` 通配(`*` 兜底、`cmd*` 前缀匹配)
* - 最后匹配优先:后声明的规则覆盖先声明的同命中规则
* - auto 模式:仅 allow 放行,deny/ask 均拒绝
*/

export type PermissionAction = "allow" | "deny" | "ask"

/** pattern 是否命中 command:`*` 全匹配,`前缀*` 前缀匹配,否则精确匹配 */
export function permissionPatternMatches(pattern: string, command: string): boolean {
if (pattern === "*") return true
if (pattern.endsWith("*")) return command.startsWith(pattern.slice(0, -1))
return command === pattern
}

/**
* 按规则集解析 command 的最终动作。
* 按声明顺序遍历,命中即记录,后命中覆盖先前 → 最后匹配优先。
* 无任何命中返回 "ask"(默认询问,auto 模式下等效拒绝)。
*/
export function matchPermission(rules: Record<string, string>, command: string): PermissionAction {
let action: PermissionAction = "ask"
for (const [pattern, raw] of Object.entries(rules)) {
if (!permissionPatternMatches(pattern, command)) continue
action = raw === "allow" ? "allow" : raw === "deny" ? "deny" : "ask"
}
return action
}

/**
* auto 模式门禁:仅 allow 放行;deny 与 ask(无命中)均拒绝。
*/
export function isAllowedInAutoMode(rules: Record<string, string>, command: string): boolean {
return matchPermission(rules, command) === "allow"
}
51 changes: 5 additions & 46 deletions src/plugin/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,11 @@ import { readFileSync, readdirSync, existsSync } from "node:fs"
import path from "node:path"
import { parse as parseYaml } from "yaml"

export interface AgentTools {
read?: boolean
bash?: boolean
write?: boolean
edit?: boolean
}

export interface AgentCapabilities {
create_pr: boolean
merge_pr: boolean
modify_files: boolean
run_tests: boolean
push_branch: boolean
approve_review: boolean
complete_goal: boolean
}

/**
* Agent frontmatter 的 permission 解析。
* 值可为字符串(如 "deny"、"npm test|git push")或嵌套规则对象
* (OpenCode 语义:pattern → action,如 bash: { "*": "deny", "npm *": "allow" })。
*/
export interface AgentPermission {
[key: string]: string | Record<string, string> | undefined
}
Expand All @@ -28,8 +16,6 @@ export interface AgentEntry {
description?: string
mode?: "subagent" | "primary" | "all"
color?: string
tools?: AgentTools
capabilities?: AgentCapabilities
permission?: AgentPermission
prompt: string
}
Expand All @@ -53,31 +39,6 @@ function parseAgentFile(filePath: string): AgentEntry | null {
const name = String(parsed.name ?? "")
if (!name) return null

const toolsRaw = parsed.tools
const tools: AgentTools | undefined =
toolsRaw && typeof toolsRaw === "object" && !Array.isArray(toolsRaw)
? {
read: Boolean((toolsRaw as Record<string, unknown>).read),
bash: Boolean((toolsRaw as Record<string, unknown>).bash),
write: Boolean((toolsRaw as Record<string, unknown>).write),
edit: Boolean((toolsRaw as Record<string, unknown>).edit),
}
: undefined

const capabilitiesRaw = parsed.capabilities
const capabilities: AgentCapabilities | undefined =
capabilitiesRaw && typeof capabilitiesRaw === "object" && !Array.isArray(capabilitiesRaw)
? {
create_pr: Boolean((capabilitiesRaw as Record<string, unknown>).create_pr),
merge_pr: Boolean((capabilitiesRaw as Record<string, unknown>).merge_pr),
modify_files: Boolean((capabilitiesRaw as Record<string, unknown>).modify_files),
run_tests: Boolean((capabilitiesRaw as Record<string, unknown>).run_tests),
push_branch: Boolean((capabilitiesRaw as Record<string, unknown>).push_branch),
approve_review: Boolean((capabilitiesRaw as Record<string, unknown>).approve_review),
complete_goal: Boolean((capabilitiesRaw as Record<string, unknown>).complete_goal),
}
: undefined

const permissionRaw = parsed.permission
const permission: AgentPermission | undefined =
permissionRaw && typeof permissionRaw === "object" && !Array.isArray(permissionRaw)
Expand All @@ -103,8 +64,6 @@ function parseAgentFile(filePath: string): AgentEntry | null {
description: parsed.description as string | undefined,
mode: parsed.mode as AgentEntry["mode"],
color: parsed.color as string | undefined,
tools,
capabilities,
permission,
prompt: body,
}
Expand Down
Loading
Loading