Compare commits

...

6 Commits

Author SHA1 Message Date
john a5d109d585 fix(page-data): grant agent role set permission 2026-07-17 09:32:31 +08:00
john 331a863288 merge: fix goosed release dependency check
Memind CI / Test, build, and release guards (push) Successful in 7m27s
2026-07-16 22:37:58 +08:00
john 486e25f86b fix(release): validate composed goosed containers 2026-07-16 22:37:50 +08:00
john f4bc716789 merge: bundle MindSearch MCP runtime
Memind CI / Test, build, and release guards (push) Successful in 2m23s
2026-07-16 22:29:01 +08:00
john 3cafadba5a fix(runtime): bundle MindSearch MCP for production 2026-07-16 22:26:47 +08:00
john 122e478894 merge: pnpm release dependency lock
Memind CI / Test, build, and release guards (push) Successful in 2m54s
2026-07-16 21:44:53 +08:00
9 changed files with 123 additions and 17 deletions
@@ -26,3 +26,13 @@ npm run verify:mindspace-page-sync-guards
```
涉及 H5 交付时,还必须验证:未注册 dataset 时不产生可用 Page Data policy,且最终链接交付被拒绝或进入明确 repair 状态。
## PostgreSQL 用户空间角色守卫
生产用户空间 PostgreSQL(独立于 Goose session PostgreSQL)通过 `SET LOCAL ROLE ms_u_*_agent` 隔离每个用户。必须保留以下约束:
1. provisioning 必须显式授予运行连接用户 agent role 的 `SET` 权限。
2. agent role 保持 `INHERIT FALSE`;只允许显式 `SET ROLE` 后访问用户 schema,不能让连接用户默认继承全部用户权限。
3. 已存在的用户空间在首次访问时必须幂等检查并修复缺失的 `SET` 权限,不能只修复新注册用户。
4. 验收必须使用与生产等价的非超级用户连接完成 `SET LOCAL ROLE`、建表、dataset 注册和读写;超级用户会绕过角色切换限制,不能作为该问题的验收依据。
5. 修复角色授权时禁止修改用户 schema、表和数据;生产操作前保留用户空间 PG dump、角色授权快照和回滚 SQL。
+1
View File
@@ -8,6 +8,7 @@
1. 构建只发生在本机 Mac`node scripts/build-portal-runtime.mjs`
2. 103 只接收 `.runtime/portal/` 打出来的 artifact,不直接覆盖源码树。
- 所有由 goosed 以 stdio 启动的 Portal MCP 入口都必须打进 artifact,并同时加入发布脚本的必需文件和容器可见性检查;当前包括 `mindspace-sandbox-mcp.mjs``tkmind-search-mcp.mjs`
3. 103 在切换前必须做 `Memind` 全量备份,并单独备份持久目录。
4. 103 **禁止** `npm install``npm run build`、在线改源码后继续运行。
5. 切换后必须通过 Portal 健康检查;失败立即回滚。
+20
View File
@@ -18,6 +18,25 @@ export function quotePgIdentifier(value) {
return `"${String(value).replaceAll('"', '""')}"`;
}
function quotePgLiteral(value) {
return `'${String(value).replaceAll("'", "''")}'`;
}
export function buildEnsureCurrentUserCanSetRoleSql(roleName) {
const role = String(roleName);
const roleLiteral = quotePgLiteral(role);
return `DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_auth_members am
JOIN pg_roles r ON r.oid = am.roleid
JOIN pg_roles m ON m.oid = am.member
WHERE r.rolname = ${roleLiteral} AND m.rolname = CURRENT_USER AND am.set_option
) THEN
EXECUTE format('GRANT %I TO %I WITH INHERIT FALSE, SET TRUE', ${roleLiteral}, CURRENT_USER);
END IF;
END $$`;
}
export function deriveUserSpaceNames(value) {
const userId = assertUserId(value);
const compact = userId.replaceAll('-', '');
@@ -118,6 +137,7 @@ export function buildProvisionUserSql(userId, { sourceSqlitePath = null, quotaBy
EXECUTE format('GRANT %I TO %I', '${names.ownerRole}', CURRENT_USER);
END IF;
END $$`,
buildEnsureCurrentUserCanSetRoleSql(names.agentRole),
`CREATE SCHEMA IF NOT EXISTS ${schema} AUTHORIZATION ${owner}`,
`REVOKE ALL ON SCHEMA ${schema} FROM PUBLIC`,
`GRANT USAGE, CREATE ON SCHEMA ${schema} TO ${agent}`,
+10
View File
@@ -5,6 +5,7 @@ import path from 'node:path';
import test from 'node:test';
import {
buildControlSchemaSql,
buildEnsureCurrentUserCanSetRoleSql,
buildPostgresTableSql,
buildPostgresCheckSql,
buildPostgresForeignKeySql,
@@ -44,12 +45,21 @@ test('provision SQL creates isolated no-login roles and safety limits', () => {
assert.match(sql, /CREATE ROLE "ms_u_ecc1c649fff7_owner" NOLOGIN/);
assert.match(sql, /CREATE ROLE "ms_u_ecc1c649fff7_agent" NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE/);
assert.match(sql, /pg_auth_members/);
assert.match(sql, /r\.rolname = 'ms_u_ecc1c649fff7_agent'.*am\.set_option/s);
assert.match(sql, /GRANT %I TO %I WITH INHERIT FALSE, SET TRUE/);
assert.match(sql, /REVOKE ALL ON SCHEMA "u_ecc1c649fff74361a243a69b460cc407" FROM PUBLIC/);
assert.match(sql, /statement_timeout = '30s'/);
assert.match(sql, /NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION/);
assert.doesNotMatch(sql, /temp_file_limit/);
});
test('agent role reconciliation requires SET without inherited privileges', () => {
const sql = buildEnsureCurrentUserCanSetRoleSql('ms_u_ecc1c649fff7_agent');
assert.match(sql, /m\.rolname = CURRENT_USER AND am\.set_option/);
assert.match(sql, /WITH INHERIT FALSE, SET TRUE/);
assert.doesNotMatch(sql, /WITH INHERIT TRUE/);
});
test('SQLite types and defaults map to conservative PostgreSQL equivalents', () => {
assert.equal(mapSqliteTypeToPostgres({ type: 'INTEGER', pk: 1 }), 'bigint GENERATED BY DEFAULT AS IDENTITY');
assert.equal(mapSqliteTypeToPostgres({ type: 'REAL', pk: 0 }), 'double precision');
+2 -2
View File
@@ -58,7 +58,7 @@
"test:scenario": "node scripts/run-scenario-test.mjs",
"verify:children-hobby-diet-survey": "node scripts/verify-children-hobby-diet-survey.mjs",
"test:scenario:john4-diet": "node scripts/run-scenario-test.mjs --scenario john4-children-hobby-diet-update",
"test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-token-state.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs wechat-mp.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs chat-intent-router.test.mjs chat-finish-sync.test.mjs chat-agent-run-gate.test.mjs conversation-display.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs session-broker.test.mjs sse-event-taxonomy.test.mjs goosed-proxy-boundary.test.mjs agent-run-stream.test.mjs mindspace-h5-html-finish-guard.test.mjs admin-routes.test.mjs direct-chat-service.test.mjs tool-gateway.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-local-runtime-services.test.mjs mindspace-local-server-adapter.test.mjs mindspace-public-asset-token.test.mjs mindspace-remote-server-adapter.test.mjs mindspace-server-adapter.test.mjs mindspace-pages.test.mjs mindspace-page-sync-service.test.mjs public-site-bases.test.mjs mindspace-html-download-links.test.mjs mindspace-page-purge.test.mjs mindspace-public-delivery.test.mjs mindspace-public-page-context.test.mjs mindspace-published-page-csp.test.mjs mindspace-published-script-localize.test.mjs agent-run-deliverable-check.test.mjs mindspace-public-route.test.mjs mindspace-publications.test.mjs mindspace-public-links.test.mjs mindspace-chat-save.test.mjs mindspace-chat-docx-package.test.mjs mindspace-public-finish-sync.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-backfill.test.mjs mindspace-conversation-package-public-html.test.mjs mindspace-conversation-package-verify.test.mjs mindspace-conversation-package-registry.test.mjs mindspace-conversation-package-routes.test.mjs mindspace-conversation-package-store.test.mjs mindspace-conversation-schema.test.mjs mindspace-runtime-config.test.mjs mindspace-config.test.mjs mindspace-analytics.test.mjs mindspace-service.test.mjs mindspace-storage-adapter.test.mjs mindspace-content-scan.test.mjs mindspace-html-localize.test.mjs mindspace-visual-editor.test.mjs mindspace-cleanup.test.mjs mindspace-thumbnails.test.mjs mindspace-workspace-thumbnails.test.mjs mindspace-workspace-sync.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs mindspace-sandbox-mcp.test.mjs user-data-space-service.test.mjs page-data-routes.test.mjs page-access-policy.test.mjs page-access-visitor.test.mjs page-data-public-service.test.mjs page-data-integration.test.mjs page-data-log-store.test.mjs page-data-ops.test.mjs page-data-session-store.test.mjs page-data-browser-client.test.mjs page-data-policy-index.test.mjs message-stream.test.mjs mindspace-service/mindspace-rpc-server.test.mjs plaza-posts.test.mjs plaza-interactions.test.mjs plaza-algorithm.test.mjs plaza-seo.test.mjs plaza-ops.test.mjs user-auth.test.mjs llm-providers.test.mjs admin-guard.test.mjs user-feedback.test.mjs memory-v2.test.mjs memory-v2-admin-config.test.mjs memory-v2-lifecycle.test.mjs memory-v2-adapter-scaffold.test.mjs memory-v2-backend-contract.test.mjs memory-v2-health.test.mjs memory-v2-runtime.test.mjs memory-v2-plugin-backends.test.mjs memory-v2-pgvector.test.mjs memory-v2-pgvector-schema.test.mjs memory-v2-pgvector-backfill.test.mjs memory-v2-pgvector-smoke.test.mjs memory-v2-qdrant.test.mjs memory-v2-weaviate.test.mjs memory-v2-mem0.test.mjs memory-v2-letta.test.mjs memory-v2-external-adapters.test.mjs scripts/embed-memory-v2-local-hash.test.mjs scripts/check-memory-v2-app-canary.test.mjs scripts/check-memory-v2-config-gaps.test.mjs scripts/check-memory-v2-contracts.test.mjs scripts/check-memory-v2-health.test.mjs scripts/check-memory-v2-session-flow.test.mjs scripts/check-memory-v2-stack.test.mjs scripts/setup-memory-v2-pgvector-schema.test.mjs scripts/backfill-memory-v2-pgvector.test.mjs scripts/scaffold-memory-v2-backend.test.mjs scripts/smoke-memory-v2-pgvector.test.mjs scripts/smoke-memory-v2-qdrant.test.mjs scripts/smoke-memory-v2-external.test.mjs scripts/mock-memory-v2-services.test.mjs",
"test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-token-state.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs wechat-mp.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs chat-intent-router.test.mjs chat-finish-sync.test.mjs chat-agent-run-gate.test.mjs conversation-display.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs session-broker.test.mjs sse-event-taxonomy.test.mjs goosed-proxy-boundary.test.mjs agent-run-stream.test.mjs mindspace-h5-html-finish-guard.test.mjs admin-routes.test.mjs direct-chat-service.test.mjs tool-gateway.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-local-runtime-services.test.mjs mindspace-local-server-adapter.test.mjs mindspace-public-asset-token.test.mjs mindspace-remote-server-adapter.test.mjs mindspace-server-adapter.test.mjs mindspace-pages.test.mjs mindspace-page-sync-service.test.mjs public-site-bases.test.mjs mindspace-html-download-links.test.mjs mindspace-page-purge.test.mjs mindspace-public-delivery.test.mjs mindspace-public-page-context.test.mjs mindspace-published-page-csp.test.mjs mindspace-published-script-localize.test.mjs agent-run-deliverable-check.test.mjs mindspace-public-route.test.mjs mindspace-publications.test.mjs mindspace-public-links.test.mjs mindspace-chat-save.test.mjs mindspace-chat-docx-package.test.mjs mindspace-public-finish-sync.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-backfill.test.mjs mindspace-conversation-package-public-html.test.mjs mindspace-conversation-package-verify.test.mjs mindspace-conversation-package-registry.test.mjs mindspace-conversation-package-routes.test.mjs mindspace-conversation-package-store.test.mjs mindspace-conversation-schema.test.mjs mindspace-runtime-config.test.mjs mindspace-config.test.mjs mindspace-analytics.test.mjs mindspace-service.test.mjs mindspace-storage-adapter.test.mjs mindspace-content-scan.test.mjs mindspace-html-localize.test.mjs mindspace-visual-editor.test.mjs mindspace-cleanup.test.mjs mindspace-thumbnails.test.mjs mindspace-workspace-thumbnails.test.mjs mindspace-workspace-sync.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs mindspace-sandbox-mcp.test.mjs mindspace-userdata-postgres.test.mjs postgres-user-data-space-service.test.mjs user-data-space-service.test.mjs page-data-routes.test.mjs page-access-policy.test.mjs page-access-visitor.test.mjs page-data-public-service.test.mjs page-data-integration.test.mjs page-data-log-store.test.mjs page-data-ops.test.mjs page-data-session-store.test.mjs page-data-browser-client.test.mjs page-data-policy-index.test.mjs message-stream.test.mjs mindspace-service/mindspace-rpc-server.test.mjs plaza-posts.test.mjs plaza-interactions.test.mjs plaza-algorithm.test.mjs plaza-seo.test.mjs plaza-ops.test.mjs user-auth.test.mjs llm-providers.test.mjs admin-guard.test.mjs user-feedback.test.mjs memory-v2.test.mjs memory-v2-admin-config.test.mjs memory-v2-lifecycle.test.mjs memory-v2-adapter-scaffold.test.mjs memory-v2-backend-contract.test.mjs memory-v2-health.test.mjs memory-v2-runtime.test.mjs memory-v2-plugin-backends.test.mjs memory-v2-pgvector.test.mjs memory-v2-pgvector-schema.test.mjs memory-v2-pgvector-backfill.test.mjs memory-v2-pgvector-smoke.test.mjs memory-v2-qdrant.test.mjs memory-v2-weaviate.test.mjs memory-v2-mem0.test.mjs memory-v2-letta.test.mjs memory-v2-external-adapters.test.mjs scripts/embed-memory-v2-local-hash.test.mjs scripts/check-memory-v2-app-canary.test.mjs scripts/check-memory-v2-config-gaps.test.mjs scripts/check-memory-v2-contracts.test.mjs scripts/check-memory-v2-health.test.mjs scripts/check-memory-v2-session-flow.test.mjs scripts/check-memory-v2-stack.test.mjs scripts/setup-memory-v2-pgvector-schema.test.mjs scripts/backfill-memory-v2-pgvector.test.mjs scripts/scaffold-memory-v2-backend.test.mjs scripts/smoke-memory-v2-pgvector.test.mjs scripts/smoke-memory-v2-qdrant.test.mjs scripts/smoke-memory-v2-external.test.mjs scripts/mock-memory-v2-services.test.mjs",
"test:mindspace-service": "node --test mindspace-service/mindspace-rpc-server.test.mjs",
"verify:chat-finish-sync": "node scripts/verify-chat-finish-sync.mjs",
"verify:public-finish-sync-runtime": "node scripts/verify-public-finish-sync-runtime.mjs",
@@ -66,7 +66,7 @@
"verify:mindspace-publish-guards:full": "node scripts/verify-mindspace-publish-guards.mjs --with-runtime",
"verify:mindspace-page-sync-guards": "node scripts/verify-mindspace-page-sync-guards.mjs",
"verify:public-page-interaction": "node scripts/verify-public-page-interaction.mjs",
"verify:page-data": "node --test page-data-acceptance.test.mjs page-data-integration.test.mjs page-data-public-service.test.mjs page-data-ops.test.mjs page-data-delivery-assess.test.mjs mindspace-page-data-finish-guard.test.mjs mindspace-page-data-finish-guard.integration.test.mjs",
"verify:page-data": "node --test mindspace-userdata-postgres.test.mjs postgres-user-data-space-service.test.mjs page-data-acceptance.test.mjs page-data-integration.test.mjs page-data-public-service.test.mjs page-data-ops.test.mjs page-data-delivery-assess.test.mjs mindspace-page-data-finish-guard.test.mjs mindspace-page-data-finish-guard.integration.test.mjs",
"verify:page-data-delivery": "node scripts/repair-page-data-workspace-bindings.mjs --dry-run",
"repair:page-data-bindings": "node scripts/repair-page-data-workspace-bindings.mjs",
"repair:page-data:103": "node scripts/ensure-page-data-datasets.mjs && node scripts/repair-page-data-workspace-bindings.mjs",
+5
View File
@@ -1,6 +1,7 @@
import pg from 'pg';
import {
buildControlSchemaSql,
buildEnsureCurrentUserCanSetRoleSql,
deriveUserSpaceNames,
provisionUserSpace,
quotePgIdentifier,
@@ -107,6 +108,10 @@ export function createPostgresUserDataSpaceService(options = {}) {
await client.query('ROLLBACK');
throw error;
}
} else {
// PostgreSQL 17 grants CREATEROLE owners ADMIN but not SET by default.
// Reconcile existing spaces before the runtime executes SET LOCAL ROLE.
await client.query(buildEnsureCurrentUserCanSetRoleSql(names.agentRole));
}
provisionedUsers.add(names.userId);
} finally {
+34
View File
@@ -0,0 +1,34 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createPostgresUserDataSpaceService } from './postgres-user-data-space-service.mjs';
const USER_ID = '18a143d1-3ac5-42b3-a4af-d07f8445bce6';
test('existing PostgreSQL user spaces reconcile agent SET permission before tenant access', async () => {
const queries = [];
const client = {
async query(sql) {
queries.push(String(sql));
if (String(sql).includes('SELECT migration_state FROM mindspace_control.user_spaces')) {
return { rows: [{ migration_state: 'cutover' }] };
}
if (String(sql).includes('FROM information_schema.columns')) return { rows: [] };
return { rows: [] };
},
release() {},
};
const service = createPostgresUserDataSpaceService({
userId: USER_ID,
pgPool: { connect: async () => client },
});
await service.getSchema();
const reconcileIndex = queries.findIndex((sql) => (
sql.includes("r.rolname = 'ms_u_18a143d13ac5_agent'")
&& sql.includes('WITH INHERIT FALSE, SET TRUE')
));
const setRoleIndex = queries.findIndex((sql) => sql.includes('SET LOCAL ROLE "ms_u_18a143d13ac5_agent"'));
assert.ok(reconcileIndex >= 0, 'existing space must reconcile the agent role membership');
assert.ok(setRoleIndex > reconcileIndex, 'role reconciliation must happen before SET LOCAL ROLE');
});
+22
View File
@@ -143,6 +143,26 @@ async function bundleSandboxMcp() {
await run(esbuildBin, args);
}
async function bundleMindSearchMcp() {
if (!(await exists(esbuildBin))) {
throw new Error(`未找到 esbuild: ${esbuildBin}`);
}
console.log('==> 打包 MindSearch MCP 为单文件 runtime');
const args = [
'tkmind-search-mcp.mjs',
'--bundle',
'--platform=node',
'--format=esm',
`--target=${runtimeNodeTarget}`,
'--outfile=.runtime/portal/tkmind-search-mcp.mjs',
'--banner:js=import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);',
];
for (const pkg of externalPackages) {
args.push(`--external:${pkg}`);
}
await run(esbuildBin, args);
}
async function bundleAgentRunWorker() {
if (!(await exists(esbuildBin))) {
throw new Error(`未找到 esbuild: ${esbuildBin}`);
@@ -414,6 +434,7 @@ async function writeMetadata() {
'',
'Bundled alongside server.mjs (required for sandbox-fs MCP):',
' mindspace-sandbox-mcp.mjs (esbuild bundle; includes schedule-service deps)',
' tkmind-search-mcp.mjs (esbuild bundle; required when MindSearch is enabled)',
' wechat-mp.bundle.mjs (esbuild bundle; hot-swappable WeChat MP module)',
'',
'Post-deploy validation (scripts/check-mindspace-public-links.mjs):',
@@ -486,6 +507,7 @@ async function main() {
await bundleServer();
await bundleWechatMp();
await bundleSandboxMcp();
await bundleMindSearchMcp();
await bundleAgentRunWorker();
if (runtimeInstallMode === 'bundle-node-modules' && !skipNodeModules) {
await copyNodeModules();
+19 -15
View File
@@ -152,7 +152,7 @@ fi
verify_runtime_artifact() {
local missing=0
for required in server.mjs wechat-mp.bundle.mjs mindspace-sandbox-mcp.mjs mindspace-public-links.mjs dist package.json scripts/run-memind-portal-prod.sh scripts/check-mindspace-public-links.mjs scripts/load-env.mjs scripts/wechat-mp-menu.mjs scripts/memind-portal-tunnel.sh; do
for required in server.mjs wechat-mp.bundle.mjs mindspace-sandbox-mcp.mjs tkmind-search-mcp.mjs mindspace-public-links.mjs dist package.json scripts/run-memind-portal-prod.sh scripts/check-mindspace-public-links.mjs scripts/load-env.mjs scripts/wechat-mp-menu.mjs scripts/memind-portal-tunnel.sh; do
if [[ ! -e "${RUNTIME_ROOT}/${required}" ]]; then
echo "runtime 产物缺失: ${RUNTIME_ROOT}/${required}" >&2
missing=1
@@ -214,21 +214,25 @@ else
fi
docker_bin="/opt/homebrew/bin/docker"
if [[ -x "${docker_bin}" ]] && "${docker_bin}" ps --format '{{.Names}}' 2>/dev/null | grep -q '^goosed-prod-1$'; then
goosed_indexes=()
while IFS= read -r name; do
if [[ "${name}" =~ ^goosed-prod-([0-9]+)$ ]]; then
goosed_containers=()
goosed_indexes=()
if [[ -x "${docker_bin}" ]]; then
while IFS='|' read -r name service; do
if [[ "${service}" =~ ^goosed-([0-9]+)$ ]]; then
goosed_containers+=("${name}")
goosed_indexes+=("${BASH_REMATCH[1]}")
fi
done < <("${docker_bin}" ps --format '{{.Names}}' | sort -V)
if ((${#goosed_indexes[@]} == 0)); then
echo "goosed dependency check failed: no goosed-prod-* containers running" >&2
missing=1
fi
for index in "${goosed_indexes[@]}"; do
container="goosed-prod-${index}"
done < <("${docker_bin}" ps \
--filter 'label=com.docker.compose.project=goosed-prod' \
--format '{{.Names}}|{{.Label "com.docker.compose.service"}}')
fi
if ((${#goosed_containers[@]} > 0)); then
for position in "${!goosed_containers[@]}"; do
container="${goosed_containers[$position]}"
index="${goosed_indexes[$position]}"
host_port=$((18005 + index))
if ! "${docker_bin}" ps --format '{{.Names}} {{.Ports}} {{.Status}}' | grep -q "^${container} .*0.0.0.0:${host_port}->18006/tcp.*healthy"; then
health="$("${docker_bin}" inspect "${container}" --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' 2>/dev/null || true)"
if [[ "${health}" != "healthy" ]] || ! "${docker_bin}" port "${container}" 18006/tcp 2>/dev/null | grep -q "0.0.0.0:${host_port}$"; then
echo "goosed dependency check failed: ${container} is not healthy on host port ${host_port}" >&2
missing=1
fi
@@ -244,8 +248,7 @@ if [[ -x "${docker_bin}" ]] && "${docker_bin}" ps --format '{{.Names}}' 2>/dev/n
echo "goosed dependency check failed: Portal .env must set GOOSED_MCP_NODE_PATH and GOOSED_MCP_SERVER_PATH for Docker goosed" >&2
missing=1
else
for index in "${goosed_indexes[@]}"; do
container="goosed-prod-${index}"
for container in "${goosed_containers[@]}"; do
if ! "${docker_bin}" exec "${container}" sh -lc "test -x '${portal_mcp_node}' && test -f '${portal_mcp_server}'" >/dev/null 2>&1; then
echo "goosed dependency check failed: ${container} cannot resolve Portal .env MCP paths '${portal_mcp_node}' and '${portal_mcp_server}'" >&2
missing=1
@@ -627,6 +630,7 @@ say "检查 live 目录中不再保留源码树"
allowed_live_mjs=(
"${APP_DIR}/server.mjs"
"${APP_DIR}/mindspace-sandbox-mcp.mjs"
"${APP_DIR}/tkmind-search-mcp.mjs"
"${APP_DIR}/mindspace-public-links.mjs"
)
extra_files=""