flywheel-bootstrap-staging 0.1.9.202601291439__tar.gz
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.
- flywheel_bootstrap_staging-0.1.9.202601291439/.gitignore +222 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/PKG-INFO +94 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/README.md +78 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/bootstrap/__init__.py +3 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/bootstrap/__main__.py +48 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/bootstrap/artifacts.py +101 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/bootstrap/config_loader.py +122 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/bootstrap/constants.py +20 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/bootstrap/git_ops.py +324 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/bootstrap/install.py +129 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/bootstrap/orchestrator.py +797 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/bootstrap/payload.py +119 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/bootstrap/prompts.py +79 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/bootstrap/py.typed +1 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/bootstrap/runner.py +145 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/bootstrap/telemetry.py +147 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/bootstrap.sh +38 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/examples/config.example.toml +36 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/pyproject.toml +44 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/tests/test_artifacts.py +154 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/tests/test_entrypoint.py +12 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/tests/test_git_ops.py +277 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/tests/test_orchestrator.py +416 -0
- flywheel_bootstrap_staging-0.1.9.202601291439/uv.lock +255 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# repo-specific
|
|
2
|
+
data/
|
|
3
|
+
.zshrc
|
|
4
|
+
.moebial/
|
|
5
|
+
# Byte-compiled / optimized / DLL files
|
|
6
|
+
.DS_Store
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[codz]
|
|
9
|
+
*$py.class
|
|
10
|
+
|
|
11
|
+
# C extensions
|
|
12
|
+
*.so
|
|
13
|
+
config_new.toml
|
|
14
|
+
# Distribution / packaging
|
|
15
|
+
.Python
|
|
16
|
+
build/
|
|
17
|
+
develop-eggs/
|
|
18
|
+
dist/
|
|
19
|
+
downloads/
|
|
20
|
+
eggs/
|
|
21
|
+
.eggs/
|
|
22
|
+
lib/
|
|
23
|
+
lib64/
|
|
24
|
+
parts/
|
|
25
|
+
sdist/
|
|
26
|
+
var/
|
|
27
|
+
wheels/
|
|
28
|
+
share/python-wheels/
|
|
29
|
+
*.egg-info/
|
|
30
|
+
.installed.cfg
|
|
31
|
+
*.egg
|
|
32
|
+
MANIFEST
|
|
33
|
+
|
|
34
|
+
# PyInstaller
|
|
35
|
+
# Usually these files are written by a python script from a template
|
|
36
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
37
|
+
*.manifest
|
|
38
|
+
*.spec
|
|
39
|
+
|
|
40
|
+
# Installer logs
|
|
41
|
+
pip-log.txt
|
|
42
|
+
pip-delete-this-directory.txt
|
|
43
|
+
|
|
44
|
+
# Unit test / coverage reports
|
|
45
|
+
htmlcov/
|
|
46
|
+
.tox/
|
|
47
|
+
.nox/
|
|
48
|
+
.coverage
|
|
49
|
+
.coverage.*
|
|
50
|
+
.cache
|
|
51
|
+
nosetests.xml
|
|
52
|
+
coverage.xml
|
|
53
|
+
*.cover
|
|
54
|
+
*.py.cover
|
|
55
|
+
.hypothesis/
|
|
56
|
+
.pytest_cache/
|
|
57
|
+
cover/
|
|
58
|
+
|
|
59
|
+
# Translations
|
|
60
|
+
*.mo
|
|
61
|
+
*.pot
|
|
62
|
+
|
|
63
|
+
# Django stuff:
|
|
64
|
+
*.log
|
|
65
|
+
local_settings.py
|
|
66
|
+
db.sqlite3
|
|
67
|
+
db.sqlite3-journal
|
|
68
|
+
|
|
69
|
+
# Flask stuff:
|
|
70
|
+
instance/
|
|
71
|
+
.webassets-cache
|
|
72
|
+
|
|
73
|
+
# Scrapy stuff:
|
|
74
|
+
.scrapy
|
|
75
|
+
|
|
76
|
+
# Sphinx documentation
|
|
77
|
+
docs/_build/
|
|
78
|
+
|
|
79
|
+
# PyBuilder
|
|
80
|
+
.pybuilder/
|
|
81
|
+
target/
|
|
82
|
+
|
|
83
|
+
# Jupyter Notebook
|
|
84
|
+
.ipynb_checkpoints
|
|
85
|
+
|
|
86
|
+
# IPython
|
|
87
|
+
profile_default/
|
|
88
|
+
ipython_config.py
|
|
89
|
+
|
|
90
|
+
# pyenv
|
|
91
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
92
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
93
|
+
# .python-version
|
|
94
|
+
|
|
95
|
+
# pipenv
|
|
96
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
97
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
98
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
99
|
+
# install all needed dependencies.
|
|
100
|
+
# Pipfile.lock
|
|
101
|
+
|
|
102
|
+
# UV
|
|
103
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
104
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
105
|
+
# commonly ignored for libraries.
|
|
106
|
+
# uv.lock
|
|
107
|
+
|
|
108
|
+
# poetry
|
|
109
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
110
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
111
|
+
# commonly ignored for libraries.
|
|
112
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
113
|
+
# poetry.lock
|
|
114
|
+
# poetry.toml
|
|
115
|
+
|
|
116
|
+
# pdm
|
|
117
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
118
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
119
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
120
|
+
# pdm.lock
|
|
121
|
+
# pdm.toml
|
|
122
|
+
.pdm-python
|
|
123
|
+
.pdm-build/
|
|
124
|
+
|
|
125
|
+
# pixi
|
|
126
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
127
|
+
# pixi.lock
|
|
128
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
129
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
130
|
+
.pixi
|
|
131
|
+
|
|
132
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
133
|
+
__pypackages__/
|
|
134
|
+
|
|
135
|
+
# Celery stuff
|
|
136
|
+
celerybeat-schedule
|
|
137
|
+
celerybeat.pid
|
|
138
|
+
|
|
139
|
+
# Redis
|
|
140
|
+
*.rdb
|
|
141
|
+
*.aof
|
|
142
|
+
*.pid
|
|
143
|
+
|
|
144
|
+
# RabbitMQ
|
|
145
|
+
mnesia/
|
|
146
|
+
rabbitmq/
|
|
147
|
+
rabbitmq-data/
|
|
148
|
+
|
|
149
|
+
# ActiveMQ
|
|
150
|
+
activemq-data/
|
|
151
|
+
|
|
152
|
+
# SageMath parsed files
|
|
153
|
+
*.sage.py
|
|
154
|
+
|
|
155
|
+
# Environments
|
|
156
|
+
.env
|
|
157
|
+
.envrc
|
|
158
|
+
.vercel/
|
|
159
|
+
.venv
|
|
160
|
+
env/
|
|
161
|
+
venv/
|
|
162
|
+
ENV/
|
|
163
|
+
env.bak/
|
|
164
|
+
venv.bak/
|
|
165
|
+
|
|
166
|
+
# Spyder project settings
|
|
167
|
+
.spyderproject
|
|
168
|
+
.spyproject
|
|
169
|
+
|
|
170
|
+
# Rope project settings
|
|
171
|
+
.ropeproject
|
|
172
|
+
|
|
173
|
+
# mkdocs documentation
|
|
174
|
+
/site
|
|
175
|
+
|
|
176
|
+
# mypy
|
|
177
|
+
.mypy_cache/
|
|
178
|
+
.dmypy.json
|
|
179
|
+
dmypy.json
|
|
180
|
+
|
|
181
|
+
# Pyre type checker
|
|
182
|
+
.pyre/
|
|
183
|
+
|
|
184
|
+
# pytype static type analyzer
|
|
185
|
+
.pytype/
|
|
186
|
+
|
|
187
|
+
# Cython debug symbols
|
|
188
|
+
cython_debug/
|
|
189
|
+
|
|
190
|
+
# PyCharm
|
|
191
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
192
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
193
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
194
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
195
|
+
# .idea/
|
|
196
|
+
|
|
197
|
+
# Abstra
|
|
198
|
+
# Abstra is an AI-powered process automation framework.
|
|
199
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
200
|
+
# Learn more at https://abstra.io/docs
|
|
201
|
+
.abstra/
|
|
202
|
+
|
|
203
|
+
# Visual Studio Code
|
|
204
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
205
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
206
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
207
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
208
|
+
# .vscode/
|
|
209
|
+
|
|
210
|
+
# Ruff stuff:
|
|
211
|
+
.ruff_cache/
|
|
212
|
+
|
|
213
|
+
# PyPI configuration file
|
|
214
|
+
.pypirc
|
|
215
|
+
|
|
216
|
+
# Marimo
|
|
217
|
+
marimo/_static/
|
|
218
|
+
marimo/_lsp/
|
|
219
|
+
__marimo__/
|
|
220
|
+
|
|
221
|
+
# Streamlit
|
|
222
|
+
.streamlit/secrets.toml
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flywheel-bootstrap-staging
|
|
3
|
+
Version: 0.1.9.202601291439
|
|
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,78 @@
|
|
|
1
|
+
# Bootstrap
|
|
2
|
+
|
|
3
|
+
This package hosts the BYOC bootstrapper that:
|
|
4
|
+
|
|
5
|
+
- Ensures Codex is available (prefers release tarball; skips install if already
|
|
6
|
+
on `PATH`).
|
|
7
|
+
- Fetches the bootstrap payload for a run from the Flywheel backend.
|
|
8
|
+
- Launches `codex exec` with the provided prompt/config and streams logs.
|
|
9
|
+
- Collects artifacts (manifest-on-exit) and reports completion or error back to
|
|
10
|
+
the backend.
|
|
11
|
+
|
|
12
|
+
## Configuration
|
|
13
|
+
|
|
14
|
+
Bootstrap reads the user's Codex `config.toml` and requires one of:
|
|
15
|
+
|
|
16
|
+
```toml
|
|
17
|
+
[flywheel]
|
|
18
|
+
# inline instructions (host-specific tips, paths, sandbox notes)
|
|
19
|
+
workspace_instructions = """
|
|
20
|
+
Use /mnt/work as your workspace. Write artifacts under ./artifacts.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# or: reference a file (relative paths are resolved against the config file directory)
|
|
24
|
+
workspace_instructions_file = "workspace_notes.md"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Rules:
|
|
28
|
+
|
|
29
|
+
- At least one of `workspace_instructions` or `workspace_instructions_file` is
|
|
30
|
+
required; otherwise bootstrap exits before contacting the server.
|
|
31
|
+
- If both are set, the file wins and the inline value is ignored (warns once).
|
|
32
|
+
- File contents must be non-empty; the path is resolved relative to the config
|
|
33
|
+
file if not absolute.
|
|
34
|
+
|
|
35
|
+
Prompt assembly written to `flywheel_prompt.txt`:
|
|
36
|
+
|
|
37
|
+
1. Flywheel engineer context (logging/artifact expectations).
|
|
38
|
+
2. Task Description (prompt fetched from the server).
|
|
39
|
+
3. Workspace Instructions (resolved from config as above).
|
|
40
|
+
|
|
41
|
+
Example config: `project/bootstrap/examples/config.example.toml` can be used as
|
|
42
|
+
a starting point; update the paths and instructions for your machine.
|
|
43
|
+
|
|
44
|
+
## End-to-end flow (bootstrap.sh → Python bootstrapper)
|
|
45
|
+
|
|
46
|
+
1. User runs `bash ./bootstrap.sh --run-id <id> --token <token> --config /path/to/config.toml [--server <url>]` on their BYOC machine.
|
|
47
|
+
2. The shim:
|
|
48
|
+
- Ensures `uvx` is available (installs via `https://astral.sh/uv/install.sh` if missing, then rechecks PATH with `~/.cargo/bin`).
|
|
49
|
+
- Points `PKG_PATH` to the local repo copy `project/bootstrap`.
|
|
50
|
+
- Executes `uvx --no-cache --from "$PKG_PATH" flywheel-bootstrap "$@"` so the latest local package runs.
|
|
51
|
+
3. Python entrypoint (`python -m bootstrap`):
|
|
52
|
+
- Parses args/env: requires run id + token, required `--config`, optional `--server` (default `http://localhost:8000`).
|
|
53
|
+
- Loads Codex config.toml, enforces presence of workspace instructions (inline or file), extracts workspace/sandbox settings.
|
|
54
|
+
4. Workspace resolution:
|
|
55
|
+
- Uses `cd`/`workspace_dir` from config if set; otherwise `~/.flywheel/runs/<run_id>`.
|
|
56
|
+
- Creates the workspace and validates the artifact manifest path is inside sandbox `writable_roots` when sandboxing is enabled; else exits with an error.
|
|
57
|
+
5. Codex availability:
|
|
58
|
+
- If `BOOTSTRAP_MOCK_CODEX` is set, skips install and runs a mock flow.
|
|
59
|
+
- Else, if `codex` is already on PATH, reuse it; otherwise download the Codex release tarball to the workspace/run root and mark it executable.
|
|
60
|
+
6. Fetch bootstrap payload:
|
|
61
|
+
- `GET <server>/runs/<run_id>/bootstrap` with `X-Run-Token`; payload contains the task prompt.
|
|
62
|
+
7. Build prompt file:
|
|
63
|
+
- Combine base Flywheel engineer context, “Task Description” (server prompt), and “Workspace Instructions” (user config) into `flywheel_prompt.txt` in the workspace.
|
|
64
|
+
8. Launch Codex:
|
|
65
|
+
- Run `codex exec --json --cd <workspace> --skip-git-repo-check flywheel_prompt.txt` with env `FLYWHEEL_RUN_ID/TOKEN/SERVER`.
|
|
66
|
+
- Start a heartbeat thread posting `/runs/{id}/heartbeat` every 30s.
|
|
67
|
+
- Stream Codex stdout lines as logs to `/runs/{id}/logs`; capture Codex `run_id` if emitted.
|
|
68
|
+
9. After Codex exits:
|
|
69
|
+
- Read `flywheel_artifacts.json`; if empty and Codex `run_id` is known, attempt one `codex resume <id>` then re-read.
|
|
70
|
+
- POST artifacts to `/runs/{id}/artifacts`; POST `/complete` on exit 0, else `/error` with the exit code.
|
|
71
|
+
- Stop/join the heartbeat thread.
|
|
72
|
+
10. Mock mode (`BOOTSTRAP_MOCK_CODEX=1`):
|
|
73
|
+
- Sends a heartbeat, a few logs, writes a mock artifact manifest, returns 0 (used in e2e tests).
|
|
74
|
+
|
|
75
|
+
## Next steps
|
|
76
|
+
|
|
77
|
+
- Publish bootstrap package and switch `uvx --from` to a release URL.
|
|
78
|
+
- Iterate on prompts / general polish
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""CLI entry point for the bootstrap flow.
|
|
2
|
+
|
|
3
|
+
Usage (placeholder):
|
|
4
|
+
python -m bootstrap --run-id <id> --token <token> --config /path/to/config.toml
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from bootstrap.orchestrator import BootstrapOrchestrator, build_config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parse_args(argv: list[str]) -> argparse.Namespace:
|
|
16
|
+
parser = argparse.ArgumentParser(description="Flywheel BYOC bootstrapper")
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--run-id",
|
|
19
|
+
help="Run identifier issued by the Flywheel backend (falls back to FLYWHEEL_RUN_ID)",
|
|
20
|
+
default=None,
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--token",
|
|
24
|
+
help="Capability token for authenticating to the backend (or FLYWHEEL_RUN_TOKEN)",
|
|
25
|
+
default=None,
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--server",
|
|
29
|
+
help="Backend base URL (default: http://localhost:8000 or FLYWHEEL_SERVER)",
|
|
30
|
+
default=None,
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--config",
|
|
34
|
+
help="Path to the Codex config.toml file",
|
|
35
|
+
required=True,
|
|
36
|
+
)
|
|
37
|
+
return parser.parse_args(argv)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main(argv: list[str] | None = None) -> int:
|
|
41
|
+
args = _parse_args(argv or sys.argv[1:])
|
|
42
|
+
config = build_config(args)
|
|
43
|
+
orchestrator = BootstrapOrchestrator(config)
|
|
44
|
+
return orchestrator.run()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Artifact manifest helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Mapping, Sequence
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ManifestStatus(Enum):
|
|
16
|
+
"""Outcome of reading the artifact manifest."""
|
|
17
|
+
|
|
18
|
+
MISSING = "missing"
|
|
19
|
+
VALID = "valid"
|
|
20
|
+
MALFORMED = "malformed"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ManifestResult:
|
|
25
|
+
"""Result of reading the artifact manifest, with diagnostic info."""
|
|
26
|
+
|
|
27
|
+
status: ManifestStatus
|
|
28
|
+
artifacts: Sequence[Mapping[str, object]]
|
|
29
|
+
error: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def read_manifest(manifest_path: Path) -> ManifestResult:
|
|
33
|
+
"""Load artifact entries from the manifest path.
|
|
34
|
+
|
|
35
|
+
Tolerant of common LLM output variations:
|
|
36
|
+
- A well-formed JSON list is returned as-is.
|
|
37
|
+
- A dict wrapping a list (e.g. ``{"artifacts": [...]}``) is unwrapped.
|
|
38
|
+
- A single artifact dict is wrapped in a list.
|
|
39
|
+
- Truncated / invalid JSON is reported as malformed.
|
|
40
|
+
- Non-dict, non-list scalars are reported as malformed.
|
|
41
|
+
|
|
42
|
+
Returns a ``ManifestResult`` carrying the parsed artifacts, the outcome
|
|
43
|
+
status, and an optional human-readable error description for feedback.
|
|
44
|
+
"""
|
|
45
|
+
if not manifest_path.exists():
|
|
46
|
+
return ManifestResult(status=ManifestStatus.MISSING, artifacts=[])
|
|
47
|
+
raw = manifest_path.read_text(encoding="utf-8")
|
|
48
|
+
if not raw.strip():
|
|
49
|
+
msg = "artifact manifest file is empty"
|
|
50
|
+
logger.warning("%s: %s", msg, manifest_path)
|
|
51
|
+
return ManifestResult(status=ManifestStatus.MALFORMED, artifacts=[], error=msg)
|
|
52
|
+
try:
|
|
53
|
+
data = json.loads(raw)
|
|
54
|
+
except json.JSONDecodeError as exc:
|
|
55
|
+
msg = f"artifact manifest contains invalid JSON: {exc}"
|
|
56
|
+
logger.warning("%s: %s", msg, manifest_path)
|
|
57
|
+
return ManifestResult(status=ManifestStatus.MALFORMED, artifacts=[], error=msg)
|
|
58
|
+
return _coerce_manifest(data, manifest_path)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _coerce_manifest(data: object, manifest_path: Path) -> ManifestResult:
|
|
62
|
+
"""Best-effort coercion of parsed JSON into a list of artifact dicts."""
|
|
63
|
+
if isinstance(data, list):
|
|
64
|
+
return ManifestResult(status=ManifestStatus.VALID, artifacts=data)
|
|
65
|
+
if isinstance(data, dict):
|
|
66
|
+
return _unwrap_dict(data, manifest_path)
|
|
67
|
+
msg = f"artifact manifest is a {type(data).__name__}, expected a JSON list"
|
|
68
|
+
logger.warning("%s: %s", msg, manifest_path)
|
|
69
|
+
return ManifestResult(status=ManifestStatus.MALFORMED, artifacts=[], error=msg)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _unwrap_dict(data: dict[str, object], manifest_path: Path) -> ManifestResult:
|
|
73
|
+
"""Extract an artifact list from a dict, or treat it as a single artifact."""
|
|
74
|
+
# If the dict itself looks like an artifact, treat it as one.
|
|
75
|
+
# Check this BEFORE scanning for nested lists — a single artifact dict
|
|
76
|
+
# like {"artifact_type": "text", "payload": {"items": [...]}} must not
|
|
77
|
+
# have its nested list mistakenly extracted.
|
|
78
|
+
if "artifact_type" in data:
|
|
79
|
+
msg = "artifact manifest is a single artifact dict, wrapping in list"
|
|
80
|
+
logger.warning("%s: %s", msg, manifest_path)
|
|
81
|
+
return ManifestResult(
|
|
82
|
+
status=ManifestStatus.MALFORMED, artifacts=[data], error=msg
|
|
83
|
+
)
|
|
84
|
+
# Prefer the "artifacts" key if present and is a list.
|
|
85
|
+
if "artifacts" in data and isinstance(data["artifacts"], list):
|
|
86
|
+
msg = "artifact manifest wrapped in dict with 'artifacts' key, unwrapping"
|
|
87
|
+
logger.warning("%s: %s", msg, manifest_path)
|
|
88
|
+
return ManifestResult(
|
|
89
|
+
status=ManifestStatus.MALFORMED, artifacts=data["artifacts"], error=msg
|
|
90
|
+
)
|
|
91
|
+
# Fall back to the first value that is a list.
|
|
92
|
+
for key, value in data.items():
|
|
93
|
+
if isinstance(value, list):
|
|
94
|
+
msg = f"artifact manifest wrapped in dict with '{key}' key, unwrapping"
|
|
95
|
+
logger.warning("%s: %s", msg, manifest_path)
|
|
96
|
+
return ManifestResult(
|
|
97
|
+
status=ManifestStatus.MALFORMED, artifacts=value, error=msg
|
|
98
|
+
)
|
|
99
|
+
msg = "artifact manifest is a dict with no recognisable artifact data"
|
|
100
|
+
logger.warning("%s: %s", msg, manifest_path)
|
|
101
|
+
return ManifestResult(status=ManifestStatus.MALFORMED, artifacts=[], error=msg)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Codex config parsing helpers (skeleton)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
import tomllib
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class UserConfig:
|
|
13
|
+
"""Parsed subset of Codex config relevant to bootstrap."""
|
|
14
|
+
|
|
15
|
+
raw: Mapping[str, Any]
|
|
16
|
+
working_dir: Path | None
|
|
17
|
+
sandbox_mode: str | None
|
|
18
|
+
approval_policy: str | None
|
|
19
|
+
oss_provider: str | None
|
|
20
|
+
writable_roots: tuple[Path, ...]
|
|
21
|
+
workspace_instructions: str
|
|
22
|
+
instructions_source: str
|
|
23
|
+
warnings: tuple[str, ...] = ()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_codex_config(path: Path) -> UserConfig:
|
|
27
|
+
"""Load and extract relevant fields from the user's Codex config."""
|
|
28
|
+
|
|
29
|
+
with path.open("rb") as fp:
|
|
30
|
+
data = tomllib.load(fp)
|
|
31
|
+
|
|
32
|
+
flywheel_raw = data.get("flywheel")
|
|
33
|
+
flywheel_section: Mapping[str, Any] = (
|
|
34
|
+
flywheel_raw if isinstance(flywheel_raw, Mapping) else {}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
inline_instructions = _get_str(flywheel_section, "workspace_instructions")
|
|
38
|
+
instructions_file = _get_path(flywheel_section, "workspace_instructions_file", path)
|
|
39
|
+
|
|
40
|
+
warnings: list[str] = []
|
|
41
|
+
if instructions_file is not None and inline_instructions:
|
|
42
|
+
warnings.append(
|
|
43
|
+
"workspace_instructions ignored because workspace_instructions_file is set"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if instructions_file is not None:
|
|
47
|
+
try:
|
|
48
|
+
instructions_text = instructions_file.read_text(encoding="utf-8").strip()
|
|
49
|
+
except FileNotFoundError as exc:
|
|
50
|
+
raise SystemExit(
|
|
51
|
+
f"workspace_instructions_file not found: {instructions_file}"
|
|
52
|
+
) from exc
|
|
53
|
+
if not instructions_text:
|
|
54
|
+
raise SystemExit(
|
|
55
|
+
f"workspace_instructions_file is empty: {instructions_file}"
|
|
56
|
+
)
|
|
57
|
+
source = "file"
|
|
58
|
+
else:
|
|
59
|
+
instructions_text = inline_instructions.strip() if inline_instructions else ""
|
|
60
|
+
source = "inline"
|
|
61
|
+
|
|
62
|
+
if not instructions_text:
|
|
63
|
+
raise SystemExit(
|
|
64
|
+
"workspace instructions are required; set [flywheel].workspace_instructions "
|
|
65
|
+
"or [flywheel].workspace_instructions_file"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Best-effort extraction; Codex config schema may evolve.
|
|
69
|
+
working_dir = _get_path(data, "cd") or _get_path(data, "workspace_dir")
|
|
70
|
+
sandbox_mode = (
|
|
71
|
+
data.get("sandbox_mode") if isinstance(data.get("sandbox_mode"), str) else None
|
|
72
|
+
)
|
|
73
|
+
approval_policy = (
|
|
74
|
+
data.get("approval_policy")
|
|
75
|
+
if isinstance(data.get("approval_policy"), str)
|
|
76
|
+
else None
|
|
77
|
+
)
|
|
78
|
+
oss_provider = (
|
|
79
|
+
data.get("oss_provider") if isinstance(data.get("oss_provider"), str) else None
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
writable_roots: tuple[Path, ...] = tuple()
|
|
83
|
+
sandbox_write = data.get("sandbox_workspace_write")
|
|
84
|
+
if isinstance(sandbox_write, dict):
|
|
85
|
+
roots = sandbox_write.get("writable_roots")
|
|
86
|
+
if isinstance(roots, list):
|
|
87
|
+
writable_roots = tuple(
|
|
88
|
+
Path(str(r)).expanduser().resolve() for r in roots if isinstance(r, str)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return UserConfig(
|
|
92
|
+
raw=data,
|
|
93
|
+
working_dir=working_dir,
|
|
94
|
+
sandbox_mode=sandbox_mode,
|
|
95
|
+
approval_policy=approval_policy,
|
|
96
|
+
oss_provider=oss_provider,
|
|
97
|
+
writable_roots=writable_roots,
|
|
98
|
+
workspace_instructions=instructions_text,
|
|
99
|
+
instructions_source=source,
|
|
100
|
+
warnings=tuple(warnings),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _get_path(
|
|
105
|
+
data: Mapping[str, Any], key: str, relative_to: Path | None = None
|
|
106
|
+
) -> Path | None:
|
|
107
|
+
value = data.get(key)
|
|
108
|
+
if isinstance(value, str) and value:
|
|
109
|
+
path = Path(value).expanduser()
|
|
110
|
+
if not path.is_absolute() and relative_to is not None:
|
|
111
|
+
path = (relative_to.parent / path).resolve()
|
|
112
|
+
else:
|
|
113
|
+
path = path.resolve()
|
|
114
|
+
return path
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _get_str(data: Mapping[str, Any], key: str) -> str | None:
|
|
119
|
+
value = data.get(key)
|
|
120
|
+
if isinstance(value, str) and value:
|
|
121
|
+
return value
|
|
122
|
+
return None
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Shared constants for the bootstrap flow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
DEFAULT_SERVER_URL = "http://localhost:8000"
|
|
8
|
+
DEFAULT_RUN_ROOT = Path.home() / ".flywheel" / "runs"
|
|
9
|
+
DEFAULT_ARTIFACT_MANIFEST = "flywheel_artifacts.json"
|
|
10
|
+
HEARTBEAT_INTERVAL_SECONDS = 30
|
|
11
|
+
MAX_ARTIFACT_RETRIES = 2
|
|
12
|
+
|
|
13
|
+
# Environment variables that let the backend command override defaults.
|
|
14
|
+
ENV_SERVER_URL = "FLYWHEEL_SERVER"
|
|
15
|
+
ENV_RUN_ID = "FLYWHEEL_RUN_ID"
|
|
16
|
+
ENV_RUN_TOKEN = "FLYWHEEL_RUN_TOKEN"
|
|
17
|
+
|
|
18
|
+
# Codex download
|
|
19
|
+
DEFAULT_CODEX_VERSION = None # latest
|
|
20
|
+
CODEX_RELEASE_BASE = "https://github.com/openai/codex/releases/latest/download"
|