Skip to content

fix(cron): stamp + replay creator sender/role across cron fires#1129

Open
mozaa-solana wants to merge 1 commit into
nextlevelbuilder:devfrom
mozaa-solana:fix/cron-creator-sender-propagation
Open

fix(cron): stamp + replay creator sender/role across cron fires#1129
mozaa-solana wants to merge 1 commit into
nextlevelbuilder:devfrom
mozaa-solana:fix/cron-creator-sender-propagation

Conversation

@mozaa-solana

Copy link
Copy Markdown
Contributor

Summary

Cron jobs created in a group context (UserID like `group:telegram:-100…`) consistently fail group-scope permission checks the moment they fire — the scheduled agent ends up with an empty SenderID, and any `write_file` or `cron` mutation inside that turn trips:

permission denied: system context cannot ... in group chats. If this is a legitimate user action, ensure the acting sender is preserved through the tool chain.

Root cause: `gateway_cron.go` built the RunRequest with `UserID = job.UserID` but no `SenderID`. `loop_context.injectContext` skips `WithSenderID` when `req.SenderID` is empty, so the agent's resumed ctx had no human attribution. `UserID` alone is the group scope, not a real user.

Fix

Capture the human's sender + role at cron-create time; replay them at every fire.

  • `store.CronPayload`: new `CreatorSenderID` + `CreatorRole` fields (JSON-serialized into the existing `payload` column — no DB migration).
  • `CronStore.AddJob`: signature gains `creatorSenderID` + `creatorRole`. PG + sqlite impls thread them into the new payload fields.
  • `internal/tools/cron.go` `handleAdd`: reads `SenderIDFromContext` + `RoleFromContext`, drops synthetic prefixes (`subagent:`, `teammate:`, `ticker:`, `system:`, `notification:`, `session_send_tool`) so we never attribute future fires to a system component, then calls `AddJob` with the captured creator.
  • `cmd/gateway_cron.go`: passes `job.Payload.CreatorSenderID` + `CreatorRole` into `RunRequest.SenderID` + `Role` each fire. Empty values preserve prior behaviour — DM crons, system-installed crons, and pre-fix crons all keep the deny-on-empty guard at fire time. No security relaxation.
  • `gateway/methods/cron.go` (WS RPC): explicitly passes empty creator strings. There is no separate "Telegram numeric sender" upstream of the WS handler, so WS-installed crons that need to fire in group chats should be created via a Telegram-rooted flow (ctx-aware) instead.

Failure repro

  1. Human pings agent in a group → `@bot create cron neo-daily for the content cycle…`
  2. Agent calls `cron(action="add", ...)` → job stored with `UserID="group:..."`, no creator sender
  3. Cron fires → agent resumes, calls `write_file` to log cycle output
  4. Returns: `permission denied: system context cannot write files in group chats`

After this fix the same flow:
1-3. As above; job now stores `Payload.CreatorSenderID = ""`
4. Cron fires with `req.SenderID = ""` → `write_file` succeeds with attributed audit trail.

Test plan

  • `go build ./...` clean
  • `go test ./internal/store/... ./internal/tools/ ./internal/gateway/methods/ ./cmd/` passes (existing + new)
  • New tests:
    • `TestCronTool_handleAdd_PropagatesRealSender` — Telegram-rooted human dispatch survives roundtrip into `Payload.CreatorSenderID`.
    • `TestCronTool_handleAdd_DropsSyntheticSender` (table-driven, 6 synthetic prefixes) — synthetic ctx senders are deliberately dropped, never attributed.

Related

Companion fix to #1128 (team-task announce sender propagation). Together they close the "system context cannot write" denial across both re-ingress paths a Lead-style agent uses inside a daily-cycle workflow.

Cron jobs created in a group context (group-scope UserID like
"group:telegram:-100…") consistently fail group-scope permission
checks once they fire — the scheduled agent's turn ends up with an
empty SenderID, and any write_file / cron mutation inside that turn
trips:

    permission denied: system context cannot ... in group chats.
    If this is a legitimate user action, ensure the acting sender is
    preserved through the tool chain.

Root cause: gateway_cron.go built RunRequest with UserID=job.UserID
but no SenderID, so loop_context.injectContext skipped WithSenderID
and the agent ran with no human attribution. UserID alone is the
group scope, not a real user.

