cairn-operator-dataflow-skypilot 0.1.3__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.
@@ -0,0 +1,100 @@
1
+ # Secrets
2
+ .env
3
+ prod/.env.prod
4
+ .env.local
5
+ .env.*.local
6
+
7
+ # Python
8
+ __pycache__/
9
+ *.py[cod]
10
+ *$py.class
11
+ *.so
12
+ .Python
13
+ *.egg-info/
14
+ .installed.cfg
15
+ *.egg
16
+ .pytest_cache/
17
+ .mypy_cache/
18
+ .ruff_cache/
19
+ .coverage
20
+ .coverage.*
21
+ htmlcov/
22
+
23
+ # Virtualenvs
24
+ .venv/
25
+ venv/
26
+ env/
27
+
28
+ # IDE
29
+ .vscode/
30
+ .idea/
31
+ *.swp
32
+ *.swo
33
+
34
+ # OS
35
+ .DS_Store
36
+ Thumbs.db
37
+
38
+ # Data caches
39
+ data/chroma/
40
+ *.log
41
+
42
+ # Eval reports
43
+ evals/last_report.json
44
+ evals/*.report.json
45
+
46
+ # Build
47
+ build/
48
+ dist/
49
+ # The platform image's build context (#2514): the public wheels the ship step copies beside
50
+
51
+ # Observability sinks (src/obs)
52
+ events.jsonl
53
+ replays/
54
+
55
+ # CLI run artifacts: trace dirs (cairn run) + the hash-chained audit log.
56
+ .cairn/
57
+ audit.jsonl
58
+
59
+ # DuckDB local analytics (scripts/dashboard.py)
60
+ obs.duckdb
61
+ *.duckdb.wal
62
+ *.duckdb.tmp/
63
+
64
+ # Optional external MCP backend — operators drop this in to enable CAIRN_BACKEND=otel.
65
+ # Without it, the selector falls back to the mock backend with a warning.
66
+ src/mcp/tools/_backends/otel.py
67
+ tests/test_otel_backend.py
68
+
69
+ # Refactor scratch — incident agent package split kept on disk for reference
70
+ # but not part of the shipped tree. __init__.py files were removed so Python
71
+ # imports src/agents/incident.py (the working single-file module) instead.
72
+ src/agents/incident/
73
+
74
+
75
+ .cache/
76
+ .chainlit/
77
+ .files/
78
+ chainlit_app.py
79
+ chainlit.md
80
+ .deepeval/
81
+ .langgraph_api/
82
+ prod/
83
+
84
+ # stale pre-rename data dir (runtime traces); data now lives in .cairn/
85
+ .obs-agent/
86
+
87
+ # Never commit a secrets file under any name (an untracked .env backup once held
88
+ # the prod Postgres password, JWT secret and admin hash), nor local work areas.
89
+ .env.*
90
+ !.env.example
91
+ !deploy/light/.env.infisical.example
92
+ # the feature-proof profile's generated keys (scripts/oss/proof_seed.py), never committed
93
+ deploy/light/.proof/
94
+ .proof-out/
95
+ *.bak
96
+ /private/
97
+ /site/
98
+ /data/
99
+ # the generated hello-cairn proof report (scripts/oss/feature_proof.sh)
100
+ /FEATURE_PROOF.md
@@ -0,0 +1,168 @@
1
+ # cairn-operator-dataflow-skypilot — design
2
+
3
+ - **Distribution:** `cairn-operator-dataflow-skypilot` (`cairn/operators/optional/dataflow_skypilot/`),
4
+ offered by `cairndev[skypilot]`
5
+ - **Capabilities provided:** none as operators — it registers the `skypilot` dataflow engine
6
+ (`cairn.dataflow_engines`), which `dataflow.run` and `ml.run` submit to when selected
7
+ - **Status:** experimental — unit-tested against a fake SkyPilot SDK; not in the hello-cairn
8
+ feature proof
9
+ - **Decisions:** ADR-0104 D5 (SkyPilot as the provisioning engine), ADR-0099 §1 (derive, refuse,
10
+ never provision), ADR-0134 D3 (an optional wheel; its launch check is the engine-neutral
11
+ flow-engine door asking this engine's `check_ready()`), #2073, #2102
12
+
13
+ ## 1. Purpose
14
+
15
+ Run a cairn flow on a machine this engine ACQUIRES — an SSH node pool built from a
16
+ `compute.ssh` box, or a Vast / RunPod / AWS instance — and give it back afterwards. It is the
17
+ engine for an environment that has no standing compute. It does NOT add operators, govern, or
18
+ register artifacts (the `dataflow/run` / `ml/run` operators of cairn-operator-dataflow do), and
19
+ it never provisions to make a readiness check pass. Until ADR-0134 it lived inside
20
+ cairn-operator-dataflow; it moved out so a default `cairndev` install carries no cloud-SDK
21
+ adapter.
22
+
23
+ ## 2. Architecture
24
+
25
+ ```
26
+ dataflow/run | ml/run (cairn-operator-dataflow; governance, budget, artifacts)
27
+ │ resolve_engine(kind) — core: cairn.core.dataflow.engine_registry
28
+ ▼
29
+ skypilot_engine(kind, target) factory.py — reads the skypilot_* backend keys
30
+ ▼
31
+ SkyPilotEngine.submit/poll/result/cancel engines_skypilot.py — sky.launch / sky.status / sky.down
32
+ │ the task YAML runs cairn-dataflow-run (cairn-operator-dataflow) on the machine
33
+ ▼
34
+ SSH node pool | Vast | RunPod | AWS
35
+ ```
36
+
37
+ Before any of that, the launch door (`cairn.core.dataflow.engine_door`, ADR-0134 D3c) builds
38
+ this engine and awaits `SkyPilotEngine.check_ready()`: a target is set, the extra is
39
+ importable, `ssh` and `rsync` are on PATH, the target resolves to an infra, that infra is
40
+ enabled (`sky check`). Imports: `cairn.core.*` only (the operator contract,
41
+ `cairn.core.dataflow` incl. the shared `resources` helpers); never another operator pack; `sky`
42
+ lazily.
43
+
44
+ The one engine that *acquires* the machine. Three protocol calls map onto three
45
+ SkyPilot calls — `submit`→`sky.launch`, `poll`→`sky.status`, `cancel`→`sky.down`
46
+ — and nothing above the engine changes: the spec still travels as
47
+ `CAIRN_DATAFLOW_SPEC`, the machine still runs `cairn-dataflow-run`, the curve
48
+ still streams over the job channel. Five properties are this module's alone,
49
+ because nothing above it can enforce them:
50
+
51
+ 1. **The cost estimate runs BEFORE `sky.launch`** and refuses; a refused submit
52
+ has provisioned nothing.
53
+ 2. **Every terminal state tears the cluster down, exactly once** — success,
54
+ failure and cancellation alike.
55
+ 3. **The cluster carries its own deadline.** Autostop (as autoDOWN, so the disk
56
+ goes too) is set from the run's `budget.max_seconds` plus a grace window, or
57
+ a fixed 120-minute ceiling when the run declared no ceiling. It is the last
58
+ line of defence, not the first — it covers only the case cancel and teardown
59
+ cannot: a control plane that dies mid-run.
60
+ 4. **Every teardown writes a receipt** (cluster, infra, resources, duration,
61
+ estimate, and how many outputs came home) as a `skypilot.receipt` run event,
62
+ so what a run bought is on the run's own timeline instead of in a cloud
63
+ console.
64
+ 5. **The result comes home before the machine stops existing** (#2102). The
65
+ flow runs in `$HOME/.cairn-jobs/<cluster>` — named explicitly, because
66
+ `cairn-dataflow-run` writes `result.json`/`metrics.json` into `os.getcwd()`
67
+ and SkyPilot's run script only does `cd ~/sky_workdir`, a directory that
68
+ need not exist. On every terminal state *and on cancel*, `_teardown` rsyncs
69
+ the declared outputs off the cluster **before** `sky down`; fetching after
70
+ would lose the product with the disk.
71
+
72
+ The SDK sits behind one seam (`sky_sdk()`, overridable with
73
+ `CAIRN_SKY_SDK_FACTORY`, mirroring `compute_lifecycle.ec2_client`) because a
74
+ live `sky launch` provisions a paid machine: the adapter has to be provable
75
+ without one. The fetch has its own seam (`fetch=`) for the same reason.
76
+
77
+ `SkyPilotEngine.result()` reads the fetched `result.json` and is byte-for-byte
78
+ the SSH engine's contract, through the SSH engine's own functions —
79
+ `fetch_list()` (which files), `fetch_dir()` (where they land:
80
+ `$TMPDIR/cairn-remote-<cluster>`, deterministic so a restarted control plane
81
+ finds them) and `rewrite_fetched()` (remote workdir paths in `stats` → the
82
+ local copies). One result contract, two transports: a pipeline hoist and the
83
+ next leg cannot tell which engine ran this one. A missing `result.json` is a
84
+ loud refusal naming the receipt to check, never an empty result.
85
+
86
+ **Why rsync and not the SDK.** Checked against skypilot 0.13.0: `sky.client.sdk`
87
+ has no generic file-download call — `download_logs` returns the job's *logs*,
88
+ not the workdir. What SkyPilot does write, for every cluster it manages, is a
89
+ per-cluster SSH stanza at `~/.sky/generated/ssh/<cluster>`
90
+ (`SSHConfigHelper.add_cluster`; `SKY_USER_FILE_PATH = '~/.sky/generated'`)
91
+ whose `Host` is the cluster name and which already carries the address, user,
92
+ key, port and any proxy command. `rsync -e "ssh -F <that file>"` is therefore
93
+ the transport SkyPilot itself uses, with nothing re-derived here. It is also
94
+ why the door refuses when `ssh`/`rsync` are missing (below).
95
+
96
+ Target resolution turns a cairn connection into a SkyPilot infra. The door asks
97
+ for the infra *name* only (`materialize=False`) — a dry run must not put a
98
+ private key or an API token on disk; only a real submit writes these:
99
+
100
+ | Connection kind | Becomes | What lands on disk |
101
+ |---|---|---|
102
+ | `compute.ssh` | `infra: ssh/<pool>` | one entry in `~/.sky/ssh_node_pools.yaml` (merged, never clobbered) + the key at `~/.sky/cairn-keys/<pool>.pem`, mode 0600 — SkyPilot reads the key as a PATH |
103
+ | `compute.vast` | `infra: vast` | `~/.config/vastai/vast_api_key`, mode 0600 — the only place SkyPilot's Vast provider reads it |
104
+ | `compute.runpod` | `infra: runpod` | `~/.runpod/config.toml`, mode 0600 — what `runpod config` writes |
105
+ | any connection holding `AWS_ACCESS_KEY_ID` | `infra: aws` | nothing: the keys are passed as process env for the SDK call only |
106
+
107
+ ## 3. Operators
108
+
109
+ None. The pack registers one `cairn.dataflow_engines` entry, `skypilot` →
110
+ `operator_dataflow_skypilot.factory:skypilot_engine`. The operators that reach it are
111
+ `dataflow/run` and `ml/run` in cairn-operator-dataflow.
112
+
113
+ ## 4. Configuration
114
+
115
+ | Key | Kind | Scope | Default | Meaning |
116
+ |---|---|---|---|---|
117
+ | `ml_engine` / `dataflow_engine` = `skypilot` | backend field | deployment | `builtin` | select this engine for a flow kind |
118
+ | `ml_target` / `dataflow_target` | backend field | deployment | `` | the compute connection to provision on — required; empty is a refusal at the door |
119
+ | `skypilot_image` / `CAIRN_SKYPILOT_IMAGE` | backend field | deployment | `` | image the provisioned machine runs; empty = a `pip install` setup step (slower, driftable) |
120
+ | `skypilot_accelerator` / `CAIRN_SKYPILOT_ACCELERATOR` | backend field | deployment | `` | the accelerator TYPE the target offers (`A100`, `T4`); the COUNT is the stage's `gpu` hint, sent as `<type>:<count>`. A flow asking for GPUs with this empty is refused |
121
+ | `skypilot_use_spot` / `CAIRN_SKYPILOT_USE_SPOT` | backend field | deployment | `false` | ask for pre-emptible capacity; a reclaimed machine surfaces as a RETRIABLE failure |
122
+ | `CAIRN_SKYPILOT_SETUP` | env | deployment | `pip install 'cairndev[dataflow-engine]'` | override the setup line for a bare VM |
123
+ | `CAIRN_SKYPILOT_HOURLY_USD` | env | deployment | — | price used for the pre-launch estimate when the connection declares none |
124
+ | `CAIRN_SKY_SDK_FACTORY` | env | dev/test | — | point the SkyPilot seam at a fake; unset in production |
125
+
126
+ ## 5. Governance
127
+
128
+ Reached only through `dataflow/run` / `ml/run`, so every flow leg passes `enforce_flow_gate` and
129
+ the flow-engine door before any record exists. The pre-launch cost estimate is checked against
130
+ the leg's budget before `sky launch`; the cluster is torn down on cancel and after the result is
131
+ fetched; a receipt (`skypilot.receipt`) is recorded per run. The environment allowlist is core's
132
+ engine-neutral set only — no Metaflow configuration and no ambient credential reaches a
133
+ provisioned machine (ADR-0134 D3b).
134
+
135
+ ## 6. Failure modes
136
+
137
+ | Failure | Cause | Effect | Recovered? |
138
+ |---|---|---|---|
139
+ | Engine not installed | `skypilot` selected without `cairndev[skypilot]` | refused at the launch door, naming the extra | n/a |
140
+ | No target | `ml_target`/`dataflow_target` empty | `check_ready()` refusal at the door | n/a |
141
+ | `ssh`/`rsync` missing (#2102) | the image carries the extra but not the binaries | `check_ready()` refusal | n/a — install them in the image |
142
+ | Infra not enabled | `sky check` does not list the target's infra | `check_ready()` refusal | n/a |
143
+ | Pre-emption | a spot machine reclaimed | the leg fails; no preemption recovery yet | GAP |
144
+ | Cluster left running after a cancelled or failed run (#2073) | a provisioner with no teardown on the terminal path | every terminal state and `cancel` call `sky down` once and write a receipt | yes |
145
+ | Cluster outlives the control plane (#2073) | the process that would tear it down died | autostop/autodown, derived from `budget.max_seconds` + grace (default 120 min) | yes — but it is a backstop; no receipt is written for a cluster the control plane never saw end |
146
+ | A run launches over budget (#2073) | no pre-launch cost check existed for a flow spec | `EngineError` naming the estimate, the ceiling and the infra; nothing provisioned | yes |
147
+ | A run's cost cannot be priced (#2073) | neither the connection's `hourly_usd` nor `CAIRN_SKYPILOT_HOURLY_USD` is set | launches, and the receipt records `priced: false` | no — deliberate: refusing every unpriced run would make the engine unusable, and the receipt makes the gap visible |
148
+ | SkyPilot forgets a cluster it launched (#2073) | its state is a SQLite db under `~/.sky`, which was not on a volume — a control-plane restart lost the cluster records, the SSH node pool and the materialised key | the Helm chart persists `/root/.sky` on the same claim, and REFUSES to render `CAIRN_ML_ENGINE=skypilot` with persistence disabled | yes — but only for the chart; any other deployment shape must mount that path itself, or `sky down` cannot reach a machine that is still billing |
149
+ | The door refused every launch on a good install (#2073) | `sky.enabled_clouds` does not exist at the top level — it is `sky.client.sdk.enabled_clouds`, and the door turns any exception into a refusal | the adapter calls the path that exists; verified against skypilot 0.13 in a throwaway venv | yes |
150
+ | A GPU request asked for an accelerator named "1" (#2073) | `accelerators` was the COUNT alone; SkyPilot wants `<type>:<count>` and parses a bare count as a device NAME (`{'1': 1}`) | the count comes from the stage hint, the TYPE from `skypilot_accelerator`; a GPU request without a type is refused before launch | yes — and the pack's own test had asserted the broken value |
151
+ | The door passed with NOTHING enabled (#2102) | `check_ready` guarded the enabled-infra check with `if enabled and …`, so an empty `sky check` skipped it; seen live in the deployed control plane (`enabled_infra() == []`, verdict ok) | an empty `sky check` is a refusal naming `sky check` and, for an SSH pool, `sky ssh up <pool>`; the membership check always runs | yes |
152
+ | A control plane with the extra installed could launch nothing (#2102) | `Dockerfile.engine` is `python:3.13-slim-trixie`, which ships neither `ssh` nor `rsync`; SkyPilot shells out to both for every node it provisions or adopts and `sky ssh up` needs them (asyncssh is pure Python and does not help). Seen live in the chart-installed pod: `command -v ssh` → nothing | the image installs `openssh-client` + `rsync` (with a build-time `command -v` gate), and the door refuses NAMING the missing binary | yes |
153
+ | A flow leg on SkyPilot could never hand its product to the next leg (#2102) | `result()` raised: the lifecycle shipped without the artifact fetch-back, so only the live metric stream came home | `_teardown` rsyncs the declared outputs off the cluster BEFORE `sky down` (on cancel too), over SkyPilot's own generated per-cluster ssh config; `result()` reads them through the SSH engine's `fetch_list`/`fetch_dir`/`rewrite_fetched` | yes — but only against fakes; the rsync itself has never met a real cluster |
154
+
155
+ ## 7. Testing
156
+
157
+ `tests/test_skypilot_engine.py` drives a fake SkyPilot SDK (`CAIRN_SKY_SDK_FACTORY` seam): task
158
+ YAML, target resolution, the receipt, teardown, the fetch, and `check_ready()` including the
159
+ empty-target refusal. `tests/test_skypilot_env_allowlist.py` pins the allowlist. No test talks
160
+ to a real cloud — a live `sky launch` provisions a paid machine.
161
+
162
+ ## 8. History
163
+
164
+ | Date | Change | Issue |
165
+ |---|---|---|
166
+ | 2026-09 | SkyPilot as the provisioning engine (ADR-0104 D5), in cairn-operator-dataflow | #2073, #2102 |
167
+ | 2026-10-16 | The launch check moves from a core SkyPilot door to `check_ready()` behind the engine-neutral flow-engine door (ADR-0134 D3c) | #2683 |
168
+ | 2026-10-16 | Its own optional wheel behind `cairndev[skypilot]` (ADR-0134 D3a) | #2678 |
@@ -0,0 +1,202 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "{}"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 The cairn Authors
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
202
+
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.5
2
+ Name: cairn-operator-dataflow-skypilot
3
+ Version: 0.1.3
4
+ Summary: The SkyPilot provisioning dataflow engine (ADR-0104 D5) for cairn's ml and data flows — offered by cairndev[skypilot] (ADR-0134 D3).
5
+ Project-URL: Homepage, https://cairndev.sh
6
+ Project-URL: Documentation, https://docs.cairndev.sh
7
+ Project-URL: Source, https://github.com/cairndev-sh/cairn
8
+ Project-URL: Issues, https://github.com/cairndev-sh/cairn/issues
9
+ Author: cairn maintainers
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Requires-Python: >=3.12
13
+ Provides-Extra: skypilot
14
+ Requires-Dist: skypilot[aws,kubernetes,runpod,ssh,vast]>=0.13; extra == 'skypilot'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # cairn-operator-dataflow-skypilot — see DESIGN.md + ADR-0104 D5, ADR-0134 D3.
18
+
19
+ The SkyPilot provisioning engine for cairn: `pip install 'cairndev[skypilot]'`, set
20
+ `[profile.backends] ml_engine = "skypilot"` (or `dataflow_engine`) and point `ml_target` /
21
+ `dataflow_target` at a compute connection. The launch door asks this engine's `check_ready()`
22
+ before any record exists — nothing is provisioned to make the check pass.
@@ -0,0 +1,6 @@
1
+ # cairn-operator-dataflow-skypilot — see DESIGN.md + ADR-0104 D5, ADR-0134 D3.
2
+
3
+ The SkyPilot provisioning engine for cairn: `pip install 'cairndev[skypilot]'`, set
4
+ `[profile.backends] ml_engine = "skypilot"` (or `dataflow_engine`) and point `ml_target` /
5
+ `dataflow_target` at a compute connection. The launch door asks this engine's `check_ready()`
6
+ before any record exists — nothing is provisioned to make the check pass.
@@ -0,0 +1,15 @@
1
+ # cairn-operator-dataflow-skypilot — the SkyPilot provisioning dataflow engine (ADR-0104 D5),
2
+ # moved out of cairn-operator-dataflow into its own optional wheel (ADR-0134 D3a, #2678). It
3
+ # registers no operator: the `dataflow/run` and `ml/run` operators (cairn-operator-dataflow)
4
+ # submit to it when an environment selects `skypilot` as its dataflow_engine / ml_engine.
5
+ [pack]
6
+ name = "obs-operator-dataflow-skypilot"
7
+ version = "0.1.0"
8
+ type = "operator"
9
+ description = "The SkyPilot provisioning engine behind dataflow/run and ml/run (cairndev[skypilot])."
10
+ kind = "service_provider"
11
+ publisher = "cairn"
12
+ trust_level = "official"
13
+
14
+ [compat]
15
+ cairn_engine = ">=0.1.0"
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cairn-operator-dataflow-skypilot"
7
+ version = "0.1.3"
8
+ description = "The SkyPilot provisioning dataflow engine (ADR-0104 D5) for cairn's ml and data flows — offered by cairndev[skypilot] (ADR-0134 D3)."
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ authors = [{name = "cairn maintainers"}]
12
+ requires-python = ">=3.12"
13
+ # No hard dependency on SkyPilot: the engine imports `sky` lazily, so this wheel imports and
14
+ # registers without it (its tests drive a fake). `cairndev[skypilot]` installs both; installing
15
+ # this wheel alone, add the extra below.
16
+ dependencies = []
17
+
18
+ [project.optional-dependencies]
19
+ # The PROVISIONING engine (ADR-0104 D5, #2073): the clouds cairn's compute connections
20
+ # already name are the extras picked. A control plane that never acquires a machine must not
21
+ # carry this cloud SDK stack, which is why it is optional.
22
+ skypilot = ["skypilot[aws,kubernetes,runpod,ssh,vast]>=0.13"]
23
+
24
+ [project.urls]
25
+ Homepage = "https://cairndev.sh"
26
+ Documentation = "https://docs.cairndev.sh"
27
+ Source = "https://github.com/cairndev-sh/cairn"
28
+ Issues = "https://github.com/cairndev-sh/cairn/issues"
29
+
30
+ # #1512 — core selects an engine by NAME through this group and never imports this pack.
31
+ [project.entry-points."cairn.dataflow_engines"]
32
+ "skypilot" = "operator_dataflow_skypilot.factory:skypilot_engine"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/operator_dataflow_skypilot"]
36
+
37
+ [tool.hatch.build.targets.wheel.force-include]
38
+ "pack.toml" = "operator_dataflow_skypilot/pack.toml"
@@ -0,0 +1,4 @@
1
+ """cairn-operator-dataflow-skypilot — the SkyPilot provisioning dataflow engine (ADR-0104 D5),
2
+ an optional wheel offered by ``cairndev[skypilot]`` (ADR-0134 D3). Registered as ``skypilot``
3
+ in the ``cairn.dataflow_engines`` group; core selects it by name and never imports this
4
+ package, and asks its ``check_ready()`` at the launch door (ADR-0134 D3c)."""