@miller-tech/uap 2.21.2 → 2.36.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 +22 -10
- package/config/model-profiles/qwen38-coder390.json +46 -0
- package/config/model-profiles/qwen38.json +3 -2
- package/config/model-registry.json +110 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/benchmarks/paired/types.d.ts +2 -2
- package/dist/bin/cli.js +315 -20
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/attention.d.ts +6 -0
- package/dist/cli/attention.d.ts.map +1 -0
- package/dist/cli/attention.js +115 -0
- package/dist/cli/attention.js.map +1 -0
- package/dist/cli/bench-report.d.ts +82 -0
- package/dist/cli/bench-report.d.ts.map +1 -0
- package/dist/cli/bench-report.js +237 -0
- package/dist/cli/bench-report.js.map +1 -0
- package/dist/cli/clients.d.ts +6 -0
- package/dist/cli/clients.d.ts.map +1 -0
- package/dist/cli/clients.js +142 -0
- package/dist/cli/clients.js.map +1 -0
- package/dist/cli/dashboard-grants.d.ts +26 -0
- package/dist/cli/dashboard-grants.d.ts.map +1 -0
- package/dist/cli/dashboard-grants.js +69 -0
- package/dist/cli/dashboard-grants.js.map +1 -0
- package/dist/cli/dashboard-pace.d.ts +31 -0
- package/dist/cli/dashboard-pace.d.ts.map +1 -0
- package/dist/cli/dashboard-pace.js +42 -0
- package/dist/cli/dashboard-pace.js.map +1 -0
- package/dist/cli/dashboard-session.d.ts +25 -0
- package/dist/cli/dashboard-session.d.ts.map +1 -0
- package/dist/cli/dashboard-session.js +814 -0
- package/dist/cli/dashboard-session.js.map +1 -0
- package/dist/cli/dashboard-types.d.ts +18 -0
- package/dist/cli/dashboard-types.d.ts.map +1 -0
- package/dist/cli/dashboard-types.js +9 -0
- package/dist/cli/dashboard-types.js.map +1 -0
- package/dist/cli/dashboard.d.ts +2 -18
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +43 -793
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/deliver.d.ts +35 -0
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +93 -9
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/diff.d.ts +85 -0
- package/dist/cli/diff.d.ts.map +1 -0
- package/dist/cli/diff.js +293 -0
- package/dist/cli/diff.js.map +1 -0
- package/dist/cli/eta.d.ts +35 -0
- package/dist/cli/eta.d.ts.map +1 -0
- package/dist/cli/eta.js +101 -0
- package/dist/cli/eta.js.map +1 -0
- package/dist/cli/handsfree.d.ts +7 -0
- package/dist/cli/handsfree.d.ts.map +1 -1
- package/dist/cli/handsfree.js +27 -0
- package/dist/cli/handsfree.js.map +1 -1
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +126 -5
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/loops.d.ts +24 -5
- package/dist/cli/loops.d.ts.map +1 -1
- package/dist/cli/loops.js +92 -12
- package/dist/cli/loops.js.map +1 -1
- package/dist/cli/models.d.ts +62 -0
- package/dist/cli/models.d.ts.map +1 -0
- package/dist/cli/models.js +567 -0
- package/dist/cli/models.js.map +1 -0
- package/dist/cli/plan.d.ts +9 -0
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +57 -4
- package/dist/cli/plan.js.map +1 -1
- package/dist/cli/proxy-relay.d.ts +55 -0
- package/dist/cli/proxy-relay.d.ts.map +1 -0
- package/dist/cli/proxy-relay.js +116 -0
- package/dist/cli/proxy-relay.js.map +1 -0
- package/dist/cli/proxy.d.ts +4 -0
- package/dist/cli/proxy.d.ts.map +1 -1
- package/dist/cli/proxy.js +26 -1
- package/dist/cli/proxy.js.map +1 -1
- package/dist/cli/sandbox.d.ts +101 -4
- package/dist/cli/sandbox.d.ts.map +1 -1
- package/dist/cli/sandbox.js +393 -44
- package/dist/cli/sandbox.js.map +1 -1
- package/dist/cli/self.d.ts +13 -0
- package/dist/cli/self.d.ts.map +1 -0
- package/dist/cli/self.js +94 -0
- package/dist/cli/self.js.map +1 -0
- package/dist/cli/systemd-services.d.ts +2 -8
- package/dist/cli/systemd-services.d.ts.map +1 -1
- package/dist/cli/systemd-services.js +5 -20
- package/dist/cli/systemd-services.js.map +1 -1
- package/dist/cli/task.js +1 -1
- package/dist/cli/task.js.map +1 -1
- package/dist/cli/wizard-config.d.ts.map +1 -1
- package/dist/cli/wizard-config.js +31 -2
- package/dist/cli/wizard-config.js.map +1 -1
- package/dist/config/settings-registry.d.ts +1 -1
- package/dist/config/settings-registry.d.ts.map +1 -1
- package/dist/config/settings-registry.js +108 -0
- package/dist/config/settings-registry.js.map +1 -1
- package/dist/coordination/capability-router.js +2 -2
- package/dist/coordination/capability-router.js.map +1 -1
- package/dist/coordination/database.d.ts +13 -0
- package/dist/coordination/database.d.ts.map +1 -1
- package/dist/coordination/database.js +63 -7
- package/dist/coordination/database.js.map +1 -1
- package/dist/coordination/eta-inject.d.ts +32 -0
- package/dist/coordination/eta-inject.d.ts.map +1 -0
- package/dist/coordination/eta-inject.js +64 -0
- package/dist/coordination/eta-inject.js.map +1 -0
- package/dist/coordination/reactor.d.ts.map +1 -1
- package/dist/coordination/reactor.js +14 -0
- package/dist/coordination/reactor.js.map +1 -1
- package/dist/coordination/service.d.ts +18 -8
- package/dist/coordination/service.d.ts.map +1 -1
- package/dist/coordination/service.js +38 -28
- package/dist/coordination/service.js.map +1 -1
- package/dist/dashboard/client-registry.d.ts +163 -0
- package/dist/dashboard/client-registry.d.ts.map +1 -0
- package/dist/dashboard/client-registry.js +549 -0
- package/dist/dashboard/client-registry.js.map +1 -0
- package/dist/dashboard/controls.d.ts.map +1 -1
- package/dist/dashboard/controls.js +33 -4
- package/dist/dashboard/controls.js.map +1 -1
- package/dist/dashboard/data-seeder.d.ts +10 -7
- package/dist/dashboard/data-seeder.d.ts.map +1 -1
- package/dist/dashboard/data-seeder.js +31 -75
- package/dist/dashboard/data-seeder.js.map +1 -1
- package/dist/dashboard/data-service.d.ts +9 -0
- package/dist/dashboard/data-service.d.ts.map +1 -1
- package/dist/dashboard/data-service.js +32 -3
- package/dist/dashboard/data-service.js.map +1 -1
- package/dist/dashboard/placement-routes.d.ts +119 -0
- package/dist/dashboard/placement-routes.d.ts.map +1 -0
- package/dist/dashboard/placement-routes.js +331 -0
- package/dist/dashboard/placement-routes.js.map +1 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +408 -2
- package/dist/dashboard/server.js.map +1 -1
- package/dist/delivery/attention.d.ts +130 -0
- package/dist/delivery/attention.d.ts.map +1 -0
- package/dist/delivery/attention.js +341 -0
- package/dist/delivery/attention.js.map +1 -0
- package/dist/delivery/decompose.d.ts +18 -0
- package/dist/delivery/decompose.d.ts.map +1 -1
- package/dist/delivery/decompose.js +22 -1
- package/dist/delivery/decompose.js.map +1 -1
- package/dist/delivery/eta-estimate.d.ts +111 -0
- package/dist/delivery/eta-estimate.d.ts.map +1 -0
- package/dist/delivery/eta-estimate.js +171 -0
- package/dist/delivery/eta-estimate.js.map +1 -0
- package/dist/delivery/eta.d.ts +23 -0
- package/dist/delivery/eta.d.ts.map +1 -0
- package/dist/delivery/eta.js +59 -0
- package/dist/delivery/eta.js.map +1 -0
- package/dist/delivery/orchestrated-mission.d.ts +10 -0
- package/dist/delivery/orchestrated-mission.d.ts.map +1 -1
- package/dist/delivery/orchestrated-mission.js +15 -1
- package/dist/delivery/orchestrated-mission.js.map +1 -1
- package/dist/delivery/plan-check.d.ts +7 -1
- package/dist/delivery/plan-check.d.ts.map +1 -1
- package/dist/delivery/plan-check.js +23 -2
- package/dist/delivery/plan-check.js.map +1 -1
- package/dist/delivery/run-state.d.ts +1 -1
- package/dist/delivery/run-state.d.ts.map +1 -1
- package/dist/delivery/run-state.js +8 -1
- package/dist/delivery/run-state.js.map +1 -1
- package/dist/delivery/task-orchestrator.d.ts +8 -3
- package/dist/delivery/task-orchestrator.d.ts.map +1 -1
- package/dist/delivery/task-orchestrator.js +75 -43
- package/dist/delivery/task-orchestrator.js.map +1 -1
- package/dist/delivery/task-sync.d.ts +21 -0
- package/dist/delivery/task-sync.d.ts.map +1 -1
- package/dist/delivery/task-sync.js +44 -8
- package/dist/delivery/task-sync.js.map +1 -1
- package/dist/delivery/task-workspace.d.ts +3 -3
- package/dist/delivery/task-workspace.js +3 -3
- package/dist/generators/self-card.d.ts +65 -0
- package/dist/generators/self-card.d.ts.map +1 -0
- package/dist/generators/self-card.js +256 -0
- package/dist/generators/self-card.js.map +1 -0
- package/dist/inference/probe.js +1 -1
- package/dist/inference/probe.js.map +1 -1
- package/dist/memory/prepopulate.d.ts.map +1 -1
- package/dist/memory/prepopulate.js +2 -0
- package/dist/memory/prepopulate.js.map +1 -1
- package/dist/models/analytics.d.ts +32 -0
- package/dist/models/analytics.d.ts.map +1 -1
- package/dist/models/analytics.js +68 -5
- package/dist/models/analytics.js.map +1 -1
- package/dist/models/plan-readability.d.ts +55 -0
- package/dist/models/plan-readability.d.ts.map +1 -0
- package/dist/models/plan-readability.js +339 -0
- package/dist/models/plan-readability.js.map +1 -0
- package/dist/placement/admission.d.ts +59 -0
- package/dist/placement/admission.d.ts.map +1 -0
- package/dist/placement/admission.js +197 -0
- package/dist/placement/admission.js.map +1 -0
- package/dist/placement/auto.d.ts +44 -0
- package/dist/placement/auto.d.ts.map +1 -0
- package/dist/placement/auto.js +212 -0
- package/dist/placement/auto.js.map +1 -0
- package/dist/placement/enforce.d.ts +128 -0
- package/dist/placement/enforce.d.ts.map +1 -0
- package/dist/placement/enforce.js +655 -0
- package/dist/placement/enforce.js.map +1 -0
- package/dist/placement/idle.d.ts +50 -0
- package/dist/placement/idle.d.ts.map +1 -0
- package/dist/placement/idle.js +213 -0
- package/dist/placement/idle.js.map +1 -0
- package/dist/placement/ledger.d.ts +323 -0
- package/dist/placement/ledger.d.ts.map +1 -0
- package/dist/placement/ledger.js +263 -0
- package/dist/placement/ledger.js.map +1 -0
- package/dist/placement/preview.d.ts +51 -0
- package/dist/placement/preview.d.ts.map +1 -0
- package/dist/placement/preview.js +61 -0
- package/dist/placement/preview.js.map +1 -0
- package/dist/placement/probes.d.ts +47 -0
- package/dist/placement/probes.d.ts.map +1 -0
- package/dist/placement/probes.js +207 -0
- package/dist/placement/probes.js.map +1 -0
- package/dist/placement/proxy-env.d.ts +21 -0
- package/dist/placement/proxy-env.d.ts.map +1 -0
- package/dist/placement/proxy-env.js +42 -0
- package/dist/placement/proxy-env.js.map +1 -0
- package/dist/placement/registry.d.ts +1038 -0
- package/dist/placement/registry.d.ts.map +1 -0
- package/dist/placement/registry.js +274 -0
- package/dist/placement/registry.js.map +1 -0
- package/dist/policies/liveness.d.ts +6 -8
- package/dist/policies/liveness.d.ts.map +1 -1
- package/dist/policies/liveness.js +6 -25
- package/dist/policies/liveness.js.map +1 -1
- package/dist/policies/sandbox-anchor.d.ts +91 -0
- package/dist/policies/sandbox-anchor.d.ts.map +1 -0
- package/dist/policies/sandbox-anchor.js +187 -0
- package/dist/policies/sandbox-anchor.js.map +1 -0
- package/dist/policies/sandbox-grants.d.ts +131 -0
- package/dist/policies/sandbox-grants.d.ts.map +1 -0
- package/dist/policies/sandbox-grants.js +562 -0
- package/dist/policies/sandbox-grants.js.map +1 -0
- package/dist/state/manifest.d.ts +4 -2
- package/dist/state/manifest.d.ts.map +1 -1
- package/dist/state/manifest.js +32 -16
- package/dist/state/manifest.js.map +1 -1
- package/dist/utils/checkout-root.d.ts +2 -0
- package/dist/utils/checkout-root.d.ts.map +1 -0
- package/dist/utils/checkout-root.js +43 -0
- package/dist/utils/checkout-root.js.map +1 -0
- package/dist/utils/resolve-uap-bin.d.ts +5 -0
- package/dist/utils/resolve-uap-bin.d.ts.map +1 -0
- package/dist/utils/resolve-uap-bin.js +33 -0
- package/dist/utils/resolve-uap-bin.js.map +1 -0
- package/dist/utils/terminal-safe.d.ts +12 -0
- package/dist/utils/terminal-safe.d.ts.map +1 -0
- package/dist/utils/terminal-safe.js +53 -0
- package/dist/utils/terminal-safe.js.map +1 -0
- package/docs/ATTRIBUTION.md +1 -0
- package/docs/DASHBOARD_UPLIFT_SPEC.md +8 -8
- package/docs/INDEX.md +6 -5
- package/docs/architecture/OVERVIEW.md +15 -3
- package/docs/architecture/PROTOCOL.md +7 -1
- package/docs/architecture/adr/0006-operator-directed-model-placement.md +86 -0
- package/docs/architecture/adr/0007-auto-displacement-allowlist-as-standing-consent.md +79 -0
- package/docs/architecture/adr/0008-fleet-clients-link-out.md +92 -0
- package/docs/architecture/adr/0009-completion-triggered-dispatch-and-per-leaf-model-dials.md +71 -0
- package/docs/architecture/adr/0010-attention-queue-and-afk-auto-decider.md +159 -0
- package/docs/architecture/adr/0011-sandbox-grants-and-network-egress-boundary.md +219 -0
- package/docs/architecture/adr/0012-operator-legibility-surfaces.md +175 -0
- package/docs/getting-started/INSTALLATION.md +1 -1
- package/docs/guides/AUTOMATIC_FEATURES.md +8 -0
- package/docs/guides/LOCAL_MODELS.md +1 -1
- package/docs/guides/PROXY.md +2 -1
- package/docs/guides/SANDBOX.md +91 -4
- package/docs/integrations/MCP_ROUTER.md +1 -1
- package/docs/plans/model-performance-proof-options.md +176 -0
- package/docs/reference/API.md +1 -1
- package/docs/reference/CLI.md +169 -4
- package/docs/reference/COMPARISON.md +1 -1
- package/docs/reference/CONFIGURATION.md +12 -3
- package/docs/reference/CONFIGURATION_REFERENCE.md +269 -14
- package/docs/reference/DATABASE_SCHEMA.md +1 -1
- package/docs/reference/FEATURES.md +15 -3
- package/docs/reference/PATTERNS.md +1 -1
- package/docs/reference/PLATFORMS.md +14 -1
- package/docs/specs/operator-model-placement.md +1314 -0
- package/docs/specs/operator-model-placement.review.md +339 -0
- package/package.json +2 -2
- package/scripts/maintenance/publish-npm.sh +23 -11
- package/scripts/version-bump.sh +6 -51
- package/src/policies/enforcers/__pycache__/_common.cpython-311.pyc +0 -0
- package/src/policies/enforcers/__pycache__/expert_review_required.cpython-311.pyc +0 -0
- package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-311.pyc +0 -0
- package/src/policies/schemas/policies/mandatory-testing-deployment.md +2 -5
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-311.pyc +0 -0
- package/templates/hooks/session-start.sh +44 -0
- package/tools/agents/UAP/cli.py +14 -3
- package/tools/agents/UAP/paths.py +50 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-311.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +1386 -99
- package/tools/agents/scripts/toolcall_path_normalizer.py +46 -1
- package/tools/agents/tests/test_anthropic_proxy_streaming.py +29 -6
- package/tools/agents/tests/test_doubling_break.py +64 -5
- package/tools/agents/tests/test_error_loop_break.py +10 -4
- package/tools/agents/tests/test_error_loop_ignores_correctives.py +13 -2
- package/tools/agents/tests/test_lockstep_escalation.py +280 -9
- package/tools/agents/tests/test_path_containment.py +94 -0
- package/tools/agents/tests/test_proxy_unix_socket.py +162 -0
- package/tools/agents/tests/test_signature_quality.py +349 -0
- package/tools/agents/tests/test_uap_compliance.py +11 -1
- package/web/dash/core.js +1 -0
- package/web/dash/tab-clients.js +196 -0
- package/web/dash/tab-models.js +387 -8
- package/web/dash/tab-policies.js +87 -4
- package/web/dash/tabs.js +77 -73
- package/web/dashboard.html +6 -0
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/__pycache__/expert_review_required.cpython-312.pyc +0 -0
- package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/web/dash/tab-agents.js +0 -21
- package/web/dash/tab-deliver.js +0 -12
- package/web/dash/tab-memory.js +0 -12
- package/web/dash/tab-orchestration.js +0 -21
- package/web/dash/tab-overview.js +0 -21
- package/web/dash/tab-tasks.js +0 -21
|
@@ -144,6 +144,100 @@ class TestContainToolUses(unittest.TestCase):
|
|
|
144
144
|
tus = [_tu("t1", file_path="/home/cogtec/dev/octus_invaders/a.js")]
|
|
145
145
|
self.assertEqual(N.contain_tool_uses(tus, ""), [])
|
|
146
146
|
|
|
147
|
+
def _wd(self, *parts):
|
|
148
|
+
# Journal geometry: the 2026-10-18 incident workdir .../p37/e2e made
|
|
149
|
+
# the 'e2e' basename ANCHOR the container path /tmp/e2e/..., which is
|
|
150
|
+
# what let containment misclassify it as a garbled host path. Tests
|
|
151
|
+
# must reproduce that anchor or they do not discriminate.
|
|
152
|
+
wd = os.path.join(self._tmp.name, *parts)
|
|
153
|
+
os.makedirs(wd, exist_ok=True)
|
|
154
|
+
return wd
|
|
155
|
+
|
|
156
|
+
def test_docker_exec_container_paths_left_alone(self):
|
|
157
|
+
# Regression 2026-10-18, journal geometry exactly: the session
|
|
158
|
+
# workdir .../p37/e2e anchored the container path /tmp/e2e/... and
|
|
159
|
+
# pre-fix code snapped it onto the workdir -- an 11-hour
|
|
160
|
+
# deterministic-failure loop. The fix passes the command verbatim.
|
|
161
|
+
wd = self._wd("p37", "e2e")
|
|
162
|
+
cmd = ("docker exec p37koral bash -lc "
|
|
163
|
+
"'cat /tmp/e2e/koral-p59probe_proda4.log'")
|
|
164
|
+
tus = [{"type": "tool_use", "id": "t1", "name": "Bash",
|
|
165
|
+
"input": {"command": cmd}}]
|
|
166
|
+
self.assertEqual(N.contain_tool_uses(tus, wd), [])
|
|
167
|
+
self.assertEqual(tus[0]["input"]["command"], cmd)
|
|
168
|
+
|
|
169
|
+
def test_container_exec_variants_left_alone(self):
|
|
170
|
+
# Every variant uses the anchoring geometry so each discriminates:
|
|
171
|
+
# pre-fix, each gets snapped (1 correction); post-fix, all pass
|
|
172
|
+
# verbatim. DOCKER pins the case-insensitive match.
|
|
173
|
+
wd = self._wd("p37", "e2e")
|
|
174
|
+
for cmd in (
|
|
175
|
+
"kubectl -n prod exec koral-0 -- cat /tmp/e2e/app.log",
|
|
176
|
+
"docker compose exec koral cat /tmp/e2e/config.yaml",
|
|
177
|
+
"docker-compose exec koral cat /tmp/e2e/config.yaml",
|
|
178
|
+
"sudo podman exec ctr1 ls /tmp/e2e/data",
|
|
179
|
+
"DOCKER exec ctr1 cat /tmp/e2e/UP.LOG",
|
|
180
|
+
):
|
|
181
|
+
tus = [{"type": "tool_use", "id": "t1", "name": "Bash",
|
|
182
|
+
"input": {"command": cmd}}]
|
|
183
|
+
self.assertEqual(N.contain_tool_uses(tus, wd), [], msg=cmd)
|
|
184
|
+
self.assertEqual(tus[0]["input"]["command"], cmd)
|
|
185
|
+
|
|
186
|
+
def test_host_path_before_docker_exec_still_contained(self):
|
|
187
|
+
# Boundary is the END of the exec word: the garbled HOST path
|
|
188
|
+
# before the subcommand still snaps; the container path after it
|
|
189
|
+
# passes verbatim. Full-command equality so any drift fails.
|
|
190
|
+
wd = self._wd("e2e")
|
|
191
|
+
cmd = ("mkdir -p /home/cogtk/dev/e2e/css && "
|
|
192
|
+
"docker exec koral cat /tmp/e2e/x.log")
|
|
193
|
+
tus = [{"type": "tool_use", "id": "t1", "name": "Bash",
|
|
194
|
+
"input": {"command": cmd}}]
|
|
195
|
+
corr = N.contain_tool_uses(tus, wd)
|
|
196
|
+
self.assertEqual(tus[0]["input"]["command"],
|
|
197
|
+
f"mkdir -p {wd}/css && docker exec koral cat /tmp/e2e/x.log")
|
|
198
|
+
self.assertEqual(len(corr), 1)
|
|
199
|
+
|
|
200
|
+
def test_engine_flag_path_before_exec_still_contained(self):
|
|
201
|
+
# Pins the END-of-exec-word boundary: a host-side engine flag
|
|
202
|
+
# (kubectl --kubeconfig=...) sits before the subcommand and stays
|
|
203
|
+
# contained; the container payload after it is exempt.
|
|
204
|
+
wd = self._wd("e2e")
|
|
205
|
+
cmd = ("kubectl --kubeconfig=/home/cogtk/dev/e2e/kc.conf exec koral-0 "
|
|
206
|
+
"-- cat /tmp/e2e/x.log")
|
|
207
|
+
tus = [{"type": "tool_use", "id": "t1", "name": "Bash",
|
|
208
|
+
"input": {"command": cmd}}]
|
|
209
|
+
corr = N.contain_tool_uses(tus, wd)
|
|
210
|
+
self.assertEqual(tus[0]["input"]["command"],
|
|
211
|
+
f"kubectl --kubeconfig={wd}/kc.conf exec koral-0 "
|
|
212
|
+
"-- cat /tmp/e2e/x.log")
|
|
213
|
+
self.assertEqual(len(corr), 1)
|
|
214
|
+
|
|
215
|
+
def test_exec_exempt_scope_is_sticky_to_end_of_command(self):
|
|
216
|
+
# Pinned semantics (architect + security reviews): once a
|
|
217
|
+
# container-exec boundary is found, the exemption covers the whole
|
|
218
|
+
# remainder, INCLUDING later host-side segments after &&. Narrowing
|
|
219
|
+
# to the next separator would need quote-awareness a regex cannot
|
|
220
|
+
# provide (bash -lc 'cat /a; cat /b') and would reintroduce the
|
|
221
|
+
# incident class. Fail-safe: an exempted garble errors loudly.
|
|
222
|
+
wd = self._wd("e2e")
|
|
223
|
+
cmd = ("docker exec koral cat /tmp/e2e/x.log "
|
|
224
|
+
"&& cat /home/cogtk/dev/e2e/creds.txt")
|
|
225
|
+
tus = [{"type": "tool_use", "id": "t1", "name": "Bash",
|
|
226
|
+
"input": {"command": cmd}}]
|
|
227
|
+
self.assertEqual(N.contain_tool_uses(tus, wd), [])
|
|
228
|
+
self.assertEqual(tus[0]["input"]["command"], cmd)
|
|
229
|
+
|
|
230
|
+
def test_exec_decoy_still_triggers_exemption(self):
|
|
231
|
+
# A quoted "docker exec" decoy triggers the exemption too -- accepted
|
|
232
|
+
# fail-safe semantics: the worst case is an un-healed garble failing
|
|
233
|
+
# loudly (pre-normalizer behavior), never a wrong-write.
|
|
234
|
+
wd = self._wd("e2e")
|
|
235
|
+
cmd = 'echo "docker exec" && cat /tmp/e2e/creds.txt'
|
|
236
|
+
tus = [{"type": "tool_use", "id": "t1", "name": "Bash",
|
|
237
|
+
"input": {"command": cmd}}]
|
|
238
|
+
self.assertEqual(N.contain_tool_uses(tus, wd), [])
|
|
239
|
+
self.assertEqual(tus[0]["input"]["command"], cmd)
|
|
240
|
+
|
|
147
241
|
|
|
148
242
|
if __name__ == "__main__":
|
|
149
243
|
unittest.main()
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Tests for the PROXY_SOCKET unix-socket surface (bridge P4: D1 model-path
|
|
2
|
+
choke point).
|
|
3
|
+
|
|
4
|
+
The socket is a permission-gated transport for the model gateway — only the
|
|
5
|
+
owning user can dial it (0600 socket in a 0700 parent) — serving alongside
|
|
6
|
+
the TCP port. Its preparation is the boundary itself: correct bind, tight
|
|
7
|
+
permissions, stale-corpse cleanup, refusal to evict a live proxy's socket
|
|
8
|
+
(including one that cannot be dialed — EACCES is not proof of death), refusal
|
|
9
|
+
to replace a non-socket file at the path, and a warning when the parent dir
|
|
10
|
+
is group/other-writable.
|
|
11
|
+
"""
|
|
12
|
+
import contextlib
|
|
13
|
+
import importlib.util
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import socket
|
|
17
|
+
import stat
|
|
18
|
+
import tempfile
|
|
19
|
+
import unittest
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_proxy_module():
|
|
24
|
+
proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
|
|
25
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
|
|
26
|
+
module = importlib.util.module_from_spec(spec)
|
|
27
|
+
spec.loader.exec_module(module)
|
|
28
|
+
return module
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
proxy = _load_proxy_module()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ProxySocketPrepTest(unittest.TestCase):
|
|
35
|
+
def setUp(self):
|
|
36
|
+
self.tmp = tempfile.mkdtemp(prefix="uap-sock-")
|
|
37
|
+
# The parent dir exists up front so tests can bind corpses directly.
|
|
38
|
+
os.makedirs(os.path.join(self.tmp, "sub"))
|
|
39
|
+
self.sock_path = os.path.join(self.tmp, "sub", "proxy.sock")
|
|
40
|
+
|
|
41
|
+
def tearDown(self):
|
|
42
|
+
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
43
|
+
|
|
44
|
+
def _close_quietly(self, sock):
|
|
45
|
+
# Teardown: an already-closed socket (uvicorn may have shut it down)
|
|
46
|
+
# must not fail the test run. contextlib.suppress states that intent.
|
|
47
|
+
with contextlib.suppress(OSError):
|
|
48
|
+
sock.close()
|
|
49
|
+
|
|
50
|
+
def test_binds_with_tight_permissions(self):
|
|
51
|
+
prepared = proxy._prepare_proxy_socket(self.sock_path)
|
|
52
|
+
self.assertIsNotNone(prepared)
|
|
53
|
+
try:
|
|
54
|
+
mode = stat.S_IMODE(os.stat(self.sock_path).st_mode)
|
|
55
|
+
self.assertEqual(mode, 0o600, "the socket is the egress boundary; 0600 or nothing")
|
|
56
|
+
self.assertTrue(proxy._socket_is_live(self.sock_path))
|
|
57
|
+
finally:
|
|
58
|
+
self._close_quietly(prepared)
|
|
59
|
+
|
|
60
|
+
def test_creates_a_missing_parent_dir_with_0700(self):
|
|
61
|
+
# A deep path whose leaf directory does not exist yet: the surface
|
|
62
|
+
# must create it PRIVATE (0700) — a world-readable parent would let
|
|
63
|
+
# any local user name-squat the socket path.
|
|
64
|
+
deep = os.path.join(self.tmp, "a", "b", "c", "proxy.sock")
|
|
65
|
+
prepared = proxy._prepare_proxy_socket(deep)
|
|
66
|
+
self.assertIsNotNone(prepared)
|
|
67
|
+
try:
|
|
68
|
+
leaf = os.path.dirname(deep)
|
|
69
|
+
mode = stat.S_IMODE(os.stat(leaf).st_mode)
|
|
70
|
+
self.assertEqual(mode & 0o777, 0o700, "parent dirs of the socket must be 0700")
|
|
71
|
+
self.assertTrue(proxy._socket_is_live(deep))
|
|
72
|
+
finally:
|
|
73
|
+
self._close_quietly(prepared)
|
|
74
|
+
|
|
75
|
+
def test_binds_when_the_parent_exists_with_loose_perms(self):
|
|
76
|
+
# The pre-existing-parent case must not regress: bind still works
|
|
77
|
+
# and the SOCKET itself stays 0600 (the parent is left as the
|
|
78
|
+
# operator made it — chmod-ing a dir the operator owns would be a
|
|
79
|
+
# surprising side effect).
|
|
80
|
+
os.chmod(os.path.dirname(self.sock_path), 0o755)
|
|
81
|
+
prepared = proxy._prepare_proxy_socket(self.sock_path)
|
|
82
|
+
self.assertIsNotNone(prepared)
|
|
83
|
+
try:
|
|
84
|
+
mode = stat.S_IMODE(os.stat(self.sock_path).st_mode)
|
|
85
|
+
self.assertEqual(mode, 0o600)
|
|
86
|
+
finally:
|
|
87
|
+
self._close_quietly(prepared)
|
|
88
|
+
|
|
89
|
+
def test_unlinks_a_stale_corpse_and_rebinds(self):
|
|
90
|
+
# A corpse from a crashed run: nothing accepts on the other end.
|
|
91
|
+
corpse = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
92
|
+
corpse.bind(self.sock_path)
|
|
93
|
+
corpse.close() # bound but never listening => connect() refused
|
|
94
|
+
prepared = proxy._prepare_proxy_socket(self.sock_path)
|
|
95
|
+
self.assertIsNotNone(prepared, "a stale socket must be reclaimed, not fatal")
|
|
96
|
+
self.assertTrue(proxy._socket_is_live(self.sock_path))
|
|
97
|
+
self._close_quietly(prepared)
|
|
98
|
+
|
|
99
|
+
def test_refuses_to_evict_a_live_proxy(self):
|
|
100
|
+
live = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
101
|
+
live.bind(self.sock_path)
|
|
102
|
+
live.listen(8)
|
|
103
|
+
try:
|
|
104
|
+
prepared = proxy._prepare_proxy_socket(self.sock_path)
|
|
105
|
+
self.assertIsNone(prepared, "a LIVE socket must be left to its owner")
|
|
106
|
+
self.assertTrue(proxy._socket_is_live(self.sock_path))
|
|
107
|
+
finally:
|
|
108
|
+
live.close()
|
|
109
|
+
|
|
110
|
+
def test_refuses_to_evict_a_live_socket_it_cannot_dial(self):
|
|
111
|
+
# EACCES must read as LIVE, not corpse (P4 security review): another
|
|
112
|
+
# user's 0600 socket in a shared dir cannot be proven dead, and
|
|
113
|
+
# evicting it would let this process impersonate their endpoint.
|
|
114
|
+
# Simulated same-user: chmod 000 on the socket makes connect() fail
|
|
115
|
+
# with EACCES while the server keeps accepting on its own fd.
|
|
116
|
+
live = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
117
|
+
live.bind(self.sock_path)
|
|
118
|
+
live.listen(8)
|
|
119
|
+
os.chmod(self.sock_path, 0o000)
|
|
120
|
+
try:
|
|
121
|
+
prepared = proxy._prepare_proxy_socket(self.sock_path)
|
|
122
|
+
self.assertIsNone(prepared, "an undialable LIVE socket must not be evicted")
|
|
123
|
+
self.assertTrue(os.path.exists(self.sock_path), "the socket file must survive")
|
|
124
|
+
finally:
|
|
125
|
+
os.chmod(self.sock_path, 0o600)
|
|
126
|
+
live.close()
|
|
127
|
+
|
|
128
|
+
def test_never_replaces_a_regular_file(self):
|
|
129
|
+
# PROXY_SOCKET can arrive from the operator's env or .uap/proxy.env
|
|
130
|
+
# (inside the sandbox-writable checkout): a typo or planted value
|
|
131
|
+
# must not become an arbitrary-file-delete primitive. The file must
|
|
132
|
+
# survive byte-for-byte.
|
|
133
|
+
data = b'{"important": "operator data"}'
|
|
134
|
+
with open(self.sock_path, "wb") as f:
|
|
135
|
+
f.write(data)
|
|
136
|
+
prepared = proxy._prepare_proxy_socket(self.sock_path)
|
|
137
|
+
self.assertIsNone(prepared, "a non-socket at the path must disable the surface")
|
|
138
|
+
with open(self.sock_path, "rb") as f:
|
|
139
|
+
self.assertEqual(f.read(), data, "the file must be untouched")
|
|
140
|
+
|
|
141
|
+
def test_warns_when_the_parent_dir_is_shared(self):
|
|
142
|
+
# A group/other-writable parent lets another local user unlink or
|
|
143
|
+
# squat the socket path; the operator gets the consequence named.
|
|
144
|
+
os.chmod(os.path.dirname(self.sock_path), 0o770)
|
|
145
|
+
try:
|
|
146
|
+
with self.assertLogs("uap.anthropic_proxy", level="WARNING") as logs:
|
|
147
|
+
prepared = proxy._prepare_proxy_socket(self.sock_path)
|
|
148
|
+
self.assertIsNotNone(prepared)
|
|
149
|
+
self.assertTrue(any("group/other-writable" in line for line in logs.output), logs.output)
|
|
150
|
+
self._close_quietly(prepared)
|
|
151
|
+
finally:
|
|
152
|
+
os.chmod(os.path.dirname(self.sock_path), 0o755)
|
|
153
|
+
|
|
154
|
+
def test_empty_and_too_long_paths_disable_the_surface(self):
|
|
155
|
+
self.assertIsNone(proxy._prepare_proxy_socket(""))
|
|
156
|
+
too_long = os.path.join(self.tmp, "x" * 130 + ".sock")
|
|
157
|
+
self.assertIsNone(proxy._prepare_proxy_socket(too_long))
|
|
158
|
+
self.assertFalse(os.path.exists(too_long))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
unittest.main()
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Loop-steer signature quality: the ledger classes the guards mis-sighted.
|
|
3
|
+
|
|
4
|
+
Built from the loop-incident ledger after the v2.33/v2.34 uplift on this branch:
|
|
5
|
+
- 2026-09-25: three distinct bash failures shared the content-free
|
|
6
|
+
"Traceback (most recent call last):" signature and escalated to a hard
|
|
7
|
+
stop that never named the real error.
|
|
8
|
+
- 2026-10-08: a worktree status listing whose slug contains "error"
|
|
9
|
+
("openobserve-error-scan") became the signature of a healthy progress
|
|
10
|
+
report.
|
|
11
|
+
- 2026-10-17: reading a shell script containing a die() helper entered the
|
|
12
|
+
failure streak -- a client that never sends is_error made file CONTENT
|
|
13
|
+
look like a tool failure.
|
|
14
|
+
- 2026-10-18: different execute failures shared the content-free
|
|
15
|
+
"error: command failed (exit code: #)" wrapper and manufactured a
|
|
16
|
+
lockstep streak of 4 across distinct calls.
|
|
17
|
+
|
|
18
|
+
The ladder these tests pin: a traceback anchors on its decisive line; a real
|
|
19
|
+
error line anchors the signature; a wrapper-only result anchors on its first
|
|
20
|
+
meaningful line; and a failure that cannot be named yields "" -- DOUBLING-DOWN
|
|
21
|
+
owns that shape, not a signature streak.
|
|
22
|
+
"""
|
|
23
|
+
import importlib.util
|
|
24
|
+
import unittest
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_proxy_module():
|
|
29
|
+
proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
|
|
30
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
|
|
31
|
+
mod = importlib.util.module_from_spec(spec)
|
|
32
|
+
spec.loader.exec_module(mod)
|
|
33
|
+
return mod
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
proxy = _load_proxy_module()
|
|
37
|
+
|
|
38
|
+
TRACEBACK_FULL = (
|
|
39
|
+
"Traceback (most recent call last):\n"
|
|
40
|
+
' File "/app/run.py", line 3, in <module>\n'
|
|
41
|
+
" raise ValueError('boom')\n"
|
|
42
|
+
"ValueError: boom"
|
|
43
|
+
)
|
|
44
|
+
WRAPPER_ONLY = "error: command failed (exit code: 1)"
|
|
45
|
+
WRAPPER_WITH_STDERR = (
|
|
46
|
+
"error: command failed (exit code: 1)\n"
|
|
47
|
+
'psql: error: connection to server at "plane-db" (10.5.0.2), port 5432 '
|
|
48
|
+
"failed: fe_sendauth: no password supplied"
|
|
49
|
+
)
|
|
50
|
+
WRAPPER_WITH_OUTPUT = "error: command failed (exit code: 1)\nCompiling main.c"
|
|
51
|
+
WORKTREE_STATUS = (
|
|
52
|
+
"wt-3 in_progress | worktree: openobserve-error-scan "
|
|
53
|
+
"(feature/263-stuck-break-hard)"
|
|
54
|
+
)
|
|
55
|
+
SHELL_SOURCE_READ = "die() { printf 'fatal: %s\\n' \"$*\" >&2; exit 1; }\n"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TracebackAnchoringTests(unittest.TestCase):
|
|
59
|
+
def test_decisive_line_anchors_not_the_header(self):
|
|
60
|
+
sig = proxy._error_signature(TRACEBACK_FULL)
|
|
61
|
+
self.assertEqual(sig, "valueerror: boom")
|
|
62
|
+
self.assertNotIn("traceback", sig)
|
|
63
|
+
|
|
64
|
+
def test_decisive_line_found_when_not_keyword_shaped(self):
|
|
65
|
+
# SystemExit is a real decisive line but carries no _ERROR_LINE_RE
|
|
66
|
+
# token, so only traceback anchoring can find it.
|
|
67
|
+
text = (
|
|
68
|
+
"Traceback (most recent call last):\n"
|
|
69
|
+
' File "/app/x.py", line 7, in <module>\n'
|
|
70
|
+
"SystemExit: 1"
|
|
71
|
+
)
|
|
72
|
+
self.assertEqual(proxy._error_signature(text), "systemexit: #")
|
|
73
|
+
|
|
74
|
+
def test_truncated_traceback_anchors_on_last_visible_frame(self):
|
|
75
|
+
# Token-limit cut-off before the exception line: the frames still
|
|
76
|
+
# identify the failing call, which is all a streak needs. (The path
|
|
77
|
+
# normalizer swallowing the trailing quote+comma is pre-existing and
|
|
78
|
+
# stable, so the signature remains edit-invariant.)
|
|
79
|
+
text = (
|
|
80
|
+
"Traceback (most recent call last):\n"
|
|
81
|
+
' File "/app/x.py", line 3, in main\n'
|
|
82
|
+
)
|
|
83
|
+
self.assertEqual(proxy._error_signature(text), 'file "<path> line #, in main')
|
|
84
|
+
|
|
85
|
+
def test_header_alone_names_nothing(self):
|
|
86
|
+
self.assertEqual(
|
|
87
|
+
proxy._error_signature("Traceback (most recent call last):"), ""
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def test_mid_payload_traceback_still_anchors(self):
|
|
91
|
+
# code-quality review 2026-10-18: without re.MULTILINE on the header
|
|
92
|
+
# regex, ^ never matched after leading output and the decisive ladder
|
|
93
|
+
# was skipped for every mid-payload traceback.
|
|
94
|
+
text = (
|
|
95
|
+
"building frontend...\n"
|
|
96
|
+
"ok\n"
|
|
97
|
+
"Traceback (most recent call last):\n"
|
|
98
|
+
' File "/app/run.py", line 3, in <module>\n'
|
|
99
|
+
" raise KeyError('x')\n"
|
|
100
|
+
"KeyError: 'x'"
|
|
101
|
+
)
|
|
102
|
+
self.assertEqual(proxy._error_signature(text), "keyerror: 'x'")
|
|
103
|
+
|
|
104
|
+
def test_mid_payload_truncated_traceback_anchors_on_last_frame(self):
|
|
105
|
+
text = (
|
|
106
|
+
"starting\n"
|
|
107
|
+
"Traceback (most recent call last):\n"
|
|
108
|
+
' File "/app/x.py", line 3, in main\n'
|
|
109
|
+
)
|
|
110
|
+
self.assertEqual(
|
|
111
|
+
proxy._error_signature(text), 'file "<path> line #, in main'
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def test_chained_traceback_names_the_final_exception(self):
|
|
115
|
+
# Each traceback opens with its own header, which resets the decisive
|
|
116
|
+
# line, so the LAST traceback's exception is the signature.
|
|
117
|
+
text = (
|
|
118
|
+
"Traceback (most recent call last):\n"
|
|
119
|
+
' File "/app/run.py", line 3, in <module>\n'
|
|
120
|
+
" raise KeyError('x')\n"
|
|
121
|
+
"KeyError: 'x'\n"
|
|
122
|
+
"\n"
|
|
123
|
+
"During handling of the above exception, another exception "
|
|
124
|
+
"occurred:\n"
|
|
125
|
+
"\n"
|
|
126
|
+
"Traceback (most recent call last):\n"
|
|
127
|
+
' File "/app/handle.py", line 9, in recover\n'
|
|
128
|
+
"TypeError: cannot convert\n"
|
|
129
|
+
)
|
|
130
|
+
self.assertEqual(
|
|
131
|
+
proxy._error_signature(text), "typeerror: cannot convert"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def test_trailing_wrapper_exception_cannot_hijack_the_signature(self):
|
|
135
|
+
# code-quality review 2026-10-18: the last-decisive-wins overwrite let
|
|
136
|
+
# a trailing "...RetryError:" rename a KeyError failure.
|
|
137
|
+
text = TRACEBACK_FULL + "\nrequests.exceptions.RetryError: retrying"
|
|
138
|
+
self.assertEqual(proxy._error_signature(text), "valueerror: boom")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ContentFreeWrapperTests(unittest.TestCase):
|
|
142
|
+
def test_wrapper_alone_names_nothing(self):
|
|
143
|
+
self.assertEqual(proxy._error_signature(WRAPPER_ONLY), "")
|
|
144
|
+
self.assertEqual(proxy._error_signature("Exit code 1"), "")
|
|
145
|
+
|
|
146
|
+
def test_wrapper_with_stderr_anchors_on_stderr(self):
|
|
147
|
+
sig = proxy._error_signature(WRAPPER_WITH_STDERR)
|
|
148
|
+
self.assertTrue(sig.startswith("psql: "))
|
|
149
|
+
self.assertIn("fe_sendauth", sig)
|
|
150
|
+
|
|
151
|
+
def test_wrapper_with_only_output_anchors_on_first_line(self):
|
|
152
|
+
self.assertEqual(
|
|
153
|
+
proxy._error_signature(WRAPPER_WITH_OUTPUT), "compiling main.c"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def test_unrelated_wrapper_failures_stop_sharing_a_signature(self):
|
|
157
|
+
# The 2026-10-18 incident: distinct failures manufactured one streak.
|
|
158
|
+
a = proxy._error_signature(WRAPPER_WITH_OUTPUT)
|
|
159
|
+
b = proxy._error_signature(
|
|
160
|
+
"error: command failed (exit code: 1)\nLinking libfoo.so"
|
|
161
|
+
)
|
|
162
|
+
self.assertNotEqual(a, b)
|
|
163
|
+
# ...while the identical failing call keeps a stable one, so LOCKSTEP
|
|
164
|
+
# stays armed for real lockstep loops.
|
|
165
|
+
self.assertEqual(
|
|
166
|
+
proxy._error_signature(WRAPPER_WITH_OUTPUT),
|
|
167
|
+
proxy._error_signature(WRAPPER_WITH_OUTPUT),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class EmbeddedTokenTests(unittest.TestCase):
|
|
172
|
+
def test_status_listing_with_error_slug_is_not_a_failure(self):
|
|
173
|
+
# The exact shape of the 2026-10-08 incident.
|
|
174
|
+
self.assertEqual(proxy._error_signature(WORKTREE_STATUS), "")
|
|
175
|
+
|
|
176
|
+
def test_real_error_lines_survive_the_embedded_filter(self):
|
|
177
|
+
combined = (
|
|
178
|
+
WORKTREE_STATUS
|
|
179
|
+
+ "\npsql: error: connection to server failed: fe_sendauth: "
|
|
180
|
+
"no password supplied"
|
|
181
|
+
)
|
|
182
|
+
self.assertTrue(proxy._error_signature(combined).startswith("psql: "))
|
|
183
|
+
self.assertNotEqual(
|
|
184
|
+
proxy._error_signature("FATAL: password authentication failed"), ""
|
|
185
|
+
)
|
|
186
|
+
self.assertNotEqual(
|
|
187
|
+
proxy._error_signature("error: relation does not exist"), ""
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class ReadOnlyContentTests(unittest.TestCase):
|
|
192
|
+
"""A read whose client never declared is_error is file CONTENT (ledger
|
|
193
|
+
2026-10-17: a shell script's die() helper made a healthy Read fail)."""
|
|
194
|
+
|
|
195
|
+
def test_read_source_without_flag_forms_no_signature(self):
|
|
196
|
+
m = proxy.SessionMonitor()
|
|
197
|
+
for _ in range(4):
|
|
198
|
+
m.note_tool_result_error(SHELL_SOURCE_READ, None, tool_names=["read"])
|
|
199
|
+
self.assertEqual(m.error_signature_streak, 0)
|
|
200
|
+
self.assertEqual(m.last_error_signature, "")
|
|
201
|
+
|
|
202
|
+
def test_declared_error_on_read_still_counts(self):
|
|
203
|
+
m = proxy.SessionMonitor()
|
|
204
|
+
m.note_tool_result_error(SHELL_SOURCE_READ, True, tool_names=["read"])
|
|
205
|
+
self.assertEqual(m.error_signature_streak, 1)
|
|
206
|
+
|
|
207
|
+
def test_bash_without_flag_keeps_heuristics(self):
|
|
208
|
+
m = proxy.SessionMonitor()
|
|
209
|
+
m.note_tool_result_error(SHELL_SOURCE_READ, None, tool_names=["bash"])
|
|
210
|
+
self.assertEqual(m.error_signature_streak, 1)
|
|
211
|
+
|
|
212
|
+
def test_unknown_tool_name_keeps_heuristics(self):
|
|
213
|
+
# A result whose producing tool cannot be named behaves as before.
|
|
214
|
+
m = proxy.SessionMonitor()
|
|
215
|
+
m.note_tool_result_error(SHELL_SOURCE_READ, None, tool_names=[""])
|
|
216
|
+
self.assertEqual(m.error_signature_streak, 1)
|
|
217
|
+
|
|
218
|
+
def test_ledger_read_incident_never_arms_the_guard(self):
|
|
219
|
+
m = proxy.SessionMonitor()
|
|
220
|
+
for _ in range(4):
|
|
221
|
+
m.note_tool_result_error(SHELL_SOURCE_READ, None, tool_names=["read"])
|
|
222
|
+
body = {"messages": [{"role": "user", "content": "go"}], "tool_choice": "auto"}
|
|
223
|
+
proxy._maybe_inject_error_loop_break(body, m)
|
|
224
|
+
self.assertEqual(m.error_loop_fires, 0)
|
|
225
|
+
self.assertEqual(len(body["messages"]), 1)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class KnownFixCoverageTests(unittest.TestCase):
|
|
229
|
+
"""The uncovered ledger classes now carry a KNOWN FIX."""
|
|
230
|
+
|
|
231
|
+
def test_page_not_found(self):
|
|
232
|
+
fix = proxy.loop_class_fix('{"error": "page not found."}')
|
|
233
|
+
self.assertIn("404", fix)
|
|
234
|
+
self.assertIn("CANNOT work", fix)
|
|
235
|
+
|
|
236
|
+
def test_psql_no_password(self):
|
|
237
|
+
fix = proxy.loop_class_fix(WRAPPER_WITH_STDERR)
|
|
238
|
+
self.assertIn("CANNOT work", fix)
|
|
239
|
+
self.assertIn("Do NOT guess passwords", fix)
|
|
240
|
+
|
|
241
|
+
def test_missing_pgcrypto(self):
|
|
242
|
+
fix = proxy.loop_class_fix(
|
|
243
|
+
"error: function gen_random_bytes(integer) does not exist"
|
|
244
|
+
)
|
|
245
|
+
self.assertIn("pgcrypto", fix)
|
|
246
|
+
|
|
247
|
+
def test_decisive_traceback_signature_gets_no_invented_fix(self):
|
|
248
|
+
self.assertEqual(proxy.loop_class_fix("valueerror: boom"), "")
|
|
249
|
+
|
|
250
|
+
def test_original_classes_still_covered(self):
|
|
251
|
+
self.assertIn(
|
|
252
|
+
"byte-for-byte",
|
|
253
|
+
proxy.loop_class_fix(
|
|
254
|
+
"error: edit could not find oldstring in the file. it must match"
|
|
255
|
+
),
|
|
256
|
+
)
|
|
257
|
+
self.assertIn(
|
|
258
|
+
"does not exist",
|
|
259
|
+
proxy.loop_class_fix(
|
|
260
|
+
'skill "uap-router__react" not found. available skills: adapt'
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class SecretRedactionTests(unittest.TestCase):
|
|
266
|
+
"""Signatures flow into ledger records and tail directives the model
|
|
267
|
+
reads, and error text is UNTRUSTED tool output: credential-shaped
|
|
268
|
+
material must be redacted before it can anchor a signature (security
|
|
269
|
+
review 2026-10-18)."""
|
|
270
|
+
|
|
271
|
+
def test_key_value_secret_never_survives(self):
|
|
272
|
+
sig = proxy._error_signature("error: password=hunter2 rejected")
|
|
273
|
+
self.assertNotIn("hunter2", sig)
|
|
274
|
+
self.assertIn("<secret>", sig)
|
|
275
|
+
|
|
276
|
+
def test_aws_access_key_never_survives(self):
|
|
277
|
+
# Built at runtime: the AWS-docs example key must not sit as one whole
|
|
278
|
+
# literal in the source or the repo's own secret scanner flags the
|
|
279
|
+
# test itself (prepass [aws-access-key], 2026-10-18).
|
|
280
|
+
aws_key = "AKIA" + "IOSFODNN7EXAMPLE"
|
|
281
|
+
sig = proxy._error_signature("error: invalid access key " + aws_key)
|
|
282
|
+
self.assertNotIn(aws_key.lower(), sig)
|
|
283
|
+
self.assertIn("<secret>", sig)
|
|
284
|
+
|
|
285
|
+
def test_url_credentials_never_survive(self):
|
|
286
|
+
sig = proxy._error_signature(
|
|
287
|
+
"fatal: https://admin:hunter2@db.internal:5432 refused"
|
|
288
|
+
)
|
|
289
|
+
self.assertNotIn("hunter2", sig)
|
|
290
|
+
self.assertNotIn("admin", sig)
|
|
291
|
+
|
|
292
|
+
def test_high_entropy_token_never_survives(self):
|
|
293
|
+
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
|
|
294
|
+
sig = proxy._error_signature("auth failed: bearer " + token)
|
|
295
|
+
self.assertNotIn(token.lower(), sig)
|
|
296
|
+
self.assertIn("<secret>", sig)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class ClockBannerTests(unittest.TestCase):
|
|
300
|
+
"""A clock-banner prefix normalizes to a shared "#:#:#" shape that
|
|
301
|
+
unrelated runs would collide on; _first_meaningful_signature strips it
|
|
302
|
+
rather than keying on it (code-quality review 2026-10-18)."""
|
|
303
|
+
|
|
304
|
+
def test_banner_prefix_is_stripped_not_keyed_on(self):
|
|
305
|
+
sig = proxy._first_meaningful_signature(
|
|
306
|
+
"[12:34:56] error: command failed (exit code: 1)\n"
|
|
307
|
+
"[12:34:57] connection refused by peer"
|
|
308
|
+
)
|
|
309
|
+
self.assertEqual(sig, "connection refused by peer")
|
|
310
|
+
|
|
311
|
+
def test_a_line_that_is_only_a_banner_names_nothing(self):
|
|
312
|
+
self.assertEqual(proxy._first_meaningful_signature("[12:34:56]"), "")
|
|
313
|
+
self.assertEqual(proxy._first_meaningful_signature("12:34:56.789"), "")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class DeclaredFlagWrapperTests(unittest.TestCase):
|
|
317
|
+
"""The flagged-path fallback must route through the same first-meaningful
|
|
318
|
+
ladder: a declared wrapper-only failure is unnameable and must NOT
|
|
319
|
+
re-manufacture a content-free signature (code-quality review 2026-10-18,
|
|
320
|
+
probed live)."""
|
|
321
|
+
|
|
322
|
+
def test_declared_flag_on_wrapper_only_forms_no_signature(self):
|
|
323
|
+
m = proxy.SessionMonitor()
|
|
324
|
+
m.note_tool_result_error(WRAPPER_ONLY, True, tool_names=["bash"])
|
|
325
|
+
self.assertEqual(m.error_signature_streak, 0)
|
|
326
|
+
self.assertEqual(m.last_error_signature, "")
|
|
327
|
+
|
|
328
|
+
def test_declared_flag_on_wrapper_with_stderr_anchors_on_stderr(self):
|
|
329
|
+
m = proxy.SessionMonitor()
|
|
330
|
+
m.note_tool_result_error(WRAPPER_WITH_STDERR, True, tool_names=["read"])
|
|
331
|
+
self.assertEqual(m.error_signature_streak, 1)
|
|
332
|
+
self.assertTrue(m.last_error_signature.startswith("psql: "))
|
|
333
|
+
|
|
334
|
+
def test_declared_flag_on_wrapper_with_output_anchors_on_output(self):
|
|
335
|
+
m = proxy.SessionMonitor()
|
|
336
|
+
m.note_tool_result_error(WRAPPER_WITH_OUTPUT, True, tool_names=["bash"])
|
|
337
|
+
self.assertEqual(m.last_error_signature, "compiling main.c")
|
|
338
|
+
|
|
339
|
+
def test_declared_unnameable_failure_is_still_owned_by_doubling(self):
|
|
340
|
+
# No signature streak is correct -- but the identical-call shape must
|
|
341
|
+
# stay counted somewhere (architect review 2026-10-18).
|
|
342
|
+
m = proxy.SessionMonitor()
|
|
343
|
+
m.note_doubling_signal("Bash:abcd1234", WRAPPER_ONLY, result_error=True)
|
|
344
|
+
m.note_doubling_signal("Bash:abcd1234", WRAPPER_ONLY, result_error=True)
|
|
345
|
+
self.assertEqual(m.doubling_streak, 2)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
if __name__ == "__main__":
|
|
349
|
+
unittest.main()
|
|
@@ -16,13 +16,23 @@ import sys
|
|
|
16
16
|
import unittest
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
|
+
# Shared resolution: the data layer lives at the MAIN checkout root (shared
|
|
20
|
+
# across worktrees) — same module the CLI itself uses.
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "UAP"))
|
|
22
|
+
from paths import main_checkout_root
|
|
23
|
+
|
|
19
24
|
|
|
20
25
|
class TestUAPCompliance(unittest.TestCase):
|
|
21
26
|
"""Test suite for UAP protocol compliance."""
|
|
22
27
|
|
|
23
28
|
def setUp(self):
|
|
24
29
|
"""Setup test fixtures."""
|
|
25
|
-
|
|
30
|
+
# The pre-reorg depth arithmetic resolved to tools/ (pointing the
|
|
31
|
+
# suite at a nonexistent tools/agents/data/); and a worktree copy
|
|
32
|
+
# of this file used to look inside the worktree, where no data
|
|
33
|
+
# exists. Resolve the MAIN checkout root instead — the shared data
|
|
34
|
+
# layer the session-start hook and the TS CLI both use.
|
|
35
|
+
self.project_root = main_checkout_root(Path(__file__))
|
|
26
36
|
self.db_path = self.project_root / "agents/data/memory/short_term.db"
|
|
27
37
|
self.coord_db_path = (
|
|
28
38
|
self.project_root / "agents/data/coordination/coordination.db"
|