diff --git a/apps/control-plane-api/src/routes/workspaceRoutes.ts b/apps/control-plane-api/src/routes/workspaceRoutes.ts index e28dfbb..0cc9e40 100644 --- a/apps/control-plane-api/src/routes/workspaceRoutes.ts +++ b/apps/control-plane-api/src/routes/workspaceRoutes.ts @@ -228,7 +228,7 @@ async function validateWorkspaceAliyunCredentials(providerCredentials: Record; +type ProviderPoolInput = Array<{ provider?: string; config?: Record }>; function missingTenantCloudProviderAccess(tenantId: string, runtimeProvider: RuntimeProviderInput, sandboxProvider: SandboxProviderInput, runtimePools: ProviderPoolInput = [], sandboxPools: ProviderPoolInput = [], artifactProvider?: "tos" | "oss") { if (!tenantId) return []; @@ -262,6 +262,15 @@ function missingWorkspaceProvisioningCredentials( const required: Array<[string, unknown]> = []; const runtimeProviders = providerSet(runtimeProvider, runtimePools); const sandboxProviders = providerSet(sandboxProvider, sandboxPools); + const aliyunSandboxInvokeUrl = String( + aliyunSandboxConfig.invoke_url ?? + aliyunSandboxConfig.invokeUrl ?? + aliyunCreds.ALIYUN_FC_INVOKE_URL ?? + aliyunCreds.invoke_url ?? + aliyunCreds.invokeUrl ?? + sandboxPools.find((pool) => pool.provider === "aliyun_fc" && String(asRecord(pool.config).invoke_url ?? "").trim())?.config?.invoke_url ?? + "" + ); if (runtimeProviders.has("vefaas") || artifactProvider === "tos") { required.push( ["VOLCENGINE_ACCESS_KEY", vefaasCreds.VOLCENGINE_ACCESS_KEY], @@ -284,12 +293,13 @@ function missingWorkspaceProvisioningCredentials( ); } if (sandboxProviders.has("aliyun_fc")) { - required.push( - ["ALIYUN_ACCESS_KEY_ID", aliyunCreds.ALIYUN_ACCESS_KEY_ID ?? aliyunCreds.access_key_id ?? aliyunCreds.ak], - ["ALIYUN_ACCESS_KEY_SECRET", aliyunCreds.ALIYUN_ACCESS_KEY_SECRET ?? aliyunCreds.access_key_secret ?? aliyunCreds.sk], - ["ALIYUN_REGION", aliyunCreds.ALIYUN_REGION ?? aliyunCreds.region], - ["ALIYUN_FC_INVOKE_URL", aliyunSandboxConfig.invoke_url ?? aliyunCreds.ALIYUN_FC_INVOKE_URL] - ); + if (!aliyunSandboxInvokeUrl.trim()) { + required.push( + ["ALIYUN_ACCESS_KEY_ID", aliyunCreds.ALIYUN_ACCESS_KEY_ID ?? aliyunCreds.access_key_id ?? aliyunCreds.ak], + ["ALIYUN_ACCESS_KEY_SECRET", aliyunCreds.ALIYUN_ACCESS_KEY_SECRET ?? aliyunCreds.access_key_secret ?? aliyunCreds.sk], + ["ALIYUN_REGION", aliyunCreds.ALIYUN_REGION ?? aliyunCreds.region] + ); + } } if (sandboxProviders.has("daytona")) { required.push( diff --git a/apps/control-plane-api/src/runtime/aliyunFcRuntime.ts b/apps/control-plane-api/src/runtime/aliyunFcRuntime.ts index e077f5c..29999f6 100644 --- a/apps/control-plane-api/src/runtime/aliyunFcRuntime.ts +++ b/apps/control-plane-api/src/runtime/aliyunFcRuntime.ts @@ -106,8 +106,10 @@ export function aliyunFcSandboxRuntime( export function aliyunFcLoopAgentEnv(runtime: AliyunFcRuntimeInfo, sessionId: string, agentConfig: JsonRecord) { const agentLoop = asRecord(agentConfig.agent_loop); + const agentLoopConfig = asRecord(agentLoop.config); return stringifyRecord({ ...asRecord(runtime.envs), + ...asRecord(agentLoopConfig.env), MAPLE_SESSION_ID: sessionId, MAPLE_AGENT_TEMPLATE: JSON.stringify(agentConfig), MAPLE_AGENT_LOOP_TYPE: String(agentLoop.type || "anthropic_claude_code"), diff --git a/apps/control-plane-api/src/runtime/vefaasAgentRuntime.ts b/apps/control-plane-api/src/runtime/vefaasAgentRuntime.ts index a310948..d6d7a9d 100644 --- a/apps/control-plane-api/src/runtime/vefaasAgentRuntime.ts +++ b/apps/control-plane-api/src/runtime/vefaasAgentRuntime.ts @@ -60,8 +60,10 @@ export function vefaasLoopAgentConfig(session: JsonRecord) { export function vefaasLoopAgentEnv(runtime: VefaasRuntimeInfo, sessionId: string, agentConfig: JsonRecord) { const agentLoop = asRecord(agentConfig.agent_loop); + const agentLoopConfig = asRecord(agentLoop.config); return stringifyRecord({ ...asRecord(runtime.envs), + ...asRecord(agentLoopConfig.env), MAPLE_SESSION_ID: sessionId, MAPLE_AGENT_TEMPLATE: JSON.stringify(agentConfig), MAPLE_AGENT_LOOP_TYPE: String(agentLoop.type || "anthropic_claude_code"), diff --git a/apps/control-plane-api/src/storage/storeWorkspaceProvisioning.ts b/apps/control-plane-api/src/storage/storeWorkspaceProvisioning.ts index 2395397..5a71794 100644 --- a/apps/control-plane-api/src/storage/storeWorkspaceProvisioning.ts +++ b/apps/control-plane-api/src/storage/storeWorkspaceProvisioning.ts @@ -143,7 +143,8 @@ async function directAliyunFcRuntimeProvisioning(workspaceId: string, index: num const configuredInvokeUrl = String(process.env.MAPLE_ALIYUN_FC_INVOKE_URL || defaults.aliyun_fc.invoke_url || ""); const deployScript = process.env.MAPLE_ALIYUN_FC_RUNTIME_DEPLOY_SCRIPT || (configuredInvokeUrl ? "" : "infra/aliyun/deploy_aliyun_fc_runtime.mjs"); const configuredFunctionName = String(process.env.MAPLE_ALIYUN_FC_FUNCTION_NAME || defaults.aliyun_fc.function_name || functionName); - const envs = publicRuntimePoolMemberEnvs(runtimePoolMemberEnvs(defaults.aliyun_fc.envs, workspaceId, index, "managed-agents-platform-aliyun-fc")); + const aliyunRuntimeBaseEnvs = { MAPLE_SKIP_CLAUDE_AGENT_SDK_INSTALL: "true", ...defaults.aliyun_fc.envs }; + const envs = publicRuntimePoolMemberEnvs(runtimePoolMemberEnvs(aliyunRuntimeBaseEnvs, workspaceId, index, "managed-agents-platform-aliyun-fc")); if (!deployScript) { if (!configuredInvokeUrl) throw new Error("workspace runtime pool Aliyun FC provisioning requires MAPLE_ALIYUN_FC_RUNTIME_DEPLOY_SCRIPT or MAPLE_ALIYUN_FC_INVOKE_URL."); return { @@ -175,7 +176,7 @@ async function directAliyunFcRuntimeProvisioning(workspaceId: string, index: num MAPLE_RUNTIME_FUNCTION_MAX_CONCURRENCY: String(poolConfig.max_concurrency_per_instance), MAPLE_ALIYUN_FC_RUNTIME_ENVS: JSON.stringify({ ...runtimeEnvOverrides(process.env.MAPLE_ALIYUN_FC_RUNTIME_ENVS), - ...runtimePoolMemberEnvs(defaults.aliyun_fc.envs, workspaceId, index, "managed-agents-platform-aliyun-fc"), + ...runtimePoolMemberEnvs(aliyunRuntimeBaseEnvs, workspaceId, index, "managed-agents-platform-aliyun-fc"), MAPLE_RUNTIME_FUNCTION_MEMORY_MB: String(poolConfig.memory_mb), MAPLE_RUNTIME_FUNCTION_MIN_INSTANCES: String(poolConfig.min_instances_per_function), MAPLE_RUNTIME_FUNCTION_MAX_INSTANCES: String(poolConfig.max_instances_per_function), diff --git a/infra/aliyun/deploy_aliyun_fc_runtime.mjs b/infra/aliyun/deploy_aliyun_fc_runtime.mjs index 4991f2a..00c0b68 100644 --- a/infra/aliyun/deploy_aliyun_fc_runtime.mjs +++ b/infra/aliyun/deploy_aliyun_fc_runtime.mjs @@ -214,7 +214,15 @@ export function createAliyunFcClient(config) { regionId: config.region }); if (config.endpoint) clientConfig.endpoint = config.endpoint; - return new FCClient(clientConfig); + const ClientCtor = aliyunFcClientConstructor(); + return new ClientCtor(clientConfig); +} + +function aliyunFcClientConstructor() { + const candidates = [FCClient, FCClient?.default, FCClient?.default?.default]; + const ctor = candidates.find((candidate) => typeof candidate === "function"); + if (!ctor) throw new Error("Aliyun FC SDK client constructor not found."); + return ctor; } export async function resolveFcEndpointConfig(config) { diff --git a/infra/vefaas/deploy_vefaas_application.py b/infra/vefaas/deploy_vefaas_application.py index 348f8e2..d9db015 100644 --- a/infra/vefaas/deploy_vefaas_application.py +++ b/infra/vefaas/deploy_vefaas_application.py @@ -197,6 +197,8 @@ def build_backend_package(app_name: str) -> FunctionPackage: run([*backend_build, "--outfile", str(package_dir / "app.js"), str(ROOT / "apps/control-plane-api/src/index.ts")]) run([*backend_build, "--outfile", str(package_dir / "mysql_child.mjs"), str(ROOT / "apps/control-plane-api/src/infra/mysql_child.mjs")]) run([*backend_build, "--outfile", str(package_dir / "mysql_worker.mjs"), str(ROOT / "apps/control-plane-api/src/infra/mysql_worker.mjs")]) + (package_dir / "infra/aliyun").mkdir(parents=True, exist_ok=False) + run([*backend_build, "--outfile", str(package_dir / "infra/aliyun/deploy_aliyun_fc_runtime.mjs"), str(ROOT / "infra/aliyun/deploy_aliyun_fc_runtime.mjs")]) copy_backend_source(package_dir / "source") copy_source_dir(ROOT / "infra/vefaas", package_dir / "infra/vefaas") if (ROOT / "sandbox.config.json").exists(): diff --git a/tests/contracts/aliyun_fc_deploy_contract.ts b/tests/contracts/aliyun_fc_deploy_contract.ts index 78ee325..9fe4a72 100644 --- a/tests/contracts/aliyun_fc_deploy_contract.ts +++ b/tests/contracts/aliyun_fc_deploy_contract.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { AliyunFcRuntimeDeployer, + createAliyunFcClient, createHttpTriggerRequest, resolveFcEndpointConfig, resolveDeployConfig @@ -43,6 +44,7 @@ assert.equal(config.envs.MAPLE_SKIP_CLAUDE_AGENT_SDK_INSTALL, "true"); const endpointConfig = await resolveFcEndpointConfig({ ...config, accountId: "1234567890123456", endpoint: "" }); assert.equal(endpointConfig.endpoint, "1234567890123456.cn-hangzhou.fc.aliyuncs.com"); +assert.equal(typeof createAliyunFcClient(endpointConfig).createFunction, "function"); const calls: Array> = []; const fakeClient = { @@ -116,6 +118,7 @@ assert.deepEqual(calls.slice(-2).map((call) => call.type), ["deleteTrigger", "de const provisioningSource = readFileSync("apps/control-plane-api/src/storage/storeWorkspaceProvisioning.ts", "utf8"); assert.match(provisioningSource, /infra\/aliyun\/deploy_aliyun_fc_runtime\.mjs/); assert.match(provisioningSource, /MAPLE_ALIYUN_FC_CPU_MILLI/); +assert.match(provisioningSource, /MAPLE_SKIP_CLAUDE_AGENT_SDK_INSTALL/); const sandboxPoolSource = readFileSync("apps/control-plane-api/src/runtime/sandboxPoolManager.ts", "utf8"); assert.match(sandboxPoolSource, /MAPLE_ALIYUN_FC_SANDBOX_DEPLOY_SCRIPT/); assert.match(sandboxPoolSource, /ensureAliyunFcSandboxProviderReady/); diff --git a/tests/contracts/tenant_cloud_provider_contract.ts b/tests/contracts/tenant_cloud_provider_contract.ts index c886f1e..f3d077b 100644 --- a/tests/contracts/tenant_cloud_provider_contract.ts +++ b/tests/contracts/tenant_cloud_provider_contract.ts @@ -78,6 +78,25 @@ try { assert.equal(onboardingStatusText.includes("tenant-sk-5678"), false, "tenant metadata response must not include plaintext cloud secret"); assert.equal(onboardingStatusText.includes("aes-256-gcm"), false, "tenant metadata response must not include cloud secret ciphertext"); + const aliyunUser = await login(`aliyun-cloud-${stamp}@example.com`); + const aliyunOnly = await postJson("/v1/workspace_onboarding", aliyunUser.cookie, { + tenant: { name: `Aliyun ${stamp}` }, + workspace: { name: `Aliyun ${stamp}`, slug: `aliyun-${stamp}` }, + runtime_provider: "aliyun_fc", + sandbox_provider: "aliyun_fc", + runtime_pool: { desired_size: 1, min_instances_per_function: 0, max_instances_per_function: 1, max_concurrency_per_instance: 1, cpu_milli: 250, memory_mb: 512 }, + sandbox_config: { aliyun_fc: { region: "cn-hangzhou" } }, + sandbox_pool: { desired_size: 1, standby_ttl_ms: 60_000 }, + model_config_ids: [], + custom_model_configs: customModelConfigs, + api_key: { display_name: "aliyun", scopes: ["control_plane"] }, + provider_credentials: { aliyun: { ALIYUN_ACCESS_KEY_ID: "aliyun-ak-lazy", ALIYUN_ACCESS_KEY_SECRET: "aliyun-sk-lazy", ALIYUN_REGION: "cn-hangzhou" } } + }); + assert.equal(aliyunOnly.workspace.runtime_provider, "aliyun_fc", "Aliyun FC runtime should accept AK/SK provisioning without a pre-created invoke URL"); + assert.equal(aliyunOnly.workspace.sandbox_provider, "aliyun_fc", "Aliyun FC sandbox should accept AK/SK provisioning without a pre-created invoke URL"); + assert.equal(aliyunOnly.workspace.config.cloud_provider_identities.aliyun.services.includes("runtime:aliyun_fc"), true); + assert.equal(aliyunOnly.workspace.config.cloud_provider_identities.aliyun.services.includes("sandbox:aliyun_fc"), true); + const created = await postJson("/v1/workspaces", user.cookie, { tenant_id: tenantId, workspace: { name: `Second ${stamp}`, slug: `cloud-${stamp}-second` },