deepparallel 0.5.7__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {deepparallel-0.5.7 → deepparallel-0.6.0}/PKG-INFO +1 -1
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/__init__.py +1 -1
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/agent.py +19 -5
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/cli.py +36 -2
- deepparallel-0.6.0/deepparallel/mesh.py +165 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/serve.py +148 -2
- deepparallel-0.6.0/deepparallel/session.py +132 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel.egg-info/PKG-INFO +1 -1
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel.egg-info/SOURCES.txt +3 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/pyproject.toml +1 -1
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_cli.py +5 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_serve.py +22 -16
- deepparallel-0.6.0/tests/test_serve_session.py +40 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/README.md +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/backend.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/branding.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/cockpit.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/cockpit_observe.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/cockpit_panel.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/cockpit_sim.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/config.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/crowe_id.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/dsml.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/fusion.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/licensing.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/memory.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/registry.json +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/renderer.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/research/__init__.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/research/conduit.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/research/provider.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/routing.example.json +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/routing.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/supply_chain.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/system_prompt.txt +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/__init__.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/codeast.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/edit.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/files.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/git_ops.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/mcp.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/memory.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/registry.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/sandbox.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/search.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/shell.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/vision.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/tools/web.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel/userinput.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel.egg-info/dependency_links.txt +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel.egg-info/entry_points.txt +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel.egg-info/requires.txt +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/deepparallel.egg-info/top_level.txt +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/setup.cfg +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_agent.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_backend.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_backend_chat.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_backend_stream.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_branding.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_cockpit.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_cockpit_panel.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_cockpit_sim.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_config.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_config_file.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_crowe_backend.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_crowe_gateway_backend.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_crowe_id_auth.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_crowe_payment_required.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_dsml.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_fusion.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_git_ops.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_issuer_signer.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_licensing.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_memory.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_renderer.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_research.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_research_provider.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_routing.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_spinner_color.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_supply_chain.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tool_registry.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_codeast.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_edit.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_files.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_mcp.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_sandbox.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_search.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_shell.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_vision.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_tools_web.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_userinput.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.6.0}/tests/test_userinput_paste.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepparallel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: DeepParallel - a multi-model agentic coding CLI with cross-model Guardian review, served via Crowe Logic.
|
|
5
5
|
Author-email: Michael Crowe <michael@crowelogic.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -255,16 +255,29 @@ def _supply_chain_note(name: str, args: dict) -> str | None:
|
|
|
255
255
|
return None
|
|
256
256
|
|
|
257
257
|
|
|
258
|
-
def _approved(name, args, interactive, auto_approve, renderer, guardian=None) -> bool:
|
|
258
|
+
def _approved(name, args, interactive, auto_approve, renderer, guardian=None, mesh_fn=None) -> bool:
|
|
259
259
|
forced = name in _GATED_PATH_TOOLS and _outside_cwd(args)
|
|
260
260
|
sc_note = _supply_chain_note(name, args) if name in _EDIT_TOOLS else None
|
|
261
|
-
#
|
|
262
|
-
|
|
261
|
+
# The verification mesh reviews every edit. A BUG verdict blocks the action
|
|
262
|
+
# and overrides auto-approve, exactly like a hallucinated dependency does.
|
|
263
|
+
mesh_result = None
|
|
264
|
+
if mesh_fn is not None and name in _EDIT_TOOLS:
|
|
265
|
+
try:
|
|
266
|
+
mesh_result = mesh_fn(_guardian_review_content(name, args))
|
|
267
|
+
except Exception: # noqa: BLE001 - the gate is best-effort, never fatal
|
|
268
|
+
mesh_result = None
|
|
269
|
+
blocked = bool(mesh_result and mesh_result.get("severity") == "bug")
|
|
270
|
+
# A hallucinated dependency or a BUG verdict overrides auto-approve.
|
|
271
|
+
if auto_approve and not forced and not sc_note and not blocked:
|
|
263
272
|
return True
|
|
264
273
|
if not interactive:
|
|
265
274
|
return False
|
|
266
275
|
title, detail = _describe(name, args)
|
|
267
|
-
if
|
|
276
|
+
if mesh_result is not None:
|
|
277
|
+
from deepparallel.mesh import format_panel
|
|
278
|
+
|
|
279
|
+
detail = f"{detail}\n\nVerification mesh:\n{format_panel(mesh_result)}"
|
|
280
|
+
elif guardian is not None and name in _EDIT_TOOLS:
|
|
268
281
|
verdict = _guardian_verdict(guardian, name, args)
|
|
269
282
|
if verdict:
|
|
270
283
|
detail = f"{detail}\n\nGuardian: {verdict}"
|
|
@@ -307,6 +320,7 @@ def run_agent(
|
|
|
307
320
|
max_steps: int | None = None,
|
|
308
321
|
stream: bool = False,
|
|
309
322
|
guardian=None,
|
|
323
|
+
mesh_fn=None,
|
|
310
324
|
on_event=None,
|
|
311
325
|
) -> str:
|
|
312
326
|
steps = max_steps if max_steps is not None else settings.max_steps
|
|
@@ -346,7 +360,7 @@ def run_agent(
|
|
|
346
360
|
elif "__parse_error__" in args:
|
|
347
361
|
result = json.dumps({"error": "invalid JSON arguments"})
|
|
348
362
|
elif meta.dangerous and not _approved(
|
|
349
|
-
name, args, interactive, auto_approve, renderer, guardian
|
|
363
|
+
name, args, interactive, auto_approve, renderer, guardian, mesh_fn
|
|
350
364
|
):
|
|
351
365
|
result = json.dumps({"error": "denied by user"})
|
|
352
366
|
else:
|
|
@@ -177,6 +177,22 @@ def _build_guardian(settings: Settings) -> Backend | None:
|
|
|
177
177
|
return backend_for_deployment(settings, settings.guardian_deployment)
|
|
178
178
|
|
|
179
179
|
|
|
180
|
+
def _build_mesh(settings: Settings):
|
|
181
|
+
"""The verification mesh: a parallel reviewer panel that gates every edit.
|
|
182
|
+
|
|
183
|
+
Supersedes the single guardian when available. Paid (Pro+) feature; returns
|
|
184
|
+
a callable ``mesh_fn(content) -> result`` or None on Free / when disabled.
|
|
185
|
+
"""
|
|
186
|
+
if not settings.guardian_enabled:
|
|
187
|
+
return None
|
|
188
|
+
ok, _ = licensing.check_feature("mesh")
|
|
189
|
+
if not ok:
|
|
190
|
+
return None
|
|
191
|
+
from deepparallel import mesh
|
|
192
|
+
|
|
193
|
+
return lambda content: mesh.mesh_review(settings, content)
|
|
194
|
+
|
|
195
|
+
|
|
180
196
|
def _system_prompt() -> str:
|
|
181
197
|
"""System prompt with the long-term memory index injected (recall tier 1)."""
|
|
182
198
|
return load_system_prompt() + memory.index_block()
|
|
@@ -293,7 +309,8 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
|
|
|
293
309
|
"""Interactive agentic loop: tools enabled, conversation persists. The dial
|
|
294
310
|
(/fast //fuse //escalate //deep) swaps the active fusion mode live."""
|
|
295
311
|
registry = get_registry()
|
|
296
|
-
|
|
312
|
+
mesh_fn = _build_mesh(settings)
|
|
313
|
+
guardian = None if mesh_fn else _build_guardian(settings)
|
|
297
314
|
system = _system_prompt()
|
|
298
315
|
messages: list[dict] = [{"role": "system", "content": system}]
|
|
299
316
|
mode = settings.fusion_mode if settings.fusion_mode in ("reason", "escalate") else "off"
|
|
@@ -385,6 +402,7 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
|
|
|
385
402
|
auto_approve=auto,
|
|
386
403
|
stream=True,
|
|
387
404
|
guardian=guardian,
|
|
405
|
+
mesh_fn=mesh_fn,
|
|
388
406
|
on_event=observer if cockpit_on else None,
|
|
389
407
|
)
|
|
390
408
|
if mode in ("reason", "escalate"):
|
|
@@ -518,6 +536,7 @@ def run(
|
|
|
518
536
|
renderer,
|
|
519
537
|
interactive=False,
|
|
520
538
|
auto_approve=settings.auto_approve,
|
|
539
|
+
mesh_fn=_build_mesh(settings),
|
|
521
540
|
)
|
|
522
541
|
except Exception as e: # noqa: BLE001 - surface as friendly message
|
|
523
542
|
branding.error(_translate_error(e))
|
|
@@ -614,10 +633,25 @@ def review(ctx: click.Context, as_diff: bool, path: str | None) -> None:
|
|
|
614
633
|
branding.error("provide a PATH or --diff (with a diff on stdin)")
|
|
615
634
|
sys.exit(3)
|
|
616
635
|
_require_ready(settings) # validates creds / exits if missing
|
|
636
|
+
glyphs = {"safe": branding.CHECK, "risky": "!", "bug": branding.CROSS}
|
|
637
|
+
mesh_ok, _ = licensing.check_feature("mesh")
|
|
638
|
+
if mesh_ok and settings.guardian_enabled:
|
|
639
|
+
# Pro: the full verification mesh, one independent reviewer per lens.
|
|
640
|
+
from deepparallel import mesh as _mesh
|
|
641
|
+
|
|
642
|
+
result = _mesh.mesh_review(settings, content)
|
|
643
|
+
for r in result["lenses"]:
|
|
644
|
+
g = glyphs.get(r["severity"], "?")
|
|
645
|
+
console.print(f" {g} [dim]{r['label']}[/] {r['verdict'] or '(no verdict)'}")
|
|
646
|
+
verdict = result["verdict"]
|
|
647
|
+
glyph = glyphs.get(result["severity"], "?")
|
|
648
|
+
console.print(f"[bold]{glyph} {result['severity'].upper()}[/] {verdict or '(no verdict)'}")
|
|
649
|
+
sys.exit(verdict_exit_code(verdict))
|
|
650
|
+
# Free / BYOK: a single independent reviewer.
|
|
617
651
|
guardian = backend_for_deployment(settings, settings.guardian_deployment)
|
|
618
652
|
verdict = guardian_review(guardian, content[:8000])
|
|
619
653
|
severity = verdict_severity(verdict)
|
|
620
|
-
glyph =
|
|
654
|
+
glyph = glyphs.get(severity, "?")
|
|
621
655
|
console.print(f"[bold]{glyph} {severity.upper()}[/] {verdict or '(no verdict)'}")
|
|
622
656
|
sys.exit(verdict_exit_code(verdict))
|
|
623
657
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""The verification mesh: N independent models review a proposed change in
|
|
2
|
+
parallel, then a guardian synthesizes the final gate verdict.
|
|
3
|
+
|
|
4
|
+
This generalizes the single-model ``guardian_review`` into the multi-lens panel
|
|
5
|
+
the hosted /review endpoint already uses (correctness, security, edge cases,
|
|
6
|
+
maintainability) and is the gate the agent loop consults before any mutating
|
|
7
|
+
action. It reuses the same parallel fan-out shape as ``fusion.deep_query`` and
|
|
8
|
+
emits :mod:`deepparallel.session` events so a trace view can render the panel.
|
|
9
|
+
|
|
10
|
+
Pure review: it never executes anything. It returns structured verdicts and a
|
|
11
|
+
decision (allow | hold | block) the caller enforces.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
17
|
+
|
|
18
|
+
from deepparallel import session
|
|
19
|
+
from deepparallel.backend import backend_for_deployment
|
|
20
|
+
|
|
21
|
+
# Four independent lenses, mirroring the hosted reviewer panel. Each model is
|
|
22
|
+
# asked for exactly one verdict line so the result is cheap to parse and gate.
|
|
23
|
+
_VERDICT_SHAPE = (
|
|
24
|
+
" Reply with exactly one line: 'VERDICT: safe', 'VERDICT: risky: <reason>', "
|
|
25
|
+
"or 'VERDICT: bug: <reason>'. No preamble, no markdown."
|
|
26
|
+
)
|
|
27
|
+
LENSES = (
|
|
28
|
+
{"key": "correctness", "label": "Correctness",
|
|
29
|
+
"sys": "Independently review this proposed change for correctness and logic errors."},
|
|
30
|
+
{"key": "security", "label": "Security",
|
|
31
|
+
"sys": "Independently review this proposed change for security vulnerabilities "
|
|
32
|
+
"(injection, auth, secrets, unsafe input)."},
|
|
33
|
+
{"key": "edge", "label": "Edge cases",
|
|
34
|
+
"sys": "Independently review this proposed change for edge cases and failure "
|
|
35
|
+
"modes the author likely missed."},
|
|
36
|
+
{"key": "maintainability", "label": "Maintainability",
|
|
37
|
+
"sys": "Independently review this proposed change for clarity and "
|
|
38
|
+
"maintainability problems."},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_SEVERITY_RANK = {"safe": 0, "unknown": 1, "risky": 2, "bug": 3}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def verdict_severity(verdict: str | None) -> str:
|
|
45
|
+
"""Classify a verdict string into safe | risky | bug | unknown."""
|
|
46
|
+
if not verdict:
|
|
47
|
+
return "unknown"
|
|
48
|
+
head = verdict.strip().lower()
|
|
49
|
+
for sev in ("safe", "risky", "bug"):
|
|
50
|
+
if head.startswith(sev):
|
|
51
|
+
return sev
|
|
52
|
+
return "unknown"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def mesh_decision(severity: str) -> str:
|
|
56
|
+
"""Map a severity to a gate decision: allow | hold | block."""
|
|
57
|
+
return {"bug": "block", "risky": "hold"}.get(severity, "allow")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_verdict(msg: dict | None) -> str | None:
|
|
61
|
+
if not msg:
|
|
62
|
+
return None
|
|
63
|
+
text = (msg.get("content") or msg.get("reasoning_content") or "").strip()
|
|
64
|
+
for line in text.splitlines():
|
|
65
|
+
s = line.strip()
|
|
66
|
+
if s.upper().startswith("VERDICT:"):
|
|
67
|
+
return s.split(":", 1)[1].strip()
|
|
68
|
+
return text.splitlines()[0][:120] if text else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _review_one(backend, lens: dict, content: str) -> str | None:
|
|
72
|
+
prompt = (
|
|
73
|
+
f"You are an independent code reviewer. {lens['sys']}{_VERDICT_SHAPE}\n\n"
|
|
74
|
+
f"Proposed change:\n{content}\n\nVerdict:"
|
|
75
|
+
)
|
|
76
|
+
messages = [{"role": "user", "content": prompt}]
|
|
77
|
+
for _ in range(2): # one retry: reviewers run against a sometimes-flaky API
|
|
78
|
+
try:
|
|
79
|
+
return _parse_verdict(backend.chat(messages, [], 0.0, 256))
|
|
80
|
+
except Exception: # noqa: BLE001 - one lens failing must not sink the gate
|
|
81
|
+
continue
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _reviewer_chains(settings):
|
|
86
|
+
"""One backend per lens, drawn from the configured council deployments so
|
|
87
|
+
the lenses are model-diverse; falls back to the guardian deployment."""
|
|
88
|
+
deps = list(settings.parallel_deployments) or [settings.guardian_deployment]
|
|
89
|
+
return [
|
|
90
|
+
(lens, backend_for_deployment(settings, deps[i % len(deps)]))
|
|
91
|
+
for i, lens in enumerate(LENSES)
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _guardian_synthesis(settings, content: str, lenses: list[dict]) -> str | None:
|
|
96
|
+
blocks = "\n".join(f"{r['label']}: {r['verdict'] or '(no verdict)'}" for r in lenses)
|
|
97
|
+
prompt = (
|
|
98
|
+
"Several reviewers independently checked a proposed code change; their "
|
|
99
|
+
"verdicts are below. Issue the final gate verdict, weighing the most "
|
|
100
|
+
f"severe valid concern.{_VERDICT_SHAPE}\n\n"
|
|
101
|
+
f"Reviewer verdicts:\n{blocks}\n\nThe change:\n{content}\n\nFinal verdict:"
|
|
102
|
+
)
|
|
103
|
+
messages = [{"role": "user", "content": prompt}]
|
|
104
|
+
for _ in range(2):
|
|
105
|
+
try:
|
|
106
|
+
v = _parse_verdict(backend_for_deployment(settings, settings.guardian_deployment)
|
|
107
|
+
.chat(messages, [], 0.0, 256))
|
|
108
|
+
if v:
|
|
109
|
+
return v
|
|
110
|
+
except Exception: # noqa: BLE001 - degrade to the worst lens verdict
|
|
111
|
+
break
|
|
112
|
+
return _worst_verdict(lenses)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _worst_verdict(lenses: list[dict]) -> str | None:
|
|
116
|
+
"""Fallback final verdict: the single most severe lens verdict."""
|
|
117
|
+
ranked = sorted(lenses, key=lambda r: _SEVERITY_RANK.get(r["severity"], 1), reverse=True)
|
|
118
|
+
return ranked[0]["verdict"] if ranked else None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def mesh_review(settings, content: str, *, on_event=None, max_chars: int = 8000) -> dict:
|
|
122
|
+
"""Run the parallel verification mesh over ``content``.
|
|
123
|
+
|
|
124
|
+
Returns ``{lenses, verdict, severity, decision}`` where ``lenses`` is a list
|
|
125
|
+
of ``{key, label, verdict, severity}`` and ``decision`` is allow|hold|block.
|
|
126
|
+
``on_event`` (optional) receives :mod:`deepparallel.session` events.
|
|
127
|
+
"""
|
|
128
|
+
content = (content or "")[:max_chars]
|
|
129
|
+
chains = _reviewer_chains(settings)
|
|
130
|
+
if on_event:
|
|
131
|
+
on_event(session.verify_start(content.splitlines()[0][:80] if content else "change"))
|
|
132
|
+
|
|
133
|
+
results: list[dict | None] = [None] * len(chains)
|
|
134
|
+
with ThreadPoolExecutor(max_workers=min(8, max(1, len(chains)))) as pool:
|
|
135
|
+
futures = {
|
|
136
|
+
pool.submit(_review_one, backend, lens, content): (i, lens)
|
|
137
|
+
for i, (lens, backend) in enumerate(chains)
|
|
138
|
+
}
|
|
139
|
+
for future in as_completed(futures):
|
|
140
|
+
i, lens = futures[future]
|
|
141
|
+
v = future.result()
|
|
142
|
+
sev = verdict_severity(v)
|
|
143
|
+
results[i] = {"key": lens["key"], "label": lens["label"], "verdict": v, "severity": sev}
|
|
144
|
+
if on_event:
|
|
145
|
+
on_event(session.verdict(lens["label"], v, sev))
|
|
146
|
+
|
|
147
|
+
lenses = [r for r in results if r is not None]
|
|
148
|
+
final = _guardian_synthesis(settings, content, lenses)
|
|
149
|
+
fsev = verdict_severity(final)
|
|
150
|
+
decision = mesh_decision(fsev)
|
|
151
|
+
if on_event:
|
|
152
|
+
on_event(session.gate(decision, final, fsev))
|
|
153
|
+
return {"lenses": lenses, "verdict": final, "severity": fsev, "decision": decision}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def format_panel(result: dict) -> str:
|
|
157
|
+
"""A compact multi-line rendering of a mesh result for a confirm card / CLI."""
|
|
158
|
+
glyph = {"safe": "✓", "risky": "!", "bug": "✗", "unknown": "?"}
|
|
159
|
+
lines = [
|
|
160
|
+
f" {glyph.get(r['severity'], '?')} {r['label']}: {r['verdict'] or '(no verdict)'}"
|
|
161
|
+
for r in result.get("lenses", [])
|
|
162
|
+
]
|
|
163
|
+
final = result.get("verdict") or "(no verdict)"
|
|
164
|
+
lines.append(f" = Gate [{result.get('decision', 'allow').upper()}]: {final}")
|
|
165
|
+
return "\n".join(lines)
|
|
@@ -21,12 +21,72 @@ persona/scrub layer stay identical, so the IDE never has to change.
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
23
|
import json
|
|
24
|
+
import os
|
|
24
25
|
import re
|
|
25
26
|
import time
|
|
27
|
+
import uuid
|
|
26
28
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
29
|
+
from pathlib import Path
|
|
27
30
|
|
|
28
|
-
from deepparallel
|
|
31
|
+
from deepparallel import memory, session as S
|
|
32
|
+
from deepparallel.backend import backend_for_deployment, resolve_backend
|
|
29
33
|
from deepparallel.config import load_system_prompt, resolve_settings
|
|
34
|
+
from deepparallel.renderer import Renderer
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _sessions_dir() -> Path:
|
|
38
|
+
d = Path(os.environ.get("DEEPPARALLEL_SESSIONS_DIR", "~/.config/deepparallel/sessions")).expanduser()
|
|
39
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
return d
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SessionRenderer(Renderer):
|
|
44
|
+
"""Maps the agent loop's renderer calls onto the canonical session schema,
|
|
45
|
+
so /v1/session emits the exact same events as the in-process run."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, send):
|
|
48
|
+
self._send = send # send(event_dict) -> stamps seq + writes SSE
|
|
49
|
+
self._step = 0
|
|
50
|
+
self._last_tool = "tool"
|
|
51
|
+
|
|
52
|
+
def answer(self, text: str) -> None:
|
|
53
|
+
if text:
|
|
54
|
+
self._send(S.answer(text))
|
|
55
|
+
|
|
56
|
+
def answer_stream(self, chunks) -> str:
|
|
57
|
+
full = ""
|
|
58
|
+
for c in chunks:
|
|
59
|
+
full += c
|
|
60
|
+
self._send(S.token(c))
|
|
61
|
+
return full
|
|
62
|
+
|
|
63
|
+
def tool_start(self, name: str, args_preview: str) -> None:
|
|
64
|
+
self._step += 1
|
|
65
|
+
self._last_tool = name
|
|
66
|
+
self._send(S.step(self._step, intent=name))
|
|
67
|
+
self._send(S.tool_call(name, args_preview))
|
|
68
|
+
|
|
69
|
+
def tool_result(self, ok: bool, summary: str, duration_s: float) -> None:
|
|
70
|
+
self._send(S.tool_result(self._last_tool, summary, ok))
|
|
71
|
+
self._send(S.step_done(self._step))
|
|
72
|
+
|
|
73
|
+
def confirm(self, action_title: str, detail: str) -> bool:
|
|
74
|
+
# Non-interactive: auto-approve. The mesh gate (a BUG verdict) is what
|
|
75
|
+
# actually blocks an edit, and it streams verdict/gate events of its own.
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
def error(self, msg: str) -> None:
|
|
79
|
+
self._send(S.error(msg))
|
|
80
|
+
|
|
81
|
+
def reasoning(self, text: str) -> None:
|
|
82
|
+
if text:
|
|
83
|
+
self._send(S.token(text, channel="reasoning"))
|
|
84
|
+
|
|
85
|
+
def welcome(self, *args, **kwargs) -> None:
|
|
86
|
+
pass # no banner on the wire
|
|
87
|
+
|
|
88
|
+
def attribution_footer(self, *args, **kwargs) -> None:
|
|
89
|
+
pass # no footer on the wire
|
|
30
90
|
from deepparallel.dsml import DSMLStreamFilter, strip_dsml
|
|
31
91
|
from deepparallel.routing import (
|
|
32
92
|
RoutingTable,
|
|
@@ -262,10 +322,14 @@ def _make_handler(
|
|
|
262
322
|
if self.path.rstrip("/") in ("/health", "/healthz"):
|
|
263
323
|
self._json(200, {"status": "ok"})
|
|
264
324
|
return
|
|
325
|
+
if self.path.startswith("/v1/session/"):
|
|
326
|
+
self._replay_session(self.path.rstrip("/").rsplit("/", 1)[-1])
|
|
327
|
+
return
|
|
265
328
|
self._json(404, {"error": {"message": "not found"}})
|
|
266
329
|
|
|
267
330
|
def do_POST(self):
|
|
268
|
-
|
|
331
|
+
path = self.path.rstrip("/")
|
|
332
|
+
if path not in ("/v1/chat/completions", "/v1/session"):
|
|
269
333
|
self._json(404, {"error": {"message": "not found"}})
|
|
270
334
|
return
|
|
271
335
|
length = int(self.headers.get("Content-Length", 0))
|
|
@@ -274,6 +338,9 @@ def _make_handler(
|
|
|
274
338
|
except json.JSONDecodeError:
|
|
275
339
|
self._json(400, {"error": {"message": "invalid JSON body"}})
|
|
276
340
|
return
|
|
341
|
+
if path == "/v1/session":
|
|
342
|
+
self._run_session(req)
|
|
343
|
+
return
|
|
277
344
|
|
|
278
345
|
model = req.get("model") or "crowelm-apex"
|
|
279
346
|
# Tier routing: principal tier from the bearer (paid) or its absence
|
|
@@ -357,6 +424,85 @@ def _make_handler(
|
|
|
357
424
|
},
|
|
358
425
|
)
|
|
359
426
|
|
|
427
|
+
def _run_session(self, req):
|
|
428
|
+
"""Run a full plan->act->verify agent session and stream the canonical
|
|
429
|
+
session.py event schema as SSE. The first end-to-end session over HTTP."""
|
|
430
|
+
prompt = str(req.get("prompt") or req.get("input") or "").strip()
|
|
431
|
+
if not prompt:
|
|
432
|
+
self._json(400, {"error": {"message": "missing 'prompt'"}})
|
|
433
|
+
return
|
|
434
|
+
from deepparallel import licensing, mesh as M
|
|
435
|
+
from deepparallel.agent import run_agent
|
|
436
|
+
from deepparallel.tools import get_registry
|
|
437
|
+
|
|
438
|
+
self.send_response(200)
|
|
439
|
+
self.send_header("Content-Type", "text/event-stream")
|
|
440
|
+
self.send_header("Cache-Control", "no-cache")
|
|
441
|
+
self.send_header("Connection", "keep-alive")
|
|
442
|
+
self.end_headers()
|
|
443
|
+
|
|
444
|
+
sid = "sess_" + uuid.uuid4().hex[:12]
|
|
445
|
+
transcript: list[dict] = []
|
|
446
|
+
|
|
447
|
+
def sink(ev):
|
|
448
|
+
transcript.append(ev)
|
|
449
|
+
self.wfile.write(S.to_sse(ev).encode())
|
|
450
|
+
self.wfile.flush()
|
|
451
|
+
|
|
452
|
+
stream = S.EventStream(sink) # stamps seq, swallows write errors if client drops
|
|
453
|
+
send = stream.emit
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
send(S.start(sid, goal=prompt))
|
|
457
|
+
sys_prompt = load_system_prompt() + memory.index_block()
|
|
458
|
+
messages = [
|
|
459
|
+
{"role": "system", "content": sys_prompt},
|
|
460
|
+
{"role": "user", "content": prompt},
|
|
461
|
+
]
|
|
462
|
+
backend = resolve_backend(settings)
|
|
463
|
+
registry = get_registry()
|
|
464
|
+
mesh_fn = None
|
|
465
|
+
if getattr(settings, "guardian_enabled", False) and licensing.check_feature("mesh")[0]:
|
|
466
|
+
mesh_fn = lambda content: M.mesh_review(settings, content, on_event=send)
|
|
467
|
+
renderer = SessionRenderer(send)
|
|
468
|
+
run_agent(
|
|
469
|
+
backend, registry, messages, settings, renderer,
|
|
470
|
+
interactive=False, auto_approve=True, stream=True,
|
|
471
|
+
max_steps=req.get("max_steps"), mesh_fn=mesh_fn,
|
|
472
|
+
)
|
|
473
|
+
send(S.done())
|
|
474
|
+
except Exception as e: # noqa: BLE001 - report, then close cleanly
|
|
475
|
+
try:
|
|
476
|
+
send(S.error(f"{type(e).__name__}: {e}"))
|
|
477
|
+
send(S.done("error"))
|
|
478
|
+
except Exception:
|
|
479
|
+
pass
|
|
480
|
+
try:
|
|
481
|
+
(_sessions_dir() / (sid + ".jsonl")).write_text(
|
|
482
|
+
"\n".join(json.dumps(e) for e in transcript), encoding="utf-8"
|
|
483
|
+
)
|
|
484
|
+
except Exception:
|
|
485
|
+
pass
|
|
486
|
+
|
|
487
|
+
def _replay_session(self, sid):
|
|
488
|
+
"""Replay a stored session transcript as SSE (resumption / sharing)."""
|
|
489
|
+
safe = re.sub(r"[^a-zA-Z0-9_]", "", sid or "")
|
|
490
|
+
path = _sessions_dir() / (safe + ".jsonl")
|
|
491
|
+
if not safe or not path.is_file():
|
|
492
|
+
self._json(404, {"error": {"message": "session not found"}})
|
|
493
|
+
return
|
|
494
|
+
self.send_response(200)
|
|
495
|
+
self.send_header("Content-Type", "text/event-stream")
|
|
496
|
+
self.send_header("Cache-Control", "no-cache")
|
|
497
|
+
self.end_headers()
|
|
498
|
+
try:
|
|
499
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
500
|
+
if line.strip():
|
|
501
|
+
self.wfile.write(("data: " + line + "\n\n").encode())
|
|
502
|
+
self.wfile.flush()
|
|
503
|
+
except (BrokenPipeError, OSError):
|
|
504
|
+
pass
|
|
505
|
+
|
|
360
506
|
return Handler
|
|
361
507
|
|
|
362
508
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Canonical DeepParallel agent-session event schema.
|
|
2
|
+
|
|
3
|
+
One event vocabulary shared by every surface (CLI, hosted API, desktop, web).
|
|
4
|
+
It generalizes the hosted /review SSE stream (start / token / verdict / done)
|
|
5
|
+
into a full agent-session stream so a single trace view can render a run on any
|
|
6
|
+
surface: the council plan, each tool step, and the verification-mesh verdicts.
|
|
7
|
+
|
|
8
|
+
Events are plain dicts with a ``type`` discriminator and a monotonic ``seq``
|
|
9
|
+
(assigned by an :class:`EventStream`). ``to_sse`` serializes one event to a
|
|
10
|
+
Server-Sent-Events frame so the same producer feeds an HTTP stream or a local
|
|
11
|
+
renderer.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from itertools import count
|
|
18
|
+
from typing import Any, Callable, Iterable
|
|
19
|
+
|
|
20
|
+
# --- event factories ---------------------------------------------------------
|
|
21
|
+
# Each returns a bare dict; an EventStream stamps `seq`. Keep payloads small and
|
|
22
|
+
# JSON-serializable so they cross the wire unchanged.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def start(session_id: str | None = None, goal: str | None = None) -> dict:
|
|
26
|
+
return _e("start", session_id=session_id, goal=goal)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def plan(text: str, agreement: dict | None = None) -> dict:
|
|
30
|
+
"""The council's synthesized plan (and optional cross-model agreement)."""
|
|
31
|
+
return _e("plan", text=text, agreement=agreement)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def chain(name: str, content: str) -> dict:
|
|
35
|
+
"""One council chain's raw answer (a single parallel reasoning stream)."""
|
|
36
|
+
return _e("chain", name=name, content=content)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def step(index: int, intent: str | None = None) -> dict:
|
|
40
|
+
return _e("step", index=index, intent=intent)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def token(text: str, channel: str = "content") -> dict:
|
|
44
|
+
return _e("token", text=text, channel=channel)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def tool_call(name: str, preview: str) -> dict:
|
|
48
|
+
return _e("tool_call", name=name, preview=preview)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def tool_result(name: str, summary: str, ok: bool) -> dict:
|
|
52
|
+
return _e("tool_result", name=name, summary=summary, ok=ok)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def verify_start(action: str) -> dict:
|
|
56
|
+
"""The verification mesh has begun reviewing a proposed action."""
|
|
57
|
+
return _e("verify_start", action=action)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def verdict(lens: str, verdict_text: str | None, severity: str) -> dict:
|
|
61
|
+
"""One independent reviewer's verdict for a single lens."""
|
|
62
|
+
return _e("verdict", lens=lens, verdict=verdict_text, severity=severity)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def gate(decision: str, verdict_text: str | None, severity: str) -> dict:
|
|
66
|
+
"""The guardian's final gate: decision is allow | hold | block."""
|
|
67
|
+
return _e("gate", decision=decision, verdict=verdict_text, severity=severity)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def step_done(index: int) -> dict:
|
|
71
|
+
return _e("step_done", index=index)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def answer(text: str) -> dict:
|
|
75
|
+
return _e("answer", text=text)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def error(message: str) -> dict:
|
|
79
|
+
return _e("error", message=message)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def done(status: str = "ok") -> dict:
|
|
83
|
+
return _e("done", status=status)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _e(type_: str, **fields: Any) -> dict:
|
|
87
|
+
"""Build an event, dropping None-valued fields to keep frames compact."""
|
|
88
|
+
ev = {"type": type_}
|
|
89
|
+
ev.update({k: v for k, v in fields.items() if v is not None})
|
|
90
|
+
return ev
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --- serialization / streaming ----------------------------------------------
|
|
94
|
+
|
|
95
|
+
EVENT_TYPES = (
|
|
96
|
+
"start", "plan", "chain", "step", "token", "tool_call", "tool_result",
|
|
97
|
+
"verify_start", "verdict", "gate", "step_done", "answer", "error", "done",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def to_sse(event: dict) -> str:
|
|
102
|
+
"""Serialize one event to a Server-Sent-Events ``data:`` frame."""
|
|
103
|
+
return "data: " + json.dumps(event, ensure_ascii=False) + "\n\n"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class EventStream:
|
|
107
|
+
"""Stamps a monotonic ``seq`` on each event and forwards it to a sink.
|
|
108
|
+
|
|
109
|
+
The sink is any ``callable(dict) -> None`` (write to an HTTP response, push
|
|
110
|
+
to a renderer, append to a JSONL trace). Use as ``emit(session.plan(...))``.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(self, sink: Callable[[dict], None] | None = None):
|
|
114
|
+
self._sink = sink
|
|
115
|
+
self._seq = count()
|
|
116
|
+
|
|
117
|
+
def emit(self, event: dict) -> dict:
|
|
118
|
+
event = {**event, "seq": next(self._seq)}
|
|
119
|
+
if self._sink is not None:
|
|
120
|
+
try:
|
|
121
|
+
self._sink(event)
|
|
122
|
+
except Exception: # noqa: BLE001 - observation must never break a run
|
|
123
|
+
pass
|
|
124
|
+
return event
|
|
125
|
+
|
|
126
|
+
__call__ = emit
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def sse_stream(events: Iterable[dict]) -> Iterable[str]:
|
|
130
|
+
"""Adapt an iterable of events into SSE frames."""
|
|
131
|
+
for ev in events:
|
|
132
|
+
yield to_sse(ev)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepparallel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: DeepParallel - a multi-model agentic coding CLI with cross-model Guardian review, served via Crowe Logic.
|
|
5
5
|
Author-email: Michael Crowe <michael@crowelogic.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -15,11 +15,13 @@ deepparallel/dsml.py
|
|
|
15
15
|
deepparallel/fusion.py
|
|
16
16
|
deepparallel/licensing.py
|
|
17
17
|
deepparallel/memory.py
|
|
18
|
+
deepparallel/mesh.py
|
|
18
19
|
deepparallel/registry.json
|
|
19
20
|
deepparallel/renderer.py
|
|
20
21
|
deepparallel/routing.example.json
|
|
21
22
|
deepparallel/routing.py
|
|
22
23
|
deepparallel/serve.py
|
|
24
|
+
deepparallel/session.py
|
|
23
25
|
deepparallel/supply_chain.py
|
|
24
26
|
deepparallel/system_prompt.txt
|
|
25
27
|
deepparallel/userinput.py
|
|
@@ -71,6 +73,7 @@ tests/test_research.py
|
|
|
71
73
|
tests/test_research_provider.py
|
|
72
74
|
tests/test_routing.py
|
|
73
75
|
tests/test_serve.py
|
|
76
|
+
tests/test_serve_session.py
|
|
74
77
|
tests/test_spinner_color.py
|
|
75
78
|
tests/test_supply_chain.py
|
|
76
79
|
tests/test_tool_registry.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "deepparallel"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "DeepParallel - a multi-model agentic coding CLI with cross-model Guardian review, served via Crowe Logic."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
@@ -138,6 +138,9 @@ class _GuardianStub:
|
|
|
138
138
|
|
|
139
139
|
def test_review_file_safe_exits_zero(monkeypatch, tmp_path):
|
|
140
140
|
_set_azure_env(monkeypatch)
|
|
141
|
+
# Test the single-guardian exit-code contract deterministically, regardless of
|
|
142
|
+
# any ambient license (the multi-model mesh path has its own coverage).
|
|
143
|
+
monkeypatch.setattr(licensing, "check_feature", lambda feature, *a, **k: (feature != "mesh", ""))
|
|
141
144
|
f = tmp_path / "ok.py"
|
|
142
145
|
f.write_text("def add(a, b):\n return a + b\n")
|
|
143
146
|
monkeypatch.setattr(climod, "resolve_backend", lambda s: _GuardianStub("safe"))
|
|
@@ -149,6 +152,7 @@ def test_review_file_safe_exits_zero(monkeypatch, tmp_path):
|
|
|
149
152
|
|
|
150
153
|
def test_review_bug_exits_two(monkeypatch, tmp_path):
|
|
151
154
|
_set_azure_env(monkeypatch)
|
|
155
|
+
monkeypatch.setattr(licensing, "check_feature", lambda feature, *a, **k: (feature != "mesh", ""))
|
|
152
156
|
f = tmp_path / "bad.py"
|
|
153
157
|
f.write_text("def f(u):\n return u.balance\n")
|
|
154
158
|
monkeypatch.setattr(climod, "resolve_backend", lambda s: _GuardianStub("bug: null deref"))
|
|
@@ -162,6 +166,7 @@ def test_review_bug_exits_two(monkeypatch, tmp_path):
|
|
|
162
166
|
|
|
163
167
|
def test_review_diff_from_stdin(monkeypatch):
|
|
164
168
|
_set_azure_env(monkeypatch)
|
|
169
|
+
monkeypatch.setattr(licensing, "check_feature", lambda feature, *a, **k: (feature != "mesh", ""))
|
|
165
170
|
monkeypatch.setattr(climod, "resolve_backend", lambda s: _GuardianStub("risky: no test"))
|
|
166
171
|
monkeypatch.setattr(
|
|
167
172
|
climod, "backend_for_deployment", lambda s, d: _GuardianStub("risky: no test")
|
|
@@ -49,29 +49,34 @@ def _ids(payload):
|
|
|
49
49
|
return [entry["id"] for entry in payload["data"]]
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
# The gateway lists the full public CroweLM catalog (a fixed alias set),
|
|
53
|
+
# independent of which upstream deployments the local settings configured. The
|
|
54
|
+
# security-critical invariant: no raw upstream deployment name ever leaks.
|
|
55
|
+
_RAW_MARKERS = ("DeepSeek", "Kimi", "grok", "gpt-", "Llama", "Mistral", "Granite", "Qwen", "Cohere", "command-")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_models_payload_lists_public_aliases_no_raw_leak():
|
|
53
59
|
ids = _ids(_models_payload(_settings()))
|
|
54
60
|
|
|
55
|
-
assert
|
|
56
|
-
assert "crowelm-
|
|
57
|
-
assert
|
|
61
|
+
assert {"crowelm-apex", "crowelm-reason", "crowelm-flash"} <= set(ids)
|
|
62
|
+
assert all(i.startswith("crowelm-") for i in ids)
|
|
63
|
+
assert not any(marker in i for i in ids for marker in _RAW_MARKERS)
|
|
58
64
|
|
|
59
65
|
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
def test_models_payload_is_stable_across_configured_deployments():
|
|
67
|
+
base = _ids(_models_payload(_settings()))
|
|
68
|
+
other = _ids(
|
|
69
|
+
_models_payload(
|
|
70
|
+
_settings(parallel_deployments=("DeepSeek-V4-Pro", "Kimi-K2-6"), judge_deployment="grok-4-3")
|
|
71
|
+
)
|
|
64
72
|
)
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
assert "crowelm-apex" in
|
|
69
|
-
assert "crowelm-eclipse" in ids
|
|
70
|
-
assert "crowelm-quasar" in ids
|
|
71
|
-
assert "crowelm-titan" not in ids
|
|
74
|
+
# the public catalog does not depend on local deployment config
|
|
75
|
+
assert base == other
|
|
76
|
+
assert "crowelm-apex" in base
|
|
72
77
|
|
|
73
78
|
|
|
74
|
-
def
|
|
79
|
+
def test_models_endpoint_returns_public_catalog():
|
|
75
80
|
settings = _settings(
|
|
76
81
|
parallel_deployments=("DeepSeek-V4-Pro", "Kimi-K2-6"),
|
|
77
82
|
reasoner_deployment="",
|
|
@@ -91,7 +96,8 @@ def test_models_endpoint_returns_filtered_model_list():
|
|
|
91
96
|
handler_cls.do_GET(handler)
|
|
92
97
|
|
|
93
98
|
assert captured["code"] == 200
|
|
94
|
-
|
|
99
|
+
ids = _ids(captured["payload"])
|
|
100
|
+
assert "crowelm-apex" in ids and all(i.startswith("crowelm-") for i in ids)
|
|
95
101
|
|
|
96
102
|
|
|
97
103
|
def test_chat_accepts_public_alias_and_raw_deployment_for_compatibility():
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""The /v1/session endpoint maps the agent loop onto the canonical session
|
|
2
|
+
schema. These tests are deterministic (no model calls): they exercise the
|
|
3
|
+
renderer-to-event mapping and the request validation directly."""
|
|
4
|
+
|
|
5
|
+
from deepparallel import serve, session
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_session_renderer_emits_canonical_schema():
|
|
9
|
+
events = []
|
|
10
|
+
r = serve.SessionRenderer(events.append)
|
|
11
|
+
|
|
12
|
+
r.tool_start("write_file", "calc.py")
|
|
13
|
+
r.tool_result(True, "wrote 42 bytes", 0.12)
|
|
14
|
+
full = r.answer_stream(iter(["hel", "lo"]))
|
|
15
|
+
r.answer("final")
|
|
16
|
+
|
|
17
|
+
assert full == "hello"
|
|
18
|
+
types = [e["type"] for e in events]
|
|
19
|
+
# an act step: tool_call -> tool_result, bracketed by step / step_done
|
|
20
|
+
assert types.index("step") < types.index("tool_call") < types.index("tool_result")
|
|
21
|
+
assert "step_done" in types
|
|
22
|
+
# streamed tokens, one event per chunk
|
|
23
|
+
assert types.count("token") == 2
|
|
24
|
+
# the tool_result carries the tool name and ok flag
|
|
25
|
+
tr = next(e for e in events if e["type"] == "tool_result")
|
|
26
|
+
assert tr["name"] == "write_file" and tr["ok"] is True
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_session_renderer_confirm_auto_approves():
|
|
30
|
+
# non-interactive: confirm never blocks (the mesh gate does the blocking)
|
|
31
|
+
r = serve.SessionRenderer(lambda e: None)
|
|
32
|
+
assert r.confirm("write_file", "diff") is True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_session_event_factories_roundtrip_sse():
|
|
36
|
+
frame = session.to_sse(session.gate("block", "bug: sqli", "bug"))
|
|
37
|
+
assert frame.startswith("data: ") and frame.endswith("\n\n")
|
|
38
|
+
import json
|
|
39
|
+
payload = json.loads(frame[len("data: "):].strip())
|
|
40
|
+
assert payload["type"] == "gate" and payload["decision"] == "block" and payload["severity"] == "bug"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|