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
3 changes: 3 additions & 0 deletions .github/.licenserc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ header:
# CHANGELOG.md, which carries the header itself.
- 'dokimi/changelog.d/[0-9]*.md'
- 'dokimi/changelog.d/fragment_template.md.j2'
# Third-party eval files copied unchanged, under their own licenses
# (dokimi/tests/fixtures/task_formats/MANIFEST.yaml, LICENSES/).
- 'dokimi/tests/fixtures/task_formats/**'

comment: never

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
### Added

- **`tasks inspect <path> [--json] [--out FILE]`** — detects the eval-task
format of a file, a task directory or a whole tree and reports its
structure: kind (executable, answer-only, trigger, localization), item
count, present and missing parts, lint problems and which dokimi backends
consume it today. Recognises Harbor, BenchFlow `task.md`, terminal-bench v1,
SWE-bench instances, Inspect AI samples (JSONL and CSV) and `@task` files
(scanned as text, never imported), OpenAI evals JSONL and registry specs,
skill-creator `evals.json` and trigger sets, skill-reach query sets,
promptfoo configs and test files, dokimi question sets and ExploreBench
tasks; anything else is reported as `unknown` with its files. A dataset
file a config names (an Inspect `FieldSpec` remap, an OpenAI evals
model-graded spec, promptfoo `tests: file://`) is read as that config says,
and a git-LFS pointer stub is reported as one. `--json` and `--out` write
the `task_inspect.v1` artifact. Detection only: no new format runs yet.
15 changes: 15 additions & 0 deletions dokimi/changelog.d/20261024_100000_napetrov_run-tbench-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
### Added

- **terminal-bench v1 tasks run natively** — a `task.yaml` task
(laude-institute/terminal-bench `original-tasks/`) loads as a `TaskSpec` and
runs under `docker-oracle` / `docker-claude` as terminal-bench runs it:
`solution.sh` at `/oracle`, `run-tests.sh` and `tests/` at `/tests`, the
image's user, `TEST_DIR` and the compose environment, and the pytest summary
as the verdict. A task one `docker run` cannot reproduce (extra services,
volumes, tmpfs, ports; `solution.yaml`; `run_tests_in_same_shell`) is refused
with the reason. Harbor-backed harnesses and `benchflow*` run a Harbor copy
written lazily into a content-keyed cache; its `test.sh` scores the same
pytest summary. `tasks inspect` lists `docker-*`, `harbor` and `benchflow` as
consumers. A v1 task gets 8 GB of memory
(its compose `deploy.resources.limits.memory` when set; the default is
`DOKIMI_TBENCH_V1_MEMORY_MB`, else 8192), where other formats default to 2 GB.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
### Added

- **Per-phase network policy for `docker-*` runs** — build, solve and verify
each get `none`, `allowlist` (egress sidecar) or `public`. Defaults: build
public, solve and verify none. Set from task.toml (`[environment]`,
`[agent]`, `[verifier]` `network_mode` / `allowed_hosts`, Harbor's
`allow_internet`), or `dokimi tasks run --network PHASE=MODE[:hosts]`.
The effective plan and each phase's source land in `metrics.network`; a
skill's `metadata.network` is recorded as `metrics.network.skill`, not
granted.

### Changed

