mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): expose redacted log tails over local mcp
This commit is contained in:
@@ -50,6 +50,12 @@ import {
|
||||
BUILTIN_RUN_COMPARE_TOOL_DEFINITION,
|
||||
executeBuiltInRunCompareTool,
|
||||
} from '@qinglong/runtime-core/builtin-run-compare-projection';
|
||||
import {
|
||||
BUILTIN_RUN_LOG_EXCERPT_TOOL,
|
||||
BUILTIN_RUN_LOG_EXCERPT_TOOL_DEFINITION,
|
||||
executeBuiltInRunLogExcerptTool,
|
||||
type RunAttemptLogReadPort,
|
||||
} from '@qinglong/runtime-core/builtin-run-log-excerpt-projection';
|
||||
import {
|
||||
BUILTIN_TASK_RUN_OUTCOME_COMPARE_TOOL,
|
||||
BUILTIN_TASK_RUN_OUTCOME_COMPARE_TOOL_DEFINITION,
|
||||
@@ -102,10 +108,12 @@ export interface AuthenticatedLocalMcpRequest {
|
||||
|
||||
export interface QingLongLocalMcpServerDependencies {
|
||||
readonly projectId: string;
|
||||
readonly profile: 'edge' | 'standalone';
|
||||
readonly authenticate: () => Promise<Readonly<AuthenticatedLocalMcpRequest> | null>;
|
||||
readonly policy: ToolPolicyAuthorizer;
|
||||
readonly audit: SecurityAuditSink;
|
||||
readonly runs: LocalMcpRunReader;
|
||||
readonly runAttemptLogs: RunAttemptLogReadPort;
|
||||
readonly stepRuns: Pick<StepRunRepository, 'listByRun'>;
|
||||
readonly taskDefinitions: LocalMcpTaskReader;
|
||||
readonly triggers: LocalMcpTriggerReader;
|
||||
@@ -133,7 +141,9 @@ type LocalMcpApprovalReader = Pick<
|
||||
Pick<ApprovalRequestDetailSource, 'getApprovalRequestDetail'>;
|
||||
|
||||
interface LocalMcpReadAuthority {
|
||||
readonly profile: 'edge' | 'standalone';
|
||||
readonly runs: LocalMcpRunReader;
|
||||
readonly runAttemptLogs: RunAttemptLogReadPort;
|
||||
readonly stepRuns: Pick<StepRunRepository, 'listByRun'>;
|
||||
readonly taskDefinitions: LocalMcpTaskReader;
|
||||
readonly triggers: LocalMcpTriggerReader;
|
||||
@@ -189,6 +199,24 @@ const LOCAL_MCP_READ_TOOLS: readonly LocalMcpReadToolDescriptor[] =
|
||||
input: ToolJsonValue,
|
||||
) => executeBuiltInRunReadTool(authority.runs, projectId, input),
|
||||
}),
|
||||
Object.freeze({
|
||||
tool: BUILTIN_RUN_LOG_EXCERPT_TOOL,
|
||||
definition: BUILTIN_RUN_LOG_EXCERPT_TOOL_DEFINITION,
|
||||
title: 'QingLong Run Log Tail',
|
||||
auditReason: 'tool_qinglong_run_log_excerpt',
|
||||
unavailableCode: 'run_log_excerpt_unavailable',
|
||||
execute: (
|
||||
authority: LocalMcpReadAuthority,
|
||||
projectId: string,
|
||||
input: ToolJsonValue,
|
||||
) =>
|
||||
executeBuiltInRunLogExcerptTool(
|
||||
authority.runAttemptLogs,
|
||||
authority.profile,
|
||||
projectId,
|
||||
input,
|
||||
),
|
||||
}),
|
||||
Object.freeze({
|
||||
tool: BUILTIN_RUN_COMPARE_TOOL,
|
||||
definition: BUILTIN_RUN_COMPARE_TOOL_DEFINITION,
|
||||
@@ -322,12 +350,15 @@ function validateDependencies(
|
||||
typeof dependencies !== 'object' ||
|
||||
Array.isArray(dependencies) ||
|
||||
typeof dependencies.projectId !== 'string' ||
|
||||
(dependencies.profile !== 'edge' &&
|
||||
dependencies.profile !== 'standalone') ||
|
||||
typeof dependencies.authenticate !== 'function' ||
|
||||
typeof dependencies.policy?.authorize !== 'function' ||
|
||||
typeof dependencies.audit?.record !== 'function' ||
|
||||
typeof dependencies.runs?.listRunsByProject !== 'function' ||
|
||||
typeof dependencies.runs?.findRunById !== 'function' ||
|
||||
typeof dependencies.runs?.listEvents !== 'function' ||
|
||||
typeof dependencies.runAttemptLogs?.read !== 'function' ||
|
||||
typeof dependencies.stepRuns?.listByRun !== 'function' ||
|
||||
typeof dependencies.taskDefinitions?.findCurrentTaskDefinition !==
|
||||
'function' ||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { readPrivateLocalCommandFile } from '@qinglong/local-command-file';
|
||||
import { assertProjectPolicyProjectId } from '@qinglong/runtime-core/project-policy';
|
||||
|
||||
export const LOCAL_MCP_SERVER_CONFIG_SCHEMA =
|
||||
'qinglong/local-mcp-server@v1' as const;
|
||||
'qinglong/local-mcp-server@v2' as const;
|
||||
|
||||
const MAX_PATH_BYTES = 4_096;
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface LocalMcpServerConfig {
|
||||
readonly projectId: string;
|
||||
readonly deploymentRoot: string;
|
||||
readonly databasePath: string;
|
||||
readonly artifactRoot: string;
|
||||
readonly ownerPepperKeyringDirectory: string;
|
||||
readonly credentialFilePath: string;
|
||||
readonly busyTimeoutMs?: number;
|
||||
@@ -40,6 +41,7 @@ function exactRecord(value: unknown): Record<string, unknown> {
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
const expected = [
|
||||
'artifactRoot',
|
||||
'credentialFilePath',
|
||||
'databasePath',
|
||||
'deploymentRoot',
|
||||
@@ -110,6 +112,7 @@ export function normalizeLocalMcpServerConfig(
|
||||
'deploymentRoot',
|
||||
);
|
||||
const databasePath = absolutePath(record.databasePath, 'databasePath');
|
||||
const artifactRoot = absolutePath(record.artifactRoot, 'artifactRoot');
|
||||
const ownerPepperKeyringDirectory = absolutePath(
|
||||
record.ownerPepperKeyringDirectory,
|
||||
'ownerPepperKeyringDirectory',
|
||||
@@ -119,6 +122,7 @@ export function normalizeLocalMcpServerConfig(
|
||||
'credentialFilePath',
|
||||
);
|
||||
descendant(deploymentRoot, databasePath, 'databasePath');
|
||||
descendant(deploymentRoot, artifactRoot, 'artifactRoot');
|
||||
descendant(
|
||||
deploymentRoot,
|
||||
ownerPepperKeyringDirectory,
|
||||
@@ -128,9 +132,10 @@ export function normalizeLocalMcpServerConfig(
|
||||
if (
|
||||
new Set([
|
||||
databasePath,
|
||||
artifactRoot,
|
||||
ownerPepperKeyringDirectory,
|
||||
credentialFilePath,
|
||||
]).size !== 3
|
||||
]).size !== 4
|
||||
) {
|
||||
throw new LocalMcpServerConfigError('authority paths must be distinct');
|
||||
}
|
||||
@@ -149,6 +154,7 @@ export function normalizeLocalMcpServerConfig(
|
||||
projectId: record.projectId as string,
|
||||
deploymentRoot,
|
||||
databasePath,
|
||||
artifactRoot,
|
||||
ownerPepperKeyringDirectory,
|
||||
credentialFilePath,
|
||||
...(busyTimeoutMs === undefined
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { establishAuthenticatedLocalCommand } from '@qinglong/local-owner-console/authenticated-command';
|
||||
import { LocalRunAttemptLogRangeReader } from '@qinglong/local-command-file/artifact-read';
|
||||
import {
|
||||
openLocalSqliteMcpReadDatabase,
|
||||
type LocalSqliteMcpReadDatabase,
|
||||
} from '@qinglong/local-sqlite/mcp-read-database';
|
||||
import { ProjectPolicyEngine } from '@qinglong/runtime-core/project-policy';
|
||||
import { RunAttemptLogReadService } from '@qinglong/runtime-core/run-attempt-log-read';
|
||||
|
||||
import {
|
||||
createQingLongLocalMcpServer,
|
||||
@@ -75,8 +77,19 @@ export async function openProductionLocalMcpServer(
|
||||
});
|
||||
const activeDatabase = database;
|
||||
const policy = new ProjectPolicyEngine(activeDatabase.projectPolicy);
|
||||
const runAttemptLogs = new RunAttemptLogReadService(
|
||||
activeDatabase.runs,
|
||||
new LocalRunAttemptLogRangeReader(config.artifactRoot),
|
||||
{
|
||||
executorType: 'local_process',
|
||||
artifactIdPattern: /^local-[a-f0-9]{30}$/,
|
||||
maximumReadBytes: 32 * 1024,
|
||||
},
|
||||
activeDatabase.runAttemptLogRetention,
|
||||
);
|
||||
const serverDependencies: QingLongLocalMcpServerDependencies = {
|
||||
projectId: config.projectId,
|
||||
profile: config.profile,
|
||||
authenticate: () =>
|
||||
adapters.authenticate(activeDatabase, {
|
||||
deploymentRoot: config.deploymentRoot,
|
||||
@@ -88,6 +101,7 @@ export async function openProductionLocalMcpServer(
|
||||
policy,
|
||||
audit: activeDatabase.securityAudit,
|
||||
runs: activeDatabase.runs,
|
||||
runAttemptLogs,
|
||||
stepRuns: activeDatabase.stepRuns,
|
||||
taskDefinitions: activeDatabase.taskDefinitions,
|
||||
triggers: activeDatabase.triggers,
|
||||
|
||||
Reference in New Issue
Block a user