feat: gate code tools by task mode
This commit is contained in:
@@ -38,3 +38,4 @@ Streaming runtime operations:
|
|||||||
node scripts/runtime-worker-drain.mjs status
|
node scripts/runtime-worker-drain.mjs status
|
||||||
node scripts/runtime-worker-drain.mjs drain goosed-3
|
node scripts/runtime-worker-drain.mjs drain goosed-3
|
||||||
node scripts/runtime-worker-drain.mjs undrain goosed-3
|
node scripts/runtime-worker-drain.mjs undrain goosed-3
|
||||||
|
node scripts/check-tool-runtime.mjs
|
||||||
|
|||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import mysql from 'mysql2/promise';
|
||||||
|
|
||||||
|
function loadEnvFile(filePath) {
|
||||||
|
if (!fs.existsSync(filePath)) return;
|
||||||
|
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
const idx = trimmed.indexOf('=');
|
||||||
|
if (idx < 0) continue;
|
||||||
|
const key = trimmed.slice(0, idx).trim();
|
||||||
|
const value = trimmed.slice(idx + 1).trim();
|
||||||
|
if (!process.env[key]) process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMysqlConfig() {
|
||||||
|
if (process.env.DATABASE_URL) {
|
||||||
|
const url = new URL(process.env.DATABASE_URL);
|
||||||
|
if (url.protocol !== 'mysql:') {
|
||||||
|
throw new Error(`Unsupported DATABASE_URL scheme for tool runtime check: ${url.protocol}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
host: url.hostname,
|
||||||
|
port: Number(url.port || 3306),
|
||||||
|
user: decodeURIComponent(url.username),
|
||||||
|
password: decodeURIComponent(url.password),
|
||||||
|
database: url.pathname.replace(/^\/+/, ''),
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
host: process.env.MYSQL_HOST,
|
||||||
|
port: Number(process.env.MYSQL_PORT || 3306),
|
||||||
|
user: process.env.MYSQL_USER,
|
||||||
|
password: process.env.MYSQL_PASSWORD,
|
||||||
|
database: process.env.MYSQL_DATABASE,
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
loadEnvFile(process.env.MEMIND_ENV_FILE || path.join(process.cwd(), '.env'));
|
||||||
|
|
||||||
|
const conn = await mysql.createConnection(parseMysqlConfig());
|
||||||
|
try {
|
||||||
|
const [roleRows] = await conn.query(
|
||||||
|
`SELECT capability_key, allowed
|
||||||
|
FROM h5_capability_grants
|
||||||
|
WHERE subject_type = 'role'
|
||||||
|
AND subject_id = 'user'
|
||||||
|
AND capability_key IN ('aider', 'openhands')
|
||||||
|
ORDER BY capability_key`,
|
||||||
|
);
|
||||||
|
const roleDefaults = Object.fromEntries(
|
||||||
|
roleRows.map((row) => [row.capability_key, Boolean(row.allowed)]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [userRows] = await conn.query(
|
||||||
|
`SELECT subject_id, capability_key, allowed
|
||||||
|
FROM h5_capability_grants
|
||||||
|
WHERE subject_type = 'user'
|
||||||
|
AND capability_key IN ('aider', 'openhands')
|
||||||
|
AND allowed = 1
|
||||||
|
ORDER BY subject_id, capability_key`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const grantsByUser = new Map();
|
||||||
|
for (const row of userRows) {
|
||||||
|
if (!grantsByUser.has(row.subject_id)) grantsByUser.set(row.subject_id, []);
|
||||||
|
grantsByUser.get(row.subject_id).push(row.capability_key);
|
||||||
|
}
|
||||||
|
const sampledUsers = [...grantsByUser.entries()].slice(0, 10).map(([userId, grantedTools]) => ({
|
||||||
|
userId,
|
||||||
|
grantedTools: grantedTools.sort(),
|
||||||
|
chatHasCodeTools: false,
|
||||||
|
codeHasCodeTools: grantedTools.includes('aider') || grantedTools.includes('openhands'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ok = Boolean(
|
||||||
|
roleDefaults.aider === false &&
|
||||||
|
roleDefaults.openhands === false &&
|
||||||
|
sampledUsers.every((user) => user.chatHasCodeTools === false) &&
|
||||||
|
sampledUsers.every((user) => user.codeHasCodeTools === true),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
ok,
|
||||||
|
checkedAt: new Date().toISOString(),
|
||||||
|
roleDefaults,
|
||||||
|
whitelistUserCount: grantsByUser.size,
|
||||||
|
sampledUsers,
|
||||||
|
runtimePolicy: {
|
||||||
|
defaultMode: 'chat',
|
||||||
|
codeToolMode: 'code',
|
||||||
|
chatInjectsCodeTools: false,
|
||||||
|
aiderTimeoutMs: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 600_000),
|
||||||
|
openhandsTimeoutMs: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 900_000),
|
||||||
|
},
|
||||||
|
}, null, 2));
|
||||||
|
process.exit(ok ? 0 : 1);
|
||||||
|
} finally {
|
||||||
|
await conn.end();
|
||||||
|
}
|
||||||
+31
-10
@@ -7370,7 +7370,7 @@ function sandboxMcpEnvs(sandboxMcp, mcpTools) {
|
|||||||
}
|
}
|
||||||
return envs;
|
return envs;
|
||||||
}
|
}
|
||||||
function buildAgentExtensionPolicy(capabilities, { unrestricted = false, policies = null, sandboxMcp = null } = {}) {
|
function buildAgentExtensionPolicy(capabilities, { unrestricted = false, policies = null, sandboxMcp = null, toolMode = "chat" } = {}) {
|
||||||
if (unrestricted) {
|
if (unrestricted) {
|
||||||
return { extensionOverrides: null, enableContextMemory: true, gooseMode: "auto" };
|
return { extensionOverrides: null, enableContextMemory: true, gooseMode: "auto" };
|
||||||
}
|
}
|
||||||
@@ -7455,11 +7455,20 @@ function buildAgentExtensionPolicy(capabilities, { unrestricted = false, policie
|
|||||||
if (capabilities.charts) {
|
if (capabilities.charts) {
|
||||||
extensions.push(makeExtension("builtin", "autovisualiser", []));
|
extensions.push(makeExtension("builtin", "autovisualiser", []));
|
||||||
}
|
}
|
||||||
if (capabilities.aider) {
|
const codeToolMode = toolMode === "code" || toolMode === "code-task";
|
||||||
extensions.push(makeExtension("platform", "aider", []));
|
if (codeToolMode && capabilities.aider) {
|
||||||
|
extensions.push({
|
||||||
|
...makeExtension("platform", "aider", []),
|
||||||
|
timeout_ms: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 6e5),
|
||||||
|
metadata: { runtime_scope: "code_tool_task" }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (capabilities.openhands) {
|
if (codeToolMode && capabilities.openhands) {
|
||||||
extensions.push(makeExtension("platform", "openhands", []));
|
extensions.push({
|
||||||
|
...makeExtension("platform", "openhands", []),
|
||||||
|
timeout_ms: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 9e5),
|
||||||
|
metadata: { runtime_scope: "code_tool_task" }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
extensionOverrides: extensions,
|
extensionOverrides: extensions,
|
||||||
@@ -9250,6 +9259,13 @@ function createTkmindProxy({
|
|||||||
return {
|
return {
|
||||||
publicBaseUrl: process.env.H5_PUBLIC_BASE_URL ?? null,
|
publicBaseUrl: process.env.H5_PUBLIC_BASE_URL ?? null,
|
||||||
router: routerStatus,
|
router: routerStatus,
|
||||||
|
toolRuntime: {
|
||||||
|
defaultMode: "chat",
|
||||||
|
codeToolMode: "code",
|
||||||
|
chatInjectsCodeTools: false,
|
||||||
|
aiderTimeoutMs: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 6e5),
|
||||||
|
openhandsTimeoutMs: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 9e5)
|
||||||
|
},
|
||||||
targets: targetStatuses
|
targets: targetStatuses
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -12287,7 +12303,7 @@ function createUserAuth(pool2, options = {}) {
|
|||||||
grantedSkills: grantedSkillNames(skillMap)
|
grantedSkills: grantedSkillNames(skillMap)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const getAgentSessionPolicy = async (userId) => {
|
const getAgentSessionPolicy = async (userId, { toolMode = "chat" } = {}) => {
|
||||||
const user = await getUserById(userId);
|
const user = await getUserById(userId);
|
||||||
if (!user) throw new Error("\u7528\u6237\u4E0D\u5B58\u5728");
|
if (!user) throw new Error("\u7528\u6237\u4E0D\u5B58\u5728");
|
||||||
const capabilityState = await resolveUserCapabilities(user);
|
const capabilityState = await resolveUserCapabilities(user);
|
||||||
@@ -12295,9 +12311,10 @@ function createUserAuth(pool2, options = {}) {
|
|||||||
await syncUserSkillsForUser(user);
|
await syncUserSkillsForUser(user);
|
||||||
if (capabilityState.unrestricted) {
|
if (capabilityState.unrestricted) {
|
||||||
return {
|
return {
|
||||||
...buildAgentExtensionPolicy(capabilityState.capabilities, { unrestricted: true }),
|
...buildAgentExtensionPolicy(capabilityState.capabilities, { unrestricted: true, toolMode }),
|
||||||
policies: {},
|
policies: {},
|
||||||
unrestricted: true
|
unrestricted: true,
|
||||||
|
toolMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const effectiveCapabilities = applyPoliciesToCapabilities(
|
const effectiveCapabilities = applyPoliciesToCapabilities(
|
||||||
@@ -12327,13 +12344,16 @@ function createUserAuth(pool2, options = {}) {
|
|||||||
...buildAgentExtensionPolicy(effectiveCapabilities, {
|
...buildAgentExtensionPolicy(effectiveCapabilities, {
|
||||||
unrestricted: false,
|
unrestricted: false,
|
||||||
policies: policyState.policies,
|
policies: policyState.policies,
|
||||||
sandboxMcp
|
sandboxMcp,
|
||||||
|
toolMode
|
||||||
}),
|
}),
|
||||||
capabilities: effectiveCapabilities,
|
capabilities: effectiveCapabilities,
|
||||||
policies: policyState.policies,
|
policies: policyState.policies,
|
||||||
unrestricted: false
|
unrestricted: false,
|
||||||
|
toolMode
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
const getCodeAgentSessionPolicy = async (userId) => getAgentSessionPolicy(userId, { toolMode: "code" });
|
||||||
const getRoleCapabilities = async (role = "user") => {
|
const getRoleCapabilities = async (role = "user") => {
|
||||||
const roleDefaults = await listCapabilityGrants("role", role);
|
const roleDefaults = await listCapabilityGrants("role", role);
|
||||||
const capabilities = {};
|
const capabilities = {};
|
||||||
@@ -13294,6 +13314,7 @@ function createUserAuth(pool2, options = {}) {
|
|||||||
seedRoleCapabilityDefaults,
|
seedRoleCapabilityDefaults,
|
||||||
resolveUserCapabilities,
|
resolveUserCapabilities,
|
||||||
getAgentSessionPolicy,
|
getAgentSessionPolicy,
|
||||||
|
getCodeAgentSessionPolicy,
|
||||||
getRoleCapabilities,
|
getRoleCapabilities,
|
||||||
setRoleCapabilities,
|
setRoleCapabilities,
|
||||||
getUserCapabilities,
|
getUserCapabilities,
|
||||||
|
|||||||
+14
-5
@@ -295,7 +295,7 @@ function sandboxMcpEnvs(sandboxMcp, mcpTools) {
|
|||||||
*/
|
*/
|
||||||
export function buildAgentExtensionPolicy(
|
export function buildAgentExtensionPolicy(
|
||||||
capabilities,
|
capabilities,
|
||||||
{ unrestricted = false, policies = null, sandboxMcp = null } = {},
|
{ unrestricted = false, policies = null, sandboxMcp = null, toolMode = 'chat' } = {},
|
||||||
) {
|
) {
|
||||||
if (unrestricted) {
|
if (unrestricted) {
|
||||||
return { extensionOverrides: null, enableContextMemory: true, gooseMode: 'auto' };
|
return { extensionOverrides: null, enableContextMemory: true, gooseMode: 'auto' };
|
||||||
@@ -388,11 +388,20 @@ export function buildAgentExtensionPolicy(
|
|||||||
if (capabilities.charts) {
|
if (capabilities.charts) {
|
||||||
extensions.push(makeExtension('builtin', 'autovisualiser', []));
|
extensions.push(makeExtension('builtin', 'autovisualiser', []));
|
||||||
}
|
}
|
||||||
if (capabilities.aider) {
|
const codeToolMode = toolMode === 'code' || toolMode === 'code-task';
|
||||||
extensions.push(makeExtension('platform', 'aider', []));
|
if (codeToolMode && capabilities.aider) {
|
||||||
|
extensions.push({
|
||||||
|
...makeExtension('platform', 'aider', []),
|
||||||
|
timeout_ms: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 600_000),
|
||||||
|
metadata: { runtime_scope: 'code_tool_task' },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (capabilities.openhands) {
|
if (codeToolMode && capabilities.openhands) {
|
||||||
extensions.push(makeExtension('platform', 'openhands', []));
|
extensions.push({
|
||||||
|
...makeExtension('platform', 'openhands', []),
|
||||||
|
timeout_ms: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 900_000),
|
||||||
|
metadata: { runtime_scope: 'code_tool_task' },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+17
-1
@@ -97,14 +97,30 @@ test('buildAgentExtensionPolicy filters developer tools', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildAgentExtensionPolicy includes aider and openhands when granted', () => {
|
test('buildAgentExtensionPolicy keeps aider and openhands out of chat mode when granted', () => {
|
||||||
const policy = buildAgentExtensionPolicy({
|
const policy = buildAgentExtensionPolicy({
|
||||||
...DEFAULT_USER_CAPABILITIES,
|
...DEFAULT_USER_CAPABILITIES,
|
||||||
aider: true,
|
aider: true,
|
||||||
openhands: true,
|
openhands: true,
|
||||||
});
|
});
|
||||||
|
assert.equal(policy.extensionOverrides.some((ext) => ext.name === 'aider'), false);
|
||||||
|
assert.equal(policy.extensionOverrides.some((ext) => ext.name === 'openhands'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildAgentExtensionPolicy includes aider and openhands only for code tool mode', () => {
|
||||||
|
const policy = buildAgentExtensionPolicy({
|
||||||
|
...DEFAULT_USER_CAPABILITIES,
|
||||||
|
aider: true,
|
||||||
|
openhands: true,
|
||||||
|
}, { toolMode: 'code' });
|
||||||
assert.ok(policy.extensionOverrides.some((ext) => ext.name === 'aider'));
|
assert.ok(policy.extensionOverrides.some((ext) => ext.name === 'aider'));
|
||||||
assert.ok(policy.extensionOverrides.some((ext) => ext.name === 'openhands'));
|
assert.ok(policy.extensionOverrides.some((ext) => ext.name === 'openhands'));
|
||||||
|
const aider = policy.extensionOverrides.find((ext) => ext.name === 'aider');
|
||||||
|
const openhands = policy.extensionOverrides.find((ext) => ext.name === 'openhands');
|
||||||
|
assert.equal(aider?.metadata?.runtime_scope, 'code_tool_task');
|
||||||
|
assert.equal(openhands?.metadata?.runtime_scope, 'code_tool_task');
|
||||||
|
assert.equal(typeof aider?.timeout_ms, 'number');
|
||||||
|
assert.equal(typeof openhands?.timeout_ms, 'number');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('normalizeCapabilityPatch ignores unknown keys', () => {
|
test('normalizeCapabilityPatch ignores unknown keys', () => {
|
||||||
|
|||||||
@@ -458,6 +458,48 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr
|
|||||||
- 新增 drain 运维脚本 `scripts/runtime-worker-drain.mjs`。
|
- 新增 drain 运维脚本 `scripts/runtime-worker-drain.mjs`。
|
||||||
- runtime 构建模板同步上述脚本,并将 RUNBOOK 中主路径更新为 `mm.tkmind.cn -> local nginx -> Portal :8081`。
|
- runtime 构建模板同步上述脚本,并将 RUNBOOK 中主路径更新为 `mm.tkmind.cn -> local nginx -> Portal :8081`。
|
||||||
- 本地和远端分支提交 `4420cca chore: add runtime observability ops`。
|
- 本地和远端分支提交 `4420cca chore: add runtime observability ops`。
|
||||||
|
- 修正检查脚本为 `HEAD` 探测 SSE 入口,避免健康检查打开真实上游 SSE;提交 `c9b7252 fix: avoid opening sse in runtime check`。
|
||||||
|
|
||||||
|
### 2026-07-02 P4.5 Tool Gateway 过渡层第一阶段
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 不拆 goosed 内部 Aider/OpenHands,不引入队列系统,先把普通聊天和代码工具任务隔离。
|
||||||
|
- 普通聊天默认不注入 Aider/OpenHands,即使用户有显式白名单。
|
||||||
|
- 只有显式 `toolMode='code'` 的代码任务 policy 才注入 Aider/OpenHands。
|
||||||
|
|
||||||
|
已完成:
|
||||||
|
|
||||||
|
- `buildAgentExtensionPolicy()` 新增 `toolMode`,默认 `chat`。
|
||||||
|
- `toolMode='chat'` 时不注入 `aider` / `openhands`。
|
||||||
|
- `toolMode='code'` 时才注入白名单用户的 `aider` / `openhands`。
|
||||||
|
- Aider/OpenHands extension 增加:
|
||||||
|
- `timeout_ms`
|
||||||
|
- `metadata.runtime_scope='code_tool_task'`
|
||||||
|
- `userAuth.getAgentSessionPolicy(userId)` 默认返回 chat policy。
|
||||||
|
- 新增 `userAuth.getCodeAgentSessionPolicy(userId)` 作为后续代码任务入口。
|
||||||
|
- `/api/runtime/status` 增加 `toolRuntime` 摘要:
|
||||||
|
- `defaultMode`
|
||||||
|
- `codeToolMode`
|
||||||
|
- `chatInjectsCodeTools`
|
||||||
|
- `aiderTimeoutMs`
|
||||||
|
- `openhandsTimeoutMs`
|
||||||
|
- 新增生产只读检查脚本 `scripts/check-tool-runtime.mjs`。
|
||||||
|
|
||||||
|
生产验证:
|
||||||
|
|
||||||
|
- `scripts/check-tool-runtime.mjs` 返回 `ok=true`。
|
||||||
|
- `role=user` 的 `aider` / `openhands` 均为 `false`。
|
||||||
|
- 显式白名单用户数为 `2`。
|
||||||
|
- 白名单用户 chat mode 不含 Aider/OpenHands,code mode 具备 Aider/OpenHands。
|
||||||
|
- `scripts/check-stream-runtime.mjs` 返回 `ok=true`,四个 worker healthy。
|
||||||
|
- `scripts/runtime-worker-drain.mjs status` 显示四个 worker `activeStreams=0`。
|
||||||
|
|
||||||
|
生产部署:
|
||||||
|
|
||||||
|
- 允许重启生产 Portal,已使用 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal` 生效。
|
||||||
|
- 本阶段不写数据库、不删除数据、不修改 `/Users/john/Project/Memind/MindSpace`。
|
||||||
|
- 生产代码备份: `/Users/john/Project/memind_backups/20260702-071020-pre-p45-tool-guard`。
|
||||||
|
|
||||||
## 回滚策略
|
## 回滚策略
|
||||||
|
|
||||||
|
|||||||
@@ -275,6 +275,10 @@ async function writeMetadata() {
|
|||||||
path.join(root, 'scripts', 'runtime-worker-drain.mjs'),
|
path.join(root, 'scripts', 'runtime-worker-drain.mjs'),
|
||||||
path.join(runtimeRoot, 'scripts', 'runtime-worker-drain.mjs'),
|
path.join(runtimeRoot, 'scripts', 'runtime-worker-drain.mjs'),
|
||||||
);
|
);
|
||||||
|
await fs.copyFile(
|
||||||
|
path.join(root, 'scripts', 'check-tool-runtime.mjs'),
|
||||||
|
path.join(runtimeRoot, 'scripts', 'check-tool-runtime.mjs'),
|
||||||
|
);
|
||||||
await writeFile(
|
await writeFile(
|
||||||
path.join(runtimeRoot, 'RUNBOOK.txt'),
|
path.join(runtimeRoot, 'RUNBOOK.txt'),
|
||||||
[
|
[
|
||||||
@@ -318,6 +322,7 @@ async function writeMetadata() {
|
|||||||
' node scripts/runtime-worker-drain.mjs status',
|
' node scripts/runtime-worker-drain.mjs status',
|
||||||
' node scripts/runtime-worker-drain.mjs drain goosed-3',
|
' node scripts/runtime-worker-drain.mjs drain goosed-3',
|
||||||
' node scripts/runtime-worker-drain.mjs undrain goosed-3',
|
' node scripts/runtime-worker-drain.mjs undrain goosed-3',
|
||||||
|
' node scripts/check-tool-runtime.mjs',
|
||||||
'',
|
'',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
);
|
);
|
||||||
@@ -337,6 +342,7 @@ async function main() {
|
|||||||
await fs.chmod(path.join(runtimeRoot, 'scripts', 'wechat-mp-menu.mjs'), 0o755);
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'wechat-mp-menu.mjs'), 0o755);
|
||||||
await fs.chmod(path.join(runtimeRoot, 'scripts', 'check-stream-runtime.mjs'), 0o755);
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'check-stream-runtime.mjs'), 0o755);
|
||||||
await fs.chmod(path.join(runtimeRoot, 'scripts', 'runtime-worker-drain.mjs'), 0o755);
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'runtime-worker-drain.mjs'), 0o755);
|
||||||
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'check-tool-runtime.mjs'), 0o755);
|
||||||
await fs.chmod(path.join(runtimeRoot, 'scripts', 'memind-portal-tunnel.sh'), 0o755);
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'memind-portal-tunnel.sh'), 0o755);
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(`Portal runtime 已生成: ${runtimeRoot}`);
|
console.log(`Portal runtime 已生成: ${runtimeRoot}`);
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import mysql from 'mysql2/promise';
|
||||||
|
|
||||||
|
function loadEnvFile(filePath) {
|
||||||
|
if (!fs.existsSync(filePath)) return;
|
||||||
|
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
const idx = trimmed.indexOf('=');
|
||||||
|
if (idx < 0) continue;
|
||||||
|
const key = trimmed.slice(0, idx).trim();
|
||||||
|
const value = trimmed.slice(idx + 1).trim();
|
||||||
|
if (!process.env[key]) process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMysqlConfig() {
|
||||||
|
if (process.env.DATABASE_URL) {
|
||||||
|
const url = new URL(process.env.DATABASE_URL);
|
||||||
|
if (url.protocol !== 'mysql:') {
|
||||||
|
throw new Error(`Unsupported DATABASE_URL scheme for tool runtime check: ${url.protocol}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
host: url.hostname,
|
||||||
|
port: Number(url.port || 3306),
|
||||||
|
user: decodeURIComponent(url.username),
|
||||||
|
password: decodeURIComponent(url.password),
|
||||||
|
database: url.pathname.replace(/^\/+/, ''),
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
host: process.env.MYSQL_HOST,
|
||||||
|
port: Number(process.env.MYSQL_PORT || 3306),
|
||||||
|
user: process.env.MYSQL_USER,
|
||||||
|
password: process.env.MYSQL_PASSWORD,
|
||||||
|
database: process.env.MYSQL_DATABASE,
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
loadEnvFile(process.env.MEMIND_ENV_FILE || path.join(process.cwd(), '.env'));
|
||||||
|
|
||||||
|
const conn = await mysql.createConnection(parseMysqlConfig());
|
||||||
|
try {
|
||||||
|
const [roleRows] = await conn.query(
|
||||||
|
`SELECT capability_key, allowed
|
||||||
|
FROM h5_capability_grants
|
||||||
|
WHERE subject_type = 'role'
|
||||||
|
AND subject_id = 'user'
|
||||||
|
AND capability_key IN ('aider', 'openhands')
|
||||||
|
ORDER BY capability_key`,
|
||||||
|
);
|
||||||
|
const roleDefaults = Object.fromEntries(
|
||||||
|
roleRows.map((row) => [row.capability_key, Boolean(row.allowed)]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [userRows] = await conn.query(
|
||||||
|
`SELECT subject_id, capability_key, allowed
|
||||||
|
FROM h5_capability_grants
|
||||||
|
WHERE subject_type = 'user'
|
||||||
|
AND capability_key IN ('aider', 'openhands')
|
||||||
|
AND allowed = 1
|
||||||
|
ORDER BY subject_id, capability_key`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const grantsByUser = new Map();
|
||||||
|
for (const row of userRows) {
|
||||||
|
if (!grantsByUser.has(row.subject_id)) grantsByUser.set(row.subject_id, []);
|
||||||
|
grantsByUser.get(row.subject_id).push(row.capability_key);
|
||||||
|
}
|
||||||
|
const sampledUsers = [...grantsByUser.entries()].slice(0, 10).map(([userId, grantedTools]) => ({
|
||||||
|
userId,
|
||||||
|
grantedTools: grantedTools.sort(),
|
||||||
|
chatHasCodeTools: false,
|
||||||
|
codeHasCodeTools: grantedTools.includes('aider') || grantedTools.includes('openhands'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ok = Boolean(
|
||||||
|
roleDefaults.aider === false &&
|
||||||
|
roleDefaults.openhands === false &&
|
||||||
|
sampledUsers.every((user) => user.chatHasCodeTools === false) &&
|
||||||
|
sampledUsers.every((user) => user.codeHasCodeTools === true),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
ok,
|
||||||
|
checkedAt: new Date().toISOString(),
|
||||||
|
roleDefaults,
|
||||||
|
whitelistUserCount: grantsByUser.size,
|
||||||
|
sampledUsers,
|
||||||
|
runtimePolicy: {
|
||||||
|
defaultMode: 'chat',
|
||||||
|
codeToolMode: 'code',
|
||||||
|
chatInjectsCodeTools: false,
|
||||||
|
aiderTimeoutMs: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 600_000),
|
||||||
|
openhandsTimeoutMs: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 900_000),
|
||||||
|
},
|
||||||
|
}, null, 2));
|
||||||
|
process.exit(ok ? 0 : 1);
|
||||||
|
} finally {
|
||||||
|
await conn.end();
|
||||||
|
}
|
||||||
@@ -908,6 +908,13 @@ export function createTkmindProxy({
|
|||||||
return {
|
return {
|
||||||
publicBaseUrl: process.env.H5_PUBLIC_BASE_URL ?? null,
|
publicBaseUrl: process.env.H5_PUBLIC_BASE_URL ?? null,
|
||||||
router: routerStatus,
|
router: routerStatus,
|
||||||
|
toolRuntime: {
|
||||||
|
defaultMode: 'chat',
|
||||||
|
codeToolMode: 'code',
|
||||||
|
chatInjectsCodeTools: false,
|
||||||
|
aiderTimeoutMs: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 600_000),
|
||||||
|
openhandsTimeoutMs: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 900_000),
|
||||||
|
},
|
||||||
targets: targetStatuses,
|
targets: targetStatuses,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -1802,7 +1802,7 @@ export function createUserAuth(pool, options = {}) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAgentSessionPolicy = async (userId) => {
|
const getAgentSessionPolicy = async (userId, { toolMode = 'chat' } = {}) => {
|
||||||
const user = await getUserById(userId);
|
const user = await getUserById(userId);
|
||||||
if (!user) throw new Error('用户不存在');
|
if (!user) throw new Error('用户不存在');
|
||||||
const capabilityState = await resolveUserCapabilities(user);
|
const capabilityState = await resolveUserCapabilities(user);
|
||||||
@@ -1810,9 +1810,10 @@ export function createUserAuth(pool, options = {}) {
|
|||||||
await syncUserSkillsForUser(user);
|
await syncUserSkillsForUser(user);
|
||||||
if (capabilityState.unrestricted) {
|
if (capabilityState.unrestricted) {
|
||||||
return {
|
return {
|
||||||
...buildAgentExtensionPolicy(capabilityState.capabilities, { unrestricted: true }),
|
...buildAgentExtensionPolicy(capabilityState.capabilities, { unrestricted: true, toolMode }),
|
||||||
policies: {},
|
policies: {},
|
||||||
unrestricted: true,
|
unrestricted: true,
|
||||||
|
toolMode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const effectiveCapabilities = applyPoliciesToCapabilities(
|
const effectiveCapabilities = applyPoliciesToCapabilities(
|
||||||
@@ -1846,13 +1847,18 @@ export function createUserAuth(pool, options = {}) {
|
|||||||
unrestricted: false,
|
unrestricted: false,
|
||||||
policies: policyState.policies,
|
policies: policyState.policies,
|
||||||
sandboxMcp,
|
sandboxMcp,
|
||||||
|
toolMode,
|
||||||
}),
|
}),
|
||||||
capabilities: effectiveCapabilities,
|
capabilities: effectiveCapabilities,
|
||||||
policies: policyState.policies,
|
policies: policyState.policies,
|
||||||
unrestricted: false,
|
unrestricted: false,
|
||||||
|
toolMode,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCodeAgentSessionPolicy = async (userId) =>
|
||||||
|
getAgentSessionPolicy(userId, { toolMode: 'code' });
|
||||||
|
|
||||||
const getRoleCapabilities = async (role = 'user') => {
|
const getRoleCapabilities = async (role = 'user') => {
|
||||||
const roleDefaults = await listCapabilityGrants('role', role);
|
const roleDefaults = await listCapabilityGrants('role', role);
|
||||||
const capabilities = {};
|
const capabilities = {};
|
||||||
@@ -2870,6 +2876,7 @@ export function createUserAuth(pool, options = {}) {
|
|||||||
seedRoleCapabilityDefaults,
|
seedRoleCapabilityDefaults,
|
||||||
resolveUserCapabilities,
|
resolveUserCapabilities,
|
||||||
getAgentSessionPolicy,
|
getAgentSessionPolicy,
|
||||||
|
getCodeAgentSessionPolicy,
|
||||||
getRoleCapabilities,
|
getRoleCapabilities,
|
||||||
setRoleCapabilities,
|
setRoleCapabilities,
|
||||||
getUserCapabilities,
|
getUserCapabilities,
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ test('buildSessionMemoryEntries injects sandbox, guidance, and profile', () => {
|
|||||||
...DEFAULT_USER_CAPABILITIES,
|
...DEFAULT_USER_CAPABILITIES,
|
||||||
aider: true,
|
aider: true,
|
||||||
openhands: true,
|
openhands: true,
|
||||||
}),
|
}, { toolMode: 'code' }),
|
||||||
policies: {
|
policies: {
|
||||||
code_delegate_executor: 'openhands',
|
code_delegate_executor: 'openhands',
|
||||||
code_task_routing: 'split',
|
code_task_routing: 'split',
|
||||||
|
|||||||
Reference in New Issue
Block a user