- **A task's `network_mode = "public"` now also reaches the oracle**, and the
verifier no longer gets the network unless something grants it.
2 changes: 1 addition & 1 deletion dokimi/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ points up:
| Layer | Modules |
| --- | --- |
| front end | `cli`, `commands`, `__main__` (the argparse front end; no layer below imports it) |
| tracks | `runner`, `report`, `subjects`, `novelty`, `harnesses`, `context`, `repos`, `explore`, `tool_preference`, `external`, `dashboard`, `orchestrator`, `datasets_export`, `coverage`, and the checkers `config_check`, `schema_check`, `schema_compat` |
| tracks | `runner`, `report`, `subjects`, `novelty`, `harnesses`, `context`, `repos`, `explore`, `tool_preference`, `external`, `dashboard`, `orchestrator`, `datasets_export`, `coverage`, `task_formats`, and the checkers `config_check`, `schema_check`, `schema_compat` |
| evaluation | `eval`, `personas`, `questions`, `treatments`, `plugins` |
| components | `ingest`, `metrics`, `gate`, `mcp`, `skills`, `agent_profiles` |
| foundation | `__init__` (the `dokimi` package itself), `_version`, `artifacts`, `records`, `llm`, `process`, `caching`, `config`, `registry`, `intents`, `defaults`, `console`, `utils`, `metrics/usage.py` |
Expand Down
113 changes: 108 additions & 5 deletions dokimi/docs/coding-harnesses.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,73 @@ refused (a few hundred bytes of nested aliases expand without bound), and
frontmatter nested too deeply to parse is a parse error. YAML and TOML dates
and times become ISO strings, and YAML sets sorted lists.

### Other eval formats: `tasks inspect`

`python cli.py tasks inspect <path> [--json] [--out FILE]` names the eval-task format of a
file, a task directory, or every item under a tree, and reports its structure:
kind, item count, which parts are present or missing (instruction,
environment, oracle, verifier, metadata), lint problems, and what in dokimi
can consume it today. It only reads: nothing is run or converted, a Python
file is scanned as text and never imported, and a file that does not parse is
a problem on its report, not an error. A path no format claims is reported as
`unknown` with its files (or keys) and likely prompt and verifier files; when
two formats claim a path about equally, the report lists the other as
`ambiguous` with its evidence. `--json` prints and `--out` writes the report
as a `task_inspect.v1` artifact (`schemas/task_inspect.v1.json`).

A dataset file a config in the same tree names is read as that config says:
an Inspect `@task` file's `json_dataset("x.jsonl", FieldSpec(input="question",
target="answer"))` makes `x.jsonl` Inspect samples under those keys, an OpenAI
evals registry spec makes its `samples_jsonl` OpenAI evals (with no `ideal`
needed when the spec's class is model-graded), and promptfoo `tests:
file://tests.csv` makes that file promptfoo tests. The report says which file
named it. A file that is a git-LFS pointer stub (a clone without `git lfs
pull`) is reported as `git-lfs-pointer`, with the format a config expects it
to be when one names it. `tests/fixtures/task_formats/` holds a small corpus of
real files in each format, with their sources in its `MANIFEST.yaml`.

| Format | Recognised by | Kind | Consumed by |
|---|---|---|---|
| `harbor` | directory with `task.toml` | executable | Harbor-backed harnesses (unless Harbor would refuse it), `docker-*` |
| `task-md` | directory with a BenchFlow `task.md` | executable | `benchflow*`, `docker-*` |
| `terminal-bench-v1` | directory with `task.yaml` + `run-tests.sh` / `Dockerfile` / `solution.sh` | executable | `docker-*` run it as it is; `harbor`, `benchflow` through a cached Harbor export |
| `swe-bench` | JSON/JSONL rows with `instance_id`, `repo`, `base_commit`, `problem_statement` (`FAIL_TO_PASS`) | executable | none yet |
| `inspect-samples` | JSON/JSONL/CSV rows with `input` and `target` (or the keys a `FieldSpec` names) | answer-only | none yet |
| `inspect-task` | `.py` importing `inspect_ai` with a `@task` function | answer-only (executable with a `sandbox=`) | none yet |
| `openai-evals` | JSONL rows with `input` (chat messages) and `ideal` | answer-only | none yet |
| `openai-evals-spec` | registry YAML with `class: evals.elsuite.*` run specs | answer-only | none yet |
| `skill-creator-evals` | `evals.json` with `skill_name` and `evals[].prompt` | answer-only | none yet |
| `skill-creator-trigger` | rows with `query` and `should_trigger` | trigger | none yet |
| `skill-reach` | `QuerySet` (`queries[]` with `query_id`/`id`, `text`, `expected_skill`) | trigger | none yet |
| `promptfoo` | `promptfooconfig*.yaml`/`.json`, or a config with `prompts`, `providers`/`targets` and `tests`/`scenarios`/`redteam` | answer-only | none yet |
| `promptfoo-tests` | CSV with `__expected` columns, or rows of `{vars, assert}` | answer-only | none yet |
| `dokimi-questions` | questions.v1/v2 JSON | answer-only (trigger with `expected_skill`) | questions track |
| `dokimi-explore-task` | YAML with `id`, `query` and `references[].path` | localization | explore track |
| `git-lfs-pointer` | a git-LFS spec v1 pointer stub | unknown | none (fetch it first) |

