From b95c3691ef3a4a8f49980702dd54ad47035e7142 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Sun, 17 May 2026 16:33:19 +0800 Subject: [PATCH 1/4] fix(humanize2): pass agentDefaults to AgentRunCoordinator and support per-agent permission/sandbox/extraArgs Two fixes for humanize2 workflow agent launches: 1. **Critical bug**: `AgentRunCoordinator` was constructed without `agentDefaults`, so `permissionMode`, `sandbox`, and `extraArgs` from `~/.h2/config.yaml` were silently ignored for all workflow-spawned agents. This caused: - Claude agents to run with default permission mode, blocking MCP tool calls (artifact_deliver, etc.) that require user approval in headless mode - Codex agents to fail with "Not inside a trusted directory" when the working directory is not a git repo 2. **Extended config surface**: `AgentModelDefaults` now supports `permissionMode`, `sandbox`, and `extraArgs` per agent. The YAML parser handles both inline comma-separated values and multi-line list items for extraArgs. With this fix, users can configure in `~/.h2/config.yaml`: agents: claude: permissionMode: bypassPermissions codex: extraArgs: - --dangerously-bypass-approvals-and-sandbox - --skip-git-repo-check Closes #165 --- src/config.ts | 43 ++++++++++++++++++++++++++++++++++++++++--- src/hub-server.ts | 3 ++- src/hub/runs.ts | 3 +++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/config.ts b/src/config.ts index f499b58f..0eccdab2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; -import type { AgentId } from "./agents/types.js"; +import type { AgentId, PermissionMode, SandboxMode } from "./agents/types.js"; export const DEFAULT_RUN_TIMEOUT_MS = 6 * 60 * 60 * 1000; export const DEFAULT_DASHBOARD_THEME: DashboardTheme = "dark"; @@ -12,6 +12,9 @@ export type DashboardTheme = "light" | "dark"; export interface AgentModelDefaults { model?: string; reasoningEffort?: string; + permissionMode?: PermissionMode; + sandbox?: SandboxMode; + extraArgs?: string[]; } export type AgentModelDefaultsByAgent = Partial>; @@ -176,6 +179,20 @@ function parseSimpleYaml( continue; } + // agent extraArgs list items (e.g., ` - --skip-git-repo-check`) + if (section === "agents" && indent >= 6 && currentAgent !== undefined && trimmed.startsWith("- ")) { + const item = trimmed.slice(2).trim().replace(/^["']|["']$/g, ""); + if (item.length > 0) { + const defaults = values.agentDefaults?.[currentAgent] ?? {}; + defaults.extraArgs = [...(defaults.extraArgs ?? []), item]; + values.agentDefaults = { + ...values.agentDefaults, + [currentAgent]: defaults + }; + } + continue; + } + const separatorIndex = trimmed.indexOf(":"); if (separatorIndex < 0) { continue; @@ -245,6 +262,24 @@ function parseSimpleYaml( if (key === "reasoningEffort") { defaults.reasoningEffort = value; } + if (key === "permissionMode") { + defaults.permissionMode = value as PermissionMode; + } + if (key === "sandbox") { + defaults.sandbox = value as SandboxMode; + } + values.agentDefaults = { + ...values.agentDefaults, + [currentAgent]: defaults + }; + } + if (section === "agents" && indent >= 4 && currentAgent !== undefined && key === "extraArgs") { + const defaults = values.agentDefaults?.[currentAgent] ?? {}; + if (value === "[]" || value.length === 0) { + defaults.extraArgs = []; + } else { + defaults.extraArgs = parseCommaList(value); + } values.agentDefaults = { ...values.agentDefaults, [currentAgent]: defaults @@ -262,11 +297,13 @@ function mergeAgentDefaults( return { codex: { ...defaults.codex, - ...overrides?.codex + ...overrides?.codex, + extraArgs: overrides?.codex?.extraArgs ?? defaults.codex?.extraArgs }, claude: { ...defaults.claude, - ...overrides?.claude + ...overrides?.claude, + extraArgs: overrides?.claude?.extraArgs ?? defaults.claude?.extraArgs } }; } diff --git a/src/hub-server.ts b/src/hub-server.ts index 05b39864..11386839 100644 --- a/src/hub-server.ts +++ b/src/hub-server.ts @@ -30,7 +30,8 @@ async function main(): Promise { jsonRpcUrl, store, initialRuns, - defaultRunTimeoutMs: config.defaultRunTimeoutMs + defaultRunTimeoutMs: config.defaultRunTimeoutMs, + agentDefaults: config.agentDefaults }); const workflowCoordinator = new WorkflowCoordinator(coordinator, { store: workflowStore, diff --git a/src/hub/runs.ts b/src/hub/runs.ts index 09b5dade..c3d6e96c 100644 --- a/src/hub/runs.ts +++ b/src/hub/runs.ts @@ -147,6 +147,9 @@ export class AgentRunCoordinator { ...input, model: input.model ?? defaults.model, reasoningEffort: input.reasoningEffort ?? defaults.reasoningEffort, + permissionMode: input.permissionMode ?? defaults.permissionMode, + sandbox: input.sandbox ?? defaults.sandbox, + extraArgs: input.extraArgs ?? defaults.extraArgs, cwd: input.cwd ?? parentRun?.cwd, workflowContext: input.workflowContext ?? inheritedWorkflowContext }; From bd0d0e6dce27b82d3b3e3b23e1ce0727f5c89d07 Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Sun, 17 May 2026 17:41:16 +0800 Subject: [PATCH 2/4] fix(humanize2): restrict extraArgs list-item parser to explicit section state Scope the YAML list-item handler to only consume lines inside an agents.extraArgs block (similar to workflow.scripts.allow tracking), preventing unrelated `- ` lines under agent stanzas from being silently captured as CLI arguments. --- src/config.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/config.ts b/src/config.ts index 0eccdab2..533244c8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -158,7 +158,7 @@ function parseSimpleYaml( const values: Partial> = { agentDefaults: {} }; - let section: "agents" | "workflow" | "workflow.softEnforcement" | "workflow.scripts" | "workflow.scripts.allow" | undefined; + let section: "agents" | "workflow" | "workflow.softEnforcement" | "workflow.scripts" | "workflow.scripts.allow" | "agents.extraArgs" | undefined; let currentAgent: AgentId | undefined; for (const line of contents.split("\n")) { @@ -180,7 +180,7 @@ function parseSimpleYaml( } // agent extraArgs list items (e.g., ` - --skip-git-repo-check`) - if (section === "agents" && indent >= 6 && currentAgent !== undefined && trimmed.startsWith("- ")) { + if (section === "agents.extraArgs" && indent >= 6 && currentAgent !== undefined && trimmed.startsWith("- ")) { const item = trimmed.slice(2).trim().replace(/^["']|["']$/g, ""); if (item.length > 0) { const defaults = values.agentDefaults?.[currentAgent] ?? {}; @@ -250,7 +250,7 @@ function parseSimpleYaml( section = "workflow.scripts.allow"; continue; } - if (section === "agents" && indent === 2) { + if ((section === "agents" || section === "agents.extraArgs") && indent === 2) { currentAgent = key === "codex" || key === "claude" ? key : undefined; continue; } @@ -277,6 +277,7 @@ function parseSimpleYaml( const defaults = values.agentDefaults?.[currentAgent] ?? {}; if (value === "[]" || value.length === 0) { defaults.extraArgs = []; + section = "agents.extraArgs"; } else { defaults.extraArgs = parseCommaList(value); } From 557a19cb3fa58c216bc9d6f7cd0f4dca8e059c2e Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Sun, 17 May 2026 19:05:05 +0800 Subject: [PATCH 3/4] fix(humanize2): reset section to "agents" when switching agent in config parser When the parser was in `agents.extraArgs` state (multiline list) and encountered a new indent-2 agent header, it updated currentAgent but left section unchanged. Subsequent model/reasoningEffort/permissionMode/sandbox fields for that agent were silently ignored because their handler requires section === "agents". --- src/config.ts | 1 + tests/config.test.ts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/config.ts b/src/config.ts index 533244c8..8f649cc0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -252,6 +252,7 @@ function parseSimpleYaml( } if ((section === "agents" || section === "agents.extraArgs") && indent === 2) { currentAgent = key === "codex" || key === "claude" ? key : undefined; + section = "agents"; continue; } if (section === "agents" && indent >= 4 && currentAgent !== undefined && value.length > 0) { diff --git a/tests/config.test.ts b/tests/config.test.ts index aefc78d9..e7e80a3c 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -93,6 +93,42 @@ describe("humanize2 user config", () => { expect(config.defaultTheme).toBe("light"); }); + it("loads agent model fields after a prior agent's multiline extraArgs list", async () => { + const home = await tempDirectory(); + const configPath = join(home, "multiline-extraargs-config.yaml"); + + const contents = [ + "version: 1", + "cacheDir: /tmp/humanize2-cache", + "defaultRunTimeoutMs: 12345", + "defaultTheme: dark", + "agents:", + " codex:", + " model: gpt-5.4", + " extraArgs:", + " - --temperature", + " - 0.7", + " - --max-tokens", + " - 4096", + " claude:", + " model: claude-opus-4-7", + " reasoningEffort: xhigh", + "" + ].join("\n"); + await writeFile(configPath, contents, "utf8"); + + const config = await loadHumanizeConfig({ HUMANIZE2_CONFIG: configPath }, home); + + expect(config.agentDefaults.codex).toMatchObject({ + model: "gpt-5.4", + extraArgs: ["--temperature", "0.7", "--max-tokens", "4096"] + }); + expect(config.agentDefaults.claude).toMatchObject({ + model: "claude-opus-4-7", + reasoningEffort: "xhigh" + }); + }); + it("loads workflow retry and script allowlist settings from the user config", async () => { const home = await tempDirectory(); const configPath = join(home, "workflow-config.yaml"); From fee2ec7138e0f8fa1f34ccc858f9f0b0ad18228b Mon Sep 17 00:00:00 2001 From: Chao Liu Date: Sun, 17 May 2026 19:22:01 +0800 Subject: [PATCH 4/4] fix(humanize2): reset extraArgs section when list ends for sibling fields When extraArgs: [] switched section to "agents.extraArgs", subsequent sibling fields (model, reasoningEffort, permissionMode, sandbox) under the same agent were silently ignored because section was never reverted. Now reset section back to "agents" when a non-list-item line is encountered at indent >= 4. --- src/config.ts | 6 ++++++ tests/config.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/config.ts b/src/config.ts index 8f649cc0..7036aab1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -193,6 +193,12 @@ function parseSimpleYaml( continue; } + // extraArgs list is done — reset section so sibling fields (model, + // permissionMode, sandbox) under the same agent are not silently ignored + if (section === "agents.extraArgs" && indent >= 4 && currentAgent !== undefined && !trimmed.startsWith("- ")) { + section = "agents"; + } + const separatorIndex = trimmed.indexOf(":"); if (separatorIndex < 0) { continue; diff --git a/tests/config.test.ts b/tests/config.test.ts index e7e80a3c..c57a2fec 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -129,6 +129,33 @@ describe("humanize2 user config", () => { }); }); + it("loads sibling fields after empty extraArgs under the same agent", async () => { + const home = await tempDirectory(); + const configPath = join(home, "sibling-after-extraargs-config.yaml"); + + const contents = [ + "version: 1", + "cacheDir: /tmp/humanize2-cache", + "defaultRunTimeoutMs: 12345", + "defaultTheme: dark", + "agents:", + " codex:", + " extraArgs: []", + " model: gpt-5.5", + " reasoningEffort: xhigh", + "" + ].join("\n"); + await writeFile(configPath, contents, "utf8"); + + const config = await loadHumanizeConfig({ HUMANIZE2_CONFIG: configPath }, home); + + expect(config.agentDefaults.codex).toMatchObject({ + model: "gpt-5.5", + reasoningEffort: "xhigh", + extraArgs: [] + }); + }); + it("loads workflow retry and script allowlist settings from the user config", async () => { const home = await tempDirectory(); const configPath = join(home, "workflow-config.yaml");