gridlinegpu 0.1.6__tar.gz → 0.1.8__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.
Files changed (53) hide show
  1. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/PKG-INFO +4 -2
  2. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/README.md +3 -1
  3. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/pyproject.toml +1 -1
  4. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_contract.py +3 -3
  5. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_interfaces.py +34 -4
  6. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_job.py +22 -8
  7. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_release.py +1 -1
  8. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu.egg-info/PKG-INFO +4 -2
  9. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_client_source.py +1 -1
  10. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_contract.py +45 -7
  11. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_curated_run.py +3 -2
  12. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_job.py +6 -6
  13. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_job_results.py +2 -2
  14. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/setup.cfg +0 -0
  15. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/UNICODE-LICENSE.txt +0 -0
  16. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/__init__.py +0 -0
  17. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/__main__.py +0 -0
  18. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_admission.py +0 -0
  19. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_capture.py +0 -0
  20. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_casefold.json +0 -0
  21. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_credits.py +0 -0
  22. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_errors.py +0 -0
  23. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_history.py +0 -0
  24. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_outputs.py +0 -0
  25. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_public.py +0 -0
  26. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_results.py +0 -0
  27. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_run.py +0 -0
  28. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_submission.py +0 -0
  29. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/_source_transfer.py +0 -0
  30. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/cli.py +0 -0
  31. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/cli_auth.py +0 -0
  32. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/client.py +0 -0
  33. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu/errors.py +0 -0
  34. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu.egg-info/SOURCES.txt +0 -0
  35. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu.egg-info/dependency_links.txt +0 -0
  36. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu.egg-info/entry_points.txt +0 -0
  37. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu.egg-info/requires.txt +0 -0
  38. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/src/gridlinegpu.egg-info/top_level.txt +0 -0
  39. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_cli.py +0 -0
  40. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_cli_auth.py +0 -0
  41. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_client.py +0 -0
  42. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_admission.py +0 -0
  43. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_canary.py +0 -0
  44. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_capture.py +0 -0
  45. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_capture_gaps.py +0 -0
  46. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_credits.py +0 -0
  47. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_errors.py +0 -0
  48. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_history.py +0 -0
  49. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_release.py +0 -0
  50. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_results.py +0 -0
  51. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_run.py +0 -0
  52. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_submission.py +0 -0
  53. {gridlinegpu-0.1.6 → gridlinegpu-0.1.8}/tests/test_source_transfer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridlinegpu
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Python client for Gridline YAML workload routing
5
5
  Author: Gridline GPU
6
6
  License: Proprietary
@@ -140,7 +140,7 @@ top-up or payment step in the SDK. Install the source extra and turn the beta on
140
140
  shell:
141
141
 
142
142
  ```sh
143
- python -m pip install 'gridlinegpu[source-contract]>=0.1.6,<0.2'
143
+ python -m pip install 'gridlinegpu[source-contract]>=0.1.7,<0.2'
144
144
  export GRIDLINE_SOURCE_SDK_ENABLED=true
145
145
  gridline setup --apply # once: saves a private, scoped token file
146
146
  ```
@@ -213,6 +213,8 @@ print(job.report()["settlement"]) # reserved, held, charged and released credit
213
213
  ```
214
214
 
215
215
  `job.status()` reads the current state without waiting, and `job.cancel()` requests a stop.
216
+ `job.isolation_tier()` names the isolation tier the job's code ran under (`kernel_cgroup` or
217
+ `hardened_user_separation`, see the security page), or `None` before a machine is allocated.
216
218
  `client.history(limit=20)` lists your recent jobs, newest first. `report()` and `history()`
217
219
  raise `report_unavailable` / `history_unavailable` until those reads are switched on for
218
220
  the beta; the job itself is unaffected. A complete minimal
@@ -122,7 +122,7 @@ top-up or payment step in the SDK. Install the source extra and turn the beta on
122
122
  shell:
123
123
 
124
124
  ```sh
125
- python -m pip install 'gridlinegpu[source-contract]>=0.1.6,<0.2'
125
+ python -m pip install 'gridlinegpu[source-contract]>=0.1.7,<0.2'
126
126
  export GRIDLINE_SOURCE_SDK_ENABLED=true
