@miller-tech/uap 1.221.0 → 2.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.
- package/README.md +19 -9
- package/config/exl3-profiles/qwen38-27b-exl3.env +68 -0
- package/config/llama-profiles/qwen38-27b-dflash2.env +6 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/benchmarks/paired/adapter.d.ts +15 -0
- package/dist/benchmarks/paired/adapter.d.ts.map +1 -1
- package/dist/benchmarks/paired/adapter.js +22 -1
- package/dist/benchmarks/paired/adapter.js.map +1 -1
- package/dist/bin/cli.js +20 -24
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/deliver.d.ts +34 -0
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +147 -20
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/guided-setup.d.ts.map +1 -1
- package/dist/cli/guided-setup.js +15 -0
- package/dist/cli/guided-setup.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +3 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +8 -2
- package/dist/cli/plan.js.map +1 -1
- package/dist/cli/policy.d.ts.map +1 -1
- package/dist/cli/policy.js +104 -2
- package/dist/cli/policy.js.map +1 -1
- package/dist/cli/quality.d.ts +11 -0
- package/dist/cli/quality.d.ts.map +1 -0
- package/dist/cli/quality.js +213 -0
- package/dist/cli/quality.js.map +1 -0
- package/dist/cli/wizard-config.d.ts.map +1 -1
- package/dist/cli/wizard-config.js +19 -9
- package/dist/cli/wizard-config.js.map +1 -1
- package/dist/cli/worktree.d.ts.map +1 -1
- package/dist/cli/worktree.js +8 -1
- package/dist/cli/worktree.js.map +1 -1
- package/dist/dashboard/savings.d.ts +1 -4
- package/dist/dashboard/savings.d.ts.map +1 -1
- package/dist/dashboard/savings.js +2 -52
- package/dist/dashboard/savings.js.map +1 -1
- package/dist/delivery/acceptance-judge.d.ts +10 -0
- package/dist/delivery/acceptance-judge.d.ts.map +1 -1
- package/dist/delivery/acceptance-judge.js +45 -3
- package/dist/delivery/acceptance-judge.js.map +1 -1
- package/dist/delivery/agentic-executor.d.ts +12 -1
- package/dist/delivery/agentic-executor.d.ts.map +1 -1
- package/dist/delivery/agentic-executor.js +25 -5
- package/dist/delivery/agentic-executor.js.map +1 -1
- package/dist/delivery/bash-sweep.d.ts +12 -0
- package/dist/delivery/bash-sweep.d.ts.map +1 -1
- package/dist/delivery/bash-sweep.js +8 -1
- package/dist/delivery/bash-sweep.js.map +1 -1
- package/dist/delivery/convergence-loop.d.ts.map +1 -1
- package/dist/delivery/convergence-loop.js +6 -4
- package/dist/delivery/convergence-loop.js.map +1 -1
- package/dist/delivery/execution-gate.d.ts +1 -1
- package/dist/delivery/execution-gate.d.ts.map +1 -1
- package/dist/delivery/execution-gate.js +248 -5
- package/dist/delivery/execution-gate.js.map +1 -1
- package/dist/delivery/snapshot.d.ts +16 -0
- package/dist/delivery/snapshot.d.ts.map +1 -1
- package/dist/delivery/snapshot.js +113 -2
- package/dist/delivery/snapshot.js.map +1 -1
- package/dist/delivery/user-validation.d.ts.map +1 -1
- package/dist/delivery/user-validation.js +55 -10
- package/dist/delivery/user-validation.js.map +1 -1
- package/dist/delivery/verifier-ladder.d.ts +24 -0
- package/dist/delivery/verifier-ladder.d.ts.map +1 -1
- package/dist/delivery/verifier-ladder.js +119 -2
- package/dist/delivery/verifier-ladder.js.map +1 -1
- package/dist/delivery/visual-gate.d.ts +24 -0
- package/dist/delivery/visual-gate.d.ts.map +1 -1
- package/dist/delivery/visual-gate.js +70 -9
- package/dist/delivery/visual-gate.js.map +1 -1
- package/dist/generators/claude-md.js +17 -4
- package/dist/generators/claude-md.js.map +1 -1
- package/dist/policies/liveness.d.ts +105 -0
- package/dist/policies/liveness.d.ts.map +1 -0
- package/dist/policies/liveness.js +251 -0
- package/dist/policies/liveness.js.map +1 -0
- package/dist/policies/policy-memory.d.ts.map +1 -1
- package/dist/policies/policy-memory.js +3 -4
- package/dist/policies/policy-memory.js.map +1 -1
- package/dist/quality/baseline.d.ts +29 -0
- package/dist/quality/baseline.d.ts.map +1 -0
- package/dist/quality/baseline.js +79 -0
- package/dist/quality/baseline.js.map +1 -0
- package/dist/quality/complexity.d.ts +38 -0
- package/dist/quality/complexity.d.ts.map +1 -0
- package/dist/quality/complexity.js +239 -0
- package/dist/quality/complexity.js.map +1 -0
- package/dist/quality/config.d.ts +40 -0
- package/dist/quality/config.d.ts.map +1 -0
- package/dist/quality/config.js +77 -0
- package/dist/quality/config.js.map +1 -0
- package/dist/quality/crap.d.ts +11 -0
- package/dist/quality/crap.d.ts.map +1 -0
- package/dist/quality/crap.js +54 -0
- package/dist/quality/crap.js.map +1 -0
- package/dist/quality/languages.d.ts +25 -0
- package/dist/quality/languages.d.ts.map +1 -0
- package/dist/quality/languages.js +150 -0
- package/dist/quality/languages.js.map +1 -0
- package/dist/quality/report.d.ts +38 -0
- package/dist/quality/report.d.ts.map +1 -0
- package/dist/quality/report.js +251 -0
- package/dist/quality/report.js.map +1 -0
- package/dist/quality/scanner.d.ts +32 -0
- package/dist/quality/scanner.d.ts.map +1 -0
- package/dist/quality/scanner.js +127 -0
- package/dist/quality/scanner.js.map +1 -0
- package/dist/quality/tools.d.ts +28 -0
- package/dist/quality/tools.d.ts.map +1 -0
- package/dist/quality/tools.js +189 -0
- package/dist/quality/tools.js.map +1 -0
- package/dist/types/config.d.ts +148 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +35 -0
- package/dist/types/config.js.map +1 -1
- package/docs/ATTRIBUTION.md +0 -1
- package/docs/INDEX.md +38 -4
- package/docs/analysis/MARKET_POSITION.md +179 -0
- package/docs/architecture/OVERVIEW.md +82 -18
- package/docs/architecture/PROTOCOL.md +1 -1
- package/docs/benchmarks/PAIRED_FINDINGS.md +106 -0
- package/docs/benchmarks/PAIRED_HARNESS.md +16 -0
- package/docs/benchmarks/README.md +59 -0
- package/docs/design/UAP_REACTOR.md +0 -1
- package/docs/getting-started/INSTALLATION.md +1 -1
- package/docs/guides/AUTOMATIC.md +0 -14
- package/docs/guides/AUTOMATIC_FEATURES.md +1 -9
- package/docs/guides/COORDINATION.md +1 -1
- package/docs/guides/DELIVERY_PIPELINE.md +5 -0
- package/docs/guides/DEPLOY_BATCHING.md +1 -1
- package/docs/guides/DROIDS_AND_SKILLS.md +1 -1
- package/docs/guides/LOCAL_MODELS.md +6 -1
- package/docs/guides/MCP_ROUTER.md +1 -1
- package/docs/guides/MEMORY.md +1 -1
- package/docs/guides/MULTI_MODEL.md +1 -1
- package/docs/guides/ORCHESTRATOR.md +1 -1
- package/docs/guides/POLICIES.md +19 -3
- package/docs/guides/QWEN38_EXL3.md +114 -0
- package/docs/guides/WORKTREE_WORKFLOW.md +45 -1
- package/docs/integrations/MCP_ROUTER.md +1 -4
- package/docs/plans/deliver-hardening-plan-2026-07-13.md +173 -0
- package/docs/plans/deliver-hardening-review-response-2026-07-13.md +180 -0
- package/docs/plans/k3-uap-uplift-validation.md +216 -0
- package/docs/plans/qwen38-uap-retest-plan.md +196 -0
- package/docs/reference/API.md +1 -1
- package/docs/reference/CLI.md +49 -14
- package/docs/reference/COMPARISON.md +248 -0
- package/docs/reference/CONFIGURATION.md +1 -3
- package/docs/reference/DATABASE_SCHEMA.md +1 -1
- package/docs/reference/FEATURES.md +3 -3
- package/docs/reference/PATTERNS.md +1 -1
- package/docs/reference/PLATFORMS.md +1 -1
- package/package.json +2 -2
- package/scripts/maintenance/update-skills.py +0 -22
- package/scripts/maintenance/verify-compliance.sh +3 -3
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
- package/src/policies/enforcers/adr_guard.py +0 -0
- package/src/policies/enforcers/bearer_lockdown.py +0 -0
- package/src/policies/enforcers/branch_freshness.py +0 -0
- package/src/policies/enforcers/design_token_gate.py +0 -0
- package/src/policies/enforcers/enforcement_self_protect.py +70 -0
- package/src/policies/enforcers/expert_review_required.py +22 -0
- package/src/policies/enforcers/iac_plan_destruction_check.py +0 -0
- package/src/policies/enforcers/quality_metrics_gate.py +447 -0
- package/src/policies/enforcers/ship_loop_gate.py +0 -0
- package/src/policies/enforcers/validate_plan_on_change.py +0 -0
- package/src/policies/enforcers/visual_verification.py +0 -0
- package/src/policies/enforcers/workdir_scope.py +0 -0
- package/src/policies/schemas/policies/expert-review-required.md +9 -1
- package/src/policies/schemas/policies/quality-metrics-gate.md +74 -0
- package/templates/CLAUDE.template.md +1 -1
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/templates/hooks/pre-tool-use-bash.sh +50 -3
- package/templates/hooks/pre-tool-use-edit-write.sh +31 -0
- package/templates/hooks/session-start.sh +15 -0
- package/templates/hooks/uap-policy-gate.sh +187 -5
- package/templates/stryker.config.json +14 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +254 -28
- package/tools/agents/tests/run_acceptance.py +0 -0
- package/tools/agents/tests/test_client_disconnect.py +0 -0
- package/tools/agents/tests/test_cycle_break_exploration.py +0 -0
- package/tools/agents/tests/test_deferral_break.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_all_langs.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_exemptions.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_filepath.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_web_and_bash.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_worktree.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_write_complexity.py +0 -0
- package/tools/agents/tests/test_disconnect_watcher.py +0 -0
- package/tools/agents/tests/test_doubling_break.py +0 -0
- package/tools/agents/tests/test_empty_maxtokens_recovery.py +0 -0
- package/tools/agents/tests/test_empty_tool_loop_break.py +0 -0
- package/tools/agents/tests/test_enforcement_self_protect.py +90 -0
- package/tools/agents/tests/test_enforcer_escape_hatches.py +0 -0
- package/tools/agents/tests/test_error_loop_break.py +0 -0
- package/tools/agents/tests/test_expert_review_pr_scope.py +13 -5
- package/tools/agents/tests/test_fastpath_config.py +0 -0
- package/tools/agents/tests/test_finalize_suppression.py +0 -0
- package/tools/agents/tests/test_gate_evidence.py +0 -0
- package/tools/agents/tests/test_gate_failclosed_indirection.py +0 -0
- package/tools/agents/tests/test_gate_integrity.py +0 -0
- package/tools/agents/tests/test_live_deliver_kill.py +0 -0
- package/tools/agents/tests/test_malformed_unclosed_think.py +0 -0
- package/tools/agents/tests/test_mandate_beats_recon.py +0 -0
- package/tools/agents/tests/test_mandate_deliver.py +0 -0
- package/tools/agents/tests/test_non_stream_resp_log.py +0 -0
- package/tools/agents/tests/test_output_token_ceilings.py +0 -0
- package/tools/agents/tests/test_overflow_truncate_count_tokens.py +0 -0
- package/tools/agents/tests/test_path_containment.py +0 -0
- package/tools/agents/tests/test_path_normalizer_hardened.py +0 -0
- package/tools/agents/tests/test_repeat_call_guard.py +0 -0
- package/tools/agents/tests/test_repeat_guard.py +0 -0
- package/tools/agents/tests/test_session_admission.py +0 -0
- package/tools/agents/tests/test_stream_heartbeat.py +0 -0
- package/tools/agents/tests/test_stream_thinking_deltas.py +405 -0
- package/tools/agents/tests/test_stuck_break_hard.py +0 -0
- package/tools/agents/tests/test_stuck_break_reattach.py +0 -0
- package/tools/agents/tests/test_turn_count_breaker_periodic.py +0 -0
- package/tools/agents/tests/test_upstream_chokepoint.py +0 -0
- package/tools/agents/tests/test_upstream_model_aliases.py +0 -0
- package/tools/agents/tests/test_validate_plan_gate.py +0 -0
- package/tools/agents/tests/test_vision_passthrough.py +0 -0
- package/tools/agents/tests/test_workdir_scope_enforcer.py +0 -0
- package/dist/cli/rtk.d.ts +0 -34
- package/dist/cli/rtk.d.ts.map +0 -1
- package/dist/cli/rtk.js +0 -401
- package/dist/cli/rtk.js.map +0 -1
- package/docs/integrations/RTK.md +0 -113
- package/scripts/setup/install-rtk.sh +0 -184
- package/src/policies/enforcers/rtk_wrap.py +0 -205
- package/src/policies/schemas/policies/rtk-wrap.md +0 -50
- package/tools/agents/tests/test_rtk_wrap.py +0 -151
|
@@ -1172,6 +1172,40 @@ PROXY_DISABLE_THINKING_ON_TOOL_TURNS = os.environ.get(
|
|
|
1172
1172
|
"off",
|
|
1173
1173
|
"no",
|
|
1174
1174
|
}
|
|
1175
|
+
# Stream upstream `reasoning_content` deltas to the client as Anthropic
|
|
1176
|
+
# `thinking` blocks on the TRUE streaming path. Default off: reasoning is
|
|
1177
|
+
# chain-of-thought and the standing policy is not to leak it unless the
|
|
1178
|
+
# client opted in. Enable for clients whose stream watchdog disconnects on
|
|
1179
|
+
# silence (Factory Droid ~300 s): a slow-thinking model otherwise produces
|
|
1180
|
+
# zero client-visible events for minutes, the client aborts and retries, and
|
|
1181
|
+
# each retry regenerates the turn from scratch (observed 2026-09-05 with
|
|
1182
|
+
# Qwen3.8-27B on exllamav3: >4-min reasoning runs on tool turns, one
|
|
1183
|
+
# watchdog kill + full retry per long turn).
|
|
1184
|
+
PROXY_STREAM_THINKING_DELTAS = os.environ.get(
|
|
1185
|
+
"PROXY_STREAM_THINKING_DELTAS", "off"
|
|
1186
|
+
).lower() not in {
|
|
1187
|
+
"0",
|
|
1188
|
+
"false",
|
|
1189
|
+
"off",
|
|
1190
|
+
"no",
|
|
1191
|
+
}
|
|
1192
|
+
# Emit a one-space thinking_delta every N seconds while the upstream stream
|
|
1193
|
+
# has produced no content yet (prompt prefill) or is idle before the first
|
|
1194
|
+
# text/tool block. Motivation (2026-09-09, Qwen3.8-Flash-Next 125B-A6B on a
|
|
1195
|
+
# single RTX 3090): a 64k-token session turn prefills at ~160-390 tok/s =
|
|
1196
|
+
# 170-400 s, past the Factory Droid ~242 s stream watchdog, and message_start
|
|
1197
|
+
# / ping events do NOT reset that watchdog — only content deltas do (verified
|
|
1198
|
+
# 2026-09-05). Each kill landed mid-prefill and the retry started from token
|
|
1199
|
+
# 0: a livelock. The space-only deltas keep the client alive without altering
|
|
1200
|
+
# visible output; the thinking block they open is the same block any real
|
|
1201
|
+
# leading reasoning then streams into. Requires PROXY_STREAM_THINKING_DELTAS
|
|
1202
|
+
# (the lazy block layout); 0 disables.
|
|
1203
|
+
try:
|
|
1204
|
+
PROXY_PREFILL_HEARTBEAT_SECS = float(
|
|
1205
|
+
os.environ.get("PROXY_PREFILL_HEARTBEAT_SECS", "0")
|
|
1206
|
+
)
|
|
1207
|
+
except (TypeError, ValueError):
|
|
1208
|
+
PROXY_PREFILL_HEARTBEAT_SECS = 0.0
|
|
1175
1209
|
# Disable thinking on EVERY turn (not just tool turns). For models like Gemma 4
|
|
1176
1210
|
# that emit ~100 thinking tokens for trivial replies, this halves output cost.
|
|
1177
1211
|
PROXY_DISABLE_THINKING_ALWAYS = os.environ.get(
|
|
@@ -11815,6 +11849,16 @@ async def _heartbeat_then_buffered(produce_coro, model: str, input_tokens: int =
|
|
|
11815
11849
|
a Starlette ``Response`` (the guarded path's error returns). Since the stream
|
|
11816
11850
|
has already committed to HTTP 200, an error Response is re-emitted as an SSE
|
|
11817
11851
|
``error`` event rather than an HTTP status.
|
|
11852
|
+
|
|
11853
|
+
When PROXY_STREAM_THINKING_DELTAS and PROXY_PREFILL_HEARTBEAT_SECS are both
|
|
11854
|
+
on, the keep-alive is a ``thinking`` block at index 0 fed with one-space
|
|
11855
|
+
thinking_deltas instead of bare pings: pings and message_start do NOT reset
|
|
11856
|
+
the Factory Droid ~242 s stream watchdog, only content deltas do
|
|
11857
|
+
(2026-09-09: no-tool turns like session summarization — 51k-token prefill
|
|
11858
|
+
plus up to 8000 buffered output tokens ≈ 470 s — livelocked on this path
|
|
11859
|
+
while tool turns on the streaming relay stayed alive). The heartbeat block
|
|
11860
|
+
is closed before the buffered content streams, which is then re-indexed
|
|
11861
|
+
+1 via ``stream_anthropic_message(index_offset=1)``.
|
|
11818
11862
|
"""
|
|
11819
11863
|
msg_id = f"msg_{uuid.uuid4().hex[:24]}"
|
|
11820
11864
|
yield (
|
|
@@ -11822,6 +11866,8 @@ async def _heartbeat_then_buffered(produce_coro, model: str, input_tokens: int =
|
|
|
11822
11866
|
f"data: {json.dumps({'type': 'message_start', 'message': {'id': msg_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': model, 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': input_tokens, 'output_tokens': 0}}})}\n\n"
|
|
11823
11867
|
)
|
|
11824
11868
|
interval = PROXY_STREAM_HEARTBEAT_SECS if PROXY_STREAM_HEARTBEAT_SECS > 0 else 15.0
|
|
11869
|
+
thinking_heartbeat = PROXY_STREAM_THINKING_DELTAS and PROXY_PREFILL_HEARTBEAT_SECS > 0
|
|
11870
|
+
hb_thinking_open = False
|
|
11825
11871
|
task = asyncio.ensure_future(produce_coro)
|
|
11826
11872
|
try:
|
|
11827
11873
|
while True:
|
|
@@ -11831,7 +11877,19 @@ async def _heartbeat_then_buffered(produce_coro, model: str, input_tokens: int =
|
|
|
11831
11877
|
produced = await asyncio.wait_for(asyncio.shield(task), timeout=interval)
|
|
11832
11878
|
break
|
|
11833
11879
|
except asyncio.TimeoutError:
|
|
11834
|
-
|
|
11880
|
+
if thinking_heartbeat:
|
|
11881
|
+
if not hb_thinking_open:
|
|
11882
|
+
hb_thinking_open = True
|
|
11883
|
+
yield (
|
|
11884
|
+
f"event: content_block_start\n"
|
|
11885
|
+
f"data: {json.dumps({'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'thinking', 'thinking': '', 'signature': ''}})}\n\n"
|
|
11886
|
+
)
|
|
11887
|
+
yield (
|
|
11888
|
+
f"event: content_block_delta\n"
|
|
11889
|
+
f"data: {json.dumps({'type': 'content_block_delta', 'index': 0, 'delta': {'type': 'thinking_delta', 'thinking': ' '}})}\n\n"
|
|
11890
|
+
)
|
|
11891
|
+
else:
|
|
11892
|
+
yield 'event: ping\ndata: {"type": "ping"}\n\n'
|
|
11835
11893
|
except asyncio.CancelledError:
|
|
11836
11894
|
# Client disconnected — cancel the in-flight produce AND await its
|
|
11837
11895
|
# cleanup so httpx returns/closes the upstream connection before we
|
|
@@ -11853,6 +11911,14 @@ async def _heartbeat_then_buffered(produce_coro, model: str, input_tokens: int =
|
|
|
11853
11911
|
)
|
|
11854
11912
|
return
|
|
11855
11913
|
|
|
11914
|
+
# Close the heartbeat thinking block (if opened) before any terminal
|
|
11915
|
+
# event so the stream stays well-formed.
|
|
11916
|
+
if hb_thinking_open:
|
|
11917
|
+
yield (
|
|
11918
|
+
f"event: content_block_stop\n"
|
|
11919
|
+
f"data: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
|
|
11920
|
+
)
|
|
11921
|
+
|
|
11856
11922
|
if isinstance(produced, Response):
|
|
11857
11923
|
# Guarded path returned an error Response; re-emit as an SSE error event.
|
|
11858
11924
|
try:
|
|
@@ -11866,16 +11932,23 @@ async def _heartbeat_then_buffered(produce_coro, model: str, input_tokens: int =
|
|
|
11866
11932
|
return
|
|
11867
11933
|
|
|
11868
11934
|
# produced is the finalized Anthropic response dict — stream its content
|
|
11869
|
-
# without a second message_start (already sent above).
|
|
11870
|
-
|
|
11935
|
+
# without a second message_start (already sent above). Blocks shift +1
|
|
11936
|
+
# when the heartbeat thinking block occupies index 0.
|
|
11937
|
+
async for chunk in stream_anthropic_message(
|
|
11938
|
+
produced, emit_message_start=False, index_offset=1 if hb_thinking_open else 0
|
|
11939
|
+
):
|
|
11871
11940
|
yield chunk
|
|
11872
11941
|
|
|
11873
11942
|
|
|
11874
|
-
async def stream_anthropic_message(
|
|
11943
|
+
async def stream_anthropic_message(
|
|
11944
|
+
anthropic_resp: dict, emit_message_start: bool = True, index_offset: int = 0
|
|
11945
|
+
):
|
|
11875
11946
|
"""Stream a finalized Anthropic message as SSE events.
|
|
11876
11947
|
|
|
11877
11948
|
emit_message_start=False skips the leading message_start event for callers
|
|
11878
11949
|
(the heartbeat wrapper) that have already emitted one to start the stream.
|
|
11950
|
+
index_offset shifts every content block index — used when the caller has
|
|
11951
|
+
already occupied index 0 (the prefill-heartbeat thinking block).
|
|
11879
11952
|
"""
|
|
11880
11953
|
if emit_message_start:
|
|
11881
11954
|
message = {
|
|
@@ -11897,7 +11970,7 @@ async def stream_anthropic_message(anthropic_resp: dict, emit_message_start: boo
|
|
|
11897
11970
|
yield f"event: message_start\ndata: {json.dumps({'type': 'message_start', 'message': message})}\n\n"
|
|
11898
11971
|
|
|
11899
11972
|
content_blocks = anthropic_resp.get("content", []) or [{"type": "text", "text": ""}]
|
|
11900
|
-
block_index =
|
|
11973
|
+
block_index = index_offset
|
|
11901
11974
|
for block in content_blocks:
|
|
11902
11975
|
btype = block.get("type", "text")
|
|
11903
11976
|
if btype == "tool_use":
|
|
@@ -11976,11 +12049,24 @@ async def stream_anthropic_response(
|
|
|
11976
12049
|
f"data: {json.dumps({'type': 'message_start', 'message': {'id': msg_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': model, 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': _client_input_tokens(monitor), 'output_tokens': 0}}})}\n\n"
|
|
11977
12050
|
)
|
|
11978
12051
|
|
|
11979
|
-
#
|
|
11980
|
-
|
|
11981
|
-
|
|
11982
|
-
|
|
11983
|
-
)
|
|
12052
|
+
# Block bookkeeping. Two layouts:
|
|
12053
|
+
# - legacy (PROXY_STREAM_THINKING_DELTAS off): text block pre-opened at
|
|
12054
|
+
# index 0, tool blocks from 1. Reasoning deltas are collected but never
|
|
12055
|
+
# streamed, so a slow-thinking model is client-silent for minutes.
|
|
12056
|
+
# - lazy (flag on): blocks open in arrival order. A leading reasoning run
|
|
12057
|
+
# opens a `thinking` block and streams thinking_deltas, so the client
|
|
12058
|
+
# sees continuous activity instead of a silence its watchdog will kill.
|
|
12059
|
+
stream_thinking = PROXY_STREAM_THINKING_DELTAS
|
|
12060
|
+
next_block_index = 0 # lazy layout only; legacy uses fixed 0/1+
|
|
12061
|
+
open_block: int | None = None # currently open block (lazy layout)
|
|
12062
|
+
text_block_index: int | None = None # lazy layout; legacy hardcodes 0
|
|
12063
|
+
|
|
12064
|
+
if not stream_thinking:
|
|
12065
|
+
# content_block_start for text (index 0)
|
|
12066
|
+
yield (
|
|
12067
|
+
f"event: content_block_start\n"
|
|
12068
|
+
f"data: {json.dumps({'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'text', 'text': ''}})}\n\n"
|
|
12069
|
+
)
|
|
11984
12070
|
|
|
11985
12071
|
yield 'event: ping\ndata: {"type": "ping"}\n\n'
|
|
11986
12072
|
|
|
@@ -11989,7 +12075,7 @@ async def stream_anthropic_response(
|
|
|
11989
12075
|
|
|
11990
12076
|
# Track tool call state for streaming tool_calls
|
|
11991
12077
|
tool_calls_by_index: dict[int, dict] = {}
|
|
11992
|
-
tool_block_index = 1 # anthropic block index (0 = text)
|
|
12078
|
+
tool_block_index = 1 # anthropic block index (0 = text) — legacy layout
|
|
11993
12079
|
text_chunks: list[str] = [] # accumulate text for logging
|
|
11994
12080
|
reasoning_chunks: list[str] = [] # accumulate reasoning for fallback
|
|
11995
12081
|
|
|
@@ -12007,8 +12093,42 @@ async def stream_anthropic_response(
|
|
|
12007
12093
|
# upstream truth for telemetry and the session monitor.
|
|
12008
12094
|
upstream_usage: dict = {}
|
|
12009
12095
|
|
|
12096
|
+
# Heartbeat during upstream silence (prefill). aiter_lines() blocks with
|
|
12097
|
+
# no idle callback, so each line read is a persistent Task raced against
|
|
12098
|
+
# asyncio.wait — NOT wait_for, which cancels the inner read on timeout and
|
|
12099
|
+
# would destroy the line iterator's state mid-stream. On timeout, emit a
|
|
12100
|
+
# one-space thinking_delta (the only event class verified to reset the
|
|
12101
|
+
# Droid stream watchdog) in the leading phase, else a ping.
|
|
12102
|
+
_hb_secs = PROXY_PREFILL_HEARTBEAT_SECS if stream_thinking else 0.0
|
|
12103
|
+
_line_iter = openai_stream.aiter_lines()
|
|
12104
|
+
_pending_line: asyncio.Task | None = None
|
|
12010
12105
|
try:
|
|
12011
|
-
|
|
12106
|
+
while True:
|
|
12107
|
+
if _pending_line is None:
|
|
12108
|
+
_pending_line = asyncio.ensure_future(_line_iter.__anext__())
|
|
12109
|
+
if _hb_secs > 0:
|
|
12110
|
+
_ready, _ = await asyncio.wait({_pending_line}, timeout=_hb_secs)
|
|
12111
|
+
if not _ready:
|
|
12112
|
+
if text_block_index is None and not tool_calls_by_index:
|
|
12113
|
+
if open_block is None:
|
|
12114
|
+
open_block = next_block_index
|
|
12115
|
+
next_block_index += 1
|
|
12116
|
+
yield (
|
|
12117
|
+
f"event: content_block_start\n"
|
|
12118
|
+
f"data: {json.dumps({'type': 'content_block_start', 'index': open_block, 'content_block': {'type': 'thinking', 'thinking': '', 'signature': ''}})}\n\n"
|
|
12119
|
+
)
|
|
12120
|
+
yield (
|
|
12121
|
+
f"event: content_block_delta\n"
|
|
12122
|
+
f"data: {json.dumps({'type': 'content_block_delta', 'index': open_block, 'delta': {'type': 'thinking_delta', 'thinking': ' '}})}\n\n"
|
|
12123
|
+
)
|
|
12124
|
+
else:
|
|
12125
|
+
yield 'event: ping\ndata: {"type": "ping"}\n\n'
|
|
12126
|
+
continue
|
|
12127
|
+
try:
|
|
12128
|
+
line = await _pending_line
|
|
12129
|
+
except StopAsyncIteration:
|
|
12130
|
+
break
|
|
12131
|
+
_pending_line = None
|
|
12012
12132
|
if not line.startswith("data: "):
|
|
12013
12133
|
continue
|
|
12014
12134
|
data = line[6:].strip()
|
|
@@ -12030,14 +12150,55 @@ async def stream_anthropic_response(
|
|
|
12030
12150
|
reasoning = delta.get("reasoning_content", "")
|
|
12031
12151
|
if reasoning:
|
|
12032
12152
|
reasoning_chunks.append(reasoning)
|
|
12153
|
+
# Stream leading reasoning as a thinking block so the client's
|
|
12154
|
+
# stream watchdog sees activity during long thinking runs.
|
|
12155
|
+
# Only when it LEADS the turn (no text/tool output yet): late
|
|
12156
|
+
# interleaved reasoning keeps the legacy collect-only path
|
|
12157
|
+
# rather than churning block open/close per chunk.
|
|
12158
|
+
thinking_open = (
|
|
12159
|
+
stream_thinking
|
|
12160
|
+
and text_block_index is None
|
|
12161
|
+
and not tool_calls_by_index
|
|
12162
|
+
)
|
|
12163
|
+
if thinking_open and open_block is None:
|
|
12164
|
+
open_block = next_block_index
|
|
12165
|
+
next_block_index += 1
|
|
12166
|
+
yield (
|
|
12167
|
+
f"event: content_block_start\n"
|
|
12168
|
+
f"data: {json.dumps({'type': 'content_block_start', 'index': open_block, 'content_block': {'type': 'thinking', 'thinking': '', 'signature': ''}})}\n\n"
|
|
12169
|
+
)
|
|
12170
|
+
if thinking_open and open_block is not None:
|
|
12171
|
+
yield (
|
|
12172
|
+
f"event: content_block_delta\n"
|
|
12173
|
+
f"data: {json.dumps({'type': 'content_block_delta', 'index': open_block, 'delta': {'type': 'thinking_delta', 'thinking': reasoning}})}\n\n"
|
|
12174
|
+
)
|
|
12033
12175
|
|
|
12034
12176
|
# Handle text content deltas
|
|
12035
12177
|
if delta.get("content"):
|
|
12036
12178
|
output_tokens += 1 # rough token estimate
|
|
12037
12179
|
text_chunks.append(delta["content"])
|
|
12180
|
+
text_idx = 0
|
|
12181
|
+
if stream_thinking:
|
|
12182
|
+
# Close a leading thinking block before the text block.
|
|
12183
|
+
if open_block is not None and open_block != text_block_index:
|
|
12184
|
+
yield (
|
|
12185
|
+
f"event: content_block_stop\n"
|
|
12186
|
+
f"data: {json.dumps({'type': 'content_block_stop', 'index': open_block})}\n\n"
|
|
12187
|
+
)
|
|
12188
|
+
open_block = None
|
|
12189
|
+
if text_block_index is None:
|
|
12190
|
+
text_block_index = next_block_index
|
|
12191
|
+
next_block_index += 1
|
|
12192
|
+
if open_block is None:
|
|
12193
|
+
open_block = text_block_index
|
|
12194
|
+
yield (
|
|
12195
|
+
f"event: content_block_start\n"
|
|
12196
|
+
f"data: {json.dumps({'type': 'content_block_start', 'index': text_block_index, 'content_block': {'type': 'text', 'text': ''}})}\n\n"
|
|
12197
|
+
)
|
|
12198
|
+
text_idx = text_block_index
|
|
12038
12199
|
yield (
|
|
12039
12200
|
f"event: content_block_delta\n"
|
|
12040
|
-
f"data: {json.dumps({'type': 'content_block_delta', 'index':
|
|
12201
|
+
f"data: {json.dumps({'type': 'content_block_delta', 'index': text_idx, 'delta': {'type': 'text_delta', 'text': delta['content']}})}\n\n"
|
|
12041
12202
|
)
|
|
12042
12203
|
|
|
12043
12204
|
# Degenerate-repetition guard: stop a runaway while it is still
|
|
@@ -12077,15 +12238,28 @@ async def stream_anthropic_response(
|
|
|
12077
12238
|
tc_id = tc_delta.get("id", f"toolu_{uuid.uuid4().hex[:12]}")
|
|
12078
12239
|
fn = tc_delta.get("function", {})
|
|
12079
12240
|
initial_args = fn.get("arguments", "")
|
|
12241
|
+
if stream_thinking:
|
|
12242
|
+
this_block_index = next_block_index
|
|
12243
|
+
next_block_index += 1
|
|
12244
|
+
else:
|
|
12245
|
+
this_block_index = tool_block_index
|
|
12080
12246
|
tool_calls_by_index[tc_idx] = {
|
|
12081
12247
|
"id": tc_id,
|
|
12082
12248
|
"name": _restore_tool_name(fn.get("name", "")),
|
|
12083
12249
|
"arguments": initial_args,
|
|
12084
|
-
"block_index":
|
|
12250
|
+
"block_index": this_block_index,
|
|
12085
12251
|
}
|
|
12086
12252
|
|
|
12087
|
-
# Close text block before first
|
|
12088
|
-
|
|
12253
|
+
# Close the open text/thinking block before the first
|
|
12254
|
+
# tool block.
|
|
12255
|
+
if stream_thinking:
|
|
12256
|
+
if open_block is not None:
|
|
12257
|
+
yield (
|
|
12258
|
+
f"event: content_block_stop\n"
|
|
12259
|
+
f"data: {json.dumps({'type': 'content_block_stop', 'index': open_block})}\n\n"
|
|
12260
|
+
)
|
|
12261
|
+
open_block = None
|
|
12262
|
+
elif tool_block_index == 1:
|
|
12089
12263
|
yield (
|
|
12090
12264
|
f"event: content_block_stop\n"
|
|
12091
12265
|
f"data: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
|
|
@@ -12094,7 +12268,7 @@ async def stream_anthropic_response(
|
|
|
12094
12268
|
# Emit content_block_start for this tool_use
|
|
12095
12269
|
yield (
|
|
12096
12270
|
f"event: content_block_start\n"
|
|
12097
|
-
f"data: {json.dumps({'type': 'content_block_start', 'index':
|
|
12271
|
+
f"data: {json.dumps({'type': 'content_block_start', 'index': this_block_index, 'content_block': {'type': 'tool_use', 'id': tc_id, 'name': fn.get('name', '')}})}\n\n"
|
|
12098
12272
|
)
|
|
12099
12273
|
|
|
12100
12274
|
# Emit initial arguments fragment (e.g. "{") that
|
|
@@ -12105,7 +12279,7 @@ async def stream_anthropic_response(
|
|
|
12105
12279
|
if initial_args:
|
|
12106
12280
|
yield (
|
|
12107
12281
|
f"event: content_block_delta\n"
|
|
12108
|
-
f"data: {json.dumps({'type': 'content_block_delta', 'index':
|
|
12282
|
+
f"data: {json.dumps({'type': 'content_block_delta', 'index': this_block_index, 'delta': {'type': 'input_json_delta', 'partial_json': initial_args}})}\n\n"
|
|
12109
12283
|
)
|
|
12110
12284
|
|
|
12111
12285
|
tool_block_index += 1
|
|
@@ -12144,6 +12318,8 @@ async def stream_anthropic_response(
|
|
|
12144
12318
|
logger.error("Unexpected stream error: %s: %s", type(exc).__name__, exc)
|
|
12145
12319
|
finish_reason = "end_turn"
|
|
12146
12320
|
finally:
|
|
12321
|
+
if _pending_line is not None and not _pending_line.done():
|
|
12322
|
+
_pending_line.cancel()
|
|
12147
12323
|
if _sr_client is not None:
|
|
12148
12324
|
_inflight_dec(_sr_client)
|
|
12149
12325
|
# Detached close: a bare 'await aclose()' here is itself cancellable
|
|
@@ -12199,9 +12375,29 @@ async def stream_anthropic_response(
|
|
|
12199
12375
|
PROXY_STREAM_REASONING_FALLBACK,
|
|
12200
12376
|
)
|
|
12201
12377
|
text_chunks.append(fallback_text)
|
|
12378
|
+
fallback_idx = 0
|
|
12379
|
+
if stream_thinking:
|
|
12380
|
+
# The thinking block (if any) is still open: close it and
|
|
12381
|
+
# open the text block the fallback lands in.
|
|
12382
|
+
if open_block is not None and open_block != text_block_index:
|
|
12383
|
+
yield (
|
|
12384
|
+
f"event: content_block_stop\n"
|
|
12385
|
+
f"data: {json.dumps({'type': 'content_block_stop', 'index': open_block})}\n\n"
|
|
12386
|
+
)
|
|
12387
|
+
open_block = None
|
|
12388
|
+
if text_block_index is None:
|
|
12389
|
+
text_block_index = next_block_index
|
|
12390
|
+
next_block_index += 1
|
|
12391
|
+
if open_block is None:
|
|
12392
|
+
open_block = text_block_index
|
|
12393
|
+
yield (
|
|
12394
|
+
f"event: content_block_start\n"
|
|
12395
|
+
f"data: {json.dumps({'type': 'content_block_start', 'index': text_block_index, 'content_block': {'type': 'text', 'text': ''}})}\n\n"
|
|
12396
|
+
)
|
|
12397
|
+
fallback_idx = text_block_index
|
|
12202
12398
|
yield (
|
|
12203
12399
|
f"event: content_block_delta\n"
|
|
12204
|
-
f"data: {json.dumps({'type': 'content_block_delta', 'index':
|
|
12400
|
+
f"data: {json.dumps({'type': 'content_block_delta', 'index': fallback_idx, 'delta': {'type': 'text_delta', 'text': fallback_text}})}\n\n"
|
|
12205
12401
|
)
|
|
12206
12402
|
else:
|
|
12207
12403
|
logger.warning(
|
|
@@ -12210,10 +12406,30 @@ async def stream_anthropic_response(
|
|
|
12210
12406
|
PROXY_STREAM_REASONING_FALLBACK,
|
|
12211
12407
|
)
|
|
12212
12408
|
|
|
12213
|
-
|
|
12214
|
-
|
|
12215
|
-
|
|
12216
|
-
|
|
12409
|
+
if stream_thinking:
|
|
12410
|
+
if open_block is not None:
|
|
12411
|
+
yield (
|
|
12412
|
+
f"event: content_block_stop\n"
|
|
12413
|
+
f"data: {json.dumps({'type': 'content_block_stop', 'index': open_block})}\n\n"
|
|
12414
|
+
)
|
|
12415
|
+
open_block = None
|
|
12416
|
+
elif text_block_index is None and next_block_index == 0:
|
|
12417
|
+
# Completely empty turn: keep the legacy guarantee of at least
|
|
12418
|
+
# one (empty) text block so clients always see a well-formed
|
|
12419
|
+
# message body.
|
|
12420
|
+
yield (
|
|
12421
|
+
f"event: content_block_start\n"
|
|
12422
|
+
f"data: {json.dumps({'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'text', 'text': ''}})}\n\n"
|
|
12423
|
+
)
|
|
12424
|
+
yield (
|
|
12425
|
+
f"event: content_block_stop\n"
|
|
12426
|
+
f"data: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
|
|
12427
|
+
)
|
|
12428
|
+
else:
|
|
12429
|
+
yield (
|
|
12430
|
+
f"event: content_block_stop\n"
|
|
12431
|
+
f"data: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
|
|
12432
|
+
)
|
|
12217
12433
|
|
|
12218
12434
|
# Log response summary
|
|
12219
12435
|
accumulated_text = "".join(text_chunks)
|
|
@@ -12267,11 +12483,21 @@ async def stream_anthropic_response(
|
|
|
12267
12483
|
# We already streamed the text as-is. We cannot un-stream it,
|
|
12268
12484
|
# but we CAN close the text block, emit the recovered tool_use
|
|
12269
12485
|
# blocks, and fix the finish_reason so Claude Code sees them.
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12486
|
+
if stream_thinking:
|
|
12487
|
+
if open_block is not None:
|
|
12488
|
+
yield (
|
|
12489
|
+
f"event: content_block_stop\n"
|
|
12490
|
+
f"data: {json.dumps({'type': 'content_block_stop', 'index': open_block})}\n\n"
|
|
12491
|
+
)
|
|
12492
|
+
open_block = None
|
|
12493
|
+
xml_base = next_block_index
|
|
12494
|
+
else:
|
|
12495
|
+
yield (
|
|
12496
|
+
f"event: content_block_stop\n"
|
|
12497
|
+
f"data: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
|
|
12498
|
+
)
|
|
12499
|
+
xml_base = 1
|
|
12500
|
+
for idx, xtc in enumerate(xml_extracted, start=xml_base):
|
|
12275
12501
|
fn = xtc.get("function", {})
|
|
12276
12502
|
tc_id = xtc.get("id", f"toolu_{uuid.uuid4().hex[:12]}")
|
|
12277
12503
|
tc_name = _restore_tool_name(fn.get("name", ""))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -41,6 +41,96 @@ class SelfProtectTest(unittest.TestCase):
|
|
|
41
41
|
rc, out = run("Write", {"file_path": str(REPO / ".policy-tools/abc_delivery_enforcement.py")})
|
|
42
42
|
self.assertEqual(rc, 2)
|
|
43
43
|
|
|
44
|
+
def test_blocks_edit_to_policies_db(self):
|
|
45
|
+
# The inversion fix (F2): the enforcement registry the gates READ must
|
|
46
|
+
# be at least as protected as the artifact paths the gates demand.
|
|
47
|
+
rc, out = run("Edit", {"file_path": str(REPO / "agents/data/memory/policies.db")})
|
|
48
|
+
self.assertEqual(rc, 2)
|
|
49
|
+
self.assertFalse(out["allowed"])
|
|
50
|
+
|
|
51
|
+
def test_blocks_bash_rm_policies_db(self):
|
|
52
|
+
rc, out = run("Bash", {"command": "rm -f agents/data/memory/policies.db"})
|
|
53
|
+
self.assertEqual(rc, 2)
|
|
54
|
+
|
|
55
|
+
def test_allows_uap_policy_cli_bash(self):
|
|
56
|
+
# Policy management goes through the CLI process, not direct file ops;
|
|
57
|
+
# a command that merely RUNS `uap policy` must not trip the DB guard.
|
|
58
|
+
rc, out = run("Bash", {"command": "uap policy list"})
|
|
59
|
+
self.assertEqual(rc, 0)
|
|
60
|
+
|
|
61
|
+
def test_blocks_edit_to_operator_overrides(self):
|
|
62
|
+
# F3: the signed override file is operator-only; the "/.uap/" marker
|
|
63
|
+
# already refuses Edit/Write. Pin it so a future carve-out cannot
|
|
64
|
+
# silently reopen the self-mint path.
|
|
65
|
+
rc, out = run("Write", {"file_path": str(REPO / ".uap/operator-overrides.json")})
|
|
66
|
+
self.assertEqual(rc, 2)
|
|
67
|
+
|
|
68
|
+
def test_blocks_bash_redirect_creating_operator_overrides(self):
|
|
69
|
+
# F3: the file-op marker does not cover shell redirects — without this
|
|
70
|
+
# target entry `echo …> .uap/operator-overrides.json` self-mints the
|
|
71
|
+
# bypass the file exists to restrict to the operator.
|
|
72
|
+
rc, out = run("Bash", {"command": 'echo "{}" > .uap/operator-overrides.json'})
|
|
73
|
+
self.assertEqual(rc, 2)
|
|
74
|
+
|
|
75
|
+
def test_blocks_bash_rm_operator_overrides(self):
|
|
76
|
+
rc, out = run("Bash", {"command": "rm -f .uap/operator-overrides.json"})
|
|
77
|
+
self.assertEqual(rc, 2)
|
|
78
|
+
|
|
79
|
+
def test_blocks_bash_forging_liveness_cache(self):
|
|
80
|
+
# F1: the degrade consult trusts this file — a forged entry downgrades
|
|
81
|
+
# a blocking policy to advisory, so writing it directly must be refused.
|
|
82
|
+
# The legitimate writer is `uap policy liveness`, whose command text
|
|
83
|
+
# never names the file.
|
|
84
|
+
rc, out = run("Bash", {"command": 'echo "{}" > .uap/policy-liveness.json'})
|
|
85
|
+
self.assertEqual(rc, 2)
|
|
86
|
+
|
|
87
|
+
def test_allows_uap_policy_liveness_cli(self):
|
|
88
|
+
rc, out = run("Bash", {"command": "uap policy liveness --quiet"})
|
|
89
|
+
self.assertEqual(rc, 0)
|
|
90
|
+
|
|
91
|
+
def test_allows_reading_operator_overrides(self):
|
|
92
|
+
rc, out = run("Bash", {"command": "cat .uap/operator-overrides.json"})
|
|
93
|
+
self.assertEqual(rc, 0)
|
|
94
|
+
|
|
95
|
+
def test_blocks_python_minting_operator_overrides(self):
|
|
96
|
+
# Security review (deliver-hardening, 2026-07-13): an interpreter-
|
|
97
|
+
# mediated write carries no destructive verb and no redirect, so the
|
|
98
|
+
# redirect/verb scans miss it — and before the root-ownership check
|
|
99
|
+
# this one command minted a full bypass. Blocked on sight now.
|
|
100
|
+
rc, out = run("Bash", {"command":
|
|
101
|
+
'python3 -c \'import json; json.dump({"deliverBypass": True}, open(".uap/operator-overrides.json", "w"))\''})
|
|
102
|
+
self.assertEqual(rc, 2)
|
|
103
|
+
|
|
104
|
+
def test_blocks_python_forging_liveness_cache(self):
|
|
105
|
+
rc, out = run("Bash", {"command":
|
|
106
|
+
'python3 -c \'open(".uap/policy-liveness.json", "w").write("{}")\''})
|
|
107
|
+
self.assertEqual(rc, 2)
|
|
108
|
+
|
|
109
|
+
def test_blocks_node_writing_trust_anchor(self):
|
|
110
|
+
rc, out = run("Bash", {"command":
|
|
111
|
+
'node -e \'require("fs").writeFileSync(".uap/operator-overrides.json", "{}")\''})
|
|
112
|
+
self.assertEqual(rc, 2)
|
|
113
|
+
|
|
114
|
+
def test_blocks_bash_c_wrapped_python_mint(self):
|
|
115
|
+
# The rule fires on the full command text, so a shell wrapper does not
|
|
116
|
+
# hide the write.
|
|
117
|
+
rc, out = run("Bash", {"command":
|
|
118
|
+
'bash -c "python3 -c \'open(".uap/operator-overrides.json", "w")\'"'})
|
|
119
|
+
self.assertEqual(rc, 2)
|
|
120
|
+
|
|
121
|
+
def test_allows_python_READING_a_trust_anchor(self):
|
|
122
|
+
# Reads must stay cheap: no write primitive, no interpreter-write rule.
|
|
123
|
+
rc, out = run("Bash", {"command":
|
|
124
|
+
'python3 -c \'import json; print(json.load(open(".uap/policy-liveness.json")))\''})
|
|
125
|
+
self.assertEqual(rc, 0)
|
|
126
|
+
|
|
127
|
+
def test_allows_python_write_to_ordinary_uap_state(self):
|
|
128
|
+
# The narrow list is deliberate: runtime state under .uap/ stays
|
|
129
|
+
# writable, only the two trust anchors get the interpreter rule.
|
|
130
|
+
rc, out = run("Bash", {"command":
|
|
131
|
+
'python3 -c \'open(".uap/verify-cadence", "w").write("0")\''})
|
|
132
|
+
self.assertEqual(rc, 0)
|
|
133
|
+
|
|
44
134
|
def test_blocks_bash_setting_bypass(self):
|
|
45
135
|
rc, out = run("Bash", {"command": "export UAP_DELIVER_BYPASS=1 && echo hi"})
|
|
46
136
|
self.assertEqual(rc, 2)
|
|
File without changes
|
|
File without changes
|
|
@@ -207,9 +207,9 @@ class TestShipDetectionIsCommandPosition(unittest.TestCase):
|
|
|
207
207
|
review artifact was refused because the notes described the bug being fixed.
|
|
208
208
|
|
|
209
209
|
The risk in the fix is UNDER-detection, so the ship cases below matter more
|
|
210
|
-
than the prose ones.
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
than the prose ones. The wrapper cases especially: rtk is no longer
|
|
211
|
+
integrated in UAP, but the binary can still exist on operator machines, so
|
|
212
|
+
a verb check that stopped at the wrapper would miss wrapped ship commands.
|
|
213
213
|
"""
|
|
214
214
|
|
|
215
215
|
def setUp(self):
|
|
@@ -247,10 +247,18 @@ class TestShipDetectionIsCommandPosition(unittest.TestCase):
|
|
|
247
247
|
self.assert_ship("git push origin master")
|
|
248
248
|
|
|
249
249
|
def test_rtk_wrapped_git_is_a_ship_action(self):
|
|
250
|
-
#
|
|
251
|
-
#
|
|
250
|
+
# rtk is no longer a UAP integration, but the wrapper unwrap stays as
|
|
251
|
+
# defense-in-depth for machines where the binary remains installed.
|
|
252
252
|
self.assert_ship("rtk git push origin master")
|
|
253
253
|
|
|
254
|
+
def test_rtk_wrapped_git_with_value_option_is_a_ship_action(self):
|
|
255
|
+
# The `-C <path>` form needs the wrapper unwrap — the raw prose
|
|
256
|
+
# patterns only catch adjacent `git push`. Regression guard.
|
|
257
|
+
self.assert_ship("rtk git -C /repo push origin master")
|
|
258
|
+
|
|
259
|
+
def test_rtk_wrapped_plumbing_is_a_ship_action(self):
|
|
260
|
+
self.assert_ship("rtk git send-pack origin master")
|
|
261
|
+
|
|
254
262
|
def test_ship_action_after_a_chained_command(self):
|
|
255
263
|
self.assert_ship("cd sub && git commit -m x")
|
|
256
264
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|