diff --git a/docs/release-canary-103.md b/docs/release-canary-103.md index d10aeec..9942c3c 100644 --- a/docs/release-canary-103.md +++ b/docs/release-canary-103.md @@ -16,6 +16,13 @@ Portal 的首个生产动作必须是用户级灰度,不能直接整包替换 `MEMIND_RUNTIME_ROLE=candidate` 会禁止重复启动 MindSpace、Plaza、提醒、订阅续期和 Agent 恢复扫描等单例后台任务。 +候选 Portal 必须从候选 runtime 目录启动,使相对模块只解析到本次候选 artifact。 +候选 goosed 在 Docker 内执行扩展,MCP Node 和入口路径必须固定为 +`/usr/local/bin/node` 与 `/opt/portal/mindspace-sandbox-mcp.mjs`;禁止继承稳定 +`.env` 中仅宿主机可见的 `/Users/...` 路径。若日志出现 +`Failed to add extension` 和宿主机绝对路径的 `MODULE_NOT_FOUND`,应先核对候选 +进程环境与 `/opt/portal` 容器挂载,禁止把宿主机文件复制进容器临时修补。 + ## 身份和回落规则 - `john`:按数据库中的唯一用户名或用户 ID 匹配。 diff --git a/release-gate/release-script.test.mjs b/release-gate/release-script.test.mjs index 40cb81c..8c71424 100644 --- a/release-gate/release-script.test.mjs +++ b/release-gate/release-script.test.mjs @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import fs from 'node:fs/promises'; +import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; @@ -163,6 +164,60 @@ test('production canary keeps stable 8081 live and switches only after verified assert.doesNotMatch(source, /bootout.*cn\.tkmind\.memind-portal/); }); +test('candidate runner overrides stable host MCP paths with container-visible paths', async (t) => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-canary-runner-')); + t.after(() => fs.rm(tempRoot, { recursive: true, force: true })); + + const stableRoot = path.join(tempRoot, 'stable'); + const fakeNode = path.join(tempRoot, 'node'); + await fs.mkdir(stableRoot); + await fs.writeFile( + path.join(stableRoot, '.env'), + [ + 'GOOSED_MCP_NODE_PATH=/opt/homebrew/opt/node@24/bin/node', + 'GOOSED_MCP_SERVER_PATH=/Users/john/Project/Memind/mindspace-sandbox-mcp.mjs', + '', + ].join('\n'), + ); + await fs.writeFile( + fakeNode, + [ + '#!/usr/bin/env bash', + 'printf "cwd=%s\\n" "$PWD"', + 'printf "mcp_node=%s\\n" "$GOOSED_MCP_NODE_PATH"', + 'printf "mcp_server=%s\\n" "$GOOSED_MCP_SERVER_PATH"', + 'printf "entrypoint=%s\\n" "$1"', + '', + ].join('\n'), + { mode: 0o755 }, + ); + + const result = spawnSync( + 'bash', + [path.join(ROOT, 'scripts', 'run-memind-portal-candidate.sh')], + { + cwd: '/', + encoding: 'utf8', + env: { + ...process.env, + MEMIND_CANARY_STABLE_ROOT: stableRoot, + MEMIND_CANARY_RELEASE_ID: 'test-release', + MEMIND_CANARY_CANDIDATE_PORT: '65534', + NODE_BIN: fakeNode, + }, + }, + ); + + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, new RegExp(`^cwd=${ROOT}$`, 'm')); + assert.match(result.stdout, /^mcp_node=\/usr\/local\/bin\/node$/m); + assert.match( + result.stdout, + /^mcp_server=\/opt\/portal\/mindspace-sandbox-mcp\.mjs$/m, + ); + assert.match(result.stdout, new RegExp(`^entrypoint=${ROOT}/server\\.mjs$`, 'm')); +}); + test('canary release and rollback remote shells remain syntactically valid', async () => { const releaseSource = await fs.readFile(CANARY_RELEASE, 'utf8'); const rollbackSource = await fs.readFile(CANARY_ROLLBACK, 'utf8'); diff --git a/scripts/run-memind-portal-candidate.sh b/scripts/run-memind-portal-candidate.sh index dc5f21b..e53c9af 100755 --- a/scripts/run-memind-portal-candidate.sh +++ b/scripts/run-memind-portal-candidate.sh @@ -9,6 +9,8 @@ if [[ ! -d "${STABLE_ROOT}" || ! -f "${STABLE_ROOT}/.env" ]]; then exit 1 fi +cd "${ROOT}" + set -a # shellcheck disable=SC1091 source "${STABLE_ROOT}/.env" @@ -27,6 +29,10 @@ export H5_HOST=127.0.0.1 export H5_PUBLIC_BASE_URL="${H5_PUBLIC_BASE_URL:-https://m.tkmind.cn}" export TKMIND_API_TARGETS="${MEMIND_CANARY_GOOSED_URL:-https://127.0.0.1:18015}" export TKMIND_API_TARGET="${MEMIND_CANARY_GOOSED_URL:-https://127.0.0.1:18015}" +# Extensions are spawned inside goosed-canary, where the candidate artifact is +# mounted at /opt/portal. Never inherit host-only MCP paths from the stable .env. +export GOOSED_MCP_NODE_PATH=/usr/local/bin/node +export GOOSED_MCP_SERVER_PATH=/opt/portal/mindspace-sandbox-mcp.mjs unset TKMIND_API_TARGET_1 NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"