Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions apps/control-plane-api/src/routes/workspaceRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ async function validateWorkspaceAliyunCredentials(providerCredentials: Record<st

type RuntimeProviderInput = "vefaas" | "local_docker" | "aliyun_fc";
type SandboxProviderInput = "e2b" | "vefaas" | "local_docker" | "daytona" | "aliyun_fc";
type ProviderPoolInput = Array<{ provider?: string }>;
type ProviderPoolInput = Array<{ provider?: string; config?: Record<string, unknown> }>;

function missingTenantCloudProviderAccess(tenantId: string, runtimeProvider: RuntimeProviderInput, sandboxProvider: SandboxProviderInput, runtimePools: ProviderPoolInput = [], sandboxPools: ProviderPoolInput = [], artifactProvider?: "tos" | "oss") {
if (!tenantId) return [];
Expand Down Expand Up @@ -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 ??

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require credentials for the selected Aliyun sandbox pool

This treats an invoke_url on any Aliyun sandbox pool as satisfying the credential requirement for all Aliyun FC sandbox usage. If the selected/primary Aliyun pool has no config.invoke_url but a standby Aliyun pool does, validation passes without AK/SK; later workspaceSandboxRuntimeConfig() uses the selected provider's first pool config and ensureAliyunFcSandboxProviderReady() attempts lazy deployment with empty credentials, so workspace provisioning fails instead of returning provider_credentials_required.

Useful? React with 👍 / 👎.

""
);
if (runtimeProviders.has("vefaas") || artifactProvider === "tos") {
required.push(
["VOLCENGINE_ACCESS_KEY", vefaasCreds.VOLCENGINE_ACCESS_KEY],
Expand All @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions apps/control-plane-api/src/runtime/aliyunFcRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
2 changes: 2 additions & 0 deletions apps/control-plane-api/src/runtime/vefaasAgentRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore SDK install for Aliyun source runtimes

When the built-in Aliyun deploy path is used, it packages infra/vefaas/runtime-app as a source zip and passes this env to the new function. That package's run.sh only installs requirements.txt when MAPLE_SKIP_CLAUDE_AGENT_SDK_INSTALL is not true, but the default anthropic_claude_code runtime path in app.py still requires import claude_agent_sdk; with the default source package there is no vendored SDK, so newly provisioned Aliyun FC runtime workspaces fail on the first agent run unless every agent is forced to a non-SDK protocol or a pre-baked source is supplied.

Useful? React with 👍 / 👎.

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 {
Expand Down Expand Up @@ -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),
Expand Down
10 changes: 9 additions & 1 deletion infra/aliyun/deploy_aliyun_fc_runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions infra/vefaas/deploy_vefaas_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
3 changes: 3 additions & 0 deletions tests/contracts/aliyun_fc_deploy_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import {
AliyunFcRuntimeDeployer,
createAliyunFcClient,
createHttpTriggerRequest,
resolveFcEndpointConfig,
resolveDeployConfig
Expand Down Expand Up @@ -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<Record<string, unknown>> = [];
const fakeClient = {
Expand Down Expand Up @@ -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/);
Expand Down
19 changes: 19 additions & 0 deletions tests/contracts/tenant_cloud_provider_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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` },
Expand Down
Loading