ltcai 11.7.0 → 12.0.0

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 (174) hide show
  1. package/README.md +100 -76
  2. package/docs/BENCHMARKS.md +9 -2
  3. package/docs/CHANGELOG.md +249 -0
  4. package/docs/CI_AND_RELEASE_GATES.md +126 -41
  5. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  6. package/docs/DEVELOPMENT.md +271 -103
  7. package/docs/ENTERPRISE.md +1 -1
  8. package/docs/LEGACY_COMPATIBILITY.md +10 -6
  9. package/docs/MULTI_AGENT_RUNTIME.md +4 -4
  10. package/docs/ONBOARDING.md +16 -4
  11. package/docs/OPERATIONS.md +14 -1
  12. package/docs/PERMISSION_MODE.md +14 -9
  13. package/docs/REALTIME_COLLABORATION.md +1 -1
  14. package/docs/ROADMAP.md +113 -0
  15. package/docs/TRUST_MODEL.md +28 -7
  16. package/docs/USABILITY_AUDIT.md +5 -0
  17. package/docs/WHY_LATTICE.md +13 -5
  18. package/docs/WORKFLOW_DESIGNER.md +2 -2
  19. package/docs/kg-schema.md +57 -7
  20. package/docs/mcp-tools.md +93 -82
  21. package/docs/security-model.md +6 -3
  22. package/lattice_brain/__init__.py +1 -1
  23. package/lattice_brain/graph/_kg_common/__init__.py +1 -54
  24. package/lattice_brain/graph/_kg_common/extraction.py +459 -105
  25. package/lattice_brain/graph/_kg_common/normalize.py +305 -0
  26. package/lattice_brain/graph/_kg_common/patterns.py +275 -0
  27. package/lattice_brain/graph/_kg_common/relations.py +12 -3
  28. package/lattice_brain/graph/_kg_common/sections.py +107 -0
  29. package/lattice_brain/graph/_kg_common/text.py +14 -450
  30. package/lattice_brain/graph/_kg_constants.py +7 -0
  31. package/lattice_brain/ingestion/__init__.py +6 -3
  32. package/lattice_brain/multimodal/__init__.py +9 -3
  33. package/latticeai/__init__.py +1 -1
  34. package/latticeai/api/agent_worker_seam.py +44 -1
  35. package/latticeai/api/models.py +18 -110
  36. package/latticeai/api/search.py +7 -30
  37. package/latticeai/api/worker_compute.py +127 -106
  38. package/latticeai/api/worker_seams.py +17 -2
  39. package/latticeai/core/embedding_providers/__init__.py +16 -0
  40. package/latticeai/core/embedding_providers/autodetect.py +302 -0
  41. package/latticeai/core/embedding_providers/base.py +25 -0
  42. package/latticeai/core/embedding_providers/profiles.py +44 -0
  43. package/latticeai/core/embedding_providers/text.py +74 -8
  44. package/latticeai/core/http_origin.py +3 -3
  45. package/latticeai/core/messages.py +0 -5
  46. package/latticeai/core/policy.py +1 -6
  47. package/latticeai/core/quiet.py +1 -20
  48. package/latticeai/core/security.py +29 -83
  49. package/latticeai/core/sessions.py +95 -4
  50. package/latticeai/core/users.py +0 -38
  51. package/latticeai/core/vector_index/__init__.py +61 -0
  52. package/latticeai/core/vector_index/hnsw.py +383 -0
  53. package/latticeai/core/vector_index/sidecar.py +329 -0
  54. package/latticeai/models/router/catalog.py +2 -2
  55. package/latticeai/models/router/generation.py +176 -30
  56. package/latticeai/models/router/loading.py +150 -9
  57. package/latticeai/runtime/access_runtime.py +7 -4
  58. package/latticeai/runtime/brain_runtime.py +43 -9
  59. package/latticeai/runtime/build_phases/features.py +8 -31
  60. package/latticeai/runtime/build_phases/foundation.py +7 -16
  61. package/latticeai/runtime/build_phases/web.py +3 -3
  62. package/latticeai/runtime/build_phases/worker_profile.py +29 -27
  63. package/latticeai/runtime/runtime_context.py +0 -2
  64. package/latticeai/services/architecture_readiness.py +18 -19
  65. package/latticeai/services/process_audit.py +1 -22
  66. package/latticeai/services/product_readiness.py +39 -12
  67. package/latticeai/services/search_service.py +7 -0
  68. package/latticeai/services/voice_capture.py +8 -28
  69. package/latticeai/tools/__init__.py +12 -47
  70. package/latticeai/tools/commands.py +9 -15
  71. package/latticeai/tools/documents.py +12 -0
  72. package/latticeai/tools/knowledge.py +0 -6
  73. package/latticeai/tools/markup.py +152 -0
  74. package/package.json +4 -5
  75. package/requirements.txt +0 -1
  76. package/scripts/check_current_release_docs.mjs +1 -1
  77. package/scripts/check_openapi_drift.mjs +3 -2
  78. package/scripts/check_server_i18n.mjs +5 -4
  79. package/scripts/compose_openapi.py +4 -1
  80. package/scripts/export_openapi.py +5 -4
  81. package/scripts/gen_worker_allowlist_fixture.py +2 -2
  82. package/scripts/openapi_route_families.json +19 -74
  83. package/scripts/publish_release.mjs +157 -0
  84. package/scripts/release_screen_claims.json +144 -28
  85. package/src-tauri/Cargo.lock +45 -10
  86. package/src-tauri/Cargo.toml +1 -1
  87. package/src-tauri/tauri.conf.json +1 -1
  88. package/static/app/asset-manifest.json +47 -41
  89. package/static/app/assets/Act-Cf1L2709.js +2 -0
  90. package/static/app/assets/AdminConsole-DPAbLTYV.js +1 -0
  91. package/static/app/assets/Brain-DqamGrj-.js +2 -0
  92. package/static/app/assets/BrainHome-MHe2_RYs.js +2 -0
  93. package/static/app/assets/BrainSignals-CQPPfyyH.js +1 -0
  94. package/static/app/assets/Capture-DGdIH_Zc.js +1 -0
  95. package/static/app/assets/Chronicle-C-UlCJoJ.js +1 -0
  96. package/static/app/assets/CommandPalette-WNT4EqUX.js +1 -0
  97. package/static/app/assets/DigitalBrainExplorer-CEBH5Cwc.js +321 -0
  98. package/static/app/assets/Library-C6xd1dlf.js +1 -0
  99. package/static/app/assets/LivingBrain-BEk-0ohw.js +1 -0
  100. package/static/app/assets/ProductFlow-CZLm5iXh.js +1 -0
  101. package/static/app/assets/QueryClientProvider-B3OjqSyJ.js +1 -0
  102. package/static/app/assets/{ReviewCard-HXRle3qq.js → ReviewCard-CEHG6evf.js} +2 -2
  103. package/static/app/assets/RunsListPanel-CLtEJSRW.js +1 -0
  104. package/static/app/assets/System-CAxwBUXw.js +1 -0
  105. package/static/app/assets/WorkflowGraph-Dj10RuGE.js +1 -0
  106. package/static/app/assets/WorkflowsPanel-Kyeh_LIT.js +2 -0
  107. package/static/app/assets/actHelpers-CtSmK9Dw.js +1 -0
  108. package/static/app/assets/arrow-left-CRl5EO4D.js +1 -0
  109. package/static/app/assets/{bot-Cn8bWRuq.js → bot-DhUGRel2.js} +1 -1
  110. package/static/app/assets/brain-CLkhHsHF.js +1 -0
  111. package/static/app/assets/button-CmaEqG1T.js +1 -0
  112. package/static/app/assets/circle-check-CFgejkOS.js +1 -0
  113. package/static/app/assets/{circle-pause-CmzC_apg.js → circle-pause-l96izbxj.js} +1 -1
  114. package/static/app/assets/{circle-play-D8mW2aQ7.js → circle-play-CrZa25_q.js} +1 -1
  115. package/static/app/assets/{cpu-DZcdd0PZ.js → cpu-BaXudqwl.js} +1 -1
  116. package/static/app/assets/{download-bv1KEPGQ.js → download-hCVFPiyc.js} +1 -1
  117. package/static/app/assets/{folder-open-d-Pip5gr.js → folder-open-CHL82Yp7.js} +1 -1
  118. package/static/app/assets/{hard-drive-D20iavUb.js → hard-drive-DDzET7lk.js} +1 -1
  119. package/static/app/assets/index-CB93CZWW.css +2 -0
  120. package/static/app/assets/index-D2H-wSl6.js +13 -0
  121. package/static/app/assets/input-Df1CAY_I.js +1 -0
  122. package/static/app/assets/jsx-runtime-bzQ4Vb5N.js +1 -0
  123. package/static/app/assets/{link-2-BPJOFlAy.js → link-2-xNnTIX1_.js} +1 -1
  124. package/static/app/assets/{permissionCopy-ChdJd493.js → permissionCopy-D3aWHco-.js} +1 -1
  125. package/static/app/assets/primitives-BioD2slS.js +1 -0
  126. package/static/app/assets/search-BzBw8YcW.js +1 -0
  127. package/static/app/assets/{share-2-YNX_NtMU.js → share-2-FkzGf8Df.js} +1 -1
  128. package/static/app/assets/{shield-alert-DuQ3zrVL.js → shield-alert-B3dwzik4.js} +1 -1
  129. package/static/app/assets/sourceMeta-DQSY_tah.js +1 -0
  130. package/static/app/assets/textarea-P8o6pvOP.js +1 -0
  131. package/static/app/assets/useFocusTrap-hswOIkXE.js +1 -0
  132. package/static/app/assets/useMutation-OJLrYSRA.js +1 -0
  133. package/static/app/assets/workspace-BCuk3Ku9.js +1 -0
  134. package/static/app/index.html +4 -4
  135. package/static/sw.js +1 -1
  136. package/lattice_brain/ingestion/pipeline.py +0 -108
  137. package/latticeai/api/local_files.py +0 -44
  138. package/latticeai/api/tools.py +0 -126
  139. package/latticeai/api/voice_capture.py +0 -32
  140. package/latticeai/core/agent_permission.py +0 -85
  141. package/scripts/agent_eval.py +0 -34
  142. package/scripts/brain_quality_eval.py +0 -37
  143. package/scripts/check_legacy_debt.mjs +0 -91
  144. package/scripts/check_python.py +0 -100
  145. package/scripts/chunking_parity_corpus.py +0 -449
  146. package/scripts/generate_agent_parity_fixtures.py +0 -771
  147. package/scripts/generate_chunking_parity_fixtures.py +0 -259
  148. package/static/app/assets/Act-BPcVAbOL.js +0 -1
  149. package/static/app/assets/AdminConsole-Bw1ATQL0.js +0 -1
  150. package/static/app/assets/Brain-CT92Kos0.js +0 -321
  151. package/static/app/assets/BrainHome-CFBkt1K_.js +0 -2
  152. package/static/app/assets/BrainSignals-ReLWF2H8.js +0 -1
  153. package/static/app/assets/Capture-BsTokYkk.js +0 -1
  154. package/static/app/assets/Chronicle-B6f0T9id.js +0 -1
  155. package/static/app/assets/CommandPalette-CuvjTv1u.js +0 -1
  156. package/static/app/assets/Library-BGJbG9Hd.js +0 -1
  157. package/static/app/assets/LivingBrain-DGYK_Jsa.js +0 -1
  158. package/static/app/assets/ProductFlow-DXBC6brE.js +0 -1
  159. package/static/app/assets/System-CMHSO9qM.js +0 -1
  160. package/static/app/assets/arrow-left-BfmkskWx.js +0 -1
  161. package/static/app/assets/brain-CQJberbE.js +0 -1
  162. package/static/app/assets/button-Ct9f2_oT.js +0 -1
  163. package/static/app/assets/circle-check-DruOxB-4.js +0 -1
  164. package/static/app/assets/index-D9x-kSNy.css +0 -2
  165. package/static/app/assets/index-Do83hDzJ.js +0 -10
  166. package/static/app/assets/input-BLXVNmj1.js +0 -1
  167. package/static/app/assets/primitives-Cv5tbZBY.js +0 -1
  168. package/static/app/assets/search-CT9aho2j.js +0 -1
  169. package/static/app/assets/textarea-DqwLnli4.js +0 -1
  170. package/static/app/assets/useFocusTrap-ZVI98jaW.js +0 -1
  171. package/static/app/assets/useMutation-CVC4qv_D.js +0 -1
  172. package/static/app/assets/useQuery-C7BeG4HU.js +0 -1
  173. package/static/app/assets/utils-CiFtIdZq.js +0 -4
  174. package/static/app/assets/workspace-DQz9vIId.js +0 -1
