agent-devkit 0.0.4 → 0.1.5

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 (60) hide show
  1. package/README.md +42 -2
  2. package/package.json +1 -1
  3. package/runtime/README.md +50 -2
  4. package/runtime/agent +29 -13
  5. package/runtime/agents/README.md +3 -0
  6. package/runtime/agents/github-pr-reviewer/AGENTS.md +10 -0
  7. package/runtime/agents/github-pr-reviewer/README.md +7 -0
  8. package/runtime/agents/github-pr-reviewer/agent.yaml +33 -0
  9. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/capability.yaml +20 -0
  10. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/decision-rules.md +8 -0
  11. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/workflow.md +9 -0
  12. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/capability.yaml +20 -0
  13. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/decision-rules.md +7 -0
  14. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/workflow.md +8 -0
  15. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/capability.yaml +20 -0
  16. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/decision-rules.md +7 -0
  17. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/workflow.md +8 -0
  18. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/capability.yaml +20 -0
  19. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/decision-rules.md +9 -0
  20. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/workflow.md +10 -0
  21. package/runtime/agents/github-pr-reviewer/infra/README.md +7 -0
  22. package/runtime/agents/github-pr-reviewer/knowledge/context.md +7 -0
  23. package/runtime/agents/github-pr-reviewer/knowledge/system.md +17 -0
  24. package/runtime/agents/github-pr-reviewer/templates/pr-automation-output.md +10 -0
  25. package/runtime/agents/github-pr-reviewer/templates/pr-inspection-output.md +9 -0
  26. package/runtime/agents/github-pr-reviewer/templates/pr-list-output.md +9 -0
  27. package/runtime/agents/github-pr-reviewer/templates/pr-review-output.md +17 -0
  28. package/runtime/cli/README.md +37 -1
  29. package/runtime/cli/aikit/__init__.py +1 -1
  30. package/runtime/cli/aikit/aliases.py +196 -0
  31. package/runtime/cli/aikit/app_home.py +78 -0
  32. package/runtime/cli/aikit/audit.py +344 -0
  33. package/runtime/cli/aikit/calendar.py +163 -0
  34. package/runtime/cli/aikit/configuration_orchestrator.py +291 -0
  35. package/runtime/cli/aikit/decision_store.py +158 -0
  36. package/runtime/cli/aikit/diagnostics.py +9 -0
  37. package/runtime/cli/aikit/fallback.py +48 -2
  38. package/runtime/cli/aikit/github_pr.py +254 -0
  39. package/runtime/cli/aikit/identity.py +75 -0
  40. package/runtime/cli/aikit/llm.py +249 -48
  41. package/runtime/cli/aikit/main.py +1240 -32
  42. package/runtime/cli/aikit/memory.py +148 -8
  43. package/runtime/cli/aikit/model_router.py +70 -0
  44. package/runtime/cli/aikit/notifications.py +9 -0
  45. package/runtime/cli/aikit/ollama.py +237 -0
  46. package/runtime/cli/aikit/permissions.py +345 -0
  47. package/runtime/cli/aikit/personality.py +123 -0
  48. package/runtime/cli/aikit/provider_wizard.py +19 -0
  49. package/runtime/cli/aikit/providers.py +4 -5
  50. package/runtime/cli/aikit/review_gate.py +40 -0
  51. package/runtime/cli/aikit/scheduler.py +18 -0
  52. package/runtime/cli/aikit/sessions.py +396 -0
  53. package/runtime/cli/aikit/setup_wizard.py +12 -0
  54. package/runtime/cli/aikit/tasks.py +351 -0
  55. package/runtime/cli/aikit/toolchain.py +274 -0
  56. package/runtime/providers/calendar.yaml +28 -0
  57. package/runtime/providers/github.yaml +42 -0
  58. package/runtime/providers/local-notification.yaml +19 -0
  59. package/runtime/providers/local-scheduler.yaml +24 -0
  60. package/runtime/vendor/skills/napkin/napkin.md +13 -3
@@ -13,6 +13,9 @@ from dataclasses import dataclass
13
13
  from pathlib import Path
14
14
  from typing import Any
15
15
 
