@zhijiewang/openharness 2.35.0 → 2.36.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 CHANGED
@@ -563,6 +563,17 @@ 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
+
566
577
  ## Headless Mode
567
578
 
568
579
  Run a single prompt without interactive UI — perfect for CI/CD and scripting:
package/README.zh-CN.md CHANGED
@@ -563,6 +563,17 @@ 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
+
566
577
  ## 无头模式
567
578
 
568
579
  跑一次提示词,不走交互 UI —— 适合 CI/CD 和脚本化:
@@ -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,16 @@ 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
+ const parentMode = context.permissionMode ?? "trust";
113
+ const subagentMode = clampSubagentPermissionMode(parentMode, input.permission_mode);
104
114
  const config = {
105
115
  provider: context.provider,
106
116
  tools: agentTools,
107
117
  systemPrompt,
108
- permissionMode: context.permissionMode ?? "trust",
118
+ permissionMode: subagentMode,
109
119
  model: agentModel,
110
120
  maxTurns: 20,
111
121
  abortSignal: context.abortSignal,
@@ -225,7 +235,8 @@ export const AgentTool = {
225
235
  - run_in_background (boolean, optional): Run the agent in the background. Returns immediately; you will be notified when it completes.
226
236
  - model (string, optional): Override the model for this sub-agent (e.g., use a faster model for exploration).
227
237
  - 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.`;
238
+ - 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.
239
+ - 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
240
  },
230
241
  };
231
242
  //# 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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhijiewang/openharness",
3
- "version": "2.35.0",
3
+ "version": "2.36.0",
4
4
  "description": "Open-source terminal coding agent. Works with any LLM.",
5
5
  "type": "module",
6
6
  "bin": {