sync: 188 file da Baida98/AI@3e7ad6c6 (2026-08-26 07:25 UTC) [deploy-all]

#105
by Baida07 - opened
agents/unified_loop.py CHANGED
@@ -3510,13 +3510,17 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
3510
 
3511
  async def run(self, goal: str, context: str = "", max_steps: int = 8,
3512
  on_step: StepCallback | None = None,
3513
- session_id: str = "", allow_tools: bool = True) -> dict[str, Any]:
 
3514
  """Run the loop and close unexpected exceptions as a controlled FAILED state."""
3515
  previous_state = _ACTIVE_LOOP_STATE.get()
3516
  previous_engineering_state = _ACTIVE_ENGINEERING_STATE.get()
3517
  previous_engineering_mode = _ACTIVE_ENGINEERING_MODE.get()
3518
  try:
3519
- return await self._run_impl(goal, context, max_steps, on_step, session_id, allow_tools)
 
 
 
3520
  except Exception as _run_error:
3521
  state = _ACTIVE_LOOP_STATE.get()
3522
  error_text = f"{type(_run_error).__name__}: {str(_run_error)[:500]}"
@@ -3555,7 +3559,8 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
3555
 
3556
  async def _run_impl(self, goal: str, context: str = "", max_steps: int = 8,
3557
  on_step: StepCallback | None = None,
3558
- session_id: str = "", allow_tools: bool = True) -> dict[str, Any]:
 
3559
  # S390-B-L: strip role prefixes che causano prompt injection
3560
  # Es. "SYSTEM: ignore..." o "ASSISTANT: ..." nel goal utente
3561
  # S762-BUG3: re.sub con ^ strippava solo il PRIMO prefisso — input come
