Skip to content

Commit ecf5e14

Browse files
committed
feat: add solution-first selling pipeline
1 parent dbd76e3 commit ecf5e14

241 files changed

Lines changed: 45496 additions & 1789 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitattributes‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Hash-pinned Aliyun metadata and generated catalogs use canonical LF bytes.
22
src/iac_code/tools/cloud/aliyun/data/** text eol=lf
33
tests/tools/cloud/aliyun/fixtures/** text eol=lf
4+
# Pipeline definitions, prompts, and bundled skills are byte-validated release resources.
5+
src/iac_code/pipeline/selling_solution_first/** text eol=lf
46
src/iac_code/web/static/js/vendor/mermaid.min.js -whitespace

‎babel.cfg‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[python: src/iac_code/**.py]
2-
keywords = translate_message
2+
keywords = translate_message CompletionEnrichmentError CompletionValidationError

‎scripts/a2a/e2e/README.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ copy can invalidate the source for the next scenario. It then fixes the server
1313
policy at `300 / 300 / 30`, enables the shared-backup commit protocol, uses
1414
unique Stack/VSwitch names, and performs an exact-name cleanup fallback.
1515

16+
`--run-dir` remains the output root for logs and evidence and may use paths such
17+
as `/tmp/...`. The runner places the Qoder workspace and ROS Agent manager state
18+
under Python's `tempfile.gettempdir()` automatically, because the managed Skill
19+
accepts local manager paths only under the current user's home or
20+
Python-recognized temporary tree. This matters on macOS, where `/tmp` resolves
21+
to `/private/tmp` while Python commonly reports a different per-user temporary
22+
root.
23+
1624
The Qoder flag that bypasses host Bash/file confirmation applies only to the
1725
test driver. It does not approve ROS Agent permissions: the isolated iac-code
1826
settings use the default permission mode, explicitly allow incidental tools,
@@ -99,6 +107,13 @@ credentials. The Sub-Pipeline fixture asserts one denial ToolResult, continued
99107
Agent-loop execution, parent candidate selection/completion, and the absence of
100108
grace, durable permission checkpoints, and permission-critical backup.
101109

110+
The same restart suite also covers all three `selling_solution_first` steps
111+
(`solution_planning_and_selection`, `materialize_selected_candidate`, and
112+
`deploying`) plus normal chat after the pipeline handoff. Every scope runs both
113+
`allow_once` and `deny`, emits exactly one permission, restarts the real HTTP
114+
A2A server before answering, verifies the safe operation/parameter projection,
115+
and uses deterministic local tools only—no cloud write is performed.
116+
102117
This directory contains headless end-to-end checks for A2A pipeline session
103118
recovery and redaction regressions. The runner drives the public A2A JSON-RPC
104119
streaming endpoint and records SSE events and pipeline snapshots. Recovery

‎scripts/a2a/e2e/README.zh-CN.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
一次性副本中刷新并轮换 OAuth refresh token,导致源配置在下一个场景失效。随后把服务端策略固定为
1010
`300 / 300 / 30`,启用共享备份提交协议,使用唯一的 Stack/VSwitch 名称,并在结束时仅按精确名称做兜底清理。
1111

12+
`--run-dir` 仍是日志和证据的输出根目录,可以继续使用 `/tmp/...` 等路径。runner 会自动把 Qoder 工作区和
13+
ROS Agent manager 状态放到 Python 的 `tempfile.gettempdir()` 下,因为托管 Skill 只接受位于当前用户主目录
14+
或 Python 所识别临时目录中的本地 manager 路径。这一点在 macOS 上尤其重要:`/tmp` 会解析为
15+
`/private/tmp`,而 Python 通常返回另一个用户级临时目录。
16+
1217
Qoder 的 host 权限绕过只用于允许测试驱动执行本地 Bash/文件操作,不会批准 ROS Agent 权限。隔离的 iac-code
1318
配置使用默认权限模式,显式允许辅助工具并要求云资源变更工具确认;A2A server 同时保持
1419
`auto_approve_permissions: false`,非只读云操作仍必须通过带完整关联字段的 StartChat 权限回答。
@@ -80,6 +85,12 @@ uv run pytest -q tests/a2a_e2e/test_permission_wait_restart.py
8085
Agent loop 继续、父 Pipeline 进入 candidate 选择并完成,且全程没有 grace、持久化 permission checkpoint
8186
或权限关键备份。
8287

88+
同一套重启测试还覆盖 `selling_solution_first` 的三个步骤
89+
(`solution_planning_and_selection`、`materialize_selected_candidate`、`deploying`)以及
90+
Pipeline handoff 后的普通对话。每个作用域都分别执行 `allow_once` 和 `deny`,严格只触发一次权限,
91+
在回答前重启真实 HTTP A2A server,并校验安全 operation/参数投影。fixture 只执行确定性的本地工具,
92+
不会产生真实云写操作。
93+
8394
本目录包含用于 A2A pipeline 会话恢复和脱敏回归的 headless 端到端检查。Runner 会驱动公开的
8495
A2A JSON-RPC streaming endpoint 并记录 SSE 事件和 pipeline snapshot。恢复场景会用 `SIGKILL`
8596
杀掉 A2A server,再用相同持久化目录重启;`redaction-step4` 则在候选方案选择处停止,不重启、

‎scripts/a2a/e2e/fixtures/backup-delay-sitecustomize/sitecustomize.py‎

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
_CONTROL_ENV = "IAC_CODE_E2E_BACKUP_DELAY_CONTROL"
1717
_ARM_WAIT_SECONDS = 5.0
1818
_claim_lock = threading.Lock()
19-
_claimed = False
19+
_claimed_controls: set[Path] = set()
2020

2121

2222
def _marker_path(control: Path, marker: str) -> Path:
@@ -31,8 +31,6 @@ def _write_marker(path: Path, payload: dict[str, Any]) -> None:
3131

3232

3333
def _claim_delay(reason: Any) -> tuple[Path, float, float] | None:
34-
global _claimed
35-
3634
reason_value = getattr(reason, "value", reason)
3735
if reason_value != session_backup.BackupReason.INPUT_REQUIRED.value:
3836
return None
@@ -44,17 +42,25 @@ def _claim_delay(reason: Any) -> tuple[Path, float, float] | None:
4442
if not control_value or delay_seconds <= 0:
4543
return None
4644

47-
control = Path(control_value)
48-
arm_path = _marker_path(control, "arm")
49-
with _claim_lock:
50-
if _claimed:
51-
return None
52-
deadline = time.monotonic() + _ARM_WAIT_SECONDS
53-
while not arm_path.is_file() and time.monotonic() < deadline:
45+
configured_control = Path(control_value)
46+
deadline = time.monotonic() + _ARM_WAIT_SECONDS
47+
control: Path | None = None
48+
while control is None and time.monotonic() < deadline:
49+
if configured_control.is_dir():
50+
candidates = sorted(
51+
arm_path.with_name(arm_path.name.removesuffix(".arm.json"))
52+
for arm_path in configured_control.glob("backup-delay-*.arm.json")
53+
)
54+
else:
55+
candidates = [configured_control] if _marker_path(configured_control, "arm").is_file() else []
56+
with _claim_lock:
57+
control = next((candidate for candidate in candidates if candidate not in _claimed_controls), None)
58+
if control is not None:
59+
_claimed_controls.add(control)
60+
if control is None:
5461
time.sleep(0.02)
55-
if not arm_path.is_file():
56-
return None
57-
_claimed = True
62+
if control is None:
63+
return None
5864

5965
started_at = time.time()
6066
started_monotonic = time.monotonic()

0 commit comments

Comments
 (0)