A terminal-bench v1 task (`task.yaml`, laude-institute/terminal-bench
`original-tasks/`) runs as it is under `docker-*`: `solution.sh` is copied to
`/oracle/solution.sh`, `run-tests.sh` and `tests/` to `/tests`, both run as the
image's user with `TEST_DIR=/tests` and the compose `environment`, and the
verdict is the pytest `short test summary info` (every listed test passed or
skipped), not the exit code, as terminal-bench reads it. `docker-*` refuses,
with the reason, a task whose `docker-compose.yaml` needs more than one
`docker run` (a second service, volumes, tmpfs, ports), a `parser_name` other
than `pytest`, `run_tests_in_same_shell`, or (oracle only) a `solution.yaml`.
The Harbor-backed harnesses and `benchflow*` run a Harbor copy written on first
use to `$TMPDIR/dokimi-harbor-export/<content hash>/` by
[`harbor_export.py`](../dokimi/harnesses/harbor_export.py); its `test.sh` scores
the same pytest summary, so a task gets one verdict on every backend.

Memory: terminal-bench sets no limit, so a v1 task gets
`environment.memory_mb = 8192` (not the 2048 of other formats; tasks such as
`predicate-pushdown-bench` are OOM-killed at 2 GB), unless its compose file
sets `deploy.resources.limits.memory`, which `docker compose` honours and
dokimi reads as the task's own value. `DOKIMI_TBENCH_V1_MEMORY_MB` changes the
8192 default. The Harbor export writes the same value to `task.toml`.

`tasks list` and `tasks run` still read only the first two.

The `docker-*` harnesses mount the oracle at `/solution` and `/oracle` and the
verifier at `/tests` and `/verifier`, because a script written for one layout
refers to its siblings by that layout's path. The directory's contents are
Expand Down Expand Up @@ -261,11 +328,11 @@ The result is in `metrics.isolation_probe` and `isolation_probe.json`:
`docker-probe` reads `violated` on a passing
cell, because the probe requests denied hosts (`example.com`) by design;
judge a probe by `isolation_probe.json`, not by `egress.status`.
- **`network_mode = "public"`.** It applies to the verifier only. The
container joins the default bridge after the agent exits. Every
uxlfoundation/skills task declares it, and none of their solutions or tests
uses the network, so a task whose solve needs a host must list it under
`environment.egress`.
- **`network_mode = "public"`.** An `[environment]`-level `public` applies
to the oracle and the verifier, not to an agent: every uxlfoundation/skills
task declares it, and none of their solutions or tests uses the network. A
task whose agent needs a host lists it under `environment.egress`. See
[Network policy](#network-policy) for the other sources.
- **Environment.** HOME is its own tmpfs, `/run/abhome`; the image's `/tmp`
is left alone. The environment is:
- the provider-credential allowlist;
Expand Down Expand Up @@ -474,6 +541,42 @@ A `command:` harness (including a `--command-template` that wraps `harbor`)
keeps its own rule: a `reward.txt` / `reward.json` under the run dir, else a
`reward: x` on its output, else exit code 0 is a pass.

### Network policy

Every `docker-*` run has a network per phase, recorded in
`metrics.network` with the source of each phase's setting, so two runs can
be checked for the same network before their verdicts are compared:

| Phase | Default | What runs |
|---|---|---|
| `build` | `public` (host network, host proxy as build args) | `docker build` |
| `solve` | `none` (an in-container agent still reaches its LLM through the sidecar) | the oracle, or the agent |
| `verify` | `none` | `tests/test.sh` |

The modes are Harbor's: `none` (Harbor's `no-network`), `public` (the
default bridge plus the host's proxy variables), and `allowlist` (the listed
`host[:port]` entries only, through an egress sidecar; the verifier's
decisions land in `metrics.network.verify_egress`). Later sources win:

