@zhijiewang/openharness 2.35.0 → 2.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/README.zh-CN.md +13 -0
- package/dist/agents/roles.d.ts +14 -0
- package/dist/agents/roles.js +15 -0
- package/dist/tools/AgentTool/index.d.ts +3 -0
- package/dist/tools/AgentTool/index.js +21 -2
- package/dist/types/permissions.d.ts +17 -0
- package/dist/types/permissions.js +50 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -563,6 +563,19 @@ Agent({ subagent_type: 'architect', prompt: 'Plan a migration from option A to o
|
|
|
563
563
|
Agent({ subagent_type: 'editor', prompt: '<paste plan>' })
|
|
564
564
|
```
|
|
565
565
|
|
|
566
|
+
### Sub-agent permission isolation
|
|
567
|
+
|
|
568
|
+
Each `Agent` call accepts a `permission_mode` override that **narrows** the parent's permission mode (never loosens it). Useful when running in `trust` and you want a subagent's review/audit pass to stay strictly read-only:
|
|
569
|
+
|
|
570
|
+
```
|
|
571
|
+
Agent({ subagent_type: 'code-reviewer', prompt: '...', permission_mode: 'plan' })
|
|
572
|
+
Agent({ subagent_type: 'security-auditor', prompt: '...', permission_mode: 'deny' })
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
If a less-restrictive mode is requested (e.g. parent is `ask`, subagent requests `trust`), the harness silently clamps to the parent — a model can never use a sub-agent to escape user-approval gates.
|
|
576
|
+
|
|
577
|
+
**Read-only roles default to `plan` automatically.** `code-reviewer`, `evaluator`, `security-auditor`, `architect`, and `planner` ship with `permissionMode: 'plan'` — spawn them under any parent and they're statically read-only, no `permission_mode` override needed. Markdown-defined agents in `.oh/agents/*.md` can set their own default with `permissionMode: plan` (or `permission-mode: plan`) frontmatter.
|
|
578
|
+
|
|
566
579
|
## Headless Mode
|
|
567
580
|
|
|
568
581
|
Run a single prompt without interactive UI — perfect for CI/CD and scripting:
|
package/README.zh-CN.md
CHANGED
|
@@ -563,6 +563,19 @@ Agent({ subagent_type: 'architect', prompt: 'Plan a migration from option A to o
|
|
|
563
563
|
Agent({ subagent_type: 'editor', prompt: '<paste plan>' })
|
|
564
564
|
```
|
|
565
565
|
|
|
566
|
+
### 子代理的权限隔离
|
|
567
|
+
|
|
568
|
+
`Agent` 调用支持 `permission_mode` 参数,**只能收紧不能放宽**父级的权限模式。当父代理跑在 `trust` 但你希望某个评审/审计子代理保持只读时尤其有用:
|
|
569
|
+
|
|
570
|
+
```
|
|
571
|
+
Agent({ subagent_type: 'code-reviewer', prompt: '...', permission_mode: 'plan' })
|
|
572
|
+
Agent({ subagent_type: 'security-auditor', prompt: '...', permission_mode: 'deny' })
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
如果请求的模式比父级更宽松(比如父级 `ask`、子代理请求 `trust`),harness 会静默回退到父级的模式 —— 模型永远不能借助子代理绕过用户的批准门。
|
|
576
|
+
|
|
577
|
+
**只读角色自动默认 `plan` 模式。** `code-reviewer`、`evaluator`、`security-auditor`、`architect`、`planner` 内置 `permissionMode: 'plan'` —— 在任何父级权限下启动它们都是静态只读,无需在调用处再传 `permission_mode`。`.oh/agents/*.md` 里自定义的 markdown agent 也可以在 frontmatter 写 `permissionMode: plan`(或 `permission-mode: plan`)来设默认。
|
|
578
|
+
|
|
566
579
|
## 无头模式
|
|
567
580
|
|
|
568
581
|
跑一次提示词,不走交互 UI —— 适合 CI/CD 和脚本化:
|
package/dist/agents/roles.d.ts
CHANGED
|
@@ -21,6 +21,20 @@ export type AgentRole = {
|
|
|
21
21
|
model?: string;
|
|
22
22
|
/** Isolation mode for sub-agent execution. Default: inherits parent. */
|
|
23
23
|
isolation?: "none" | "worktree";
|
|
24
|
+
/**
|
|
25
|
+
* Default permission mode for this role. When the AgentTool caller doesn't
|
|
26
|
+
* pass `permission_mode`, this value is used. Always passes through the
|
|
27
|
+
* narrowing-only safety clamp (v2.36): a role marked `plan` can't loosen
|
|
28
|
+
* the parent's stricter mode, only set the floor when the parent is looser.
|
|
29
|
+
*
|
|
30
|
+
* Use cases:
|
|
31
|
+
* - `plan` for read-only roles (code-reviewer, evaluator, security-auditor,
|
|
32
|
+
* architect, planner) — locks them as read-only even when the parent
|
|
33
|
+
* is in `trust`, so the review pass can't accidentally write anything.
|
|
34
|
+
* - Leave undefined for roles that mutate (editor, migrator, refactorer,
|
|
35
|
+
* test-writer, debugger) — they need the parent's mode to do their job.
|
|
36
|
+
*/
|
|
37
|
+
permissionMode?: import("../types/permissions.js").PermissionMode;
|
|
24
38
|
/** Per-agent MCP servers injected only when this agent runs (parsed via raw passthrough; dispatcher wiring is a future task). */
|
|
25
39
|
mcpServers?: Record<string, unknown>;
|
|
26
40
|
/** Per-agent hooks (parsed via raw passthrough; dispatcher wiring is a future task). */
|
package/dist/agents/roles.js
CHANGED
|
@@ -22,6 +22,7 @@ const roles = [
|
|
|
22
22
|
|
|
23
23
|
Be specific: cite file paths, line numbers, and code snippets. Prioritize issues by severity (critical > major > minor). Don't mention things that look fine — focus on problems.`,
|
|
24
24
|
suggestedTools: ["Read", "Glob", "Grep", "LS"],
|
|
25
|
+
permissionMode: "plan",
|
|
25
26
|
},
|
|
26
27
|
{
|
|
27
28
|
id: "test-writer",
|
|
@@ -99,6 +100,7 @@ Do NOT add new features or change behavior. The refactored code must be function
|
|
|
99
100
|
|
|
100
101
|
Report findings with severity (Critical/High/Medium/Low), affected file:line, and recommended fix.`,
|
|
101
102
|
suggestedTools: ["Read", "Glob", "Grep", "Bash"],
|
|
103
|
+
permissionMode: "plan",
|
|
102
104
|
},
|
|
103
105
|
{
|
|
104
106
|
id: "evaluator",
|
|
@@ -113,6 +115,7 @@ Report findings with severity (Critical/High/Medium/Low), affected file:line, an
|
|
|
113
115
|
|
|
114
116
|
You CANNOT modify files. Only read, search, and run test/lint commands to evaluate.`,
|
|
115
117
|
suggestedTools: ["Read", "Glob", "Grep", "LS", "Bash", "Diagnostics"],
|
|
118
|
+
permissionMode: "plan",
|
|
116
119
|
},
|
|
117
120
|
{
|
|
118
121
|
id: "planner",
|
|
@@ -127,6 +130,7 @@ You CANNOT modify files. Only read, search, and run test/lint commands to evalua
|
|
|
127
130
|
|
|
128
131
|
Do NOT implement anything. Your output is a plan document, not code. Read widely before planning.`,
|
|
129
132
|
suggestedTools: ["Read", "Glob", "Grep", "LS", "Bash"],
|
|
133
|
+
permissionMode: "plan",
|
|
130
134
|
},
|
|
131
135
|
{
|
|
132
136
|
id: "architect",
|
|
@@ -152,6 +156,7 @@ When you've finished planning, output a structured "Plan" the editor can apply m
|
|
|
152
156
|
|
|
153
157
|
Keep each step small enough that an editor (a cheaper model) can apply it without re-deriving your reasoning. Group related edits together; surface dependencies between steps.`,
|
|
154
158
|
suggestedTools: ["Read", "Glob", "Grep", "LS"],
|
|
159
|
+
permissionMode: "plan",
|
|
155
160
|
},
|
|
156
161
|
{
|
|
157
162
|
id: "editor",
|
|
@@ -238,6 +243,7 @@ function parseAgentMarkdown(raw, filePath) {
|
|
|
238
243
|
const disallowedMatch = fm.match(/^disallowedTools:\s*(.+)$/m) ?? fm.match(/^disallowed-tools:\s*(.+)$/m);
|
|
239
244
|
const modelMatch = fm.match(/^model:\s*(.+)$/m);
|
|
240
245
|
const isolationMatch = fm.match(/^isolation:\s*(.+)$/m);
|
|
246
|
+
const permModeMatch = fm.match(/^permissionMode:\s*(.+)$/m) ?? fm.match(/^permission-mode:\s*(.+)$/m);
|
|
241
247
|
const fmEnd = raw.indexOf("---", raw.indexOf("---") + 3);
|
|
242
248
|
const content = fmEnd > 0 ? raw.slice(fmEnd + 3).trim() : "";
|
|
243
249
|
const id = basename(filePath, ".md")
|
|
@@ -245,6 +251,14 @@ function parseAgentMarkdown(raw, filePath) {
|
|
|
245
251
|
.replace(/[^a-z0-9]+/g, "-");
|
|
246
252
|
const isolation = isolationMatch?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
247
253
|
const validIsolation = isolation === "worktree" || isolation === "none" ? isolation : undefined;
|
|
254
|
+
// permissionMode: only honor known values; silently drop typos. Same
|
|
255
|
+
// pattern as `isolation` above — we'd rather a misspelled mode produce
|
|
256
|
+
// "no override" than a runtime crash.
|
|
257
|
+
const permModeRaw = permModeMatch?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
258
|
+
const validModes = ["ask", "trust", "deny", "acceptEdits", "plan", "auto", "bypassPermissions"];
|
|
259
|
+
const validPermissionMode = validModes.includes(permModeRaw)
|
|
260
|
+
? permModeRaw
|
|
261
|
+
: undefined;
|
|
248
262
|
// Parse optional inline-JSON fields (mcpServers, hooks). These are block fields in
|
|
249
263
|
// Anthropic's YAML but we support single-line JSON for lightweight frontmatter use.
|
|
250
264
|
const mcpServersMatch = fm.match(/^mcpServers:\s*(\{[\s\S]*?\})\s*$/m);
|
|
@@ -258,6 +272,7 @@ function parseAgentMarkdown(raw, filePath) {
|
|
|
258
272
|
disallowedTools: disallowedMatch ? parseAgentList(disallowedMatch[1]) : undefined,
|
|
259
273
|
model: modelMatch?.[1]?.trim().replace(/^["']|["']$/g, ""),
|
|
260
274
|
isolation: validIsolation,
|
|
275
|
+
permissionMode: validPermissionMode,
|
|
261
276
|
mcpServers: mcpServersMatch ? tryParseJson(mcpServersMatch[1]) : undefined,
|
|
262
277
|
hooks: hooksMatch ? tryParseJson(hooksMatch[1]) : undefined,
|
|
263
278
|
};
|
|
@@ -21,6 +21,7 @@ declare const inputSchema: z.ZodObject<{
|
|
|
21
21
|
model: z.ZodOptional<z.ZodString>;
|
|
22
22
|
subagent_type: z.ZodOptional<z.ZodString>;
|
|
23
23
|
allowed_tools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
24
|
+
permission_mode: z.ZodOptional<z.ZodEnum<["ask", "trust", "deny", "acceptEdits", "plan", "auto", "bypassPermissions"]>>;
|
|
24
25
|
}, "strip", z.ZodTypeAny, {
|
|
25
26
|
prompt: string;
|
|
26
27
|
model?: string | undefined;
|
|
@@ -30,6 +31,7 @@ declare const inputSchema: z.ZodObject<{
|
|
|
30
31
|
run_in_background?: boolean | undefined;
|
|
31
32
|
subagent_type?: string | undefined;
|
|
32
33
|
allowed_tools?: string[] | undefined;
|
|
34
|
+
permission_mode?: "ask" | "deny" | "trust" | "acceptEdits" | "plan" | "auto" | "bypassPermissions" | undefined;
|
|
33
35
|
}, {
|
|
34
36
|
prompt: string;
|
|
35
37
|
model?: string | undefined;
|
|
@@ -39,6 +41,7 @@ declare const inputSchema: z.ZodObject<{
|
|
|
39
41
|
run_in_background?: boolean | undefined;
|
|
40
42
|
subagent_type?: string | undefined;
|
|
41
43
|
allowed_tools?: string[] | undefined;
|
|
44
|
+
permission_mode?: "ask" | "deny" | "trust" | "acceptEdits" | "plan" | "auto" | "bypassPermissions" | undefined;
|
|
42
45
|
}>;
|
|
43
46
|
export declare const AgentTool: Tool<typeof inputSchema>;
|
|
44
47
|
export {};
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { createWorktree, hasWorktreeChanges, isGitRepo, removeWorktree } from "../../git/index.js";
|
|
3
3
|
import { emitHook } from "../../harness/hooks.js";
|
|
4
4
|
import { getMessageBus } from "../../services/agent-messaging.js";
|
|
5
|
+
import { clampSubagentPermissionMode } from "../../types/permissions.js";
|
|
5
6
|
/**
|
|
6
7
|
* Forward a single inner-query event to the outer stream via `context.emitChildEvent`,
|
|
7
8
|
* stamping it with `parentCallId = context.callId`.
|
|
@@ -36,6 +37,10 @@ const inputSchema = z.object({
|
|
|
36
37
|
model: z.string().optional(),
|
|
37
38
|
subagent_type: z.string().optional(),
|
|
38
39
|
allowed_tools: z.array(z.string()).optional(),
|
|
40
|
+
permission_mode: z
|
|
41
|
+
.enum(["ask", "trust", "deny", "acceptEdits", "plan", "auto", "bypassPermissions"])
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Restrict the sub-agent's permission mode. Narrowing-only: the harness clamps to the parent's mode if a less-restrictive value is requested. Useful for spawning read-only review/audit agents while the parent runs in 'trust'."),
|
|
39
44
|
});
|
|
40
45
|
export const AgentTool = {
|
|
41
46
|
name: "Agent",
|
|
@@ -101,11 +106,24 @@ export const AgentTool = {
|
|
|
101
106
|
}
|
|
102
107
|
// Model override for sub-agent
|
|
103
108
|
const agentModel = input.model ?? context.model;
|
|
109
|
+
// Permission mode override — narrowing-only. Subagent can be same-or-stricter
|
|
110
|
+
// than parent; a less-restrictive request silently clamps to the parent so a
|
|
111
|
+
// model in `ask` can't spawn a `trust`-mode subagent to bypass user approval.
|
|
112
|
+
//
|
|
113
|
+
// Resolution order, most-specific-wins:
|
|
114
|
+
// 1. input.permission_mode (the call site's explicit choice)
|
|
115
|
+
// 2. role.permissionMode (the role's documented default — e.g.
|
|
116
|
+
// code-reviewer / security-auditor / planner default to "plan")
|
|
117
|
+
// 3. parent's mode (no narrowing — current default)
|
|
118
|
+
// The clamp applies in all cases so #1 and #2 can only narrow, not loosen.
|
|
119
|
+
const parentMode = context.permissionMode ?? "trust";
|
|
120
|
+
const requestedMode = input.permission_mode ?? role?.permissionMode;
|
|
121
|
+
const subagentMode = clampSubagentPermissionMode(parentMode, requestedMode);
|
|
104
122
|
const config = {
|
|
105
123
|
provider: context.provider,
|
|
106
124
|
tools: agentTools,
|
|
107
125
|
systemPrompt,
|
|
108
|
-
permissionMode:
|
|
126
|
+
permissionMode: subagentMode,
|
|
109
127
|
model: agentModel,
|
|
110
128
|
maxTurns: 20,
|
|
111
129
|
abortSignal: context.abortSignal,
|
|
@@ -225,7 +243,8 @@ export const AgentTool = {
|
|
|
225
243
|
- run_in_background (boolean, optional): Run the agent in the background. Returns immediately; you will be notified when it completes.
|
|
226
244
|
- model (string, optional): Override the model for this sub-agent (e.g., use a faster model for exploration).
|
|
227
245
|
- subagent_type (string, optional): Specialize the agent behavior. Types: "Explore" (read-only codebase search), "Plan" (design implementation plans), "code-reviewer", "test-writer", "debugger", "refactorer", "security-auditor", "evaluator" (read-only evaluation), "planner" (implementation plans), "architect" (system design), "migrator" (codebase migrations).
|
|
228
|
-
- allowed_tools (string[], optional): Restrict the sub-agent to only these tools by name. If omitted and a role has suggested tools, those are used
|
|
246
|
+
- allowed_tools (string[], optional): Restrict the sub-agent to only these tools by name. If omitted and a role has suggested tools, those are used.
|
|
247
|
+
- permission_mode (string, optional): Override the sub-agent's permission mode (one of: ask, trust, deny, acceptEdits, plan, auto, bypassPermissions). Narrowing-only — a less-restrictive value silently clamps to the parent's mode. Use to spawn a read-only audit/review sub-agent in "plan" or "deny" while the parent runs in "trust".`;
|
|
229
248
|
},
|
|
230
249
|
};
|
|
231
250
|
//# sourceMappingURL=index.js.map
|
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
import type { ToolPermissionRule } from "../harness/config.js";
|
|
5
5
|
export type PermissionMode = "ask" | "trust" | "deny" | "acceptEdits" | "plan" | "auto" | "bypassPermissions";
|
|
6
6
|
export type RiskLevel = "low" | "medium" | "high";
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a subagent's effective permission mode given the parent's mode and
|
|
9
|
+
* an optionally-requested override. The contract: a subagent may be the same
|
|
10
|
+
* strictness as its parent or stricter, never looser.
|
|
11
|
+
*
|
|
12
|
+
* Why this asymmetry: the AgentTool input schema lets the model pick a
|
|
13
|
+
* subagent's mode. Allowing a less-restrictive choice would mean a model in
|
|
14
|
+
* `ask` mode could spawn a `trust`-mode subagent and quietly bypass user
|
|
15
|
+
* approval on its own tool calls — exactly the kind of escalation the
|
|
16
|
+
* permission system exists to prevent. The same pattern as `allowed_tools`,
|
|
17
|
+
* which is also narrowing-only.
|
|
18
|
+
*
|
|
19
|
+
* Returns the parent's mode if the requested override is undefined or would
|
|
20
|
+
* loosen the gate. Returns the requested override only when it's the same or
|
|
21
|
+
* stricter than the parent.
|
|
22
|
+
*/
|
|
23
|
+
export declare function clampSubagentPermissionMode(parentMode: PermissionMode, requestedMode: PermissionMode | undefined): PermissionMode;
|
|
7
24
|
export type PermissionResult = {
|
|
8
25
|
readonly allowed: boolean;
|
|
9
26
|
readonly reason: string;
|
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
* Permission types — tool permission context and risk-based gating.
|
|
3
3
|
*/
|
|
4
4
|
import { analyzeBashCommand, isReadOnlyBashCommand, splitCommands, stripProcessWrappers, } from "../utils/bash-safety.js";
|
|
5
|
+
/**
|
|
6
|
+
* Strictness rank for permission modes. Lower = more permissive. Used by
|
|
7
|
+
* `clampSubagentPermissionMode` to enforce that a subagent can never run
|
|
8
|
+
* less restrictively than its parent — only the same or stricter.
|
|
9
|
+
*
|
|
10
|
+
* The ordering is deliberate, not lexical:
|
|
11
|
+
* bypassPermissions (0) — approves everything unconditionally
|
|
12
|
+
* trust (1) — approves everything user-trusted
|
|
13
|
+
* auto (2) — approves except dangerous bash
|
|
14
|
+
* acceptEdits (3) — auto-approves edit-safe tool subset
|
|
15
|
+
* plan (4) — read-only allowed; writes blocked
|
|
16
|
+
* ask (5) — every non-trivial call prompts the user
|
|
17
|
+
* deny (6) — denies everything
|
|
18
|
+
*
|
|
19
|
+
* Why a rank instead of a Set: most-restrictive comparisons are easier when
|
|
20
|
+
* the dimension is total-ordered, and adding a new mode means adding one row
|
|
21
|
+
* to this table rather than re-deriving the partial order across call sites.
|
|
22
|
+
*/
|
|
23
|
+
const PERMISSION_STRICTNESS_RANK = {
|
|
24
|
+
bypassPermissions: 0,
|
|
25
|
+
trust: 1,
|
|
26
|
+
auto: 2,
|
|
27
|
+
acceptEdits: 3,
|
|
28
|
+
plan: 4,
|
|
29
|
+
ask: 5,
|
|
30
|
+
deny: 6,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a subagent's effective permission mode given the parent's mode and
|
|
34
|
+
* an optionally-requested override. The contract: a subagent may be the same
|
|
35
|
+
* strictness as its parent or stricter, never looser.
|
|
36
|
+
*
|
|
37
|
+
* Why this asymmetry: the AgentTool input schema lets the model pick a
|
|
38
|
+
* subagent's mode. Allowing a less-restrictive choice would mean a model in
|
|
39
|
+
* `ask` mode could spawn a `trust`-mode subagent and quietly bypass user
|
|
40
|
+
* approval on its own tool calls — exactly the kind of escalation the
|
|
41
|
+
* permission system exists to prevent. The same pattern as `allowed_tools`,
|
|
42
|
+
* which is also narrowing-only.
|
|
43
|
+
*
|
|
44
|
+
* Returns the parent's mode if the requested override is undefined or would
|
|
45
|
+
* loosen the gate. Returns the requested override only when it's the same or
|
|
46
|
+
* stricter than the parent.
|
|
47
|
+
*/
|
|
48
|
+
export function clampSubagentPermissionMode(parentMode, requestedMode) {
|
|
49
|
+
if (!requestedMode)
|
|
50
|
+
return parentMode;
|
|
51
|
+
return PERMISSION_STRICTNESS_RANK[requestedMode] >= PERMISSION_STRICTNESS_RANK[parentMode]
|
|
52
|
+
? requestedMode
|
|
53
|
+
: parentMode;
|
|
54
|
+
}
|
|
5
55
|
/** Tools auto-approved in acceptEdits mode */
|
|
6
56
|
const EDIT_SAFE_TOOLS = new Set([
|
|
7
57
|
"FileRead",
|