feat(FN-3961): propagate taskEnv into agent subprocess sessions
Propagates `taskEnv` (task-scoped environment variables) through the agent session creation pipeline, including executor sessions, spawned child agents, and workflow sessions. The engine's session factory (`agent-session-helpers.ts`), executor, PI agent creation (`pi.ts`), and step session executor Fusion-Task-Id: FN-3961
This commit is contained in:
5
.changeset/FN-3961-task-scoped-agent-env.md
Normal file
5
.changeset/FN-3961-task-scoped-agent-env.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Executor task runtime environment now flows through `createResolvedAgentSession()` and `createFnAgent()` into task-scoped agent subprocesses (including executor-session bash commands). Plugin-provided `executorRuntimeEnv` PATH/env contributions are available inside agent-issued subprocesses while remaining isolated per task/session with no global `process.env` mutation.
|
||||||
@@ -1,10 +1,22 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
extractRuntimeHint,
|
extractRuntimeHint,
|
||||||
resolveHeartbeatSessionModels,
|
resolveHeartbeatSessionModels,
|
||||||
resolveMergerSessionModel,
|
resolveMergerSessionModel,
|
||||||
} from "../agent-session-helpers.js";
|
} from "../agent-session-helpers.js";
|
||||||
|
|
||||||
|
const { resolveRuntimeMock } = vi.hoisted(() => ({
|
||||||
|
resolveRuntimeMock: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../runtime-resolution.js", async () => {
|
||||||
|
const actual = await vi.importActual<typeof import("../runtime-resolution.js")>("../runtime-resolution.js");
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resolveRuntime: resolveRuntimeMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe("extractRuntimeHint", () => {
|
describe("extractRuntimeHint", () => {
|
||||||
it("returns undefined for undefined config", () => {
|
it("returns undefined for undefined config", () => {
|
||||||
expect(extractRuntimeHint(undefined)).toBeUndefined();
|
expect(extractRuntimeHint(undefined)).toBeUndefined();
|
||||||
@@ -74,6 +86,48 @@ describe("resolveHeartbeatSessionModels", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("createResolvedAgentSession", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resolveRuntimeMock.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards taskEnv unchanged to runtime session factory", async () => {
|
||||||
|
const mockSession = { prompt: vi.fn() } as any;
|
||||||
|
const createSessionMock = vi.fn().mockResolvedValue({
|
||||||
|
session: mockSession,
|
||||||
|
sessionFile: "session.json",
|
||||||
|
});
|
||||||
|
resolveRuntimeMock.mockResolvedValue({
|
||||||
|
runtime: {
|
||||||
|
id: "pi",
|
||||||
|
name: "Default PI Runtime",
|
||||||
|
createSession: createSessionMock,
|
||||||
|
promptWithFallback: vi.fn(),
|
||||||
|
describeModel: vi.fn(() => "mock/model"),
|
||||||
|
},
|
||||||
|
runtimeId: "pi",
|
||||||
|
wasConfigured: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { createResolvedAgentSession } = await import("../agent-session-helpers.js");
|
||||||
|
|
||||||
|
const taskEnv = { PATH: "/tmp/bin", FUSION_TEST_VAR: "value" };
|
||||||
|
await createResolvedAgentSession({
|
||||||
|
sessionPurpose: "executor",
|
||||||
|
pluginRunner: undefined,
|
||||||
|
cwd: "/tmp/project",
|
||||||
|
systemPrompt: "system",
|
||||||
|
taskEnv,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createSessionMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
taskEnv,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("resolveMergerSessionModel", () => {
|
describe("resolveMergerSessionModel", () => {
|
||||||
it("uses assigned agent runtime model when both provider and modelId are present", () => {
|
it("uses assigned agent runtime model when both provider and modelId are present", () => {
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import type { PathLike } from "node:fs";
|
import type { PathLike } from "node:fs";
|
||||||
|
|
||||||
const createAgentSessionMock = vi.fn();
|
const createAgentSessionMock = vi.fn();
|
||||||
|
const createBashToolMock = vi.fn((cwd: string, options?: any) => ({ name: "bash", cwd, options }));
|
||||||
const createCodingToolsMock = vi.fn(() => []);
|
const createCodingToolsMock = vi.fn(() => []);
|
||||||
const createReadOnlyToolsMock = vi.fn(() => []);
|
const createReadOnlyToolsMock = vi.fn(() => []);
|
||||||
const createExtensionRuntimeMock = vi.fn();
|
const createExtensionRuntimeMock = vi.fn();
|
||||||
@@ -81,7 +82,7 @@ vi.mock("@mariozechner/pi-coding-agent", () => ({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
createAgentSession: createAgentSessionMock,
|
createAgentSession: createAgentSessionMock,
|
||||||
createBashTool: () => ({ name: "bash" }),
|
createBashTool: createBashToolMock,
|
||||||
createCodingTools: createCodingToolsMock,
|
createCodingTools: createCodingToolsMock,
|
||||||
createEditTool: () => ({ name: "edit" }),
|
createEditTool: () => ({ name: "edit" }),
|
||||||
createExtensionRuntime: createExtensionRuntimeMock,
|
createExtensionRuntime: createExtensionRuntimeMock,
|
||||||
@@ -810,6 +811,7 @@ describe("createFnAgent", () => {
|
|||||||
readFileSyncMock.mockReturnValue("{}");
|
readFileSyncMock.mockReturnValue("{}");
|
||||||
readCustomProvidersMock.mockReturnValue([]);
|
readCustomProvidersMock.mockReturnValue([]);
|
||||||
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
|
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
|
||||||
|
createBashToolMock.mockClear();
|
||||||
createAgentSessionMock.mockResolvedValue({
|
createAgentSessionMock.mockResolvedValue({
|
||||||
session: {
|
session: {
|
||||||
prompt: vi.fn(),
|
prompt: vi.fn(),
|
||||||
@@ -820,6 +822,55 @@ describe("createFnAgent", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes task-scoped env into bash spawn hook when provided", async () => {
|
||||||
|
const { createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
|
await createFnAgent({
|
||||||
|
cwd: "/project",
|
||||||
|
systemPrompt: "test",
|
||||||
|
tools: "coding",
|
||||||
|
taskEnv: { PATH: "/task/bin", TASK_ONLY: "1" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createBashToolMock).toHaveBeenCalledWith(
|
||||||
|
"/project",
|
||||||
|
expect.objectContaining({
|
||||||
|
spawnHook: expect.any(Function),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const spawnHook = createBashToolMock.mock.calls.at(-1)?.[1]?.spawnHook;
|
||||||
|
const originalEnv = { PATH: "/base/bin", HOME: "/home/user" };
|
||||||
|
const spawned = spawnHook({
|
||||||
|
command: "echo hi",
|
||||||
|
cwd: "/project",
|
||||||
|
env: originalEnv,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(spawned).toEqual({
|
||||||
|
command: "echo hi",
|
||||||
|
cwd: "/project",
|
||||||
|
env: {
|
||||||
|
PATH: "/task/bin",
|
||||||
|
HOME: "/home/user",
|
||||||
|
TASK_ONLY: "1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(originalEnv).toEqual({ PATH: "/base/bin", HOME: "/home/user" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps bash tool default behavior when taskEnv is not provided", async () => {
|
||||||
|
const { createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
|
await createFnAgent({
|
||||||
|
cwd: "/project",
|
||||||
|
systemPrompt: "test",
|
||||||
|
tools: "coding",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createBashToolMock).toHaveBeenCalledWith("/project", undefined);
|
||||||
|
});
|
||||||
|
|
||||||
it("refuses to start a coding agent in an unregistered worktree", async () => {
|
it("refuses to start a coding agent in an unregistered worktree", async () => {
|
||||||
existsSyncMock.mockImplementation((path) => {
|
existsSyncMock.mockImplementation((path) => {
|
||||||
const value = String(path);
|
const value = String(path);
|
||||||
|
|||||||
@@ -737,6 +737,32 @@ describe("StepSessionExecutor", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("sequential execution", () => {
|
describe("sequential execution", () => {
|
||||||
|
it("forwards taskEnv into step session creation", async () => {
|
||||||
|
const prompt = makeStepPrompt("FN-001", 1);
|
||||||
|
const task = makeTaskDetail({ prompt, steps: [{ name: "Step 0", status: "pending" }] });
|
||||||
|
const settings = makeSettings({ maxParallelSteps: 1 });
|
||||||
|
const session = makeMockSession();
|
||||||
|
mockedCreateFnAgent.mockResolvedValue({ session } as any);
|
||||||
|
|
||||||
|
const executor = new StepSessionExecutor({
|
||||||
|
taskDetail: task,
|
||||||
|
worktreePath: "/project/.worktrees/main",
|
||||||
|
rootDir: "/project",
|
||||||
|
settings,
|
||||||
|
pluginRunner: undefined,
|
||||||
|
taskEnv: { PATH: "/task/bin", TASK_ONLY: "1" },
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const result = await executor.executeAll();
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]?.success).toBe(true);
|
||||||
|
expect(mockedCreateFnAgent).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
taskEnv: { PATH: "/task/bin", TASK_ONLY: "1" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("happy path: 3-step task, all steps succeed", async () => {
|
it("happy path: 3-step task, all steps succeed", async () => {
|
||||||
const prompt = makeStepPrompt("FN-001", 3);
|
const prompt = makeStepPrompt("FN-001", 3);
|
||||||
const task = makeTaskDetail({ prompt, steps: [
|
const task = makeTaskDetail({ prompt, steps: [
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ export interface AgentRuntimeOptions {
|
|||||||
skills?: string[];
|
skills?: string[];
|
||||||
/** Runtime-facing context for non-pi runtimes that cannot consume JS ToolDefinition objects directly. */
|
/** Runtime-facing context for non-pi runtimes that cannot consume JS ToolDefinition objects directly. */
|
||||||
runtimeContext?: AgentRuntimeContext;
|
runtimeContext?: AgentRuntimeContext;
|
||||||
|
/** Optional task-scoped environment variables for session-local subprocesses. */
|
||||||
|
taskEnv?: NodeJS.ProcessEnv;
|
||||||
/**
|
/**
|
||||||
* Last-chance abort hook fired by the runtime *immediately before* the
|
* Last-chance abort hook fired by the runtime *immediately before* the
|
||||||
* underlying LLM session is instantiated — i.e., after all of the runtime's
|
* underlying LLM session is instantiated — i.e., after all of the runtime's
|
||||||
|
|||||||
@@ -45,13 +45,9 @@ export interface ResolvedSessionOptions extends AgentRuntimeOptions {
|
|||||||
/** Optional runtime hint from task/agent configuration */
|
/** Optional runtime hint from task/agent configuration */
|
||||||
runtimeHint?: string;
|
runtimeHint?: string;
|
||||||
/**
|
/**
|
||||||
* `beforeSpawnSession` is inherited from {@link AgentRuntimeOptions} — see
|
* `beforeSpawnSession` and `taskEnv` are inherited from
|
||||||
* its definition there for the contract. Callers (e.g. the reviewer's
|
* {@link AgentRuntimeOptions}. Both are forwarded verbatim to
|
||||||
* pause gate) throw from this callback to cancel session creation when
|
* `runtime.createSession()`.
|
||||||
* external state changed during the async setup window. Forwarded
|
|
||||||
* verbatim to `runtime.createSession()`; the runtime is responsible for
|
|
||||||
* invoking it at its latest synchronous point before the underlying LLM
|
|
||||||
* session is instantiated.
|
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2507,9 +2507,9 @@ export class TaskExecutor {
|
|||||||
});
|
});
|
||||||
const pathPrepend = runtimeEnvContribution?.pathPrepend ?? [];
|
const pathPrepend = runtimeEnvContribution?.pathPrepend ?? [];
|
||||||
const injectedEnv = runtimeEnvContribution?.env ?? {};
|
const injectedEnv = runtimeEnvContribution?.env ?? {};
|
||||||
// We intentionally do NOT mutate process.env globally. Agent session subprocesses
|
// We intentionally do NOT mutate process.env globally. This task-scoped env is
|
||||||
// currently inherit the engine process env only; piping taskEnv into AgentRuntimeOptions
|
// passed through AgentRuntimeOptions so executor session subprocesses inherit it
|
||||||
// is tracked as follow-up work.
|
// without leaking across concurrent tasks.
|
||||||
taskEnv = {
|
taskEnv = {
|
||||||
...process.env,
|
...process.env,
|
||||||
...injectedEnv,
|
...injectedEnv,
|
||||||
@@ -3013,7 +3013,7 @@ export class TaskExecutor {
|
|||||||
...(executionMode !== "fast" ? [
|
...(executionMode !== "fast" ? [
|
||||||
this.createReviewStepTool(task.id, worktreePath, detail.prompt, codeReviewVerdicts, sessionRef, stepCheckpoints, detail, stuckDetector),
|
this.createReviewStepTool(task.id, worktreePath, detail.prompt, codeReviewVerdicts, sessionRef, stepCheckpoints, detail, stuckDetector),
|
||||||
] : []),
|
] : []),
|
||||||
this.createSpawnAgentTool(task.id, worktreePath, settings),
|
this.createSpawnAgentTool(task.id, worktreePath, settings, taskEnv),
|
||||||
this.createTaskDocumentWriteTool(task.id),
|
this.createTaskDocumentWriteTool(task.id),
|
||||||
this.createTaskDocumentReadTool(task.id),
|
this.createTaskDocumentReadTool(task.id),
|
||||||
...(isResearchToolSurfaceEnabled(settings)
|
...(isResearchToolSurfaceEnabled(settings)
|
||||||
@@ -3142,6 +3142,7 @@ export class TaskExecutor {
|
|||||||
fallbackModelId: executorFallbackModelId,
|
fallbackModelId: executorFallbackModelId,
|
||||||
defaultThinkingLevel: executorThinkingLevel,
|
defaultThinkingLevel: executorThinkingLevel,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
|
taskEnv,
|
||||||
// Skill selection: use assigned agent skills if available, otherwise role fallback
|
// Skill selection: use assigned agent skills if available, otherwise role fallback
|
||||||
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
||||||
actionGateContext: this.buildActionGateContext(task.id, assignedAgent),
|
actionGateContext: this.buildActionGateContext(task.id, assignedAgent),
|
||||||
@@ -3472,6 +3473,7 @@ export class TaskExecutor {
|
|||||||
fallbackModelId: executorFallbackModelId,
|
fallbackModelId: executorFallbackModelId,
|
||||||
defaultThinkingLevel: executorThinkingLevel,
|
defaultThinkingLevel: executorThinkingLevel,
|
||||||
sessionManager: SessionManager.create(worktreePath),
|
sessionManager: SessionManager.create(worktreePath),
|
||||||
|
taskEnv,
|
||||||
// Skill selection: use assigned agent skills if available, otherwise role fallback
|
// Skill selection: use assigned agent skills if available, otherwise role fallback
|
||||||
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
||||||
actionGateContext: this.buildActionGateContext(task.id, assignedAgent),
|
actionGateContext: this.buildActionGateContext(task.id, assignedAgent),
|
||||||
@@ -4927,6 +4929,7 @@ Do not refactor, rename broadly, or make opportunistic improvements.
|
|||||||
defaultProvider: executorProvider,
|
defaultProvider: executorProvider,
|
||||||
defaultModelId: executorModelId,
|
defaultModelId: executorModelId,
|
||||||
defaultThinkingLevel: settings.defaultThinkingLevel,
|
defaultThinkingLevel: settings.defaultThinkingLevel,
|
||||||
|
taskEnv: extraEnv,
|
||||||
...(skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
...(skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -5369,7 +5372,7 @@ ${failureFeedback}
|
|||||||
try {
|
try {
|
||||||
const result: WorkflowStepOutcome = stepMode === "script"
|
const result: WorkflowStepOutcome = stepMode === "script"
|
||||||
? await this.executeScriptWorkflowStep(task, ws, worktreePath, settings, taskEnv)
|
? await this.executeScriptWorkflowStep(task, ws, worktreePath, settings, taskEnv)
|
||||||
: await this.executeWorkflowStep(task, ws, worktreePath, settings);
|
: await this.executeWorkflowStep(task, ws, worktreePath, settings, taskEnv);
|
||||||
if (await this.shouldDeferWorkflowStepCompletion(task.id, `workflow step '${ws.name}'`)) {
|
if (await this.shouldDeferWorkflowStepCompletion(task.id, `workflow step '${ws.name}'`)) {
|
||||||
return "deferred-paused";
|
return "deferred-paused";
|
||||||
}
|
}
|
||||||
@@ -5532,6 +5535,7 @@ ${failureFeedback}
|
|||||||
workflowStep: WorkflowStep,
|
workflowStep: WorkflowStep,
|
||||||
worktreePath: string,
|
worktreePath: string,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
|
taskEnv?: NodeJS.ProcessEnv,
|
||||||
): Promise<WorkflowStepOutcome> {
|
): Promise<WorkflowStepOutcome> {
|
||||||
const toolMode: "coding" | "readonly" = workflowStep.toolMode || "readonly";
|
const toolMode: "coding" | "readonly" = workflowStep.toolMode || "readonly";
|
||||||
|
|
||||||
@@ -5684,6 +5688,7 @@ and show an appropriate message to the user.\`
|
|||||||
fallbackProvider: settings.fallbackProvider,
|
fallbackProvider: settings.fallbackProvider,
|
||||||
fallbackModelId: settings.fallbackModelId,
|
fallbackModelId: settings.fallbackModelId,
|
||||||
defaultThinkingLevel: settings.defaultThinkingLevel,
|
defaultThinkingLevel: settings.defaultThinkingLevel,
|
||||||
|
taskEnv,
|
||||||
// Skill selection: use assigned agent skills if available, otherwise role fallback
|
// Skill selection: use assigned agent skills if available, otherwise role fallback
|
||||||
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
||||||
});
|
});
|
||||||
@@ -7152,7 +7157,12 @@ and show an appropriate message to the user.\`
|
|||||||
* Create the fn_spawn_agent tool definition.
|
* Create the fn_spawn_agent tool definition.
|
||||||
* Allows the parent agent to spawn child agents with delegated tasks.
|
* Allows the parent agent to spawn child agents with delegated tasks.
|
||||||
*/
|
*/
|
||||||
private createSpawnAgentTool(taskId: string, worktreePath: string, settings: Settings): ToolDefinition {
|
private createSpawnAgentTool(
|
||||||
|
taskId: string,
|
||||||
|
worktreePath: string,
|
||||||
|
settings: Settings,
|
||||||
|
taskEnv?: NodeJS.ProcessEnv,
|
||||||
|
): ToolDefinition {
|
||||||
return {
|
return {
|
||||||
name: "fn_spawn_agent",
|
name: "fn_spawn_agent",
|
||||||
label: "Spawn Agent",
|
label: "Spawn Agent",
|
||||||
@@ -7264,6 +7274,7 @@ Child agent: ${agent.id} (${name})`;
|
|||||||
defaultModelId: childExecutorModelId,
|
defaultModelId: childExecutorModelId,
|
||||||
fallbackProvider: settings.fallbackProvider,
|
fallbackProvider: settings.fallbackProvider,
|
||||||
fallbackModelId: settings.fallbackModelId,
|
fallbackModelId: settings.fallbackModelId,
|
||||||
|
taskEnv,
|
||||||
// Skill selection: use assigned agent skills if available, otherwise role fallback
|
// Skill selection: use assigned agent skills if available, otherwise role fallback
|
||||||
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -749,6 +749,8 @@ export interface AgentOptions {
|
|||||||
* (and `skillSelection` is not), auto-constructs a SkillSelectionContext
|
* (and `skillSelection` is not), auto-constructs a SkillSelectionContext
|
||||||
* from the cwd and these names. Ignored when `skillSelection` is set. */
|
* from the cwd and these names. Ignored when `skillSelection` is set. */
|
||||||
skills?: string[];
|
skills?: string[];
|
||||||
|
/** Optional task-scoped env injected into this session's subprocess tools only. */
|
||||||
|
taskEnv?: NodeJS.ProcessEnv;
|
||||||
/** Last-chance abort hook fired immediately before `createAgentSession`.
|
/** Last-chance abort hook fired immediately before `createAgentSession`.
|
||||||
* See `AgentRuntimeOptions.beforeSpawnSession`. */
|
* See `AgentRuntimeOptions.beforeSpawnSession`. */
|
||||||
beforeSpawnSession?: () => Promise<void> | void;
|
beforeSpawnSession?: () => Promise<void> | void;
|
||||||
@@ -1598,6 +1600,19 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
|
|||||||
// Grep→grep). When a coding session ran via Claude CLI tried `Glob`, pi
|
// Grep→grep). When a coding session ran via Claude CLI tried `Glob`, pi
|
||||||
// returned "Tool find not found" and the agent looped. Compose explicitly
|
// returned "Tool find not found" and the agent looped. Compose explicitly
|
||||||
// so every tool referenced by tool-mapping.ts is registered.
|
// so every tool referenced by tool-mapping.ts is registered.
|
||||||
|
const bashToolOptions = options.taskEnv
|
||||||
|
? {
|
||||||
|
spawnHook: ({ command, cwd, env }: { command: string; cwd: string; env: NodeJS.ProcessEnv }) => ({
|
||||||
|
command,
|
||||||
|
cwd,
|
||||||
|
env: {
|
||||||
|
...env,
|
||||||
|
...options.taskEnv,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const tools =
|
const tools =
|
||||||
options.tools === "readonly"
|
options.tools === "readonly"
|
||||||
? [
|
? [
|
||||||
@@ -1608,7 +1623,7 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
|
|||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
createReadTool(options.cwd),
|
createReadTool(options.cwd),
|
||||||
createBashTool(options.cwd),
|
createBashTool(options.cwd, bashToolOptions),
|
||||||
createEditTool(options.cwd),
|
createEditTool(options.cwd),
|
||||||
createWriteTool(options.cwd),
|
createWriteTool(options.cwd),
|
||||||
createGrepTool(options.cwd),
|
createGrepTool(options.cwd),
|
||||||
|
|||||||
@@ -1004,6 +1004,7 @@ Follow instructions precisely and avoid unrelated changes.`,
|
|||||||
taskId: taskDetail.id,
|
taskId: taskDetail.id,
|
||||||
taskTitle: taskDetail.title,
|
taskTitle: taskDetail.title,
|
||||||
}),
|
}),
|
||||||
|
taskEnv: this.options.taskEnv,
|
||||||
});
|
});
|
||||||
session = createResult.session;
|
session = createResult.session;
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ Generated artifacts are expected under:
|
|||||||
When the plugin contributes `executorRuntimeEnv`, executor-spawned task commands receive extra runtime wiring:
|
When the plugin contributes `executorRuntimeEnv`, executor-spawned task commands receive extra runtime wiring:
|
||||||
|
|
||||||
- Generated CLI artifact directories for each service's latest `generated` spec are prepended to task `PATH` (deduped, absolute paths only).
|
- Generated CLI artifact directories for each service's latest `generated` spec are prepended to task `PATH` (deduped, absolute paths only).
|
||||||
- Credentials with `kind: "env_var"` are decoded and injected as environment variables for task subprocesses.
|
- Credentials with `kind: "env_var"` are decoded and injected as environment variables for task subprocesses, including executor agent-session subprocesses (for example `bash` tool commands run inside `createFnAgent(...)`).
|
||||||
- Non-env credential kinds (`header`, `query_param`, `basic_auth`, `bearer_token`, `api_key`) are intentionally excluded from env injection and remain request-time concerns.
|
- Non-env credential kinds (`header`, `query_param`, `basic_auth`, `bearer_token`, `api_key`) are intentionally excluded from env injection and remain request-time concerns.
|
||||||
|
|
||||||
Security model:
|
Security model:
|
||||||
|
|||||||
Reference in New Issue
Block a user