@zhijiewang/openharness 2.36.0 → 2.38.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 +2 -0
- package/README.zh-CN.md +2 -0
- package/dist/agents/roles.d.ts +14 -0
- package/dist/agents/roles.js +15 -0
- package/dist/services/AgentDispatcher.d.ts +10 -1
- package/dist/services/AgentDispatcher.js +6 -1
- package/dist/tools/AgentTool/index.js +9 -1
- package/dist/tools/ParallelAgentTool/index.d.ts +10 -0
- package/dist/tools/ParallelAgentTool/index.js +22 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -574,6 +574,8 @@ Agent({ subagent_type: 'security-auditor', prompt: '...', permission_mode: 'deny
|
|
|
574
574
|
|
|
575
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
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
|
+
|
|
577
579
|
## Headless Mode
|
|
578
580
|
|
|
579
581
|
Run a single prompt without interactive UI — perfect for CI/CD and scripting:
|
package/README.zh-CN.md
CHANGED
|
@@ -574,6 +574,8 @@ Agent({ subagent_type: 'security-auditor', prompt: '...', permission_mode: 'deny
|
|
|
574
574
|
|
|
575
575
|
如果请求的模式比父级更宽松(比如父级 `ask`、子代理请求 `trust`),harness 会静默回退到父级的模式 —— 模型永远不能借助子代理绕过用户的批准门。
|
|
576
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
|
+
|
|
577
579
|
## 无头模式
|
|
578
580
|
|
|
579
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
|
};
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { Provider } from "../providers/base.js";
|
|
9
9
|
import type { Tools } from "../Tool.js";
|
|
10
10
|
import type { StreamEvent, ToolCallComplete, ToolCallEnd, ToolCallStart, ToolOutputDelta } from "../types/events.js";
|
|
11
|
-
import type
|
|
11
|
+
import { type PermissionMode } from "../types/permissions.js";
|
|
12
12
|
/**
|
|
13
13
|
* Forward inner-loop tool events to the outer stream, stamping parentCallId.
|
|
14
14
|
* Exported for direct unit testing.
|
|
@@ -20,6 +20,15 @@ export type AgentTask = {
|
|
|
20
20
|
description?: string;
|
|
21
21
|
blockedBy?: string[];
|
|
22
22
|
allowedTools?: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Per-task permission mode override — narrowing-only, same contract as
|
|
25
|
+
* AgentTool's `permission_mode` (v2.36). When set, the task's effective
|
|
26
|
+
* mode is `clampSubagentPermissionMode(dispatcher.permissionMode, task.permissionMode)`,
|
|
27
|
+
* so a task can be the same strictness as the outer call or stricter,
|
|
28
|
+
* never looser. Use to mark specific tasks in a parallel batch as
|
|
29
|
+
* read-only review/audit while letting siblings keep full write access.
|
|
30
|
+
*/
|
|
31
|
+
permissionMode?: PermissionMode;
|
|
23
32
|
};
|
|
24
33
|
export type AgentTaskResult = {
|
|
25
34
|
id: string;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* and triggers dependent tasks when their blockers complete.
|
|
7
7
|
*/
|
|
8
8
|
import { createWorktree, isGitRepo, removeWorktree } from "../git/index.js";
|
|
9
|
+
import { clampSubagentPermissionMode } from "../types/permissions.js";
|
|
9
10
|
/**
|
|
10
11
|
* Forward inner-loop tool events to the outer stream, stamping parentCallId.
|
|
11
12
|
* Exported for direct unit testing.
|
|
@@ -168,11 +169,15 @@ export class AgentDispatcher {
|
|
|
168
169
|
// matching `process.chdir(originalCwd)` in `finally` — but since
|
|
169
170
|
// `process.cwd()` is process-wide, two concurrent tasks would clobber
|
|
170
171
|
// each other's directory mid-execution.
|
|
172
|
+
// Per-task permission mode — narrowing-only clamp applied so a task
|
|
173
|
+
// can override only to a same-or-stricter mode than the dispatcher's
|
|
174
|
+
// outer mode (#115 contract).
|
|
175
|
+
const taskPermissionMode = clampSubagentPermissionMode(this.permissionMode, task.permissionMode);
|
|
171
176
|
const config = {
|
|
172
177
|
provider: this.provider,
|
|
173
178
|
tools: taskTools,
|
|
174
179
|
systemPrompt: this.systemPrompt,
|
|
175
|
-
permissionMode:
|
|
180
|
+
permissionMode: taskPermissionMode,
|
|
176
181
|
model: this.model,
|
|
177
182
|
maxTurns: 20,
|
|
178
183
|
abortSignal: this.abortSignal,
|
|
@@ -109,8 +109,16 @@ export const AgentTool = {
|
|
|
109
109
|
// Permission mode override — narrowing-only. Subagent can be same-or-stricter
|
|
110
110
|
// than parent; a less-restrictive request silently clamps to the parent so a
|
|
111
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.
|
|
112
119
|
const parentMode = context.permissionMode ?? "trust";
|
|
113
|
-
const
|
|
120
|
+
const requestedMode = input.permission_mode ?? role?.permissionMode;
|
|
121
|
+
const subagentMode = clampSubagentPermissionMode(parentMode, requestedMode);
|
|
114
122
|
const config = {
|
|
115
123
|
provider: context.provider,
|
|
116
124
|
tools: agentTools,
|
|
@@ -6,15 +6,21 @@ declare const inputSchema: z.ZodObject<{
|
|
|
6
6
|
prompt: z.ZodString;
|
|
7
7
|
description: z.ZodOptional<z.ZodString>;
|
|
8
8
|
blockedBy: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
9
|
+
allowed_tools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
10
|
+
permission_mode: z.ZodOptional<z.ZodEnum<["ask", "trust", "deny", "acceptEdits", "plan", "auto", "bypassPermissions"]>>;
|
|
9
11
|
}, "strip", z.ZodTypeAny, {
|
|
10
12
|
id: string;
|
|
11
13
|
prompt: string;
|
|
12
14
|
description?: string | undefined;
|
|
15
|
+
allowed_tools?: string[] | undefined;
|
|
16
|
+
permission_mode?: "ask" | "deny" | "trust" | "acceptEdits" | "plan" | "auto" | "bypassPermissions" | undefined;
|
|
13
17
|
blockedBy?: string[] | undefined;
|
|
14
18
|
}, {
|
|
15
19
|
id: string;
|
|
16
20
|
prompt: string;
|
|
17
21
|
description?: string | undefined;
|
|
22
|
+
allowed_tools?: string[] | undefined;
|
|
23
|
+
permission_mode?: "ask" | "deny" | "trust" | "acceptEdits" | "plan" | "auto" | "bypassPermissions" | undefined;
|
|
18
24
|
blockedBy?: string[] | undefined;
|
|
19
25
|
}>, "many">;
|
|
20
26
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -22,6 +28,8 @@ declare const inputSchema: z.ZodObject<{
|
|
|
22
28
|
id: string;
|
|
23
29
|
prompt: string;
|
|
24
30
|
description?: string | undefined;
|
|
31
|
+
allowed_tools?: string[] | undefined;
|
|
32
|
+
permission_mode?: "ask" | "deny" | "trust" | "acceptEdits" | "plan" | "auto" | "bypassPermissions" | undefined;
|
|
25
33
|
blockedBy?: string[] | undefined;
|
|
26
34
|
}[];
|
|
27
35
|
}, {
|
|
@@ -29,6 +37,8 @@ declare const inputSchema: z.ZodObject<{
|
|
|
29
37
|
id: string;
|
|
30
38
|
prompt: string;
|
|
31
39
|
description?: string | undefined;
|
|
40
|
+
allowed_tools?: string[] | undefined;
|
|
41
|
+
permission_mode?: "ask" | "deny" | "trust" | "acceptEdits" | "plan" | "auto" | "bypassPermissions" | undefined;
|
|
32
42
|
blockedBy?: string[] | undefined;
|
|
33
43
|
}[];
|
|
34
44
|
}>;
|
|
@@ -5,6 +5,11 @@ const taskSchema = z.object({
|
|
|
5
5
|
prompt: z.string(),
|
|
6
6
|
description: z.string().optional(),
|
|
7
7
|
blockedBy: z.array(z.string()).optional(),
|
|
8
|
+
allowed_tools: z.array(z.string()).optional(),
|
|
9
|
+
permission_mode: z
|
|
10
|
+
.enum(["ask", "trust", "deny", "acceptEdits", "plan", "auto", "bypassPermissions"])
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("Restrict THIS task's permission mode. Narrowing-only — clamps to the outer mode if a less-restrictive value is requested. Use to mark a single task as read-only review/audit while sibling tasks keep full write access."),
|
|
8
13
|
});
|
|
9
14
|
const inputSchema = z.object({
|
|
10
15
|
tasks: z.array(taskSchema).min(1),
|
|
@@ -27,7 +32,18 @@ export const ParallelAgentTool = {
|
|
|
27
32
|
const systemPrompt = context.systemPrompt ?? "You are a sub-agent. Complete the delegated task concisely.";
|
|
28
33
|
const dispatcher = new AgentDispatcher(context.provider, context.tools, systemPrompt, context.permissionMode ?? "trust", context.model, context.workingDir, context.abortSignal, 4, // maxConcurrency default
|
|
29
34
|
context.callId, context.emitChildEvent);
|
|
30
|
-
|
|
35
|
+
// Map snake_case input fields to the AgentTask camelCase shape — the
|
|
36
|
+
// input schema uses `allowed_tools` / `permission_mode` to stay
|
|
37
|
+
// consistent with AgentTool, but the dispatcher's task type uses
|
|
38
|
+
// `allowedTools` / `permissionMode`.
|
|
39
|
+
dispatcher.addTasks(input.tasks.map((t) => ({
|
|
40
|
+
id: t.id,
|
|
41
|
+
prompt: t.prompt,
|
|
42
|
+
description: t.description,
|
|
43
|
+
blockedBy: t.blockedBy,
|
|
44
|
+
allowedTools: t.allowed_tools,
|
|
45
|
+
permissionMode: t.permission_mode,
|
|
46
|
+
})));
|
|
31
47
|
const results = await dispatcher.execute();
|
|
32
48
|
const output = results
|
|
33
49
|
.map((r) => {
|
|
@@ -48,12 +64,13 @@ Parameters:
|
|
|
48
64
|
- prompt (string): Instructions for the sub-agent
|
|
49
65
|
- description (string, optional): Short label
|
|
50
66
|
- blockedBy (string[], optional): IDs of tasks that must complete first
|
|
67
|
+
- allowed_tools (string[], optional): Restrict THIS task's agent to specific tools
|
|
68
|
+
- permission_mode (string, optional): Override THIS task's permission mode. Narrowing-only — a less-restrictive value clamps to the outer mode. Useful for marking review/audit tasks as "plan" or "deny" while sibling tasks keep full write access.
|
|
51
69
|
|
|
52
|
-
Example:
|
|
70
|
+
Example: parallel test-write + read-only review:
|
|
53
71
|
tasks: [
|
|
54
|
-
{ id: "
|
|
55
|
-
{ id: "
|
|
56
|
-
{ id: "c", prompt: "...", blockedBy: ["a", "b"] }
|
|
72
|
+
{ id: "tests", prompt: "Add tests for the new auth module" },
|
|
73
|
+
{ id: "review", prompt: "Audit the new auth module for security issues", permission_mode: "plan" }
|
|
57
74
|
]`;
|
|
58
75
|
},
|
|
59
76
|
};
|