A5-R3-R3A dormant dual-path snapshot identity

#110
Files changed (3) hide show
  1. core/generation_runner.py +93 -0
  2. managed_auth.py +14 -12
  3. task_executor.py +113 -0
core/generation_runner.py CHANGED
@@ -221,6 +221,7 @@ class GenerationRunner:
221
  executor_instance_id: str = "",
222
  confirmed_checkpoint_seq: int | None = None,
223
  confirmed_checkpoint_stage: str = "",
 
224
  progress_callback: ProgressCallback | None = None,
225
  cancel_checker: Callable[[], bool] | None = None,
226
  ):
@@ -232,6 +233,9 @@ class GenerationRunner:
232
  None if confirmed_checkpoint_seq is None else max(0, int(confirmed_checkpoint_seq))
233
  )
234
  self.confirmed_checkpoint_stage = str(confirmed_checkpoint_stage or "")[:120]
 
 
 
235
  self.progress_callback = progress_callback or (lambda **_: None)
236
  self.cancel_checker = cancel_checker or (lambda: False)
237
  expected_hash = str(self.request.get("request_hash") or "")
@@ -2809,6 +2813,87 @@ class GenerationRunner:
2809
  output.append(str(path))
2810
  return output
2811
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2812
  def _run_impl(self) -> dict[str, Any]:
2813
  req = self.request
2814
  mode = str(req.get("mode") or "").lower()
@@ -2817,6 +2902,9 @@ class GenerationRunner:
2817
  target_words = int(req.get("target_words") or 0)
2818
  if not APP_CONFIG["target_words_min"] <= target_words <= APP_CONFIG["target_words_max"]:
2819
  raise RuntimeError("target_words_out_of_range")
 
 
 
2820
 
2821
  # A2: provider credentials are adapter-owned. DeepSeek remains the
2822
  # current default adapter, but the runner no longer hard-codes it as the
@@ -2851,6 +2939,11 @@ class GenerationRunner:
2851
  quality_diagnostics = list(state.get("quality_diagnostics") or [])
2852
  quality_notes = list(state.get("quality_notes") or [])
2853
  state.setdefault("plan_policy_snapshot", copy.deepcopy(plan_policy_snapshot))
 
 
 
 
 
2854
  self._evidence_coverage_events = list(state.get("evidence_coverage_events") or [])
2855
  if str(state.get("evidence_status") or "") == "limited":
2856
  self._preview_evidence_status = "limited"
 
221
  executor_instance_id: str = "",
222
  confirmed_checkpoint_seq: int | None = None,
223
  confirmed_checkpoint_stage: str = "",
224
+ trusted_task_policy_snapshot: dict[str, Any] | None = None,
225
  progress_callback: ProgressCallback | None = None,
226
  cancel_checker: Callable[[], bool] | None = None,
227
  ):
 
233
  None if confirmed_checkpoint_seq is None else max(0, int(confirmed_checkpoint_seq))
234
  )
235
  self.confirmed_checkpoint_stage = str(confirmed_checkpoint_stage or "")[:120]
236
+ self.trusted_task_policy_snapshot = copy.deepcopy(
237
+ dict(trusted_task_policy_snapshot or {})
238
+ )
239
  self.progress_callback = progress_callback or (lambda **_: None)
240
  self.cancel_checker = cancel_checker or (lambda: False)
241
  expected_hash = str(self.request.get("request_hash") or "")
 
2813
  output.append(str(path))
2814
  return output
2815
 
