vis-agent 0.2.54.dev7904__tar.gz → 0.3.1__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 (146) hide show
  1. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/PKG-INFO +1 -1
  2. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/agents.json +34 -0
  3. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/config.json +0 -6
  4. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/pyproject.toml +1 -1
  5. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/_outside.py +20 -0
  6. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/engine/_agents.py +5 -1
  7. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/extension.py +17 -0
  8. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/extension_package.py +31 -0
  9. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_agents.py +23 -0
  10. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_extension_package.py +22 -0
  11. vis_agent-0.2.54.dev7904/release-tag +0 -1
  12. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/.gitignore +0 -0
  13. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/LICENSE +0 -0
  14. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/README.md +0 -0
  15. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/activity-arguments.json +0 -0
  16. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/activity-cases.json +0 -0
  17. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/activity-copy.json +0 -0
  18. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/activity-execution.json +0 -0
  19. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/activity-groups.json +0 -0
  20. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/activity-read-session.json +0 -0
  21. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/activity-reads.json +0 -0
  22. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/activity-results.json +0 -0
  23. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/activity-stream.json +0 -0
  24. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/activity-tables.json +0 -0
  25. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/activity.json +0 -0
  26. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/diff.json +0 -0
  27. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/live-primitives.json +0 -0
  28. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/session-health.json +0 -0
  29. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/fixtures/view.json +0 -0
  30. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/activity.json +0 -0
  31. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/automations.json +0 -0
  32. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/common.json +0 -0
  33. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/content.json +0 -0
  34. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/council.json +0 -0
  35. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/diff.json +0 -0
  36. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/evals.json +0 -0
  37. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/gateway.json +0 -0
  38. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/improve.json +0 -0
  39. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/provider.json +0 -0
  40. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/rooms.json +0 -0
  41. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/symbol.json +0 -0
  42. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/toggle.json +0 -0
  43. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/contracts/schema/view.json +0 -0
  44. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/examples/greeter/tests/test_greeter.py +0 -0
  45. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/hatch_build.py +0 -0
  46. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/__init__.py +0 -0
  47. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/_contracts.py +0 -0
  48. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/_wire.py +0 -0
  49. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/activity.py +0 -0
  50. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/engine/__init__.py +0 -0
  51. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/engine/_agent.py +0 -0
  52. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/engine/_client.py +0 -0
  53. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/engine/_council.py +0 -0
  54. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/engine/_extensions.py +0 -0
  55. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/engine/_host.py +0 -0
  56. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/engine/_install.py +0 -0
  57. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/engine/_local.py +0 -0
  58. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/engine/_models.py +0 -0
  59. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/__init__.py +0 -0
  60. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/_gateway.py +0 -0
  61. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/agent.py +0 -0
  62. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/bench.py +0 -0
  63. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/check.py +0 -0
  64. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/compare.py +0 -0
  65. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/environment.py +0 -0
  66. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/grade.py +0 -0
  67. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/harbor.py +0 -0
  68. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/judge-rubric.toml +0 -0
  69. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/judge.py +0 -0
  70. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/labels.py +0 -0
  71. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/leaderboard.py +0 -0
  72. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/repeats.py +0 -0
  73. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/rounds.py +0 -0
  74. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/stages.py +0 -0
  75. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/stats.py +0 -0
  76. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/evals/trajectory.py +0 -0
  77. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/py.typed +0 -0
  78. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/rooms.py +0 -0
  79. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/src/blockether/vis/views.py +0 -0
  80. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/conftest.py +0 -0
  81. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/evals_atif.py +0 -0
  82. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_activity.py +0 -0
  83. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_agent.py +0 -0
  84. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_agent_extension_lifecycle.py +0 -0
  85. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_agent_extensions.py +0 -0
  86. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_api_guides.py +0 -0
  87. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_attachments.py +0 -0
  88. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_authoring.py +0 -0
  89. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_catalog.py +0 -0
  90. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_catalog_recipe.py +0 -0
  91. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_ci_report_recipe.py +0 -0
  92. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_client.py +0 -0
  93. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_client_extensions_engine.py +0 -0
  94. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_code_quality_recipe.py +0 -0
  95. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_contracts.py +0 -0
  96. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_council.py +0 -0
  97. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_declarations.py +0 -0
  98. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_engine.py +0 -0
  99. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_engine_docstrings.py +0 -0
  100. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_agent.py +0 -0
  101. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_bench.py +0 -0
  102. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_check.py +0 -0
  103. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_compare.py +0 -0
  104. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_environment.py +0 -0
  105. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_gateway.py +0 -0
  106. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_grade.py +0 -0
  107. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_guards.py +0 -0
  108. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_harbor.py +0 -0
  109. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_judge.py +0 -0
  110. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_labels.py +0 -0
  111. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_repeats.py +0 -0
  112. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_rounds.py +0 -0
  113. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_stages.py +0 -0
  114. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_stats.py +0 -0
  115. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_tasks.py +0 -0
  116. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_evals_trajectory.py +0 -0
  117. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_extension_reference.py +0 -0
  118. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_gateway_performance.py +0 -0
  119. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_goals.py +0 -0
  120. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_install.py +0 -0
  121. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_live_primitives.py +0 -0
  122. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_live_wait.py +0 -0
  123. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_local.py +0 -0
  124. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_method_typing.py +0 -0
  125. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_models.py +0 -0
  126. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_monitor_recipe.py +0 -0
  127. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_namespace.py +0 -0
  128. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_outside.py +0 -0
  129. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_providers.py +0 -0
  130. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_read_session.py +0 -0
  131. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_record_arguments.py +0 -0
  132. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_registration.py +0 -0
  133. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_repair_hooks.py +0 -0
  134. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_result_sequence.py +0 -0
  135. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_rooms.py +0 -0
  136. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_scoped_settings.py +0 -0
  137. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_sdk_bootstrap.py +0 -0
  138. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_sdk_guide.py +0 -0
  139. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_sdk_typing.py +0 -0
  140. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_shortcuts_live.py +0 -0
  141. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_structured_live.py +0 -0
  142. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_symbol_contract.py +0 -0
  143. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_testing.py +0 -0
  144. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_text_list_arguments.py +0 -0
  145. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_tools.py +0 -0
  146. {vis_agent-0.2.54.dev7904 → vis_agent-0.3.1}/tests/test_views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vis-agent
