refactor: centralize host evidence actions (#194)

This commit is contained in:
Eyejoker
2026-05-30 00:08:28 +09:00
committed by GitHub
parent adf9c16b9a
commit 46b5543cb5
8 changed files with 193 additions and 143 deletions

View File

@@ -1,54 +1,22 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {
ARTIFACT_EVIDENCE_KINDS,
DB_EVIDENCE_ACTIONS,
DEPLOY_EVIDENCE_ACTIONS,
GITHUB_EVIDENCE_ACTIONS,
HOST_EVIDENCE_ACTIONS,
type HostEvidenceAction,
} from 'ejclaw-runners-shared';
export const HOST_EVIDENCE_ACTIONS = [ export {
'ejclaw_service_status', ARTIFACT_EVIDENCE_KINDS,
'ejclaw_service_logs', DB_EVIDENCE_ACTIONS,
'ejclaw_role_runtime_config', DEPLOY_EVIDENCE_ACTIONS,
'ejclaw_deploy_state', GITHUB_EVIDENCE_ACTIONS,
'ejclaw_artifact_metadata', HOST_EVIDENCE_ACTIONS,
'db_paired_task_status', type HostEvidenceAction,
'db_paired_task_flow', };
'db_recent_paired_failures',
'db_recent_scheduled_tasks',
'db_scheduled_task_runs',
'github_pr_status',
'github_pr_diff_stat',
'github_run_status',
'github_run_jobs',
'github_workflow_file',
] as const;
export const DB_EVIDENCE_ACTIONS = [
'db_paired_task_status',
'db_paired_task_flow',
'db_recent_paired_failures',
'db_recent_scheduled_tasks',
'db_scheduled_task_runs',
] 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',
'github_run_jobs',
'github_workflow_file',
] 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];
export interface HostEvidenceResponse { export interface HostEvidenceResponse {
requestId: string; requestId: string;

View File

@@ -0,0 +1,78 @@
export const DB_EVIDENCE_ACTIONS = [
'db_paired_task_status',
'db_paired_task_flow',
'db_recent_paired_failures',
'db_recent_scheduled_tasks',
'db_scheduled_task_runs',
] 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',
'github_run_jobs',
'github_workflow_file',
] as const;
export const HOST_EVIDENCE_ACTIONS = [
'ejclaw_service_status',
'ejclaw_service_logs',
'ejclaw_role_runtime_config',
...DEPLOY_EVIDENCE_ACTIONS,
...DB_EVIDENCE_ACTIONS,
...GITHUB_EVIDENCE_ACTIONS,
] as const;
export const ARTIFACT_EVIDENCE_KINDS = [
'build_outputs',
'dashboard_dist',
'runner_dist',
'android_debug_apk',
'attachments_dir',
] as const;
export type DbEvidenceAction = (typeof DB_EVIDENCE_ACTIONS)[number];
export type DeployEvidenceAction = (typeof DEPLOY_EVIDENCE_ACTIONS)[number];
export type GitHubEvidenceAction = (typeof GITHUB_EVIDENCE_ACTIONS)[number];
export type HostEvidenceAction = (typeof HOST_EVIDENCE_ACTIONS)[number];
export type ArtifactEvidenceKind = (typeof ARTIFACT_EVIDENCE_KINDS)[number];
function includesEvidenceValue<T extends readonly string[]>(
values: T,
value: unknown,
): value is T[number] {
return typeof value === 'string' && values.includes(value as T[number]);
}
export function isDbEvidenceAction(value: unknown): value is DbEvidenceAction {
return includesEvidenceValue(DB_EVIDENCE_ACTIONS, value);
}
export function isDeployEvidenceAction(
value: unknown,
): value is DeployEvidenceAction {
return includesEvidenceValue(DEPLOY_EVIDENCE_ACTIONS, value);
}
export function isGitHubEvidenceAction(
value: unknown,
): value is GitHubEvidenceAction {
return includesEvidenceValue(GITHUB_EVIDENCE_ACTIONS, value);
}
export function isHostEvidenceAction(
value: unknown,
): value is HostEvidenceAction {
return includesEvidenceValue(HOST_EVIDENCE_ACTIONS, value);
}
export function isArtifactEvidenceKind(
value: unknown,
): value is ArtifactEvidenceKind {
return includesEvidenceValue(ARTIFACT_EVIDENCE_KINDS, value);
}

View File

@@ -10,6 +10,23 @@ export {
PAIRED_ROOM_ROLES, PAIRED_ROOM_ROLES,
type PairedRoomRole, type PairedRoomRole,
} from './paired-room-role.js'; } from './paired-room-role.js';
export {
ARTIFACT_EVIDENCE_KINDS,
DB_EVIDENCE_ACTIONS,
DEPLOY_EVIDENCE_ACTIONS,
GITHUB_EVIDENCE_ACTIONS,
HOST_EVIDENCE_ACTIONS,
isArtifactEvidenceKind,
isDbEvidenceAction,
isDeployEvidenceAction,
isGitHubEvidenceAction,
isHostEvidenceAction,
type ArtifactEvidenceKind,
type DbEvidenceAction,
type DeployEvidenceAction,
type GitHubEvidenceAction,
type HostEvidenceAction,
} from './evidence-actions.js';
export { export {
extractMarkdownImageAttachments, extractMarkdownImageAttachments,
extractMediaAttachments, extractMediaAttachments,

View File

@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest';
import {
ARTIFACT_EVIDENCE_KINDS,
DB_EVIDENCE_ACTIONS,
DEPLOY_EVIDENCE_ACTIONS,
GITHUB_EVIDENCE_ACTIONS,
HOST_EVIDENCE_ACTIONS,
isArtifactEvidenceKind,
isDbEvidenceAction,
isDeployEvidenceAction,
isGitHubEvidenceAction,
isHostEvidenceAction,
} from '../src/evidence-actions.js';
describe('evidence action constants', () => {
it('keeps host evidence actions as the composed allowlist', () => {
expect(HOST_EVIDENCE_ACTIONS).toEqual([
'ejclaw_service_status',
'ejclaw_service_logs',
'ejclaw_role_runtime_config',
...DEPLOY_EVIDENCE_ACTIONS,
...DB_EVIDENCE_ACTIONS,
...GITHUB_EVIDENCE_ACTIONS,
]);
});
it('recognizes scoped evidence actions', () => {
expect(isHostEvidenceAction('github_run_jobs')).toBe(true);
expect(isDbEvidenceAction('db_recent_scheduled_tasks')).toBe(true);
expect(isDeployEvidenceAction('ejclaw_artifact_metadata')).toBe(true);
expect(isGitHubEvidenceAction('github_workflow_file')).toBe(true);
expect(isHostEvidenceAction('cat /etc/shadow')).toBe(false);
});
it('recognizes artifact evidence kinds', () => {
expect(ARTIFACT_EVIDENCE_KINDS).toContain('runner_dist');
expect(isArtifactEvidenceKind('runner_dist')).toBe(true);
expect(isArtifactEvidenceKind('../secret')).toBe(false);
});
});

View File

@@ -1,19 +1,16 @@
import type { Database } from 'bun:sqlite'; import type { Database } from 'bun:sqlite';
import {
DB_EVIDENCE_ACTIONS,
isDbEvidenceAction,
type DbEvidenceAction,
} from 'ejclaw-runners-shared';
import { parseGitHubCiMetadata } from './github-ci.js'; import { parseGitHubCiMetadata } from './github-ci.js';
import { extractWatchCiTarget } from './task-watch-status.js'; import { extractWatchCiTarget } from './task-watch-status.js';
type SqlBinding = string | number | bigint | boolean | null | Uint8Array; type SqlBinding = string | number | bigint | boolean | null | Uint8Array;
export const DB_EVIDENCE_ACTIONS = [ export { DB_EVIDENCE_ACTIONS, isDbEvidenceAction, type DbEvidenceAction };
'db_paired_task_status',
'db_paired_task_flow',
'db_recent_paired_failures',
'db_recent_scheduled_tasks',
'db_scheduled_task_runs',
] as const;
export type DbEvidenceAction = (typeof DB_EVIDENCE_ACTIONS)[number];
export interface DbEvidenceRequest { export interface DbEvidenceRequest {
action: DbEvidenceAction; action: DbEvidenceAction;
@@ -33,13 +30,6 @@ const DEFAULT_ROW_LIMIT = 20;
const MAX_ROW_LIMIT = 100; const MAX_ROW_LIMIT = 100;
const TASK_ID_PATTERN = /^[A-Za-z0-9._:@/-]{1,200}$/; const TASK_ID_PATTERN = /^[A-Za-z0-9._:@/-]{1,200}$/;
export function isDbEvidenceAction(value: unknown): value is DbEvidenceAction {
return (
typeof value === 'string' &&
DB_EVIDENCE_ACTIONS.includes(value as DbEvidenceAction)
);
}
export function normalizeDbEvidenceMinutes(value?: number): number { export function normalizeDbEvidenceMinutes(value?: number): number {
if (!Number.isFinite(value)) { if (!Number.isFinite(value)) {
return DEFAULT_RECENT_MINUTES; return DEFAULT_RECENT_MINUTES;

View File

@@ -2,22 +2,22 @@ import { createHash } from 'crypto';
import { execFile } from 'child_process'; import { execFile } from 'child_process';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {
ARTIFACT_EVIDENCE_KINDS,
DEPLOY_EVIDENCE_ACTIONS,
isArtifactEvidenceKind,
isDeployEvidenceAction,
type ArtifactEvidenceKind,
type DeployEvidenceAction,
} from 'ejclaw-runners-shared';
export const DEPLOY_EVIDENCE_ACTIONS = [ export {
'ejclaw_deploy_state', ARTIFACT_EVIDENCE_KINDS,
'ejclaw_artifact_metadata', DEPLOY_EVIDENCE_ACTIONS,
] as const; isDeployEvidenceAction,
type ArtifactEvidenceKind,
export const ARTIFACT_EVIDENCE_KINDS = [ type DeployEvidenceAction,
'build_outputs', };
'dashboard_dist',
'runner_dist',
'android_debug_apk',
'attachments_dir',
] as const;
export type DeployEvidenceAction = (typeof DEPLOY_EVIDENCE_ACTIONS)[number];
export type ArtifactEvidenceKind = (typeof ARTIFACT_EVIDENCE_KINDS)[number];
export interface DeployEvidenceRequest { export interface DeployEvidenceRequest {
action: DeployEvidenceAction; action: DeployEvidenceAction;
@@ -35,21 +35,12 @@ const COMMAND_MAX_BUFFER = 1024 * 1024;
const MAX_DIR_ENTRIES = 5_000; const MAX_DIR_ENTRIES = 5_000;
const MAX_LATEST_FILES = 12; const MAX_LATEST_FILES = 12;
export function isDeployEvidenceAction(
value: unknown,
): value is DeployEvidenceAction {
return (
typeof value === 'string' &&
DEPLOY_EVIDENCE_ACTIONS.includes(value as DeployEvidenceAction)
);
}
export function normalizeArtifactEvidenceKind( export function normalizeArtifactEvidenceKind(
value?: string, value?: string,
): ArtifactEvidenceKind { ): ArtifactEvidenceKind {
if (!value) return 'build_outputs'; if (!value) return 'build_outputs';
if (ARTIFACT_EVIDENCE_KINDS.includes(value as ArtifactEvidenceKind)) { if (isArtifactEvidenceKind(value)) {
return value as ArtifactEvidenceKind; return value;
} }
throw new Error(`Unsupported artifact evidence kind: ${value}`); throw new Error(`Unsupported artifact evidence kind: ${value}`);
} }

View File

@@ -1,14 +1,15 @@
import { execFile } from 'child_process'; import { execFile } from 'child_process';
import {
GITHUB_EVIDENCE_ACTIONS,
isGitHubEvidenceAction,
type GitHubEvidenceAction,
} from 'ejclaw-runners-shared';
export const GITHUB_EVIDENCE_ACTIONS = [ export {
'github_pr_status', GITHUB_EVIDENCE_ACTIONS,
'github_pr_diff_stat', isGitHubEvidenceAction,
'github_run_status', type GitHubEvidenceAction,
'github_run_jobs', };
'github_workflow_file',
] as const;
export type GitHubEvidenceAction = (typeof GITHUB_EVIDENCE_ACTIONS)[number];
export interface GitHubEvidenceRequest { export interface GitHubEvidenceRequest {
action: GitHubEvidenceAction; action: GitHubEvidenceAction;
@@ -33,15 +34,6 @@ const COMMAND_TIMEOUT_MS = 10_000;
const COMMAND_MAX_BUFFER = 2 * 1024 * 1024; const COMMAND_MAX_BUFFER = 2 * 1024 * 1024;
const MAX_OUTPUT_CHARS = 24_000; const MAX_OUTPUT_CHARS = 24_000;
export function isGitHubEvidenceAction(
value: unknown,
): value is GitHubEvidenceAction {
return (
typeof value === 'string' &&
GITHUB_EVIDENCE_ACTIONS.includes(value as GitHubEvidenceAction)
);
}
export function normalizeGitHubRepo(value?: string): string { export function normalizeGitHubRepo(value?: string): string {
const repo = value?.trim(); const repo = value?.trim();
if (!repo || repo.includes('..') || !REPO_PATTERN.test(repo)) { if (!repo || repo.includes('..') || !REPO_PATTERN.test(repo)) {

View File

@@ -23,37 +23,24 @@ import {
WEB_DASHBOARD, WEB_DASHBOARD,
} from './config.js'; } from './config.js';
import type { AgentType } from './types.js'; import type { AgentType } from './types.js';
import {
HOST_EVIDENCE_ACTIONS,
isHostEvidenceAction,
type HostEvidenceAction,
} from 'ejclaw-runners-shared';
import { import {
collectArtifactMetadata, collectArtifactMetadata,
collectDeployState, collectDeployState,
DEPLOY_EVIDENCE_ACTIONS,
type DeployEvidenceAction,
} from './deploy-evidence.js'; } from './deploy-evidence.js';
import { import { isDbEvidenceAction, runDbEvidenceRequest } from './db-evidence.js';
DB_EVIDENCE_ACTIONS,
isDbEvidenceAction,
type DbEvidenceAction,
runDbEvidenceRequest,
} from './db-evidence.js';
import { requireDatabase } from './db/runtime-database.js'; import { requireDatabase } from './db/runtime-database.js';
import { import {
GITHUB_EVIDENCE_ACTIONS,
isGitHubEvidenceAction, isGitHubEvidenceAction,
runGitHubEvidenceCommand, runGitHubEvidenceCommand,
type GitHubEvidenceAction,
} from './github-evidence.js'; } from './github-evidence.js';
import { resolveGroupIpcPath } from './group-folder.js'; import { resolveGroupIpcPath } from './group-folder.js';
export const HOST_EVIDENCE_ACTIONS = [ export { HOST_EVIDENCE_ACTIONS, isHostEvidenceAction, type HostEvidenceAction };
'ejclaw_service_status',
'ejclaw_service_logs',
'ejclaw_role_runtime_config',
...DEPLOY_EVIDENCE_ACTIONS,
...DB_EVIDENCE_ACTIONS,
...GITHUB_EVIDENCE_ACTIONS,
] as const;
export type HostEvidenceAction = (typeof HOST_EVIDENCE_ACTIONS)[number];
export interface HostEvidenceRequest { export interface HostEvidenceRequest {
requestId: string; requestId: string;
@@ -74,12 +61,7 @@ export interface HostEvidenceRequest {
export interface HostEvidenceResult { export interface HostEvidenceResult {
ok: boolean; ok: boolean;
action: action: HostEvidenceAction | 'ejclaw_room_runtime';
| HostEvidenceAction
| DbEvidenceAction
| DeployEvidenceAction
| GitHubEvidenceAction
| 'ejclaw_room_runtime';
command: string; command: string;
stdout: string; stdout: string;
stderr: string; stderr: string;
@@ -104,15 +86,6 @@ const COMMAND_TIMEOUT_MS = 5_000;
const COMMAND_MAX_BUFFER = 1024 * 1024; const COMMAND_MAX_BUFFER = 1024 * 1024;
const PROJECT_ROOT = process.cwd(); const PROJECT_ROOT = process.cwd();
export function isHostEvidenceAction(
value: unknown,
): value is HostEvidenceAction {
return (
typeof value === 'string' &&
HOST_EVIDENCE_ACTIONS.includes(value as HostEvidenceAction)
);
}
export function clampHostEvidenceTailLines(value?: number): number { export function clampHostEvidenceTailLines(value?: number): number {
if (!Number.isFinite(value)) { if (!Number.isFinite(value)) {
return DEFAULT_LOG_TAIL_LINES; return DEFAULT_LOG_TAIL_LINES;