127
127
  gridline setup --apply # once: saves a private, scoped token file
128
128
  ```
@@ -195,6 +195,8 @@ print(job.report()["settlement"]) # reserved, held, charged and released credit
195
195
  ```
196
196
 
197
197
  `job.status()` reads the current state without waiting, and `job.cancel()` requests a stop.
198
+ `job.isolation_tier()` names the isolation tier the job's code ran under (`kernel_cgroup` or
199
+ `hardened_user_separation`, see the security page), or `None` before a machine is allocated.
198
200
  `client.history(limit=20)` lists your recent jobs, newest first. `report()` and `history()`
199
201
  raise `report_unavailable` / `history_unavailable` until those reads are switched on for
200
202
  the beta; the job itself is unaffected. A complete minimal
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gridlinegpu"
7
- version = "0.1.6"
7
+ version = "0.1.8"
8
8
  description = "Python client for Gridline YAML workload routing"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -253,7 +253,7 @@ SOURCE_CURATED_ACTIVATION_PRECONDITIONS = MappingProxyType({
253
253
  # The models: capability word; off by default (a code constant), as in versions.ts. Off, the
254
254
  # registry is the v2.1 and v2.3 tuples and every check answers as before.
255
255
  SOURCE_MODELS_CAPABILITY = "models_hf_v1"
256
- SOURCE_MODELS_HF_V1_ENABLED = False
256
+ SOURCE_MODELS_HF_V1_ENABLED = True
257
257
  # versions.ts SOURCE_MODELS_ACTIVATION_PRECONDITIONS: blocking 2h-2 items, each false until landed
258
258
  # (capture and frame refuse captured files under models/ first, compared case-folded).
259
259
  SOURCE_MODELS_ACTIVATION_PRECONDITIONS = MappingProxyType({
@@ -271,11 +271,11 @@ SOURCE_MODELS_ACTIVATION_PRECONDITIONS = MappingProxyType({
271
271
  "registerPublished": True,
272
272
  # MR-1: the manifest ledger, quota, retention and purge (source_runtime_manifests).
273
273
  "manifestRetention": True,
274
- "hubContractRecorded": False, "imageAllowanceRegistered": True,
274
+ "hubContractRecorded": True, "imageAllowanceRegistered": True,
275
275
  # AR-1: the supervisor refuses app_root_unsafe for a payload-writable app root (models staged).
276
276
  "appRootOwnership": True,
277
277
  # Command (MA-2 ruling 1): launcher.ts SOURCE_LAUNCHER_HUB_CDN_HOSTS_ENABLED (off).
278
- "hubCdnHostsEnabled": False,
278
+ "hubCdnHostsEnabled": True,
279
279
  })
280
280
  # versions.ts SOURCE_MODELS_HARDENING_ITEMS (Command, MA-2): tracked, never gating activation.
281
281
  SOURCE_MODELS_HARDENING_ITEMS = MappingProxyType({"executionReadOnlyMount": False})
@@ -603,7 +603,7 @@ def validate_source_job_status(value):
603
603
  # Status v3 (job.ts): v2 plus the current task's stage, a coarse packaging step and the failure
604
604
  # envelope. failureCode, error.stage and error.code are open words (H6): a newer server's code
605
605
  # or stage is accepted and arrives with its own text. Structural vocabularies stay closed.
606
- SOURCE_JOB_STATUS_VERSIONS = (2, 3, 4, 5, 6, 7)
606
+ SOURCE_JOB_STATUS_VERSIONS = (2, 3, 4, 5, 6, 7, 8)
607
607
  SOURCE_JOB_TASK_STAGES = ("runtime_resolution", "runtime_packaging", "compute_routing")
608
608
  SOURCE_JOB_PACKAGING_STEPS = ("dependencies", "wheels", "image", "qualification", "handoff")
609
609
  _open_word = _pattern(r"[a-z_]{1,64}")
@@ -1244,10 +1244,38 @@ def validate_source_job_status_v7(value):
1244
1244
  return value
1245
1245
 
1246
1246
 
1247
+ # Status v8 (job.ts, O6-C PR-D): v7 plus the run's isolation tier. The shared vectors are
1248
+ # job-status-v8.json.
1249
+ SOURCE_JOB_ISOLATION_TIERS = ("kernel_cgroup", "hardened_user_separation", "user_separation")
1250
+ SOURCE_JOB_STATUS_V8_FIELDS = SOURCE_JOB_STATUS_V7_FIELDS + ("isolationTier",)
1251
+ _JOB_STATUS_V8 = {
1252
+ **_JOB_STATUS_V7,
1253
+ "schemaVersion": _number_literal(8),
1254
+ "isolationTier": _nullable(_one_of(*SOURCE_JOB_ISOLATION_TIERS)),
1255
+ }
1256
+
1257
+
1258
+ def validate_source_job_status_v8(value):
1259
+ """GET /jobs/{jobId}?schemaVersion=8: v7 plus ``isolationTier``, at most 64 KiB (job.ts
1260
+ sourceJobStatusV8Schema). ``isolationTier`` is the isolation tier of the machine the job's
1261
+ code ran on: ``kernel_cgroup``, ``hardened_user_separation`` (the disclosed hardened tier,
1262
+ see the security page) or ``user_separation`` (the owner canary only); null before such a
1263
+ machine. Never a location, SKU or resource id."""
1264
+ _require(value, _JOB_STATUS_V8, "jobStatus")
1265
+ _status_v6_rules(value)
1266
+ attempts = value["launchAttempts"]
1267
+ if attempts is not None and (
1268
+ attempts["refusals"] > attempts["attempt"]
1269
+ or (attempts["lastRefusal"] is None) != (attempts["refusals"] == 0)):
1270
+ _record_fail("jobStatus.launchAttempts")
1271
+ _bounded(value, SOURCE_JOB_LIMITS["statusBytes"], "jobStatus")
1272
+ return value
1273
+
1274
+
1247
1275
  def validate_source_job_status_answer(value):
1248
- """A status answer of any version this SDK reads (2 to 7), chosen by its schemaVersion. A
1249
- server that does not serve v7, v6, v5, v4 or v3 (older, or switched off) answers the highest
1250
- it serves; v2 to v5 stay readable for jobs without Step 10 facts (the server refuses them for
1276
+ """A status answer of any version this SDK reads (2 to 8), chosen by its schemaVersion. A
1277
+ server that does not serve v8, v7, v6, v5, v4 or v3 (older, or switched off) answers the
1278
+ highest it serves; v2 to v5 stay readable for jobs without Step 10 facts (the server refuses them for
1251
1279
  a job with a Step 10 outcome or execution state: status_version_unsupported)."""
1252
1280
  version = value.get("schemaVersion") if type(value) is dict else None
1253
1281
  if _number_literal(2)(version):
@@ -1262,6 +1290,8 @@ def validate_source_job_status_answer(value):
1262
1290
  return validate_source_job_status_v6(value)
1263
1291
  if _number_literal(7)(version):
1264
1292
  return validate_source_job_status_v7(value)
1293
+ if _number_literal(8)(version):
1294
+ return validate_source_job_status_v8(value)
1265
1295
  _record_fail("jobStatus.schemaVersion")
1266
1296
 
1267
1297
 
@@ -5,10 +5,12 @@ SDK gate is on; it is off by default.
5
5
  - The fields are the receipt's (spec 5.2), snake-cased. The receipt is historical: status
6
6
  may already have advanced past ``initial_phase``.
7
7
  - ``status()`` reads the current bounded status (spec 5.2/5.7); it writes nothing. It asks
8
- for status v7 (credits S4b: v6 plus ``launchAttempts``) and also accepts v6, v5, v4, v3 or
9
- v2, which an older server or a switched-off v7 (or v6, v5, v4, v3) answers; a server refuses
8
+ for status v8 (O6-C PR-D: v7 plus ``isolationTier``) and also accepts v7 (credits S4b: v6
9
+ plus ``launchAttempts``), v6, v5, v4, v3 or v2, which an older server or a switched-off v8
10
+ (or v7, v6, v5, v4, v3) answers; a server refuses
10
11
  v2-v5 for a job with a Step 10 outcome (``status_version_unsupported``). ``route()`` returns
11
- the v4-v7 route summary (spec 8.6), or None.
12
+ the v4-v8 route summary (spec 8.6), or None. ``isolation_tier()`` returns the v8 tier of the
13
+ machine the code ran on, or None (before one, or from an older answer).
12
14
  - ``funding_action()`` returns the server-owned Add funds action (status v5, spec 9.3), or
13
15
  None; ``recheck_funding()`` sends exactly that action's binding back to ask for one bounded
14
16
  recheck of the provider's funds. Neither starts a payment; the customer pays on the
@@ -60,8 +62,8 @@ from .errors import WaitTimeout
60
62
 
61
63
  _JOBS = "/api/v2/source/jobs/"
62
64
  _JOB_ID = re.compile(r"job_[a-f0-9]{32}")
63
- # Status version asked for; the answer's schemaVersion (2 to 7) names the shape served.
64
- _STATUS_VERSION = 7
65
+ # Status version asked for; the answer's schemaVersion (2 to 8) names the shape served.
66
+ _STATUS_VERSION = 8
65
67
  # Asked once more after an answer of HTTP 500: v3 reads no route plan, launch row or Step 10
66
68
  # record, so a server failure confined to those projections never makes status (or wait)
67
69
  # unavailable (8-ix, 9-xii). A Step 10 job refuses v3 (S10-R16): the 500 then stands.
@@ -220,7 +222,8 @@ class JobHandle:
220
222
  def status(self):
221
223
  """Current owner-authorized job status, bound to this job and its immutable receipt.
222
224
 
223
- Returns the validated answer as served: v6 (v5 with ``financial`` replaced by
225
+ Returns the validated answer as served: v8 (v7 plus ``isolationTier``; see
226
+ ``isolation_tier()``), v7 (v6 plus ``launchAttempts``), v6 (v5 with ``financial`` replaced by
224
227
  ``cost``, plus the Step 10 ``execution``, ``result``, ``logs`` and ``reminder`` blocks),
225
228
  v5 (v4 plus the launch phases, the ``provider_launch`` or ``execution`` task,
226
229
  ``cleanup``, ``financial`` and the funding action; see ``funding_action()``), v4 (v3
@@ -239,6 +242,17 @@ class JobHandle:
239
242
  """
240
243
  return _read_status(self._client, self.job_id, self._fail, self.receipt())
241
244
 
245
+ def isolation_tier(self):
246
+ """The isolation tier of the machine this job's code ran on (status v8), or None.
247
+
248
+ Reads status exactly as ``status()`` does (same errors) and returns ``"kernel_cgroup"``,
249
+ ``"hardened_user_separation"`` (the disclosed hardened tier: per-process memory and
250
+ file-size limits and a sampled memory check; see the security page) or
251
+ ``"user_separation"`` (the owner canary only). None before such a machine, and from a
252
+ server that answers v7 or older.
253
+ """
254
+ return self.status().get("isolationTier")
255
+
242
256
  def funding_action(self):
243
257
  """The job's current Add funds action (status v5, spec 9.3 L1721), or None.
244
258
 
@@ -455,12 +469,12 @@ def _job_handle(receipt, client):
455
469
 
456
470
 
457
471
  def _read_status(client, job_id, fail, receipt=None):
458
- """The job's validated status, asked as v7 (once more as v3 after a 500), bound to the job
472
+ """The job's validated status, asked as v8 (once more as v3 after a 500), bound to the job
459
473
  (and to its receipt when known)."""
460
474
  status, headers, data = _request_status(client, job_id, fail, _STATUS_VERSION)
461
475
  if status == 500:
462
476
  fallback = _request_status(client, job_id, fail, _FALLBACK_STATUS_VERSION)
463
- # A Step 10 job refuses v3 (S10-R16): the v7 failure stays an unavailable read.
477
+ # A Step 10 job refuses v3 (S10-R16): the v8 failure stays an unavailable read.
464
478
  if not (fallback[0] == 409 and _error_code(fallback[2]) == "status_version_unsupported"):
465
479
  status, headers, data = fallback
466
480
  if status == 200:
@@ -72,5 +72,5 @@ def sdk_surface_digest(bounds=None):
72
72
  # RECORD content digest plus the candidate SHA).
73
73
  SOURCE_RELEASE_COMPATIBILITY = {
74
74
  "contract": SOURCE_RELEASE_CONFIG_CONTRACT,
75
- "surfaces": {"sdk": "ad9e2daf472c76affddd93a36aabe19f7686b0394a953345ffae931201efedae"},
75
+ "surfaces": {"sdk": "2d6b1bd3408b61f57f788f3e68d502cdb1626c1bf0f486b4dfefc5dc9bd47f24"},
76
76
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridlinegpu
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Python client for Gridline YAML workload routing
5
5
  Author: Gridline GPU
6
6
  License: Proprietary
@@ -140,7 +140,7 @@ top-up or payment step in the SDK. Install the source extra and turn the beta on
140
140
  shell:
141
141
 
142
142
  ```sh
143
- python -m pip install 'gridlinegpu[source-contract]>=0.1.6,<0.2'
143
+ python -m pip install 'gridlinegpu[source-contract]>=0.1.7,<0.2'
144
144
  export GRIDLINE_SOURCE_SDK_ENABLED=true
145
145
  gridline setup --apply # once: saves a private, scoped token file
146
146
  ```
@@ -213,6 +213,8 @@ print(job.report()["settlement"]) # reserved, held, charged and released credit
213
213
  ```
214
214
 
215
215
  `job.status()` reads the current state without waiting, and `job.cancel()` requests a stop.
216
+ `job.isolation_tier()` names the isolation tier the job's code ran under (`kernel_cgroup` or
217
+ `hardened_user_separation`, see the security page), or `None` before a machine is allocated.
216
218
  `client.history(limit=20)` lists your recent jobs, newest first. `report()` and `history()`
217
219
  raise `report_unavailable` / `history_unavailable` until those reads are switched on for
218
220
  the beta; the job itself is unaffected. A complete minimal
@@ -531,7 +531,7 @@ class ClientJobTests(ClientSourceTestCase):
531
531
  self.assertEqual((status["schemaVersion"], status["phase"], status["outcome"]),
532
532
  (3, "terminal", "cancelled"))
533
533
  self.assertIsNone(status["error"])
534
- self.assertEqual(self.server.queries, [("GET", "schemaVersion=7")] * 2)
534
+ self.assertEqual(self.server.queries, [("GET", "schemaVersion=8")] * 2)
535
535
  terminal = job_status_v3(self.server.job, "terminal")
536
536
  _, reason, action = describe("build_failed")
537
537
  failed = {**terminal, "outcome": "failed", "failureCode": "build_failed",
@@ -44,7 +44,7 @@ from gridlinegpu._source_interfaces import (
44
44
  validate_source_admission_receipt, validate_source_admission_request, validate_source_funding_recheck_answer,
45
45
  validate_source_job_status, validate_source_job_status_answer, validate_source_job_status_v3,
46
46
  validate_source_job_status_v4, validate_source_job_status_v5, validate_source_job_status_v6,
47
- validate_source_job_status_v7,
47
+ validate_source_job_status_v7, validate_source_job_status_v8,
48
48
  validate_source_job_stop_acknowledgement,
49
49
  validate_source_preflight_response, validate_source_preparation_envelope, validate_source_preparation_receipt,
50
50
  validate_source_verified_set,
@@ -602,7 +602,7 @@ class SourceInterfaceTests(unittest.TestCase):
602
602
 
603
603
  def test_status_answers_dispatch_on_their_version(self):
604
604
  answers = fixtures("interfaces.json")["answers"]
605
- self.assertEqual(SOURCE_JOB_STATUS_VERSIONS, (2, 3, 4, 5, 6, 7))
605
+ self.assertEqual(SOURCE_JOB_STATUS_VERSIONS, (2, 3, 4, 5, 6, 7, 8))
606
606
  for name in ("jobStatus", "jobStatusV3"):
607
607
  for case in answers[name]["valid"]:
608
608
  with self.subTest(answer=name, case=case["name"]):
@@ -614,7 +614,7 @@ class SourceInterfaceTests(unittest.TestCase):
614
614
  validate_source_job_status_answer(case["value"])
615
615
  v3 = answers["jobStatusV3"]["valid"][0]["value"]
616
616
  without = {key: item for key, item in v3.items() if key != "schemaVersion"}
617
- wrong = [{**v3, "schemaVersion": version} for version in (1, 8, "3", "4", True, None, 2.5)]
617
+ wrong = [{**v3, "schemaVersion": version} for version in (1, 9, "3", "4", True, None, 2.5)]
618
618
  for value in wrong + [without, [], "3", None]:
619
619
  with self.subTest(value=repr(value)[:40]):
620
620
  with self.assertRaises(SourceRecordError) as caught:
@@ -865,6 +865,38 @@ class SourceInterfaceTests(unittest.TestCase):
865
865
  with self.assertRaises(SourceRecordError):
866
866
  validate_source_job_status_v7({**status_v6_answer({}), "schemaVersion": 7})
867
867
 
868
+ def test_shared_status_v8_vectors(self):
869
+ # O6-C PR-D parity with job.ts sourceJobStatusV8Schema: v7 plus isolationTier.
870
+ from gridlinegpu import _source_interfaces as interfaces
871
+ vectors = fixtures("job-status-v8.json")
872
+ self.assertEqual(vectors["contract"], "gridline-source-job-status-v8-fixtures-v1")
873
+
874
+ def v8(patch):
875
+ return status_v6_answer(patch, "job-status-v8.json")
876
+
877
+ answers = vectors["answers"]
878
+ self.assertTrue(answers["valid"] and answers["invalid"])
879
+ for name, patch in answers["valid"]:
880
+ with self.subTest(case=name):
881
+ value = v8(patch)
882
+ self.assertIs(validate_source_job_status_v8(value), value)
883
+ self.assertIs(validate_source_job_status_answer(value), value)
884
+ for name, patch, path in answers["invalid"]:
885
+ with self.subTest(case=name):
886
+ with self.assertRaises(SourceRecordError) as caught:
887
+ validate_source_job_status_v8(v8(patch))
888
+ self.assertEqual(caught.exception.code, "schema_invalid")
889
+ with self.assertRaises(SourceRecordError):
890
+ validate_source_job_status_answer(v8(patch))
891
+ self.assertEqual(interfaces.SOURCE_JOB_STATUS_V8_FIELDS, tuple(vectors["fields"]))
892
+ self.assertEqual(list(v8({})), vectors["fields"])
893
+ self.assertEqual(list(interfaces.SOURCE_JOB_ISOLATION_TIERS), vectors["vocabularies"]["isolationTiers"])
894
+ # A v8 answer is never a v7 answer, and the reverse.
895
+ with self.assertRaises(SourceRecordError):
896
+ validate_source_job_status_v7({**v8({}), "schemaVersion": 7})
897
+ with self.assertRaises(SourceRecordError):
898
+ validate_source_job_status_v8({**status_v6_answer({}, "job-status-v7.json"), "schemaVersion": 8})
899
+
868
900
  def test_funding_surfaces_match_the_shared_vectors(self):
869
901
  vectors = fixtures("job-status-v5.json")
870
902
  billing = vectors["billing"]
@@ -1011,7 +1043,8 @@ class SourceInterfaceTests(unittest.TestCase):
1011
1043
  self.assertEqual(source_models_activation_missing(), missing)
1012
1044
  # The activation checker: off passes; on passes only with every precondition true.
1013
1045
  self.assertTrue(source_models_activation_allowed(False))
1014
- self.assertFalse(source_models_activation_allowed(True))
1046
+ # On passes exactly when every precondition has landed (C1 landed hubContractRecorded).
1047
+ self.assertIs(source_models_activation_allowed(True), not missing)
1015
1048
  landed = {name: True for name in SOURCE_MODELS_ACTIVATION_PRECONDITIONS}
1016
1049
  self.assertTrue(source_models_activation_allowed(True, landed))
1017
1050
  for name in SOURCE_MODELS_ACTIVATION_PRECONDITIONS:
@@ -1106,17 +1139,22 @@ class SourceInterfaceTests(unittest.TestCase):
1106
1139
  vectors["enabled"])
1107
1140
  self.assertEqual(json.loads(json.dumps(dict(SOURCE_CURATED_MODELS_VERSION_TUPLE))), vectors["tuple"])
1108
1141
  self.assertIn(SOURCE_CURATED_MODELS_VERSION_TUPLE, SOURCE_REGISTERED_VERSION_TUPLES)
1109
- self.assertNotIn(SOURCE_CURATED_MODELS_VERSION_TUPLE, SOURCE_VERSION_TUPLES)
1142
+ # Production holds v2.7 exactly while both capabilities are on (the models activation).
1143
+ both = SOURCE_CURATED_RUNTIMES_ENABLED and SOURCE_MODELS_HF_V1_ENABLED
1144
+ self.assertIs(SOURCE_CURATED_MODELS_VERSION_TUPLE in SOURCE_VERSION_TUPLES, both)
1110
1145
  for case in vectors["tuples"]:
1111
1146
  with self.subTest(case=case["name"]):
1112
1147
  self.assertEqual(check_source_version_tuple(case["tuple"], SOURCE_REGISTERED_VERSION_TUPLES),
1113
1148
  case["expected"])
1114
- self.assertEqual(check_source_version_tuple(case["tuple"]), case["expectedProduction"])
1149
+ production = case.get("expectedProductionOn", case["expectedProduction"]) if both \
1150
+ else case["expectedProduction"]
1151
+ self.assertEqual(check_source_version_tuple(case["tuple"]), production)
1115
1152
  for case in vectors["offers"]:
1116
1153
  with self.subTest(offer=case["name"]):
1117
1154
  self.assertEqual(source_curated_models_offered(case["capabilities"], case["curated"], case["models"]),
1118
1155
  case["offered"])
1119
- self.assertFalse(source_curated_models_offered(case["capabilities"]))
1156
+ self.assertEqual(source_curated_models_offered(case["capabilities"]),
1157
+ source_curated_models_offered(case["capabilities"], both, both))
1120
1158
 
1121
1159
  def test_curated_models_activation_needs_both_precondition_sets(self):
1122
1160
  curated = {name: True for name in SOURCE_CURATED_ACTIVATION_PRECONDITIONS}
@@ -160,9 +160,10 @@ class CuratedModelsRunTests(SourceRunTestCase):
160
160
  with self.assertRaises(SourceSubmissionError) as caught:
161
161
  self.authored(curated, models, preflight)
162
162
  self.assertEqual(caught.exception.code, "curated_runtimes_unavailable")
163
- # This release: both off, so a run is refused right after the preflight.
163
+ # A release with models off refuses a run right after the preflight.
164
164
  server = FakeSource(preflight=offering_curated_models())
165
- with self.assertRaises(SourceSubmissionError):
165
+ with mock.patch.object(_source_contract, "SOURCE_MODELS_HF_V1_ENABLED", False), \
166
+ self.assertRaises(SourceSubmissionError):
166
167
  self.run_workload(server)
167
168
  self.assertEqual(self.calls(server), [PREFLIGHT])
168
169
  self.assertEqual(self.captures(), [])
@@ -247,7 +247,7 @@ class JobHandleTests(unittest.TestCase):
247
247
  self.assertEqual(job.status(), job_status(RECEIPT))
248
248
  self.server.status_version = 3
249
249
  self.assertEqual(job.status(), job_status_v3(RECEIPT))
250
- self.assertEqual(self.server.queries, [("GET", "schemaVersion=7")] * 2)
250
+ self.assertEqual(self.server.queries, [("GET", "schemaVersion=8")] * 2)
251
251
  for name, patch in fixtures("job-status-v4.json")["answers"]["valid"]:
252
252
  with self.subTest(name=name):
253
253
  answer = status_v4_answer(patch)
@@ -299,7 +299,7 @@ class JobHandleTests(unittest.TestCase):
299
299
  self.server.phases = [_json(200, answer)]
300
300
  self.assertEqual(job.route(), expected)
301
301
  self.assertEqual(best_effort["route"]["estimate"], None)
302
- self.assertEqual(self.server.queries, [("GET", "schemaVersion=7")] * len(cases))
302
+ self.assertEqual(self.server.queries, [("GET", "schemaVersion=8")] * len(cases))
303
303
  sunk = status_v4_answer({"route": "$route:sunkUnknown"})
304
304
  self.server.phases = [_json(200, sunk)]
305
305
  self.assertEqual(job.route()["estimate"]["totalUnknownReason"], "sunk_cost_unknown")
@@ -325,7 +325,7 @@ class JobHandleTests(unittest.TestCase):
325
325
  v3 = v3_vector("routing pending after commit")
326
326
  self.server.phases = [(500, {}, b""), _json(200, v3)]
327
327
  self.assertEqual(job.status(), v3)
328
- self.assertEqual(self.server.queries, [("GET", "schemaVersion=7"), ("GET", "schemaVersion=3")])
328
+ self.assertEqual(self.server.queries, [("GET", "schemaVersion=8"), ("GET", "schemaVersion=3")])
329
329
  # route() over the fallback is None: v3 carries no route.
330
330
  self.server.phases = [(500, {}, b""), _json(200, v3)]
331
331
  self.assertIsNone(job.route())
@@ -340,13 +340,13 @@ class JobHandleTests(unittest.TestCase):
340
340
  with self.assertRaises(SourceSubmissionError) as caught:
341
341
  job.status()
342
342
  self.assertEqual((caught.exception.code, caught.exception.status), ("job_status_unavailable", 500))
343
- self.assertEqual(self.server.queries, [("GET", "schemaVersion=7"), ("GET", "schemaVersion=3")])
343
+ self.assertEqual(self.server.queries, [("GET", "schemaVersion=8"), ("GET", "schemaVersion=3")])
344
344
  # Other failures are not retried as v3.
345
345
  self.server.queries.clear()
346
346
  self.server.phases = [(503, {}, b"{}")]
347
347
  with self.assertRaises(SourceSubmissionError):
348
348
  job.status()
349
- self.assertEqual(self.server.queries, [("GET", "schemaVersion=7")])
349
+ self.assertEqual(self.server.queries, [("GET", "schemaVersion=8")])
350
350
 
351
351
  def test_unknown_failure_codes_and_error_stages_never_crash(self):
352
352
  # H6: failure codes and error stages are open words. A newer server's code arrives with
@@ -576,7 +576,7 @@ class JobFundingTests(unittest.TestCase):
576
576
  expected = None if answer["fundingAction"] is None else {
577
577
  key: answer[key] for key in ("actionRequired", "fundingAction", "billing")}
578
578
  self.assertEqual(job.funding_action(), expected)
579
- self.assertEqual(set(self.server.queries), {("GET", "schemaVersion=7")})
579
+ self.assertEqual(set(self.server.queries), {("GET", "schemaVersion=8")})
580
580
  # Older servers (v4, v3, v2) carry no action.
581
581
  for answer in (status_v4_answer({}), v3_vector(CLAIMED), job_status(RECEIPT)):
582
582
  self.server.phases = [_json(200, answer)]
@@ -950,7 +950,7 @@ class JobStatusV6Tests(unittest.TestCase):
950
950
  with self.assertRaises(SourceSubmissionError) as caught:
951
951
  job.status()
952
952
  self.assertEqual(caught.exception.code, "remote_contract_invalid")
953
- self.assertEqual(set(self.server.queries), {("GET", "schemaVersion=7")})
953
+ self.assertEqual(set(self.server.queries), {("GET", "schemaVersion=8")})
954
954
 
955
955
  def test_wait_returns_the_structured_result_with_separate_states(self):
956
956
  running = vector("running: started, advisory progress, result pending, no receipt yet")
@@ -1014,7 +1014,7 @@ class JobStatusV6Tests(unittest.TestCase):
1014
1014
  with self.assertRaises(SourceSubmissionError) as caught:
1015
1015
  job.status()
1016
1016
  self.assertEqual((caught.exception.code, caught.exception.status), ("job_status_unavailable", 500))
1017
- self.assertEqual(self.server.queries, [("GET", "schemaVersion=7"), ("GET", "schemaVersion=3")])
1017
+ self.assertEqual(self.server.queries, [("GET", "schemaVersion=8"), ("GET", "schemaVersion=3")])
1018
1018
 
1019
1019
 
1020
1020
  class JobCostTests(unittest.TestCase):
File without changes