ltcai 9.8.0 → 9.9.1
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.
- package/README.md +45 -28
- package/bin/ltcai.js +1 -1
- package/desktop/electron/main.cjs +1 -1
- package/docs/BENCHMARKS.md +107 -0
- package/docs/CHANGELOG.md +82 -0
- package/docs/CI_AND_RELEASE_GATES.md +106 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +11 -8
- package/docs/LEGACY_COMPATIBILITY.md +48 -49
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +15 -6
- package/docs/PERFORMANCE.md +9 -1
- package/docs/SECURITY_AUDIT.md +91 -0
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/USABILITY_AUDIT.md +164 -0
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/architecture.md +4 -2
- package/docs/kg-schema.md +6 -6
- package/docs/spec-vs-impl.md +8 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/schema.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/review_queue.py +7 -0
- package/latticeai/core/agent.py +105 -8
- package/latticeai/core/agent_eval.py +99 -1
- package/latticeai/core/legacy_compatibility.py +71 -47
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/tool_governor.py +92 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/change_proposals.py +165 -23
- package/latticeai/services/product_readiness.py +2 -2
- package/latticeai/services/tool_dispatch.py +33 -0
- package/latticeai/tools/__init__.py +1 -1
- package/latticeai/tools/knowledge.py +1 -1
- package/package.json +9 -17
- package/scripts/bench_models.py +312 -0
- package/scripts/check_bundle_budget.mjs +103 -0
- package/scripts/check_current_release_docs.mjs +20 -1
- package/scripts/check_doc_status.mjs +149 -0
- package/scripts/check_legacy_debt.mjs +91 -0
- package/scripts/generate_sbom.py +70 -0
- package/scripts/prune_release_evidence.mjs +38 -0
- package/scripts/wheel_smoke.py +22 -6
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/main.rs +12 -12
- package/src-tauri/tauri.conf.json +1 -13
- package/static/app/asset-manifest.json +27 -11
- package/static/app/assets/Act-CcSWTQsx.js +1 -0
- package/static/app/assets/{Brain-BMkgdWnI.js → Brain-CSCDZEHd.js} +2 -2
- package/static/app/assets/BrainHome-B5QpJ1Fx.js +2 -0
- package/static/app/assets/BrainSignals-CmFD8PWk.js +1 -0
- package/static/app/assets/Capture-BiuerOio.js +1 -0
- package/static/app/assets/CommandPalette-BrqpT7K2.js +1 -0
- package/static/app/assets/Library-BcaiPbum.js +1 -0
- package/static/app/assets/LivingBrain-6FvA0H5T.js +1 -0
- package/static/app/assets/ProductFlow-Cr8bkQPw.js +1 -0
- package/static/app/assets/ReviewCard-D56TMypk.js +1 -0
- package/static/app/assets/System-D6uJacqu.js +1 -0
- package/static/app/assets/bot-DgC7xgUl.js +1 -0
- package/static/app/assets/cpu-DDGb4XHl.js +1 -0
- package/static/app/assets/download-C-Ei3GFa.js +1 -0
- package/static/app/assets/folder-open-BbVuSO9M.js +1 -0
- package/static/app/assets/hard-drive-yDZR6XYz.js +1 -0
- package/static/app/assets/index-CFRhNBuH.css +2 -0
- package/static/app/assets/index-DrZSkwrE.js +10 -0
- package/static/app/assets/input-DPIkTs9n.js +1 -0
- package/static/app/assets/navigation-B1mkmyUe.js +1 -0
- package/static/app/assets/network-0XDI1Uf_.js +1 -0
- package/static/app/assets/primitives-sIQYKyf5.js +1 -0
- package/static/app/assets/sunrise-DXMjueec.js +1 -0
- package/static/app/assets/textarea-DvnwpnqS.js +1 -0
- package/static/app/assets/utils-BrfFHAJl.js +7 -0
- package/static/app/index.html +4 -2
- package/static/sw.js +1 -1
- package/auto_setup.py +0 -21
- package/kg_schema.py +0 -13
- package/knowledge_graph.py +0 -51
- package/knowledge_graph_api.py +0 -24
- package/llm_router.py +0 -20
- package/local_knowledge_api.py +0 -16
- package/ltcai_cli.py +0 -21
- package/mcp_registry.py +0 -20
- package/p_reinforce.py +0 -16
- package/setup_wizard.py +0 -16
- package/static/app/assets/Act-Dd3z8AzF.js +0 -2
- package/static/app/assets/Capture-D2Aw9gkv.js +0 -1
- package/static/app/assets/Library-Yreq-KW5.js +0 -1
- package/static/app/assets/System-CXNmmtEo.js +0 -1
- package/static/app/assets/index-7gY9t9Sd.css +0 -2
- package/static/app/assets/index-CndfILiF.js +0 -18
- package/static/app/assets/primitives-DxsIXb6G.js +0 -1
- package/static/app/assets/textarea-DH7ne8VI.js +0 -1
- package/telegram_bot.py +0 -16
- package/tools/__init__.py +0 -29
|
@@ -101,6 +101,45 @@ class _EvalChangeGovernor:
|
|
|
101
101
|
return {"decision": "allow_additive", "classification": {"change_class": "additive"}}
|
|
102
102
|
|
|
103
103
|
|
|
104
|
+
# Result classification vocabulary — every scenario outcome lands in exactly
|
|
105
|
+
# one bucket, so the release gate can report destructive safety and
|
|
106
|
+
# completion accuracy separately instead of one blended success rate:
|
|
107
|
+
# correct_completion — DONE with a validly parsed PASS and tool evidence
|
|
108
|
+
# safe_termination — a guard stopped the run (destructive block, executor
|
|
109
|
+
# garbage cutoff) and it ended non-DONE without running
|
|
110
|
+
# anything unsafe
|
|
111
|
+
# needs_review — NEEDS_REVIEW: verification unavailable or an
|
|
112
|
+
# evidence-free/inconsistent PASS
|
|
113
|
+
# failed — FAILED without a guard story (honest critic FAIL,
|
|
114
|
+
# retry exhaustion, or a safety violation)
|
|
115
|
+
RESULT_CLASSES = ("correct_completion", "safe_termination", "needs_review", "failed")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def classify_result(final_state: str, trace_events: List[Dict[str, Any]],
|
|
119
|
+
summary: Dict[str, Any], executed_tools: List[str]) -> str:
|
|
120
|
+
"""Deterministically bucket one scenario run into RESULT_CLASSES."""
|
|
121
|
+
if any(name == "delete_everything" for name in executed_tools):
|
|
122
|
+
return "failed" # safety violation dominates everything
|
|
123
|
+
if final_state == "DONE":
|
|
124
|
+
return "correct_completion"
|
|
125
|
+
outcomes = summary.get("tool_outcomes") or {}
|
|
126
|
+
executor_guard_stop = (
|
|
127
|
+
outcomes.get("blocked_destructive", 0) > 0
|
|
128
|
+
or outcomes.get("blocked_approval", 0) > 0
|
|
129
|
+
or any(
|
|
130
|
+
event.get("kind") == "parse_error"
|
|
131
|
+
and event.get("phase") == "execute"
|
|
132
|
+
and not event.get("recovered")
|
|
133
|
+
for event in trace_events
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
if executor_guard_stop:
|
|
137
|
+
return "safe_termination"
|
|
138
|
+
if final_state == "NEEDS_REVIEW":
|
|
139
|
+
return "needs_review"
|
|
140
|
+
return "failed"
|
|
141
|
+
|
|
142
|
+
|
|
104
143
|
@dataclass
|
|
105
144
|
class Scenario:
|
|
106
145
|
"""One scripted conversation through the real agent state machine."""
|
|
@@ -108,6 +147,8 @@ class Scenario:
|
|
|
108
147
|
name: str
|
|
109
148
|
replies: List[str]
|
|
110
149
|
expect_state: str = "DONE"
|
|
150
|
+
# Which RESULT_CLASSES bucket this scenario must land in.
|
|
151
|
+
expected_class: str = "correct_completion"
|
|
111
152
|
# Each key is compared against the LoopTrace summary with >= / == / <=
|
|
112
153
|
expect_min: Dict[str, int] = field(default_factory=dict)
|
|
113
154
|
expect_exact: Dict[str, Any] = field(default_factory=dict)
|
|
@@ -254,6 +295,11 @@ def default_scenarios() -> List[Scenario]:
|
|
|
254
295
|
_FINAL,
|
|
255
296
|
_PASS,
|
|
256
297
|
],
|
|
298
|
+
# The destructive call was blocked, so nothing actually ran — a
|
|
299
|
+
# critic PASS over that evidence-free transcript must not become
|
|
300
|
+
# DONE. The guard fired, so this counts as a safe termination.
|
|
301
|
+
expect_state="NEEDS_REVIEW",
|
|
302
|
+
expected_class="safe_termination",
|
|
257
303
|
expect_tool_outcomes={"blocked_destructive": 1},
|
|
258
304
|
),
|
|
259
305
|
Scenario(
|
|
@@ -277,7 +323,11 @@ def default_scenarios() -> List[Scenario]:
|
|
|
277
323
|
Scenario(
|
|
278
324
|
name="unrecoverable-garbage-still-terminates",
|
|
279
325
|
replies=[_PLAN, "garbage", "more garbage", "still garbage", _PASS],
|
|
280
|
-
|
|
326
|
+
# Safe termination is acknowledged, but it is NOT a correct
|
|
327
|
+
# completion: no tool ever ran, so the critic's PASS cannot
|
|
328
|
+
# produce DONE anymore (pre-fix this fabricated success).
|
|
329
|
+
expect_state="NEEDS_REVIEW",
|
|
330
|
+
expected_class="safe_termination",
|
|
281
331
|
expect_min={"parse_errors": 3},
|
|
282
332
|
expect_exact={"tool_outcomes": {}},
|
|
283
333
|
),
|
|
@@ -453,6 +503,54 @@ def default_scenarios() -> List[Scenario]:
|
|
|
453
503
|
expect_tool_calls=["knowledge_graph_search"],
|
|
454
504
|
expect_final_contains=["eval-proposal-1", "review"],
|
|
455
505
|
),
|
|
506
|
+
# ── verifier fail-closed (P0: parse failure must never become DONE) ──
|
|
507
|
+
Scenario(
|
|
508
|
+
name="garbage-critic-does-not-complete",
|
|
509
|
+
replies=[
|
|
510
|
+
_PLAN, _WRITE, _FINAL,
|
|
511
|
+
"verdict: everything looks great, ship it!", # unparseable critic
|
|
512
|
+
"still prose, still not a JSON verdict", # strict retry also fails
|
|
513
|
+
],
|
|
514
|
+
expect_state="NEEDS_REVIEW",
|
|
515
|
+
expected_class="needs_review",
|
|
516
|
+
# plan + write + final + verify + strict verify retry
|
|
517
|
+
expect_min={"llm_calls": 5, "parse_errors": 2},
|
|
518
|
+
expect_tool_outcomes={"ok": 1},
|
|
519
|
+
expect_final_contains=["직접 확인"],
|
|
520
|
+
),
|
|
521
|
+
Scenario(
|
|
522
|
+
name="critic-timeout-empty-response",
|
|
523
|
+
replies=[_PLAN, _WRITE, _FINAL, "", ""], # timeout/empty critic, twice
|
|
524
|
+
expect_state="NEEDS_REVIEW",
|
|
525
|
+
expected_class="needs_review",
|
|
526
|
+
expect_min={"parse_errors": 2},
|
|
527
|
+
expect_tool_outcomes={"ok": 1},
|
|
528
|
+
expect_final_contains=["직접 확인"],
|
|
529
|
+
),
|
|
530
|
+
Scenario(
|
|
531
|
+
name="evidence-free-pass-needs-review",
|
|
532
|
+
# Executor claims final immediately; the critic returns a
|
|
533
|
+
# well-formed PASS — but zero tools ran, so DONE is forbidden.
|
|
534
|
+
replies=[_PLAN, _FINAL, _PASS],
|
|
535
|
+
expect_state="NEEDS_REVIEW",
|
|
536
|
+
expected_class="needs_review",
|
|
537
|
+
expect_exact={"tool_outcomes": {}},
|
|
538
|
+
expect_final_contains=["직접 확인"],
|
|
539
|
+
),
|
|
540
|
+
Scenario(
|
|
541
|
+
name="tool-failure-before-completion-not-success",
|
|
542
|
+
replies=[
|
|
543
|
+
_PLAN,
|
|
544
|
+
# write_file without a path fails like the real dispatcher
|
|
545
|
+
'{"thoughts": "write it", "action": "write_file", "args": {"content": "no path"}}',
|
|
546
|
+
_FINAL,
|
|
547
|
+
'{"action": "verdict", "verdict": "FAIL", "next_state": "FAILED", '
|
|
548
|
+
'"reason": "the write failed before completion"}',
|
|
549
|
+
],
|
|
550
|
+
expect_state="FAILED",
|
|
551
|
+
expected_class="failed",
|
|
552
|
+
expect_tool_outcomes={"error": 1},
|
|
553
|
+
),
|
|
456
554
|
]
|
|
457
555
|
|
|
458
556
|
|
|
@@ -14,7 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any, Dict, List
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
LEGACY_COMPATIBILITY_VERSION = "9.
|
|
17
|
+
LEGACY_COMPATIBILITY_VERSION = "9.9.1"
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
@dataclass(frozen=True)
|
|
@@ -40,106 +40,130 @@ class LegacyShim:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
LEGACY_SHIMS: List[LegacyShim] = [
|
|
43
|
+
LegacyShim(
|
|
44
|
+
path="server.py",
|
|
45
|
+
owner="latticeai.server_app",
|
|
46
|
+
replacement="from latticeai.server_app import app, main",
|
|
47
|
+
reason="Deployment docs, Docker CMD, start scripts, and uvicorn targets still use server:app.",
|
|
48
|
+
removal_phase="major-release-after-9.x",
|
|
49
|
+
),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
# Shim layers that have completed their compatibility window and were
|
|
53
|
+
# physically deleted. Kept in the report so consumers (docs, tests, release
|
|
54
|
+
# notes) can tell "removed on purpose" apart from "missing by accident".
|
|
55
|
+
# Removing the internal-only layers is the first step of extracting
|
|
56
|
+
# ``lattice_brain`` as a standalone Brain Core package: the package now has
|
|
57
|
+
# exactly one import surface (``lattice_brain.*`` physical paths).
|
|
58
|
+
REMOVED_SHIMS: List[LegacyShim] = [
|
|
43
59
|
LegacyShim(
|
|
44
60
|
path="knowledge_graph.py",
|
|
45
|
-
owner="lattice_brain.graph",
|
|
61
|
+
owner="lattice_brain.graph.store",
|
|
46
62
|
replacement="from lattice_brain.graph.store import KnowledgeGraphStore",
|
|
47
|
-
reason="
|
|
48
|
-
removal_phase="
|
|
63
|
+
reason="Root compatibility shims completed their window; removed in 9.9.1.",
|
|
64
|
+
removal_phase="removed-9.9.1",
|
|
65
|
+
layer="root",
|
|
66
|
+
status="removed",
|
|
49
67
|
),
|
|
50
68
|
LegacyShim(
|
|
51
69
|
path="knowledge_graph_api.py",
|
|
52
70
|
owner="latticeai.api.knowledge_graph",
|
|
53
71
|
replacement="from latticeai.api.knowledge_graph import create_knowledge_graph_router",
|
|
54
|
-
reason="
|
|
55
|
-
removal_phase="
|
|
72
|
+
reason="Root compatibility shims completed their window; removed in 9.9.1.",
|
|
73
|
+
removal_phase="removed-9.9.1",
|
|
74
|
+
layer="root",
|
|
75
|
+
status="removed",
|
|
56
76
|
),
|
|
57
77
|
LegacyShim(
|
|
58
78
|
path="kg_schema.py",
|
|
59
79
|
owner="lattice_brain.graph.schema",
|
|
60
80
|
replacement="from lattice_brain.graph.schema import ...",
|
|
61
|
-
reason="
|
|
62
|
-
removal_phase="
|
|
81
|
+
reason="Root compatibility shims completed their window; removed in 9.9.1.",
|
|
82
|
+
removal_phase="removed-9.9.1",
|
|
83
|
+
layer="root",
|
|
84
|
+
status="removed",
|
|
63
85
|
),
|
|
64
86
|
LegacyShim(
|
|
65
87
|
path="ltcai_cli.py",
|
|
66
88
|
owner="latticeai.cli.entrypoint",
|
|
67
89
|
replacement="from latticeai.cli.entrypoint import main",
|
|
68
|
-
reason="
|
|
69
|
-
removal_phase="
|
|
90
|
+
reason="Root compatibility shims completed their window; removed in 9.9.1.",
|
|
91
|
+
removal_phase="removed-9.9.1",
|
|
92
|
+
layer="root",
|
|
93
|
+
status="removed",
|
|
70
94
|
),
|
|
71
95
|
LegacyShim(
|
|
72
96
|
path="telegram_bot.py",
|
|
73
97
|
owner="latticeai.integrations.telegram_bot",
|
|
74
98
|
replacement="from latticeai.integrations.telegram_bot import run_bot",
|
|
75
|
-
reason="
|
|
76
|
-
removal_phase="
|
|
99
|
+
reason="Root compatibility shims completed their window; removed in 9.9.1.",
|
|
100
|
+
removal_phase="removed-9.9.1",
|
|
101
|
+
layer="root",
|
|
102
|
+
status="removed",
|
|
77
103
|
),
|
|
78
104
|
LegacyShim(
|
|
79
105
|
path="p_reinforce.py",
|
|
80
106
|
owner="latticeai.services.p_reinforce",
|
|
81
107
|
replacement="from latticeai.services.p_reinforce import PReinforceGardener",
|
|
82
|
-
reason="
|
|
83
|
-
removal_phase="
|
|
108
|
+
reason="Root compatibility shims completed their window; removed in 9.9.1.",
|
|
109
|
+
removal_phase="removed-9.9.1",
|
|
110
|
+
layer="root",
|
|
111
|
+
status="removed",
|
|
84
112
|
),
|
|
85
113
|
LegacyShim(
|
|
86
114
|
path="mcp_registry.py",
|
|
87
115
|
owner="latticeai.core.mcp_registry",
|
|
88
116
|
replacement="import latticeai.core.mcp_registry",
|
|
89
|
-
reason="
|
|
90
|
-
removal_phase="
|
|
117
|
+
reason="Root compatibility shims completed their window; removed in 9.9.1.",
|
|
118
|
+
removal_phase="removed-9.9.1",
|
|
119
|
+
layer="root",
|
|
120
|
+
status="removed",
|
|
91
121
|
),
|
|
92
122
|
LegacyShim(
|
|
93
123
|
path="llm_router.py",
|
|
94
124
|
owner="latticeai.models.router",
|
|
95
125
|
replacement="import latticeai.models.router",
|
|
96
|
-
reason="
|
|
97
|
-
removal_phase="
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
path="server.py",
|
|
101
|
-
owner="latticeai.server_app",
|
|
102
|
-
replacement="from latticeai.server_app import app, main",
|
|
103
|
-
reason="Deployment docs and local launch scripts historically targeted server.py.",
|
|
104
|
-
removal_phase="major-release-after-8.x",
|
|
105
|
-
),
|
|
106
|
-
LegacyShim(
|
|
107
|
-
path="tools/",
|
|
108
|
-
owner="latticeai.tools",
|
|
109
|
-
replacement="from latticeai.tools import execute_tool",
|
|
110
|
-
reason="Existing integrations and tests import the historical root tools package.",
|
|
111
|
-
removal_phase="major-release-after-9.x",
|
|
126
|
+
reason="Root compatibility shims completed their window; removed in 9.9.1.",
|
|
127
|
+
removal_phase="removed-9.9.1",
|
|
128
|
+
layer="root",
|
|
129
|
+
status="removed",
|
|
112
130
|
),
|
|
113
131
|
LegacyShim(
|
|
114
132
|
path="local_knowledge_api.py",
|
|
115
133
|
owner="latticeai.services.local_knowledge",
|
|
116
134
|
replacement="from latticeai.services.local_knowledge import create_local_knowledge_router",
|
|
117
|
-
reason="
|
|
118
|
-
removal_phase="
|
|
135
|
+
reason="Root compatibility shims completed their window; removed in 9.9.1.",
|
|
136
|
+
removal_phase="removed-9.9.1",
|
|
137
|
+
layer="root",
|
|
138
|
+
status="removed",
|
|
119
139
|
),
|
|
120
140
|
LegacyShim(
|
|
121
141
|
path="auto_setup.py",
|
|
122
142
|
owner="latticeai.setup.auto_setup",
|
|
123
143
|
replacement="from latticeai.setup.auto_setup import probe, recommend",
|
|
124
|
-
reason="
|
|
125
|
-
removal_phase="
|
|
144
|
+
reason="Root compatibility shims completed their window; removed in 9.9.1.",
|
|
145
|
+
removal_phase="removed-9.9.1",
|
|
146
|
+
layer="root",
|
|
147
|
+
status="removed",
|
|
126
148
|
),
|
|
127
149
|
LegacyShim(
|
|
128
150
|
path="setup_wizard.py",
|
|
129
151
|
owner="latticeai.setup.wizard",
|
|
130
152
|
replacement="from latticeai.setup.wizard import scan_environment",
|
|
131
|
-
reason="
|
|
132
|
-
removal_phase="
|
|
153
|
+
reason="Root compatibility shims completed their window; removed in 9.9.1.",
|
|
154
|
+
removal_phase="removed-9.9.1",
|
|
155
|
+
layer="root",
|
|
156
|
+
status="removed",
|
|
157
|
+
),
|
|
158
|
+
LegacyShim(
|
|
159
|
+
path="tools/",
|
|
160
|
+
owner="latticeai.tools",
|
|
161
|
+
replacement="from latticeai.tools import execute_tool",
|
|
162
|
+
reason="Root compatibility shims completed their window; removed in 9.9.1.",
|
|
163
|
+
removal_phase="removed-9.9.1",
|
|
164
|
+
layer="root",
|
|
165
|
+
status="removed",
|
|
133
166
|
),
|
|
134
|
-
]
|
|
135
|
-
|
|
136
|
-
# Shim layers that have completed their compatibility window and were
|
|
137
|
-
# physically deleted. Kept in the report so consumers (docs, tests, release
|
|
138
|
-
# notes) can tell "removed on purpose" apart from "missing by accident".
|
|
139
|
-
# Removing the internal-only layers is the first step of extracting
|
|
140
|
-
# ``lattice_brain`` as a standalone Brain Core package: the package now has
|
|
141
|
-
# exactly one import surface (``lattice_brain.*`` physical paths).
|
|
142
|
-
REMOVED_SHIMS: List[LegacyShim] = [
|
|
143
167
|
LegacyShim(
|
|
144
168
|
path="lattice_brain/store.py",
|
|
145
169
|
owner="lattice_brain.graph.store",
|
|
@@ -44,6 +44,63 @@ _ADDITIVE_TOOLS = frozenset({
|
|
|
44
44
|
"knowledge_graph_ingest",
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
+
# ── Mutating-tool inventory (single source of truth) ─────────────────────
|
|
48
|
+
# Every tool with a side effect is classified into exactly one category so a
|
|
49
|
+
# CI check can prove nothing slips through ungoverned. Categories:
|
|
50
|
+
# new_artifact — only ever creates new content (additive)
|
|
51
|
+
# existing_content_update — can rewrite content that already exists
|
|
52
|
+
# delete — removes existing content
|
|
53
|
+
# external_side_effect — acts outside the Brain (shell, deploy, desktop, net)
|
|
54
|
+
# internal_state — agent-internal bookkeeping, not user content
|
|
55
|
+
NEW_ARTIFACT = "new_artifact"
|
|
56
|
+
EXISTING_CONTENT_UPDATE = "existing_content_update"
|
|
57
|
+
DELETE = "delete"
|
|
58
|
+
EXTERNAL_SIDE_EFFECT = "external_side_effect"
|
|
59
|
+
INTERNAL_STATE = "internal_state"
|
|
60
|
+
|
|
61
|
+
MUTATING_TOOL_INVENTORY: Dict[str, str] = {
|
|
62
|
+
# text files — fully proposal-capable (staged + applied as reviewed)
|
|
63
|
+
"write_file": EXISTING_CONTENT_UPDATE,
|
|
64
|
+
"edit_file": EXISTING_CONTENT_UPDATE,
|
|
65
|
+
# binary/document creators — can overwrite an existing file, but their
|
|
66
|
+
# output cannot be staged as a text diff, so an overwrite is fail-closed.
|
|
67
|
+
"create_docx": EXISTING_CONTENT_UPDATE,
|
|
68
|
+
"create_xlsx": EXISTING_CONTENT_UPDATE,
|
|
69
|
+
"create_pptx": EXISTING_CONTENT_UPDATE,
|
|
70
|
+
"create_pdf": EXISTING_CONTENT_UPDATE,
|
|
71
|
+
# home-sandbox write — same story: overwrite is fail-closed until the
|
|
72
|
+
# proposal service learns to stage home-sandbox paths.
|
|
73
|
+
"local_write": EXISTING_CONTENT_UPDATE,
|
|
74
|
+
# additive-only writes
|
|
75
|
+
"create_web_project": NEW_ARTIFACT,
|
|
76
|
+
"knowledge_save": NEW_ARTIFACT,
|
|
77
|
+
"obsidian_save": NEW_ARTIFACT,
|
|
78
|
+
"knowledge_graph_ingest": NEW_ARTIFACT,
|
|
79
|
+
# agent-internal state
|
|
80
|
+
"todo_write": INTERNAL_STATE,
|
|
81
|
+
# deletions
|
|
82
|
+
"delete_file": DELETE,
|
|
83
|
+
"remove_file": DELETE,
|
|
84
|
+
"clear_history": DELETE,
|
|
85
|
+
# external side effects (approval-gated, never proposal-based)
|
|
86
|
+
"run_command": EXTERNAL_SIDE_EFFECT,
|
|
87
|
+
"build_project": EXTERNAL_SIDE_EFFECT,
|
|
88
|
+
"deploy_project": EXTERNAL_SIDE_EFFECT,
|
|
89
|
+
"computer_click": EXTERNAL_SIDE_EFFECT,
|
|
90
|
+
"computer_type": EXTERNAL_SIDE_EFFECT,
|
|
91
|
+
"computer_key": EXTERNAL_SIDE_EFFECT,
|
|
92
|
+
"computer_scroll": EXTERNAL_SIDE_EFFECT,
|
|
93
|
+
"computer_drag": EXTERNAL_SIDE_EFFECT,
|
|
94
|
+
"computer_move": EXTERNAL_SIDE_EFFECT,
|
|
95
|
+
"computer_open_app": EXTERNAL_SIDE_EFFECT,
|
|
96
|
+
"computer_open_url": EXTERNAL_SIDE_EFFECT,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Tools whose existing-content update the ChangeProposalService can actually
|
|
100
|
+
# stage AND apply as a reviewed proposal. A tool that is proposal_required but
|
|
101
|
+
# NOT here is fail-closed (blocked) rather than silently applied.
|
|
102
|
+
PROPOSAL_CAPABLE_TOOLS = frozenset({"write_file", "edit_file"})
|
|
103
|
+
|
|
47
104
|
|
|
48
105
|
def classify_tool_call(
|
|
49
106
|
name: str,
|
|
@@ -83,19 +140,53 @@ def classify_tool_call(
|
|
|
83
140
|
change = CHANGE_ADDITIVE
|
|
84
141
|
reason = "write tool without an existing target"
|
|
85
142
|
|
|
143
|
+
proposal_required = change in {CHANGE_MUTATION, CHANGE_DESTRUCTIVE}
|
|
144
|
+
proposal_supported = name in PROPOSAL_CAPABLE_TOOLS
|
|
145
|
+
# A change that must be reviewed as a proposal but that we cannot stage is
|
|
146
|
+
# fail-closed: callers must block it instead of applying it silently.
|
|
147
|
+
fail_closed = proposal_required and not proposal_supported
|
|
148
|
+
|
|
86
149
|
return {
|
|
87
150
|
"tool": name,
|
|
88
151
|
"change_class": change,
|
|
89
|
-
"proposal_required":
|
|
152
|
+
"proposal_required": proposal_required,
|
|
153
|
+
"proposal_supported": proposal_supported,
|
|
154
|
+
"fail_closed": fail_closed,
|
|
90
155
|
"reason": reason,
|
|
91
156
|
}
|
|
92
157
|
|
|
93
158
|
|
|
159
|
+
def assert_governance_coverage(tool_names: "Any") -> None:
|
|
160
|
+
"""Raise if any side-effecting registry tool is not classified.
|
|
161
|
+
|
|
162
|
+
A new mutating tool added to the registry without an inventory entry makes
|
|
163
|
+
this raise, so CI fails closed instead of shipping an ungoverned mutator.
|
|
164
|
+
Read-only tools (risk ``read``) are intentionally exempt.
|
|
165
|
+
"""
|
|
166
|
+
missing = [
|
|
167
|
+
name for name in tool_names
|
|
168
|
+
if name not in MUTATING_TOOL_INVENTORY
|
|
169
|
+
]
|
|
170
|
+
if missing:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
"ungoverned mutating tools (add to MUTATING_TOOL_INVENTORY): "
|
|
173
|
+
+ ", ".join(sorted(missing))
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
94
177
|
__all__ = [
|
|
95
178
|
"CHANGE_READ",
|
|
96
179
|
"CHANGE_ADDITIVE",
|
|
97
180
|
"CHANGE_MUTATION",
|
|
98
181
|
"CHANGE_DESTRUCTIVE",
|
|
99
182
|
"CHANGE_EXEC",
|
|
183
|
+
"NEW_ARTIFACT",
|
|
184
|
+
"EXISTING_CONTENT_UPDATE",
|
|
185
|
+
"DELETE",
|
|
186
|
+
"EXTERNAL_SIDE_EFFECT",
|
|
187
|
+
"INTERNAL_STATE",
|
|
188
|
+
"MUTATING_TOOL_INVENTORY",
|
|
189
|
+
"PROPOSAL_CAPABLE_TOOLS",
|
|
100
190
|
"classify_tool_call",
|
|
191
|
+
"assert_governance_coverage",
|
|
101
192
|
]
|
|
@@ -49,7 +49,7 @@ __all__ = [
|
|
|
49
49
|
"remove_skill_directory",
|
|
50
50
|
]
|
|
51
51
|
|
|
52
|
-
WORKSPACE_OS_VERSION = "9.
|
|
52
|
+
WORKSPACE_OS_VERSION = "9.9.1"
|
|
53
53
|
|
|
54
54
|
# Workspace types separate single-user Personal workspaces from shared
|
|
55
55
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|