feat: add verification evidence presets

This commit is contained in:
ejclaw
2026-05-26 02:04:04 +09:00
parent 48ee682b3e
commit c4c590ec9b
14 changed files with 1435 additions and 75 deletions

View File

@@ -4,6 +4,39 @@ import path from 'path';
export const HOST_EVIDENCE_ACTIONS = [
'ejclaw_service_status',
'ejclaw_service_logs',
'ejclaw_deploy_state',
'ejclaw_artifact_metadata',
'db_paired_task_status',
'db_paired_task_flow',
'db_recent_paired_failures',
'github_pr_status',
'github_pr_diff_stat',
'github_run_status',
] as const;
export const DB_EVIDENCE_ACTIONS = [
'db_paired_task_status',
'db_paired_task_flow',
'db_recent_paired_failures',
] as const;
export const DEPLOY_EVIDENCE_ACTIONS = [
'ejclaw_deploy_state',
'ejclaw_artifact_metadata',
] as const;
export const GITHUB_EVIDENCE_ACTIONS = [
'github_pr_status',
'github_pr_diff_stat',
'github_run_status',
] as const;
export const ARTIFACT_EVIDENCE_KINDS = [
'build_outputs',
'dashboard_dist',
'runner_dist',
'android_debug_apk',
'attachments_dir',
] as const;
export type HostEvidenceAction = (typeof HOST_EVIDENCE_ACTIONS)[number];

View File

@@ -0,0 +1,188 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import {
ARTIFACT_EVIDENCE_KINDS,
DB_EVIDENCE_ACTIONS,
DEPLOY_EVIDENCE_ACTIONS,
formatHostEvidenceResponse,
GITHUB_EVIDENCE_ACTIONS,
HOST_EVIDENCE_ACTIONS,
normalizeHostEvidenceTailLines,
type HostEvidenceAction,
waitForHostEvidenceResponse,
} from './host-evidence.js';
type IpcWriter = (dir: string, data: object) => string;
interface McpTextToolResult {
[key: string]: unknown;
content: Array<{ type: 'text'; text: string }>;
isError?: boolean;
}
interface RegisterHostEvidenceToolsOptions {
server: McpServer;
tasksDir: string;
responseDir: string;
groupFolder: string;
writeIpcFile: IpcWriter;
}
async function requestHostEvidence(
options: RegisterHostEvidenceToolsOptions,
payload: Record<string, unknown> & { action: HostEvidenceAction },
): Promise<McpTextToolResult> {
const requestId = `host-evidence-${Date.now()}-${Math.random()
.toString(36)
.slice(2, 8)}`;
options.writeIpcFile(options.tasksDir, {
type: 'host_evidence_request',
requestId,
...payload,
groupFolder: options.groupFolder,
timestamp: new Date().toISOString(),
});
try {
const response = await waitForHostEvidenceResponse(
options.responseDir,
requestId,
);
return {
content: [
{
type: 'text' as const,
text: formatHostEvidenceResponse(response),
},
],
isError: !response.ok,
};
} catch (error) {
return {
content: [
{
type: 'text' as const,
text: error instanceof Error ? error.message : String(error),
},
],
isError: true,
};
}
}
export function registerHostEvidenceTools(
options: RegisterHostEvidenceToolsOptions,
): void {
const { server } = options;
server.tool(
'read_host_evidence',
'Read host-side deployment evidence through a narrow allowlist. Use this instead of broad shell access when reviewer/arbiter needs service status, deploy state, DB state, GitHub PR state, or artifact metadata.',
{
action: z
.enum(HOST_EVIDENCE_ACTIONS)
.describe(
'Allowlisted evidence action. Prefer specific read_db_evidence/read_deploy_evidence/read_github_evidence helpers when available.',
),
tail_lines: z
.number()
.int()
.min(1)
.max(200)
.optional()
.describe('Only for ejclaw_service_logs. Defaults to 20.'),
task_id: z.string().optional().describe('Only for DB task actions.'),
minutes: z
.number()
.int()
.min(1)
.max(1440)
.optional()
.describe('Only for recent DB evidence. Defaults to 60.'),
limit: z
.number()
.int()
.min(1)
.max(100)
.optional()
.describe('Only for DB evidence row limits. Defaults to 20.'),
repo: z
.string()
.optional()
.describe('Only for GitHub evidence, in owner/repo form.'),
pr_number: z.number().int().positive().optional(),
run_id: z.number().int().positive().optional(),
artifact_kind: z.enum(ARTIFACT_EVIDENCE_KINDS).optional(),
},
async (args) =>
requestHostEvidence(options, {
action: args.action,
tail_lines:
args.action === 'ejclaw_service_logs'
? normalizeHostEvidenceTailLines(args.tail_lines)
: undefined,
task_id: args.task_id,
minutes: args.minutes,
limit: args.limit,
repo: args.repo,
pr_number: args.pr_number,
run_id: args.run_id,
artifact_kind: args.artifact_kind,
}),
);
server.tool(
'read_db_evidence',
'Read fixed DB evidence presets without arbitrary SQL. Raw message/output bodies are not returned.',
{
action: z.enum(DB_EVIDENCE_ACTIONS),
task_id: z
.string()
.optional()
.describe('Required for paired task status/flow actions.'),
minutes: z.number().int().min(1).max(1440).optional(),
limit: z.number().int().min(1).max(100).optional(),
},
async (args) =>
requestHostEvidence(options, {
action: args.action,
task_id: args.task_id,
minutes: args.minutes,
limit: args.limit,
}),
);
server.tool(
'read_deploy_evidence',
'Read fixed deploy/artifact evidence without shell access. Returns commit, dirty state, build artifact mtimes, sizes, and hashes where applicable.',
{
action: z.enum(DEPLOY_EVIDENCE_ACTIONS),
artifact_kind: z.enum(ARTIFACT_EVIDENCE_KINDS).optional(),
},
async (args) =>
requestHostEvidence(options, {
action: args.action,
artifact_kind: args.artifact_kind,
}),
);
server.tool(
'read_github_evidence',
'Read fixed GitHub PR/run evidence through host gh CLI without broad shell access.',
{
action: z.enum(GITHUB_EVIDENCE_ACTIONS),
repo: z.string().describe('GitHub repository in owner/repo form.'),
pr_number: z.number().int().positive().optional(),
run_id: z.number().int().positive().optional(),
},
async (args) =>
requestHostEvidence(options, {
action: args.action,
repo: args.repo,
pr_number: args.pr_number,
run_id: args.run_id,
}),
);
}