3
- Version: 0.2.54.dev7904
3
+ Version: 0.3.1
4
4
  Summary: The Vis Python SDK — author extensions and control local or remote engines.
5
5
  Project-URL: Homepage, https://vis.blockether.com
6
6
  Project-URL: Documentation, https://vis.blockether.com
@@ -481,6 +481,40 @@
481
481
  },
482
482
  "warning": {
483
483
  "type": "string"
484
+ },
485
+ "error": {
486
+ "description": "Why a failed child stopped. Code rate_limited means a 429 from its provider.",
487
+ "type": "object",
488
+ "properties": {
489
+ "code": {
490
+ "enum": [
491
+ "rate_limited"
492
+ ]
493
+ },
494
+ "message": {
495
+ "type": "string"
496
+ },
497
+ "provider": {
498
+ "type": "string",
499
+ "minLength": 1
500
+ },
501
+ "model": {
502
+ "type": "string",
503
+ "minLength": 1
504
+ }
505
+ },
506
+ "required": [
507
+ "code",
508
+ "message"
509
+ ],
510
+ "additionalProperties": false
511
+ },
512
+ "alternatives": {
513
+ "description": "Working provider and model routes for a new child, outside a rate-limit or authentication cooldown.",
514
+ "type": "array",
515
+ "items": {
516
+ "$ref": "#/$defs/model_pair"
517
+ }
484
518
  }
485
519
  },
