NullVoider commited on
Commit
2134652
·
verified ·
1 Parent(s): 50a8a77

Upload folder using huggingface_hub

Browse files
scripts/entrypoint.sh ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # --- Configuration ---
5
+ SOURCE_IMG="/vm/source.qcow2"
6
+ EPISODE_DISK="/run/episode.qcow2"
7
+ VARS_TEMPLATE="/usr/share/OVMF/OVMF_VARS_4M.fd"
8
+ OVMF_CODE="/usr/share/OVMF/OVMF_CODE_4M.fd"
9
+ DATA_DIR="/run/storage"
10
+ mkdir -p "$DATA_DIR"
11
+ VARS_FILE="$DATA_DIR/OVMF_VARS.fd"
12
+
13
+ export QEMU_AUDIO_DRV=none
14
+
15
+ echo "--- Windows 11 Standard Boot ---"
16
+
17
+ # 1. Create Ephemeral Overlay
18
+ echo "Creating ephemeral overlay..."
19
+ qemu-img create -f qcow2 -b "$SOURCE_IMG" -F qcow2 "$EPISODE_DISK"
20
+
21
+ # 2. Prepare UEFI Variables
22
+ if [ ! -f "$VARS_FILE" ]; then
23
+ echo "Initializing UEFI variables..."
24
+ cp "$VARS_TEMPLATE" "$VARS_FILE"
25
+ fi
26
+
27
+ # 2. Start TPM Emulator
28
+ mkdir -p /run/tpm
29
+ swtpm socket --tpmstate dir=/run/tpm --ctrl type=unixio,path=/run/tpm/swtpm-sock --tpm2 -d
30
+ sleep 1
31
+
32
+ # 3. Start Web Viewer
33
+ echo "Starting web viewer..."
34
+ websockify -D --web=/usr/share/novnc/ 8006 localhost:5900
35
+
36
+ echo "Booting Windows 11..."
37
+
38
+ # 5. Launch QEMU
39
+ exec qemu-system-x86_64 \
40
+ -enable-kvm \
41
+ -m 8G \
42
+ -smp 4,cores=4,threads=1,sockets=1 \
43
+ -machine q35,accel=kvm \
44
+ -boot menu=on,splash-time=0 \
45
+ -cpu host,hv_relaxed,hv_spinlocks=0x1fff,hv_vapic,hv_time,+invtsc \
46
+ -device intel-hda -device hda-output,audiodev=nomix \
47
+ -audiodev id=nomix,driver=none \
48
+ -drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
49
+ -drive if=pflash,format=raw,file="$VARS_FILE" \
50
+ -chardev socket,id=chrtpm,path=/run/tpm/swtpm-sock \
51
+ -tpmdev emulator,id=tpm0,chardev=chrtpm \
52
+ -device tpm-tis,tpmdev=tpm0 \
53
+ -device virtio-balloon-pci,free-page-reporting=on,deflate-on-oom=on \
54
+ -vga std \
55
+ -object iothread,id=iothread0 \
56
+ -device virtio-scsi-pci,id=scsi0,iothread=iothread0,num_queues=4 \
57
+ -drive file="$EPISODE_DISK",format=qcow2,if=none,id=disk0,cache=writeback,aio=threads,discard=unmap,l2-cache-size=4M \
58
+ -device scsi-hd,drive=disk0,bootindex=1,rotation_rate=1 \
59
+ -netdev user,id=net0,hostfwd=tcp::3389-:3389,hostfwd=tcp::2222-:22,hostfwd=tcp::9090-:9090 \
60
+ -device virtio-net-pci,netdev=net0,id=net0,romfile="" \
61
+ -vnc 0.0.0.0:0 \
62
+ -usb \
63
+ -device usb-kbd \
64
+ -device usb-tablet \
65
+ -monitor tcp:0.0.0.0:4444,server,nowait \
66
+ -qmp tcp:0.0.0.0:4445,server,nowait
scripts/setup-win11.ps1 ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # setup-win11.ps1
3
+ # One-command clone + automatic download of win11.qcow2 into win11-image/
4
+ # =============================================================================
5
+
6
+ $ErrorActionPreference = 'Stop' # Exit immediately if any command fails
7
+
8
+ $GithubRepo = "https://github.com/nullvoider07/windows11-base"
9
+ $RepoName = Split-Path $GithubRepo -Leaf
10
+
11
+ Write-Host "🚀 Cloning GitHub repo: $GithubRepo"
12
+
13
+ # ----------------------------- Clone with GitHub CLI -----------------------
14
+ Write-Host "🔧 Checking GitHub CLI..."
15
+
16
+ if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
17
+ Write-Host " GitHub CLI not found. Installing via winget..."
18
+
19
+ # Install gh silently, accepting agreements automatically
20
+ winget install --id GitHub.cli --exact --accept-source-agreements --accept-package-agreements
21
+
22
+ # Refresh the PATH variables in the current session so 'gh' is recognized immediately
23
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
24
+
25
+ # Verify installation succeeded
26
+ if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
27
+ throw "❌ Failed to install GitHub CLI automatically. Please install it manually from https://cli.github.com"
28
+ }
29
+ Write-Host " ✅ GitHub CLI installed successfully."
30
+ } else {
31
+ Write-Host " ✅ GitHub CLI already available."
32
+ }
33
+
34
+ gh repo clone $GithubRepo $RepoName -- --depth=1
35
+
36
+ # ----------------------------- Create folder -------------------------------
37
+ Write-Host "📁 Creating folder: win11-image/"
38
+ $ImagePath = Join-Path $RepoName "win11-image"
39
+ New-Item -ItemType Directory -Force -Path $ImagePath | Out-Null
40
+
41
+ # ----------------------------- Ensure uv is available ---------------------
42
+ Write-Host "🔧 Checking uv..."
43
+
44
+ if (Get-Command uv -ErrorAction SilentlyContinue) {
45
+ Write-Host " uv already available — skipping installation."
46
+ } else {
47
+ Write-Host " Installing uv package manager..."
48
+ Invoke-RestMethod -Uri https://astral.sh/uv/install.ps1 | Invoke-Expression
49
+ $env:Path += ";$HOME\.cargo\bin;$HOME\.local\bin"
50
+ }
51
+
52
+ # ----------------------------- Ephemeral venv for huggingface-cli ----------
53
+ Write-Host "🔧 Creating ephemeral venv for huggingface-cli..."
54
+
55
+ $TempDir = [System.IO.Path]::GetTempPath()
56
+ $HfVenv = Join-Path $TempDir "hf-venv-$(New-Guid)"
57
+
58
+ # uv venv picks the correct current Python automatically
59
+ uv venv $HfVenv --quiet
60
+
61
+ # Install directly into the venv — no --system, no activation needed
62
+ uv pip install --python "$HfVenv\Scripts\python.exe" transformers --quiet
63
+
64
+ Write-Host " ✅ huggingface-hub installed in ephemeral venv."
65
+
66
+ # ----------------------------- Download QCOW2 ------------------------------
67
+ Write-Host "📥 Downloading win11.qcow2 (large file) into $RepoName\win11-image\ ..."
68
+ Write-Host " (This may take a while — progress bar will show)"
69
+
70
+ # Call huggingface-cli directly by its venv path — no PATH lookup, no cache
71
+ & "$HfVenv\Scripts\hf.exe" download NullVoider/windows11-base win11.qcow2 --local-dir $ImagePath
72
+
73
+ # ----------------------------- Cleanup venv --------------------------------
74
+ Write-Host "🧹 Cleaning up ephemeral venv..."
75
+ Remove-Item -Recurse -Force $HfVenv
76
+
77
+ # ----------------------------- Final message -------------------------------
78
+ Write-Host ""
79
+ Write-Host "✅ SUCCESS!" -ForegroundColor Green
80
+ Write-Host " Repository cloned → $RepoName\"
81
+ Write-Host " QCOW2 image ready at: $RepoName\win11-image\win11.qcow2"
82
+ Write-Host ""
83
+ Write-Host " Next time just run: cd $RepoName ; git pull"
scripts/setup-win11.sh ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # setup-win11.sh
4
+ # One-command clone + automatic download of win11.qcow2 into win11-image/
5
+ # =============================================================================
6
+
7
+ set -e # Exit immediately if any command fails
8
+
9
+ GITHUB_REPO="https://github.com/nullvoider07/windows11-base"
10
+ REPO_NAME=$(basename "$GITHUB_REPO")
11
+
12
+ echo "🚀 Cloning GitHub repo: $GITHUB_REPO"
13
+
14
+ # ----------------------------- Clone with GitHub CLI -----------------------
15
+ echo "🔧 Checking GitHub CLI..."
16
+
17
+ if ! command -v gh >/dev/null 2>&1; then
18
+ echo " GitHub CLI not found. Attempting to install..."
19
+
20
+ # Try Homebrew (macOS/Linux)
21
+ if command -v brew >/dev/null 2>&1; then
22
+ brew install gh
23
+ # Try APT (Debian/Ubuntu)
24
+ elif command -v apt-get >/dev/null 2>&1; then
25
+ curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && \
26
+ sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && \
27
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null && \
28
+ sudo apt-get update && sudo apt-get install gh -y
29
+ else
30
+ echo "❌ Unsupported package manager. Please install GitHub CLI manually:"
31
+ echo " https://cli.github.com"
32
+ exit 1
33
+ fi
34
+ else
35
+ echo " ✅ GitHub CLI already available."
36
+ fi
37
+
38
+ gh repo clone "$GITHUB_REPO" "$REPO_NAME" -- --depth=1
39
+
40
+ # ----------------------------- Create folder -------------------------------
41
+ echo "📁 Creating folder: win11-image/"
42
+ mkdir -p "$REPO_NAME/win11-image"
43
+
44
+ # ----------------------------- Ensure uv is available ---------------------
45
+ echo "🔧 Checking uv..."
46
+
47
+ if command -v uv >/dev/null 2>&1; then
48
+ echo " uv already available — skipping installation."
49
+ else
50
+ echo " Installing uv package manager..."
51
+ curl -LsSf https://astral.sh/uv/install.sh | sh
52
+ export PATH="$HOME/.cargo/bin:$HOME/.local/bin:$PATH"
53
+ hash -r
54
+ fi
55
+
56
+ # ----------------------------- Ephemeral venv for huggingface-cli ----------
57
+ # Create a temporary venv, install huggingface-hub into it, run the download,
58
+ # then delete the venv. This avoids all PATH/hash-cache/interpreter-mismatch
59
+ # issues caused by stale system-wide or tool-level installs.
60
+ echo "🔧 Creating ephemeral venv for huggingface-cli..."
61
+
62
+ HF_VENV="$(mktemp -d)/hf-venv"
63
+
64
+ # uv venv picks the correct current Python automatically
65
+ uv venv "$HF_VENV" --quiet
66
+
67
+ # Install directly into the venv — no --system, no activation needed
68
+ uv pip install --python "$HF_VENV/bin/python" transformers --quiet
69
+
70
+ echo " ✅ huggingface-hub installed in ephemeral venv."
71
+
72
+ # ----------------------------- Download QCOW2 ------------------------------
73
+ echo "📥 Downloading win11.qcow2 (large file) into $REPO_NAME/win11-image/ ..."
74
+ echo " (This may take a while — progress bar will show)"
75
+
76
+ # Call huggingface-cli directly by its venv path — no PATH lookup, no cache
77
+ "$HF_VENV/bin/hf" download NullVoider/windows11-base win11.qcow2 \
78
+ --local-dir "$REPO_NAME/win11-image"
79
+
80
+ # ----------------------------- Cleanup venv --------------------------------
81
+ echo "🧹 Cleaning up ephemeral venv..."
82
+ rm -rf "$HF_VENV"
83
+
84
+ # ----------------------------- Final message -------------------------------
85
+ echo ""
86
+ echo "✅ SUCCESS!"
87
+ echo " Repository cloned → $REPO_NAME/"
88
+ echo " QCOW2 image ready at: $REPO_NAME/win11-image/win11.qcow2"
89
+ echo ""
90
+ echo " Next time just run: cd $REPO_NAME && git pull"
scripts/task_executor.py ADDED
@@ -0,0 +1,604 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ task_executor_windows.py — REST API task executor for the Windows AI Agent environment.
3
+ """
4
+
5
+ import difflib
6
+ import logging
7
+ import os
8
+ import re
9
+ import shutil
10
+ import subprocess
11
+ import threading
12
+ import time
13
+ import uuid
14
+ from http import HTTPStatus
15
+
16
+ from flask import Flask, jsonify, request
17
+ from waitress import serve
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Configuration
21
+ # ---------------------------------------------------------------------------
22
+
23
+ TASK_BASE_DIR = os.environ.get("TASK_BASE_DIR", r"C:\Users\AgentUser\tasks")
24
+ API_PORT = int(os.environ.get("API_PORT", "9090"))
25
+ API_TOKEN = os.environ.get("API_TOKEN", "")
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Logging → C:\Users\AgentUser\tasks\task_executor.log
29
+ # ---------------------------------------------------------------------------
30
+ os.makedirs(TASK_BASE_DIR, exist_ok=True)
31
+ LOG_FILE = os.path.join(TASK_BASE_DIR, "task_executor.log")
32
+
33
+ logging.basicConfig(
34
+ level=logging.INFO,
35
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
36
+ handlers=[
37
+ logging.FileHandler(LOG_FILE, encoding="utf-8"),
38
+ logging.StreamHandler(),
39
+ ],
40
+ )
41
+ log = logging.getLogger("task_executor")
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # In-memory task store
45
+ # ---------------------------------------------------------------------------
46
+ _tasks: dict[str, dict] = {}
47
+ _tasks_lock = threading.Lock()
48
+ _TASK_MAX_AGE = int(os.environ.get("TASK_MAX_AGE", "3600")) # 1 hour default
49
+
50
+
51
+ def _evict_old_tasks() -> None:
52
+ """Drop completed/failed tasks older than TASK_MAX_AGE seconds."""
53
+ cutoff = time.monotonic() - _TASK_MAX_AGE
54
+ with _tasks_lock:
55
+ stale = [
56
+ tid for tid, t in _tasks.items()
57
+ if t["status"] not in ("pending", "running")
58
+ and t.get("_created", 0) < cutoff
59
+ ]
60
+ for tid in stale:
61
+ _tasks.pop(tid)
62
+
63
+ app = Flask(__name__)
64
+
65
+ def _check_auth() -> bool:
66
+ """Return True if the request is authorised (or auth is disabled)."""
67
+ if not API_TOKEN:
68
+ return True # auth disabled if no token configured
69
+ auth = request.headers.get("Authorization", "")
70
+ return auth == f"Bearer {API_TOKEN}"
71
+
72
+ # ===========================================================================
73
+ # Custom exceptions
74
+ # ===========================================================================
75
+
76
+ class _TaskTimeoutError(RuntimeError):
77
+ """Raised by _run() when a subprocess exceeds its allotted time."""
78
+
79
+
80
+ # ===========================================================================
81
+ # Subprocess helper
82
+ # ===========================================================================
83
+
84
+ def _run(
85
+ command: list[str] | str,
86
+ cwd: str | None = None,
87
+ timeout: int = 120,
88
+ shell: bool = False,
89
+ ) -> tuple[int, str, str]:
90
+ """
91
+ Run a command on Windows with a new process group so the entire child
92
+ tree can be killed on timeout via taskkill /F /T /PID.
93
+
94
+ • list[str] → all internal commands (git clone/checkout/apply/diff).
95
+ argv passed directly; no shell, no injection.
96
+ • shell=True → user-supplied test_command and lint_command only.
97
+
98
+ Windows-specific notes:
99
+ • CREATE_NEW_PROCESS_GROUP isolates the child in its own process group.
100
+ start_new_session and creationflags are mutually exclusive on Windows;
101
+ we use creationflags exclusively here.
102
+ • taskkill /F /T /PID forcefully terminates the process tree — the
103
+ Windows equivalent of POSIX os.killpg(SIGKILL).
104
+
105
+ Raises _TaskTimeoutError on timeout.
106
+ Returns (exit_code, stdout, stderr).
107
+ """
108
+ proc = subprocess.Popen(
109
+ command,
110
+ cwd=cwd,
111
+ shell=shell,
112
+ stdout=subprocess.PIPE,
113
+ stderr=subprocess.PIPE,
114
+ text=True,
115
+ env=os.environ.copy(),
116
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
117
+ )
118
+ try:
119
+ out, err = proc.communicate(timeout=timeout)
120
+ return proc.returncode, out, err
121
+ except subprocess.TimeoutExpired:
122
+ subprocess.run(
123
+ ["taskkill", "/F", "/T", "/PID", str(proc.pid)],
124
+ capture_output=True,
125
+ )
126
+ proc.wait()
127
+ raise _TaskTimeoutError(f"Command timed out after {timeout}s")
128
+
129
+
130
+ # ===========================================================================
131
+ # Test result parsers
132
+ # ===========================================================================
133
+
134
+ def _parse_pytest(text: str) -> tuple[int, int]:
135
+ passed, failed = 0, 0
136
+ m = re.search(r"(\d+)\s+passed", text)
137
+ if m:
138
+ passed = int(m.group(1))
139
+ m = re.search(r"(\d+)\s+failed", text)
140
+ if m:
141
+ failed = int(m.group(1))
142
+ m = re.search(r"(\d+)\s+error", text)
143
+ if m:
144
+ failed += int(m.group(1))
145
+ return passed, failed
146
+
147
+
148
+ def _parse_cargo(text: str) -> tuple[int, int]:
149
+ passed, failed = 0, 0
150
+ for m in re.finditer(r"test result:.*?(\d+)\s+passed;\s*(\d+)\s+failed", text):
151
+ passed += int(m.group(1))
152
+ failed += int(m.group(2))
153
+ return passed, failed
154
+
155
+
156
+ def _parse_go(text: str) -> tuple[int, int]:
157
+ passed = len(re.findall(r"^--- PASS:", text, re.MULTILINE))
158
+ failed = len(re.findall(r"^--- FAIL:", text, re.MULTILINE))
159
+ if passed == 0 and failed == 0:
160
+ passed = len(re.findall(r"^ok\s+\S+", text, re.MULTILINE))
161
+ failed = len(re.findall(r"^FAIL\s+\S+", text, re.MULTILINE))
162
+ return passed, failed
163
+
164
+
165
+ def _parse_jest(text: str) -> tuple[int, int]:
166
+ passed, failed = 0, 0
167
+ m = re.search(r"^Tests:\s+(.+)$", text, re.MULTILINE)
168
+ if m:
169
+ summary = m.group(1)
170
+ p = re.search(r"(\d+)\s+passed", summary)
171
+ f = re.search(r"(\d+)\s+failed", summary)
172
+ if p:
173
+ passed = int(p.group(1))
174
+ if f:
175
+ failed = int(f.group(1))
176
+ return passed, failed
177
+
178
+
179
+ def _parse_dotnet(text: str) -> tuple[int, int]:
180
+ passed, failed = 0, 0
181
+ m = re.search(r"Failed:\s*(\d+),\s*Passed:\s*(\d+)", text)
182
+ if m:
183
+ failed = int(m.group(1))
184
+ passed = int(m.group(2))
185
+ return passed, failed
186
+
187
+
188
+ def _parse_junit(text: str) -> tuple[int, int]:
189
+ passed_total = failed_total = 0
190
+ for m in re.finditer(
191
+ r"Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+)", text
192
+ ):
193
+ run = int(m.group(1))
194
+ failures = int(m.group(2))
195
+ errors = int(m.group(3))
196
+ failed_total += failures + errors
197
+ passed_total += max(run - failures - errors, 0)
198
+ return passed_total, failed_total
199
+
200
+
201
+ def _dispatch_test_parser(test_command: str, text: str) -> tuple[int, int]:
202
+ cmd = test_command.lower()
203
+ if "pytest" in cmd or "py.test" in cmd:
204
+ return _parse_pytest(text)
205
+ if "cargo" in cmd:
206
+ return _parse_cargo(text)
207
+ if "go test" in cmd:
208
+ return _parse_go(text)
209
+ if (
210
+ "jest" in cmd
211
+ or ("npm" in cmd and "test" in cmd)
212
+ or ("yarn" in cmd and "test" in cmd)
213
+ or ("pnpm" in cmd and "test" in cmd)
214
+ ):
215
+ return _parse_jest(text)
216
+ if "dotnet" in cmd:
217
+ return _parse_dotnet(text)
218
+ if "mvn" in cmd or "gradle" in cmd or "sbt" in cmd or "junit" in cmd:
219
+ return _parse_junit(text)
220
+ for parser in (
221
+ _parse_pytest, _parse_cargo, _parse_go,
222
+ _parse_jest, _parse_dotnet, _parse_junit,
223
+ ):
224
+ p, f = parser(text)
225
+ if p or f:
226
+ return p, f
227
+ return 0, 0
228
+
229
+
230
+ # ===========================================================================
231
+ # Lint error parser
232
+ # ===========================================================================
233
+
234
+ def _parse_lint_errors(lint_command: str, text: str, exit_code: int) -> int:
235
+ """
236
+ Extract an error count from linter output.
237
+ Soft scoring only — never changes task status.
238
+ """
239
+ cmd = lint_command.lower()
240
+
241
+ if "ruff" in cmd:
242
+ m = re.search(r"Found\s+(\d+)\s+error", text)
243
+ if m:
244
+ return int(m.group(1))
245
+ if "--output-format json" in cmd or "-o json" in cmd:
246
+ try:
247
+ import json
248
+ return len(json.loads(text))
249
+ except Exception:
250
+ pass
251
+
252
+ if "flake8" in cmd:
253
+ return len([l for l in text.splitlines() if re.match(r".+:\d+:\d+:\s+[EWF]", l)])
254
+
255
+ if "mypy" in cmd:
256
+ m = re.search(r"Found\s+(\d+)\s+error", text)
257
+ if m:
258
+ return int(m.group(1))
259
+ return text.count(": error:")
260
+
261
+ if "pylint" in cmd:
262
+ return len(re.findall(r"^\S+:\d+:\d+:\s+[EF]\d{4}:", text, re.MULTILINE))
263
+
264
+ if "clippy" in cmd or ("cargo" in cmd and "check" in cmd):
265
+ return len(re.findall(r"^error\[", text, re.MULTILINE))
266
+
267
+ if "eslint" in cmd:
268
+ if "--format json" in cmd or "-f json" in cmd:
269
+ try:
270
+ import json
271
+ data = json.loads(text)
272
+ return sum(
273
+ sum(1 for msg in f.get("messages", []) if msg.get("severity") == 2)
274
+ for f in data
275
+ )
276
+ except Exception:
277
+ pass
278
+ m = re.search(r"(\d+)\s+error", text)
279
+ return int(m.group(1)) if m else 0
280
+
281
+ if "go vet" in cmd or "staticcheck" in cmd:
282
+ return len([l for l in text.splitlines() if l.strip()])
283
+
284
+ if "clang-tidy" in cmd or "cppcheck" in cmd:
285
+ return len(re.findall(r"\berror\b", text, re.IGNORECASE))
286
+
287
+ if "dotnet" in cmd and "build" in cmd:
288
+ m = re.search(r"(\d+)\s+Error\(s\)", text)
289
+ return int(m.group(1)) if m else 0
290
+
291
+ if exit_code != 0:
292
+ return len(re.findall(r"\berror\b", text, re.IGNORECASE))
293
+ return 0
294
+
295
+
296
+ # ===========================================================================
297
+ # Patch normaliser + similarity scorer
298
+ # ===========================================================================
299
+
300
+ def _normalise_patch(patch: str) -> list[str]:
301
+ kept: list[str] = []
302
+ for line in patch.splitlines():
303
+ if (
304
+ line.startswith("diff ")
305
+ or line.startswith("index ")
306
+ or line.startswith("--- ")
307
+ or line.startswith("+++ ")
308
+ or line.startswith("@@ ")
309
+ ):
310
+ continue
311
+ kept.append(line)
312
+ return kept
313
+
314
+
315
+ def _patch_similarity(agent_patch: str, reference_patch: str) -> float:
316
+ a = _normalise_patch(agent_patch)
317
+ b = _normalise_patch(reference_patch)
318
+ if not a and not b:
319
+ return 1.0
320
+ if not a or not b:
321
+ return 0.0
322
+ return difflib.SequenceMatcher(None, a, b).ratio()
323
+
324
+
325
+ # ===========================================================================
326
+ # Core task executor
327
+ # ===========================================================================
328
+
329
+ def _execute(
330
+ task_id: str,
331
+ repo_url: str,
332
+ base_commit: str,
333
+ patch: str,
334
+ test_command: str,
335
+ timeout: int,
336
+ lint_command: str,
337
+ capture_diff: bool,
338
+ reference_patch: str,
339
+ ) -> None:
340
+ task_dir = os.path.join(TASK_BASE_DIR, task_id)
341
+ repo_dir = os.path.join(task_dir, "repo")
342
+ patch_file = os.path.join(task_dir, "task.patch")
343
+
344
+ stdout_parts: list[str] = []
345
+ stderr_parts: list[str] = []
346
+ start = time.monotonic()
347
+
348
+ final_update: dict = {
349
+ "status": "failed",
350
+ "exit_code": -1,
351
+ "stdout": "",
352
+ "stderr": "",
353
+ "tests_passed": 0,
354
+ "tests_failed": 0,
355
+ "lint_errors": None,
356
+ "lint_output": None,
357
+ "patch_diff": None,
358
+ "patch_similarity": None,
359
+ "execution_time": 0.0,
360
+ }
361
+
362
+ def _update(**kw: object) -> None:
363
+ with _tasks_lock:
364
+ _tasks[task_id].update(kw)
365
+
366
+ _update(status="running")
367
+
368
+ try:
369
+ os.makedirs(task_dir, exist_ok=True)
370
+
371
+ rc, out, err = _run(["git", "clone", repo_url, repo_dir], timeout=120)
372
+ stdout_parts.append(out); stderr_parts.append(err)
373
+ if rc != 0:
374
+ raise RuntimeError(f"git clone failed (rc={rc}): {err.strip()}")
375
+
376
+ rc, out, err = _run(["git", "checkout", base_commit], cwd=repo_dir, timeout=60)
377
+ stdout_parts.append(out); stderr_parts.append(err)
378
+ if rc != 0:
379
+ raise RuntimeError(f"git checkout failed (rc={rc}): {err.strip()}")
380
+
381
+ if patch and patch.strip():
382
+ with open(patch_file, "w", encoding="utf-8") as fh:
383
+ fh.write(patch)
384
+ rc, out, err = _run(["git", "apply", patch_file], cwd=repo_dir, timeout=30)
385
+ stdout_parts.append(out); stderr_parts.append(err)
386
+ if rc != 0:
387
+ raise RuntimeError(f"git apply failed (rc={rc}): {err.strip()}")
388
+
389
+ rc, out, err = _run(test_command, cwd=repo_dir, timeout=timeout, shell=True)
390
+ stdout_parts.append(out); stderr_parts.append(err)
391
+ test_exit_code = rc
392
+
393
+ combined_stdout = "\n".join(filter(None, stdout_parts))
394
+ combined_stderr = "\n".join(filter(None, stderr_parts))
395
+ passed, failed = _dispatch_test_parser(
396
+ test_command, combined_stdout + "\n" + combined_stderr
397
+ )
398
+
399
+ lint_errors_count: int | None = None
400
+ lint_out: str | None = None
401
+
402
+ if lint_command and lint_command.strip():
403
+ try:
404
+ lint_rc, l_out, l_err = _run(
405
+ lint_command, cwd=repo_dir, timeout=120, shell=True
406
+ )
407
+ lint_out = (l_out + "\n" + l_err).strip() or None
408
+ lint_errors_count = _parse_lint_errors(
409
+ lint_command, lint_out or "", lint_rc
410
+ )
411
+ log.info("Task %s lint finished — rc=%d errors=%s",
412
+ task_id, lint_rc, lint_errors_count)
413
+ except _TaskTimeoutError:
414
+ lint_out = "Lint timed out after 120s"
415
+ lint_errors_count = None
416
+ log.warning("Task %s lint timed out", task_id)
417
+ except Exception as exc:
418
+ lint_out = f"Lint error: {exc}"
419
+ lint_errors_count = None
420
+ log.warning("Task %s lint exception: %s", task_id, exc)
421
+
422
+ patch_diff_text: str | None = None
423
+
424
+ if capture_diff or (reference_patch and reference_patch.strip()):
425
+ try:
426
+ _, diff_out, _ = _run(
427
+ ["git", "diff", base_commit], cwd=repo_dir, timeout=30
428
+ )
429
+ patch_diff_text = diff_out.strip() or None
430
+ except Exception as exc:
431
+ log.warning("Task %s git diff failed: %s", task_id, exc)
432
+
433
+ similarity: float | None = None
434
+
435
+ if reference_patch and reference_patch.strip():
436
+ try:
437
+ agent_diff = patch_diff_text or (patch if patch and patch.strip() else "")
438
+ if agent_diff:
439
+ similarity = round(_patch_similarity(agent_diff, reference_patch), 4)
440
+ log.info("Task %s patch_similarity=%.4f", task_id, similarity)
441
+ except Exception as exc:
442
+ log.warning("Task %s similarity computation failed: %s", task_id, exc)
443
+
444
+ final_update = {
445
+ "status": "completed",
446
+ "exit_code": test_exit_code,
447
+ "stdout": combined_stdout,
448
+ "stderr": combined_stderr,
449
+ "tests_passed": passed,
450
+ "tests_failed": failed,
451
+ "lint_errors": lint_errors_count,
452
+ "lint_output": lint_out,
453
+ "patch_diff": patch_diff_text,
454
+ "patch_similarity": similarity,
455
+ "execution_time": round(time.monotonic() - start, 3),
456
+ }
457
+
458
+ except _TaskTimeoutError as exc:
459
+ stderr_parts.append(str(exc))
460
+ log.error("Task %s timed out after %ds", task_id, timeout)
461
+ final_update.update({
462
+ "stdout": "\n".join(filter(None, stdout_parts)),
463
+ "stderr": "\n".join(filter(None, stderr_parts)),
464
+ "execution_time": round(time.monotonic() - start, 3),
465
+ })
466
+
467
+ except Exception as exc:
468
+ stderr_parts.append(str(exc))
469
+ log.exception("Task %s failed: %s", task_id, exc)
470
+ final_update.update({
471
+ "stdout": "\n".join(filter(None, stdout_parts)),
472
+ "stderr": "\n".join(filter(None, stderr_parts)),
473
+ "execution_time": round(time.monotonic() - start, 3),
474
+ })
475
+
476
+ finally:
477
+ _update(**final_update)
478
+ try:
479
+ shutil.rmtree(task_dir, ignore_errors=True)
480
+ except Exception:
481
+ pass
482
+
483
+
484
+ # ===========================================================================
485
+ # REST endpoints
486
+ # ===========================================================================
487
+
488
+ @app.route("/task/submit", methods=["POST"])
489
+ def submit():
490
+ """
491
+ POST /task/submit
492
+
493
+ Body (JSON):
494
+ repo_url str required
495
+ base_commit str optional (default: HEAD)
496
+ patch str optional
497
+ test_command str required
498
+ timeout int optional (default: 300)
499
+ lint_command str optional
500
+ capture_diff bool optional (default: false)
501
+ reference_patch str optional
502
+
503
+ Returns 202: { "task_id": "<uuid>", "status": "pending" }
504
+ """
505
+ if not _check_auth():
506
+ return jsonify(error="Unauthorized"), HTTPStatus.UNAUTHORIZED
507
+
508
+ _evict_old_tasks()
509
+
510
+ body = request.get_json(force=True, silent=True)
511
+ if not body:
512
+ return jsonify(error="Request body must be valid JSON"), HTTPStatus.BAD_REQUEST
513
+
514
+ missing = [f for f in ("repo_url", "test_command") if not body.get(f)]
515
+ if missing:
516
+ return jsonify(error=f"Missing required fields: {missing}"), HTTPStatus.BAD_REQUEST
517
+
518
+ task_id = str(uuid.uuid4())
519
+ record: dict = {
520
+ "task_id": task_id,
521
+ "status": "pending",
522
+ "_created": time.monotonic(),
523
+ "repo_url": body["repo_url"],
524
+ "base_commit": body.get("base_commit", "HEAD"),
525
+ "test_command": body["test_command"],
526
+ "timeout": int(body.get("timeout", 300)),
527
+ "exit_code": None,
528
+ "stdout": None,
529
+ "stderr": None,
530
+ "tests_passed": None,
531
+ "tests_failed": None,
532
+ "lint_errors": None,
533
+ "lint_output": None,
534
+ "patch_diff": None,
535
+ "patch_similarity": None,
536
+ "execution_time": None,
537
+ }
538
+
539
+ with _tasks_lock:
540
+ _tasks[task_id] = record
541
+
542
+ threading.Thread(
543
+ target=_execute,
544
+ args=(
545
+ task_id,
546
+ body["repo_url"],
547
+ body.get("base_commit", "HEAD"),
548
+ body.get("patch", ""),
549
+ body["test_command"],
550
+ int(body.get("timeout", 300)),
551
+ body.get("lint_command", ""),
552
+ bool(body.get("capture_diff", False)),
553
+ body.get("reference_patch", ""),
554
+ ),
555
+ daemon=True,
556
+ ).start()
557
+
558
+ log.info("Task %s submitted — repo=%s", task_id, body["repo_url"])
559
+ return jsonify(task_id=task_id, status="pending"), HTTPStatus.ACCEPTED
560
+
561
+
562
+ @app.route("/task/<task_id>", methods=["GET"])
563
+ def status(task_id: str):
564
+ if not _check_auth():
565
+ return jsonify(error="Unauthorized"), HTTPStatus.UNAUTHORIZED
566
+ with _tasks_lock:
567
+ t = _tasks.get(task_id)
568
+ if t is None:
569
+ return jsonify(error="Task not found"), HTTPStatus.NOT_FOUND
570
+ return jsonify(task_id=t["task_id"], status=t["status"])
571
+
572
+
573
+ @app.route("/task/<task_id>/result", methods=["GET"])
574
+ def result(task_id: str):
575
+ if not _check_auth():
576
+ return jsonify(error="Unauthorized"), HTTPStatus.UNAUTHORIZED
577
+ with _tasks_lock:
578
+ t = _tasks.get(task_id)
579
+ if t is None:
580
+ return jsonify(error="Task not found"), HTTPStatus.NOT_FOUND
581
+ if t["status"] in ("pending", "running"):
582
+ return jsonify(
583
+ task_id=task_id,
584
+ status=t["status"],
585
+ message="Task not yet complete — poll again shortly",
586
+ ), HTTPStatus.ACCEPTED
587
+ return jsonify(t)
588
+
589
+
590
+ @app.route("/task/<task_id>", methods=["DELETE"])
591
+ def delete(task_id: str):
592
+ if not _check_auth():
593
+ return jsonify(error="Unauthorized"), HTTPStatus.UNAUTHORIZED
594
+ with _tasks_lock:
595
+ if task_id not in _tasks:
596
+ return jsonify(error="Task not found"), HTTPStatus.NOT_FOUND
597
+ _tasks.pop(task_id)
598
+ log.info("Task %s deleted", task_id)
599
+ return jsonify(task_id=task_id, deleted=True)
600
+
601
+
602
+ if __name__ == "__main__":
603
+ log.info("Task executor starting on 0.0.0.0:%d", API_PORT)
604
+ serve(app, host="0.0.0.0", port=API_PORT, threads=16)
scripts/win11.yaml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ windows:
3
+ image: dockurr/windows
4
+ container_name: windows
5
+ environment:
6
+ VERSION: "11"
7
+ DISK_SIZE: "2T"
8
+ RAM_SIZE: "8G"
9
+ CPU_CORES: "4"
10
+ USERNAME: "AgentUser"
11
+ PASSWORD: "AgentPassword1"
12
+ TIMEZONE: "UTC"
13
+ REGION: "en-US"
14
+ KEYBOARD: "en-US"
15
+ HOSTNAME: "Workspace"
16
+ devices:
17
+ - /dev/kvm
18
+ - /dev/net/tun
19
+ cap_add:
20
+ - NET_ADMIN
21
+ ports:
22
+ - 8006:8006
23
+ - 3389:3389/tcp
24
+ - 3389:3389/udp
25
+ - 4444:4444
26
+ - 2222:22
27
+ volumes:
28
+ - ./windows11-storage:/storage
29
+ restart: "no"
30
+ stop_grace_period: 2m