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
44 changes: 35 additions & 9 deletions frontend/server/deployment_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,16 @@ class DeploymentSourceError(ValueError):
"""Deployment source does not satisfy the trusted package contract."""


def ensure_default_agentkit_dockerfile(base: Path, cloud_provider: str) -> bool:
"""Add Studio's canonical Dockerfile when a deployment has no custom one."""
def ensure_default_agentkit_dockerfile(
base: Path,
cloud_provider: str,
*,
entry_point: str | None = None,
) -> bool:
"""Add Studio's canonical Dockerfile when a deployment has no custom one.

When provided, ``entry_point`` is used as the Python startup script.
"""

dockerfile = base / "Dockerfile"
if dockerfile.exists():
Expand All @@ -59,7 +67,10 @@ def ensure_default_agentkit_dockerfile(base: Path, cloud_provider: str) -> bool:
from veadk.cli.generated_agent_codegen import render_default_agentkit_dockerfile

dockerfile.write_text(
render_default_agentkit_dockerfile(cloud_provider),
render_default_agentkit_dockerfile(
cloud_provider,
entry_point=entry_point,
),
encoding="utf-8",
)
return True
Expand Down Expand Up @@ -102,26 +113,26 @@ def _is_macos_metadata(relative: str) -> bool:
)


def _configured_entry_point(base: Path) -> str:
def _configured_entry_point(base: Path, *, fallback: str) -> str:
manifest_path = base / "agentkit.yaml"
if not manifest_path.is_file():
return _DEFAULT_ENTRY_POINT
return fallback
try:
manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, yaml.YAMLError) as error:
raise DeploymentSourceError(f"agentkit.yaml 无法解析:{error}") from error
if manifest is None:
return _DEFAULT_ENTRY_POINT
return fallback
if not isinstance(manifest, Mapping):
raise DeploymentSourceError("agentkit.yaml 根节点必须是对象。")
common = manifest.get("common")
if common is None:
return _DEFAULT_ENTRY_POINT
return fallback
if not isinstance(common, Mapping):
raise DeploymentSourceError("agentkit.yaml 的 common 必须是对象。")
value = common.get("entry_point")
if value is None:
return _DEFAULT_ENTRY_POINT
return fallback
return _relative_path(value, field="agentkit.yaml common.entry_point")


Expand All @@ -132,6 +143,20 @@ def _require_entry_point(base: Path, entry_point: str) -> str:
return entry_point


def resolve_agentkit_entry_point(
base: Path,
*,
fallback: str = _DEFAULT_ENTRY_POINT,
) -> str:
"""Resolve and validate the root AgentKit manifest's Python entry point."""

safe_fallback = _relative_path(fallback, field="部署入口文件")
return _require_entry_point(
base,
_configured_entry_point(base, fallback=safe_fallback),
)


def _reject_path_collisions(paths: set[str]) -> None:
for relative in paths:
path = PurePosixPath(relative)
Expand Down Expand Up @@ -211,7 +236,7 @@ def write_inline_source(base: Path, files: object) -> str:
target = _target(base, relative)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
return _require_entry_point(base, _configured_entry_point(base))
return resolve_agentkit_entry_point(base)


def _manifest_files(manifest: object) -> tuple[dict[str, tuple[int, str]], str]:
Expand Down Expand Up @@ -306,5 +331,6 @@ def extract_migration_source(
__all__ = [
"DeploymentSourceError",
"extract_migration_source",
"resolve_agentkit_entry_point",
"write_inline_source",
]
8 changes: 8 additions & 0 deletions frontend/server/intelligent_development_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
IntelligentDevelopmentVersion,
IntelligentDevelopmentVersionIntegrityError,
IntelligentDevelopmentVersionNotFound,
SourceVersionProducer,
)
from frontend.server.sandbox_remote import SandboxRemoteTransport
from frontend.server.source_project_limits import (
Expand Down Expand Up @@ -107,6 +108,7 @@ class TrustedDeploymentSource:
environment_required: tuple[str, ...] = ()
environment_optional: tuple[str, ...] = ()
environment_defaults: tuple[tuple[str, str], ...] = ()
producer: SourceVersionProducer = "intelligent-development"


@dataclass(frozen=True)
Expand Down Expand Up @@ -362,6 +364,7 @@ async def _materialize_intelligent_development_source(
tuple(stored_metadata.environment.required),
tuple(stored_metadata.environment.optional),
tuple(sorted(stored_metadata.environment.defaults.items())),
producer=stored_metadata.producer,
),
artifact,
)
Expand Down Expand Up @@ -466,6 +469,11 @@ async def _materialize_intelligent_development_source(
source_files,
project_id if isinstance(project_id, str) else "",
version_id if isinstance(version_id, str) else "",
producer=(
stored_metadata.producer
if stored_metadata is not None
else "intelligent-development"
),
),
artifact,
)
Expand Down
5 changes: 5 additions & 0 deletions frontend/tests/migrationWorkspace.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ test("keeps new migration and migrated projects as parallel workspace pages", ()
assert.match(projects, /onCreate=\{onOptimize\}/);
assert.match(projects, /onDownload=\{onDownload\}/);
assert.match(projects, /onDeploy=\{onDeploy\}/);
assert.match(source, /migrationTaskId: task\.id/);
assert.match(
appSource,
/onDeploySavedVersion=\{\(delivery\) => \{[\s\S]*?setIntelligentDeployment\(delivery\)/,
);
assert.match(
appSource,
/initialPage=\{migrationProjectReturn \? "projects" : "new"\}/,
Expand Down
1 change: 0 additions & 1 deletion reference/actb-mono
Submodule actb-mono deleted from 7d6536
170 changes: 166 additions & 4 deletions tests/cli/test_studio_rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -1402,10 +1402,13 @@ def test_code_package_manifest_entry_point_reaches_agentkit_sdk(
tmp_path: Path,
) -> None:
captured_config: dict[str, Any] = {}
captured_dockerfile = ""

def launch(*, config_file: str, **_kwargs: Any) -> SimpleNamespace:
nonlocal captured_dockerfile
config_path = Path(config_file)
captured_config.update(yaml.safe_load(config_path.read_text()))
captured_dockerfile = (config_path.parent / "Dockerfile").read_text()
assert (config_path.parent / "runtime" / "main.py").read_text() == (
"app = object()\n"
)
Expand Down Expand Up @@ -1462,6 +1465,7 @@ def launch(*, config_file: str, **_kwargs: Any) -> SimpleNamespace:
assert response.status_code == 200
assert frames[-1]["success"] is True
assert captured_config["common"]["entry_point"] == "runtime/main.py"
assert 'CMD ["python", "-m", "app"]' in captured_dockerfile


def test_migration_deployment_materializes_owned_session_source_server_side(
Expand All @@ -1472,6 +1476,7 @@ def test_migration_deployment_materializes_owned_session_source_server_side(
from veadk.config import veadk_environments

captured_config: dict[str, Any] = {}
captured_dockerfile = ""
materialized: dict[str, str] = {}

def materialize(
Expand All @@ -1481,15 +1486,42 @@ def materialize(
target: Path,
) -> str:
materialized.update(task_id=task_id, owner_id=owner_id)
entry = target / "runtime" / "migrated.py"
entry.parent.mkdir(parents=True)
entry.write_text("app = object()\n", encoding="utf-8")
configured_entry = target / "bailian-test-workflow-agent.py"
configured_entry.write_text("app = object()\n", encoding="utf-8")
startup_entry = target / "runtime" / "migrated.py"
startup_entry.parent.mkdir(parents=True)
startup_entry.write_text("app = object()\n", encoding="utf-8")
(target / "agentkit.yaml").write_text(
"common:\n"
" agent_name: bailian-test-workflow-agent\n"
" entry_point: bailian-test-workflow-agent.py\n"
" description: AgentKit project bailian-test-workflow-agent - Agent Server App\n"
" language: Python\n"
' language_version: "3.12"\n'
" agent_type: WebServer App\n"
" dependencies_file: requirements.txt\n"
" launch_type: cloud\n",
encoding="utf-8",
)
nested_dockerfile = target / ".agentkit" / "Dockerfile"
nested_dockerfile.parent.mkdir()
nested_dockerfile.write_text(
'FROM example.com/nested:latest\nCMD ["python", "wrong.py"]\n',
encoding="utf-8",
)
return "runtime/migrated.py"

def launch(*, config_file: str, **_kwargs: Any) -> SimpleNamespace:
nonlocal captured_dockerfile
config_path = Path(config_file)
captured_config.update(yaml.safe_load(config_path.read_text()))
captured_dockerfile = (config_path.parent / "Dockerfile").read_text()
assert (config_path.parent / "bailian-test-workflow-agent.py").is_file()
assert (config_path.parent / "runtime" / "migrated.py").is_file()
assert (
'CMD ["python", "wrong.py"]'
in (config_path.parent / ".agentkit" / "Dockerfile").read_text()
)
assert not (config_path.parent / "browser.py").exists()
return SimpleNamespace(
success=True,
Expand Down Expand Up @@ -1560,7 +1592,8 @@ def launch(*, config_file: str, **_kwargs: Any) -> SimpleNamespace:
"task_id": "migration-v1-" + "1" * 32,
"owner_id": "developer",
}
assert captured_config["common"]["entry_point"] == "runtime/migrated.py"
assert captured_config["common"]["entry_point"] == "bailian-test-workflow-agent.py"
assert 'CMD ["python", "bailian-test-workflow-agent.py"]' in captured_dockerfile
runtime_envs = captured_config["launch_types"]["cloud"]["runtime_envs"]
assert runtime_envs["MODEL_AGENT_NAME"] == "doubao-seed-2-1-pro-260628"
assert runtime_envs["MODEL_NAME"] == "doubao-seed-2-1-pro-260628"
Expand All @@ -1569,6 +1602,135 @@ def launch(*, config_file: str, **_kwargs: Any) -> SimpleNamespace:
)


@pytest.mark.parametrize(
("producer", "expected_command"),
[
(
"migration",
'CMD ["python", "bailian-test-workflow-agent.py"]',
),
(
"intelligent-development",
'CMD ["python", "-m", "app"]',
),
],
)
def test_saved_project_deployment_scopes_manifest_entry_point_to_migrations(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
producer: str,
expected_command: str,
) -> None:
from veadk.config import veadk_environments

captured_config: dict[str, Any] = {}
captured_dockerfile = ""
materialized: dict[str, str] = {}

async def materialize(
target: Path,
source: dict[str, str],
*,
owner_id: str,
**_kwargs: Any,
) -> SimpleNamespace:
materialized.update(
kind=source["kind"],
project_id=source["projectId"],
version_id=source["versionId"],
owner_id=owner_id,
)
configured_entry = target / "bailian-test-workflow-agent.py"
configured_entry.write_text("app = object()\n", encoding="utf-8")
startup_entry = target / "runtime" / "migrated.py"
startup_entry.parent.mkdir()
startup_entry.write_text("app = object()\n", encoding="utf-8")
(target / "agentkit.yaml").write_text(
"common:\n"
" agent_name: bailian-test-workflow-agent\n"
" entry_point: bailian-test-workflow-agent.py\n",
encoding="utf-8",
)
return SimpleNamespace(
agent_name="bailian-test-workflow-agent",
entry_point="runtime/migrated.py",
producer=producer,
)

def launch(*, config_file: str, **_kwargs: Any) -> SimpleNamespace:
nonlocal captured_dockerfile
config_path = Path(config_file)
captured_config.update(yaml.safe_load(config_path.read_text()))
captured_dockerfile = (config_path.parent / "Dockerfile").read_text()
assert (config_path.parent / "bailian-test-workflow-agent.py").is_file()
return SimpleNamespace(
success=True,
error=None,
deploy_result=SimpleNamespace(
endpoint_url="https://runtime.example.com",
metadata={
"runtime_id": "runtime-saved-migration",
"runtime_name": "saved-migration-agent",
"runtime_endpoint": "https://runtime.example.com",
"runtime_apikey": "secret",
},
),
)

monkeypatch.setattr(
"frontend.server.intelligent_development_source."
"materialize_intelligent_development_source",
materialize,
)
monkeypatch.setattr("agentkit.toolkit.sdk.launch", launch)
monkeypatch.setitem(veadk_environments, "MODEL_AGENT_API_KEY", "test-model-key")
app = _create_studio_app(monkeypatch, tmp_path, developers="developer")

with (
TestClient(app) as client,
client.stream(
"POST",
"/web/deploy-agentkit",
headers={"X-VeADK-Local-User": "developer"},
json={
"name": "saved-migration-agent",
"files": [],
"source": {
"kind": "intelligentDevelopment",
"sessionId": "session-saved-migration",
"projectId": "project-saved-migration",
"versionId": "version-saved-migration",
"artifactSha256": "a" * 64,
"validationReportSha256": "b" * 64,
},
"config": {"region": "cn-beijing", "projectName": "default"},
"createEvaluationSets": False,
},
) as response,
):
frames = [
json.loads(line.removeprefix("data: "))
for line in response.iter_lines()
if line.startswith("data: ")
]

assert response.status_code == 200
assert frames[-1]["success"] is True
assert materialized == {
"kind": "intelligentDevelopment",
"project_id": "project-saved-migration",
"version_id": "version-saved-migration",
"owner_id": "developer",
}
expected_entry_point = (
"bailian-test-workflow-agent.py"
if producer == "migration"
else "runtime/migrated.py"
)
assert captured_config["common"]["entry_point"] == expected_entry_point
assert expected_command in captured_dockerfile


def test_migration_deployment_rejection_removes_temporary_source(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
Expand Down
Loading
Loading