Skip to content

Encapsulate execution behind run/run_captured/run_agent (v0.3.0) - #11

Merged
jhheider merged 1 commit into
mainfrom
feat/encapsulate-0.3
Jul 21, 2026
Merged

Encapsulate execution behind run/run_captured/run_agent (v0.3.0)#11
jhheider merged 1 commit into
mainfrom
feat/encapsulate-0.3

Conversation

@jhheider

Copy link
Copy Markdown
Owner

Breaking (v0.3.0). Narrows mdtask-core's public API so consumers deal only in jobs, metadata, and running — never in execution mechanics. Every future change to how a job runs (scriptfile delivery, Interpreter:, cancellation, new interpreters, per #10) becomes internal, with no consumer seam.

Public surface

  • parse, find_task_files, TaskFile::jobs()/job()/warnings(), agent_jobs(files).
  • Job { name, description, args, requires, agent_allow } (renamed from Task; script/lang/opts/env now pub(crate)).
  • run (CLI, inherits stdio) / run_captured (gloaming) / run_agent (MCP, gated), all running the Requires: chain internally; RunError.
  • Internal now: Invocation, invocation(), interpreter(), dependency_order, bind, substitute. No consumer ever holds a command.

Consumers collapsed

  • CLI: the whole run loop is one mdtask_core::run(...).
  • MCP: run_task is one run_agent(...); the allowlist/same-file/injection logic moved into core (one audited place), so the script never leaves the library.
  • Lint deleted (the one thing that forced the script to be exposed; substitutable by shellcheck, out of scope).

Security preserved (verified end-to-end, not just unit tests)

Re-ran both prior RCE proof-of-concepts against the built binary: the dependency-shadow PoC prints real-build, not PWNED; the {{ arg }} injection PoC is refused with no marker file written. run_agent still enforces nearest-def allowlist, same-file dependency resolution, and injection refusal.

Verification

fmt, clippy -D warnings (default + --features mcp), all tests both ways (incl. both RCE tests and a 200k-deep chain), builds, CLI smoke (list / Requires chain / usage / errors), no em-dashes. Both crates bumped to 0.3.0.

Follow-up after release: rework gloaming onto jobs() + run_captured() and bump its dep.

🤖 Generated with Claude Code

Narrow mdtask-core's public API so consumers deal only in jobs, their
metadata, and running them, never in execution mechanics. The Task struct
is renamed Job; its script, lang, opts, and env fields become internal, as
do Invocation, TaskFile::invocation, interpreter, dependency_order, bind,
substitute, parse_args, and Job::inherits_cwd/script_arg_templates. No
consumer sees a program, argv, interpreter, or temp file.

Three entry points run a job and its Requires: chain:

  run           inherits stdio (the CLI, streaming)
  run_captured  aggregates stdout/stderr across the chain (embedders)
  run_agent     the agent gate: allowlist, within-file dependency
                resolution, and the {{ arg }} injection guard, captured

The two previously found RCEs stay fixed: run_agent resolves the Requires:
chain within the allowed target's own file (so a nearer untrusted task file
cannot shadow a dependency), and refuses a target that raw-templates a
declared arg. Both are covered by regression tests in lib.rs and, through
the tool path, in mcp.rs.

The CLI run loop collapses to mdtask_core::run; mcp.rs run_task delegates to
run_agent and list_tasks to agent_jobs, with all security logic moved to
core. The lint subcommand (shellcheck) is removed entirely. The fixture test
now drives the public surface (parse, jobs(), run_captured, run_agent). Both
crates bump to 0.3.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jhheider
jhheider merged commit 4f8a03d into main Jul 21, 2026
6 checks passed
@jhheider
jhheider deleted the feat/encapsulate-0.3 branch July 21, 2026 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant