feat(cron): add cronjob(action='output') to read recently delivered cron results - #37071
feat(cron): add cronjob(action='output') to read recently delivered cron results#37071beardthelion wants to merge 2 commits into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
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:492joins an unresolved rawjob_id; the fallback at PR line 628 makes this reachable. Current main rejects unsafe output-path components incron/jobs.py:324-337, so this read path must not bypass that guard.- The PR exposes
limitattools/cronjob_tools.py:816-819, but the registry handler has nolimit=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-589andwebsite/docs/reference/tools-reference.md:57.
Automated hermes-sweeper review.
| return [] | ||
|
|
||
| if job_id: | ||
| job_dir = root / job_id |
There was a problem hiding this comment.
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.
| }, | ||
| "limit": { | ||
| "type": "integer", | ||
| "description": "For action=output: how many recent deliveries to return (default 5)." |
There was a problem hiding this comment.
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.
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)
7bfefbe to
f4b5d58
Compare
|
Both findings addressed, and the branch is rebased onto current main (new head b7c2f80). Path containment. Limit forwarding. The registered handler now maps 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.
f4b5d58 to
b7c2f80
Compare
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) andno_agent(header, then---, then script stdout).Related Issue
Part of #37070.
(Not using the
Fixeskeyword: #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
Changes Made
tools/cronjob_tools.py: add helpers_extract_delivered_content,_parse_output_file,_read_recent_outputs; add theoutputaction to the dispatcher; add alimitparameter; update the tool schema (action list +limit).How to Test
cronjob(action='output')and confirm it returns recent deliveries (name, run time, content), newest first.cronjob(action='output', job_id=<id>)returns only that job's deliveries;limit=Ncaps the count.scripts/run_tests.sh tests/tools/test_cronjob_tools.py(the AGENTS.md-mandated runner) => 62 passed, 0 failed.Checklist
Code
feat(cron):)scripts/run_tests.sh tests/tools/test_cronjob_tools.py=> 62 passed)TestReadRecentOutputs, 6 cases)Documentation & Housekeeping
cli-config.yaml.example: N/A (no config keys added)CONTRIBUTING.md/AGENTS.md: N/Apathlib/re)outputaction andlimitparameter)Screenshots / Logs