Skip to content

feat(functions): two-phase ZIP deploy via presigned COS upload - #1084

Merged
binggg merged 3 commits into
mainfrom
feat/functions-zip-two-phase
Sep 24, 2026
Merged

binggg merged 3 commits into
mainfrom
feat/functions-zip-two-phase

Conversation

@binggg

@binggg binggg commented Sep 23, 2026

Copy link
Copy Markdown
Member

What

Two-phase ZIP deployment for cloud functions: the agent PUTs the code zip straight to the environment's own COS bucket with a presigned URL, then deploys by reference — no local code directory required.

  • queryFunctions → getFunctionUploadUrl (phase A): resolves the environment's storage bucket (DescribeEnvs → Storages[0]) and mints a presigned PUT URL from the caller's own credentials. Returns uploadUrl, uploadHeaders (session-token header when the credential is temporary), and the cosBucketName / cosBucketRegion / cosObjectName triplet. functionName is optional and only shapes the object key.
  • manageFunctions → code (phase B): new strict input for createFunction / updateFunctionCode, wired to the SDK's deployMode: "cos" path. The cloud-mode gate allows it (like image deploy, it touches no local directory).
  • New pure module function-cos-upload.ts replicates the COS q-sign algorithm with node:crypto only (no new dependency). Session tokens are signed into x-cos-security-token and returned as a request header.
  • i18n (zh/en) for the new action, schema fields, and next-step guidance; scripts/tools.json + doc/mcp-tools.md regenerated.

Why

Hosted / cloud-mode callers previously had no way to deploy function code: local directory paths don't exist on the caller machine, and the inline zipFile channel is capped at 20MB base64. A presigned upload to the environment's own bucket removes both limits with zero new CAM permissions, works for permanent and temporary credentials, and never exposes the appId to the agent.

How to verify

cd mcp
npx vitest run src/tools/function-cos-upload.test.ts src/tools/functions.test.ts  # 81 tests
npx tsc --noEmit
npm run build:webpack
  • Signature tests include golden-value comparison against cos-nodejs-sdk-v5 getAuth for both permanent keys and session tokens.
  • Tool-level tests cover the gate (code passes cloud mode), strict schema rejection, and deployMode=cos passthrough for create/update.
  • Live-checked end to end against a scratch function: getFunctionUploadUrl → PUT → create/update with code → Invoke returns the expected payload.

What:
- queryFunctions: new getFunctionUploadUrl action (phase A) that resolves
  the environment's own COS bucket (DescribeEnvs) and mints a presigned
  PUT URL with the caller's credentials
- manageFunctions: new code input (cosBucketName/cosObjectName/
  cosBucketRegion) for createFunction/updateFunctionCode (phase B),
  wired to deployMode=cos; strict schema, cloud-mode gate allows it
- new pure module function-cos-upload.ts replicates the COS q-sign
  algorithm (no new dependency); tests include golden-value comparison
  against cos-nodejs-sdk-v5 getAuth for permanent keys and session
  tokens
- i18n (zh/en) for the new action, schema fields, and follow-up
  guidance; regenerated scripts/tools.json and doc/mcp-tools.md

Why:
- hosted/cloud mode callers previously had no way to deploy function
  code without a local directory or the 20MB inline zipFile limit;
  a presigned upload to the environment bucket removes both limits
  without new CAM permissions or exposing the appId

How to verify:
- cd mcp && npx vitest run src/tools/function-cos-upload.test.ts
  src/tools/functions.test.ts (81 tests, incl. SDK golden values and
  cloud-mode gate cases)
- npx tsc --noEmit && npm run build:webpack
- live: getFunctionUploadUrl -> PUT zip -> manageFunctions code deploy
  verified against a scratch function (create + update, invoke OK)
@binggg

binggg commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

Cloud-mode verification: ran the built server with --cloud-mode (CLOUDBASE_MCP_CLOUD_MODE=true, env pinned via CLOUDBASE_ENV_ID) and drove the full two-phase flow over MCP stdio:

  1. tools/list → queryFunctions / manageFunctions both registered in cloud mode (33 tools total; auth/manageStorage/deploy* correctly skipped)
  2. queryFunctions action=getFunctionUploadUrl → presigned PUT returned 200 (environment's own bucket, fnzip-upload/ key prefix)
  3. manageFunctions action=createFunction with code triplet → OK; Invoke returned {"v":"cloudmode-e2e-create-..."} — exact expected payload
  4. Repeat phase A + updateFunctionCode with new zip → OK; Invoke returned the update marker

Both create and update paths deploy real code end-to-end in cloud mode with no local directory involved. Driver script: output/fnzip-probe/e2e-cloudmode.cjs (scratch function deleted afterwards).

What:
- resolveEnvFunctionCosStorage now falls back to the env-scoped
  DescribeEnvInfo (EnvInfo.EnvBaseInfo.Storages, same shape as
  DescribeEnvs Storages) when DescribeEnvs fails or returns nothing
- test mock covers both actions

Why:
- DescribeEnvs is an account-level action; env-scoped credentials
  (e.g. STS exchanged from an API Key, as remote MCP uses) get
  rejected by the gateway. The fallback keeps the presigned-upload
  flow working under those credentials.

How to verify:
- cd mcp && npx vitest run src/tools/functions.test.ts (81 passed)
- live: with CLOUDBASE_API_KEY credentials, full two-phase flow
  (getFunctionUploadUrl -> PUT -> createFunction/updateFunctionCode
  with code) verified end-to-end; Invoke returned the uploaded
  marker on both create and update
@binggg

binggg commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

Remote-MCP-style verification (ephemeral API Key credentials): ran the built server with CLOUDBASE_API_KEY + --cloud-mode — the same env-var credential injection the hosted path uses — against a real environment:

  • First run exposed a real gap: DescribeEnvs is account-level and gets rejected (invalid token) under API-Key-exchanged STS, so the bucket lookup failed.
  • Fix (b83d47a): fall back to the env-scoped DescribeEnvInfo (EnvInfo.EnvBaseInfo.Storages, same shape).
  • Re-run, full flow PASS: phase A PUT 200 → createFunction(code) OK → Invoke returned exact uploaded marker → phase A again → updateFunctionCode(code) OK → Invoke returned update marker → cleanup.

So the two-phase flow works under API-Key credentials with zero local directory.

@binggg

binggg commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

OAuth 登录态路径 E2E 实测(两段式 ZIP 部署)

场景:hosted MCP 同款 OAuth 登录态(refreshToken → 临时密钥)。驱动进程注入面剥掉 CLOUDBASE_API_KEY、TENCENTCLOUD_* 等全部凭据环境变量,server 只能从本地 OAuth 登录态取凭据。

结果(国内站环境,ap-shanghai,函数创建/更新/调用后已清理):

步骤 结果
预检:OAuth 临时密钥直调 DescribeEnvs(账号级动作) 正常返回,账号级权限可用
阶段A getFunctionUploadUrl → COS PUT 200
createFunction(code) → Invoke 返回值精确命中 create 版本标记
阶段A → updateFunctionCode(code) → Invoke 返回值精确命中 update 版本标记
server 侧核验 全程未触发 DescribeEnvInfo 兜底,走 DescribeEnvs 主路径

结论:OAuth 登录态与 API Key 登录态两条链路下,两段式部署均端到端可用。OAuth 临时密钥为账号级权限,getFunctionUploadUrl 直接走 DescribeEnvs 主路径,无需兜底。

@binggg

binggg commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

生产远程 MCP 现状实测(OAuth 浏览器登录全链路)

用标准 OAuth 链路(discovery → 动态客户端注册 → PKCE 授权码 → 浏览器登录授权 → token 交换)连接生产远程 MCP,验证函数部署能力现状:

检查项 结果
OAuth 全链路(authorize/consent/token) 正常,access_token 1h + refresh_token
MCP initialize + tools/list 正常,33 个工具
tools/list 是否含 getFunctionUploadUrl 否(本 PR 未发版,符合预期)
manageFunctions schema 无 code / deployMode 入参(旧版一次性部署代码面)
createFunction(cloud mode) 被拒:「在 cloud mode 下不可用…请改用本地模式执行,或使用镜像部署」

结论:生产远程 MCP 目前在 cloud mode 下无法用代码方式创建函数(只剩镜像部署一条路)。本 PR 合并发版后,code 内联与两段式 COS 上传都成为 cloud mode 下的合法部署路径,正好补上这个缺口。发版后可用本轮的 OAuth 驱动脚本(remote-oauth-login.mjs + remote-oauth-fne2e.mjs)直接复测两段式。

@binggg

binggg commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

CAPI 层预验证:生产远程 MCP OAuth 会话下,两段式依赖的底层云 API 实测

通过生产远程 MCP 暴露的 callCloudApi 工具(OAuth 浏览器登录会话),把两段式部署链路依赖的底层云 API 逐个实测:

云 API 服务 结果
DescribeEnvInfo tcb ✅ 正常返回 EnvBaseInfo.Storages(桶存在、Status=NORMAL)——阶段A 取桶源可用
DescribeEnvs tcb ❌ invalid token——账号级动作被网关拒绝,与 API Key 路径现象一致
CreateFunction(Code.ZipFile,Stamp=MINI_QCBASE + Role=TCB_QcsRole) tcb ✅ 创建成功
UpdateFunctionCode(Code.ZipFile) tcb ✅ 更新成功
Invoke scf ✅ 返回值精确命中更新后版本(Create→Update 代码链路生效)
DeleteFunction tcb ✅ 清理成功

结论:

  1. 两段式的全部底层云 API 在生产远程 MCP 的 OAuth 凭据下均可用;此前 manageFunctions createFunction 被拒是 MCP 层 cloud-mode gate 拦截,不是云端权限不足
  2. 账号级 DescribeEnvs 在远程 OAuth 凭据下同样被拒,进一步证实本 PR 的 DescribeEnvInfo 兜底在远程链路是必需路径,而非可选优化
  3. 唯一未从外部实测的是 COS PUT 上传段(STS 由服务端内部持有),该路径已在本地 OAuth 登录态 E2E 中验证,与 PR 代码同构

即:PR 合并发版后,远程 MCP 的两段式部署在云端接口层面已全部就绪。

@binggg

binggg commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

补充:PUT 上传段的实测(外部 OAuth 会话下的可行路径)\n\n之前外部探测打不了 COS PUT,根因已定位:环境自有桶的预签名需要调用者持有 STS,而远程 MCP 的 OAuth access_token 是 MCP 层 JWT,/capi/credential 不收这种 token(实测报 JWT 校验错误),STS 无法在客户端侧取得。\n\n改用 SDK 内置的另一条上传路径(服务端下发签名,不需要本地 STS)实测成功:\n\n| 步骤 | 结果 |\n| --- | --- |\n| scf GetTempCosInfo(下发 Date + 签名) | ✅ |\n| PUT zip → 官方临时桶(Authorization 用服务端签名) | ✅ HTTP 200 |\n| tcb CreateFunction Code.TempCosObjectName → Invoke | ✅ 精确命中 v1 |\n| 再 PUT v2 → tcb UpdateFunctionCode → Invoke | ✅ 精确命中 v2 |\n| DeleteFunction 清理 | ✅ |\n\n结论:\n1. 「Agent 先 PUT 上传、再触发部署」的两段式交互模式在远程链路上端到端可行,PUT 段已实测打通;\n2. 正式产品路径(getFunctionUploadUrl 在 MCP 服务端用其持有的 STS 铸预签名 URL)发版后即可用,外部探测因拿不到服务端 STS 而无法提前验,属探测面限制而非产品缺陷;\n3. 顺带发现 scf GetTempCosInfo 是一条服务端签名、无需本地 STS 的现成兜底路径,后续如遇环境自有桶取不到的场景可作为备选(SDK 内已有完整实现)。

pkg.pr.new build (webpack) compiles *.test.ts and failed to resolve
cos-nodejs-sdk-v5, which is not a direct dependency of the package.
Freeze the SDK getAuth outputs as expected values so the golden-value
comparison no longer needs the SDK at build time.
@binggg
binggg merged commit 8f687ef into main Sep 24, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant