feat: add code run rollout policy gates

This commit is contained in:
Your Name
2026-07-02 10:47:22 +08:00
parent f1fd4cc316
commit 212e163006
3 changed files with 188 additions and 0 deletions
+42
View File
@@ -13,11 +13,41 @@ function parseUserIdSet(value) {
);
}
function parseTaskTypeSet(value) {
return new Set(
String(value ?? '')
.split(',')
.map((item) => item.trim().toLowerCase())
.filter(Boolean),
);
}
function hasExpectedFileValidation(userMessage) {
const metadata = userMessage?.metadata;
const runMetadata = metadata?.memindRun ?? metadata?.agentRun ?? {};
const validation = runMetadata.validation ?? metadata?.toolGatewayValidation;
if (!validation || typeof validation !== 'object' || Array.isArray(validation)) return false;
const expectedFile = validation.expectedFile ?? validation.expectedPath;
if (typeof expectedFile === 'string' && expectedFile.trim()) return true;
if (expectedFile && typeof expectedFile === 'object' && !Array.isArray(expectedFile)) {
const filePath = String(expectedFile.path ?? expectedFile.file ?? expectedFile.relativePath ?? '').trim();
if (filePath) return true;
}
if (!Array.isArray(validation.expectedFiles)) return false;
return validation.expectedFiles.some((item) => {
if (typeof item === 'string') return Boolean(item.trim());
if (!item || typeof item !== 'object' || Array.isArray(item)) return false;
return Boolean(String(item.path ?? item.file ?? item.relativePath ?? '').trim());
});
}
export function createPostAgentRunsHandler({
userAuth,
agentRunGateway,
codeRunsEnabled = envFlag(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED),
codeRunUserIds = parseUserIdSet(process.env.MEMIND_AGENT_CODE_RUNS_USER_IDS),
codeRunTaskTypes = parseTaskTypeSet(process.env.MEMIND_AGENT_CODE_RUN_TASK_TYPES),
requireCodeRunValidation = envFlag(process.env.MEMIND_AGENT_CODE_RUNS_REQUIRE_VALIDATION),
}) {
return async function postAgentRuns(request, response) {
try {
@@ -55,6 +85,18 @@ export function createPostAgentRunsHandler({
response.status(403).json({ message: '当前用户未开启代码任务灰度' });
return;
}
if (
toolMode === 'code' &&
codeRunTaskTypes.size > 0 &&
(!taskType || !codeRunTaskTypes.has(taskType.toLowerCase()))
) {
response.status(403).json({ message: '当前代码任务类型未开启灰度' });
return;
}
if (toolMode === 'code' && requireCodeRunValidation && !hasExpectedFileValidation(userMessage)) {
response.status(400).json({ message: '代码任务必须声明产物校验规则' });
return;
}
if (sessionId) {
const owns = await userAuth.ownsSession(request.currentUser.id, sessionId);
if (!owns) {
+123
View File
@@ -179,6 +179,129 @@ test('POST /agent/runs accepts code tool mode for whitelisted user', async () =>
assert.equal(created[0].payload.toolMode, 'code');
});
test('POST /agent/runs accepts allowlisted code task type with validation policy', async () => {
const created = [];
const handler = createPostAgentRunsHandler({
userAuth: {
async ownsSession() {
return true;
},
},
agentRunGateway: {
async createRun(userId, payload) {
created.push({ userId, payload });
return { id: 'run-code', status: 'queued' };
},
},
codeRunsEnabled: true,
codeRunUserIds: new Set(['user-1']),
codeRunTaskTypes: new Set(['small_patch']),
requireCodeRunValidation: true,
});
const userMessage = {
role: 'user',
content: [{ type: 'text', text: 'create artifact' }],
metadata: {
memindRun: {
validation: {
expectedFile: {
path: 'RESULT.md',
contains: 'ok',
},
},
},
},
};
const res = createResponseRecorder();
await handler(
{
currentUser: { id: 'user-1' },
body: {
request_id: 'req-code-small-patch',
user_message: userMessage,
tool_mode: 'code',
task_type: 'small_patch',
},
},
res,
);
assert.equal(res.statusCode, 202);
assert.equal(created[0].payload.taskType, 'small_patch');
assert.deepEqual(created[0].payload.userMessage, userMessage);
});
test('POST /agent/runs rejects non-allowlisted code task type', async () => {
const handler = createPostAgentRunsHandler({
userAuth: {
async ownsSession() {
throw new Error('should not check ownership after task type gate rejects code mode');
},
},
agentRunGateway: {
async createRun() {
throw new Error('should not be called');
},
},
codeRunsEnabled: true,
codeRunUserIds: new Set(['user-1']),
codeRunTaskTypes: new Set(['small_patch']),
});
const res = createResponseRecorder();
await handler(
{
currentUser: { id: 'user-1' },
body: {
request_id: 'req-code-repo-refactor',
user_message: { role: 'user', content: [] },
tool_mode: 'code',
task_type: 'repo_refactor',
},
},
res,
);
assert.equal(res.statusCode, 403);
assert.deepEqual(res.body, { message: '当前代码任务类型未开启灰度' });
});
test('POST /agent/runs requires validation metadata for guarded code tasks', async () => {
const handler = createPostAgentRunsHandler({
userAuth: {
async ownsSession() {
throw new Error('should not check ownership after validation gate rejects code mode');
},
},
agentRunGateway: {
async createRun() {
throw new Error('should not be called');
},
},
codeRunsEnabled: true,
codeRunUserIds: new Set(['user-1']),
requireCodeRunValidation: true,
});
const res = createResponseRecorder();
await handler(
{
currentUser: { id: 'user-1' },
body: {
request_id: 'req-code-no-validation',
user_message: { role: 'user', content: [] },
tool_mode: 'code',
task_type: 'small_patch',
},
},
res,
);
assert.equal(res.statusCode, 400);
assert.deepEqual(res.body, { message: '代码任务必须声明产物校验规则' });
});
test('POST /agent/runs rejects code tool mode for non-whitelisted user', async () => {
const handler = createPostAgentRunsHandler({
userAuth: {
+23
View File
@@ -1783,6 +1783,28 @@ api.get('/status', async (_req, res, next) => {
return next();
});
function runtimeEnvFlag(value, fallback = false) {
const raw = String(value ?? '').trim().toLowerCase();
if (!raw) return fallback;
return ['1', 'true', 'yes', 'on'].includes(raw);
}
function runtimeCsvList(value) {
return String(value ?? '')
.split(',')
.map((item) => item.trim())
.filter(Boolean);
}
function runtimeCodeRunPolicyStatus() {
return {
enabled: runtimeEnvFlag(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED),
userAllowlist: runtimeCsvList(process.env.MEMIND_AGENT_CODE_RUNS_USER_IDS),
taskTypeAllowlist: runtimeCsvList(process.env.MEMIND_AGENT_CODE_RUN_TASK_TYPES),
requireValidation: runtimeEnvFlag(process.env.MEMIND_AGENT_CODE_RUNS_REQUIRE_VALIDATION),
};
}
api.get('/runtime/status', async (_req, res) => {
await userAuthReady;
if (!tkmindProxy?.getRuntimeStatus) {
@@ -1798,6 +1820,7 @@ api.get('/runtime/status', async (_req, res) => {
if (toolQueue) {
status.toolRuntime = {
...(status.toolRuntime ?? {}),
codeRunPolicy: runtimeCodeRunPolicyStatus(),
queue: toolQueue,
};
}