@cloudbase/cloudbase-mcp 2.25.4 → 2.25.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -205,13 +205,13 @@ function buildEnvDomainManagementResult(params) {
205
205
  operation: action,
206
206
  targetDomains: domains,
207
207
  asyncState: "PENDING",
208
- message: '安全域名已提交添加请求。该变更通常需要约 10 分钟传播,请继续轮询 queryEnv(action="domains"),直到目标域名状态为 ENABLE。',
208
+ message: '安全域名已提交添加请求。该变更通常需要数分钟传播(平台侧);请每 10 秒轮询 queryEnv(action="domains") 直到 Status 为 ENABLE,勿一次 sleep 满 10 分钟。',
209
209
  propagation: {
210
210
  requiresPolling: true,
211
211
  pollTool: "queryEnv",
212
212
  pollAction: "domains",
213
213
  pollIntervalSuggestionSeconds: 10,
214
- timeoutSuggestionSeconds: 600,
214
+ timeoutSuggestionSeconds: 300,
215
215
  successCondition: '目标域名出现在 queryEnv(action="domains") 返回中,且 Status 为 ENABLE。',
216
216
  },
217
217
  next_step: {
@@ -230,13 +230,13 @@ function buildEnvDomainManagementResult(params) {
230
230
  operation: action,
231
231
  targetDomains: domains,
232
232
  asyncState: "PENDING",
233
- message: '安全域名已提交删除请求。该变更可能需要数分钟传播,请继续轮询 queryEnv(action="domains"),直到目标域名不再出现。',
233
+ message: '安全域名已提交删除请求。该变更通常需要数分钟传播;请每 10 秒轮询 queryEnv(action="domains") 直到目标域名不再出现,勿一次 sleep 满数分钟。',
234
234
  propagation: {
235
235
  requiresPolling: true,
236
236
  pollTool: "queryEnv",
237
237
  pollAction: "domains",
238
238
  pollIntervalSuggestionSeconds: 10,
239
- timeoutSuggestionSeconds: 600,
239
+ timeoutSuggestionSeconds: 300,
240
240
  successCondition: '目标域名不再出现在 queryEnv(action="domains") 返回中。',
241
241
  },
242
242
  next_step: {
@@ -1944,7 +1944,7 @@ function registerEnvTools(server) {
1944
1944
  if (server.ide !== 'wxide') {
1945
1945
  server.registerTool?.("envDomainManagement", {
1946
1946
  title: "CloudBase 环境域名管理(安全域名 / CORS 白名单)",
1947
- description: "管理 CloudBase 环境的安全域名(安全域名 / CORS 白名单),支持添加和删除操作。(原工具名:createEnvDomain/deleteEnvDomain,为兼容旧AI规则可继续使用这些名称)当浏览器 Web 应用需要从本地 Vite / dev server 直接访问 CloudBase 资源时,应先用 queryEnv(action=domains) 检查当前实际浏览器 origin 对应的 host:port 是否已在白名单中,再按该实际值添加。新增或删除后通常需要继续轮询 queryEnv(action=domains) 确认状态收敛;安全域名一般约 10 分钟生效。⚠️ 重要:此工具仅用于 CORS/请求来源验证,不涉及 SSL 证书。自定义域名公网 HTTPS:先 queryGateway(listCustomDomains);已有域名则 manageGateway(createRoute) 显式传 domain(无需证书);仅首次绑定新域名才用 bindCustomDomain(需 certificateId)。",
1947
+ description: "管理 CloudBase 环境的安全域名(安全域名 / CORS 白名单),支持添加和删除操作。(原工具名:createEnvDomain/deleteEnvDomain,为兼容旧AI规则可继续使用这些名称)当浏览器 Web 应用需要从本地 Vite / dev server 直接访问 CloudBase 资源时,应先用 queryEnv(action=domains) 检查当前实际浏览器 origin 对应的 host:port 是否已在白名单中,再按该实际值添加。新增或删除后请每约 10 秒轮询 queryEnv(action=domains) 确认状态收敛,勿一次 sleep 满 10 分钟;多数环境数分钟内可收敛。⚠️ 重要:此工具仅用于 CORS/请求来源验证,不涉及 SSL 证书。自定义域名公网 HTTPS:先 queryGateway(listCustomDomains);已有域名则 manageGateway(createRoute) 显式传 domain(无需证书);仅首次绑定新域名才用 bindCustomDomain(需 certificateId)。",
1948
1948
  inputSchema: {
1949
1949
  action: zod_1.z
1950
1950
  .enum(["create", "delete"])
@@ -2772,7 +2772,7 @@ ${envIdSection}
2772
2772
  ## 环境信息
2773
2773
  - 操作系统: ${os_1.default.type()} ${os_1.default.release()}
2774
2774
  - Node.js版本: ${process.version}
2775
- - MCP 版本:${process.env.npm_package_version || "2.25.4" || 0}
2775
+ - MCP 版本:${process.env.npm_package_version || "2.25.6" || 0}
2776
2776
  - 系统架构: ${os_1.default.arch()}
2777
2777
  - 时间: ${new Date().toISOString()}
2778
2778
  - 请求ID: ${requestId}
@@ -2853,7 +2853,7 @@ function createWrappedHandler(name, handler, server) {
2853
2853
  requestId: (typeof error === 'object' && error && 'requestId' in error) ? error.requestId : '',
2854
2854
  ide: server.ide || process.env.INTEGRATION_IDE || ''
2855
2855
  });
2856
- const mcpVersion = true ? "2.25.4" : 0;
2856
+ const mcpVersion = true ? "2.25.6" : 0;
2857
2857
  const requestIdSuffix = requestId ? `\n🆔 RequestId: ${requestId}` : '';
2858
2858
  const enhancedErrorMessage = `${errorMessage}${requestIdSuffix}\n\n📦 CloudBase MCP v${mcpVersion}\n🔗 遇到问题?请复制以下链接到浏览器打开\n即可自动携带错误详情快速创建 GitHub Issue:\n${issueLink}`;
2859
2859
  // 创建新的错误对象,保持原有的错误类型但更新消息
@@ -7165,7 +7165,7 @@ const KnowledgeBaseIdMap = {
7165
7165
  const CACHE_BASE_DIR = path.join(os.homedir(), ".cloudbase-mcp");
7166
7166
  const CACHE_META_FILE = path.join(CACHE_BASE_DIR, "cache-meta.json");
7167
7167
  const LOCK_FILE = path.join(CACHE_BASE_DIR, ".download.lock");
7168
- const DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 默认 24 小时
7168
+ const DEFAULT_CACHE_TTL_MS = 60 * 60 * 1000; // default 1 hour (was 24h; templates/docs refresh more often)
7169
7169
  // Promise wrapper for lockfile methods
7170
7170
  function acquireLock(lockPath, options) {
7171
7171
  return new Promise((resolve, reject) => {
@@ -7960,7 +7960,11 @@ function registerGatewayTools(server) {
7960
7960
  return result ?? {};
7961
7961
  };
7962
7962
  const buildPrivilegeDescription = (privilege) => {
7963
- const serviceStatus = privilege.EnableService === true ? "已开启" : "未开启";
7963
+ const serviceStatus = privilege.EnableService === undefined
7964
+ ? "未知"
7965
+ : privilege.EnableService === true
7966
+ ? "已开启"
7967
+ : "未开启";
7964
7968
  const authStatus = privilege.EnableAuth === undefined
7965
7969
  ? "未知"
7966
7970
  : privilege.EnableAuth === true
@@ -8052,7 +8056,7 @@ function registerGatewayTools(server) {
8052
8056
  {
8053
8057
  tool: "queryGateway",
8054
8058
  action: "getRoute",
8055
- reason: "等待 30 秒到 3 分钟后再确认访问入口是否已生效",
8059
+ reason: "创建后通常数秒到约 30 秒内生效;请立刻轮询 getRoute 或探测 accessUrl,勿盲等 60 秒以上",
8056
8060
  },
8057
8061
  {
8058
8062
  tool: "queryPermissions",
@@ -8200,7 +8204,7 @@ function registerGatewayTools(server) {
8200
8204
  const privilege = await getGatewayPrivilege();
8201
8205
  if (privilege.EnableService !== true) {
8202
8206
  privilegeHint =
8203
- "⚠️ HTTP 网关总开关未开启,路由创建成功后访问仍将返回 HTTPSERVICE_NONACTIVATED(403);请先调用 manageGateway(action=\"enableService\", enable=true) 开启,再等待 30 秒到 3 分钟访问。";
8207
+ "⚠️ HTTP 网关总开关未开启,路由创建成功后访问仍将返回 HTTPSERVICE_NONACTIVATED(403);请先调用 manageGateway(action=\"enableService\", enable=true) 开启,再立刻探测 accessUrl(通常数秒到约 30 秒内生效,勿盲等 60 秒以上)。";
8204
8208
  privilegeNextActions = [
8205
8209
  {
8206
8210
  tool: "manageGateway",
@@ -8236,7 +8240,7 @@ function registerGatewayTools(server) {
8236
8240
  : payload.resolved.enablePathTransmission === false
8237
8241
  ? ";路径透传关闭(网关会剥掉触发路径前缀后再转发给后端)"
8238
8242
  : ";路径透传未显式设置(平台默认 false,会剥掉触发路径前缀)") +
8239
- `。注意:路由配置传播通常需要等待 30 秒到 3 分钟,请勿立即访问。该操作只创建网关入口,不会自动放开上游权限;若上游是云函数且需要匿名或浏览器直接访问,请继续检查函数资源权限。` +
8243
+ `。注意:路由配置传播通常数秒到约 30 秒;请立刻用 queryGateway(getRoute) 或探测 accessUrl 确认,勿盲等 60 秒以上。该操作只创建网关入口,不会自动放开上游权限;若上游是云函数且需要匿名或浏览器直接访问,请继续检查函数资源权限。` +
8240
8244
  (privilegeHint ? ` ${privilegeHint}` : ""), [
8241
8245
  ...privilegeNextActions,
8242
8246
  ...(routeMutationNextActions(payload.resolved.upstreamResourceName) ??
@@ -8300,12 +8304,28 @@ function registerGatewayTools(server) {
8300
8304
  if (!input.domain || !input.certificateId) {
8301
8305
  throw new Error("action=bindCustomDomain 时必须提供 domain 和 certificateId");
8302
8306
  }
8307
+ const accessType = input.accessType ?? "DIRECT";
8308
+ if (accessType === "CUSTOM" && !input.customCname) {
8309
+ throw new Error("action=bindCustomDomain 且 accessType=CUSTOM 时必须提供 customCname(自有 CDN/WAF 的回源/回填地址,不是 DNS 解析目标)。" +
8310
+ "说明:https://docs.cloudbase.net/service/custom-domain");
8311
+ }
8312
+ if (accessType !== "CUSTOM" && input.customCname) {
8313
+ throw new Error("customCname 仅在 accessType=CUSTOM 时可用;普通绑定用默认 DIRECT,不要传 customCname。" +
8314
+ "说明:https://docs.cloudbase.net/service/custom-domain");
8315
+ }
8303
8316
  const cloudbase = await getManager();
8304
8317
  const result = await cloudbase.env.bindCustomDomain({
8305
8318
  EnvId: await resolveEnvId(),
8306
8319
  Domain: {
8307
8320
  Domain: input.domain,
8308
8321
  CertId: input.certificateId,
8322
+ AccessType: accessType,
8323
+ ...(input.enable !== undefined
8324
+ ? { Enable: input.enable }
8325
+ : {}),
8326
+ ...(input.customCname
8327
+ ? { CustomCname: input.customCname }
8328
+ : {}),
8309
8329
  },
8310
8330
  });
8311
8331
  (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
@@ -8313,24 +8333,64 @@ function registerGatewayTools(server) {
8313
8333
  action: input.action,
8314
8334
  domain: input.domain,
8315
8335
  certificateId: input.certificateId,
8336
+ accessType,
8337
+ ...(input.customCname
8338
+ ? { customCname: input.customCname }
8339
+ : {}),
8340
+ ...(input.enable !== undefined
8341
+ ? { enable: input.enable }
8342
+ : {}),
8316
8343
  raw: result,
8317
- }, "自定义域名绑定成功");
8344
+ }, `自定义域名绑定成功(${accessType})`, [
8345
+ {
8346
+ tool: "manageGateway",
8347
+ action: "createRoute",
8348
+ reason: "绑定后需 createRoute 添加访问路径,并完成 DNS CNAME 解析后才可访问",
8349
+ },
8350
+ ]);
8318
8351
  }
8319
8352
  case "deleteCustomDomain": {
8320
8353
  if (!input.domain) {
8321
8354
  throw new Error("action=deleteCustomDomain 时必须提供 domain");
8322
8355
  }
8323
8356
  const cloudbase = await getManager();
8324
- const result = await cloudbase.env.deleteCustomDomain({
8325
- EnvId: await resolveEnvId(),
8326
- Domain: input.domain,
8327
- });
8328
- (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
8329
- return buildEnvelope({
8330
- action: input.action,
8331
- domain: input.domain,
8332
- raw: result,
8333
- }, "自定义域名删除成功");
8357
+ try {
8358
+ const result = await cloudbase.env.deleteCustomDomain({
8359
+ EnvId: await resolveEnvId(),
8360
+ Domain: input.domain,
8361
+ });
8362
+ (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
8363
+ return buildEnvelope({
8364
+ action: input.action,
8365
+ domain: input.domain,
8366
+ raw: result,
8367
+ }, "自定义域名删除成功");
8368
+ }
8369
+ catch (error) {
8370
+ const message = error instanceof Error ? error.message : String(error);
8371
+ // SDK 在域名下仍有路由绑定时抛错,需先删路由再删域名
8372
+ if (/route binding/i.test(message)) {
8373
+ return {
8374
+ success: false,
8375
+ data: { action: input.action, domain: input.domain },
8376
+ message: `域名 ${input.domain} 下仍有路由绑定,需先删除路由再删除域名。` +
8377
+ `原始错误:${message}`,
8378
+ nextActions: [
8379
+ {
8380
+ tool: "queryGateway",
8381
+ action: "listRoutes",
8382
+ reason: "查看该域名下的路由,确认需要删除的路径",
8383
+ },
8384
+ {
8385
+ tool: "manageGateway",
8386
+ action: "deleteRoute",
8387
+ reason: "先删除该域名下的全部路由(逐个 deleteRoute),再重试 deleteCustomDomain",
8388
+ },
8389
+ ],
8390
+ };
8391
+ }
8392
+ throw error;
8393
+ }
8334
8394
  }
8335
8395
  case "enableService": {
8336
8396
  if (typeof input.enable !== "boolean") {
@@ -8443,7 +8503,8 @@ function registerGatewayTools(server) {
8443
8503
  "enableService/authSwitch 开关 HTTP 网关总开关与访问鉴权(需配合 enable 参数)。" +
8444
8504
  "createRoute/updateRoute 必须提供 upstreamResourceType。" +
8445
8505
  "已有自定义域名时优先 createRoute(domain=已有域名) 实现访问,不必再次 bindCustomDomain / 传入 certificateId;" +
8446
- "bindCustomDomain 仅用于首次绑定新域名。"),
8506
+ "bindCustomDomain 仅用于首次绑定新域名(需 certificateId;可选 accessType=DIRECT|CDN|CUSTOM,CUSTOM 需 customCname;普通场景用默认 DIRECT)。" +
8507
+ "接入说明:https://docs.cloudbase.net/service/custom-domain"),
8447
8508
  targetName: zod_1.z
8448
8509
  .string()
8449
8510
  .optional()
@@ -8506,11 +8567,24 @@ function registerGatewayTools(server) {
8506
8567
  .optional()
8507
8568
  .describe("证书 ID。仅首次 bindCustomDomain 必填。" +
8508
8569
  "在已有自定义域名上 createRoute / updateRoute / deleteRoute 不需要 certificateId。"),
8570
+ accessType: zod_1.z
8571
+ .enum(["DIRECT", "CDN", "CUSTOM"])
8572
+ .optional()
8573
+ .describe("绑定类型(仅 bindCustomDomain,默认 DIRECT)。" +
8574
+ "DIRECT=直连(普通绑域名用这个);CDN=云开发 CDN;CUSTOM=自有 CDN/WAF(需 customCname)。" +
8575
+ "详见 https://docs.cloudbase.net/service/custom-domain"),
8576
+ customCname: zod_1.z
8577
+ .string()
8578
+ .optional()
8579
+ .describe("自有 CDN/WAF 的回源/回填地址(仅 bindCustomDomain 且 accessType=CUSTOM)。" +
8580
+ "不是 DNS 里用户域名要解析到的那个 CNAME;DIRECT/CDN 不要传。" +
8581
+ "详见 https://docs.cloudbase.net/service/custom-domain"),
8509
8582
  enable: zod_1.z
8510
8583
  .boolean()
8511
8584
  .optional()
8512
- .describe("开关目标状态(仅 enableService / authSwitch 使用,必填):" +
8513
- "enable=true 开启,enable=false 关闭。省略或非布尔值会返回参数错误。"),
8585
+ .describe("开关目标状态(enableService / authSwitch 必填):enable=true 开启,enable=false 关闭。" +
8586
+ "bindCustomDomain 可选:enable=false 表示绑定后禁用域名(默认启用)。" +
8587
+ "省略或非布尔值会返回参数错误。"),
8514
8588
  },
8515
8589
  annotations: {
8516
8590
  readOnlyHint: false,
@@ -11134,7 +11208,7 @@ class TelemetryReporter {
11134
11208
  const nodeVersion = process.version; // Node.js版本
11135
11209
  const arch = os_1.default.arch(); // 系统架构
11136
11210
  // 从构建时注入的版本号获取MCP版本信息
11137
- const mcpVersion = process.env.npm_package_version || "2.25.4" || 0;
11211
+ const mcpVersion = process.env.npm_package_version || "2.25.6" || 0;
11138
11212
  return {
11139
11213
  userAgent: `${osType} ${osRelease} ${arch} ${nodeVersion} CloudBase-MCP/${mcpVersion}`,
11140
11214
  deviceId: this.deviceId,
@@ -11471,6 +11545,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
11471
11545
  exports.CUSTOM_IMAGE_RUNTIME = exports.MANAGE_FUNCTION_ACTIONS = exports.QUERY_FUNCTION_ACTIONS = exports.TRIGGER_CONFIG_EXAMPLES = exports.SUPPORTED_TRIGGER_TYPES = exports.DEFAULT_NODEJS_RUNTIME = exports.SUPPORTED_NODEJS_RUNTIMES = exports.RECOMMENDED_RUNTIMES = exports.DEFAULT_RUNTIME = exports.ALL_SUPPORTED_RUNTIMES = exports.SUPPORTED_RUNTIMES = void 0;
11472
11546
  exports.formatRuntimeList = formatRuntimeList;
11473
11547
  exports.validateTimerCron = validateTimerCron;
11548
+ exports.pickManageFunctionName = pickManageFunctionName;
11474
11549
  exports.shouldInstallDependencyForFunction = shouldInstallDependencyForFunction;
11475
11550
  exports.resolveEventFunctionRuntime = resolveEventFunctionRuntime;
11476
11551
  exports.buildFunctionOperationErrorMessage = buildFunctionOperationErrorMessage;
@@ -11699,6 +11774,19 @@ const MANAGE_LAYER_SCHEMA = zod_1.z.object({
11699
11774
  layerName: zod_1.z.string().describe("层名称"),
11700
11775
  layerVersion: zod_1.z.number().describe("层版本号"),
11701
11776
  });
11777
+ /**
11778
+ * Prefer top-level functionName; fall back to func.name for the common
11779
+ * mistake of nesting the target name under func (createFunction shape).
11780
+ */
11781
+ function pickManageFunctionName(input) {
11782
+ if (typeof input.functionName === "string" && input.functionName.trim()) {
11783
+ return input.functionName.trim();
11784
+ }
11785
+ if (typeof input.func?.name === "string" && input.func.name.trim()) {
11786
+ return input.func.name.trim();
11787
+ }
11788
+ return undefined;
11789
+ }
11702
11790
  function normalizeFunctionLayers(layers) {
11703
11791
  if (!Array.isArray(layers)) {
11704
11792
  return [];
@@ -12165,7 +12253,12 @@ function registerFunctionTools(server) {
12165
12253
  throw new Error(`不支持的操作类型: ${input.action}`);
12166
12254
  }
12167
12255
  };
12168
- const handleManageFunctions = async (input) => {
12256
+ const handleManageFunctions = async (rawInput) => {
12257
+ // Accept func.name as a fallback for top-level functionName (common agent mistake).
12258
+ const pickedName = pickManageFunctionName(rawInput);
12259
+ const input = pickedName && rawInput.functionName !== pickedName
12260
+ ? { ...rawInput, functionName: pickedName }
12261
+ : rawInput;
12169
12262
  ensureActionAllowedInCloudMode(input);
12170
12263
  switch (input.action) {
12171
12264
  case "createFunction": {
@@ -12871,7 +12964,12 @@ function registerFunctionTools(server) {
12871
12964
  "此参数传 cloudfunctions 或 functions 目录的绝对路径。" +
12872
12965
  "SDK 会自动拼接函数名子目录,无需预先压缩 zip 或 base64 编码。"),
12873
12966
  force: zod_1.z.boolean().optional().describe("createFunction 时是否覆盖"),
12874
- functionName: zod_1.z.string().optional().describe("函数名称。大多数 action 使用该字段作为统一目标"),
12967
+ functionName: zod_1.z
12968
+ .string()
12969
+ .optional()
12970
+ .describe("目标函数名称(顶层)。updateFunctionCode / updateFunctionConfig / invokeFunction 等 action 使用此字段。" +
12971
+ "不要只写在 func.name:createFunction 用 func.name,其它 action 用顶层 functionName。" +
12972
+ "若误传 func.name,也会被识别为 functionName。"),
12875
12973
  zipFile: zod_1.z.string().optional().describe("仅兼容特殊场景:预先准备好的代码包 base64 编码。普通 createFunction/updateFunctionCode 默认不要先压缩 zip,优先使用 functionRootPath。"),
12876
12974
  handler: zod_1.z.string().optional().describe("函数入口"),
12877
12975
  timeout: zod_1.z.number().optional().describe("配置更新时的超时时间"),
@@ -12926,12 +13024,47 @@ module.exports = __WEBPACK_EXTERNAL_MODULE_ws__;
12926
13024
  /***/ }),
12927
13025
 
12928
13026
  /***/ 6473:
12929
- /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
13027
+ /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
12930
13028
 
12931
13029
 
13030
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13031
+ if (k2 === undefined) k2 = k;
13032
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13033
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13034
+ desc = { enumerable: true, get: function() { return m[k]; } };
13035
+ }
13036
+ Object.defineProperty(o, k2, desc);
13037
+ }) : (function(o, m, k, k2) {
13038
+ if (k2 === undefined) k2 = k;
13039
+ o[k2] = m[k];
13040
+ }));
13041
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
13042
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
13043
+ }) : function(o, v) {
13044
+ o["default"] = v;
13045
+ });
13046
+ var __importStar = (this && this.__importStar) || (function () {
13047
+ var ownKeys = function(o) {
13048
+ ownKeys = Object.getOwnPropertyNames || function (o) {
13049
+ var ar = [];
13050
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
13051
+ return ar;
13052
+ };
13053
+ return ownKeys(o);
13054
+ };
13055
+ return function (mod) {
13056
+ if (mod && mod.__esModule) return mod;
13057
+ var result = {};
13058
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
13059
+ __setModuleDefault(result, mod);
13060
+ return result;
13061
+ };
13062
+ })();
12932
13063
  Object.defineProperty(exports, "__esModule", ({ value: true }));
12933
13064
  exports.__resetPgReadyCache = __resetPgReadyCache;
12934
13065
  exports.registerPGDatabaseTools = registerPGDatabaseTools;
13066
+ const fs = __importStar(__webpack_require__(4421));
13067
+ const path = __importStar(__webpack_require__(2521));
12935
13068
  const zod_1 = __webpack_require__(2971);
12936
13069
  const cloudbase_manager_js_1 = __webpack_require__(3431);
12937
13070
  const tool_result_js_1 = __webpack_require__(9835);
@@ -12952,6 +13085,8 @@ const MANAGE_ACTIONS = [
12952
13085
  "applyMigration",
12953
13086
  "listMigrations",
12954
13087
  "migrationDetail",
13088
+ "describeMigrationTask",
13089
+ "fetchMigration",
12955
13090
  "rollbackMigration",
12956
13091
  "repairMigration",
12957
13092
  ];
@@ -13112,8 +13247,170 @@ function isSchemaDdlRisk(risk, sql) {
13112
13247
  const verb = getSqlVerb(stripLeadingSqlComments(sql));
13113
13248
  return ["DROP", "TRUNCATE", "ALTER"].includes(verb);
13114
13249
  }
13250
+ /**
13251
+ * Canonical local migration directory — must match CloudBase CLI
13252
+ * `MIGRATIONS_DIR` (`cloudbase/migrations`) so MCP+CLI mixed workflows share files.
13253
+ * Legacy bare `migrations/` is accepted only when an existing matching file is found.
13254
+ */
13255
+ const LOCAL_MIGRATIONS_DIR = "cloudbase/migrations";
13256
+ const LOCAL_MIGRATIONS_DIR_LEGACY = "migrations";
13257
+ /** Same constraint as applyMigration.migrationName (broader than CLI /^[a-z_]+$/ — digits allowed). */
13258
+ const MIGRATION_NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
13259
+ function getMigrationProjectRoot() {
13260
+ const fromEnv = process.env.WORKSPACE_FOLDER_PATHS ||
13261
+ process.env.PROJECT_ROOT ||
13262
+ process.env.GITHUB_WORKSPACE ||
13263
+ process.env.CI_PROJECT_DIR ||
13264
+ process.env.BUILD_SOURCESDIRECTORY;
13265
+ if (fromEnv && fromEnv.trim()) {
13266
+ // IDEs may inject multiple folders separated by path.delimiter.
13267
+ const first = fromEnv.split(path.delimiter).map((p) => p.trim()).find(Boolean);
13268
+ if (first) {
13269
+ return first;
13270
+ }
13271
+ }
13272
+ return process.cwd();
13273
+ }
13274
+ function buildLocalMigrationFileName(version, name) {
13275
+ return `${version}_${name}.sql`;
13276
+ }
13115
13277
  function buildLocalMigrationFileHint(version, name) {
13116
- return `migrations/${version}_${name}.sql`;
13278
+ return `${LOCAL_MIGRATIONS_DIR}/${buildLocalMigrationFileName(version, name)}`;
13279
+ }
13280
+ /** Normalize SQL for local file compare/write (LF + single trailing newline). */
13281
+ function normalizeMigrationSqlForLocalFile(sql) {
13282
+ const normalized = sql.replace(/\r\n/g, "\n").replace(/\s+$/u, "");
13283
+ return `${normalized}\n`;
13284
+ }
13285
+ /**
13286
+ * Ensure the workspace has a Git-truth SQL file for this migration before Push.
13287
+ * Prefer write-to-workspace when missing; fail closed on mismatch or write failure
13288
+ * (Supabase MCP#241 class: remote apply without local truth source).
13289
+ */
13290
+ function ensureLocalMigrationSqlFile(version, name, sql) {
13291
+ const fileName = buildLocalMigrationFileName(version, name);
13292
+ const expected = normalizeMigrationSqlForLocalFile(sql);
13293
+ const projectRoot = getMigrationProjectRoot();
13294
+ const candidates = [
13295
+ {
13296
+ relativePath: `${LOCAL_MIGRATIONS_DIR}/${fileName}`,
13297
+ absolutePath: path.join(projectRoot, LOCAL_MIGRATIONS_DIR, fileName),
13298
+ },
13299
+ {
13300
+ relativePath: `${LOCAL_MIGRATIONS_DIR_LEGACY}/${fileName}`,
13301
+ absolutePath: path.join(projectRoot, LOCAL_MIGRATIONS_DIR_LEGACY, fileName),
13302
+ },
13303
+ ];
13304
+ for (const candidate of candidates) {
13305
+ if (!fs.existsSync(candidate.absolutePath)) {
13306
+ continue;
13307
+ }
13308
+ let existing;
13309
+ try {
13310
+ existing = fs.readFileSync(candidate.absolutePath, "utf8");
13311
+ }
13312
+ catch (error) {
13313
+ return {
13314
+ ok: false,
13315
+ errorCode: "LOCAL_MIGRATION_FILE_WRITE_FAILED",
13316
+ message: `Failed to read existing local migration file ${candidate.relativePath}: ` +
13317
+ `${error instanceof Error ? error.message : String(error)}`,
13318
+ relativePath: candidate.relativePath,
13319
+ absolutePath: candidate.absolutePath,
13320
+ };
13321
+ }
13322
+ if (normalizeMigrationSqlForLocalFile(existing) === expected) {
13323
+ return {
13324
+ ok: true,
13325
+ relativePath: candidate.relativePath,
13326
+ absolutePath: candidate.absolutePath,
13327
+ action: "matched",
13328
+ };
13329
+ }
13330
+ return {
13331
+ ok: false,
13332
+ errorCode: "LOCAL_MIGRATION_FILE_MISMATCH",
13333
+ message: `Local migration file ${candidate.relativePath} already exists but its content does not match the sql argument. ` +
13334
+ "Refuse to overwrite (fail closed). Update the local file to match, or use a new migrationVersion / migrationName. " +
13335
+ "Push was NOT submitted.",
13336
+ relativePath: candidate.relativePath,
13337
+ absolutePath: candidate.absolutePath,
13338
+ };
13339
+ }
13340
+ const target = candidates[0];
13341
+ try {
13342
+ fs.mkdirSync(path.dirname(target.absolutePath), { recursive: true });
13343
+ fs.writeFileSync(target.absolutePath, expected, "utf8");
13344
+ }
13345
+ catch (error) {
13346
+ return {
13347
+ ok: false,
13348
+ errorCode: "LOCAL_MIGRATION_FILE_WRITE_FAILED",
13349
+ message: `Failed to write local migration file ${target.relativePath} under workspace ${projectRoot}: ` +
13350
+ `${error instanceof Error ? error.message : String(error)}. ` +
13351
+ "Push was NOT submitted. Fix workspace permissions or write the file manually, then retry applyMigration.",
13352
+ relativePath: target.relativePath,
13353
+ absolutePath: target.absolutePath,
13354
+ };
13355
+ }
13356
+ return {
13357
+ ok: true,
13358
+ relativePath: target.relativePath,
13359
+ absolutePath: target.absolutePath,
13360
+ action: "created",
13361
+ };
13362
+ }
13363
+ /**
13364
+ * Sync remote migration Query bodies into cloudbase/migrations/ (CLI fetch parity).
13365
+ * Without force, existing files are skipped; with force, overwritten.
13366
+ */
13367
+ function syncFetchedMigrationsToLocalFiles(items, force) {
13368
+ const projectRoot = getMigrationProjectRoot();
13369
+ const migrationsDir = path.join(projectRoot, LOCAL_MIGRATIONS_DIR);
13370
+ const files = [];
13371
+ for (const item of items) {
13372
+ const version = item.Version.trim();
13373
+ const name = item.Name.trim();
13374
+ if (!MIGRATION_NAME_PATTERN.test(name)) {
13375
+ return {
13376
+ ok: false,
13377
+ errorCode: "LOCAL_MIGRATION_FETCH_INVALID_NAME",
13378
+ message: `Remote migration name "${name}" (version=${version}) is invalid. ` +
13379
+ "Names must match /^[a-z][a-z0-9_]*$/ (same as applyMigration.migrationName). " +
13380
+ "Repair the remote history record, then retry fetchMigration.",
13381
+ };
13382
+ }
13383
+ if (!item.Query.trim()) {
13384
+ return {
13385
+ ok: false,
13386
+ errorCode: "LOCAL_MIGRATION_FETCH_EMPTY_QUERY",
13387
+ message: `DescribePGUserMigration returned empty Query for version=${version} (${name}). ` +
13388
+ "Refuse to write an empty local migration file (would poison Git checksums).",
13389
+ };
13390
+ }
13391
+ const relativePath = buildLocalMigrationFileHint(version, name);
13392
+ const absolutePath = path.join(migrationsDir, buildLocalMigrationFileName(version, name));
13393
+ const exists = fs.existsSync(absolutePath);
13394
+ if (exists && !force) {
13395
+ files.push({ version, name, relativePath, absolutePath, action: "skipped" });
13396
+ continue;
13397
+ }
13398
+ try {
13399
+ fs.mkdirSync(migrationsDir, { recursive: true });
13400
+ fs.writeFileSync(absolutePath, normalizeMigrationSqlForLocalFile(item.Query), "utf8");
13401
+ }
13402
+ catch (error) {
13403
+ return {
13404
+ ok: false,
13405
+ errorCode: "LOCAL_MIGRATION_FILE_WRITE_FAILED",
13406
+ message: `Failed to write local migration file ${relativePath} under workspace ${projectRoot}: ` +
13407
+ `${error instanceof Error ? error.message : String(error)}`,
13408
+ relativePath,
13409
+ };
13410
+ }
13411
+ files.push({ version, name, relativePath, absolutePath, action: "written" });
13412
+ }
13413
+ return { ok: true, files };
13117
13414
  }
13118
13415
  /** How many recent migration records to scan when verifying a freshly pushed version. */
13119
13416
  const MIGRATION_VERIFY_LIMIT = 100;
@@ -13144,6 +13441,31 @@ function collectMigrationVersions(value, found = new Set()) {
13144
13441
  /** Brief retries before concluding a pushed migration never landed (Push can be async). */
13145
13442
  const MIGRATION_VERIFY_ATTEMPTS = 4;
13146
13443
  const MIGRATION_VERIFY_DELAY_MS = 1500;
13444
+ /** Page size when hydrating remote migration history into a Push/Preview payload. */
13445
+ const MIGRATION_LIST_PAGE_SIZE = 100;
13446
+ /**
13447
+ * PushPGUserMigrations is async: poll DescribeTaskResult until Succeed/Failed.
13448
+ * Default wait aligns with CLI `tcb db pg migration up` (10 minutes).
13449
+ * Override per call via applyMigration.taskPollTimeoutMs.
13450
+ */
13451
+ const MIGRATION_TASK_POLL_INTERVAL_MS = 1500;
13452
+ /** CLI parity: `TASK_MAX_WAIT_MS = 10 * 60 * 1000` in cloudbase-cli migration up. */
13453
+ const MIGRATION_TASK_DEFAULT_WAIT_MS = 10 * 60 * 1000;
13454
+ const MIGRATION_TASK_MIN_WAIT_MS = 5_000;
13455
+ const MIGRATION_TASK_MAX_WAIT_MS = MIGRATION_TASK_DEFAULT_WAIT_MS;
13456
+ function resolveMigrationTaskPollTimeoutMs(requested) {
13457
+ if (requested === undefined || !Number.isFinite(requested)) {
13458
+ return MIGRATION_TASK_DEFAULT_WAIT_MS;
13459
+ }
13460
+ const rounded = Math.floor(requested);
13461
+ if (rounded < MIGRATION_TASK_MIN_WAIT_MS) {
13462
+ return MIGRATION_TASK_MIN_WAIT_MS;
13463
+ }
13464
+ if (rounded > MIGRATION_TASK_MAX_WAIT_MS) {
13465
+ return MIGRATION_TASK_MAX_WAIT_MS;
13466
+ }
13467
+ return rounded;
13468
+ }
13147
13469
  /**
13148
13470
  * Confirm a pushed migration version is present in the remote migration history.
13149
13471
  *
@@ -13177,8 +13499,224 @@ async function verifyMigrationApplied(context, version, cloudBaseOptions) {
13177
13499
  }
13178
13500
  return { applied: null, error: lastError };
13179
13501
  }
13502
+ function extractMigrationSummaries(history) {
13503
+ const raw = history.Migrations;
13504
+ if (!Array.isArray(raw)) {
13505
+ return [];
13506
+ }
13507
+ const summaries = [];
13508
+ for (const item of raw) {
13509
+ if (!item || typeof item !== "object") {
13510
+ continue;
13511
+ }
13512
+ const row = item;
13513
+ const version = typeof row.Version === "string" ? row.Version.trim() : "";
13514
+ const name = typeof row.Name === "string" ? row.Name.trim() : "";
13515
+ if (version) {
13516
+ summaries.push({ Version: version, Name: name || version });
13517
+ }
13518
+ }
13519
+ return summaries;
13520
+ }
13521
+ /**
13522
+ * List every remote migration summary (paginated). Required before Push/Preview:
13523
+ * submitting only the pending migration while remote history exists yields
13524
+ * Executable=false with Conflicts reason remote_history_not_found_locally, and the
13525
+ * async task fails with "migration plan is not executable".
13526
+ */
13527
+ async function listAllRemoteMigrationSummaries(context, cloudBaseOptions) {
13528
+ const summaries = [];
13529
+ let offset = 0;
13530
+ let latestVersion = "";
13531
+ let total = 0;
13532
+ while (true) {
13533
+ const page = await callPgMigrationApi(context, "ListPGUserMigrations", { Limit: MIGRATION_LIST_PAGE_SIZE, Offset: offset }, cloudBaseOptions);
13534
+ const pageItems = extractMigrationSummaries(page);
13535
+ summaries.push(...pageItems);
13536
+ if (typeof page.LatestVersion === "string" && page.LatestVersion.trim()) {
13537
+ latestVersion = page.LatestVersion.trim();
13538
+ }
13539
+ if (typeof page.Total === "number" && Number.isFinite(page.Total)) {
13540
+ total = page.Total;
13541
+ }
13542
+ else {
13543
+ total = summaries.length;
13544
+ }
13545
+ if (pageItems.length < MIGRATION_LIST_PAGE_SIZE) {
13546
+ break;
13547
+ }
13548
+ offset += MIGRATION_LIST_PAGE_SIZE;
13549
+ }
13550
+ return { summaries, latestVersion, total };
13551
+ }
13552
+ /** `<version>_<name>.sql` — version is the identity key for completeness checks. */
13553
+ const LOCAL_MIGRATION_FILENAME_RE = /^(\d{14})_(.+)\.sql$/;
13554
+ function listLocalMigrationFilesInDir(dirAbsolute, dirRelative) {
13555
+ const map = new Map();
13556
+ if (!fs.existsSync(dirAbsolute)) {
13557
+ return map;
13558
+ }
13559
+ let stat;
13560
+ try {
13561
+ stat = fs.statSync(dirAbsolute);
13562
+ }
13563
+ catch {
13564
+ return map;
13565
+ }
13566
+ if (!stat.isDirectory()) {
13567
+ return map;
13568
+ }
13569
+ let fileNames;
13570
+ try {
13571
+ fileNames = fs.readdirSync(dirAbsolute);
13572
+ }
13573
+ catch {
13574
+ return map;
13575
+ }
13576
+ for (const fileName of fileNames) {
13577
+ const match = LOCAL_MIGRATION_FILENAME_RE.exec(fileName);
13578
+ if (!match) {
13579
+ continue;
13580
+ }
13581
+ const version = match[1];
13582
+ const name = match[2];
13583
+ const absolutePath = path.join(dirAbsolute, fileName);
13584
+ let query;
13585
+ try {
13586
+ query = fs.readFileSync(absolutePath, "utf8");
13587
+ }
13588
+ catch {
13589
+ continue;
13590
+ }
13591
+ if (!query.trim()) {
13592
+ continue;
13593
+ }
13594
+ map.set(version, {
13595
+ version,
13596
+ name,
13597
+ query,
13598
+ relativePath: `${dirRelative}/${fileName}`,
13599
+ });
13600
+ }
13601
+ return map;
13602
+ }
13603
+ /**
13604
+ * Index local migration SQL by version. Canonical `cloudbase/migrations/` wins over legacy `migrations/`.
13605
+ */
13606
+ function listLocalMigrationFilesByVersion() {
13607
+ const projectRoot = getMigrationProjectRoot();
13608
+ const legacy = listLocalMigrationFilesInDir(path.join(projectRoot, LOCAL_MIGRATIONS_DIR_LEGACY), LOCAL_MIGRATIONS_DIR_LEGACY);
13609
+ const canonical = listLocalMigrationFilesInDir(path.join(projectRoot, LOCAL_MIGRATIONS_DIR), LOCAL_MIGRATIONS_DIR);
13610
+ const merged = new Map(legacy);
13611
+ for (const [version, entry] of canonical) {
13612
+ merged.set(version, entry);
13613
+ }
13614
+ return merged;
13615
+ }
13616
+ function remoteHistoryCoveredByLocalFiles(summaries, localByVersion, pendingVersion) {
13617
+ for (const summary of summaries) {
13618
+ if (summary.Version === pendingVersion) {
13619
+ // Pending SQL comes from the tool args / ensureLocal write.
13620
+ continue;
13621
+ }
13622
+ const local = localByVersion.get(summary.Version);
13623
+ if (!local || !local.query.trim()) {
13624
+ return false;
13625
+ }
13626
+ }
13627
+ return true;
13628
+ }
13629
+ /**
13630
+ * Build Push/Preview Migrations = full candidate set (server skips already-applied).
13631
+ *
13632
+ * Prefer CLI parity when the workspace already has SQL for every remote version:
13633
+ * read local files (no per-version DescribePGUserMigration). Otherwise hydrate
13634
+ * remote Queries via List + N×Describe (agent/remote-first when the local tree is incomplete).
13635
+ */
13636
+ async function buildMigrationsPayloadWithRemoteHistory(context, pending, cloudBaseOptions) {
13637
+ const { summaries, latestVersion } = await listAllRemoteMigrationSummaries(context, cloudBaseOptions);
13638
+ const localByVersion = listLocalMigrationFilesByVersion();
13639
+ if (remoteHistoryCoveredByLocalFiles(summaries, localByVersion, pending.Version)) {
13640
+ const migrations = [];
13641
+ for (const summary of summaries) {
13642
+ if (summary.Version === pending.Version) {
13643
+ continue;
13644
+ }
13645
+ const local = localByVersion.get(summary.Version);
13646
+ migrations.push({
13647
+ Version: local.version,
13648
+ Name: local.name || summary.Name,
13649
+ Query: local.query,
13650
+ });
13651
+ }
13652
+ migrations.push(pending);
13653
+ return {
13654
+ migrations,
13655
+ remoteCount: summaries.length,
13656
+ latestVersion,
13657
+ payloadSource: "local",
13658
+ };
13659
+ }
13660
+ const migrations = [];
13661
+ for (const summary of summaries) {
13662
+ if (summary.Version === pending.Version) {
13663
+ // Caller supplies the authoritative SQL for this version.
13664
+ continue;
13665
+ }
13666
+ const detail = await callPgMigrationApi(context, "DescribePGUserMigration", { MigrationVersion: summary.Version }, cloudBaseOptions);
13667
+ const query = typeof detail.Query === "string" ? detail.Query : "";
13668
+ if (!query.trim()) {
13669
+ throw new Error(`DescribePGUserMigration returned empty Query for version=${summary.Version} (${summary.Name}). ` +
13670
+ "Cannot hydrate remote history for PushPGUserMigrations.");
13671
+ }
13672
+ migrations.push({
13673
+ Version: typeof detail.Version === "string" && detail.Version.trim() ? detail.Version.trim() : summary.Version,
13674
+ Name: typeof detail.Name === "string" && detail.Name.trim() ? detail.Name.trim() : summary.Name,
13675
+ Query: query,
13676
+ });
13677
+ }
13678
+ migrations.push(pending);
13679
+ return {
13680
+ migrations,
13681
+ remoteCount: summaries.length,
13682
+ latestVersion,
13683
+ payloadSource: "hydrate",
13684
+ };
13685
+ }
13686
+ function formatMigrationsPayloadSourceLabel(source, remoteCount) {
13687
+ if (remoteCount <= 0) {
13688
+ return "no remote history";
13689
+ }
13690
+ return source === "local"
13691
+ ? `local tree covered ${remoteCount} remote migration(s) (CLI parity, skipped Describe hydrate)`
13692
+ : `hydrated ${remoteCount} remote migration(s) via Describe (local tree incomplete)`;
13693
+ }
13694
+ async function waitPgMigrationTask(context, taskId, cloudBaseOptions, maxWaitMs = MIGRATION_TASK_DEFAULT_WAIT_MS) {
13695
+ const deadline = Date.now() + maxWaitMs;
13696
+ let last = { TaskId: taskId };
13697
+ while (Date.now() <= deadline) {
13698
+ last = (await callPgMigrationApi(context, "DescribeTaskResult", { TaskId: taskId }, cloudBaseOptions));
13699
+ const status = String(last.Status || "").toLowerCase();
13700
+ if (status === "succeed" || status === "failed") {
13701
+ return last;
13702
+ }
13703
+ await sleep(MIGRATION_TASK_POLL_INTERVAL_MS);
13704
+ }
13705
+ const error = new Error(`DescribeTaskResult timed out after ${Math.floor(maxWaitMs / 1000)}s ` +
13706
+ `for TaskId=${taskId} (last Status=${last.Status || "-"}, Phase=${last.Phase || "-"}).`);
13707
+ error.lastTask = last;
13708
+ throw error;
13709
+ }
13710
+ function buildMigrationTaskPendingNextActions(version, taskId) {
13711
+ const actions = [];
13712
+ if (taskId) {
13713
+ actions.push(buildNextAction(MANAGE_PG_DATABASE, "describeMigrationTask", "FIRST: poll DescribeTaskResult for Status/Phase/Reason. listMigrations alone cannot explain a Failed task (see issue #857).", { action: "describeMigrationTask", taskId }));
13714
+ }
13715
+ actions.push(buildNextAction(MANAGE_PG_DATABASE, "listMigrations", "Check whether migrationVersion landed in remote history. Do NOT re-push the same version while the task may still be running.", { action: "listMigrations", limit: 20 }), buildNextAction(MANAGE_PG_DATABASE, "migrationDetail", "If listMigrations already shows this version, inspect the applied record. If missing after several minutes and the task is Failed/terminal, inspect Conflicts/SQL before considering a NEW migrationVersion.", { action: "migrationDetail", migrationVersion: version }));
13716
+ return actions;
13717
+ }
13180
13718
  const MIGRATION_VERSION_REQUIRED_MESSAGE = "migrationVersion is required (14-digit UTC timestamp YYYYMMDDHHMMSS). " +
13181
- "Decide the version first, write local file migrations/<version>_<migrationName>.sql, " +
13719
+ "Decide the version first, write local file cloudbase/migrations/<version>_<migrationName>.sql, " +
13182
13720
  "then call planMigration/applyMigration with the same migrationVersion and migrationName.";
13183
13721
  function requireExplicitMigrationVersion(args, actionLabel) {
13184
13722
  const version = args.migrationVersion?.trim();
@@ -13515,6 +14053,30 @@ function createDefaultDependencies(server) {
13515
14053
  createClient: async (context) => createManagerPgClient(context, server?.cloudBaseOptions),
13516
14054
  };
13517
14055
  }
14056
+ function isPgMigrationApiDatabase(value) {
14057
+ return Boolean(value &&
14058
+ typeof value === "object" &&
14059
+ "previewPGUserMigrations" in value &&
14060
+ typeof value.previewPGUserMigrations === "function" &&
14061
+ "pushPGUserMigrations" in value &&
14062
+ typeof value.pushPGUserMigrations === "function" &&
14063
+ "listPGUserMigrations" in value &&
14064
+ typeof value.listPGUserMigrations === "function" &&
14065
+ "describeTaskResult" in value &&
14066
+ typeof value.describeTaskResult === "function");
14067
+ }
14068
+ /**
14069
+ * Actions without a native wrapper in manager-node (e.g. RollbackPGUserMigrations)
14070
+ * are intentionally absent and keep using the commonService fallback.
14071
+ */
14072
+ const PgMigrationNativeActionMethods = {
14073
+ PreviewPGUserMigrations: "previewPGUserMigrations",
14074
+ PushPGUserMigrations: "pushPGUserMigrations",
14075
+ RepairPGUserMigrationHistory: "repairPGUserMigrationHistory",
14076
+ ListPGUserMigrations: "listPGUserMigrations",
14077
+ DescribePGUserMigration: "describePGUserMigration",
14078
+ DescribeTaskResult: "describeTaskResult",
14079
+ };
13518
14080
  function isExecutePGSqlDatabase(value) {
13519
14081
  return Boolean(value &&
13520
14082
  typeof value === "object" &&
@@ -13555,13 +14117,22 @@ async function executeManagerPGSql(context, sql, cloudBaseOptions) {
13555
14117
  }
13556
14118
  throw new Error("Current @cloudbase/manager-node runtime does not expose database.executePGSql or commonService fallback. Upgrade to @cloudbase/manager-node >= 5.4.0.");
13557
14119
  }
13558
- /** Call a CloudBase PG migration API via commonService fallback */
14120
+ /**
14121
+ * Call a CloudBase PG migration API, preferring native manager-node methods
14122
+ * (>= 5.6.5) and falling back to the commonService channel for older runtimes
14123
+ * and actions without a native wrapper (RollbackPGUserMigrations).
14124
+ */
13559
14125
  async function callPgMigrationApi(context, action, params, cloudBaseOptions) {
13560
14126
  const manager = await (0, cloudbase_manager_js_1.getCloudBaseManager)({
13561
14127
  cloudBaseOptions: cloudBaseOptions
13562
14128
  ? { ...cloudBaseOptions, envId: context.envId }
13563
14129
  : { envId: context.envId },
13564
14130
  });
14131
+ const nativeMethod = PgMigrationNativeActionMethods[action];
14132
+ if (nativeMethod && isPgMigrationApiDatabase(manager.database)) {
14133
+ const invoke = manager.database[nativeMethod];
14134
+ return invoke.call(manager.database, { EnvId: context.envId, ...params });
14135
+ }
13565
14136
  if (!isCloudBaseWithCommonService(manager)) {
13566
14137
  throw new Error("Current @cloudbase/manager-node runtime does not support migration APIs. Upgrade to @cloudbase/manager-node >= 5.4.0.");
13567
14138
  }
@@ -13802,11 +14373,11 @@ async function handleExecuteSql(args, context, deps) {
13802
14373
  success: false,
13803
14374
  errorCode: "DDL_USE_APPLY_MIGRATION",
13804
14375
  message: "Schema DDL (CREATE/ALTER/DROP/TRUNCATE/...) must use applyMigration with an explicit migrationVersion, " +
13805
- "not execute. Write local migrations/<version>_<name>.sql first, then call applyMigration. " +
14376
+ "not execute. Write local cloudbase/migrations/<version>_<name>.sql first, then call applyMigration. " +
13806
14377
  "Set allowDdlViaExecute=true only for exceptional one-off ops that intentionally bypass migration history.",
13807
14378
  data: {
13808
14379
  classification,
13809
- localFileHintPattern: "migrations/<migrationVersion>_<migrationName>.sql",
14380
+ localFileHintPattern: "cloudbase/migrations/<migrationVersion>_<migrationName>.sql",
13810
14381
  },
13811
14382
  nextActions: [
13812
14383
  buildNextAction(MANAGE_PG_DATABASE, "planMigration", "Preview the schema change as a versioned migration (provide migrationVersion + migrationName).", {
@@ -13915,7 +14486,7 @@ async function handleDryRun(args) {
13915
14486
  wouldExecute: false,
13916
14487
  sqlPreview: args.sql.trim().slice(0, 500),
13917
14488
  preferredAction: "applyMigration",
13918
- localFileHintPattern: "migrations/<migrationVersion>_<migrationName>.sql",
14489
+ localFileHintPattern: "cloudbase/migrations/<migrationVersion>_<migrationName>.sql",
13919
14490
  },
13920
14491
  message: "SQL dry run completed. Schema DDL should use applyMigration with an explicit migrationVersion, not execute.",
13921
14492
  nextActions: [
@@ -13963,35 +14534,50 @@ async function handlePlanMigration(args, context, deps, cloudBaseOptions) {
13963
14534
  }
13964
14535
  const version = versionOrError;
13965
14536
  const localFileHint = buildLocalMigrationFileHint(version, args.migrationName);
13966
- const migration = {
14537
+ const pending = {
13967
14538
  Version: version,
13968
14539
  Name: args.migrationName,
13969
14540
  Query: args.sql,
13970
14541
  };
13971
14542
  if (args.rollbackSql?.trim()) {
13972
- migration.Rollback = args.rollbackSql;
14543
+ pending.Rollback = args.rollbackSql;
13973
14544
  }
13974
14545
  try {
14546
+ const hydrated = await buildMigrationsPayloadWithRemoteHistory(context, pending, cloudBaseOptions);
14547
+ const includeAll = Boolean(args.includeAll);
13975
14548
  const result = await callPgMigrationApi(context, "PreviewPGUserMigrations", {
13976
- Migrations: [migration],
14549
+ Migrations: hydrated.migrations,
14550
+ IncludeAll: includeAll,
13977
14551
  }, cloudBaseOptions);
14552
+ const executable = result.Executable === true;
14553
+ const payloadSourceLabel = formatMigrationsPayloadSourceLabel(hydrated.payloadSource, hydrated.remoteCount);
13978
14554
  return buildPgToolResult({
13979
14555
  success: true,
13980
14556
  data: {
13981
14557
  migrationVersion: version,
13982
14558
  migrationName: args.migrationName,
13983
14559
  localFileHint,
14560
+ hydratedRemoteCount: hydrated.remoteCount,
14561
+ latestRemoteVersion: hydrated.latestVersion || null,
14562
+ payloadSource: hydrated.payloadSource,
14563
+ includeAll,
14564
+ executable,
13984
14565
  apiResult: result,
13985
14566
  },
13986
- message: `Migration plan generated via PreviewPGUserMigrations. Reuse migrationVersion=${version} on applyMigration. Ensure local file ${localFileHint} exists and matches.`,
14567
+ message: executable
14568
+ ? `Migration plan generated via PreviewPGUserMigrations (${payloadSourceLabel}${includeAll ? ", includeAll=true" : ""}). Reuse migrationVersion=${version} on applyMigration. Ensure local file ${localFileHint} exists and matches.`
14569
+ : `Migration plan is NOT executable (PreviewPGUserMigrations.Executable=false after ${payloadSourceLabel}${includeAll ? ", includeAll=true" : ""}). Inspect apiResult.Conflicts before applyMigration. Common causes: version older than latest remote (${hydrated.latestVersion || "unknown"}) — retry with includeAll=true (CLI --include-all) if intentionally out-of-order, or pick a newer version; checksum mismatch.`,
13987
14570
  nextActions: [
13988
- buildNextAction(MANAGE_PG_DATABASE, "applyMigration", "Review the plan above. If it looks correct, call applyMigration with the same migrationVersion.", {
14571
+ buildNextAction(MANAGE_PG_DATABASE, "applyMigration", executable
14572
+ ? "Review the plan above. If it looks correct, call applyMigration with the same migrationVersion."
14573
+ : "Resolve Conflicts (often pick a migrationVersion newer than LatestVersion, or set includeAll=true for intentional out-of-order), then retry planMigration/applyMigration.", {
13989
14574
  action: "applyMigration",
13990
14575
  migrationName: args.migrationName,
13991
14576
  migrationVersion: version,
13992
14577
  sql: args.sql,
13993
14578
  confirm: true,
13994
14579
  ...(args.rollbackSql ? { rollbackSql: args.rollbackSql } : {}),
14580
+ ...(includeAll ? { includeAll: true } : {}),
13995
14581
  }),
13996
14582
  ],
13997
14583
  });
@@ -14032,26 +14618,181 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
14032
14618
  }
14033
14619
  const version = versionOrError;
14034
14620
  const localFileHint = buildLocalMigrationFileHint(version, args.migrationName);
14035
- const migration = {
14621
+ // Prefer write-to-workspace (or fail closed) so remote history cannot advance without a Git truth source.
14622
+ const localFile = ensureLocalMigrationSqlFile(version, args.migrationName, args.sql);
14623
+ if (!localFile.ok) {
14624
+ return buildPgToolResult({
14625
+ success: false,
14626
+ errorCode: localFile.errorCode,
14627
+ data: {
14628
+ migrationVersion: version,
14629
+ migrationName: args.migrationName,
14630
+ localFileHint,
14631
+ localFilePath: localFile.relativePath ?? localFileHint,
14632
+ localFileAbsolutePath: localFile.absolutePath ?? null,
14633
+ verified: false,
14634
+ },
14635
+ message: localFile.message,
14636
+ nextActions: [
14637
+ buildNextAction(MANAGE_PG_DATABASE, "planMigration", "After aligning the local SQL file with the sql argument (or choosing a new migrationVersion), preview again.", {
14638
+ action: "planMigration",
14639
+ migrationName: args.migrationName,
14640
+ migrationVersion: version,
14641
+ sql: args.sql,
14642
+ ...(args.rollbackSql ? { rollbackSql: args.rollbackSql } : {}),
14643
+ ...(args.includeAll ? { includeAll: true } : {}),
14644
+ }),
14645
+ ],
14646
+ });
14647
+ }
14648
+ const pending = {
14036
14649
  Version: version,
14037
14650
  Name: args.migrationName,
14038
14651
  Query: args.sql,
14039
14652
  };
14040
14653
  if (args.rollbackSql?.trim()) {
14041
- migration.Rollback = args.rollbackSql;
14042
- }
14043
- const params = { Migrations: [migration] };
14044
- if (args.lockTimeoutMs !== undefined) {
14045
- params.LockTimeoutMs = args.lockTimeoutMs;
14046
- }
14047
- if (args.statementTimeoutMs !== undefined) {
14048
- params.StatementTimeoutMs = args.statementTimeoutMs;
14654
+ pending.Rollback = args.rollbackSql;
14049
14655
  }
14656
+ const includeAll = Boolean(args.includeAll);
14050
14657
  try {
14658
+ // Pushing only the pending migration while remote history exists makes the plan
14659
+ // non-executable (remote_history_not_found_locally). Prefer local tree when complete
14660
+ // (CLI parity); otherwise hydrate remote Queries (List + N×Describe).
14661
+ const hydrated = await buildMigrationsPayloadWithRemoteHistory(context, pending, cloudBaseOptions);
14662
+ const payloadSourceLabel = formatMigrationsPayloadSourceLabel(hydrated.payloadSource, hydrated.remoteCount);
14663
+ const preview = await callPgMigrationApi(context, "PreviewPGUserMigrations", { Migrations: hydrated.migrations, IncludeAll: includeAll }, cloudBaseOptions);
14664
+ if (preview.Executable !== true) {
14665
+ return buildPgToolResult({
14666
+ success: false,
14667
+ errorCode: "MIGRATION_NOT_EXECUTABLE",
14668
+ data: {
14669
+ migrationVersion: version,
14670
+ migrationName: args.migrationName,
14671
+ localFileHint,
14672
+ hydratedRemoteCount: hydrated.remoteCount,
14673
+ latestRemoteVersion: hydrated.latestVersion || null,
14674
+ payloadSource: hydrated.payloadSource,
14675
+ includeAll,
14676
+ previewResult: preview,
14677
+ verified: false,
14678
+ },
14679
+ message: `PreviewPGUserMigrations reported Executable=false for migrationVersion=${version} ` +
14680
+ `(latest remote=${hydrated.latestVersion || "unknown"}, ${payloadSourceLabel}, includeAll=${includeAll}). ` +
14681
+ "Push was NOT submitted. Inspect previewResult.Conflicts — common reasons: " +
14682
+ "local_migration_before_latest_remote (pick a newer 14-digit version, or set includeAll=true / CLI --include-all for intentional out-of-order) or checksum_mismatch. " +
14683
+ "If the schema change is urgent you may fall back to action=execute with allowDdlViaExecute=true, but that bypasses migration history.",
14684
+ nextActions: [
14685
+ buildNextAction(MANAGE_PG_DATABASE, "listMigrations", "Check LatestVersion and pick a migrationVersion strictly newer than it.", { action: "listMigrations", limit: 20 }),
14686
+ buildNextAction(MANAGE_PG_DATABASE, "planMigration", "Re-run planMigration after adjusting migrationVersion/SQL (or includeAll=true for out-of-order).", {
14687
+ action: "planMigration",
14688
+ migrationName: args.migrationName,
14689
+ migrationVersion: version,
14690
+ sql: args.sql,
14691
+ ...(args.rollbackSql ? { rollbackSql: args.rollbackSql } : {}),
14692
+ ...(includeAll ? { includeAll: true } : {}),
14693
+ }),
14694
+ ],
14695
+ });
14696
+ }
14697
+ const params = {
14698
+ Migrations: hydrated.migrations,
14699
+ IncludeAll: includeAll,
14700
+ };
14701
+ if (args.lockTimeoutMs !== undefined) {
14702
+ params.LockTimeoutMs = args.lockTimeoutMs;
14703
+ }
14704
+ if (args.statementTimeoutMs !== undefined) {
14705
+ params.StatementTimeoutMs = args.statementTimeoutMs;
14706
+ }
14051
14707
  const result = await callPgMigrationApi(context, "PushPGUserMigrations", params, cloudBaseOptions);
14052
- // PushPGUserMigrations may return a TaskId without the migration having taken
14053
- // effect. Confirm against the remote history before reporting success, so callers
14054
- // never get a false-positive success for a silently dropped migration.
14708
+ const taskId = typeof result.TaskId === "string" ? result.TaskId.trim() : "";
14709
+ const pollTimeoutMs = resolveMigrationTaskPollTimeoutMs(args.taskPollTimeoutMs);
14710
+ const shouldWaitForTask = args.waitForTask !== false;
14711
+ let taskResult = null;
14712
+ if (taskId && !shouldWaitForTask) {
14713
+ return buildPgToolResult({
14714
+ success: false,
14715
+ errorCode: "MIGRATION_TASK_PENDING",
14716
+ data: {
14717
+ migrationVersion: version,
14718
+ migrationName: args.migrationName,
14719
+ localFileHint,
14720
+ hydratedRemoteCount: hydrated.remoteCount,
14721
+ payloadSource: hydrated.payloadSource,
14722
+ apiResult: result,
14723
+ taskResult: { TaskId: taskId },
14724
+ taskPollTimeoutMs: pollTimeoutMs,
14725
+ waitForTask: false,
14726
+ verified: null,
14727
+ },
14728
+ message: `PushPGUserMigrations accepted TaskId=${taskId} (waitForTask=false). ` +
14729
+ "TaskId means accepted, not applied. Call action=describeMigrationTask FIRST for Status/Phase/Reason, " +
14730
+ `then listMigrations to see whether migrationVersion=${version} landed. ` +
14731
+ "Do NOT re-push the same version while the task may still be running.",
14732
+ nextActions: buildMigrationTaskPendingNextActions(version, taskId),
14733
+ });
14734
+ }
14735
+ if (taskId) {
14736
+ try {
14737
+ taskResult = await waitPgMigrationTask(context, taskId, cloudBaseOptions, pollTimeoutMs);
14738
+ }
14739
+ catch (error) {
14740
+ const lastTask = error.lastTask;
14741
+ return buildPgToolResult({
14742
+ success: false,
14743
+ errorCode: "MIGRATION_TASK_TIMEOUT",
14744
+ data: {
14745
+ migrationVersion: version,
14746
+ migrationName: args.migrationName,
14747
+ localFileHint,
14748
+ hydratedRemoteCount: hydrated.remoteCount,
14749
+ apiResult: result,
14750
+ taskResult: lastTask ?? { TaskId: taskId },
14751
+ taskPollTimeoutMs: pollTimeoutMs,
14752
+ verified: null,
14753
+ },
14754
+ message: `PushPGUserMigrations returned TaskId=${taskId}, but waiting for DescribeTaskResult timed out ` +
14755
+ `after ${Math.floor(pollTimeoutMs / 1000)}s: ${error instanceof Error ? error.message : String(error)}. ` +
14756
+ "The migration may STILL be running in the background (large DDL / lock waits). " +
14757
+ "Call action=describeMigrationTask FIRST (Status/Phase/Reason), then listMigrations. " +
14758
+ "Do NOT re-push the same migrationVersion, and do NOT fall back to execute until the task is terminal " +
14759
+ "and listMigrations confirms the version is missing.",
14760
+ nextActions: buildMigrationTaskPendingNextActions(version, taskId),
14761
+ });
14762
+ }
14763
+ const status = String(taskResult.Status || "").toLowerCase();
14764
+ if (status === "failed") {
14765
+ return buildPgToolResult({
14766
+ success: false,
14767
+ errorCode: "MIGRATION_TASK_FAILED",
14768
+ data: {
14769
+ migrationVersion: version,
14770
+ migrationName: args.migrationName,
14771
+ localFileHint,
14772
+ hydratedRemoteCount: hydrated.remoteCount,
14773
+ apiResult: result,
14774
+ taskResult: taskResult,
14775
+ verified: false,
14776
+ },
14777
+ message: `PushPGUserMigrations TaskId=${taskId} failed at phase=${taskResult.Phase || "-"}: ` +
14778
+ `${taskResult.Reason || "unknown reason"}. ` +
14779
+ "The migration did NOT take effect. Fix Conflicts/SQL (or use a newer migrationVersion), then retry applyMigration. " +
14780
+ "Urgent bypass: action=execute with allowDdlViaExecute=true (skips migration history).",
14781
+ nextActions: [
14782
+ buildNextAction(MANAGE_PG_DATABASE, "planMigration", "Preview again to inspect Conflicts before retrying apply.", {
14783
+ action: "planMigration",
14784
+ migrationName: args.migrationName,
14785
+ migrationVersion: version,
14786
+ sql: args.sql,
14787
+ ...(args.rollbackSql ? { rollbackSql: args.rollbackSql } : {}),
14788
+ ...(includeAll ? { includeAll: true } : {}),
14789
+ }),
14790
+ buildNextAction(MANAGE_PG_DATABASE, "listMigrations", "Confirm the version is still absent from remote history.", { action: "listMigrations", limit: 20 }),
14791
+ ],
14792
+ });
14793
+ }
14794
+ }
14795
+ // Task Succeed (or sync response without TaskId): confirm history records the version.
14055
14796
  const verification = await verifyMigrationApplied(context, version, cloudBaseOptions);
14056
14797
  if (verification.applied === false) {
14057
14798
  return buildPgToolResult({
@@ -14061,10 +14802,13 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
14061
14802
  migrationVersion: version,
14062
14803
  migrationName: args.migrationName,
14063
14804
  localFileHint,
14805
+ hydratedRemoteCount: hydrated.remoteCount,
14806
+ includeAll,
14064
14807
  apiResult: result,
14808
+ taskResult: taskResult,
14065
14809
  verified: false,
14066
14810
  },
14067
- message: `PushPGUserMigrations returned without error, but migrationVersion=${version} is not present in the remote migration history, ` +
14811
+ message: `PushPGUserMigrations completed (TaskId=${taskId || "none"}), but migrationVersion=${version} is not present in the remote migration history, ` +
14068
14812
  "so the migration did NOT take effect. Do not assume the schema change is applied. " +
14069
14813
  "Re-check the SQL and migrationVersion, inspect the migration with action=migrationDetail, and retry action=applyMigration. " +
14070
14814
  "If the schema change is urgent you may fall back to action=execute with allowDdlViaExecute=true, but that bypasses migration history.",
@@ -14082,7 +14826,9 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
14082
14826
  migrationVersion: version,
14083
14827
  migrationName: args.migrationName,
14084
14828
  localFileHint,
14829
+ hydratedRemoteCount: hydrated.remoteCount,
14085
14830
  apiResult: result,
14831
+ taskResult: taskResult,
14086
14832
  verified: null,
14087
14833
  },
14088
14834
  message: `PushPGUserMigrations was submitted, but verifying migrationVersion=${version} against the remote history failed: ${verification.error}. ` +
@@ -14098,10 +14844,18 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
14098
14844
  migrationVersion: version,
14099
14845
  migrationName: args.migrationName,
14100
14846
  localFileHint,
14847
+ localFilePath: localFile.relativePath,
14848
+ localFileAbsolutePath: localFile.absolutePath,
14849
+ localFileAction: localFile.action,
14850
+ hydratedRemoteCount: hydrated.remoteCount,
14851
+ payloadSource: hydrated.payloadSource,
14852
+ includeAll,
14101
14853
  apiResult: result,
14854
+ taskResult: taskResult,
14102
14855
  verified: true,
14103
14856
  },
14104
- message: `Migrations applied via PushPGUserMigrations and verified present in the remote migration history. Ensure local file ${localFileHint} exists and matches.`,
14857
+ message: `Migrations applied via PushPGUserMigrations (${payloadSourceLabel}${includeAll ? ", includeAll=true" : ""}), task ${taskId ? `TaskId=${taskId} ` : ""}verified present in the remote migration history. ` +
14858
+ `Local SQL ${localFile.action === "created" ? "written" : "matched"} at ${localFile.relativePath}.`,
14105
14859
  });
14106
14860
  }
14107
14861
  catch (error) {
@@ -14162,6 +14916,156 @@ async function handleMigrationDetail(args, context, deps, cloudBaseOptions) {
14162
14916
  });
14163
14917
  }
14164
14918
  }
14919
+ /**
14920
+ * One-shot DescribeTaskResult for a PushPGUserMigrations TaskId.
14921
+ * Complements listMigrations: history shows whether a version landed; task result shows
14922
+ * Status/Phase/Reason while the job is running or after it Failed (issue #857 evidence path).
14923
+ */
14924
+ async function handleDescribeMigrationTask(args, context, _deps, cloudBaseOptions) {
14925
+ const taskId = args.taskId?.trim();
14926
+ if (!taskId) {
14927
+ return buildPgToolResult({
14928
+ success: false,
14929
+ errorCode: "TASK_ID_REQUIRED",
14930
+ message: "Provide taskId when action=describeMigrationTask " +
14931
+ "(from applyMigration Push / MIGRATION_TASK_PENDING / MIGRATION_TASK_TIMEOUT data.taskResult.TaskId).",
14932
+ });
14933
+ }
14934
+ try {
14935
+ const taskResult = (await callPgMigrationApi(context, "DescribeTaskResult", { TaskId: taskId }, cloudBaseOptions));
14936
+ const status = String(taskResult.Status || "").toLowerCase();
14937
+ const versionHint = args.migrationVersion?.trim();
14938
+ const nextActions = [];
14939
+ if (status === "succeed") {
14940
+ nextActions.push(buildNextAction(MANAGE_PG_DATABASE, "listMigrations", "Task Succeed — confirm migrationVersion is present in remote history before dependent SQL.", { action: "listMigrations", limit: 20 }));
14941
+ if (versionHint) {
14942
+ nextActions.push(buildNextAction(MANAGE_PG_DATABASE, "migrationDetail", "Inspect the applied migration record.", { action: "migrationDetail", migrationVersion: versionHint }));
14943
+ }
14944
+ }
14945
+ else if (status === "failed") {
14946
+ nextActions.push(buildNextAction(MANAGE_PG_DATABASE, "listMigrations", "Confirm the version is absent from history, then fix Conflicts/SQL and retry with a NEW migrationVersion (do not re-push the failed version blindly).", { action: "listMigrations", limit: 20 }));
14947
+ }
14948
+ else {
14949
+ nextActions.push(buildNextAction(MANAGE_PG_DATABASE, "describeMigrationTask", "Task still running — poll again shortly. Do NOT re-push the same migrationVersion.", { action: "describeMigrationTask", taskId }), buildNextAction(MANAGE_PG_DATABASE, "listMigrations", "Optionally check whether the version already landed while Status is still non-terminal.", { action: "listMigrations", limit: 20 }));
14950
+ }
14951
+ const statusLabel = taskResult.Status || "unknown";
14952
+ return buildPgToolResult({
14953
+ success: true,
14954
+ data: {
14955
+ taskId,
14956
+ taskResult: taskResult,
14957
+ terminal: status === "succeed" || status === "failed",
14958
+ ...(versionHint ? { migrationVersion: versionHint } : {}),
14959
+ },
14960
+ message: status === "succeed"
14961
+ ? `DescribeTaskResult TaskId=${taskId} Status=Succeed (Phase=${taskResult.Phase || "-"}). Verify with listMigrations before treating schema as applied.`
14962
+ : status === "failed"
14963
+ ? `DescribeTaskResult TaskId=${taskId} Status=Failed at phase=${taskResult.Phase || "-"}: ${taskResult.Reason || "unknown reason"}. Do NOT re-push the same migrationVersion until Conflicts/SQL are fixed.`
14964
+ : `DescribeTaskResult TaskId=${taskId} Status=${statusLabel} Phase=${taskResult.Phase || "-"}. Task is not terminal yet — poll again; do not re-push.`,
14965
+ nextActions,
14966
+ });
14967
+ }
14968
+ catch (error) {
14969
+ return buildPgToolResult({
14970
+ success: false,
14971
+ errorCode: "MIGRATION_API_ERROR",
14972
+ message: `DescribeTaskResult failed for TaskId=${taskId}: ${error instanceof Error ? error.message : String(error)}`,
14973
+ });
14974
+ }
14975
+ }
14976
+ /**
14977
+ * Pull remote migration history SQL into cloudbase/migrations/ (CLI `tcb db pg migration fetch`).
14978
+ * Optional migrationVersion fetches one record; omit for full history. force overwrites existing files.
14979
+ */
14980
+ async function handleFetchMigration(args, context, _deps, cloudBaseOptions) {
14981
+ const force = args.force === true;
14982
+ const singleVersion = args.migrationVersion?.trim();
14983
+ try {
14984
+ const remoteItems = [];
14985
+ if (singleVersion) {
14986
+ const detail = await callPgMigrationApi(context, "DescribePGUserMigration", { MigrationVersion: singleVersion }, cloudBaseOptions);
14987
+ const version = typeof detail.Version === "string" && detail.Version.trim()
14988
+ ? detail.Version.trim()
14989
+ : singleVersion;
14990
+ const name = typeof detail.Name === "string" && detail.Name.trim()
14991
+ ? detail.Name.trim()
14992
+ : "";
14993
+ const query = typeof detail.Query === "string" ? detail.Query : "";
14994
+ remoteItems.push({ Version: version, Name: name || version, Query: query });
14995
+ }
14996
+ else {
14997
+ const { summaries } = await listAllRemoteMigrationSummaries(context, cloudBaseOptions);
14998
+ for (const summary of summaries) {
14999
+ const detail = await callPgMigrationApi(context, "DescribePGUserMigration", { MigrationVersion: summary.Version }, cloudBaseOptions);
15000
+ const version = typeof detail.Version === "string" && detail.Version.trim()
15001
+ ? detail.Version.trim()
15002
+ : summary.Version;
15003
+ const name = typeof detail.Name === "string" && detail.Name.trim()
15004
+ ? detail.Name.trim()
15005
+ : summary.Name;
15006
+ const query = typeof detail.Query === "string" ? detail.Query : "";
15007
+ remoteItems.push({ Version: version, Name: name, Query: query });
15008
+ }
15009
+ }
15010
+ const sync = syncFetchedMigrationsToLocalFiles(remoteItems, force);
15011
+ if (!sync.ok) {
15012
+ return buildPgToolResult({
15013
+ success: false,
15014
+ errorCode: sync.errorCode,
15015
+ message: sync.message,
15016
+ data: {
15017
+ migrationsDir: LOCAL_MIGRATIONS_DIR,
15018
+ force,
15019
+ localFilePath: sync.relativePath,
15020
+ },
15021
+ });
15022
+ }
15023
+ const written = sync.files.filter((f) => f.action === "written");
15024
+ const skipped = sync.files.filter((f) => f.action === "skipped");
15025
+ return buildPgToolResult({
15026
+ success: true,
15027
+ data: {
15028
+ migrationsDir: LOCAL_MIGRATIONS_DIR,
15029
+ force,
15030
+ migrationVersion: singleVersion || null,
15031
+ total: remoteItems.length,
15032
+ writtenCount: written.length,
15033
+ skippedCount: skipped.length,
15034
+ written: written.map((f) => f.relativePath),
15035
+ skipped: skipped.map((f) => f.relativePath),
15036
+ files: sync.files.map(({ version, name, relativePath, action }) => ({
15037
+ version,
15038
+ name,
15039
+ relativePath,
15040
+ action,
15041
+ })),
15042
+ },
15043
+ message: remoteItems.length === 0
15044
+ ? "No remote migrations to fetch. Local cloudbase/migrations/ unchanged."
15045
+ : `Fetched ${remoteItems.length} remote migration(s): wrote ${written.length}, skipped ${skipped.length}` +
15046
+ (skipped.length > 0 && !force
15047
+ ? " (existing files left untouched; pass force=true to overwrite, matching CLI --force)."
15048
+ : ".") +
15049
+ ` Directory: ${LOCAL_MIGRATIONS_DIR}/.`,
15050
+ nextActions: skipped.length > 0 && !force
15051
+ ? [
15052
+ buildNextAction(MANAGE_PG_DATABASE, "fetchMigration", "Re-run with force=true to overwrite skipped local files from remote history (checksum realign).", {
15053
+ action: "fetchMigration",
15054
+ force: true,
15055
+ ...(singleVersion ? { migrationVersion: singleVersion } : {}),
15056
+ }),
15057
+ ]
15058
+ : undefined,
15059
+ });
15060
+ }
15061
+ catch (error) {
15062
+ return buildPgToolResult({
15063
+ success: false,
15064
+ errorCode: "MIGRATION_API_ERROR",
15065
+ message: `fetchMigration failed: ${error instanceof Error ? error.message : String(error)}`,
15066
+ });
15067
+ }
15068
+ }
14165
15069
  async function handleRollbackMigration(args, context, deps, cloudBaseOptions) {
14166
15070
  if (args.lastN === undefined || args.lastN < 1 || !Number.isInteger(args.lastN)) {
14167
15071
  return buildPgToolResult({
@@ -14376,11 +15280,11 @@ function registerPGDatabaseTools(server, providedDeps) {
14376
15280
  });
14377
15281
  server.registerTool?.(MANAGE_PG_DATABASE, {
14378
15282
  title: "管理 PostgreSQL 上下文或执行写入 SQL",
14379
- description: "管理 CloudBase PostgreSQL:执行已确认的写入 SQL、SQL 风险预检、迁移管理。建表/ALTER/DROP 等 schema 变更必须使用 applyMigration(显式 migrationVersion + 本地 migrations/ 留档),不要默认用 execute。execute 主要用于 DML 与 GRANT/RLS 等运维 SQL。",
15283
+ description: "管理 CloudBase PostgreSQL:执行已确认的写入 SQL、SQL 风险预检、迁移管理。建表/ALTER/DROP 等 schema 变更必须使用 applyMigration(显式 migrationVersion;成功前自动写入或校验本地 cloudbase/migrations/<version>_<name>.sql,与 CLI tcb db pg migration 一致),不要默认用 execute。execute 主要用于 DML 与 GRANT/RLS 等运维 SQL。",
14380
15284
  inputSchema: {
14381
15285
  action: zod_1.z
14382
15286
  .enum(MANAGE_ACTIONS)
14383
- .describe("操作类型:execute=执行已确认的写入 SQL(DML/GRANT/RLS;schema DDL 默认拒绝,需 allowDdlViaExecute=true);dryRun=只分析 SQL 风险不执行;planMigration=预览迁移计划(需 migrationName + migrationVersion + sql);applyMigration=应用迁移,建表/改 schema 首选(需 migrationName + migrationVersion + sql + confirm=true;成功返回前会自动校验该 migrationVersion 已落入远端迁移历史,未落库时返回 success=false 且 errorCode=MIGRATION_NOT_APPLIED);listMigrations=查询已应用的 Migration 列表(可传 limit/offset 分页);migrationDetail=查看单条 Migration 详情(需 migrationVersion);rollbackMigration=回滚最近 N 条 Migration(需 lastN + confirm=true);repairMigration=修复 Migration 历史记录(需 migrationVersion + migrationName + repairStatus + repairReason)"),
15287
+ .describe("操作类型:execute=执行已确认的写入 SQL(DML/GRANT/RLS;schema DDL 默认拒绝,需 allowDdlViaExecute=true);dryRun=只分析 SQL 风险不执行;planMigration=预览迁移计划(需 migrationName + migrationVersion + sql;可选 includeAll=true 允许乱序,对齐 CLI --include-all);applyMigration=应用迁移,建表/改 schema 首选(需 migrationName + migrationVersion + sql + confirm=true;可选 includeAll;本地 SQL 缺失则自动写入 cloudbase/migrations/,内容不一致则 LOCAL_MIGRATION_FILE_MISMATCH fail-closed;成功返回前会轮询 DescribeTaskResult(默认最长 10 分钟,可用 taskPollTimeoutMs / waitForTask 调整)并校验 migrationVersion 已落入远端历史;超时返回 MIGRATION_TASK_TIMEOUT,必须先 describeMigrationTask 再 listMigrations,禁止立刻重推同 version;未落库时返回 success=false 且 errorCode=MIGRATION_NOT_APPLIED);listMigrations=查询已应用的 Migration 列表(可传 limit/offset 分页);migrationDetail=查看单条 Migration 详情(需 migrationVersion);describeMigrationTask=按 TaskId 查询 Push 异步任务状态(DescribeTaskResult:Status/Phase/Reason;需 taskId;用于 waitForTask=false / MIGRATION_TASK_TIMEOUT / 失败诊断,listMigrations 看不到 Reason);fetchMigration=从远端 history 拉取 SQL 写入本地 cloudbase/migrations/(对齐 CLI tcb db pg migration fetch;可选 migrationVersion 拉单条,省略则全量;force=true 覆盖已存在文件,默认跳过);rollbackMigration=回滚最近 N 条 Migration(需 lastN + confirm=true);repairMigration=修复 Migration 历史记录(需 migrationVersion + migrationName + repairStatus + repairReason)"),
14384
15288
  sql: zod_1.z
14385
15289
  .string()
14386
15290
  .optional()
@@ -14418,7 +15322,7 @@ function registerPGDatabaseTools(server, providedDeps) {
14418
15322
  .string()
14419
15323
  .regex(/^\d{14}$/)
14420
15324
  .optional()
14421
- .describe("14 位时间戳 YYYYMMDDHHMMSS。plan/apply/detail/repair 必填;禁止由服务端静默生成,避免与本地 migrations/<version>_<name>.sql 分叉。"),
15325
+ .describe("14 位时间戳 YYYYMMDDHHMMSS。plan/apply/detail/repair 必填;fetchMigration 可选(传入则只拉该条,省略则拉全量远端 history);禁止由服务端静默生成,避免与本地 cloudbase/migrations/<version>_<name>.sql 分叉。"),
14422
15326
  rollbackSql: zod_1.z
14423
15327
  .string()
14424
15328
  .optional()
@@ -14452,6 +15356,21 @@ function registerPGDatabaseTools(server, providedDeps) {
14452
15356
  .int()
14453
15357
  .optional()
14454
15358
  .describe("apply 可选:单条 SQL 执行最长时间(毫秒),默认 300000。"),
15359
+ taskPollTimeoutMs: zod_1.z
15360
+ .number()
15361
+ .int()
15362
+ .min(MIGRATION_TASK_MIN_WAIT_MS)
15363
+ .max(MIGRATION_TASK_MAX_WAIT_MS)
15364
+ .optional()
15365
+ .describe(`apply 可选:轮询 DescribeTaskResult 的最长等待(毫秒)。默认 ${MIGRATION_TASK_DEFAULT_WAIT_MS}(与 CLI tcb db pg migration up 的 10 分钟对齐)。范围 ${MIGRATION_TASK_MIN_WAIT_MS}-${MIGRATION_TASK_MAX_WAIT_MS}。超时后务必先 describeMigrationTask(taskId) 再 listMigrations,禁止立刻重推同 version。`),
15366
+ waitForTask: zod_1.z
15367
+ .boolean()
15368
+ .optional()
15369
+ .describe("apply 可选,默认 true。设为 false 时 Push 后立即返回 TaskId(errorCode=MIGRATION_TASK_PENDING),由调用方用 describeMigrationTask 轮询任务终态,再用 listMigrations 确认是否落库;适合 MCP host 工具调用超时较短的场景。默认 true 会同步等到任务终态。"),
15370
+ taskId: zod_1.z
15371
+ .string()
15372
+ .optional()
15373
+ .describe("describeMigrationTask 必填:PushPGUserMigrations / applyMigration 返回的 TaskId。用于一次性查询 DescribeTaskResult(Status/Phase/Reason),不轮询等待。"),
14455
15374
  repairStatus: zod_1.z
14456
15375
  .enum(["applied", "reverted"])
14457
15376
  .optional()
@@ -14460,6 +15379,14 @@ function registerPGDatabaseTools(server, providedDeps) {
14460
15379
  .string()
14461
15380
  .optional()
14462
15381
  .describe("repair 必填:修复原因。"),
15382
+ force: zod_1.z
15383
+ .boolean()
15384
+ .optional()
15385
+ .describe("fetchMigration 可选,默认 false。true=覆盖本地已存在的同名 SQL 文件(对齐 CLI tcb db pg migration fetch --force);false=跳过已存在文件。用于从远端 history 重新对齐 Git checksum。"),
15386
+ includeAll: zod_1.z
15387
+ .boolean()
15388
+ .optional()
15389
+ .describe("planMigration / applyMigration 可选,默认 false。true=允许 out-of-order(version 小于远端 LatestVersion)仍可 Preview/Push,对齐 CLI tcb db pg migration up --include-all;仅在确认要补历史/乱序迁移时使用,日常应选更大的 migrationVersion。"),
14463
15390
  allowDdlViaExecute: zod_1.z
14464
15391
  .boolean()
14465
15392
  .optional()
@@ -14507,6 +15434,8 @@ function registerPGDatabaseTools(server, providedDeps) {
14507
15434
  case "applyMigration":
14508
15435
  case "listMigrations":
14509
15436
  case "migrationDetail":
15437
+ case "describeMigrationTask":
15438
+ case "fetchMigration":
14510
15439
  case "rollbackMigration":
14511
15440
  case "repairMigration": {
14512
15441
  switch (args.action) {
@@ -14514,6 +15443,8 @@ function registerPGDatabaseTools(server, providedDeps) {
14514
15443
  case "applyMigration": return handleApplyMigration(args, context, deps, cbOpts);
14515
15444
  case "listMigrations": return handleListMigrations(args, context, deps, cbOpts);
14516
15445
  case "migrationDetail": return handleMigrationDetail(args, context, deps, cbOpts);
15446
+ case "describeMigrationTask": return handleDescribeMigrationTask(args, context, deps, cbOpts);
15447
+ case "fetchMigration": return handleFetchMigration(args, context, deps, cbOpts);
14517
15448
  case "rollbackMigration": return handleRollbackMigration(args, context, deps, cbOpts);
14518
15449
  case "repairMigration": return handleRepairMigration(args, context, deps, cbOpts);
14519
15450
  default: return buildPgToolResult({ success: false, errorCode: "UNSUPPORTED_ACTION", message: `Unsupported action: ${args.action}` });
@@ -14566,7 +15497,7 @@ function buildWriteVerificationHint(resourceId) {
14566
15497
  return `对于 ${resourceId} 这类有后端权限控制的集合,前端调用 .doc(id).update() / .doc(id).remove() 后,不能只看是否没有抛异常。请显式检查返回结果中的 updated / deleted 是否大于 0;如果 result.code、result.message 存在,或 updated / deleted 为 0,要把它当作真实失败并向上抛错。`;
14567
15498
  }
14568
15499
  function buildPermissionPropagationHint(resourceId) {
14569
- return `刚更新完 ${resourceId} 的安全规则时,后端权限可能需要一小段传播时间。若紧接着的真实写操作仍返回 DATABASE_PERMISSION_DENIED,请先等待一小段时间,再用同一登录态重试同一条 .doc(id).update() / .doc(id).remove();不要立刻连续重写规则,也不要在传播窗口里把旧拒绝直接当成规则表达式仍然错误。`;
15500
+ return `刚更新完 ${resourceId} 的安全规则时,后端权限通常在数秒到约 30 秒内生效。若紧接着的真实写操作仍返回 DATABASE_PERMISSION_DENIED,请先间隔数秒用同一登录态重试同一条 .doc(id).update() / .doc(id).remove();不要盲等数分钟,也不要立刻连续重写规则,更不要在短暂传播窗口里把旧拒绝直接当成规则表达式仍然错误。`;
14570
15501
  }
14571
15502
  function buildEnvelope(data, message) {
14572
15503
  return {
@@ -14591,6 +15522,171 @@ function mapResourceType(resourceType) {
14591
15522
  };
14592
15523
  return resourceTypeMap[resourceType];
14593
15524
  }
15525
+ /**
15526
+ * Platform ModifyResourcePermission / DescribeResourcePermission reject PG envs.
15527
+ * CLI migrated `tcb permission` → `tcb policy` (OPA Rego via
15528
+ * permission.modifyEnvAuthzConfig / describeEnvAuthzConfig).
15529
+ */
15530
+ function isPostgresqlPermissionApiUnsupported(error) {
15531
+ const message = error instanceof Error ? error.message : String(error);
15532
+ return /does not support PostgreSQL type environments/i.test(message);
15533
+ }
15534
+ const AUTHZ_USER_REGO_KEY = "authz.user.rego";
15535
+ function looksLikeUserRego(value) {
15536
+ return /^\s*package\s+authz\.user\b/m.test(value);
15537
+ }
15538
+ /** Detect legacy function securityRule shapes that mean "public invoke". */
15539
+ function isPublicFunctionInvokeRule(securityRule) {
15540
+ if (!securityRule || securityRule.trim() === "") {
15541
+ return false;
15542
+ }
15543
+ const trimmed = securityRule.trim();
15544
+ if (trimmed === "true") {
15545
+ return true;
15546
+ }
15547
+ try {
15548
+ const parsed = JSON.parse(trimmed);
15549
+ if (parsed === true || parsed === "true") {
15550
+ return true;
15551
+ }
15552
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15553
+ return false;
15554
+ }
15555
+ const obj = parsed;
15556
+ if (obj.invoke === true || obj.invoke === "true") {
15557
+ return true;
15558
+ }
15559
+ for (const value of Object.values(obj)) {
15560
+ if (value &&
15561
+ typeof value === "object" &&
15562
+ !Array.isArray(value) &&
15563
+ (value.invoke === true ||
15564
+ value.invoke === "true")) {
15565
+ return true;
15566
+ }
15567
+ }
15568
+ }
15569
+ catch {
15570
+ return false;
15571
+ }
15572
+ return false;
15573
+ }
15574
+ /**
15575
+ * Build a CLI-aligned user Rego that opens cloud-function access for
15576
+ * anonymous + unauthenticated callers (tcb policy set example + HTTP no-token).
15577
+ */
15578
+ function buildPublicFunctionsUserRego(resourceId) {
15579
+ const comment = resourceId && resourceId !== "*"
15580
+ ? `# Public HTTP/API access for function ${resourceId} (aligned with tcb policy set)`
15581
+ : `# Public HTTP/API access for cloud functions (aligned with tcb policy set)`;
15582
+ return [
15583
+ "package authz.user",
15584
+ "",
15585
+ "default allow := false",
15586
+ "",
15587
+ comment,
15588
+ "allow if {",
15589
+ ' input.cloudbase.resource_type == "functions"',
15590
+ ' input.subject.auth_type in {"anonymous", "unauthenticated"}',
15591
+ "}",
15592
+ "",
15593
+ ].join("\n");
15594
+ }
15595
+ function resolveFunctionAuthzRegoInput(securityRule, resourceId) {
15596
+ if (securityRule && looksLikeUserRego(securityRule)) {
15597
+ return securityRule;
15598
+ }
15599
+ if (isPublicFunctionInvokeRule(securityRule)) {
15600
+ return buildPublicFunctionsUserRego(resourceId);
15601
+ }
15602
+ throw new Error(`PostgreSQL environments manage HTTP/function gateway auth via OPA Rego ` +
15603
+ `(same as CLI \`tcb policy set\`), not ModifyResourcePermission / function security-rule JSON. ` +
15604
+ `Pass either:\n` +
15605
+ `1) permission="CUSTOM" with securityRule as a full Rego document starting with \`package authz.user\`, or\n` +
15606
+ `2) permission="CUSTOM" with securityRule='{"invoke":true}' to generate a public-functions allow policy.\n` +
15607
+ `See https://docs.cloudbase.net/cli-v1/policy/management`);
15608
+ }
15609
+ async function describeEnvAuthzUserRego(cloudbase) {
15610
+ if (!cloudbase?.permission?.describeEnvAuthzConfig) {
15611
+ throw new Error("Current @cloudbase/manager-node does not expose permission.describeEnvAuthzConfig. Upgrade manager-node (>= 5.5.5) to align with CLI tcb policy get.");
15612
+ }
15613
+ const result = await cloudbase.permission.describeEnvAuthzConfig({
15614
+ key: AUTHZ_USER_REGO_KEY,
15615
+ });
15616
+ const value = typeof result?.Item?.Value === "string"
15617
+ ? result.Item.Value
15618
+ : typeof result?.Value === "string"
15619
+ ? result.Value
15620
+ : "";
15621
+ return { value, raw: result };
15622
+ }
15623
+ async function modifyEnvAuthzUserRego(cloudbase, value) {
15624
+ if (!cloudbase?.permission?.modifyEnvAuthzConfig) {
15625
+ throw new Error("Current @cloudbase/manager-node does not expose permission.modifyEnvAuthzConfig. Upgrade manager-node (>= 5.5.5) to align with CLI tcb policy set.");
15626
+ }
15627
+ return cloudbase.permission.modifyEnvAuthzConfig({
15628
+ key: AUTHZ_USER_REGO_KEY,
15629
+ value,
15630
+ });
15631
+ }
15632
+ async function describeResourcePermissionWithFunctionPgFallback(options) {
15633
+ const { cloudbase, envId, resourceType, resources } = options;
15634
+ try {
15635
+ const result = await cloudbase.permission.describeResourcePermission({
15636
+ resourceType: mapResourceType(resourceType),
15637
+ resources,
15638
+ });
15639
+ return result;
15640
+ }
15641
+ catch (error) {
15642
+ if (resourceType !== "function" || !isPostgresqlPermissionApiUnsupported(error)) {
15643
+ throw error;
15644
+ }
15645
+ const fallback = await describeEnvAuthzUserRego(cloudbase);
15646
+ const targets = resources && resources.length > 0 ? resources : ["*"];
15647
+ const permissionList = targets.map((resourceId) => ({
15648
+ ResourceType: "function",
15649
+ Resource: resourceId,
15650
+ Permission: "CUSTOM",
15651
+ SecurityRule: fallback.value || "",
15652
+ }));
15653
+ return {
15654
+ Data: {
15655
+ TotalCount: permissionList.length,
15656
+ PermissionList: permissionList,
15657
+ },
15658
+ RequestId: fallback.raw?.RequestId,
15659
+ fallback: "describeEnvAuthzConfig",
15660
+ raw: fallback.raw,
15661
+ };
15662
+ }
15663
+ }
15664
+ async function modifyFunctionPermissionWithPgFallback(options) {
15665
+ const { cloudbase, resourceId, permission, securityRule } = options;
15666
+ try {
15667
+ const result = await cloudbase.permission.modifyResourcePermission({
15668
+ resourceType: "function",
15669
+ resource: resourceId,
15670
+ permission,
15671
+ securityRule,
15672
+ });
15673
+ return { result };
15674
+ }
15675
+ catch (error) {
15676
+ if (!isPostgresqlPermissionApiUnsupported(error)) {
15677
+ throw error;
15678
+ }
15679
+ if (permission !== "CUSTOM") {
15680
+ throw new Error(`PostgreSQL environments do not support ModifyResourcePermission. ` +
15681
+ `Align with CLI \`tcb policy set\`: use permission="CUSTOM" and pass OPA Rego ` +
15682
+ `(package authz.user) or securityRule='{"invoke":true}' for public functions. ` +
15683
+ `Underlying error: ${error instanceof Error ? error.message : String(error)}`);
15684
+ }
15685
+ const rego = resolveFunctionAuthzRegoInput(securityRule, resourceId);
15686
+ const result = await modifyEnvAuthzUserRego(cloudbase, rego);
15687
+ return { result, fallback: "modifyEnvAuthzConfig", rego };
15688
+ }
15689
+ }
14594
15690
  function normalizeRecordArray(value, label) {
14595
15691
  if (value === undefined) {
14596
15692
  return undefined;
@@ -14766,7 +15862,7 @@ function registerPermissionTools(server) {
14766
15862
  };
14767
15863
  server.registerTool?.("queryPermissions", {
14768
15864
  title: "查询 CloudBase 权限与用户配置",
14769
- description: "查询 CloudBase 权限与用户配置,支持查询资源权限(数据库/云函数/存储桶等)、角色列表/详情、应用用户列表/详情。\n\n示例:\n- 查询存储桶权限:`action=\"getResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\"`\n\n📌 跨后端边界提示:调用前先用 `envQuery(action=\"info\", envId=...)` 看 `EnvInfo.RuntimeBackends`。`resourceType=\"noSqlDatabase\"` 查询的是 CloudBase NoSQL 集合规则,与 CloudBase PostgreSQL(PG)表的行级安全(RLS)是两套独立机制——同一个 PG 环境里 NoSQL 集合若仍在使用,对那些集合查询本工具结果**仍然有效**。要查 PG 表 RLS,请改用 `queryPgDatabase(action=\"sql\", sql=\"SELECT * FROM pg_policies WHERE tablename=...\")`。本工具不涉及 MySQL 权限。",
15865
+ description: "查询 CloudBase 权限与用户配置,支持查询资源权限(数据库/云函数/存储桶等)、角色列表/详情、应用用户列表/详情。\n\n示例:\n- 查询存储桶权限:`action=\"getResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\"`\n\n📌 跨后端边界提示:调用前先用 `envQuery(action=\"info\", envId=...)` 看 `EnvInfo.RuntimeBackends`。`resourceType=\"noSqlDatabase\"` 查询的是 CloudBase NoSQL 集合规则,与 CloudBase PostgreSQL(PG)表的行级安全(RLS)是两套独立机制——同一个 PG 环境里 NoSQL 集合若仍在使用,对那些集合查询本工具结果**仍然有效**。要查 PG 表 RLS,请改用 `queryPgDatabase(action=\"sql\", sql=\"SELECT * FROM pg_policies WHERE tablename=...\")`。本工具不涉及 MySQL 权限。\n\n⚠️ PostgreSQL 环境:平台 `DescribeResourcePermission` 对 PG 环境会直接拒绝。当 `resourceType=\"function\"` 时,本工具会自动回退到 Manager SDK `describeEnvAuthzConfig`(与 CLI `tcb policy get` 一致,读取 `authz.user.rego`)。",
14770
15866
  inputSchema: {
14771
15867
  action: zod_1.z.enum(QUERY_PERMISSION_ACTIONS),
14772
15868
  resourceType: zod_1.z
@@ -14798,8 +15894,10 @@ function registerPermissionTools(server) {
14798
15894
  if (resourceType === "storage") {
14799
15895
  await ensureStorageBucketsExist(cloudbase, [resourceId]);
14800
15896
  }
14801
- const result = await cloudbase.permission.describeResourcePermission({
14802
- resourceType: mapResourceType(resourceType),
15897
+ const result = await describeResourcePermissionWithFunctionPgFallback({
15898
+ cloudbase,
15899
+ envId,
15900
+ resourceType,
14803
15901
  resources: [resourceId],
14804
15902
  });
14805
15903
  (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
@@ -14815,8 +15913,11 @@ function registerPermissionTools(server) {
14815
15913
  aclTag: matchedPermission?.Permission,
14816
15914
  permissions,
14817
15915
  hints,
14818
- raw: result,
14819
- }, "资源权限查询成功");
15916
+ ...(result.fallback ? { fallback: result.fallback } : {}),
15917
+ raw: result.raw ?? result,
15918
+ }, result.fallback
15919
+ ? "资源权限查询成功(PostgreSQL 环境已回退到 describeEnvAuthzConfig / tcb policy get)"
15920
+ : "资源权限查询成功");
14820
15921
  }
14821
15922
  case "listResourcePermissions": {
14822
15923
  if (!resourceType) {
@@ -14825,8 +15926,10 @@ function registerPermissionTools(server) {
14825
15926
  if (resourceType === "storage" && resourceIds?.length) {
14826
15927
  await ensureStorageBucketsExist(cloudbase, resourceIds);
14827
15928
  }
14828
- const result = await cloudbase.permission.describeResourcePermission({
14829
- resourceType: mapResourceType(resourceType),
15929
+ const result = await describeResourcePermissionWithFunctionPgFallback({
15930
+ cloudbase,
15931
+ envId,
15932
+ resourceType,
14830
15933
  resources: resourceIds,
14831
15934
  });
14832
15935
  (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
@@ -14847,8 +15950,11 @@ function registerPermissionTools(server) {
14847
15950
  permissions,
14848
15951
  resourceHints,
14849
15952
  total: result.Data.TotalCount ?? 0,
14850
- raw: result,
14851
- }, "资源权限列表查询成功");
15953
+ ...(result.fallback ? { fallback: result.fallback } : {}),
15954
+ raw: result.raw ?? result,
15955
+ }, result.fallback
15956
+ ? "资源权限列表查询成功(PostgreSQL 环境已回退到 describeEnvAuthzConfig / tcb policy get)"
15957
+ : "资源权限列表查询成功");
14852
15958
  }
14853
15959
  case "listRoles": {
14854
15960
  const result = await cloudbase.permission.describeRoleList({
@@ -14927,7 +16033,7 @@ function registerPermissionTools(server) {
14927
16033
  }));
14928
16034
  server.registerTool?.("managePermissions", {
14929
16035
  title: "管理 CloudBase 权限与用户配置",
14930
- description: "管理 CloudBase 权限与用户配置,支持修改资源权限(数据库/云函数/存储桶等)、角色管理、成员与策略增删、应用用户 CRUD。\n\n示例:\n- 设置存储桶为私有:`action=\"updateResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\", permission=\"PRIVATE\"`\n- 创建角色:`action=\"createRole\", roleName=\"admin\", roleIdentity=\"admin\"`\n\n注意:`createUser` / `updateUser` 是环境侧应用用户管理能力,适合测试账号、管理员或预置用户,不应替代浏览器里的 Web SDK 注册表单;前端用户名密码注册应使用 `auth.signUp({ username, password })`,登录应使用 `auth.signInWithPassword({ username, password })`。直接在浏览器里用 `auth.signUp` 创建用户名密码用户取决于 SDK/provider 支持,使用前必须验证;不支持时应走后端或管理端边界,不能在浏览器暴露密钥。`securityRule` 的详细语义取决于 `resourceType`:`doc._openid`、`auth.openid`、查询条件子集校验,以及 `create` / `update` / `delete` JSON 模板仅适用于 `resourceType=\"noSqlDatabase\"` 的文档数据库安全规则;配置 `function` 或 `storage` 时,请参考各自官方安全规则文档,而不是复用 NoSQL 模板。\n\n📌 跨后端边界提示:调用前先用 `envQuery(action=\"info\", envId=...)` 看 `EnvInfo.RuntimeBackends`:\n- `resourceType=\"noSqlDatabase\"` 仅作用于 CloudBase NoSQL 文档数据库的集合;CloudBase PostgreSQL(PG)表的行级权限**不**受它控制——PG 表请改用 RLS:`managePgDatabase(action=\"execute\", confirm=true)` 跑 `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` 与 `CREATE POLICY ...`。同一个 PG 环境里如果还有 NoSQL 集合在用,对那些**集合**继续使用 `noSqlDatabase` 规则是正确的——不是\"PG 环境就禁用本工具\"。\n- `resourceType=\"storage\"` 控制的是 NoSQL/COS 存储桶 ACL;PG 的 `pgstore` bucket 不在此 `resourceType` 覆盖范围内。\n- 本工具不涉及 MySQL;MySQL 数据库权限请走 MySQL 自身的 GRANT/REVOKE 语句(通过 `manageMysqlDatabase`)。",
16036
+ description: "管理 CloudBase 权限与用户配置,支持修改资源权限(数据库/云函数/存储桶等)、角色管理、成员与策略增删、应用用户 CRUD。\n\n示例:\n- 设置存储桶为私有:`action=\"updateResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\", permission=\"PRIVATE\"`\n- 创建角色:`action=\"createRole\", roleName=\"admin\", roleIdentity=\"admin\"`\n- 放开云函数匿名/未登录访问(PG 会走 OPA,对齐 CLI `tcb policy set`):`action=\"updateResourcePermission\", resourceType=\"function\", resourceId=\"myFn\", permission=\"CUSTOM\", securityRule='{\"invoke\":true}'`\n\n注意:`createUser` / `updateUser` 是环境侧应用用户管理能力,适合测试账号、管理员或预置用户,不应替代浏览器里的 Web SDK 注册表单;前端用户名密码注册应使用 `auth.signUp({ username, password })`,登录应使用 `auth.signInWithPassword({ username, password })`。直接在浏览器里用 `auth.signUp` 创建用户名密码用户取决于 SDK/provider 支持,使用前必须验证;不支持时应走后端或管理端边界,不能在浏览器暴露密钥。`securityRule` 的详细语义取决于 `resourceType`:`doc._openid`、`auth.openid`、查询条件子集校验,以及 `create` / `update` / `delete` JSON 模板仅适用于 `resourceType=\"noSqlDatabase\"` 的文档数据库安全规则;配置 `function` 或 `storage` 时,请参考各自官方安全规则文档,而不是复用 NoSQL 模板。\n\n📌 跨后端边界提示:调用前先用 `envQuery(action=\"info\", envId=...)` 看 `EnvInfo.RuntimeBackends`:\n- `resourceType=\"noSqlDatabase\"` 仅作用于 CloudBase NoSQL 文档数据库的集合;CloudBase PostgreSQL(PG)表的行级权限**不**受它控制——PG 表请改用 RLS:`managePgDatabase(action=\"execute\", confirm=true)` 跑 `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` 与 `CREATE POLICY ...`。同一个 PG 环境里如果还有 NoSQL 集合在用,对那些**集合**继续使用 `noSqlDatabase` 规则是正确的——不是\"PG 环境就禁用本工具\"。\n- `resourceType=\"storage\"` 控制的是 NoSQL/COS 存储桶 ACL;PG 的 `pgstore` bucket 不在此 `resourceType` 覆盖范围内。\n- 本工具不涉及 MySQL;MySQL 数据库权限请走 MySQL 自身的 GRANT/REVOKE 语句(通过 `manageMysqlDatabase`)。\n\n⚠️ PostgreSQL 环境:平台 `ModifyResourcePermission` 对 PG 环境会直接拒绝。当 `resourceType=\"function\"` 时,本工具会自动回退到 Manager SDK `modifyEnvAuthzConfig`(与 CLI `tcb policy set` 一致,写入 `authz.user.rego`)。`securityRule` 可传完整 Rego(`package authz.user`)或 `'{\"invoke\":true}'`(自动生成放通 anonymous/unauthenticated 调 functions 的策略)。设置 Rego 后旧网关鉴权会失效,行为与 CLI 相同。",
14931
16037
  inputSchema: {
14932
16038
  action: zod_1.z.enum(MANAGE_PERMISSION_ACTIONS),
14933
16039
  resourceType: zod_1.z
@@ -14975,12 +16081,29 @@ function registerPermissionTools(server) {
14975
16081
  if (!resourceType || !resourceId || !permission) {
14976
16082
  throw new Error("action=updateResourcePermission 时必须提供 resourceType、resourceId 和 permission");
14977
16083
  }
14978
- const result = await cloudbase.permission.modifyResourcePermission({
14979
- resourceType: mapResourceType(resourceType),
14980
- resource: resourceId,
14981
- permission,
14982
- securityRule,
14983
- });
16084
+ let result;
16085
+ let fallback;
16086
+ let appliedRego;
16087
+ if (resourceType === "function") {
16088
+ const updated = await modifyFunctionPermissionWithPgFallback({
16089
+ cloudbase,
16090
+ envId,
16091
+ resourceId,
16092
+ permission,
16093
+ securityRule,
16094
+ });
16095
+ result = updated.result;
16096
+ fallback = updated.fallback;
16097
+ appliedRego = updated.rego;
16098
+ }
16099
+ else {
16100
+ result = await cloudbase.permission.modifyResourcePermission({
16101
+ resourceType: mapResourceType(resourceType),
16102
+ resource: resourceId,
16103
+ permission,
16104
+ securityRule,
16105
+ });
16106
+ }
14984
16107
  (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
14985
16108
  const hints = permission === "CUSTOM" ? buildPermissionHints(securityRule, resourceId) : [];
14986
16109
  return buildEnvelope({
@@ -14990,6 +16113,8 @@ function registerPermissionTools(server) {
14990
16113
  resourceId,
14991
16114
  permission,
14992
16115
  hints,
16116
+ ...(fallback ? { fallback } : {}),
16117
+ ...(appliedRego ? { rego: appliedRego } : {}),
14993
16118
  verificationHint: resourceType === "noSqlDatabase" && permission === "CUSTOM"
14994
16119
  ? buildWriteVerificationHint(resourceId)
14995
16120
  : undefined,
@@ -14997,7 +16122,9 @@ function registerPermissionTools(server) {
14997
16122
  ? buildPermissionPropagationHint(resourceId)
14998
16123
  : undefined,
14999
16124
  raw: result,
15000
- }, "资源权限更新成功");
16125
+ }, fallback
16126
+ ? "资源权限更新成功(PostgreSQL 环境已回退到 modifyEnvAuthzConfig / tcb policy set)"
16127
+ : "资源权限更新成功");
15001
16128
  }
15002
16129
  case "createRole": {
15003
16130
  if (!roleName || !roleIdentity) {
@@ -15616,7 +16743,7 @@ function registerSetupTools(server) {
15616
16743
  server.registerTool("downloadTemplate", {
15617
16744
  title: "下载项目模板",
15618
16745
  description: `自动下载并部署CloudBase项目模板。
15619
- **Note**: Call this tool when the user requests to create a new project using a CloudBase template.\n\n支持的模板:\n- react: React + CloudBase 全栈应用模板\n- vue: Vue + CloudBase 全栈应用模板\n- miniprogram: 微信小程序 + 云开发模板 \n- uniapp: UniApp + CloudBase 跨端应用模板\n- rules: 只包含AI编辑器配置文件(包含Cursor、WindSurf、CodeBuddy等所有主流编辑器配置),适合在已有项目中补充AI编辑器配置\n\n支持的IDE类型:\n- all: 下载所有IDE配置\n- cursor: Cursor AI编辑器\n- 其他IDE类型见下方列表\n\n注意:如果未传入 ide 参数且无法从环境变量检测到 IDE,将提示错误并要求传入 ide 参数\n- windsurf: WindSurf AI编辑器\n- codebuddy: CodeBuddy AI编辑器\n- claude-code: Claude Code AI编辑器\n- cline: Cline AI编辑器\n- gemini-cli: Gemini CLI\n- opencode: OpenCode AI编辑器\n- qwen-code: 通义灵码\n- baidu-comate: 百度Comate\n- openai-codex-cli: OpenAI Codex CLI\n- augment-code: Augment Code\n- github-copilot: GitHub Copilot\n- roocode: RooCode AI编辑器\n- tongyi-lingma: 通义灵码\n- trae: Trae AI编辑器\n- qoder: Qoder AI编辑器\n- antigravity: Google Antigravity AI编辑器\n- vscode: Visual Studio Code\n- kiro: Kiro AI编辑器\n- aider: Aider AI编辑器\n\n特别说明:\n- rules 模板会自动包含当前 mcp 版本号信息(版本号:${ true ? "2.25.4" : 0}),便于后续维护和版本追踪\n- 下载 rules 模板时,如果项目中已存在 README.md 文件,系统会自动保护该文件不被覆盖(除非设置 overwrite=true)`,
16746
+ **Note**: Call this tool when the user requests to create a new project using a CloudBase template.\n\n支持的模板:\n- react: React + CloudBase 全栈应用模板\n- vue: Vue + CloudBase 全栈应用模板\n- miniprogram: 微信小程序 + 云开发模板 \n- uniapp: UniApp + CloudBase 跨端应用模板\n- rules: 只包含AI编辑器配置文件(包含Cursor、WindSurf、CodeBuddy等所有主流编辑器配置),适合在已有项目中补充AI编辑器配置\n\n支持的IDE类型:\n- all: 下载所有IDE配置\n- cursor: Cursor AI编辑器\n- 其他IDE类型见下方列表\n\n注意:如果未传入 ide 参数且无法从环境变量检测到 IDE,将提示错误并要求传入 ide 参数\n- windsurf: WindSurf AI编辑器\n- codebuddy: CodeBuddy AI编辑器\n- claude-code: Claude Code AI编辑器\n- cline: Cline AI编辑器\n- gemini-cli: Gemini CLI\n- opencode: OpenCode AI编辑器\n- qwen-code: 通义灵码\n- baidu-comate: 百度Comate\n- openai-codex-cli: OpenAI Codex CLI\n- augment-code: Augment Code\n- github-copilot: GitHub Copilot\n- roocode: RooCode AI编辑器\n- tongyi-lingma: 通义灵码\n- trae: Trae AI编辑器\n- qoder: Qoder AI编辑器\n- antigravity: Google Antigravity AI编辑器\n- vscode: Visual Studio Code\n- kiro: Kiro AI编辑器\n- aider: Aider AI编辑器\n\n特别说明:\n- rules 模板会自动包含当前 mcp 版本号信息(版本号:${ true ? "2.25.6" : 0}),便于后续维护和版本追踪\n- 下载 rules 模板时,如果项目中已存在 README.md 文件,系统会自动保护该文件不被覆盖(除非设置 overwrite=true)`,
15620
16747
  inputSchema: {
15621
16748
  template: zod_1.z
15622
16749
  .enum(["react", "vue", "miniprogram", "uniapp", "rules"])