486
520
  "required": [
@@ -700,12 +700,6 @@
700
700
  "enabled": {
701
701
  "type": "boolean"
702
702
  },
703
- "environment": {
704
- "enum": [
705
- "declared",
706
- "inherit"
707
- ]
708
- },
709
703
  "filesystem": {
710
704
  "$ref": "#/$defs/jailFilesystem"
711
705
  },
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
8
8
 
9
9
  [project]
10
10
  name = "vis-agent"
11
- version = "0.2.54.dev7904"
11
+ version = "0.3.1"
12
12
  description = "The Vis Python SDK — author extensions and control local or remote engines."
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.12"
@@ -111,6 +111,25 @@ def workspace_root():
111
111
  return str(Path.cwd())
112
112
 
113
113
 
114
+ def workspace_files():
115
+ """List the files that Git sees under the process directory, or None."""
116
+ root = Path.cwd()
117
+ try:
118
+ done = subprocess.run(
119
+ ["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"],
120
+ cwd=root,
121
+ capture_output=True,
122
+ timeout=60,
123
+ check=False,
124
+ )
125
+ except (OSError, subprocess.TimeoutExpired):
126
+ return None
127
+ if done.returncode != 0:
128
+ return None
129
+ names = done.stdout.decode("utf-8", "surrogateescape").split("\0")
130
+ return [str(root / name) for name in names if name]
131
+
132
+
114
133
  def python_environment():
115
134
  """Use the project environment when no Vis session owns this call."""
116
135
  return "project"
@@ -1443,6 +1462,7 @@ _IMPLEMENTATIONS = {
1443
1462
  "workspace_root": workspace_root,
1444
1463
  "project_root": workspace_root,
1445
1464
  "python_environment": python_environment,
1465
+ "workspace_files": workspace_files,
1446
1466
  "setting_declaration": setting_declaration,
1447
1467
  "setting": setting,
1448
1468
  "state_get": state_get,
@@ -40,7 +40,9 @@ class SubagentRecord:
40
40
  ``status`` is ``asking`` when the child waits for its parent. Then ``question``,
41
41
  ``budget_request`` or ``files_request`` tells what it needs. ``held`` means that a
42
42
  human holds the queue of the child; only a person can release it. ``unknown`` names a
43
- session that is not a subagent of the caller. ``warning`` reports a file overlap.
43
+ session that is not a subagent of the caller. ``warning`` reports a file overlap. A child
44
+ that a 429 stopped is ``failed`` with ``error`` (code ``rate_limited``) and ``alternatives``:
45
+ the working provider and model routes for a new child.
44
46
  """
45
47
 
46
48
  session_id: str
@@ -54,6 +56,8 @@ class SubagentRecord:
54
56
  iteration_budget: int | None = None
55
57
  iterations_used: int | None = None
56
58
  warning: str | None = None
59
+ error: dict[str, Any] | None = None
60
+ alternatives: list[dict[str, str]] | None = None
57
61
 
58
62
  @classmethod
59
63
  def from_wire(cls, value: dict[str, Any]) -> SubagentRecord:
@@ -65,6 +65,10 @@ class Host(Protocol):
65
65
  """Read the project's `python.environment`: `project` or `shared`."""
66
66
  ...
67
67
 
68
+ def workspace_files(self) -> list[str] | None:
69
+ """List the files that Git sees in the working copy, or None without Git."""
70
+ ...
71
+
68
72
  def setting_declaration(self, spec: Mapping[str, Any]) -> Mapping[str, Any]:
69
73
  """Validate a declaration and fill defaults from the contract schema."""
70
74
  ...
@@ -3497,6 +3501,19 @@ def project_root() -> Path:
3497
3501
  return Path(_host.project_root())
3498
3502
 
3499
3503
 
3504
+ def workspace_files() -> list[Path] | None:
3505
+ """Return the files under `workspace_root()` that Git sees.
3506
+
3507
+ The list holds tracked files and untracked files that Git does not ignore.
3508
+ The host lists the working copy once and gives the same list to each
3509
+ extension that asks within a short time. Use it instead of a child process
3510
+ for each extension. The result is None when the workspace is not in a Git
3511
+ working tree. Outside Vis, it lists the process directory.
3512
+ """
3513
+ files = _host.workspace_files()
3514
+ return None if files is None else [Path(name) for name in files]
3515
+
3516
+
3500
3517
  def python_environment() -> Literal["project", "shared"]:
3501
3518
  """Return the Python environment that the session's project selects.
3502
3519
 
@@ -1410,6 +1410,35 @@ def _sync_one(name, spec, directory, current, refresh, vis_version):
1410
1410
  return {**result, "status": "updated" if exists else "installed"}
1411
1411
 
1412
1412
 
1413
+ def _sync_read_only(specs, directory):
1414
+ """Reconcile a scope that cannot be written, without the sync lock or network.
1415
+
1416
+ A container image or a read-only mount holds packages that a writable sync prepared.
1417
+ Ready packages stay cached, also with `latest`. A needed change fails and names the step.
1418
+ """
1419
+ records = _sync_records(directory)
1420
+ results = []
1421
+ for name in sorted(set(specs) | set(records)):
1422
+ spec, current = specs.get(name), records.get(name)
1423
+ if spec is None:
1424
+ results.append({"name": name, "status": "orphaned"})
1425
+ elif (
1426
+ current is not None
1427
+ and current["spec"] == spec
1428
+ and _sync_owned(_destination(directory, name), current)
1429
+ ):
1430
+ results.append({**current["result"], "name": name, "status": "cached"})
1431
+ else:
1432
+ results.append(
1433
+ {
1434
+ "name": name,
1435
+ "status": "failed",
1436
+ "error": f"{directory} is read-only. Run the sync where it is writable",
1437
+ }
1438
+ )
1439
+ return results
1440
+
1441
+
1413
1442
  def sync(
1414
1443
  configured,
1415
1444
  directory,
@@ -1435,6 +1464,8 @@ def sync(
1435
1464
  directory = Path(directory).expanduser().resolve()
1436
1465
  if not directory.exists() and not specs:
1437
1466
  return []
1467
+ if not dry_run and directory.is_dir() and not os.access(directory, os.W_OK):
1468
+ return _sync_read_only(specs, directory)
1438
1469
  if not dry_run:
1439
1470
  directory.mkdir(parents=True, exist_ok=True)
1440
1471
  lock = directory / ".sync-lock"
@@ -180,6 +180,11 @@ def test_child_controls_answer_records_and_wait_longer_than_the_gateway():
180
180
  {"status": "completed"},
181
181
  {"session_id": "child", "status": "invented"},
182
182
  {"session_id": "child", "status": "completed", "future_field": 1},
183
+ {
184
+ "session_id": "child",
185
+ "status": "failed",
186
+ "error": {"code": "other", "message": "x"},
187
+ },
183
188
  ],
184
189
  )
185
190
  def test_child_record_response_is_validated(record):
@@ -187,6 +192,24 @@ def test_child_record_response_is_validated(record):
187
192
  SubagentRecord.from_wire(record)
188
193
 
189
194
 
195
+ def test_rate_limited_child_record_names_working_routes():
196
+ record = SubagentRecord.from_wire(
197
+ {
198
+ "session_id": "child",
199
+ "status": "failed",
200
+ "error": {
201
+ "code": "rate_limited",
202
+ "message": "Provider rate-limited",
203
+ "provider": "busy",
204
+ "model": "large",
205
+ },
206
+ "alternatives": [{"provider": "spare", "model": "small"}],
207
+ }
208
+ )
209
+ assert record.error["code"] == "rate_limited"
210
+ assert record.alternatives == [{"provider": "spare", "model": "small"}]
211
+
212
+
190
213
  @pytest.mark.parametrize("code", ["disabled", "group-not-found"])
191
214
  def test_unavailable_council_binding_does_not_block_team_controls(code):
192
215
  from blockether.vis.engine import GatewayError
@@ -1022,6 +1022,28 @@ def test_sync_local_install_cache_and_explicit_prune(tmp_path):
1022
1022
  assert not (target / "vis-greeter/current").exists()
1023
1023
 
1024
1024
 
1025
+ # A read-only image holds prepared packages; loading them must not need the sync lock.
1026
+ @pytest.mark.skipif(os.geteuid() == 0, reason="root ignores directory permissions")
1027
+ def test_sync_reads_a_read_only_scope_without_lock_or_changes(tmp_path):
1028
+ source = project(tmp_path / "source")
1029
+ target = tmp_path / "extensions"
1030
+ configured = {"vis-greeter": {"source": str(source)}}
1031
+ installed = package.sync(configured, target, trust=True)[0]
1032
+ target.chmod(0o555)
1033
+ try:
1034
+ cached = package.sync(configured, target, trust=True)[0]
1035
+ assert cached["status"] == "cached"
1036
+ assert cached["path"] == installed["path"]
1037
+ assert package.sync({}, target, trust=True)[0]["status"] == "orphaned"
1038
+ changed = {"vis-other": {"source": str(source)}}
1039
+ failed = {r["name"]: r for r in package.sync(changed, target, trust=True)}
1040
+ assert failed["vis-other"]["status"] == "failed"
1041
+ assert "read-only" in failed["vis-other"]["error"]
1042
+ assert not (target / ".sync-lock").exists()
1043
+ finally:
1044
+ target.chmod(0o755)
1045
+
1046
+
1025
1047
  def test_sync_github_warm_path_does_not_fetch_or_check_catalog(releases, monkeypatch):
1026
1048
  _, target, commands = releases
1027
1049
  configured = {
@@ -1 +0,0 @@
1
- beta-317ff6130bb5a97928fab5a74225d812ace95bff
File without changes
File without changes
File without changes