@@ -1,771 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Build the committed Python↔Rust **safety kernel** parity fixtures (v11.5.0).
3
-
4
- ``rust/lattice-agent`` owns the diagram's Tools/Sandbox/Permission labels: it
5
- decides, natively, whether a tool call may run and whether a command string is
6
- safe to execute. A decision port is only worth having if something keeps proving
7
- it still decides the same thing, so this script is the Python half of that
8
- proof. It runs the **real** kernel functions
9
-
10
- * ``latticeai.core.permission_mode`` — ``normalize_mode``, ``is_circuit_breaker``,
11
- ``effective_auto_approve``, ``should_stage_proposal``, ``plan_requires_approval``,
12
- ``mode_contract``;
13
- * ``latticeai.core.agent_permission`` — ``block_reason_for_tool``,
14
- ``non_auto_plan_steps``;
15
- * ``latticeai.core.tool_governor`` — ``classify_tool_call``;
16
- * ``latticeai.tools.commands.run_command`` — the command sandbox validator, and
17
- its real execution for a small read-only set;
18
-
19
- over a decision grid built from the **real** per-tool policy table
20
- (``latticeai.core.tool_registry.TOOL_GOVERNANCE``, read through
21
- ``ToolRegistry.policy_for`` so the blocked-prefix override is real too), and
22
- writes every verdict to ``rust/fixtures/agent/golden/``.
23
-
24
- Two consumers read what it writes:
25
-
26
- * ``tests/unit/test_agent_kernel_parity_contract.py`` re-runs the Python kernel
27
- over the same grid and asserts the committed goldens still hold — so loosening
28
- a gate in Python fails loudly instead of silently invalidating the contract
29
- the Rust side is pinned to;
30
- * ``rust/lattice-agent/tests/parity.rs`` runs the Rust kernel against the same
31
- goldens, exactly.
32
-
33
- Determinism is the design constraint: no clock, no network, no machine-specific
34
- path in any golden. The command fixtures run inside a throwaway workspace whose
35
- layout is *described* in the manifest (``tree``) so the Rust suite can build the
36
- identical tree, and the absolute root is written back out as ``<AGENT_ROOT>``.
37
- The ``which(1)`` lookup is deliberately outside the goldens: whether ``rg`` is
38
- installed is a property of the machine, not of the validator.
39
-
40
- Usage::
41
-
42
- .venv/bin/python scripts/generate_agent_parity_fixtures.py
43
- """
44
-
45
- from __future__ import annotations
46
-
47
- import json
48
- import os
49
- import shlex
50
- import subprocess
51
- import sys
52
- import tempfile
53
- from contextlib import contextmanager
54
- from pathlib import Path
55
- from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple
56
-
57
- REPO_ROOT = Path(__file__).resolve().parents[1]
58
- if str(REPO_ROOT) not in sys.path:
59
- sys.path.insert(0, str(REPO_ROOT))
60
-
61
- import latticeai.tools as tools # noqa: E402
62
- from latticeai.core.agent_permission import ( # noqa: E402
63
- block_reason_for_tool,
64
- non_auto_plan_steps,
65
- )
66
- from latticeai.core.permission_mode import ( # noqa: E402
67
- COMPUTER_CONTROL_TOOLS,
68
- COMPUTER_OBSERVATION_TOOLS,
69
- HARD_BLOCK_SANDBOXES,
70
- KNOWLEDGE_READ_TOOLS,
71
- WORKSPACE_WRITE_TOOLS,
72
- effective_auto_approve,
73
- is_circuit_breaker,
74
- mode_contract,
75
- normalize_mode,
76
- plan_requires_approval,
77
- should_stage_proposal,
78
- )
79
- from latticeai.core.tool_governor import ( # noqa: E402
80
- MUTATING_TOOL_INVENTORY,
81
- PROPOSAL_CAPABLE_TOOLS,
82
- classify_tool_call,
83
- )
84
- from latticeai.core.tool_registry import TOOL_GOVERNANCE # noqa: E402
85
- from latticeai.tools import commands as command_tools # noqa: E402
86
-
87
- FIXTURE_DIR = REPO_ROOT / "rust" / "fixtures" / "agent"
88
- GOLDEN_DIR = FIXTURE_DIR / "golden"
89
-
90
- SCHEMA = "agent-kernel-parity/v1"
91
- MODES: List[str] = ["strict", "trusted", "bypass"]
92
-
93
- #: LANG/LC_ALL leak into the child environment from the parent, so they are
94
- #: pinned while the fixtures are built and recorded in the manifest.
95
- PINNED_ENV: Dict[str, str] = {"LANG": "C.UTF-8", "LC_ALL": "C.UTF-8"}
96
-
97
-
98
- # ── the decision grid ─────────────────────────────────────────────────────────
99
- #: Every name any kernel table knows about: the dispatchable registry, the
100
- #: governance table, the mutating inventory, and the four permission-mode tool
101
- #: sets (which name knowledge-graph tools the registry does not dispatch).
102
- def tool_universe() -> List[str]:
103
- return sorted(
104
- set(tools.registered_tools())
105
- | set(TOOL_GOVERNANCE)
106
- | set(MUTATING_TOOL_INVENTORY)
107
- | set(KNOWLEDGE_READ_TOOLS)
108
- | set(WORKSPACE_WRITE_TOOLS)
109
- | set(COMPUTER_OBSERVATION_TOOLS)
110
- | set(COMPUTER_CONTROL_TOOLS)
111
- )
112
-
113
-
114
- #: Argument shapes chosen for the branches they reach, not for realism:
115
- #: the two path keys ``is_circuit_breaker`` reads, both command keys, the
116
- #: blocked-prefix override that rewrites a write policy into a destructive one,
117
- #: the ``rstrip("/")`` and backslash-normalisation branches of the root guard,
118
- #: and a target that exists (mutation) versus one that does not (additive).
119
- ARG_VARIANTS: Dict[str, Dict[str, Any]] = {
120
- "none": {},
121
- "benign_path": {"path": "notes/todo.md"},
122
- "existing_path": {"path": "notes/existing.md"},
123
- "filename_doc": {"filename": "report.docx"},
124
- "blocked_prefix": {"path": "/etc/hosts"},
125
- "root_path": {"path": "/"},
126
- "home_tilde_slash": {"path": "~/"},
127
- "windows_home": {"path": "\\home"},
128
- "users_root": {"path": "/Users"},
129
- "traversal": {"path": "../../etc/passwd"},
130
- "rm_rf_root": {"command": "rm -rf /"},
131
- "rm_rf_home_upper": {"cmd": "RM -RF $HOME"},
132
- "long_path": {"path": "deep/" + ("a" * 180) + ".md"},
133
- }
134
-
135
- #: Paths ``classify_tool_call``'s injected ``path_exists`` answers True for.
136
- EXISTING_PATHS = frozenset({"notes/existing.md", "report.docx", "/etc/hosts"})
137
-
138
- #: ``effective_auto_approve``'s other axis. ``None`` is a member of the trusted
139
- #: branch's accepted set, so it is a case and not an absence.
140
- CHANGE_CLASSES: List[Optional[str]] = [
141
- None, "read", "additive", "mutation", "destructive", "exec",
142
- ]
143
-
144
- #: Mode inputs, including every alias plus the shapes that reach the
145
- #: ``str(value or "")`` fallback.
146
- NORMALIZE_INPUTS: List[Any] = [
147
- "strict", "default", "manual", "trusted", "acceptedits", "accept_edits",
148
- "workspace", "bypass", "bypasspermissions", "bypass_permissions", "yolo",
149
- "dangerously-skip-permissions", "acceptEdits", " TRUSTED ", "BYPASS",
150
- "Dangerously-Skip-Permissions", "", " ", "junk", "strictly", "read-only",
151
- None, False, True, 0, 1, [], {},
152
- ]
153
-
154
- #: Plans exercising the strict governor-tool skip, the missing-``action`` skip,
155
- #: an unknown tool falling back to the default policy, and the ``plan_flag``.
156
- PLAN_CASES: List[Dict[str, Any]] = [
157
- {"key": "empty", "steps": [], "governed": [], "plan_flag": False},
158
- {"key": "reads_only", "governed": [], "plan_flag": False,
159
- "steps": [{"action": "read_file"}, {"action": "list_dir"}]},
160
- {"key": "reads_plan_flag", "governed": [], "plan_flag": True,
161
- "steps": [{"action": "read_file"}]},
162
- {"key": "workspace_writes", "governed": [], "plan_flag": False,
163
- "steps": [{"action": "write_file"}, {"action": "edit_file"}, {"action": "todo_write"}]},
164
- {"key": "governed_writes", "governed": ["write_file", "edit_file"], "plan_flag": False,
165
- "steps": [{"action": "write_file"}, {"action": "edit_file"}, {"action": "run_command"}]},
166
- {"key": "exec_and_desktop", "governed": [], "plan_flag": False,
167
- "steps": [{"action": "run_command"}, {"action": "computer_click"},
168
- {"action": "computer_screenshot"}]},
169
- {"key": "unknown_tool", "governed": [], "plan_flag": False,
170
- "steps": [{"action": "not_a_tool"}, {"action": "knowledge_search"}]},
171
- {"key": "missing_action", "governed": [], "plan_flag": False,
172
- "steps": [{"description": "no action key"}, {"action": ""}, {"action": "local_read"}]},
173
- ]
174
-
175
-
176
- # ── the command-sandbox workspace ────────────────────────────────────────────
177
- #: The throwaway ``AGENT_ROOT`` the command fixtures run inside, described so the
178
- #: Rust suite builds the byte-identical tree. ``outside`` entries are written
179
- #: beside the root — the only way a symlink escape can be a real escape.
180
- TREE: List[Dict[str, Any]] = [
181
- {"kind": "outside", "path": "outside_secret.txt", "content": "top secret\n"},
182
- {"kind": "dir", "path": "notes"},
183
- {"kind": "file", "path": "notes/a.txt", "content": "alpha\nbeta\ngamma\n"},
184
- {"kind": "dir", "path": "a b"},
185
- {"kind": "file", "path": "a b/c.txt", "content": "spaced\n"},
186
- {"kind": "file", "path": "quoted name.txt", "content": "quoted\n"},
187
- {"kind": "dir", "path": "노트"},
188
- {"kind": "file", "path": "노트/메모.txt", "content": "한글\n"},
189
- {"kind": "dir", "path": "sub"},
190
- {"kind": "file", "path": "sub/inner.txt", "content": "inner\n"},
191
- {"kind": "file", "path": "a\\b", "content": "backslash\n"},
192
- {"kind": "symlink", "path": "inside_link", "target": "notes/a.txt"},
193
- {"kind": "symlink", "path": "escape_link", "target": "../outside_secret.txt"},
194
- #: 3,000 lines of ``%07d\n`` = 24,000 characters, so ``cat`` of it is exactly
195
- #: twice ``MAX_COMMAND_OUTPUT`` and the tail-slice is observable.
196
- {"kind": "lines", "path": "big.txt", "count": 3000},
197
- ]
198
-
199
- #: ``(key, command, cwd)``. Validation only — nothing here is executed.
200
- COMMAND_CASES: List[Tuple[str, str, Optional[str]]] = [
201
- ("empty", "", None),
202
- ("whitespace_only", " ", None),
203
- ("plain_ls", "ls", None),
204
- ("ls_flags", "ls -la", None),
205
- ("pwd", "pwd", None),
206
- ("absolute_executable", "/bin/ls", None),
207
- ("relative_executable", "./ls", None),
208
- ("trailing_slash_executable", "ls/", None),
209
- ("empty_executable", "'' notes", None),
210
- ("blocked_rm", "rm -rf /", None),
211
- ("blocked_sudo", "sudo ls", None),
212
- ("not_allowlisted", "echo hi", None),
213
- ("git_status", "git status", None),
214
- ("git_log", "git log --oneline", None),
215
- ("pipe", "cat notes/a.txt | wc -l", None),
216
- ("and_and", "cat notes/a.txt && ls", None),
217
- ("semicolon", "cat notes/a.txt; ls", None),
218
- ("redirect_out", "cat notes/a.txt > out.txt", None),
219
- ("redirect_in", "wc -l < notes/a.txt", None),
220
- ("dollar_paren", "cat $(ls)", None),
221
- ("backtick", "cat `ls`", None),
222
- ("or_or", "ls || ls", None),
223
- ("find_delete", "find . -delete", None),
224
- ("find_exec", "find . -name x -exec cat {} +", None),
225
- ("find_okdir", "find . -okdir cat", None),
226
- ("find_ok_prefix_only", "find . -name -execute", None),
227
- ("find_plain", "find . -name inner.txt", None),
228
- ("rg_pre", "rg --pre cat foo", None),
229
- ("rg_pre_glob_eq", "rg --pre-glob=*.py foo", None),
230
- ("rg_pretty_is_fine", "rg --pretty foo", None),
231
- ("traversal", "cat ../outside_secret.txt", None),
232
- ("traversal_middle", "cat sub/../../outside_secret.txt", None),
233
- ("dotdot_alone", "cat ..", None),
234
- ("absolute_arg", "cat /etc/passwd", None),
235
- ("tilde_arg", "cat ~", None),
236
- ("tilde_path_arg", "cat ~/secret", None),
237
- ("dev_null_is_exempt", "cat /dev/null", None),
238
- ("bare_dash_flag", "cat -", None),
239
- ("empty_arg", "cat ''", None),
240
- ("dot_arg", "cat .", None),
241
- ("kv_plain_value", "ls --color=auto", None),
242
- ("kv_absolute_value", "ls --color=/etc", None),
243
- ("kv_traversal_value", "ls --color=../x", None),
244
- ("kv_inside_value", "ls --color=notes/a.txt", None),
245
- ("symlink_escape", "cat escape_link", None),
246
- ("symlink_inside", "cat inside_link", None),
247
- ("quoted_space", "cat 'quoted name.txt'", None),
248
- ("double_quoted_dir", 'cat "a b/c.txt"', None),
249
- ("escaped_space", "cat a\\ b/c.txt", None),
250
- ("backslash_value", "cat a\\\\b", None),
251
- ("korean_path", "cat 노트/메모.txt", None),
252
- ("unterminated_quote", "cat 'unterminated", None),
253
- ("dangling_escape", "cat trailing\\", None),
254
- ("missing_file_is_allowed", "cat missing.txt", None),
255
- ("cwd_subdir", "ls", "sub"),
256
- ("cwd_escape", "ls", "../"),
257
- ("cwd_absolute", "ls", "/etc"),
258
- ("cwd_missing", "ls", "nope"),
259
- ("cwd_is_a_file", "ls", "notes/a.txt"),
260
- ]
261
-
262
- #: Commands that really run, in the throwaway workspace. Read-only, allow-listed,
263
- #: and chosen so the bytes are identical on macOS and Linux — which rules out
264
- #: ``wc`` (BSD pads its counts) and any listing whose order is collation
265
- #: dependent. ``stderr`` is pinned only where it is empty on both.
266
- EXECUTION_CASES: List[Tuple[str, str, Optional[str], bool]] = [
267
- ("pwd", "pwd", None, True),
268
- ("pwd_in_subdir", "pwd", "sub", True),
269
- ("cat_file", "cat notes/a.txt", None, True),
270
- ("cat_quoted", "cat 'quoted name.txt'", None, True),
271
- ("cat_double_quoted", 'cat "a b/c.txt"', None, True),
272
- ("cat_korean", "cat 노트/메모.txt", None, True),
273
- ("cat_backslash", "cat a\\\\b", None, True),
274
- ("cat_symlink_inside", "cat inside_link", None, True),
275
- ("head_two", "head -n 2 notes/a.txt", None, True),
276
- ("tail_one", "tail -n 1 notes/a.txt", None, True),
277
- ("ls_subdir", "ls notes", None, True),
278
- ("ls_cwd", "ls", "sub", True),
279
- ("find_one", "find . -name inner.txt", None, True),
280
- ("cat_truncates", "cat big.txt", None, True),
281
- ("missing_file_exit_code", "cat missing.txt", None, False),
282
- ]
283
-
284
- #: ``_resolve_path`` — the file sandbox every file tool goes through.
285
- PATH_CASES: List[Tuple[str, str]] = [
286
- ("empty", ""),
287
- ("dot", "."),
288
- ("benign", "notes/a.txt"),
289
- ("normalising", "notes/../notes/a.txt"),
290
- ("korean", "노트/메모.txt"),
291
- ("traversal", "../outside_secret.txt"),
292
- ("absolute_outside", "/etc/passwd"),
293
- ("absolute_inside", "<AGENT_ROOT>/notes/a.txt"),
294
- ("absolute_root", "<AGENT_ROOT>"),
295
- ("symlink_inside", "inside_link"),
296
- ("symlink_escape", "escape_link"),
297
- ("missing_but_inside", "notes/missing/deeper.txt"),
298
- ]
299
-
300
- #: ``shlex.split`` edges. The validator splits before it decides anything, so a
301
- #: divergence here is a divergence in every rule downstream of it.
302
- SHLEX_CASES: List[str] = [
303
- "", " ", "ls", "ls -la", "ls\t-la", "ls\n-la", "a b c",
304
- "cat 'a b.txt'", 'cat "a b"', "cat a\\ b", 'cat "a\\nb"', "cat 'a\\nb'",
305
- "cat ''", 'cat ""', "''", '""', 'a"b"c', "x'y'z", "cat \"a\\\"b\"",
306
- "cat 'a\\'", "cat back\\\\", "cat \\'quoted\\'", "cat a\\\\b",
307
- "cat 노트/메모.txt", "cat '노트/메모.txt'", " leading and trailing ",
308
- "cat \"a\"b'c'", "cat '' ''", "cat $(ls)", "cat `ls`", "cat a=b --c=d",
309
- "cat 'unterminated", 'cat "unterminated', "cat trailing\\", 'cat "trailing\\',
310
- ]
311
-
312
-
313
- # ── helpers ───────────────────────────────────────────────────────────────────
314
- @contextmanager
315
- def pinned_environment() -> Iterator[None]:
316
- """Pin the environment variables that leak into the sandboxed child."""
317
- previous = {key: os.environ.get(key) for key in PINNED_ENV}
318
- os.environ.update(PINNED_ENV)
319
- try:
320
- yield
321
- finally:
322
- for key, value in previous.items():
323
- if value is None:
324
- os.environ.pop(key, None)
325
- else:
326
- os.environ[key] = value
327
-
328
-
329
- @contextmanager
330
- def agent_root(root: Path) -> Iterator[Path]:
331
- """Point the real ``AGENT_ROOT`` at ``root`` for the duration.
332
-
333
- ``latticeai.tools`` holds the module global every path helper reads, and
334
- ``commands.py`` reaches it through ``tools.AGENT_ROOT``, so rebinding that
335
- one name redirects the whole sandbox.
336
- """
337
- original = tools.AGENT_ROOT
338
- tools.AGENT_ROOT = root.resolve()
339
- try:
340
- yield tools.AGENT_ROOT
341
- finally:
342
- tools.AGENT_ROOT = original
343
-
344
-
345
- def build_tree(root: Path) -> None:
346
- """Materialise :data:`TREE`. Same spec the Rust suite builds from."""
347
- root.mkdir(parents=True, exist_ok=True)
348
- for node in TREE:
349
- kind = node["kind"]
350
- if kind == "outside":
351
- (root.parent / node["path"]).write_text(node["content"], encoding="utf-8")
352
- elif kind == "dir":
353
- (root / node["path"]).mkdir(parents=True, exist_ok=True)
354
- elif kind == "file":
355
- (root / node["path"]).write_text(node["content"], encoding="utf-8")
356
- elif kind == "lines":
357
- body = "".join(f"{index:07d}\n" for index in range(node["count"]))
358
- (root / node["path"]).write_text(body, encoding="utf-8")
359
- elif kind == "symlink":
360
- link = root / node["path"]
361
- if link.is_symlink():
362
- link.unlink()
363
- link.symlink_to(node["target"])
364
- else: # pragma: no cover - the spec is closed
365
- raise ValueError(f"unknown tree node kind: {kind}")
366
-
367
-
368
- class _Spawned(Exception):
369
- """Raised in place of ``subprocess.run`` so validation stops at the spawn."""
370
-
371
- def __init__(self, argv: List[str], cwd: Any, env: Dict[str, str]) -> None:
372
- super().__init__("spawn")
373
- self.argv = list(argv)
374
- self.cwd = cwd
375
- self.env = dict(env)
376
-
377
-
378
- @contextmanager
379
- def validation_only() -> Iterator[List[str]]:
380
- """Stop ``run_command`` at the spawn, and make ``which`` machine-independent.
381
-
382
- Whether ``rg`` is installed is a property of the machine; whether ``rg --pre``
383
- is refused is a property of the validator. Recording the second without the
384
- first is the whole point of this seam. Every ``which`` call is captured so
385
- the caller can assert the fixed PATH was the one searched.
386
- """
387
- searched: List[str] = []
388
- real_subprocess = command_tools.subprocess
389
- real_shutil = getattr(command_tools, "shutil", None)
390
-
391
- class _SubprocessShim:
392
- TimeoutExpired = subprocess.TimeoutExpired
393
-
394
- @staticmethod
395
- def run(argv: List[str], **kwargs: Any) -> None:
396
- raise _Spawned(argv, kwargs.get("cwd"), kwargs.get("env") or {})
397
-
398
- class _ShutilShim:
399
- @staticmethod
400
- def which(cmd: str, path: Optional[str] = None) -> str:
401
- searched.append(str(path))
402
- return f"<which>/{cmd}"
403
-
404
- command_tools.subprocess = _SubprocessShim # type: ignore[assignment]
405
- command_tools.shutil = _ShutilShim # type: ignore[assignment]
406
- try:
407
- yield searched
408
- finally:
409
- command_tools.subprocess = real_subprocess
410
- if real_shutil is None:
411
- delattr(command_tools, "shutil")
412
- else:
413
- command_tools.shutil = real_shutil
414
-
415
-
416
- def _error(exc: BaseException) -> Dict[str, str]:
417
- kind = "tool" if isinstance(exc, tools.ToolError) else "shlex"
418
- return {"kind": kind, "message": str(exc)}
419
-
420
-
421
- def _relative_to(root: Path, path: Path) -> str:
422
- return "." if path == root else str(path.relative_to(root))
423
-
424
-
425
- # ── grid builders ─────────────────────────────────────────────────────────────
426
- def policy_for(tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
427
- """The **real** per-tool policy, override included.
428
-
429
- ``ToolRegistry.policy_for`` is the single source of truth: the table for the
430
- ordinary case, and a synthesised destructive policy when a write targets a
431
- blocked system prefix. Both shapes belong in the goldens.
432
- """
433
- return dict(tools.DEFAULT_TOOL_REGISTRY.policy_for(tool_name, args))
434
-
435
-
436
- def policy_table() -> Dict[str, Any]:
437
- """The registry table, the default, and every args-dependent override."""
438
- default = dict(tools.DEFAULT_TOOL_REGISTRY.default_policy)
439
- table = {name: dict(policy) for name, policy in TOOL_GOVERNANCE.items()}
440
- overrides: Dict[str, Any] = {}
441
- for name in tool_universe():
442
- base = table.get(name, default)
443
- for variant, args in ARG_VARIANTS.items():
444
- policy = policy_for(name, args)
445
- if policy != base:
446
- overrides[f"{name}|{variant}"] = policy
447
- return {"schema": SCHEMA, "default": default, "tools": table, "overrides": overrides}
448
-
449
-
450
- def policy_key(name: str, variant: str, default: Dict[str, Any]) -> str:
451
- """How a case names its policy: table entry, override, or the default."""
452
- base = TOOL_GOVERNANCE.get(name)
453
- policy = policy_for(name, ARG_VARIANTS[variant])
454
- if base is not None and policy == dict(base):
455
- return name
456
- if base is None and policy == default:
457
- return "@default"
458
- return f"{name}|{variant}"
459
-
460
-
461
- def call_rows() -> List[Dict[str, Any]]:
462
- """The mode-*invariant* half of the grid: policy, breaker, classification.
463
-
464
- Circuit breakers and change classification do not read the mode — that is a
465
- documented property of the kernel, and keeping them in their own file is how
466
- the fixture states it rather than repeating it three times.
467
- """
468
- default = dict(tools.DEFAULT_TOOL_REGISTRY.default_policy)
469
- rows: List[Dict[str, Any]] = []
470
- for name in tool_universe():
471
- for variant, args in ARG_VARIANTS.items():
472
- policy = policy_for(name, args)
473
- rows.append({
474
- "tool": name,
475
- "variant": variant,
476
- "policy": policy_key(name, variant, default),
477
- "circuit_breaker": is_circuit_breaker(name, policy, args),
478
- "classification": classify_tool_call(
479
- name, args, policy=policy, path_exists=lambda p: p in EXISTING_PATHS,
480
- ),
481
- })
482
- return rows
483
-
484
-
485
- def decision_rows(mode: str) -> List[Dict[str, Any]]:
486
- """One mode's half: auto-approve, block reason, proposal staging."""
487
- rows: List[Dict[str, Any]] = []
488
- for name in tool_universe():
489
- for variant, args in ARG_VARIANTS.items():
490
- policy = policy_for(name, args)
491
- classification = classify_tool_call(
492
- name, args, policy=policy, path_exists=lambda p: p in EXISTING_PATHS,
493
- )
494
- rows.append({
495
- "tool": name,
496
- "variant": variant,
497
- "auto_approve": effective_auto_approve(mode, name, policy, args=args),
498
- "block_reason": block_reason_for_tool(mode, name, policy, args),
499
- "stage_proposal": should_stage_proposal(
500
- mode, proposal_required=classification["proposal_required"],
501
- ),
502
- })
503
- return rows
504
-
505
-
506
- def change_class_rows(mode: str) -> List[Dict[str, Any]]:
507
- """``effective_auto_approve``'s second axis, over the workspace writers."""
508
- rows: List[Dict[str, Any]] = []
509
- for name in sorted(WORKSPACE_WRITE_TOOLS | {"local_write", "read_file", "run_command"}):
510
- policy = policy_for(name, {})
511
- for change_class in CHANGE_CLASSES:
512
- rows.append({
513
- "tool": name,
514
- "change_class": change_class,
515
- "auto_approve": effective_auto_approve(
516
- mode, name, policy, change_class=change_class,
517
- ),
518
- })
519
- return rows
520
-
521
-
522
- def approval_rows(mode: str) -> List[Dict[str, Any]]:
523
- """Plan-level gates: which steps stay non-auto, and whether the plan pauses."""
524
- governance = {name: dict(policy) for name, policy in TOOL_GOVERNANCE.items()}
525
- rows: List[Dict[str, Any]] = []
526
- for case in PLAN_CASES:
527
- non_auto = non_auto_plan_steps(
528
- mode, case["steps"], governance, governed_tools=case["governed"],
529
- )
530
- rows.append({
531
- "key": case["key"],
532
- "non_auto_steps": non_auto,
533
- "requires_approval": plan_requires_approval(
534
- mode, non_auto_steps=non_auto, plan_flag=case["plan_flag"],
535
- ),
536
- })
537
- return rows
538
-
539
-
540
- def normalize_rows() -> List[Dict[str, Any]]:
541
- return [
542
- {"input": value, "mode": normalize_mode(value).value}
543
- for value in NORMALIZE_INPUTS
544
- ]
545
-
546
-
547
- def contract_payload() -> Dict[str, Any]:
548
- return {
549
- "schema": SCHEMA,
550
- "default_mode": normalize_mode(None).value,
551
- "contracts": {mode: mode_contract(mode) for mode in MODES},
552
- }
553
-
554
-
555
- def shlex_rows() -> List[Dict[str, Any]]:
556
- rows: List[Dict[str, Any]] = []
557
- for command in SHLEX_CASES:
558
- try:
559
- rows.append({"input": command, "tokens": shlex.split(command)})
560
- except ValueError as exc:
561
- rows.append({"input": command, "error": str(exc)})
562
- return rows
563
-
564
-
565
- def command_rows(root: Path) -> Tuple[List[Dict[str, Any]], Dict[str, Any], List[str]]:
566
- """Validation verdicts, plus the environment the validator would spawn into."""
567
- rows: List[Dict[str, Any]] = []
568
- spawn_env: Dict[str, Any] = {}
569
- with agent_root(root) as resolved, validation_only() as searched:
570
- for key, command, cwd in COMMAND_CASES:
571
- try:
572
- command_tools.run_command(command, cwd)
573
- except _Spawned as spawned:
574
- env = {k: v.replace(str(resolved), "<AGENT_ROOT>") for k, v in spawned.env.items()}
575
- if spawn_env and env != spawn_env: # pragma: no cover - defensive
576
- raise AssertionError("the sandbox environment is not constant")
577
- spawn_env = env
578
- rows.append({
579
- "key": key, "command": command, "cwd": cwd, "outcome": "spawn",
580
- "executable": Path(spawned.argv[0]).name,
581
- "args": spawned.argv[1:],
582
- "workdir": _relative_to(resolved, Path(str(spawned.cwd))),
583
- })
584
- except (tools.ToolError, ValueError) as exc:
585
- rows.append({
586
- "key": key, "command": command, "cwd": cwd,
587
- "outcome": "error", "error": _error(exc),
588
- })
589
- else: # pragma: no cover - the shim always raises
590
- raise AssertionError(f"{command!r} reached the real subprocess")
591
- return rows, spawn_env, searched
592
-
593
-
594
- def execution_rows(root: Path) -> List[Dict[str, Any]]:
595
- """The real ``run_command``, really executed, with the answers pinned."""
596
- rows: List[Dict[str, Any]] = []
597
- with agent_root(root) as resolved:
598
- for key, command, cwd, pin_stderr in EXECUTION_CASES:
599
- result = command_tools.run_command(command, cwd)
600
- row = {
601
- "key": key,
602
- "command": command,
603
- "cwd": cwd,
604
- "result_cwd": result["cwd"],
605
- "returncode": result["returncode"],
606
- "stdout": result["stdout"].replace(str(resolved), "<AGENT_ROOT>"),
607
- }
608
- if pin_stderr:
609
- row["stderr"] = result["stderr"]
610
- rows.append(row)
611
- return rows
612
-
613
-
614
- def path_rows(root: Path) -> List[Dict[str, Any]]:
615
- rows: List[Dict[str, Any]] = []
616
- with agent_root(root) as resolved:
617
- for key, raw in PATH_CASES:
618
- candidate = raw.replace("<AGENT_ROOT>", str(resolved))
619
- try:
620
- resolved_path = tools.resolve_workspace_path(candidate)
621
- except tools.ToolError as exc:
622
- rows.append({"key": key, "input": raw, "outcome": "error",
623
- "error": _error(exc)})
624
- else:
625
- rows.append({"key": key, "input": raw, "outcome": "ok",
626
- "relative": _relative_to(resolved, resolved_path)})
627
- return rows
628
-
629
-
630
- def constants() -> Dict[str, Any]:
631
- """Every table the Rust kernel duplicates, so drift is a failing assertion."""
632
- return {
633
- "max_file_bytes": tools.MAX_FILE_BYTES,
634
- "max_command_seconds": tools.MAX_COMMAND_SECONDS,
635
- "max_command_output": tools.MAX_COMMAND_OUTPUT,
636
- "safe_executable_path": command_tools._SAFE_EXECUTABLE_PATH,
637
- "allowed_commands": sorted(tools.ALLOWED_COMMANDS),
638
- "blocked_commands": sorted(tools.BLOCKED_COMMANDS),
639
- "allowed_git_subcommands": sorted(tools.ALLOWED_GIT_SUBCOMMANDS),
640
- "blocked_find_flags": sorted(command_tools._BLOCKED_FIND_FLAGS),
641
- "blocked_rg_flags": sorted(command_tools._BLOCKED_RG_FLAGS),
642
- "shell_operators": ["|", "&&", "||", ";", ">", "<", "$(", "`"],
643
- "hard_block_sandboxes": sorted(HARD_BLOCK_SANDBOXES),
644
- "knowledge_read_tools": sorted(KNOWLEDGE_READ_TOOLS),
645
- "workspace_write_tools": sorted(WORKSPACE_WRITE_TOOLS),
646
- "computer_observation_tools": sorted(COMPUTER_OBSERVATION_TOOLS),
647
- "computer_control_tools": sorted(COMPUTER_CONTROL_TOOLS),
648
- "mutating_tool_inventory": dict(sorted(MUTATING_TOOL_INVENTORY.items())),
649
- "proposal_capable_tools": sorted(PROPOSAL_CAPABLE_TOOLS),
650
- }
651
-
652
-
653
- def manifest(searched_paths: List[str]) -> Dict[str, Any]:
654
- return {
655
- "schema": SCHEMA,
656
- "modes": MODES,
657
- "tools": tool_universe(),
658
- "arg_variants": ARG_VARIANTS,
659
- "existing_paths": sorted(EXISTING_PATHS),
660
- "change_classes": CHANGE_CLASSES,
661
- "plans": PLAN_CASES,
662
- "tree": TREE,
663
- "pinned_env": PINNED_ENV,
664
- "constants": constants(),
665
- # Evidence that the allowlisted binary is looked up on the fixed PATH and
666
- # nowhere else: every which() the validator made, deduplicated.
667
- "which_paths": sorted(set(searched_paths)),
668
- }
669
-
670
-
671
- # ── writing ───────────────────────────────────────────────────────────────────
672
- def _dump(path: Path, payload: Any) -> None:
673
- path.write_text(
674
- json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
675
- encoding="utf-8",
676
- )
677
-
678
-
679
- def _dump_grid(path: Path, header: Dict[str, Any], groups: Dict[str, List[Any]]) -> None:
680
- """Header pretty, cases one per line — a thousand-row diff stays readable."""
681
- parts = [
682
- f" {json.dumps(key, ensure_ascii=False)}: "
683
- + json.dumps(header[key], ensure_ascii=False, sort_keys=True)
684
- for key in sorted(header)
685
- ]
686
- for name in sorted(groups):
687
- rows = ",\n ".join(
688
- json.dumps(row, ensure_ascii=False, sort_keys=True) for row in groups[name]
689
- )
690
- body = f"[\n {rows}\n ]" if rows else "[]"
691
- parts.append(f" {json.dumps(name)}: {body}")
692
- path.write_text("{\n" + ",\n".join(parts) + "\n}\n", encoding="utf-8")
693
-
694
-
695
- def build(root: Path) -> Dict[str, Callable[[], None]]:
696
- """Every golden, keyed by filename, as a thunk that writes it.
697
-
698
- """
699
- # run_command left the worker (WP-P1). commands.json / execution.json
700
- # stay FROZEN at fc65e60; only rewrite them when the last generating
701
- # commands module is still importable (recovery / historical tree).
702
- if hasattr(command_tools, "run_command"):
703
- commands, spawn_env, searched = command_rows(root)
704
- else:
705
- commands, spawn_env, searched = None, None, [command_tools._SAFE_EXECUTABLE_PATH]
706
- return {
707
- "manifest.json": lambda: _dump(GOLDEN_DIR / "manifest.json", manifest(searched)),
708
- "policies.json": lambda: _dump(GOLDEN_DIR / "policies.json", policy_table()),
709
- "contract.json": lambda: _dump(GOLDEN_DIR / "contract.json", contract_payload()),
710
- "calls.json": lambda: _dump_grid(
711
- GOLDEN_DIR / "calls.json", {"schema": SCHEMA}, {"cases": call_rows()},
712
- ),
713
- "normalize.json": lambda: _dump_grid(
714
- GOLDEN_DIR / "normalize.json", {"schema": SCHEMA}, {"cases": normalize_rows()},
715
- ),
716
- "shlex.json": lambda: _dump_grid(
717
- GOLDEN_DIR / "shlex.json", {"schema": SCHEMA}, {"cases": shlex_rows()},
718
- ),
719
- "commands.json": (
720
- (lambda: _dump_grid(
721
- GOLDEN_DIR / "commands.json",
722
- {"schema": SCHEMA, "spawn_env": spawn_env},
723
- {"cases": commands},
724
- ))
725
- if commands is not None
726
- else (lambda: None)
727
- ),
728
- "execution.json": (
729
- (lambda: _dump_grid(
730
- GOLDEN_DIR / "execution.json", {"schema": SCHEMA},
731
- {"cases": execution_rows(root)},
732
- ))
733
- if commands is not None
734
- else (lambda: None)
735
- ),
736
- "paths.json": lambda: _dump_grid(
737
- GOLDEN_DIR / "paths.json", {"schema": SCHEMA}, {"cases": path_rows(root)},
738
- ),
739
- **{
740
- f"decisions__{mode}.json": (lambda mode=mode: _dump_grid(
741
- GOLDEN_DIR / f"decisions__{mode}.json",
742
- {"schema": SCHEMA, "mode": mode},
743
- {
744
- "cases": decision_rows(mode),
745
- "change_class_cases": change_class_rows(mode),
746
- "plan_cases": approval_rows(mode),
747
- },
748
- ))
749
- for mode in MODES
750
- },
751
- }
752
-
753
-
754
- def main() -> int:
755
- # Never rmtree: commands.json / execution.json are FROZEN at fc65e60
756
- # once run_command leaves the worker. Write other goldens in place.
757
- GOLDEN_DIR.mkdir(parents=True, exist_ok=True)
758
- with pinned_environment(), tempfile.TemporaryDirectory() as tmp:
759
- root = Path(tmp) / "agent_workspace"
760
- build_tree(root)
761
- writers = build(root)
762
- for name in sorted(writers):
763
- writers[name]()
764
- total = sum(path.stat().st_size for path in GOLDEN_DIR.glob("*.json"))
765
- print(f"golden: {len(list(GOLDEN_DIR.glob('*.json')))} files, {total / 1024:.1f} KiB")
766
- print(f"grid: {len(tool_universe())} tools × {len(ARG_VARIANTS)} variants × {len(MODES)} modes")
767
- return 0
768
-
769
-
770
- if __name__ == "__main__":
771
- raise SystemExit(main())