Skip to content

feat(cron): add cronjob(action='output') to read recently delivered cron results - #37071

Open
beardthelion wants to merge 2 commits into
NousResearch:mainfrom
beardthelion:feat/cron-output-action
Open

feat(cron): add cronjob(action='output') to read recently delivered cron results#37071
beardthelion wants to merge 2 commits into
NousResearch:mainfrom
beardthelion:feat/cron-output-action

Conversation

@beardthelion

@beardthelion beardthelion commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds a cronjob(action='output') tool action so the agent can read back what its own scheduled jobs recently delivered.

Cron deliveries are sent straight to the platform and never enter the chat's session transcript, so the interactive agent currently has no way to recall what a job reported, even when the user asks "what did job X just send?" This is the read-on-demand fix: it does not touch message history or alternation, it only lets the agent look up what was delivered.

  • cronjob(action='output') returns the most recent deliveries across all jobs, newest first.
  • cronjob(action='output', job_id=...) scopes to a single job.
  • cronjob(action='output', limit=N) controls how many (default 5).

It parses the per-run markdown the scheduler already writes under ~/.hermes/cron/output/<job_id>/*.md, handling both saved-file shapes: agent-mode (## Response) and no_agent (header, then ---, then script stdout).

Related Issue

Part of #37070.

(Not using the Fixes keyword: #37070 is addressed by two independent PRs and should stay open until both land. This is the read-on-demand half; the ambient-awareness half is #37073.)

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

  • tools/cronjob_tools.py: add helpers _extract_delivered_content, _parse_output_file, _read_recent_outputs; add the output action to the dispatcher; add a limit parameter; update the tool schema (action list + limit).
  • No changes to delivery or scheduling behavior.

How to Test

  1. With at least one cron job that has delivered before, call cronjob(action='output') and confirm it returns recent deliveries (name, run time, content), newest first.
  2. cronjob(action='output', job_id=<id>) returns only that job's deliveries; limit=N caps the count.
  3. Automated: scripts/run_tests.sh tests/tools/test_cronjob_tools.py (the AGENTS.md-mandated runner) => 62 passed, 0 failed.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (feat(cron):)
  • I searched for existing PRs (no open PR adds a read-back output action; closest is feat(cron): add model label + truncated response guard #35315, which is output formatting, different scope)
  • My PR contains only changes related to this feature
  • I've run the test suite and all tests pass (scripts/run_tests.sh tests/tools/test_cronjob_tools.py => 62 passed)
  • I've added tests for my changes (TestReadRecentOutputs, 6 cases)
  • I've tested on my platform: Ubuntu (Linux 6.17)

Documentation & Housekeeping

  • Documentation: N/A (new read-only action; no change to existing feature behavior)
  • cli-config.yaml.example: N/A (no config keys added)
  • CONTRIBUTING.md / AGENTS.md: N/A
  • Cross-platform impact: N/A (pure stdlib pathlib / re)
  • I've updated tool descriptions/schemas (added the output action and limit parameter)

Screenshots / Logs

$ scripts/run_tests.sh tests/tools/test_cronjob_tools.py
=== Summary: 1 files, 62 tests passed, 0 failed (100% complete) ===

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cron Cron scheduler and job management tool/delegate Subagent delegation labels Jun 2, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the read-on-demand approach. Current main still saves each cron run but has no agent-facing action to read it, so the feature remains relevant.

Problems

  • tools/cronjob_tools.py:492 joins an unresolved raw job_id; the fallback at PR line 628 makes this reachable. Current main rejects unsafe output-path components in cron/jobs.py:324-337, so this read path must not bypass that guard.
  • The PR exposes limit at tools/cronjob_tools.py:816-819, but the registry handler has no limit=args.get("limit") mapping (current handler: tools/cronjob_tools.py:1123-1143). Tool calls therefore always use the default.
  • Saved output is not proof of delivery: current main saves before delivery (cron/scheduler.py:3475) and persists suppressed [SILENT] responses (cron/scheduler.py:3493-3509). The action should report saved run output unless delivery outcomes are recorded separately.

Suggested changes

  • Require a canonical resolved job ID and use get_cron_output_dir() (cron/jobs.py:143-145); add traversal, handler-level limit, silent, failed, and delivery-error tests.
  • Update the cron API documentation lists at website/docs/user-guide/features/cron.md:581-589 and website/docs/reference/tools-reference.md:57.

Automated hermes-sweeper review.

Comment thread tools/cronjob_tools.py
return []

if job_id:
job_dir = root / job_id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This raw path join bypasses current main's output-path validation in cron.jobs._job_output_dir(). Because line 628 retains an unresolved job_id, inputs such as ../... can escape the cron output root when globbing Markdown files. Require a canonical resolved job ID or apply the same containment validation before reading.

Comment thread tools/cronjob_tools.py
},
"limit": {
"type": "integer",
"description": "For action=output: how many recent deliveries to return (default 5)."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This schema parameter is not forwarded by the registered handler: the handler explicitly maps arguments into cronjob(...) but has no limit=args.get("limit"). Calls through the tool will always use the default value; wire it through and add a handler-level regression test.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
Cron deliveries don't enter the interactive conversation history (the
assistant-role mirror was removed in NousResearch#2313 because it broke message
alternation). This adds the pull side of cron session-awareness: the
agent can read back what its jobs delivered, on demand.

- cronjob(action='output'[, job_id][, limit]) returns recent deliveries,
  newest first, parsed from ~/.hermes/cron/output/<job_id>/*.md
- _extract_delivered_content handles both saved-file shapes: agent-mode
  ("## Response") and no_agent ("---" then script stdout)
- schema: new 'output' action + 'limit' param

Pull side only. Push side (auto-injecting a [System note] into the next
turn's system prompt) is the next step and is not in this commit.

Tests: 6 new in TestReadRecentOutputs; 62 passed.
(cherry picked from commit 0bc07530f45195a82636b9243d3cbe64c8635277)
@beardthelion
beardthelion force-pushed the feat/cron-output-action branch from 7bfefbe to f4b5d58 Compare July 17, 2026 22:56
@beardthelion

beardthelion commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Both findings addressed, and the branch is rebased onto current main (new head b7c2f80).

Path containment. _read_recent_outputs no longer joins an unvalidated job_id into the output root. _is_safe_output_component() rejects separators and dot segments before the join; an unsafe value logs a warning and returns empty instead of reading outside ~/.hermes/cron/output/. Covered by test_read_recent_rejects_parent_traversal.

Limit forwarding. The registered handler now maps limit=args.get("limit") into cronjob(...), and action='output' passes it through to _read_recent_outputs(..., limit=limit or 5). Covered by test_output_action_forwards_limit_through_registered_handler plus test_read_recent_respects_limit.

All 86 cron tool tests pass on the rebased head.

Two hermes-sweeper findings, both verified against the code:

- Path traversal: _read_recent_outputs joined an unvalidated job_id into a
  filesystem path, and the output-action fallback assigns the raw job_id back
  when resolve_job_ref returns no match, so a job_id of '../..' or an absolute
  path escaped the cron output root and globbed .md files back to the agent
  (reachable via prompt injection, since the model supplies job_id from
  untrusted inbound messages). Add _is_safe_output_component, mirroring
  cron.jobs._job_output_dir's containment guard, and reject unsafe ids in the
  reader so the hole is closed regardless of caller.
- limit not forwarded: the registered cronjob handler mapped every arg into
  cronjob(...) except limit, so tool-mediated output reads always used the
  default 5. Add limit=args.get('limit').

Tests: two traversal regressions (red-proven: '../leak' and absolute job_id
both read outside the root before the fix) and a handler-level limit-forwarding
test (red-proven: got default 5 without the mapping). 65/65 in the file.
@beardthelion
beardthelion force-pushed the feat/cron-output-action branch from f4b5d58 to b7c2f80 Compare July 17, 2026 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/delegate Subagent delegation type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants