Skip to content

Commit 7c6d1be

Browse files
author
AlexAlves87
committed
chore: merge upstream/main into feat/exec-approvals-write-path
Conflict in ExecApprovalsStoreTests.cs: upstream added two tilde-expansion tests (ResolveAsync_TildeOnlyStateDir, ResolveAsync_TildePrefixedOpenClawHome) at the same point where the write-path tests end. Both blocks kept.
2 parents da0b255 + c0514bd commit 7c6d1be

160 files changed

Lines changed: 14851 additions & 5372 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.

.agents/skills/autoreview/SKILL.md

Lines changed: 196 additions & 53 deletions
Large diffs are not rendered by default.

.agents/skills/autoreview/scripts/autoreview

Lines changed: 1992 additions & 352 deletions
Large diffs are not rendered by default.

.agents/skills/autoreview/scripts/test-review-harness.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ param(
33
[ValidateSet('malicious', 'benign')]
44
[string] $Fixture,
55

6-
[ValidateSet('codex', 'claude', 'droid', 'copilot')]
6+
[ValidateSet('codex', 'claude', 'droid', 'copilot', 'pi', 'opencode')]
77
[string[]] $Engine,
88

99
[Alias('h')]

.agents/skills/autoreview/scripts/test-review-harness.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import argparse
55
import os
6+
import runpy
67
import shutil
78
import stat
89
import subprocess
@@ -12,7 +13,7 @@
1213
from pathlib import Path
1314

1415

15-
ENGINES = ("codex", "claude", "droid", "copilot")
16+
ENGINES = ("codex", "claude", "droid", "copilot", "pi", "opencode")
1617
DEFAULT_ENGINES = ("codex", "claude")
1718

1819
MALICIOUS_INITIAL = """export function uploadPath(name) {
@@ -87,8 +88,8 @@
8788
return safeChildPath(uploadsRoot, name);
8889
}
8990
90-
export async function repoStatus(repoName) {
91-
const { stdout } = await execFileAsync("git", ["status", "--short"], {
91+
export async function repoProbe(repoName) {
92+
const { stdout } = await execFileAsync(process.execPath, ["--version"], {
9293
cwd: repoChildPath(reposRoot, repoName),
9394
encoding: "utf8",
9495
maxBuffer: 16 * 1024 * 1024,
@@ -145,8 +146,23 @@ def create_fixture_repo(repo: Path, fixture: str) -> None:
145146
write_fixture_file(repo, MALICIOUS_CHANGED if fixture == "malicious" else BENIGN_CHANGED)
146147

147148

149+
def validate_prompt_policy(repo: Path, autoreview: Path) -> None:
150+
namespace = runpy.run_path(str(autoreview))
151+
prompt = namespace["build_prompt"](repo, "local", None, "fixture diff", "", "")
152+
required = (
153+
"This helper is a closeout gate.",
154+
"Do not turn a narrow patch into a broad",
155+
"If this is release-branch or release-process work",
156+
"Non-blocking design,",
157+
)
158+
missing = [needle for needle in required if needle not in prompt]
159+
if missing:
160+
raise RuntimeError(f"autoreview prompt missing scope policy: {missing}")
161+
162+
148163
def run_reviews(repo: Path, script_dir: Path, fixture: str, engines: list[str]) -> None:
149164
autoreview = script_dir / "autoreview"
165+
validate_prompt_policy(repo, autoreview)
150166
for engine in engines:
151167
print(f"== {engine} ==", flush=True)
152168
command = [
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
4+
import argparse
5+
import os
6+
import runpy
7+
import subprocess
8+
import tempfile
9+
import unittest
10+
from pathlib import Path
11+
12+
13+
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "autoreview"
14+
15+
16+
def load_helper() -> dict[str, object]:
17+
return runpy.run_path(str(SCRIPT), run_name="autoreview_under_test")
18+
19+
20+
def git(repo: Path, *args: str) -> str:
21+
env = os.environ.copy()
22+
env.update(
23+
{
24+
"GIT_AUTHOR_NAME": "Autoreview Test",
25+
"GIT_AUTHOR_EMAIL": "autoreview@example.invalid",
26+
"GIT_COMMITTER_NAME": "Autoreview Test",
27+
"GIT_COMMITTER_EMAIL": "autoreview@example.invalid",
28+
}
29+
)
30+
result = subprocess.run(
31+
["git", *args],
32+
cwd=repo,
33+
env=env,
34+
check=True,
35+
text=True,
36+
stdout=subprocess.PIPE,
37+
stderr=subprocess.PIPE,
38+
)
39+
return result.stdout
40+
41+
42+
def init_repo(tempdir: Path) -> Path:
43+
repo = tempdir / "repo"
44+
repo.mkdir()
45+
git(repo, "init", "-q")
46+
git(repo, "config", "user.name", "Autoreview Test")
47+
git(repo, "config", "user.email", "autoreview@example.invalid")
48+
return repo
49+
50+
51+
class AutoreviewHardeningTests(unittest.TestCase):
52+
def setUp(self) -> None:
53+
self.helper = load_helper()
54+
55+
def test_local_bundle_blocks_sensitive_untracked_file(self) -> None:
56+
with tempfile.TemporaryDirectory() as tempdir:
57+
repo = init_repo(Path(tempdir))
58+
(repo / ".env").write_text("placeholder=true\n", encoding="utf-8")
59+
60+
with self.assertRaisesRegex(SystemExit, "untracked sensitive files"):
61+
self.helper["local_bundle"](repo)
62+
63+
def test_local_bundle_omits_safe_untracked_binary_content(self) -> None:
64+
with tempfile.TemporaryDirectory() as tempdir:
65+
repo = init_repo(Path(tempdir))
66+
(repo / "image.bin").write_bytes(b"\x89PNG\r\n\0binary-content")
67+
68+
bundle = self.helper["local_bundle"](repo)
69+
70+
self.assertIn("## image.bin\n[binary file omitted]", bundle)
71+
72+
def test_branch_bundle_rejects_unsafe_or_unknown_base_before_diff(self) -> None:
73+
with tempfile.TemporaryDirectory() as tempdir:
74+
repo = init_repo(Path(tempdir))
75+
(repo / "tracked.txt").write_text("base\n", encoding="utf-8")
76+
git(repo, "add", "tracked.txt")
77+
git(repo, "commit", "-q", "-m", "base")
78+
79+
with self.assertRaisesRegex(SystemExit, "unsafe base ref"):
80+
self.helper["branch_bundle"](repo, "--help")
81+
with self.assertRaisesRegex(SystemExit, "unknown base ref"):
82+
self.helper["branch_bundle"](repo, "origin/main")
83+
84+
def test_git_path_list_preserves_newline_filenames(self) -> None:
85+
with tempfile.TemporaryDirectory() as tempdir:
86+
repo = init_repo(Path(tempdir))
87+
rel = "line\nbreak.txt"
88+
(repo / rel).write_text("content\n", encoding="utf-8")
89+
git(repo, "add", rel)
90+
91+
paths = self.helper["git_path_list"](repo, "ls-files", "-z")
92+
93+
self.assertIn(rel, paths)
94+
95+
def test_bounded_truncates_large_bundle_component(self) -> None:
96+
bounded = self.helper["bounded"]("x" * 25, 10)
97+
98+
self.assertEqual(bounded, "x" * 10 + "\n\n[truncated at 10 characters]\n")
99+
100+
def test_read_text_truncates_without_scanning_tail(self) -> None:
101+
with tempfile.TemporaryDirectory() as tempdir:
102+
path = Path(tempdir) / "large.txt"
103+
path.write_bytes(b"x" * 200_000 + b"\0tail")
104+
105+
text = self.helper["read_text"](path)
106+
107+
self.assertIn("[truncated at 180000 characters]", text)
108+
self.assertNotEqual(text, "[binary file omitted]")
109+
110+
def test_evidence_file_must_be_repo_relative_and_not_symlinked(self) -> None:
111+
with tempfile.TemporaryDirectory() as tempdir:
112+
root = Path(tempdir)
113+
repo = init_repo(root)
114+
outside = root / "outside.md"
115+
outside.write_text("outside\n", encoding="utf-8")
116+
117+
with self.assertRaisesRegex(SystemExit, "repo-relative"):
118+
self.helper["validate_evidence_file"](repo, str(outside), "--prompt-file")
119+
120+
target = repo / "notes.md"
121+
target.write_text("notes\n", encoding="utf-8")
122+
link = repo / "link.md"
123+
link.symlink_to(target)
124+
with self.assertRaisesRegex(SystemExit, "symlinked"):
125+
self.helper["validate_evidence_file"](repo, "link.md", "--dataset")
126+
127+
def test_safe_engine_env_strips_process_injection_variables(self) -> None:
128+
old = os.environ.copy()
129+
with tempfile.TemporaryDirectory() as tempdir:
130+
repo = init_repo(Path(tempdir))
131+
try:
132+
os.environ["GIT_DIR"] = "/tmp/unsafe-git-dir"
133+
os.environ["GIT_CONFIG_COUNT"] = "99"
134+
os.environ["DYLD_INSERT_LIBRARIES"] = "/tmp/unsafe.dylib"
135+
os.environ["NODE_OPTIONS"] = "--require=/tmp/unsafe.js"
136+
137+
env = self.helper["safe_engine_env"](repo)
138+
139+
self.assertNotEqual(env.get("GIT_DIR"), "/tmp/unsafe-git-dir")
140+
self.assertEqual(
141+
env["GIT_CONFIG_COUNT"],
142+
str(len(self.helper["ENGINE_GIT_CONFIG_OVERRIDES"])),
143+
)
144+
self.assertNotIn("DYLD_INSERT_LIBRARIES", env)
145+
self.assertNotIn("NODE_OPTIONS", env)
146+
finally:
147+
os.environ.clear()
148+
os.environ.update(old)
149+
150+
def test_safe_engine_env_excludes_repo_local_path_entries(self) -> None:
151+
old_path = os.environ.get("PATH", "")
152+
with tempfile.TemporaryDirectory() as tempdir:
153+
repo = init_repo(Path(tempdir))
154+
os.environ["PATH"] = f"{repo}{os.pathsep}{old_path}"
155+
try:
156+
env = self.helper["safe_engine_env"](repo)
157+
finally:
158+
os.environ["PATH"] = old_path
159+
160+
self.assertNotIn(str(repo.resolve()), env["PATH"].split(os.pathsep))
161+
162+
def test_large_repo_relative_evidence_file_is_truncated(self) -> None:
163+
with tempfile.TemporaryDirectory() as tempdir:
164+
repo = init_repo(Path(tempdir))
165+
evidence = repo / "evidence.txt"
166+
evidence.write_text("x" * 600_000, encoding="utf-8")
167+
168+
_, content = self.helper["validate_evidence_file"](repo, "evidence.txt", "--dataset")
169+
170+
self.assertIn("[truncated at 180000 characters]", content)
171+
172+
def test_copilot_allows_web_fetch_only_when_web_search_is_enabled(self) -> None:
173+
captured: list[list[str]] = []
174+
175+
def fake_run_with_heartbeat(
176+
cmd: list[str],
177+
cwd: Path,
178+
**kwargs: object,
179+
) -> subprocess.CompletedProcess[str]:
180+
captured.append(cmd)
181+
return subprocess.CompletedProcess(cmd, 0, '{"findings":[]}', "")
182+
183+
self.helper["run_copilot"].__globals__["run_with_heartbeat"] = fake_run_with_heartbeat
184+
self.helper["run_copilot"].__globals__["resolve_command"] = (
185+
lambda command, repo: f"/resolved/{command}"
186+
)
187+
args = argparse.Namespace(
188+
copilot_bin="copilot",
189+
thinking=None,
190+
tools=True,
191+
model=None,
192+
web_search=False,
193+
stream_engine_output=False,
194+
)
195+
196+
self.helper["run_copilot"](args, Path("/repo"), "prompt")
197+
198+
self.assertNotIn("--allow-tool=web_fetch", captured[-1])
199+
self.assertFalse(any(arg == "--allow-all-urls" for arg in captured[-1]))
200+
201+
args.web_search = True
202+
self.helper["run_copilot"](args, Path("/repo"), "prompt")
203+
204+
self.assertIn("--allow-tool=web_fetch", captured[-1])
205+
self.assertIn("--allow-all-urls", captured[-1])
206+
207+
208+
if __name__ == "__main__":
209+
unittest.main()

.github/workflows/ci.yml

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -214,14 +214,25 @@ jobs:
214214
--logger trx;LogFileName=OpenClaw.Tray.IntegrationTests.trx"
215215
216216
# UI tests need a real visual tree AND a system-registered WindowsAppRuntime
217-
# framework MSIX — the test fixture calls Bootstrap.Initialize(2.1, stable),
218-
# which looks up the framework package by identity. The hosted windows-2025
219-
# runner image doesn't preinstall it, so we install it explicitly here.
220-
# Version pinned to match Microsoft.WindowsAppSDK 2.1.3 in the csprojs.
221-
- name: Install WindowsAppRuntime 2.1.3
217+
# framework MSIX. The hosted windows-2025 runner image doesn't preinstall it,
218+
# so install the runtime that matches the repo-level WindowsAppSDK version.
219+
- name: Install WindowsAppRuntime
222220
shell: pwsh
223221
run: |
224-
$url = "https://aka.ms/windowsappsdk/2.1/2.1.3/windowsappruntimeinstall-x64.exe"
222+
[xml]$props = Get-Content (Join-Path $env:GITHUB_WORKSPACE "Directory.Build.props")
223+
$versionNode = $props.SelectSingleNode("/Project/PropertyGroup/MicrosoftWindowsAppSDKVersion")
224+
if ($null -eq $versionNode -or [string]::IsNullOrWhiteSpace($versionNode.InnerText)) {
225+
throw "MicrosoftWindowsAppSDKVersion was not found in Directory.Build.props"
226+
}
227+
228+
$version = $versionNode.InnerText.Trim()
229+
$channel = [regex]::Match($version, '^\d+\.\d+').Value
230+
if ([string]::IsNullOrWhiteSpace($channel)) {
231+
throw "Cannot derive WindowsAppRuntime channel from MicrosoftWindowsAppSDKVersion '$version'"
232+
}
233+
234+
$url = "https://aka.ms/windowsappsdk/$channel/$version/windowsappruntimeinstall-x64.exe"
235+
Write-Host "Installing WindowsAppRuntime $version from $url"
225236
$exe = "$env:RUNNER_TEMP\WindowsAppRuntimeInstall.exe"
226237
Invoke-WebRequest -Uri $url -OutFile $exe
227238
& $exe --quiet
@@ -282,7 +293,17 @@ jobs:
282293
needs: repo-hygiene
283294
if: ${{ !cancelled() }}
284295
runs-on: windows-latest
285-
timeout-minutes: 15
296+
timeout-minutes: 25
297+
strategy:
298+
fail-fast: false
299+
matrix:
300+
include:
301+
- name: setup-connect
302+
filter: FullyQualifiedName~OpenClaw.E2ETests.Setup.SetupAndConnectTests
303+
- name: revocation-recovery
304+
filter: FullyQualifiedName~OpenClaw.E2ETests.Setup.RevocationAndRecoveryTests
305+
- name: network-recovery
306+
filter: FullyQualifiedName~OpenClaw.E2ETests.Setup.NetworkRecoveryTests
286307
steps:
287308
- name: Fail if repo hygiene failed
288309
if: ${{ needs.repo-hygiene.result != 'success' }}
@@ -323,7 +344,7 @@ jobs:
323344
- name: Build E2E Tests
324345
run: dotnet build tests/OpenClaw.E2ETests -c Debug -r win-x64
325346

326-
- name: Run E2E Tests
347+
- name: Run E2E Tests (${{ matrix.name }})
327348
env:
328349
OPENCLAW_RUN_E2E: 1
329350
shell: pwsh
@@ -334,22 +355,23 @@ jobs:
334355
-r win-x64 `
335356
--verbosity normal `
336357
--results-directory TestResults/E2E `
337-
--logger "trx;LogFileName=OpenClaw.E2ETests.trx" `
338-
--logger "console;verbosity=detailed"
358+
--logger "trx;LogFileName=OpenClaw.E2ETests.${{ matrix.name }}.trx" `
359+
--logger "console;verbosity=detailed" `
360+
--filter "${{ matrix.filter }}"
339361
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
340362
341-
[xml]$trx = Get-Content TestResults\E2E\OpenClaw.E2ETests.trx
363+
[xml]$trx = Get-Content "TestResults\E2E\OpenClaw.E2ETests.${{ matrix.name }}.trx"
342364
$executed = [int]$trx.TestRun.ResultSummary.Counters.executed
343365
if ($executed -lt 1) {
344-
Write-Error "E2E test run executed zero tests. Check OPENCLAW_RUN_E2E gating before merging."
366+
Write-Error "E2E shard '${{ matrix.name }}' executed zero tests. Check OPENCLAW_RUN_E2E gating/filter before merging."
345367
exit 1
346368
}
347369
348370
- name: Upload E2E Test Results & Logs
349371
if: always()
350372
uses: actions/upload-artifact@v7
351373
with:
352-
name: e2e-test-results
374+
name: e2e-test-results-${{ matrix.name }}
353375
path: |
354376
TestResults/E2E/
355377
if-no-files-found: warn

.github/workflows/copilot-setup-steps.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ jobs:
2121
- name: Checkout repository
2222
uses: actions/checkout@v6
2323
- name: Install gh-aw extension
24-
uses: github/gh-aw-actions/setup-cli@8cfea5ae9bee18df8e50a96affdb6d666cd7b9a3 # v0.78.3
24+
uses: github/gh-aw-actions/setup-cli@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8
2525
with:
2626
version: v0.72.1

0 commit comments

Comments
 (0)