2816
+ def _validate_trusted_task_policy_for_execution(self, mode: str, target_words: int) -> dict[str, Any] | None:
2817
+ snapshot = copy.deepcopy(self.trusted_task_policy_snapshot)
2818
+ if not snapshot:
2819
+ return None
2820
+ resolved = snapshot.get("resolved_policy")
2821
+ if not isinstance(resolved, dict):
2822
+ raise RuntimeError("trusted_task_policy_snapshot_invalid")
2823
+
2824
+ task_id = str(snapshot.get("task_id") or "").strip()
2825
+ request_task_id = str(self.request.get("task_id") or "").strip()
2826
+ if not task_id or task_id != request_task_id:
2827
+ raise RuntimeError("trusted_task_policy_task_mismatch")
2828
+
2829
+ tier = str(snapshot.get("product_tier") or "").strip().upper()
2830
+ entitlement_class = str(snapshot.get("entitlement_class") or "").strip().upper()
2831
+ if tier not in {"STANDARD", "FLAGSHIP"}:
2832
+ raise RuntimeError("trusted_task_policy_product_tier_invalid")
2833
+ if entitlement_class not in {"STANDARD_TRIAL", "STANDARD_PAID", "FLAGSHIP_PAID"}:
2834
+ raise RuntimeError("trusted_task_policy_entitlement_invalid")
2835
+ if tier == "STANDARD" and entitlement_class == "FLAGSHIP_PAID":
2836
+ raise RuntimeError("trusted_task_policy_entitlement_mismatch")
2837
+ if tier == "FLAGSHIP" and entitlement_class != "FLAGSHIP_PAID":
2838
+ raise RuntimeError("trusted_task_policy_entitlement_mismatch")
2839
+ if str(resolved.get("product_tier") or "").strip().upper() != tier:
2840
+ raise RuntimeError("trusted_task_policy_product_tier_mismatch")
2841
+ if str(resolved.get("snapshot_schema_version") or "") != str(snapshot.get("snapshot_schema_version") or ""):
2842
+ raise RuntimeError("trusted_task_policy_schema_mismatch")
2843
+ for field in ("product_policy_version", "word_count_policy_version", "model_policy_version", "cost_policy_version"):
2844
+ if str(resolved.get(field) or "") != str(snapshot.get(field) or ""):
2845
+ raise RuntimeError("trusted_task_policy_version_mismatch")
2846
+ for field in ("billing_policy_version", "trial_policy_version"):
2847
+ resolved_value = str(resolved.get(field) or "").strip() or None
2848
+ snapshot_value = str(snapshot.get(field) or "").strip() or None
2849
+ if resolved_value != snapshot_value:
2850
+ raise RuntimeError("trusted_task_policy_version_mismatch")
2851
+ if str(resolved.get("manuscript_mode") or "").strip().lower() != mode:
2852
+ raise RuntimeError("trusted_task_policy_mode_mismatch")
2853
+ minimum = int(snapshot.get("min_body_words") or 0)
2854
+ maximum = int(snapshot.get("max_body_words") or 0)
2855
+ if minimum <= 0 or maximum < minimum or not minimum <= target_words <= maximum:
2856
+ raise RuntimeError("trusted_task_policy_word_range_mismatch")
2857
+ word_count = resolved.get("word_count") if isinstance(resolved.get("word_count"), dict) else {}
2858
+ if (
2859
+ int(word_count.get("min_body_words") or 0) != minimum
2860
+ or int(word_count.get("max_body_words") or 0) != maximum
2861
+ or str(word_count.get("counting_contract_version") or "") != str(snapshot.get("counting_contract_version") or "")
2862
+ ):
2863
+ raise RuntimeError("trusted_task_policy_word_policy_mismatch")
2864
+
2865
+ envelope = {
2866
+ "task_id": task_id,
2867
+ "snapshot_schema_version": str(snapshot.get("snapshot_schema_version") or ""),
2868
+ "product_tier": str(snapshot.get("product_tier") or ""),
2869
+ "product_policy_version": str(snapshot.get("product_policy_version") or ""),
2870
+ "word_count_policy_version": str(snapshot.get("word_count_policy_version") or ""),
2871
+ "model_policy_version": str(snapshot.get("model_policy_version") or ""),
2872
+ "cost_policy_version": str(snapshot.get("cost_policy_version") or ""),
2873
+ "billing_policy_version": str(snapshot.get("billing_policy_version") or "").strip() or None,
2874
+ "trial_policy_version": str(snapshot.get("trial_policy_version") or "").strip() or None,
2875
+ "entitlement_class": str(snapshot.get("entitlement_class") or "").strip() or None,
2876
+ "min_body_words": minimum,
2877
+ "max_body_words": maximum,
2878
+ "counting_contract_version": str(snapshot.get("counting_contract_version") or ""),
2879
+ "resolved_policy": resolved,
2880
+ }
2881
+ observed_sha = hashlib.sha256(
2882
+ json.dumps(envelope, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
2883
+ ).hexdigest()
2884
+ if observed_sha != str(snapshot.get("snapshot_sha256") or "").strip().lower():
2885
+ raise RuntimeError("trusted_task_policy_snapshot_sha_mismatch")
2886
+
2887
+ model = resolved.get("model") if isinstance(resolved.get("model"), dict) else {}
2888
+ routes = model.get("routes") if isinstance(model.get("routes"), dict) else {}
2889
+ if not routes:
2890
+ raise RuntimeError("trusted_task_policy_model_routes_missing")
2891
+ # A5-R3-R3A closes snapshot identity only. It deliberately does not
2892
+ # reinterpret fine-grained logical-node routes as the legacy coarse
2893
+ # capability roles. Product execution remains fail-closed until the
2894
+ # separate logical-node execution contract is implemented.
2895
+ raise RuntimeError("trusted_task_policy_model_route_contract_not_ready")
2896
+
2897
  def _run_impl(self) -> dict[str, Any]:
2898
  req = self.request
2899
  mode = str(req.get("mode") or "").lower()
 
2902
  target_words = int(req.get("target_words") or 0)
2903
  if not APP_CONFIG["target_words_min"] <= target_words <= APP_CONFIG["target_words_max"]:
2904
  raise RuntimeError("target_words_out_of_range")
2905
+ trusted_task_policy_snapshot = self._validate_trusted_task_policy_for_execution(
2906
+ mode, target_words
2907
+ )
2908
 
2909
  # A2: provider credentials are adapter-owned. DeepSeek remains the
2910
  # current default adapter, but the runner no longer hard-codes it as the
 
2939
  quality_diagnostics = list(state.get("quality_diagnostics") or [])
2940
  quality_notes = list(state.get("quality_notes") or [])
2941
  state.setdefault("plan_policy_snapshot", copy.deepcopy(plan_policy_snapshot))
2942
+ if trusted_task_policy_snapshot is not None:
2943
+ state.setdefault(
2944
+ "trusted_task_policy_snapshot",
2945
+ copy.deepcopy(trusted_task_policy_snapshot),
2946
+ )
2947
  self._evidence_coverage_events = list(state.get("evidence_coverage_events") or [])
2948
  if str(state.get("evidence_status") or "") == "limited":
2949
  self._preview_evidence_status = "limited"
managed_auth.py CHANGED
@@ -342,24 +342,26 @@ def start_generation_task(
342
  raise RuntimeError("idempotency_key_required")
343
  projection = get_product_ui_projection()
344
  normalized_tier = str(product_tier or "").strip().upper()
345
- if projection.get("mode") == "a5_product":
 
346
  if normalized_tier not in projection.get("products", {}):
347
  raise RuntimeError("product_tier_required")
348
- # A5-R3-R1 is projection-only. Until entitlement reservation/task policy
349
- # snapshot ownership is separately authorized, never fall through to the
350
- # legacy quota task path.
351
- raise RuntimeError("a5_product_task_path_not_authorized")
352
- if normalized_tier:
353
  raise RuntimeError("product_tier_not_available_in_legacy_mode")
 
 
 
 
 
 
 
 
 
 
354
  result = _edge_call(
355
  "task_start",
356
  normalized,
357
- {
358
- "idempotency_key": normalized,
359
- "mode": str(mode or "").strip().lower(),
360
- "interface_language": str(interface_language or "zh").strip(),
361
- "target_words": int(target_words),
362
- },
363
  key_prefix="sp_task_start",
364
  )
365
  if result is None:
 
342
  raise RuntimeError("idempotency_key_required")
343
  projection = get_product_ui_projection()
344
  normalized_tier = str(product_tier or "").strip().upper()
345
+ product_mode = projection.get("mode") == "a5_product"
346
+ if product_mode:
347
  if normalized_tier not in projection.get("products", {}):
348
  raise RuntimeError("product_tier_required")
349
+ elif normalized_tier:
 
 
 
 
350
  raise RuntimeError("product_tier_not_available_in_legacy_mode")
351
+ payload = {
352
+ "idempotency_key": normalized,
353
+ "mode": str(mode or "").strip().lower(),
354
+ "interface_language": str(interface_language or "zh").strip(),
355
+ "target_words": int(target_words),
356
+ }
357
+ if product_mode:
358
+ # Product tier is user intent only. Worker/D1 resolves entitlement and
359
+ # freezes the trusted TaskPolicySnapshot; browser state is never authority.
360
+ payload["product_tier"] = normalized_tier
361
  result = _edge_call(
362
  "task_start",
363
  normalized,
364
+ payload,
 
 
 
 
 
365
  key_prefix="sp_task_start",
366
  )
367
  if result is None:
task_executor.py CHANGED
@@ -11,6 +11,7 @@ Worker so quota is never left reserved by an unowned task.
11
  from __future__ import annotations
12
 
13
  import argparse
 
14
  import json
15
  import logging
16
  import os
@@ -409,6 +410,106 @@ def _cleanup_user_cancelled_task_artifacts(task_dir: Path, request_data: dict) -
409
  logging.warning("[SP_EXECUTOR] cancel_task_cleanup_error task_id=%s error=%s", task_dir.name, _safe(exc, 160))
410
 
411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  def _load_request(task_dir: Path) -> dict:
413
  path = task_dir / "request.json"
414
  if not path.is_file():
@@ -441,6 +542,11 @@ def _bootstrap_claim(*, task_dir: Path, task_id: str, user_id: int, request_data
441
  raise RuntimeError("executor_claim_identity_mismatch")
442
  if int(task.get("target_words") or 0) != int(request_data.get("target_words") or 0):
443
  raise RuntimeError("target_words_claim_mismatch")
 
 
 
 
 
444
  try:
445
  with checkpoint_authority_lock(task_dir):
446
  _atomic_json(
@@ -454,6 +560,12 @@ def _bootstrap_claim(*, task_dir: Path, task_id: str, user_id: int, request_data
454
  "executor_instance_id": instance_id,
455
  "worker_checkpoint_seq": int(task.get("checkpoint_seq") or 0),
456
  "worker_checkpoint_stage": str(task.get("checkpoint_stage") or "")[:120],
 
 
 
 
 
 
457
  "claimed_at": datetime.now(timezone.utc).isoformat(),
458
  },
459
  )
@@ -569,6 +681,7 @@ def main() -> int:
569
  executor_instance_id=instance_id,
570
  confirmed_checkpoint_seq=remote_checkpoint_seq,
571
  confirmed_checkpoint_stage=remote_checkpoint_stage,
 
572
  progress_callback=state.touch,
573
  cancel_checker=state.cancelled,
574
  )
 
11
  from __future__ import annotations
12
 
13
  import argparse
14
+ import hashlib
15
  import json
16
  import logging
17
  import os
 
410
  logging.warning("[SP_EXECUTOR] cancel_task_cleanup_error task_id=%s error=%s", task_dir.name, _safe(exc, 160))
411
 
412
 
413
+ def _snapshot_nullable_version(value):
414
+ text = str(value or "").strip()
415
+ return text or None
416
+
417
+
418
+ def _trusted_task_policy_snapshot_envelope(snapshot: dict, resolved: dict) -> dict:
419
+ return {
420
+ "task_id": str(snapshot.get("task_id") or ""),
421
+ "snapshot_schema_version": str(snapshot.get("snapshot_schema_version") or ""),
422
+ "product_tier": str(snapshot.get("product_tier") or ""),
423
+ "product_policy_version": str(snapshot.get("product_policy_version") or ""),
424
+ "word_count_policy_version": str(snapshot.get("word_count_policy_version") or ""),
425
+ "model_policy_version": str(snapshot.get("model_policy_version") or ""),
426
+ "cost_policy_version": str(snapshot.get("cost_policy_version") or ""),
427
+ "billing_policy_version": _snapshot_nullable_version(snapshot.get("billing_policy_version")),
428
+ "trial_policy_version": _snapshot_nullable_version(snapshot.get("trial_policy_version")),
429
+ "entitlement_class": _snapshot_nullable_version(snapshot.get("entitlement_class")),
430
+ "min_body_words": int(snapshot.get("min_body_words") or 0),
431
+ "max_body_words": int(snapshot.get("max_body_words") or 0),
432
+ "counting_contract_version": str(snapshot.get("counting_contract_version") or ""),
433
+ "resolved_policy": resolved,
434
+ }
435
+
436
+
437
+ def _validate_trusted_task_policy_snapshot(claim: dict, task: dict, request_data: dict) -> dict | None:
438
+ raw = claim.get("task_policy_snapshot")
439
+ if raw is None:
440
+ return None
441
+ if not isinstance(raw, dict):
442
+ raise RuntimeError("task_policy_snapshot_invalid")
443
+ snapshot = dict(raw)
444
+ resolved = snapshot.get("resolved_policy")
445
+ if not isinstance(resolved, dict):
446
+ raise RuntimeError("task_policy_snapshot_resolved_policy_missing")
447
+
448
+ snapshot_task_id = str(snapshot.get("task_id") or "").strip()
449
+ claimed_task_id = str(task.get("task_id") or "").strip()
450
+ request_task_id = str(request_data.get("task_id") or "").strip()
451
+ if not snapshot_task_id or snapshot_task_id != claimed_task_id or snapshot_task_id != request_task_id:
452
+ raise RuntimeError("task_policy_snapshot_task_mismatch")
453
+
454
+ tier = str(snapshot.get("product_tier") or "").strip().upper()
455
+ entitlement_class = str(snapshot.get("entitlement_class") or "").strip().upper()
456
+ if tier not in {"STANDARD", "FLAGSHIP"}:
457
+ raise RuntimeError("task_policy_snapshot_product_tier_invalid")
458
+ if entitlement_class not in {"STANDARD_TRIAL", "STANDARD_PAID", "FLAGSHIP_PAID"}:
459
+ raise RuntimeError("task_policy_snapshot_entitlement_invalid")
460
+ if tier == "STANDARD" and entitlement_class == "FLAGSHIP_PAID":
461
+ raise RuntimeError("task_policy_snapshot_entitlement_mismatch")
462
+ if tier == "FLAGSHIP" and entitlement_class != "FLAGSHIP_PAID":
463
+ raise RuntimeError("task_policy_snapshot_entitlement_mismatch")
464
+
465
+ if str(resolved.get("snapshot_schema_version") or "") != str(snapshot.get("snapshot_schema_version") or ""):
466
+ raise RuntimeError("task_policy_snapshot_schema_mismatch")
467
+ if str(resolved.get("product_tier") or "").strip().upper() != tier:
468
+ raise RuntimeError("task_policy_snapshot_product_tier_mismatch")
469
+ version_fields = (
470
+ "product_policy_version",
471
+ "word_count_policy_version",
472
+ "model_policy_version",
473
+ "cost_policy_version",
474
+ )
475
+ for field in version_fields:
476
+ if str(resolved.get(field) or "") != str(snapshot.get(field) or ""):
477
+ raise RuntimeError("task_policy_snapshot_policy_version_mismatch")
478
+ for field in ("billing_policy_version", "trial_policy_version"):
479
+ if _snapshot_nullable_version(resolved.get(field)) != _snapshot_nullable_version(snapshot.get(field)):
480
+ raise RuntimeError("task_policy_snapshot_policy_version_mismatch")
481
+
482
+ if str(resolved.get("manuscript_mode") or "").strip().lower() != str(task.get("mode") or "").strip().lower():
483
+ raise RuntimeError("task_policy_snapshot_mode_mismatch")
484
+ target_words = int(task.get("target_words") or 0)
485
+ if target_words != int(request_data.get("target_words") or 0):
486
+ raise RuntimeError("target_words_claim_mismatch")
487
+ minimum = int(snapshot.get("min_body_words") or 0)
488
+ maximum = int(snapshot.get("max_body_words") or 0)
489
+ if minimum <= 0 or maximum < minimum or not minimum <= target_words <= maximum:
490
+ raise RuntimeError("task_policy_snapshot_word_range_mismatch")
491
+ word_count = resolved.get("word_count") if isinstance(resolved.get("word_count"), dict) else {}
492
+ if (
493
+ int(word_count.get("min_body_words") or 0) != minimum
494
+ or int(word_count.get("max_body_words") or 0) != maximum
495
+ or str(word_count.get("counting_contract_version") or "") != str(snapshot.get("counting_contract_version") or "")
496
+ ):
497
+ raise RuntimeError("task_policy_snapshot_word_policy_mismatch")
498
+
499
+ observed = hashlib.sha256(
500
+ json.dumps(
501
+ _trusted_task_policy_snapshot_envelope(snapshot, resolved),
502
+ ensure_ascii=False,
503
+ sort_keys=True,
504
+ separators=(",", ":"),
505
+ ).encode("utf-8")
506
+ ).hexdigest()
507
+ expected = str(snapshot.get("snapshot_sha256") or "").strip().lower()
508
+ if not re.fullmatch(r"[a-f0-9]{64}", expected) or observed != expected:
509
+ raise RuntimeError("task_policy_snapshot_sha_mismatch")
510
+ return snapshot
511
+
512
+
513
  def _load_request(task_dir: Path) -> dict:
514
  path = task_dir / "request.json"
515
  if not path.is_file():
 
542
  raise RuntimeError("executor_claim_identity_mismatch")
543
  if int(task.get("target_words") or 0) != int(request_data.get("target_words") or 0):
544
  raise RuntimeError("target_words_claim_mismatch")
545
+ trusted_task_policy_snapshot = _validate_trusted_task_policy_snapshot(
546
+ claim, task, request_data
547
+ )
548
+ if trusted_task_policy_snapshot is not None:
549
+ task["_trusted_task_policy_snapshot"] = trusted_task_policy_snapshot
550
  try:
551
  with checkpoint_authority_lock(task_dir):
552
  _atomic_json(
 
560
  "executor_instance_id": instance_id,
561
  "worker_checkpoint_seq": int(task.get("checkpoint_seq") or 0),
562
  "worker_checkpoint_stage": str(task.get("checkpoint_stage") or "")[:120],
563
+ "task_policy_snapshot_sha256": str(
564
+ (trusted_task_policy_snapshot or {}).get("snapshot_sha256") or ""
565
+ ),
566
+ "task_policy_product_tier": str(
567
+ (trusted_task_policy_snapshot or {}).get("product_tier") or ""
568
+ ),
569
  "claimed_at": datetime.now(timezone.utc).isoformat(),
570
  },
571
  )
 
681
  executor_instance_id=instance_id,
682
  confirmed_checkpoint_seq=remote_checkpoint_seq,
683
  confirmed_checkpoint_stage=remote_checkpoint_stage,
684
+ trusted_task_policy_snapshot=task.get("_trusted_task_policy_snapshot"),
685
  progress_callback=state.touch,
686
  cancel_checker=state.cancelled,
687
  )