@@ -3694,8 +3699,33 @@ class UnifiedAgentLoop(DirectToolsMixin, PromptBuilderMixin, LLMSelectionMixin,
3694
  return _with_state(result)
3695
 
3696
  # Policy fail-closed: con divieto esplicito nessun ramo tool-first, planner,
3697
- # sandbox, speculazione o tool card è raggiungibile. La risposta resta testuale.
 
3698
  if not allow_tools:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3699
  await self._transition_state(state, AgentState.THINKING, on_step)
3700
  _r = await _finish(await self._run_fallback(state, on_step))
3701
  _r["timing_ms"] = int((_time.monotonic() - _t_run) * 1000)
 
3510
 
3511
  async def run(self, goal: str, context: str = "", max_steps: int = 8,
3512
  on_step: StepCallback | None = None,
3513
+ session_id: str = "", allow_tools: bool = True,
3514
+ allow_local_csv_conversion: bool = False) -> dict[str, Any]:
3515
  """Run the loop and close unexpected exceptions as a controlled FAILED state."""
3516
  previous_state = _ACTIVE_LOOP_STATE.get()
3517
  previous_engineering_state = _ACTIVE_ENGINEERING_STATE.get()
3518
  previous_engineering_mode = _ACTIVE_ENGINEERING_MODE.get()
3519
  try:
3520
+ return await self._run_impl(
3521
+ goal, context, max_steps, on_step, session_id, allow_tools,
3522
+ allow_local_csv_conversion,
3523
+ )
3524
  except Exception as _run_error:
3525
  state = _ACTIVE_LOOP_STATE.get()
3526
  error_text = f"{type(_run_error).__name__}: {str(_run_error)[:500]}"
 
3559
 
3560
  async def _run_impl(self, goal: str, context: str = "", max_steps: int = 8,
3561
  on_step: StepCallback | None = None,
3562
+ session_id: str = "", allow_tools: bool = True,
3563
+ allow_local_csv_conversion: bool = False) -> dict[str, Any]:
3564
  # S390-B-L: strip role prefixes che causano prompt injection
3565
  # Es. "SYSTEM: ignore..." o "ASSISTANT: ..." nel goal utente
3566
  # S762-BUG3: re.sub con ^ strippava solo il PRIMO prefisso — input come
 
3699
  return _with_state(result)
3700
 
3701
  # Policy fail-closed: con divieto esplicito nessun ramo tool-first, planner,
3702
+ # sandbox, speculazione o tool card è raggiungibile. L'unica eccezione è la
3703
+ # conversione CSV→JSON già riconosciuta e validata dal parser puro al confine HTTP.
3704
  if not allow_tools:
3705
+ if allow_local_csv_conversion:
3706
+ await self._transition_state(state, AgentState.TOOL_EXECUTING, on_step)
3707
+ direct_results, _tools_count, _exec_success, _exec_errors = await self._run_direct_tools(
3708
+ goal, on_step=on_step, local_csv_only=True,
3709
+ )
3710
+ if direct_results.startswith("[DIRECT_TERMINAL]\n"):
3711
+ _r = await _finish({
3712
+ "success": _exec_success > 0,
3713
+ "output": direct_results.removeprefix("[DIRECT_TERMINAL]\n"),
3714
+ "steps": state.steps,
3715
+ })
3716
+ else:
3717
+ _r = await _finish({
3718
+ "success": False,
3719
+ "output": direct_results,
3720
+ "steps": state.steps,
3721
+ "errors": ["conversione CSV locale non completata"],
3722
+ })
3723
+ _r["timing_ms"] = int((_time.monotonic() - _t_run) * 1000)
3724
+ _r["effective_max_steps"] = state.max_steps
3725
+ if _sid_token is not None:
3726
+ try: _sid_var.reset(_sid_token)
3727
+ except Exception: pass
3728
+ return _r
3729
  await self._transition_state(state, AgentState.THINKING, on_step)
3730
  _r = await _finish(await self._run_fallback(state, on_step))
3731
  _r["timing_ms"] = int((_time.monotonic() - _t_run) * 1000)
agents/unified_loop_tools.py CHANGED
@@ -127,7 +127,13 @@ class DirectToolsMixin:
127
  if len(candidate) > 1 and candidate not in {"in", "nel", "un", "il"}:
128
  return candidate
129
  return "."
130
- async def _run_direct_tools(self, goal: str, on_step: StepCallback | None = None) -> tuple[str, int, int, int]:
 
 
 
 
 
 
131
  """
132
  S193: Esegue tool direttamente via TOOL_REGISTRY senza smolagents o LLM per routing.
133
  Returns: 4-tuple (results_str, n_called, n_success, n_errors).
@@ -610,6 +616,10 @@ class DirectToolsMixin:
610
  return (_terminal_conversion, 1,
611
  int(_terminal_conversion.startswith("[DIRECT_TERMINAL]")),
612
  int(": errore" in _terminal_conversion or ": timeout" in _terminal_conversion))
 
 
 
 
613
  _terminal_image = await _t_generate_image()
614
  if _terminal_image is not None:
615
  return (_terminal_image, 1,
 
127
  if len(candidate) > 1 and candidate not in {"in", "nel", "un", "il"}:
128
  return candidate
129
  return "."
130
+ async def _run_direct_tools(
131
+ self,
132
+ goal: str,
133
+ on_step: StepCallback | None = None,
134
+ *,
135
+ local_csv_only: bool = False,
136
+ ) -> tuple[str, int, int, int]:
137
  """
138
  S193: Esegue tool direttamente via TOOL_REGISTRY senza smolagents o LLM per routing.
139
  Returns: 4-tuple (results_str, n_called, n_success, n_errors).
 
616
  return (_terminal_conversion, 1,
617
  int(_terminal_conversion.startswith("[DIRECT_TERMINAL]")),
618
  int(": errore" in _terminal_conversion or ": timeout" in _terminal_conversion))
619
+ # Policy ristretta: dopo il riconoscimento HTTP del CSV locale non sono
620
+ # ammessi altri direct tool, né fallback impliciti a immagine/rete.
621
+ if local_csv_only:
622
+ return ("[convert_csv_to_json: conversione locale non riconosciuta]", 0, 0, 1)
623
  _terminal_image = await _t_generate_image()
624
  if _terminal_image is not None:
625
  return (_terminal_image, 1,
api/agent.py CHANGED
@@ -650,6 +650,7 @@ async def create_agent_task(body: AgentTaskIn, role: AuthRole = Depends(require_
650
  'session_id': body.session_id or '', # P17-F2: BB session key (normalize None→'')
651
  'forbid_tools': _tool_policy.forbid_tools,
652
  'literal_response': _tool_policy.literal_response,
 
653
  }
654
  # Le credenziali BYOK restano in una mappa runtime separata dai metadata task
655
  # e non raggiungono Supabase, checkpoint o buffer SSE.
@@ -827,10 +828,15 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0, rol
827
  task = _agent_tasks[task_id]
828
  # I task restaurati da persistenza potrebbero non contenere metadata runtime.
829
  # Ricostruire la policy dal goal mantiene il resume fail-closed.
830
- if "forbid_tools" not in task or "literal_response" not in task:
 
 
 
 
831
  _restored_policy = build_task_tool_policy(task.get("goal", ""))
832
  task["forbid_tools"] = _restored_policy.forbid_tools
833
  task["literal_response"] = _restored_policy.literal_response
 
834
  _last_event_id = request.headers.get("Last-Event-ID") or request.headers.get("last-event-id")
835
  _resume_from = int(_last_event_id) if (_last_event_id and _last_event_id.isdigit()) else resume
836
 
@@ -1378,6 +1384,7 @@ async def stream_agent_task(task_id: str, request: Request, resume: int = 0, rol
1378
  on_step=step_cb,
1379
  session_id=task.get('session_id', '') or '',
1380
  allow_tools=not bool(task.get('forbid_tools', False)),
 
1381
  )
1382
  _agent_tasks[task_id]['status'] = 'SUCCESS'
1383
  asyncio.create_task(sb_update_status(task_id, 'SUCCESS')).add_done_callback(_log_task_exc)
 
650
  'session_id': body.session_id or '', # P17-F2: BB session key (normalize None→'')
651
  'forbid_tools': _tool_policy.forbid_tools,
652
  'literal_response': _tool_policy.literal_response,
653
+ 'allow_local_csv_conversion': _tool_policy.allow_local_csv_conversion,
654
  }
655
  # Le credenziali BYOK restano in una mappa runtime separata dai metadata task
656
  # e non raggiungono Supabase, checkpoint o buffer SSE.
 
828
  task = _agent_tasks[task_id]
829
  # I task restaurati da persistenza potrebbero non contenere metadata runtime.
830
  # Ricostruire la policy dal goal mantiene il resume fail-closed.
831
+ if (
832
+ "forbid_tools" not in task
833
+ or "literal_response" not in task
834
+ or "allow_local_csv_conversion" not in task
835
+ ):
836
  _restored_policy = build_task_tool_policy(task.get("goal", ""))
837
  task["forbid_tools"] = _restored_policy.forbid_tools
838
  task["literal_response"] = _restored_policy.literal_response
839
+ task["allow_local_csv_conversion"] = _restored_policy.allow_local_csv_conversion
840
  _last_event_id = request.headers.get("Last-Event-ID") or request.headers.get("last-event-id")
841
  _resume_from = int(_last_event_id) if (_last_event_id and _last_event_id.isdigit()) else resume
842
 
 
1384
  on_step=step_cb,
1385
  session_id=task.get('session_id', '') or '',
1386
  allow_tools=not bool(task.get('forbid_tools', False)),
1387
+ allow_local_csv_conversion=bool(task.get('allow_local_csv_conversion', False)),
1388
  )
1389
  _agent_tasks[task_id]['status'] = 'SUCCESS'
1390
  asyncio.create_task(sb_update_status(task_id, 'SUCCESS')).add_done_callback(_log_task_exc)
api/task_tool_policy.py CHANGED
@@ -36,14 +36,33 @@ class TaskToolPolicy:
36
 
37
  forbid_tools: bool
38
  literal_response: str | None = None
 
 
 
 
39
 
40
  def to_dict(self) -> dict[str, object]:
41
  return {
42
  "forbid_tools": self.forbid_tools,
43
  "literal_response": self.literal_response,
 
44
  }
45
 
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  def build_task_tool_policy(goal: object) -> TaskToolPolicy:
48
  """Classifica solo istruzioni utente nel goal HTTP, senza inferenze LLM."""
49
  if not isinstance(goal, str):
@@ -53,4 +72,8 @@ def build_task_tool_policy(goal: object) -> TaskToolPolicy:
53
  explicit_denial = bool(_EXPLICIT_TOOL_DENIAL_RE.search(normalized))
54
  literal_match = _LITERAL_RESPONSE_RE.search(normalized) if explicit_denial else None
55
  literal = literal_match.group(1) if literal_match else None
56
- return TaskToolPolicy(forbid_tools=explicit_denial, literal_response=literal)
 
 
 
 
 
36
 
37
  forbid_tools: bool
38
  literal_response: str | None = None
39
+ # Eccezione deliberatamente stretta: una conversione CSV→JSON completamente
40
+ # locale può usare soltanto il percorso deterministico, non il planner né i
41
+ # tool generici, quando l'utente vieta rete/shell/servizi esterni.
42
+ allow_local_csv_conversion: bool = False
43
 
44
  def to_dict(self) -> dict[str, object]:
45
  return {
46
  "forbid_tools": self.forbid_tools,
47
  "literal_response": self.literal_response,
48
+ "allow_local_csv_conversion": self.allow_local_csv_conversion,
49
  }
50
 
51
 
52
+ def _is_local_csv_conversion(goal: str) -> bool:
53
+ """Riconosce solo la conversione CSV→JSON già validata dal parser puro.
54
+
55
+ L'import è locale e senza side effect: riusa la grammatica dei nomi sicuri,
56
+ il parsing CSV rigoroso e la validazione record-per-record del percorso
57
+ deterministico, evitando una seconda regex di autorizzazione divergente.
58
+ """
59
+ try:
60
+ from agents.file_conversion import convert_csv_attachment_to_json
61
+ return convert_csv_attachment_to_json(goal) is not None
62
+ except Exception:
63
+ return False
64
+
65
+
66
  def build_task_tool_policy(goal: object) -> TaskToolPolicy:
67
  """Classifica solo istruzioni utente nel goal HTTP, senza inferenze LLM."""
68
  if not isinstance(goal, str):
 
72
  explicit_denial = bool(_EXPLICIT_TOOL_DENIAL_RE.search(normalized))
73
  literal_match = _LITERAL_RESPONSE_RE.search(normalized) if explicit_denial else None
74
  literal = literal_match.group(1) if literal_match else None
75
+ return TaskToolPolicy(
76
+ forbid_tools=explicit_denial,
77
+ literal_response=literal,
78
+ allow_local_csv_conversion=explicit_denial and literal is None and _is_local_csv_conversion(normalized),
79
+ )
tests/test_task_tool_policy.py CHANGED
@@ -13,21 +13,37 @@ class TaskToolPolicyTests(unittest.TestCase):
13
  )
14
  self.assertTrue(policy.forbid_tools)
15
  self.assertEqual(policy.literal_response, "TEST_E2E_OK")
 
16
 
17
  def test_explicit_tool_denial_without_literal_contract_stays_tool_free(self) -> None:
18
  policy = build_task_tool_policy("Spiega il concetto senza usare strumenti o rete.")
19
  self.assertTrue(policy.forbid_tools)
20
  self.assertIsNone(policy.literal_response)
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  def test_normal_task_remains_unrestricted(self) -> None:
23
  policy = build_task_tool_policy("Cerca le notizie di oggi e sintetizzale.")
24
  self.assertFalse(policy.forbid_tools)
25
  self.assertIsNone(policy.literal_response)
 
26
 
27
  def test_partial_wording_does_not_create_a_literal_contract(self) -> None:
28
  policy = build_task_tool_policy("Rispondi esclusivamente con una spiegazione, senza strumenti.")
29
  self.assertTrue(policy.forbid_tools)
30
  self.assertIsNone(policy.literal_response)
 
31
 
32
 
33
  if __name__ == "__main__":
 
13
  )
14
  self.assertTrue(policy.forbid_tools)
15
  self.assertEqual(policy.literal_response, "TEST_E2E_OK")
16
+ self.assertFalse(policy.allow_local_csv_conversion)
17
 
18
  def test_explicit_tool_denial_without_literal_contract_stays_tool_free(self) -> None:
19
  policy = build_task_tool_policy("Spiega il concetto senza usare strumenti o rete.")
20
  self.assertTrue(policy.forbid_tools)
21
  self.assertIsNone(policy.literal_response)
22
+ self.assertFalse(policy.allow_local_csv_conversion)
23
+
24
+ def test_restricted_local_csv_conversion_is_explicitly_qualified(self) -> None:
25
+ policy = build_task_tool_policy(
26
+ "Esegui solo nel workspace VFS locale questa conversione deterministica.\n"
27
+ "Crea capability_catalog.csv con contenuto esatto: id,name,active\n"
28
+ "1,alpha,true\n2,beta,false\n"
29
+ ". Poi crea capability_catalog.json con lo stesso catalogo come array JSON valido di due oggetti.\n"
30
+ "Non usare rete, shell, servizi esterni o provider aggiuntivi."
31
+ )
32
+ self.assertTrue(policy.forbid_tools)
33
+ self.assertIsNone(policy.literal_response)
34
+ self.assertTrue(policy.allow_local_csv_conversion)
35
 
36
  def test_normal_task_remains_unrestricted(self) -> None:
37
  policy = build_task_tool_policy("Cerca le notizie di oggi e sintetizzale.")
38
  self.assertFalse(policy.forbid_tools)
39
  self.assertIsNone(policy.literal_response)
40
+ self.assertFalse(policy.allow_local_csv_conversion)
41
 
42
  def test_partial_wording_does_not_create_a_literal_contract(self) -> None:
43
  policy = build_task_tool_policy("Rispondi esclusivamente con una spiegazione, senza strumenti.")
44
  self.assertTrue(policy.forbid_tools)
45
  self.assertIsNone(policy.literal_response)
46
+ self.assertFalse(policy.allow_local_csv_conversion)
47
 
48
 
49
  if __name__ == "__main__":