Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/JOURNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ protocol. **Newest entries at the top.** Tag each entry with one or more of:

---

## 2026-07-13 — workflow parser rejected a bare-int access index #mistake #fix #repro
**Context:** parsing a Conductor workflow proposal in `fugu/workflow.py::_normalize_access`.
**Expected:** an `access_list` entry of `0` parses the same as `"0"` or `[0]` (read step 0).
**Actual:** `parse_workflow('... access_list=[[], 0]', 3)` returned `(None, False)` — the whole
workflow was dropped (`parsed_ok=False`, reward 0) — while the identical `access_list=[[], "0"]`
parsed fine.
**Root cause:** `_normalize_access` handled scalar `str` digits and `None`, and lists, but a bare
scalar `int` fell through every branch to `return None`, rejecting the proposal.
**Fix / decision:** add a bare-`int` branch (rejecting `bool`, which subclasses `int`) that returns
`[acc]` when `0 <= acc < step_index`, matching the accepted `"0"`/`[0]` forms. This aligns with the
parser's stated goal of not losing recoverable valid outputs. Added `tests/test_fugu_access_int.py`
(offline); 8 existing workflow tests still pass.
**Follow-up:** none.
## 2026-07-12 — code grader: add resource limits on top of the HOME/secrets fix #security #decision
**Context:** issue #71 — the code grader (`run_pass_at_1`) runs untrusted miner/LLM candidate code. The core secret-leak fix (isolated throwaway HOME/cwd, scrubbed env, `python -I`) already landed on main.
**Finding:** main's sandbox closes the HOME/secrets exfiltration vector but has **no resource limits** — an untrusted candidate can still exhaust host memory or fork-bomb the eval box within its wall-clock timeout (verified: a 4 GiB `bytearray` allocation runs to completion and "passes" on main).
Expand Down
8 changes: 8 additions & 0 deletions src/trinity/fugu/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,14 @@ def _normalize_access(acc: object, step_index: int) -> object | None:
j = int(s)
return [j] if j < step_index else None
return None
if isinstance(acc, bool):
# bool is a subclass of int; a boolean is not a valid step index.
return None
if isinstance(acc, int):
# A bare int index is the most natural model output and is semantically
# identical to the already-accepted "0" / [0] forms — accept it too rather
# than reject the whole workflow (the parser aims to avoid false negatives).
return [acc] if 0 <= acc < step_index else None
if isinstance(acc, (list, tuple)):
if len(acc) == 1 and isinstance(acc[0], str) and acc[0].strip().lower() == "all":
return "all"
Expand Down
39 changes: 39 additions & 0 deletions tests/test_fugu_access_int.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Offline unit tests for bare-int access indices in fugu workflow parsing.

Regression for: `_normalize_access` accepted a scalar string digit (`"0"` -> `[0]`)
and `None` (-> `[]`) but rejected a bare scalar int (`0`), dropping the whole
workflow (`parsed_ok=False`) even though a bare index is the most natural model
output and is semantically identical to the accepted forms. Pure functions; no GPU.
"""
from trinity.fugu.workflow import _normalize_access, parse_workflow


def test_bare_int_access_index_accepted():
txt = 'model_id=[0,1]\nsubtasks=["solve","answer"]\naccess_list=[[], 0]'
wf, ok = parse_workflow(txt, n_workers=3)
assert ok and wf is not None
assert wf.steps[1].access == [0]


def test_bare_int_matches_string_and_list_forms():
base = 'model_id=[0,1]\nsubtasks=["solve","answer"]\naccess_list=[[], {}]'
outs = []
for form in ("0", '"0"', "[0]"):
wf, ok = parse_workflow(base.format(form), n_workers=3)
assert ok, f"form {form} should parse"
outs.append(wf.steps[1].access)
assert outs[0] == outs[1] == outs[2] == [0]


def test_normalize_access_int_directly():
assert _normalize_access(0, 1) == [0]
assert _normalize_access(2, 3) == [2]
# forward / out-of-range reference is invalid (rejects), same as the list form
assert _normalize_access(3, 3) is None
assert _normalize_access(-1, 3) is None


def test_bool_is_not_a_valid_access_index():
# bool is a subclass of int, but True/False are not step indices
assert _normalize_access(True, 3) is None
assert _normalize_access(False, 3) is None
Loading