Fix: capture the human's sender + role at cron-create time, replay
them at every fire.

  - store.CronPayload: new CreatorSenderID + CreatorRole fields
    (JSON-serialized into existing payload column — no migration).
  - CronStore.AddJob: signature gains creatorSenderID + creatorRole;
    PG + sqlite impls thread them into the new payload fields.
  - tools/cron.go handleAdd: read SenderIDFromContext +
    RoleFromContext, drop synthetic prefixes (subagent:, teammate:,
    ticker:, system:, notification:, session_send_tool) so we never
    attribute future fires to a system component, then call AddJob
    with the captured creator.
  - cmd/gateway_cron.go: pass job.Payload.CreatorSenderID +
    CreatorRole into RunRequest.SenderID + Role each fire. Empty
    values preserve prior behaviour (DM crons, system-installed
    crons, pre-fix crons all keep the deny-on-empty guard at fire
    time — no security relaxation).
  - gateway/methods/cron.go (WS RPC): explicitly passes empty
    creator strings — there is no separate "Telegram numeric sender"
    upstream of the WS handler, so WS-installed crons that need to
    fire in group chats should be created via a Telegram-rooted
    flow (ctx-aware) instead.

Tests:
  - internal/tools/cron_creator_propagation_test.go: real-sender
    propagation + synthetic-sender drop coverage (table-driven).
  - internal/gateway/methods/cron_test.go: stub updated to match
    new AddJob signature, captures creator strings into Payload.

This is the cron-firing counterpart to the team-task announce
attribution fix — together they close the "system context cannot
write" denial across both re-ingress paths a Lead-style agent uses.
@mrgoonie mrgoonie added status:blocked Blocked by external dependency or decision maintain:deferred Deferred by maintain workflow agent:github-maintain Processed by github-maintain automation maintain:triaged Triaged by maintain workflow labels Jun 23, 2026
@mrgoonie

Copy link
Copy Markdown
Contributor

Maintainer recheck: current PR head still does not compile as-is. The public CronStore interface changed to AddJob(..., creatorSenderID, creatorRole), but existing store/integration tests and invariant tests still call the old 9-arg signature. A plain repo-wide grep shows old calls remain in internal/store/sqlitestore/cron_crud_test.go, tests/integration/v3_cron_store*_test.go, and tests/invariants/tenant_isolation_test.go, so go test ./... will fail once those packages are included.

Targeted checks I ran:

  • go test ./cmd ./internal/tools ./internal/gateway/methods ✅
  • go test ./internal/tools ./internal/store/sqlitestore ./internal/gateway/methods ❌ here because the local environment excludes sqlite store files via build constraints, but the stale AddJob call sites are still visible in the repo and need updating.

Please update all remaining AddJob call sites to pass explicit creatorSenderID/creatorRole values, usually empty strings for existing tests unless the test is specifically asserting creator propagation. After that, rerun the full store/integration test set.

@mrgoonie mrgoonie 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.

Blocking for now: this changes the CronStore.AddJob interface but leaves old-signature call sites in tests/integration/invariants. That means the branch will fail full compilation/test coverage outside the narrow package subset.

Evidence:

  • grep -R "AddJob(ctx" -n . --exclude-dir=.git on the PR branch still finds old 9-arg calls in:
    • internal/store/sqlitestore/cron_crud_test.go
    • tests/integration/v3_cron_store_test.go
    • tests/integration/v3_cron_store_ext_test.go
    • tests/invariants/tenant_isolation_test.go
  • The implementation call sites in internal/tools/cron.go and internal/gateway/methods/cron.go were updated, but these tests were not.
  • Targeted package checks passed for ./cmd ./internal/tools ./internal/gateway/methods, so the core idea is plausible; this is a completion/compile blocker.

Please update all remaining AddJob test call sites with explicit creatorSenderID, creatorRole arguments (empty strings for legacy cases), then rerun the full cron/store/integration test coverage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:github-maintain Processed by github-maintain automation maintain:deferred Deferred by maintain workflow maintain:triaged Triaged by maintain workflow status:blocked Blocked by external dependency or decision

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants