Skip to content

Commit 9473e6b

Browse files
congxiao-wxxSodawyx
authored andcommitted
Harden runtime export after review
Default export output now omits registry passwords unless the caller explicitly passes --include-secrets. Empty endpoint lists are omitted so apply keeps its default endpoint behavior, and the file-output test closes handles explicitly. Constraint: Review feedback identified secret leakage and empty-endpoint semantic drift risks. Rejected: Always exporting passwords with only documentation warnings | stdout/file exports can leak secrets by default. Confidence: high Scope-risk: narrow Directive: Keep sensitive fields opt-in for future export surfaces. Tested: ruff check src/ tests/; ruff format --check src/ tests/; mypy src/agentrun_cli; pytest tests/integration/test_runtime_cmd.py -v; pytest tests/unit tests/integration --cov=agentrun_cli --cov-fail-under=95 Signed-off-by: congxiao.wxx <congxiao.wxx@alibaba-inc.com> Change-Id: I3fb430af1fc41cf581911a5b84a1dd1c0941760b Co-developed-by: Codex <noreply@openai.com>
1 parent 1a03267 commit 9473e6b

4 files changed

Lines changed: 73 additions & 16 deletions

File tree

docs/en/runtime.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ before `apply`.
161161
## export
162162

163163
```
164-
ar runtime export NAME [-f FILE]
164+
ar runtime export NAME [-f FILE] [--include-secrets]
165165
```
166166

167167
Export an existing Agent Runtime and its endpoints as `ar runtime apply` YAML.
@@ -170,14 +170,16 @@ exports only fields supported by the CLI YAML schema and intentionally omits
170170
server-owned state such as IDs, ARNs, versions, status, and timestamps.
171171
`cloudBuild` cannot be reconstructed from a remote runtime because it depends on
172172
local source/build settings.
173-
If the service returns registry authentication details, the exported YAML may
174-
contain sensitive values; review it before committing or sharing.
173+
Registry authentication secrets such as passwords are omitted by default. Use
174+
`--include-secrets` only when you explicitly need a full-fidelity export, and
175+
review the YAML before committing or sharing it.
175176

176177
### Options
177178

178179
| Flag | Type | Required | Default | Description |
179180
|------|------|----------|---------|-------------|
180181
| `-f`, `--file` | path | no | | Write YAML to a file instead of stdout. |
182+
| `--include-secrets` | flag | no | `false` | Include sensitive registry authentication fields in exported YAML. |
181183

182184
### Examples
183185

docs/zh/runtime.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,20 +152,22 @@ ar runtime render -f FILE
152152
## export
153153

154154
```
155-
ar runtime export NAME [-f FILE]
155+
ar runtime export NAME [-f FILE] [--include-secrets]
156156
```
157157

158158
读取一个存量 Agent Runtime 及其 endpoints,并导出为 `ar runtime apply` 可消费的
159159
YAML。默认输出到 stdout;传入 `--file` 时写入文件。命令只导出当前 CLI YAML
160160
schema 支持的字段,会刻意省略 ID、ARN、version、status、时间戳等服务端状态字段。
161161
`cloudBuild` 依赖本地源码目录和构建参数,无法从远端 runtime 反推,因此不会导出。
162-
如果服务端返回镜像仓库认证信息,导出的 YAML 可能包含敏感值;提交或共享前请先检查。
162+
镜像仓库认证密码等敏感字段默认不导出。只有明确需要完整导出时才使用
163+
`--include-secrets`,提交或共享前请先检查 YAML。
163164

164165
### Options
165166

166167
| Flag | Type | Required | Default | Description |
167168
|------|------|----------|---------|-------------|
168169
| `-f`, `--file` | path | no | | 将 YAML 写入文件,而不是 stdout。 |
170+
| `--include-secrets` | flag | no | `false` | 在导出 YAML 中包含敏感的镜像仓库认证字段。 |
169171

170172
### Examples
171173

src/agentrun_cli/commands/runtime/export_cmd.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,14 @@ def _lazy_sdk() -> Any:
4242
type=click.Path(dir_okay=False, writable=True),
4343
help="Write YAML to a file instead of stdout.",
4444
)
45+
@click.option(
46+
"--include-secrets",
47+
is_flag=True,
48+
help="Include sensitive registry authentication fields in exported YAML.",
49+
)
4550
@click.pass_context
4651
@handle_errors
47-
def export_cmd(ctx, name, file_path):
52+
def export_cmd(ctx, name, file_path, include_secrets):
4853
rt_cls = _lazy_sdk()
4954
profile, region = ctx_cfg(ctx)
5055
build_sdk_config(profile_name=profile, region=region)
@@ -53,7 +58,7 @@ def export_cmd(ctx, name, file_path):
5358
echo_error("ResourceNotFound", f"AgentRuntime {name!r} not found.")
5459
raise SystemExit(EXIT_NOT_FOUND)
5560
try:
56-
data = runtime_to_yaml_doc(runtime)
61+
data = runtime_to_yaml_doc(runtime, include_secrets=include_secrets)
5762
except RuntimeExportError as exc:
5863
echo_error("UnsupportedRuntime", str(exc))
5964
raise SystemExit(EXIT_BAD_INPUT) from exc
@@ -66,7 +71,9 @@ def export_cmd(ctx, name, file_path):
6671
click.echo(text, nl=False)
6772

6873

69-
def runtime_to_yaml_doc(runtime: Any) -> dict[str, Any]:
74+
def runtime_to_yaml_doc(
75+
runtime: Any, *, include_secrets: bool = False
76+
) -> dict[str, Any]:
7077
artifact_type = _enum_value(_get(runtime, "artifact_type", "artifactType"))
7178
if artifact_type and artifact_type != "Container":
7279
raise RuntimeExportError(
@@ -91,7 +98,9 @@ def runtime_to_yaml_doc(runtime: Any) -> dict[str, Any]:
9198
metadata, "workspace", _get(runtime, "workspace_name", "workspaceName")
9299
)
93100

94-
spec: dict[str, Any] = {"container": _export_container(container)}
101+
spec: dict[str, Any] = {
102+
"container": _export_container(container, include_secrets=include_secrets)
103+
}
95104
for yaml_key, attr in [
96105
("cpu", "cpu"),
97106
("memory", "memory"),
@@ -145,7 +154,9 @@ def runtime_to_yaml_doc(runtime: Any) -> dict[str, Any]:
145154
)
146155

147156
if hasattr(runtime, "list_endpoints"):
148-
spec["endpoints"] = [_export_endpoint(ep) for ep in runtime.list_endpoints()]
157+
endpoints = [_export_endpoint(ep) for ep in runtime.list_endpoints()]
158+
if endpoints:
159+
spec["endpoints"] = endpoints
149160

150161
return {
151162
"apiVersion": "agentrun/v1",
@@ -155,7 +166,7 @@ def runtime_to_yaml_doc(runtime: Any) -> dict[str, Any]:
155166
}
156167

157168

158-
def _export_container(container: Any) -> dict[str, Any]:
169+
def _export_container(container: Any, *, include_secrets: bool) -> dict[str, Any]:
159170
out: dict[str, Any] = {"image": _get(container, "image")}
160171
command = _get(container, "command")
161172
if command:
@@ -172,19 +183,23 @@ def _export_container(container: Any) -> dict[str, Any]:
172183
_set_if_present(
173184
out,
174185
"registryConfig",
175-
_export_registry(_get(container, "registry_config", "registryConfig")),
186+
_export_registry(
187+
_get(container, "registry_config", "registryConfig"),
188+
include_secrets=include_secrets,
189+
),
176190
)
177191
return out
178192

179193

180-
def _export_registry(registry: Any) -> dict[str, Any] | None:
194+
def _export_registry(registry: Any, *, include_secrets: bool) -> dict[str, Any] | None:
181195
if registry is None:
182196
return None
183197
out: dict[str, Any] = {}
184198
auth = _get(registry, "auth_config", "authConfig", "auth")
185199
auth_out: dict[str, Any] = {}
186200
_set_if_present(auth_out, "userName", _get(auth, "user_name", "userName"))
187-
_set_if_present(auth_out, "password", _get(auth, "password"))
201+
if include_secrets:
202+
_set_if_present(auth_out, "password", _get(auth, "password"))
188203
_set_if_present(out, "auth", auth_out)
189204

190205
cert = _get(registry, "cert_config", "certConfig", "cert")

tests/integration/test_runtime_cmd.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ def test_export_runtime_outputs_apply_yaml():
729729
}
730730
assert out["spec"]["container"]["image"] == "registry.example.com/ns/app:v1"
731731
assert out["spec"]["container"]["registryConfig"]["auth"]["userName"] == "repo-user"
732-
assert out["spec"]["container"]["registryConfig"]["auth"]["password"] == "repo-pass"
732+
assert "password" not in out["spec"]["container"]["registryConfig"]["auth"]
733733
assert out["spec"]["protocol"]["settings"][0]["path"] == "/invoke"
734734
assert out["spec"]["nas"]["mountPoints"][0]["mountDir"] == "/mnt/nas"
735735
assert out["spec"]["ossMount"]["mountPoints"][0]["bucketName"] == "bucket-1"
@@ -776,10 +776,48 @@ def test_export_runtime_writes_file():
776776
)
777777
assert result.exit_code == 0, result.output
778778
assert result.output == ""
779-
out = yaml.safe_load(open("runtime.yaml", encoding="utf-8"))
779+
with open("runtime.yaml", encoding="utf-8") as f:
780+
out = yaml.safe_load(f)
780781
assert out["metadata"]["name"] == "my-agent"
781782

782783

784+
def test_export_runtime_can_include_secrets_explicitly():
785+
rt = _make_export_runtime()
786+
rt_cls = MagicMock()
787+
rt_cls.list_all.return_value = [rt]
788+
with (
789+
patch(
790+
"agentrun_cli.commands.runtime.export_cmd.build_sdk_config",
791+
return_value=MagicMock(),
792+
),
793+
patch("agentrun_cli.commands.runtime.export_cmd.AgentRuntime", rt_cls),
794+
):
795+
result = CliRunner().invoke(
796+
_root(), ["runtime", "export", "my-agent", "--include-secrets"]
797+
)
798+
assert result.exit_code == 0, result.output
799+
out = yaml.safe_load(result.output)
800+
assert out["spec"]["container"]["registryConfig"]["auth"]["password"] == "repo-pass"
801+
802+
803+
def test_export_runtime_omits_empty_endpoint_list():
804+
rt = _make_export_runtime()
805+
rt.list_endpoints = MagicMock(return_value=[])
806+
rt_cls = MagicMock()
807+
rt_cls.list_all.return_value = [rt]
808+
with (
809+
patch(
810+
"agentrun_cli.commands.runtime.export_cmd.build_sdk_config",
811+
return_value=MagicMock(),
812+
),
813+
patch("agentrun_cli.commands.runtime.export_cmd.AgentRuntime", rt_cls),
814+
):
815+
result = CliRunner().invoke(_root(), ["runtime", "export", "my-agent"])
816+
assert result.exit_code == 0, result.output
817+
out = yaml.safe_load(result.output)
818+
assert "endpoints" not in out["spec"]
819+
820+
783821
def test_export_runtime_not_found_exit_1():
784822
rt_cls = MagicMock()
785823
rt_cls.list_all.return_value = []

0 commit comments

Comments
 (0)