This commit is contained in:
2026-02-24 18:57:20 -05:00
parent 45374a033b
commit 7727612ce9
36 changed files with 1331 additions and 70 deletions

View File

@@ -26,6 +26,7 @@ import type { JsonObject } from "./types.js";
import { SessionWorktreeManager, type SessionMetadata } from "./session-lifecycle.js";
import {
SecureCommandExecutor,
type SecurityViolationHandling,
type SecurityAuditEvent,
type SecurityAuditSink,
SecurityRulesEngine,
@@ -46,7 +47,7 @@ export type OrchestrationSettings = {
maxRetries: number;
maxChildren: number;
mergeConflictMaxAttempts: number;
securityViolationHandling: "hard_abort" | "validation_fail";
securityViolationHandling: SecurityViolationHandling;
runtimeContext: Record<string, string | number | boolean>;
};
@@ -211,6 +212,9 @@ function createActorSecurityContext(input: {
blockedEnvAssignments: ["AGENT_STATE_ROOT", "AGENT_PROJECT_CONTEXT_PATH"],
},
auditSink,
{
violationHandling: input.settings.securityViolationHandling,
},
);
return {
@@ -342,6 +346,7 @@ export class SchemaDrivenExecutionEngine {
this.sessionWorktreeManager = new SessionWorktreeManager({
worktreeRoot: resolve(this.settings.workspaceRoot, this.config.provisioning.gitWorktree.rootDirectory),
baseRef: this.config.provisioning.gitWorktree.baseRef,
targetPath: this.config.provisioning.gitWorktree.targetPath,
});
this.actorExecutors = toExecutorMap(input.actorExecutors);
@@ -426,7 +431,11 @@ export class SchemaDrivenExecutionEngine {
}): Promise<PipelineRunSummary> {
const managerSessionId = `${input.sessionId}__pipeline`;
const managerSession = this.manager.createSession(managerSessionId);
const workspaceRoot = input.sessionMetadata?.baseWorkspacePath ?? this.settings.workspaceRoot;
const workspaceRoot = input.sessionMetadata
? this.sessionWorktreeManager.resolveWorkingDirectoryForWorktree(
input.sessionMetadata.baseWorkspacePath,
)
: this.settings.workspaceRoot;
const projectContextStore = input.sessionMetadata
? new FileSystemProjectContextStore({
filePath: resolveSessionProjectContextPath(this.settings.stateRoot, input.sessionId),
@@ -531,6 +540,7 @@ export class SchemaDrivenExecutionEngine {
return {
taskId,
workingDirectory: ensured.taskWorkingDirectory,
worktreePath: ensured.taskWorktreePath,
statusAtStart,
...(existing?.metadata ? { metadata: existing.metadata } : {}),

View File

@@ -63,6 +63,7 @@ export type ActorExecutionResult = {
export type ActorToolPermissionResult =
| {
behavior: "allow";
updatedInput?: Record<string, unknown>;
toolUseID?: string;
}
| {
@@ -171,6 +172,7 @@ export type ActorExecutionSecurityContext = {
export type TaskExecutionResolution = {
taskId: string;
workingDirectory: string;
worktreePath: string;
statusAtStart: string;
metadata?: JsonObject;
@@ -941,7 +943,7 @@ export class PipelineExecutor {
node,
toolClearance,
prompt,
worktreePathOverride: taskResolution?.worktreePath,
worktreePathOverride: taskResolution?.workingDirectory,
});
const result = await this.invokeActorExecutor({
@@ -970,6 +972,7 @@ export class PipelineExecutor {
...(taskResolution
? {
taskId: taskResolution.taskId,
workingDirectory: taskResolution.workingDirectory,
worktreePath: taskResolution.worktreePath,
}
: {}),
@@ -1309,6 +1312,7 @@ export class PipelineExecutor {
const createToolPermissionHandler = (): ActorToolPermissionHandler =>
this.createToolPermissionHandler({
allowedTools: executionContext.allowedTools,
violationMode: executionContext.security.violationMode,
sessionId: input.sessionId,
nodeId: input.nodeId,
attempt: input.attempt,
@@ -1326,6 +1330,7 @@ export class PipelineExecutor {
private createToolPermissionHandler(input: {
allowedTools: readonly string[];
violationMode: SecurityViolationHandling;
sessionId: string;
nodeId: string;
attempt: number;
@@ -1340,7 +1345,7 @@ export class PipelineExecutor {
attempt: input.attempt,
};
return async (toolName, _input, options) => {
return async (toolName, toolInput, options) => {
const toolUseID = options.toolUseID;
if (options.signal.aborted) {
return {
@@ -1358,11 +1363,28 @@ export class PipelineExecutor {
caseInsensitiveLookup: caseInsensitiveAllowLookup,
});
if (!allowMatch) {
rulesEngine?.assertToolInvocationAllowed({
tool: candidates[0] ?? toolName,
toolClearance: toolPolicy,
context: toolAuditContext,
});
if (rulesEngine) {
try {
rulesEngine.assertToolInvocationAllowed({
tool: candidates[0] ?? toolName,
toolClearance: toolPolicy,
context: toolAuditContext,
});
} catch (error) {
if (
!(input.violationMode === "dangerous_warn_only" && error instanceof SecurityViolationError)
) {
throw error;
}
}
}
if (input.violationMode === "dangerous_warn_only") {
return {
behavior: "allow",
updatedInput: toolInput,
...(toolUseID ? { toolUseID } : {}),
};
}
return {
behavior: "deny",
message: `Tool "${toolName}" is not in the resolved execution allowlist.`,
@@ -1379,6 +1401,7 @@ export class PipelineExecutor {
return {
behavior: "allow",
updatedInput: toolInput,
...(toolUseID ? { toolUseID } : {}),
};
};

View File

@@ -358,13 +358,16 @@ export class FileSystemSessionMetadataStore {
export class SessionWorktreeManager {
private readonly worktreeRoot: string;
private readonly baseRef: string;
private readonly targetPath?: string;
constructor(input: {
worktreeRoot: string;
baseRef: string;
targetPath?: string;
}) {
this.worktreeRoot = assertAbsolutePath(input.worktreeRoot, "worktreeRoot");
this.baseRef = assertNonEmptyString(input.baseRef, "baseRef");
this.targetPath = normalizeWorktreeTargetPath(input.targetPath, "targetPath");
}
resolveBaseWorkspacePath(sessionId: string): string {
@@ -378,6 +381,11 @@ export class SessionWorktreeManager {
return resolve(this.worktreeRoot, scopedSession, "tasks", scopedTask);
}
resolveWorkingDirectoryForWorktree(worktreePath: string): string {
const normalizedWorktreePath = assertAbsolutePath(worktreePath, "worktreePath");
return this.targetPath ? resolve(normalizedWorktreePath, this.targetPath) : normalizedWorktreePath;
}
private resolveBaseBranchName(sessionId: string): string {
const scoped = sanitizeSegment(sessionId, "session");
return `ai-ops/${scoped}/base`;
@@ -399,14 +407,13 @@ export class SessionWorktreeManager {
await mkdir(dirname(baseWorkspacePath), { recursive: true });
const alreadyExists = await pathExists(baseWorkspacePath);
if (alreadyExists) {
return;
if (!(await pathExists(baseWorkspacePath))) {
const repoRoot = await runGit(["-C", projectPath, "rev-parse", "--show-toplevel"]);
const branchName = this.resolveBaseBranchName(input.sessionId);
await runGit(["-C", repoRoot, "worktree", "add", "-B", branchName, baseWorkspacePath, this.baseRef]);
}
const repoRoot = await runGit(["-C", projectPath, "rev-parse", "--show-toplevel"]);
const branchName = this.resolveBaseBranchName(input.sessionId);
await runGit(["-C", repoRoot, "worktree", "add", "-B", branchName, baseWorkspacePath, this.baseRef]);
await this.ensureWorktreeTargetPath(baseWorkspacePath);
}
async ensureTaskWorktree(input: {
@@ -416,6 +423,7 @@ export class SessionWorktreeManager {
existingWorktreePath?: string;
}): Promise<{
taskWorktreePath: string;
taskWorkingDirectory: string;
}> {
const baseWorkspacePath = assertAbsolutePath(input.baseWorkspacePath, "baseWorkspacePath");
const maybeExisting = input.existingWorktreePath?.trim();
@@ -451,8 +459,10 @@ export class SessionWorktreeManager {
if (addResult.exitCode !== 0) {
const attachedAfterFailure = await this.findWorktreePathForBranch(baseWorkspacePath, branchName);
if (attachedAfterFailure === worktreePath && (await pathExists(worktreePath))) {
const taskWorkingDirectory = await this.ensureWorktreeTargetPath(worktreePath);
return {
taskWorktreePath: worktreePath,
taskWorkingDirectory,
};
}
throw new Error(
@@ -462,8 +472,10 @@ export class SessionWorktreeManager {
}
}
const taskWorkingDirectory = await this.ensureWorktreeTargetPath(worktreePath);
return {
taskWorktreePath: worktreePath,
taskWorkingDirectory,
};
}
@@ -780,4 +792,69 @@ export class SessionWorktreeManager {
}
return parseGitWorktreeRecords(result.stdout);
}
private async ensureWorktreeTargetPath(worktreePath: string): Promise<string> {
if (this.targetPath) {
await runGit(["-C", worktreePath, "sparse-checkout", "init", "--cone"]);
await runGit(["-C", worktreePath, "sparse-checkout", "set", this.targetPath]);
}
const workingDirectory = this.resolveWorkingDirectoryForWorktree(worktreePath);
let workingDirectoryStats;
try {
workingDirectoryStats = await stat(workingDirectory);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
if (this.targetPath) {
throw new Error(
`Configured worktree target path "${this.targetPath}" is not a directory in ref "${this.baseRef}".`,
);
}
throw new Error(`Worktree path "${workingDirectory}" does not exist.`);
}
throw error;
}
if (!workingDirectoryStats.isDirectory()) {
if (this.targetPath) {
throw new Error(
`Configured worktree target path "${this.targetPath}" is not a directory in ref "${this.baseRef}".`,
);
}
throw new Error(`Worktree path "${workingDirectory}" is not a directory.`);
}
return workingDirectory;
}
}
function normalizeWorktreeTargetPath(value: string | undefined, key: string): string | undefined {
if (value === undefined) {
return undefined;
}
const trimmed = value.trim();
if (trimmed.length === 0) {
return undefined;
}
const slashNormalized = trimmed.replace(/\\/g, "/");
if (isAbsolute(slashNormalized) || /^[a-zA-Z]:\//.test(slashNormalized)) {
throw new Error(`${key} must be a relative path within the repository worktree.`);
}
const normalizedSegments = slashNormalized
.split("/")
.map((segment) => segment.trim())
.filter((segment) => segment.length > 0 && segment !== ".");
if (normalizedSegments.some((segment) => segment === "..")) {
throw new Error(`${key} must not contain ".." path segments.`);
}
if (normalizedSegments.length === 0) {
return undefined;
}
return normalizedSegments.join("/");
}