flywheel-bootstrap-staging 0.1.9.202601272054__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bootstrap/__init__.py +3 -0
- bootstrap/__main__.py +48 -0
- bootstrap/artifacts.py +101 -0
- bootstrap/config_loader.py +122 -0
- bootstrap/constants.py +20 -0
- bootstrap/git_ops.py +324 -0
- bootstrap/install.py +129 -0
- bootstrap/orchestrator.py +797 -0
- bootstrap/payload.py +119 -0
- bootstrap/prompts.py +79 -0
- bootstrap/py.typed +1 -0
- bootstrap/runner.py +145 -0
- bootstrap/telemetry.py +147 -0
- flywheel_bootstrap_staging-0.1.9.202601272054.dist-info/METADATA +94 -0
- flywheel_bootstrap_staging-0.1.9.202601272054.dist-info/RECORD +17 -0
- flywheel_bootstrap_staging-0.1.9.202601272054.dist-info/WHEEL +4 -0
- flywheel_bootstrap_staging-0.1.9.202601272054.dist-info/entry_points.txt +2 -0
bootstrap/payload.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Bootstrap payload fetch/validation (skeleton)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
import urllib.request
|
|
9
|
+
import urllib.error
|
|
10
|
+
import ssl
|
|
11
|
+
import http.client
|
|
12
|
+
from typing import Any, Mapping
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class RepoContext:
|
|
17
|
+
"""Repository context for code persistence.
|
|
18
|
+
|
|
19
|
+
Mirrors the core.models.RepoContext class but is a standalone
|
|
20
|
+
definition to avoid tight coupling between bootstrap and core.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
repo_url: str
|
|
24
|
+
repo_owner: str
|
|
25
|
+
repo_name: str
|
|
26
|
+
branch_name: str
|
|
27
|
+
base_branch: str
|
|
28
|
+
is_fork: bool = False
|
|
29
|
+
fork_source_url: str | None = None
|
|
30
|
+
base_commit_sha: str | None = None
|
|
31
|
+
head_commit_sha: str | None = None
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_dict(cls, payload: Mapping[str, Any]) -> "RepoContext":
|
|
35
|
+
return cls(
|
|
36
|
+
repo_url=str(payload["repo_url"]),
|
|
37
|
+
repo_owner=str(payload["repo_owner"]),
|
|
38
|
+
repo_name=str(payload["repo_name"]),
|
|
39
|
+
branch_name=str(payload["branch_name"]),
|
|
40
|
+
base_branch=str(payload["base_branch"]),
|
|
41
|
+
is_fork=bool(payload.get("is_fork", False)),
|
|
42
|
+
fork_source_url=payload.get("fork_source_url"),
|
|
43
|
+
base_commit_sha=payload.get("base_commit_sha"),
|
|
44
|
+
head_commit_sha=payload.get("head_commit_sha"),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class BootstrapPayload:
|
|
50
|
+
"""Representation of the backend bootstrap payload."""
|
|
51
|
+
|
|
52
|
+
prompt: str
|
|
53
|
+
repo_context: RepoContext | None = None
|
|
54
|
+
github_token: str | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def fetch_bootstrap_payload(
|
|
58
|
+
server_url: str,
|
|
59
|
+
run_id: str,
|
|
60
|
+
token: str,
|
|
61
|
+
) -> BootstrapPayload:
|
|
62
|
+
"""Retrieve bootstrap payload from the backend server.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
NotImplementedError: placeholder until HTTP wiring is added.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
url = f"{server_url.rstrip('/')}/runs/{run_id}/bootstrap"
|
|
69
|
+
req = urllib.request.Request(url, headers={"X-Run-Token": token})
|
|
70
|
+
payload = _urlopen_json_with_retries(
|
|
71
|
+
req,
|
|
72
|
+
timeout_seconds=30,
|
|
73
|
+
attempts=6,
|
|
74
|
+
base_delay_seconds=0.5,
|
|
75
|
+
)
|
|
76
|
+
inner = payload.get("payload", {})
|
|
77
|
+
|
|
78
|
+
# Parse repo context if present
|
|
79
|
+
repo_context = None
|
|
80
|
+
if inner.get("repo_context"):
|
|
81
|
+
repo_context = RepoContext.from_dict(inner["repo_context"])
|
|
82
|
+
|
|
83
|
+
return BootstrapPayload(
|
|
84
|
+
prompt=str(inner.get("prompt", "")),
|
|
85
|
+
repo_context=repo_context,
|
|
86
|
+
github_token=inner.get("github_token"),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _urlopen_json_with_retries(
|
|
91
|
+
req: urllib.request.Request,
|
|
92
|
+
timeout_seconds: int,
|
|
93
|
+
attempts: int,
|
|
94
|
+
base_delay_seconds: float,
|
|
95
|
+
) -> dict:
|
|
96
|
+
"""Best-effort JSON fetch with retries.
|
|
97
|
+
|
|
98
|
+
Fly.io apps can be cold-started (min_machines_running=0), and network edges
|
|
99
|
+
can occasionally drop connections. This helper retries common transient
|
|
100
|
+
failures with exponential backoff.
|
|
101
|
+
"""
|
|
102
|
+
last_exc: Exception | None = None
|
|
103
|
+
for i in range(attempts):
|
|
104
|
+
try:
|
|
105
|
+
with urllib.request.urlopen(req, timeout=timeout_seconds) as resp:
|
|
106
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
107
|
+
except (
|
|
108
|
+
TimeoutError,
|
|
109
|
+
urllib.error.URLError,
|
|
110
|
+
http.client.RemoteDisconnected,
|
|
111
|
+
ssl.SSLError,
|
|
112
|
+
) as exc:
|
|
113
|
+
last_exc = exc
|
|
114
|
+
if i == attempts - 1:
|
|
115
|
+
break
|
|
116
|
+
delay = base_delay_seconds * (2**i)
|
|
117
|
+
time.sleep(delay)
|
|
118
|
+
assert last_exc is not None
|
|
119
|
+
raise last_exc
|
bootstrap/prompts.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Prompt assembly helpers for bootstrap."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import textwrap
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_prompt_text(
|
|
9
|
+
*, server_prompt: str, workspace_instructions: str, artifact_manifest: str
|
|
10
|
+
) -> str:
|
|
11
|
+
"""Combine base context, task prompt, and user workspace instructions."""
|
|
12
|
+
|
|
13
|
+
base_context = textwrap.dedent(
|
|
14
|
+
f"""
|
|
15
|
+
You are an autonomous engineer with FULL SYSTEM ACCESS. Your job is to complete the task by ACTUALLY EXECUTING commands and writing files—not by describing what to do.
|
|
16
|
+
|
|
17
|
+
CRITICAL: You must USE your shell and file-writing capabilities to:
|
|
18
|
+
- Create files directly (don't tell the user to save code—write it yourself)
|
|
19
|
+
- Run commands directly (don't tell the user to run something—execute it yourself)
|
|
20
|
+
- Install dependencies if needed
|
|
21
|
+
- Execute training scripts and collect results
|
|
22
|
+
|
|
23
|
+
Work autonomously until completion—there is no user to respond to questions. Execute all necessary commands yourself. The task description and workspace instructions below contain the context you need. Leverage the provided GPU hardware fully.
|
|
24
|
+
|
|
25
|
+
ARTIFACT MANIFEST (CRITICAL):
|
|
26
|
+
- The environment variable $FLYWHEEL_WORKSPACE contains the absolute path to your workspace root.
|
|
27
|
+
- When finished, write the manifest to: $FLYWHEEL_WORKSPACE/{artifact_manifest}
|
|
28
|
+
- The manifest MUST be a top-level JSON array (list). Do NOT wrap it in an object.
|
|
29
|
+
- All file paths in the manifest must be relative to $FLYWHEEL_WORKSPACE.
|
|
30
|
+
- Each entry must be an object with "artifact_type" and "payload" keys.
|
|
31
|
+
|
|
32
|
+
MANIFEST FORMAT — the file must look exactly like this (a JSON array):
|
|
33
|
+
[
|
|
34
|
+
{{"artifact_type": "text", "payload": {{"content": "Summary of results..."}}}},
|
|
35
|
+
{{"artifact_type": "image", "payload": {{"path": "plots/loss_curve.png", "format": "png"}}}},
|
|
36
|
+
{{"artifact_type": "table", "payload": {{"path": "results/metrics.csv", "format": "csv"}}}}
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
IMPORTANT: The file must start with [ and end with ]. Do NOT write {{"artifacts": [...]}} or any other wrapper object.
|
|
40
|
+
|
|
41
|
+
SUPPORTED ARTIFACT TYPES (use ONLY these):
|
|
42
|
+
- "text": For text/markdown. Payload: {{"content": "..."}} or {{"path": "path/to/file.txt"}}
|
|
43
|
+
- "table": For CSV data. Payload: {{"path": "path/to/data.csv", "format": "csv"}}
|
|
44
|
+
- "json": For JSON data. Payload: {{"path": "path/to/data.json"}} or inline the object directly
|
|
45
|
+
- "image": For PNG/JPEG. Payload: {{"path": "path/to/plot.png", "format": "png"}}
|
|
46
|
+
- "html": For interactive HTML (e.g., Plotly). Payload: {{"path": "path/to/chart.html"}} or {{"content": "<!doctype html>..."}}
|
|
47
|
+
- "vega": For Vega/Vega-Lite specs. Payload: {{"spec": {{...}}}} or {{"path": "path/to/chart.vl.json"}}
|
|
48
|
+
|
|
49
|
+
OUTPUT:
|
|
50
|
+
- Print concise progress updates to stdout; they will be forwarded as logs.
|
|
51
|
+
|
|
52
|
+
ARTIFACT QUALITY TIPS:
|
|
53
|
+
Before finalizing any artifact, verify it renders correctly. Common pitfalls:
|
|
54
|
+
|
|
55
|
+
Plots & Visualizations:
|
|
56
|
+
- Convert tensors to plain Python/numpy BEFORE plotting: use .detach().cpu().numpy() or .item()
|
|
57
|
+
- Verify numeric axes show numbers, not categorical labels (symptom of unconverted data)
|
|
58
|
+
- Always label axes and include a title
|
|
59
|
+
- For Plotly HTML, save with include_plotlyjs=True to make it self-contained
|
|
60
|
+
|
|
61
|
+
Tables & Data:
|
|
62
|
+
- Ensure consistent column types (don't mix strings and numbers in a column)
|
|
63
|
+
- Handle NaN/None values explicitly rather than leaving them as raw representations
|
|
64
|
+
- Round floats to reasonable precision for readability
|
|
65
|
+
|
|
66
|
+
General:
|
|
67
|
+
- Inspect your output files before declaring success
|
|
68
|
+
- If something looks wrong (e.g., a blank plot, garbled text), debug and fix it
|
|
69
|
+
- Prefer self-contained artifacts (inline CSS/JS, no external dependencies)
|
|
70
|
+
"""
|
|
71
|
+
).strip()
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
f"{base_context}\n\n"
|
|
75
|
+
"Task Description:\n"
|
|
76
|
+
f"{server_prompt.strip()}\n\n"
|
|
77
|
+
"Workspace Instructions:\n"
|
|
78
|
+
f"{workspace_instructions.strip()}"
|
|
79
|
+
)
|
bootstrap/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
bootstrap/runner.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Codex process launcher and stream parser (skeleton)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterable, Mapping, Iterator
|
|
8
|
+
import json
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CodexInvocation:
|
|
15
|
+
"""Command and environment prepared for invoking codex."""
|
|
16
|
+
|
|
17
|
+
args: list[str]
|
|
18
|
+
env: Mapping[str, str]
|
|
19
|
+
workdir: Path
|
|
20
|
+
prompt: str = "" # Prompt to pass via stdin
|
|
21
|
+
exit_code: int | None = None
|
|
22
|
+
stderr_output: str = "" # Captured stderr for debugging
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CodexEvent:
|
|
27
|
+
"""Structured event emitted by codex --json."""
|
|
28
|
+
|
|
29
|
+
raw: dict
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_invocation(
|
|
33
|
+
codex_executable: Path,
|
|
34
|
+
prompt: str,
|
|
35
|
+
workdir: Path,
|
|
36
|
+
env: Mapping[str, str],
|
|
37
|
+
extra_flags: Iterable[str] = (),
|
|
38
|
+
) -> CodexInvocation:
|
|
39
|
+
"""Assemble the codex exec command.
|
|
40
|
+
|
|
41
|
+
The prompt is written to a file in the workdir and passed via --prompt-file
|
|
42
|
+
to avoid shell argument length limits and stdin handling issues.
|
|
43
|
+
"""
|
|
44
|
+
# Write prompt to file in the workdir (which Codex has access to)
|
|
45
|
+
prompt_file = workdir / "flywheel_prompt.txt"
|
|
46
|
+
prompt_file.write_text(prompt, encoding="utf-8")
|
|
47
|
+
|
|
48
|
+
# Use '-' to read prompt from stdin (avoids CLI arg length limits)
|
|
49
|
+
args = [
|
|
50
|
+
str(codex_executable),
|
|
51
|
+
"exec",
|
|
52
|
+
"--json",
|
|
53
|
+
"--cd",
|
|
54
|
+
str(workdir),
|
|
55
|
+
"--skip-git-repo-check",
|
|
56
|
+
*list(extra_flags),
|
|
57
|
+
"-", # Read prompt from stdin
|
|
58
|
+
]
|
|
59
|
+
return CodexInvocation(args=args, env=dict(env), workdir=workdir, prompt=prompt)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def run_and_stream(invocation: CodexInvocation) -> Iterator[CodexEvent]:
|
|
63
|
+
"""Launch codex and yield parsed events as they arrive.
|
|
64
|
+
|
|
65
|
+
The prompt is passed via file (referenced in args) to avoid stdin issues.
|
|
66
|
+
"""
|
|
67
|
+
# Log the command being run (redact prompt for brevity)
|
|
68
|
+
cmd_display = " ".join(invocation.args)
|
|
69
|
+
print(f"[bootstrap] Running: {cmd_display}", file=sys.stderr)
|
|
70
|
+
print(f"[bootstrap] Workdir: {invocation.workdir}", file=sys.stderr)
|
|
71
|
+
print(f"[bootstrap] Prompt length: {len(invocation.prompt)} chars", file=sys.stderr)
|
|
72
|
+
|
|
73
|
+
# Emit command info as first event for server-side logging
|
|
74
|
+
yield CodexEvent(
|
|
75
|
+
raw={
|
|
76
|
+
"bootstrap_debug": "codex_launch",
|
|
77
|
+
"command": invocation.args,
|
|
78
|
+
"workdir": str(invocation.workdir),
|
|
79
|
+
"prompt_length": len(invocation.prompt),
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
proc = subprocess.Popen(
|
|
85
|
+
invocation.args,
|
|
86
|
+
cwd=invocation.workdir,
|
|
87
|
+
env=invocation.env if invocation.env else None,
|
|
88
|
+
stdin=subprocess.PIPE,
|
|
89
|
+
stdout=subprocess.PIPE,
|
|
90
|
+
stderr=subprocess.PIPE,
|
|
91
|
+
text=True,
|
|
92
|
+
encoding="utf-8", # Explicit UTF-8 for cross-platform Unicode support
|
|
93
|
+
bufsize=1,
|
|
94
|
+
)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
error_msg = f"Failed to start codex process: {e}"
|
|
97
|
+
print(f"[bootstrap] ERROR: {error_msg}", file=sys.stderr)
|
|
98
|
+
yield CodexEvent(raw={"bootstrap_error": error_msg})
|
|
99
|
+
invocation.exit_code = 127
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
assert proc.stdin is not None
|
|
103
|
+
assert proc.stdout is not None
|
|
104
|
+
|
|
105
|
+
# Write prompt to stdin and close it so Codex knows input is complete
|
|
106
|
+
try:
|
|
107
|
+
proc.stdin.write(invocation.prompt)
|
|
108
|
+
proc.stdin.close()
|
|
109
|
+
except Exception as e:
|
|
110
|
+
error_msg = f"Failed to write prompt to stdin: {e}"
|
|
111
|
+
print(f"[bootstrap] ERROR: {error_msg}", file=sys.stderr)
|
|
112
|
+
yield CodexEvent(raw={"bootstrap_error": error_msg})
|
|
113
|
+
|
|
114
|
+
for line in proc.stdout:
|
|
115
|
+
line = line.strip()
|
|
116
|
+
if not line:
|
|
117
|
+
continue
|
|
118
|
+
try:
|
|
119
|
+
payload = json.loads(line)
|
|
120
|
+
except json.JSONDecodeError:
|
|
121
|
+
payload = {"message": line}
|
|
122
|
+
yield CodexEvent(raw=payload)
|
|
123
|
+
|
|
124
|
+
proc.wait()
|
|
125
|
+
invocation.exit_code = proc.returncode
|
|
126
|
+
|
|
127
|
+
# Always capture stderr for debugging
|
|
128
|
+
stderr_content = ""
|
|
129
|
+
if proc.stderr:
|
|
130
|
+
stderr_content = proc.stderr.read().strip()
|
|
131
|
+
invocation.stderr_output = stderr_content
|
|
132
|
+
|
|
133
|
+
# Log exit status
|
|
134
|
+
print(f"[bootstrap] Codex exited with code: {proc.returncode}", file=sys.stderr)
|
|
135
|
+
if stderr_content:
|
|
136
|
+
print(f"[bootstrap] Codex stderr:\n{stderr_content}", file=sys.stderr)
|
|
137
|
+
|
|
138
|
+
# Emit detailed exit info
|
|
139
|
+
yield CodexEvent(
|
|
140
|
+
raw={
|
|
141
|
+
"bootstrap_debug": "codex_exit",
|
|
142
|
+
"exit_code": proc.returncode,
|
|
143
|
+
"stderr": stderr_content if stderr_content else None,
|
|
144
|
+
}
|
|
145
|
+
)
|
bootstrap/telemetry.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Heartbeat, log, and artifact POST helpers (skeleton)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Mapping, Sequence
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
import urllib.request
|
|
10
|
+
import urllib.error
|
|
11
|
+
import ssl
|
|
12
|
+
import http.client
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
# Defaults are tuned to be tolerant of transient backend stalls.
|
|
16
|
+
READ_TIMEOUT_SECONDS = 30
|
|
17
|
+
MAX_ATTEMPTS = 3
|
|
18
|
+
BASE_DELAY_SECONDS = 0.5
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def utcnow() -> datetime:
|
|
22
|
+
"""Return a timezone-aware UTC timestamp."""
|
|
23
|
+
return datetime.now(tz=timezone.utc)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def post_heartbeat(
|
|
27
|
+
server_url: str, run_id: str, token: str, summary: str | None
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Send a heartbeat to the backend."""
|
|
30
|
+
_post(
|
|
31
|
+
f"{server_url.rstrip('/')}/runs/{run_id}/heartbeat",
|
|
32
|
+
token,
|
|
33
|
+
{"observed_at": utcnow().isoformat(), "summary": summary},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def post_log(
|
|
38
|
+
server_url: str,
|
|
39
|
+
run_id: str,
|
|
40
|
+
token: str,
|
|
41
|
+
level: str,
|
|
42
|
+
message: str,
|
|
43
|
+
extra: Mapping[str, object] | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Send a log entry to the backend."""
|
|
46
|
+
_post(
|
|
47
|
+
f"{server_url.rstrip('/')}/runs/{run_id}/logs",
|
|
48
|
+
token,
|
|
49
|
+
{
|
|
50
|
+
"created_at": utcnow().isoformat(),
|
|
51
|
+
"level": level,
|
|
52
|
+
"message": message,
|
|
53
|
+
"extra": extra or {},
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def post_artifacts(
|
|
59
|
+
server_url: str,
|
|
60
|
+
run_id: str,
|
|
61
|
+
token: str,
|
|
62
|
+
artifacts: Sequence[Mapping[str, object]],
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Send artifact manifest entries to the backend."""
|
|
65
|
+
for artifact in artifacts:
|
|
66
|
+
payload: Mapping[str, object] | object = artifact
|
|
67
|
+
if isinstance(artifact, Mapping):
|
|
68
|
+
inner = artifact.get("payload", artifact)
|
|
69
|
+
if isinstance(inner, Mapping):
|
|
70
|
+
payload = inner
|
|
71
|
+
_post(
|
|
72
|
+
f"{server_url.rstrip('/')}/runs/{run_id}/artifacts",
|
|
73
|
+
token,
|
|
74
|
+
{
|
|
75
|
+
"artifact_type": str(artifact.get("artifact_type", "unknown")),
|
|
76
|
+
"payload": payload,
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def post_completion(server_url: str, run_id: str, token: str, summary: str) -> None:
|
|
82
|
+
"""Mark run complete."""
|
|
83
|
+
_post(
|
|
84
|
+
f"{server_url.rstrip('/')}/runs/{run_id}/complete",
|
|
85
|
+
token,
|
|
86
|
+
{"summary": summary},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def post_error(
|
|
91
|
+
server_url: str, run_id: str, token: str, reason: str, summary: str | None = None
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Mark run errored."""
|
|
94
|
+
_post(
|
|
95
|
+
f"{server_url.rstrip('/')}/runs/{run_id}/error",
|
|
96
|
+
token,
|
|
97
|
+
{"summary": summary or "", "reason": reason},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _post(url: str, token: str, body: Mapping[str, object]) -> None:
|
|
102
|
+
data = json.dumps(body).encode("utf-8")
|
|
103
|
+
req = urllib.request.Request(
|
|
104
|
+
url,
|
|
105
|
+
data=data,
|
|
106
|
+
headers={
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
"X-Run-Token": token,
|
|
109
|
+
},
|
|
110
|
+
method="POST",
|
|
111
|
+
)
|
|
112
|
+
success = _urlopen_with_retries(
|
|
113
|
+
req,
|
|
114
|
+
timeout_seconds=READ_TIMEOUT_SECONDS,
|
|
115
|
+
attempts=MAX_ATTEMPTS,
|
|
116
|
+
base_delay_seconds=BASE_DELAY_SECONDS,
|
|
117
|
+
)
|
|
118
|
+
if not success:
|
|
119
|
+
# Best-effort: log locally but do not raise, to avoid failing the run on transient stalls.
|
|
120
|
+
print(f"telemetry POST to {url} failed after retries", file=sys.stderr)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _urlopen_with_retries(
|
|
124
|
+
req: urllib.request.Request,
|
|
125
|
+
timeout_seconds: int,
|
|
126
|
+
attempts: int,
|
|
127
|
+
base_delay_seconds: float,
|
|
128
|
+
) -> bool:
|
|
129
|
+
"""Best-effort POST with retries for transient network errors."""
|
|
130
|
+
for i in range(attempts):
|
|
131
|
+
try:
|
|
132
|
+
with urllib.request.urlopen(
|
|
133
|
+
req, timeout=timeout_seconds
|
|
134
|
+
) as resp: # pragma: no cover - network
|
|
135
|
+
resp.read()
|
|
136
|
+
return True
|
|
137
|
+
except (
|
|
138
|
+
TimeoutError,
|
|
139
|
+
urllib.error.URLError,
|
|
140
|
+
http.client.RemoteDisconnected,
|
|
141
|
+
ssl.SSLError,
|
|
142
|
+
):
|
|
143
|
+
if i == attempts - 1:
|
|
144
|
+
break
|
|
145
|
+
delay = base_delay_seconds * (2**i)
|
|
146
|
+
time.sleep(delay)
|
|
147
|
+
return False
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flywheel-bootstrap-staging
|
|
3
|
+
Version: 0.1.9.202601272054
|
|
4
|
+
Summary: Bootstrap runner for Flywheel provisioned GPU instances
|
|
5
|
+
Project-URL: Homepage, http://paradigma.inc/
|
|
6
|
+
Author: Paradigma Labs
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: bootstrap,flywheel,gpu,machine-learning,ml
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Bootstrap
|
|
18
|
+
|
|
19
|
+
This package hosts the BYOC bootstrapper that:
|
|
20
|
+
|
|
21
|
+
- Ensures Codex is available (prefers release tarball; skips install if already
|
|
22
|
+
on `PATH`).
|
|
23
|
+
- Fetches the bootstrap payload for a run from the Flywheel backend.
|
|
24
|
+
- Launches `codex exec` with the provided prompt/config and streams logs.
|
|
25
|
+
- Collects artifacts (manifest-on-exit) and reports completion or error back to
|
|
26
|
+
the backend.
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
Bootstrap reads the user's Codex `config.toml` and requires one of:
|
|
31
|
+
|
|
32
|
+
```toml
|
|
33
|
+
[flywheel]
|
|
34
|
+
# inline instructions (host-specific tips, paths, sandbox notes)
|
|
35
|
+
workspace_instructions = """
|
|
36
|
+
Use /mnt/work as your workspace. Write artifacts under ./artifacts.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# or: reference a file (relative paths are resolved against the config file directory)
|
|
40
|
+
workspace_instructions_file = "workspace_notes.md"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Rules:
|
|
44
|
+
|
|
45
|
+
- At least one of `workspace_instructions` or `workspace_instructions_file` is
|
|
46
|
+
required; otherwise bootstrap exits before contacting the server.
|
|
47
|
+
- If both are set, the file wins and the inline value is ignored (warns once).
|
|
48
|
+
- File contents must be non-empty; the path is resolved relative to the config
|
|
49
|
+
file if not absolute.
|
|
50
|
+
|
|
51
|
+
Prompt assembly written to `flywheel_prompt.txt`:
|
|
52
|
+
|
|
53
|
+
1. Flywheel engineer context (logging/artifact expectations).
|
|
54
|
+
2. Task Description (prompt fetched from the server).
|
|
55
|
+
3. Workspace Instructions (resolved from config as above).
|
|
56
|
+
|
|
57
|
+
Example config: `project/bootstrap/examples/config.example.toml` can be used as
|
|
58
|
+
a starting point; update the paths and instructions for your machine.
|
|
59
|
+
|
|
60
|
+
## End-to-end flow (bootstrap.sh → Python bootstrapper)
|
|
61
|
+
|
|
62
|
+
1. User runs `bash ./bootstrap.sh --run-id <id> --token <token> --config /path/to/config.toml [--server <url>]` on their BYOC machine.
|
|
63
|
+
2. The shim:
|
|
64
|
+
- Ensures `uvx` is available (installs via `https://astral.sh/uv/install.sh` if missing, then rechecks PATH with `~/.cargo/bin`).
|
|
65
|
+
- Points `PKG_PATH` to the local repo copy `project/bootstrap`.
|
|
66
|
+
- Executes `uvx --no-cache --from "$PKG_PATH" flywheel-bootstrap "$@"` so the latest local package runs.
|
|
67
|
+
3. Python entrypoint (`python -m bootstrap`):
|
|
68
|
+
- Parses args/env: requires run id + token, required `--config`, optional `--server` (default `http://localhost:8000`).
|
|
69
|
+
- Loads Codex config.toml, enforces presence of workspace instructions (inline or file), extracts workspace/sandbox settings.
|
|
70
|
+
4. Workspace resolution:
|
|
71
|
+
- Uses `cd`/`workspace_dir` from config if set; otherwise `~/.flywheel/runs/<run_id>`.
|
|
72
|
+
- Creates the workspace and validates the artifact manifest path is inside sandbox `writable_roots` when sandboxing is enabled; else exits with an error.
|
|
73
|
+
5. Codex availability:
|
|
74
|
+
- If `BOOTSTRAP_MOCK_CODEX` is set, skips install and runs a mock flow.
|
|
75
|
+
- Else, if `codex` is already on PATH, reuse it; otherwise download the Codex release tarball to the workspace/run root and mark it executable.
|
|
76
|
+
6. Fetch bootstrap payload:
|
|
77
|
+
- `GET <server>/runs/<run_id>/bootstrap` with `X-Run-Token`; payload contains the task prompt.
|
|
78
|
+
7. Build prompt file:
|
|
79
|
+
- Combine base Flywheel engineer context, “Task Description” (server prompt), and “Workspace Instructions” (user config) into `flywheel_prompt.txt` in the workspace.
|
|
80
|
+
8. Launch Codex:
|
|
81
|
+
- Run `codex exec --json --cd <workspace> --skip-git-repo-check flywheel_prompt.txt` with env `FLYWHEEL_RUN_ID/TOKEN/SERVER`.
|
|
82
|
+
- Start a heartbeat thread posting `/runs/{id}/heartbeat` every 30s.
|
|
83
|
+
- Stream Codex stdout lines as logs to `/runs/{id}/logs`; capture Codex `run_id` if emitted.
|
|
84
|
+
9. After Codex exits:
|
|
85
|
+
- Read `flywheel_artifacts.json`; if empty and Codex `run_id` is known, attempt one `codex resume <id>` then re-read.
|
|
86
|
+
- POST artifacts to `/runs/{id}/artifacts`; POST `/complete` on exit 0, else `/error` with the exit code.
|
|
87
|
+
- Stop/join the heartbeat thread.
|
|
88
|
+
10. Mock mode (`BOOTSTRAP_MOCK_CODEX=1`):
|
|
89
|
+
- Sends a heartbeat, a few logs, writes a mock artifact manifest, returns 0 (used in e2e tests).
|
|
90
|
+
|
|
91
|
+
## Next steps
|
|
92
|
+
|
|
93
|
+
- Publish bootstrap package and switch `uvx --from` to a release URL.
|
|
94
|
+
- Iterate on prompts / general polish
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
bootstrap/__init__.py,sha256=vRTDRRAXOf9l3PGRNGMUowKObnxFzmWCYUwtn31LzIM,39
|
|
2
|
+
bootstrap/__main__.py,sha256=F94dlGd3P4icWVZnb16LjrKpBhLPCvjwqyWDqhX3JjE,1338
|
|
3
|
+
bootstrap/artifacts.py,sha256=pdwDfxduT-G9VMJiGdm9MHkfUf35TzMGhFTHn135nU4,4188
|
|
4
|
+
bootstrap/config_loader.py,sha256=dgypIOi4a5auGsarIDB5MI6qJO38UWO6RJkRTzvA9Zs,3936
|
|
5
|
+
bootstrap/constants.py,sha256=Q3bNGvKKgQxyP9Hrx6o5GydFHwDq2SRkLnxwRNyM-OY,633
|
|
6
|
+
bootstrap/git_ops.py,sha256=HPn-CAURQEMRSLJRUuy1Lor7YwPYZjb6i6ku6WhJo7M,9267
|
|
7
|
+
bootstrap/install.py,sha256=Es1F0UBOYDhuv7i9veA5Bt-yyTyTZ-dAsHmGc-iRY5k,4166
|
|
8
|
+
bootstrap/orchestrator.py,sha256=zWRroJSoC6rjyNS1-YAnl4frk-uCWveWO_BeQnr_iRw,33468
|
|
9
|
+
bootstrap/payload.py,sha256=JugHguvdjHuoEY_wTE72PFDWlmFFgEk8_wiINNNjV_Q,3417
|
|
10
|
+
bootstrap/prompts.py,sha256=jywNCfrMN30uYV6cIBSex7NxWDM9IS-y7aD_x_bj3XY,4149
|
|
11
|
+
bootstrap/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
12
|
+
bootstrap/runner.py,sha256=qFqDnHKxUIOBAfk9cX7EipXz7mJCwTJPeKMQI30x4Do,4509
|
|
13
|
+
bootstrap/telemetry.py,sha256=ONPvKTpc2puCygkakdQW7-Kkrx_yqEzZz-0FMtCQ-RQ,3983
|
|
14
|
+
flywheel_bootstrap_staging-0.1.9.202601272054.dist-info/METADATA,sha256=YhfSR1Mi1W1prvFmZ1MUasmuUwv6MxeRzPj7djlD06g,4638
|
|
15
|
+
flywheel_bootstrap_staging-0.1.9.202601272054.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
+
flywheel_bootstrap_staging-0.1.9.202601272054.dist-info/entry_points.txt,sha256=uluR7l4k_RhY5K4nYN5e6uwtceW3_7h_kvpM-csjzrg,63
|
|
17
|
+
flywheel_bootstrap_staging-0.1.9.202601272054.dist-info/RECORD,,
|