1. The defaults above.
2. The task: `[environment]` `network_mode` and `allowed_hosts` (or Harbor's
deprecated `allow_internet`), then `[agent]` and `[verifier]` overrides,
and `environment.egress` for an agent's solve. An unknown mode refuses
the cell.
3. The operator: `dokimi tasks run --network PHASE=MODE[:host,...]`,
repeatable, with `all` for every phase. For example
`--network verify=none` grades a public task offline, and
`--network solve=allowlist:pypi.org,files.pythonhosted.org` lets every
arm install packages.

A skill that needs the network at solve time declares it in its `SKILL.md`
frontmatter as `metadata.network` (`allowlist:host,...` or `public`), with
the reason for people in `compatibility`, the Agent Skills spec's field for
environment needs. The run does not widen the skill arm for it: its baseline
would then run on another network. `metrics.network.skill.missing` lists the
declared hosts this run did not grant; pass them with `--network solve=...`
so every arm gets them.

## BenchFlow Cells

Skills are not bind-mounted: BenchFlow copies the `--skills-dir` root into the
Expand Down
2 changes: 2 additions & 0 deletions dokimi/docs/contributing-terminal-bench-task.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ read it. Contribute the task itself to the content repository; contribute change
to this format here, next to the code that parses it
([`dokimi/harnesses/tasks.py`](../dokimi/harnesses/tasks.py),
[`dokimi/harnesses/docker_solver.py`](../dokimi/harnesses/docker_solver.py)).
An existing terminal-bench v1 task (`task.yaml` + `run-tests.sh`) runs without
conversion; see the format table in [coding-harnesses.md](coding-harnesses.md).

## 1 — Pick a task

Expand Down
3 changes: 2 additions & 1 deletion dokimi/dokimi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
[`dashboard`][dokimi.dashboard] aggregates results across libraries and
[`orchestrator`][dokimi.orchestrator] runs the one-command pipeline.
- Executable tasks: [`harnesses`][dokimi.harnesses] runs coding agents on task
directories.
directories; [`task_formats`][dokimi.task_formats] names the format of other
eval-task files and directories (detection only).
- [`context`][dokimi.context] reads what a run's context window held.
- Content novelty: [`novelty`][dokimi.novelty]. Repositories as subjects:
[`repos`][dokimi.repos]. Subject scorecards, one per run:
Expand Down
6 changes: 4 additions & 2 deletions dokimi/dokimi/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
model), ``guidance_reach`` (L1 reach probes), ``repo_readiness`` (L0 claim
checks), ``tool_preference`` (which of two equal tools a model picks, and
what moves it), ``repo_environment`` (static inspection of what a
repository offers an agent) and ``repo_budget`` (what a repository puts
into an agent's context). This module stamps each with a
repository offers an agent), ``repo_budget`` (what a repository puts
into an agent's context) and ``task_inspect`` (which eval-task format each
path under a tree is). This module stamps each with a
``schema_version`` and validates it against the JSON Schema in
``dokimi/schemas/`` on save and load, so fixtures and baselines stay
forward-compatible and self-describing.
Expand Down Expand Up @@ -52,6 +53,7 @@
"tool_preference": "tool_preference.v1",
"repo_environment": "repo_environment.v1",
"repo_budget": "repo_budget.v1",
"task_inspect": "task_inspect.v1",
}

