Add Claude observability tracing and diagnostics UI
This commit is contained in:
@@ -190,6 +190,9 @@ function createActorSecurityContext(input: {
|
||||
type: `security.${event.type}`,
|
||||
severity: mapSecurityAuditSeverity(event),
|
||||
message: toSecurityAuditMessage(event),
|
||||
...(event.sessionId ? { sessionId: event.sessionId } : {}),
|
||||
...(event.nodeId ? { nodeId: event.nodeId } : {}),
|
||||
...(typeof event.attempt === "number" ? { attempt: event.attempt } : {}),
|
||||
metadata: toSecurityAuditMetadata(event),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -107,6 +107,8 @@ export type ResolvedExecutionContext = {
|
||||
|
||||
export type ActorExecutionInput = {
|
||||
sessionId: string;
|
||||
attempt: number;
|
||||
depth: number;
|
||||
node: PipelineNode;
|
||||
prompt: string;
|
||||
context: NodeExecutionContext;
|
||||
@@ -912,6 +914,8 @@ export class PipelineExecutor {
|
||||
|
||||
const result = await this.invokeActorExecutor({
|
||||
sessionId,
|
||||
attempt,
|
||||
depth: recursiveDepth,
|
||||
node,
|
||||
prompt,
|
||||
context,
|
||||
@@ -1065,6 +1069,8 @@ export class PipelineExecutor {
|
||||
|
||||
private async invokeActorExecutor(input: {
|
||||
sessionId: string;
|
||||
attempt: number;
|
||||
depth: number;
|
||||
node: PipelineNode;
|
||||
prompt: string;
|
||||
context: NodeExecutionContext;
|
||||
@@ -1077,12 +1083,17 @@ export class PipelineExecutor {
|
||||
|
||||
return await input.executor({
|
||||
sessionId: input.sessionId,
|
||||
attempt: input.attempt,
|
||||
depth: input.depth,
|
||||
node: input.node,
|
||||
prompt: input.prompt,
|
||||
context: input.context,
|
||||
signal: input.signal,
|
||||
executionContext: input.executionContext,
|
||||
mcp: this.buildActorMcpContext({
|
||||
sessionId: input.sessionId,
|
||||
nodeId: input.node.id,
|
||||
attempt: input.attempt,
|
||||
executionContext: input.executionContext,
|
||||
prompt: input.prompt,
|
||||
}),
|
||||
@@ -1207,6 +1218,9 @@ export class PipelineExecutor {
|
||||
}
|
||||
|
||||
private buildActorMcpContext(input: {
|
||||
sessionId: string;
|
||||
nodeId: string;
|
||||
attempt: number;
|
||||
executionContext: ResolvedExecutionContext;
|
||||
prompt: string;
|
||||
}): ActorExecutionMcpContext {
|
||||
@@ -1261,7 +1275,12 @@ export class PipelineExecutor {
|
||||
};
|
||||
|
||||
const createToolPermissionHandler = (): ActorToolPermissionHandler =>
|
||||
this.createToolPermissionHandler(executionContext.allowedTools);
|
||||
this.createToolPermissionHandler({
|
||||
allowedTools: executionContext.allowedTools,
|
||||
sessionId: input.sessionId,
|
||||
nodeId: input.nodeId,
|
||||
attempt: input.attempt,
|
||||
});
|
||||
|
||||
return {
|
||||
allowedTools: [...executionContext.allowedTools],
|
||||
@@ -1273,10 +1292,20 @@ export class PipelineExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
private createToolPermissionHandler(allowedTools: readonly string[]): ActorToolPermissionHandler {
|
||||
const allowset = new Set(allowedTools);
|
||||
private createToolPermissionHandler(input: {
|
||||
allowedTools: readonly string[];
|
||||
sessionId: string;
|
||||
nodeId: string;
|
||||
attempt: number;
|
||||
}): ActorToolPermissionHandler {
|
||||
const allowset = new Set(input.allowedTools);
|
||||
const rulesEngine = this.securityContext?.rulesEngine;
|
||||
const toolPolicy = toAllowedToolPolicy(allowedTools);
|
||||
const toolPolicy = toAllowedToolPolicy(input.allowedTools);
|
||||
const toolAuditContext = {
|
||||
sessionId: input.sessionId,
|
||||
nodeId: input.nodeId,
|
||||
attempt: input.attempt,
|
||||
};
|
||||
|
||||
return async (toolName, _input, options) => {
|
||||
const toolUseID = options.toolUseID;
|
||||
@@ -1295,6 +1324,7 @@ export class PipelineExecutor {
|
||||
rulesEngine?.assertToolInvocationAllowed({
|
||||
tool: candidates[0] ?? toolName,
|
||||
toolClearance: toolPolicy,
|
||||
context: toolAuditContext,
|
||||
});
|
||||
return {
|
||||
behavior: "deny",
|
||||
@@ -1307,6 +1337,7 @@ export class PipelineExecutor {
|
||||
rulesEngine?.assertToolInvocationAllowed({
|
||||
tool: allowMatch,
|
||||
toolClearance: toolPolicy,
|
||||
context: toolAuditContext,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -76,6 +76,11 @@ type GitExecutionResult = {
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
type GitWorktreeRecord = {
|
||||
path: string;
|
||||
branchRef?: string;
|
||||
};
|
||||
|
||||
function toErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
@@ -198,6 +203,40 @@ function toStringLines(value: string): string[] {
|
||||
.filter((line) => line.length > 0);
|
||||
}
|
||||
|
||||
function parseGitWorktreeRecords(value: string): GitWorktreeRecord[] {
|
||||
const lines = value.split("\n");
|
||||
const records: GitWorktreeRecord[] = [];
|
||||
let current: GitWorktreeRecord | undefined;
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) {
|
||||
if (current) {
|
||||
records.push(current);
|
||||
current = undefined;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("worktree ")) {
|
||||
if (current) {
|
||||
records.push(current);
|
||||
}
|
||||
current = {
|
||||
path: line.slice("worktree ".length).trim(),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("branch ") && current) {
|
||||
current.branchRef = line.slice("branch ".length).trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (current) {
|
||||
records.push(current);
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
export class FileSystemSessionMetadataStore {
|
||||
private readonly stateRoot: string;
|
||||
|
||||
@@ -383,11 +422,44 @@ export class SessionWorktreeManager {
|
||||
const worktreePath = maybeExisting
|
||||
? assertAbsolutePath(maybeExisting, "existingWorktreePath")
|
||||
: this.resolveTaskWorktreePath(input.sessionId, input.taskId);
|
||||
const branchName = this.resolveTaskBranchName(input.sessionId, input.taskId);
|
||||
const attachedWorktree = await this.findWorktreePathForBranch(baseWorkspacePath, branchName);
|
||||
|
||||
if (attachedWorktree && attachedWorktree !== worktreePath) {
|
||||
throw new Error(
|
||||
`Task branch "${branchName}" is already attached to worktree "${attachedWorktree}", ` +
|
||||
`expected "${worktreePath}".`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!(await pathExists(worktreePath))) {
|
||||
await runGit(["-C", baseWorkspacePath, "worktree", "prune", "--expire", "now"]);
|
||||
}
|
||||
|
||||
if (!(await pathExists(worktreePath))) {
|
||||
await mkdir(dirname(worktreePath), { recursive: true });
|
||||
const branchName = this.resolveTaskBranchName(input.sessionId, input.taskId);
|
||||
await runGit(["-C", baseWorkspacePath, "worktree", "add", "-B", branchName, worktreePath, "HEAD"]);
|
||||
const addResult = await runGitWithResult([
|
||||
"-C",
|
||||
baseWorkspacePath,
|
||||
"worktree",
|
||||
"add",
|
||||
"-B",
|
||||
branchName,
|
||||
worktreePath,
|
||||
"HEAD",
|
||||
]);
|
||||
if (addResult.exitCode !== 0) {
|
||||
const attachedAfterFailure = await this.findWorktreePathForBranch(baseWorkspacePath, branchName);
|
||||
if (attachedAfterFailure === worktreePath && (await pathExists(worktreePath))) {
|
||||
return {
|
||||
taskWorktreePath: worktreePath,
|
||||
};
|
||||
}
|
||||
throw new Error(
|
||||
`git -C ${baseWorkspacePath} worktree add -B ${branchName} ${worktreePath} HEAD failed: ` +
|
||||
`${toGitFailureMessage(addResult)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -687,4 +759,25 @@ export class SessionWorktreeManager {
|
||||
const mergeBase = result.stdout.trim();
|
||||
return mergeBase || undefined;
|
||||
}
|
||||
|
||||
private async findWorktreePathForBranch(
|
||||
repoPath: string,
|
||||
branchName: string,
|
||||
): Promise<string | undefined> {
|
||||
const branchRef = `refs/heads/${branchName}`;
|
||||
const records = await this.listWorktreeRecords(repoPath);
|
||||
const matched = records.find((record) => record.branchRef === branchRef);
|
||||
if (!matched) {
|
||||
return undefined;
|
||||
}
|
||||
return resolve(matched.path);
|
||||
}
|
||||
|
||||
private async listWorktreeRecords(repoPath: string): Promise<GitWorktreeRecord[]> {
|
||||
const result = await runGitWithResult(["-C", repoPath, "worktree", "list", "--porcelain"]);
|
||||
if (result.exitCode !== 0) {
|
||||
return [];
|
||||
}
|
||||
return parseGitWorktreeRecords(result.stdout);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user