16
+ from cli.aikit.app_home import app_home, config_path as app_config_path, ensure_app_home
17
+ from cli.aikit.identity import host_cli_prompt, identity_system_prompt
18
+
16
19
 
17
20
  @dataclass(frozen=True)
18
21
  class LlmBackend:
@@ -97,17 +100,15 @@ BACKENDS: dict[str, LlmBackend] = {
97
100
 
98
101
  ENV_VAR_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
99
102
  DEFAULT_AGENT_TIMEOUT_SECONDS = 120
103
+ DEFAULT_FALLBACK_ORDER = ("claude-code", "codex-cli", "openai", "anthropic", "openrouter", "ollama")
100
104
 
101
105
 
102
106
  def config_home() -> Path:
103
- raw = os.environ.get("AIKIT_CONFIG_HOME") or os.environ.get("AI_DEVKIT_CONFIG_HOME")
104
- if raw:
105
- return Path(raw).expanduser().resolve()
106
- return (Path.home() / ".ai-devkit").resolve()
107
+ return app_home()
107
108
 
108
109
 
109
110
  def config_path() -> Path:
110
- return config_home() / "config.json"
111
+ return app_config_path()
111
112
 
112
113
 
113
114
  def empty_config() -> dict[str, Any]:
@@ -137,6 +138,7 @@ def load_config() -> dict[str, Any]:
137
138
 
138
139
 
139
140
  def save_config(config: dict[str, Any]) -> Path:
141
+ ensure_app_home()
140
142
  path = config_path()
141
143
  path.parent.mkdir(parents=True, exist_ok=True)
142
144
  with path.open("w", encoding="utf-8") as file:
@@ -156,10 +158,14 @@ def list_backends() -> dict[str, Any]:
156
158
  config = load_config()
157
159
  default_backend = config.get("llm", {}).get("default")
158
160
  configured = set(config.get("llm", {}).get("backends", {}))
161
+ preference = llm_preference(config)
162
+ from cli.aikit.decision_store import get_decision
163
+
159
164
  return {
160
165
  "kind": "llm-backends",
161
166
  "config_path": str(config_path()),
162
167
  "default": default_backend,
168
+ "preference": preference,
163
169
  "items": [
164
170
  {
165
171
  "id": backend.id,
@@ -175,6 +181,8 @@ def list_backends() -> dict[str, Any]:
175
181
  "default_model": backend.default_model,
176
182
  "command": backend.command,
177
183
  "notes": backend.notes,
184
+ "decision": get_decision("llms", backend.id),
185
+ "enabled": not bool((get_decision("llms", backend.id) or {}).get("state") in {"disabled_by_user", "denied_by_user"}),
178
186
  }
179
187
  for backend in BACKENDS.values()
180
188
  ],
@@ -252,6 +260,68 @@ def set_default_backend(backend_id: str) -> dict[str, Any]:
252
260
  }
253
261
 
254
262
 
263
+ def llm_preference(config: dict[str, Any] | None = None) -> dict[str, Any]:
264
+ config = config or load_config()
265
+ llm = config.get("llm") if isinstance(config.get("llm"), dict) else {}
266
+ raw_preference = llm.get("preference") if isinstance(llm.get("preference"), dict) else {}
267
+ primary = raw_preference.get("primary") or llm.get("default")
268
+ order = raw_preference.get("order") if isinstance(raw_preference.get("order"), list) else None
269
+ normalized_order = normalize_backend_order(order or DEFAULT_FALLBACK_ORDER)
270
+ if primary and primary in BACKENDS:
271
+ normalized_order = [primary, *[item for item in normalized_order if item != primary]]
272
+ return {
273
+ "kind": "llm-preference",
274
+ "status": "ok",
275
+ "config_path": str(config_path()),
276
+ "primary": primary,
277
+ "order": normalized_order,
278
+ "fallback_enabled": bool(raw_preference.get("fallback_enabled", True)),
279
+ }
280
+
281
+
282
+ def set_llm_preference(
283
+ *,
284
+ primary: str | None = None,
285
+ order: str | list[str] | tuple[str, ...] | None = None,
286
+ ) -> dict[str, Any]:
287
+ config = load_config()
288
+ llm = config.setdefault("llm", {"default": None, "backends": {}})
289
+ backends = llm.setdefault("backends", {})
290
+ preference = llm.setdefault("preference", {})
291
+ if not isinstance(preference, dict):
292
+ preference = {}
293
+ llm["preference"] = preference
294
+
295
+ if primary:
296
+ backend = backend_or_error(primary)
297
+ backends.setdefault(backend.id, default_backend_config(backend))
298
+ llm["default"] = backend.id
299
+ preference["primary"] = backend.id
300
+ if order is not None:
301
+ preference["order"] = normalize_backend_order(order)
302
+ preference.setdefault("fallback_enabled", True)
303
+ written_path = save_config(config)
304
+ payload = llm_preference(config)
305
+ payload.update({"status": "configured", "config_path": str(written_path), "stored_secret": False})
306
+ return payload
307
+
308
+
309
+ def normalize_backend_order(order: str | list[str] | tuple[str, ...]) -> list[str]:
310
+ raw_items = order.split(",") if isinstance(order, str) else list(order)
311
+ normalized: list[str] = []
312
+ for raw_item in raw_items:
313
+ backend_id = str(raw_item).strip()
314
+ if not backend_id:
315
+ continue
316
+ backend_or_error(backend_id)
317
+ if backend_id not in normalized:
318
+ normalized.append(backend_id)
319
+ for backend_id in DEFAULT_FALLBACK_ORDER:
320
+ if backend_id not in normalized:
321
+ normalized.append(backend_id)
322
+ return normalized
323
+
324
+
255
325
  def default_backend_config(backend: LlmBackend) -> dict[str, Any]:
256
326
  entry: dict[str, Any] = {"kind": backend.kind, "auth": backend.auth}
257
327
  if backend.auth == "api-key-env":
@@ -359,31 +429,59 @@ def resolve_backend(requested: str | None = None) -> dict[str, Any] | None:
359
429
  backend_or_error(requested)
360
430
  return doctor_backend(BACKENDS[requested], config)
361
431
 
362
- default_backend = config.get("llm", {}).get("default")
363
- if default_backend:
364
- backend_or_error(default_backend)
365
- return doctor_backend(BACKENDS[default_backend], config)
366
-
367
- configured = config.get("llm", {}).get("backends", {})
368
- if isinstance(configured, dict):
369
- for backend_id in configured:
370
- if backend_id in BACKENDS:
371
- check = doctor_backend(BACKENDS[backend_id], config)
372
- if check.get("status") == "ok":
373
- return check
432
+ for backend_id in candidate_backend_ids(config=config, allow_fallback=True):
433
+ check = doctor_backend(BACKENDS[backend_id], config)
434
+ if check.get("status") == "ok":
435
+ return check
374
436
 
375
437
  return None
376
438
 
377
439
 
378
- def invoke_agent_prompt(prompt: str, requested: str | None = None) -> dict[str, Any]:
379
- backend = resolve_backend(requested)
380
- if not backend or backend.get("status") != "ok":
440
+ def candidate_backend_ids(
441
+ *,
442
+ config: dict[str, Any],
443
+ requested: str | None = None,
444
+ allow_fallback: bool = True,
445
+ ) -> list[str]:
446
+ if requested:
447
+ backend_or_error(requested)
448
+ return [requested]
449
+ llm = config.get("llm") if isinstance(config.get("llm"), dict) else {}
450
+ configured = llm.get("backends") if isinstance(llm.get("backends"), dict) else {}
451
+ configured_ids = [backend_id for backend_id in configured if backend_id in BACKENDS]
452
+ if not configured_ids:
453
+ return []
454
+ from cli.aikit.decision_store import is_disabled
455
+
456
+ configured_ids = [backend_id for backend_id in configured_ids if not is_disabled("llms", backend_id)]
457
+ if not configured_ids:
458
+ return []
459
+ preference = llm_preference(config)
460
+ ordered = [backend_id for backend_id in preference["order"] if backend_id in configured_ids]
461
+ for backend_id in configured_ids:
462
+ if backend_id not in ordered:
463
+ ordered.append(backend_id)
464
+ return ordered if allow_fallback else ordered[:1]
465
+
466
+
467
+ def invoke_agent_prompt(
468
+ prompt: str,
469
+ requested: str | None = None,
470
+ *,
471
+ public_name: str = "Agent DevKit",
472
+ allow_fallback: bool = True,
473
+ ) -> dict[str, Any]:
474
+ config = load_config()
475
+ candidate_ids = candidate_backend_ids(config=config, requested=requested, allow_fallback=allow_fallback)
476
+ if not candidate_ids:
381
477
  return {
382
478
  "kind": "agent",
383
479
  "status": "blocked",
384
480
  "ok": False,
385
481
  "requires_llm": True,
386
482
  "llm_backend": requested,
483
+ "llm_backend_attempts": [],
484
+ "llm_fallback_enabled": allow_fallback,
387
485
  "prompt_received": True,
388
486
  "prompt_length": len(prompt),
389
487
  "message": "agent requires a configured LLM backend for natural-language tasks.",
@@ -395,36 +493,107 @@ def invoke_agent_prompt(prompt: str, requested: str | None = None) -> dict[str,
395
493
  "exit_code": 2,
396
494
  }
397
495
 
398
- try:
399
- response = invoke_resolved_backend(backend, prompt)
400
- except LlmInvocationError as exc:
496
+ attempts: list[dict[str, Any]] = []
497
+ last_error: str | None = None
498
+ for backend_id in candidate_ids:
499
+ backend = doctor_backend(BACKENDS[backend_id], config)
500
+ attempt = {"id": backend_id, "status": backend.get("status")}
501
+ attempts.append(attempt)
502
+ if backend.get("status") != "ok":
503
+ attempt["message"] = backend.get("message")
504
+ continue
505
+ try:
506
+ response = invoke_resolved_backend(backend, prompt, public_name=public_name)
507
+ except LlmPolicyError as exc:
508
+ attempt["status"] = "policy-blocked"
509
+ attempt["message"] = str(exc)
510
+ return failed_agent_payload(
511
+ prompt,
512
+ backend=backend,
513
+ attempts=attempts,
514
+ message=str(exc),
515
+ policy_error=True,
516
+ allow_fallback=allow_fallback,
517
+ )
518
+ except LlmInvocationError as exc:
519
+ attempt["status"] = "failed"
520
+ attempt["message"] = str(exc)
521
+ last_error = str(exc)
522
+ if requested or not allow_fallback:
523
+ break
524
+ continue
525
+ attempt["status"] = "ok"
401
526
  return {
402
527
  "kind": "agent",
403
- "status": "failed",
404
- "ok": False,
528
+ "status": "ok",
529
+ "ok": True,
405
530
  "requires_llm": True,
406
531
  "llm_backend": backend["id"],
407
532
  "llm_backend_status": backend["status"],
533
+ "llm_backend_attempts": attempts,
534
+ "llm_fallback_enabled": allow_fallback,
535
+ "llm_fallback_used": attempts[0]["id"] != backend["id"],
408
536
  "prompt_received": True,
409
537
  "prompt_length": len(prompt),
410
- "message": str(exc),
411
- "next_steps": [
412
- "Run `agent llm doctor` to verify the selected backend.",
413
- "Use `agent run <agent> <capability>` when deterministic execution is enough.",
414
- ],
415
- "exit_code": 1,
538
+ "response": response,
416
539
  }
417
540
 
541
+ if last_error:
542
+ return failed_agent_payload(
543
+ prompt,
544
+ backend={"id": attempts[-1]["id"], "status": attempts[-1].get("status")},
545
+ attempts=attempts,
546
+ message=last_error,
547
+ policy_error=False,
548
+ allow_fallback=allow_fallback,
549
+ )
418
550
  return {
419
551
  "kind": "agent",
420
- "status": "ok",
421
- "ok": True,
552
+ "status": "blocked",
553
+ "ok": False,
422
554
  "requires_llm": True,
423
- "llm_backend": backend["id"],
424
- "llm_backend_status": backend["status"],
555
+ "llm_backend": requested,
556
+ "llm_backend_attempts": attempts,
557
+ "llm_fallback_enabled": allow_fallback,
425
558
  "prompt_received": True,
426
559
  "prompt_length": len(prompt),
427
- "response": response,
560
+ "message": "agent did not find an available configured LLM backend.",
561
+ "next_steps": [
562
+ "Run `agent llm doctor` to verify configured backends.",
563
+ "Configure a backend with `agent llm configure <backend> --set-default`.",
564
+ "Adjust fallback order with `agent llm preference set --primary <backend>`.",
565
+ ],
566
+ "exit_code": 2,
567
+ }
568
+
569
+
570
+ def failed_agent_payload(
571
+ prompt: str,
572
+ *,
573
+ backend: dict[str, Any],
574
+ attempts: list[dict[str, Any]],
575
+ message: str,
576
+ policy_error: bool,
577
+ allow_fallback: bool,
578
+ ) -> dict[str, Any]:
579
+ return {
580
+ "kind": "agent",
581
+ "status": "failed",
582
+ "ok": False,
583
+ "requires_llm": True,
584
+ "llm_backend": backend.get("id"),
585
+ "llm_backend_status": backend.get("status"),
586
+ "llm_backend_attempts": attempts,
587
+ "llm_fallback_enabled": allow_fallback,
588
+ "llm_policy_error": policy_error,
589
+ "prompt_received": True,
590
+ "prompt_length": len(prompt),
591
+ "message": message,
592
+ "next_steps": [
593
+ "Run `agent llm doctor` to verify the selected backend.",
594
+ "Use `agent run <agent> <capability>` when deterministic execution is enough.",
595
+ ],
596
+ "exit_code": 1,
428
597
  }
429
598
 
430
599
 
@@ -432,21 +601,33 @@ class LlmInvocationError(RuntimeError):
432
601
  """Raised for safe, user-facing LLM invocation errors."""
433
602
 
434
603
 
435
- def invoke_resolved_backend(backend: dict[str, Any], prompt: str) -> str:
604
+ class LlmPolicyError(LlmInvocationError):
605
+ """Raised when the backend rejects content for policy or safety reasons."""
606
+
607
+
608
+ def invoke_resolved_backend(backend: dict[str, Any], prompt: str, *, public_name: str = "Agent DevKit") -> str:
436
609
  kind = backend.get("kind")
437
610
  backend_id = backend.get("id")
438
611
  if kind == "openai-compatible":
439
- return invoke_openai_compatible(backend, prompt)
612
+ return invoke_openai_compatible(backend, prompt, public_name=public_name)
440
613
  if kind == "anthropic":
441
- return invoke_anthropic(backend, prompt)
614
+ return invoke_anthropic(backend, prompt, public_name=public_name)
442
615
  if kind == "host-cli" and backend_id == "codex-cli":
443
- return invoke_host_cli([str(backend.get("command") or "codex"), "exec", "--skip-git-repo-check", "--ephemeral", prompt])
616
+ return invoke_host_cli(
617
+ [
618
+ str(backend.get("command") or "codex"),
619
+ "exec",
620
+ "--skip-git-repo-check",
621
+ "--ephemeral",
622
+ host_cli_prompt(prompt, name=public_name),
623
+ ]
624
+ )
444
625
  if kind == "host-cli" and backend_id == "claude-code":
445
- return invoke_host_cli([str(backend.get("command") or "claude"), "--print", "--permission-mode", "plan", prompt])
626
+ return invoke_host_cli([str(backend.get("command") or "claude"), "--print", "--permission-mode", "plan", host_cli_prompt(prompt, name=public_name)])
446
627
  raise LlmInvocationError(f"LLM backend is not invokable by agent yet: {backend_id}")
447
628
 
448
629
 
449
- def invoke_openai_compatible(backend: dict[str, Any], prompt: str) -> str:
630
+ def invoke_openai_compatible(backend: dict[str, Any], prompt: str, *, public_name: str = "Agent DevKit") -> str:
450
631
  base_url = str(backend.get("base_url") or "").rstrip("/")
451
632
  model = str(backend.get("model") or "")
452
633
  if not base_url or not model:
@@ -460,10 +641,7 @@ def invoke_openai_compatible(backend: dict[str, Any], prompt: str) -> str:
460
641
  "messages": [
461
642
  {
462
643
  "role": "system",
463
- "content": (
464
- "You are AI DevKit's natural-language router. Answer concisely, "
465
- "prefer deterministic agent/capability commands when useful, and never ask for all credentials upfront."
466
- ),
644
+ "content": identity_system_prompt(name=public_name),
467
645
  },
468
646
  {"role": "user", "content": prompt},
469
647
  ],
@@ -475,7 +653,7 @@ def invoke_openai_compatible(backend: dict[str, Any], prompt: str) -> str:
475
653
  raise LlmInvocationError("OpenAI-compatible backend returned an unexpected response shape.") from exc
476
654
 
477
655
 
478
- def invoke_anthropic(backend: dict[str, Any], prompt: str) -> str:
656
+ def invoke_anthropic(backend: dict[str, Any], prompt: str, *, public_name: str = "Agent DevKit") -> str:
479
657
  api_key = api_key_from_ref(backend.get("api_key_ref"))
480
658
  if not api_key:
481
659
  raise LlmInvocationError("Anthropic backend is missing an API key environment variable.")
@@ -491,7 +669,7 @@ def invoke_anthropic(backend: dict[str, Any], prompt: str) -> str:
491
669
  body = {
492
670
  "model": model,
493
671
  "max_tokens": 1024,
494
- "system": "You are AI DevKit's natural-language router. Answer concisely and prefer deterministic commands when useful.",
672
+ "system": identity_system_prompt(name=public_name),
495
673
  "messages": [{"role": "user", "content": prompt}],
496
674
  }
497
675
  payload = post_json(f"{base_url}/messages", headers, body)
@@ -527,6 +705,13 @@ def post_json(url: str, headers: dict[str, str], body: dict[str, Any]) -> dict[s
527
705
  with urllib.request.urlopen(request, timeout=DEFAULT_AGENT_TIMEOUT_SECONDS) as response: # noqa: S310 - user-configured backend URL.
528
706
  raw = response.read().decode("utf-8")
529
707
  except urllib.error.HTTPError as exc:
708
+ raw_error = ""
709
+ try:
710
+ raw_error = exc.read().decode("utf-8", errors="replace")
711
+ except OSError:
712
+ raw_error = ""
713
+ if is_policy_error(exc.code, raw_error):
714
+ raise LlmPolicyError(f"LLM backend policy error: {exc.code}") from exc
530
715
  raise LlmInvocationError(f"LLM backend HTTP error: {exc.code}") from exc
531
716
  except urllib.error.URLError as exc:
532
717
  raise LlmInvocationError(f"LLM backend connection failed: {exc.reason}") from exc
@@ -536,9 +721,25 @@ def post_json(url: str, headers: dict[str, str], body: dict[str, Any]) -> dict[s
536
721
  raise LlmInvocationError("LLM backend returned invalid JSON.") from exc
537
722
  if not isinstance(payload, dict):
538
723
  raise LlmInvocationError("LLM backend returned a non-object JSON payload.")
724
+ if payload_has_policy_error(payload):
725
+ raise LlmPolicyError("LLM backend returned a policy error.")
539
726
  return payload
540
727
 
541
728
 
729
+ def is_policy_error(status_code: int, body: str) -> bool:
730
+ if status_code not in {400, 403}:
731
+ return False
732
+ return any(marker in body.lower() for marker in ("policy", "safety", "content_filter", "content filter", "not allowed"))
733
+
734
+
735
+ def payload_has_policy_error(payload: dict[str, Any]) -> bool:
736
+ error = payload.get("error")
737
+ if not isinstance(error, dict):
738
+ return False
739
+ text = json.dumps(error, ensure_ascii=False).lower()
740
+ return any(marker in text for marker in ("policy", "safety", "content_filter", "content filter", "not allowed"))
741
+
742
+
542
743
  def api_key_from_ref(value: Any) -> str | None:
543
744
  ref = str(value or "")
544
745
  if not ref.startswith("env:"):