# kind -> superseded versions still accepted on *load*, newest first. An artifact written under an
Expand Down
62 changes: 62 additions & 0 deletions dokimi/dokimi/commands/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,50 @@ def cmd_tasks_list(args: argparse.Namespace) -> None:
print(f"{task.name}\t{difficulty}\t{','.join(tags)}")


def _inspect_line(report: dict) -> str:
n = report["items"]
items = "" if n is None else f"\t{n} item{'' if n == 1 else 's'}"
return f"{report['path']}\t{report['format']}\t{report['kind']}{items}\t-> {', '.join(report['consumers'])}"


def _print_inspect(reports: list[dict]) -> None:
for r in reports:
print(_inspect_line(r))
if r["present"] or r["missing"]:
print(f" parts: {', '.join(r['present']) or '-'}; missing: {', '.join(r['missing']) or '-'}")
for alt in r["alternatives"]:
print(f" ambiguous: also {alt['format']} ({alt['confidence']:.2f}: {', '.join(alt['evidence'])})")
for problem in r["problems"]:
print(f" problem: {problem}")
by_format: dict[str, int] = {}
for r in reports:
by_format[r["format"]] = by_format.get(r["format"], 0) + 1
print(f"{len(reports)} item{'' if len(reports) == 1 else 's'}: " + ", ".join(f"{f} {n}" for f, n in sorted(by_format.items())))


def cmd_tasks_inspect(args: argparse.Namespace) -> None:
"""Detect the eval-task format of a path (or of everything under it) and report its structure."""
import json

from dokimi.artifacts import save_artifact, stamp
from dokimi.task_formats import inspect_path

try:
reports = [r.as_dict() for r in inspect_path(Path(args.path))]
except FileNotFoundError as exc:
raise SystemExit(f"error: {exc}") from None
payload = {"root": str(args.path), "items": reports}
if args.out:
try:
save_artifact("task_inspect", payload, Path(args.out))
except OSError as exc:
raise SystemExit(f"error: cannot write {args.out}: {exc}") from None
if args.json:
print(json.dumps(stamp("task_inspect", dict(payload)), indent=2, default=str))
else:
_print_inspect(reports)


def cmd_tasks_harnesses(args: argparse.Namespace) -> None:
from dokimi.harnesses import known_harnesses

Expand Down Expand Up @@ -145,6 +189,7 @@ def cmd_tasks_run(args: argparse.Namespace) -> None:
command_template=args.command_template,
dry_run=args.dry_run,
repeats=args.repeats,
network=tuple(args.network),
)
try:
runner.validate_harnesses()
Expand Down Expand Up @@ -185,6 +230,15 @@ def register(sub, positive_int) -> None:
list_p.add_argument("--tasks-root", default=None, help=_TASKS_ROOT_HELP)
list_p.set_defaults(func=cmd_tasks_list)

inspect_p = tasks_sub.add_parser(
"inspect",
help="Detect the eval-task format of a file or directory tree and report its structure (no run)",
)
inspect_p.add_argument("path", help="A task directory, a dataset file, or a tree holding either")
inspect_p.add_argument("--json", action="store_true", help="Print the task_inspect.v1 report as JSON")
inspect_p.add_argument("--out", default=None, help="Write the task_inspect.v1 report to this file")
inspect_p.set_defaults(func=cmd_tasks_inspect)

harnesses_p = tasks_sub.add_parser("harnesses", help="List known coding harness aliases")
harnesses_p.set_defaults(func=cmd_tasks_harnesses)

Expand Down Expand Up @@ -216,6 +270,14 @@ def register(sub, positive_int) -> None:
"{model}, {output_dir}, {prompt}, {prompt_quoted}."
),
)
run_p.add_argument(
"--network", action="append", default=[], metavar="PHASE=MODE",
help=(
"Network for a docker-* phase (build, solve, verify, or all): none, public, or "
"allowlist:host[:port],... Repeatable; overrides the task's own network_mode. "
"Default build=public solve=none verify=none"
),
)
run_p.add_argument(
"--dry-run", action="store_true", help="Build artifact without invoking agents"
)
Expand Down
Loading
Loading