View File

@@ -15,13 +15,7 @@ import {
DEFAULT_WATCH_CI_CONTEXT_MODE,
normalizeWatchCiIntervalSeconds,
} from './watch-ci.js';
import {
formatHostEvidenceResponse,
HOST_EVIDENCE_ACTIONS,
normalizeHostEvidenceTailLines,
resolveHostEvidenceResponsesDir,
waitForHostEvidenceResponse,
} from './host-evidence.js';
import { resolveHostEvidenceResponsesDir } from './host-evidence.js';
import {
computeVerificationSnapshotId,
formatVerificationResponse,
@@ -30,6 +24,7 @@ import {
} from './verification.js';
import { resolveIpcDirectories } from './ipc-paths.js';
import { buildSendMessageIpcPayload } from './ipc-message.js';
import { registerHostEvidenceTools } from './ipc-host-evidence-tool.js';
import { registerRepoEvidenceTool } from './ipc-repo-evidence-tool.js';
const { ipcDir: IPC_DIR, hostIpcDir: HOST_IPC_DIR } = resolveIpcDirectories(
@@ -414,69 +409,13 @@ server.tool(
},
);
server.tool(
'read_host_evidence',
'Read host-side deployment evidence through a narrow allowlist. Use this instead of broad shell access when reviewer/arbiter needs service status or recent logs.',
{
action: z
.enum(HOST_EVIDENCE_ACTIONS)
.describe(
'ejclaw_service_status=systemctl --user show ejclaw, ejclaw_service_logs=recent journalctl lines',
),
tail_lines: z
.number()
.int()
.min(1)
.max(200)
.optional()
.describe(
'Only for ejclaw_service_logs. Number of recent journal lines to fetch. Defaults to 20.',
),
},
async (args) => {
const requestId = `host-evidence-${Date.now()}-${Math.random()
.toString(36)
.slice(2, 8)}`;
writeIpcFile(TASKS_DIR, {
type: 'host_evidence_request',
requestId,
action: args.action,
tail_lines:
args.action === 'ejclaw_service_logs'
? normalizeHostEvidenceTailLines(args.tail_lines)
: undefined,
groupFolder,
timestamp: new Date().toISOString(),
});
try {
const response = await waitForHostEvidenceResponse(
HOST_EVIDENCE_RESPONSES_DIR,
requestId,
);
return {
content: [
{
type: 'text' as const,
text: formatHostEvidenceResponse(response),
},
],
isError: !response.ok,
};
} catch (error) {
return {
content: [
{
type: 'text' as const,
text: error instanceof Error ? error.message : String(error),
},
],
isError: true,
};
}
},
);
registerHostEvidenceTools({
server,
tasksDir: TASKS_DIR,
responseDir: HOST_EVIDENCE_RESPONSES_DIR,
groupFolder,
writeIpcFile,
});
server.tool(
'run_verification',