When a gateway caller supplies a callerSessionKey it is explicitly
requesting session-scoped access (multi-agent / multi-user deployments).
Previously, resolveCronCallerOptions unconditionally set ownerOverride
to true whenever the client held ADMIN_SCOPE, which meant the
service-layer ownership check was a no-op for every mutation
(cron.update, cron.remove, cron.run) since those methods all require
ADMIN_SCOPE.
Now ownerOverride is only true when the client is an admin that did NOT
supply a session key — the typical local-CLI / control-UI case. When a
session key is present the ownership check fires as intended.
Also exports resolveCronCallerOptions and adds direct unit tests
covering admin + sessionKey, admin without sessionKey, non-admin, and
null client scenarios.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The per-caller ownership enforcement introduced for issue #35447 was
silently bypassed: all four mutation/list schemas used
additionalProperties:false but did not declare callerSessionKey, causing
AJV to strip the field before the handler could read it. As a result
resolveCronCallerOptions always received an empty caller and fell back to
allow-all behaviour.
Fix:
- Add optional callerSessionKey (NonEmptyString) to CronListParamsSchema,
CronUpdateParamsSchema, CronRemoveParamsSchema and CronRunParamsSchema.
- Update the four handlers in server-methods/cron.ts to read
p.callerSessionKey instead of the previous p.sessionKey (which was
never populated through these schemas).
- Add validator tests covering acceptance of the new field and rejection
of empty strings across all four operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes#35447
In multi-user deployments (Telegram, Slack, DingTalk) the cron service
exposed all jobs to all callers. Any session could list, remove, update,
or trigger jobs created by a different agent/session.
Changes:
- service/ops.ts: Add `CronMutationCallerOptions` type (callerAgentId,
callerSessionKey, ownerOverride). Add `callerOwnsJob()` helper that
matches by agentId or sessionKey and falls back to allow when no
owner metadata is present (backward compat). Thread the caller opts
through `listPage`, `remove`, `update`, `enqueueRun`, `run`, and the
internal `inspectManualRunPreflight`/`prepareManualRun` helpers.
Mutations on a job owned by a different session throw a structured
error with code CRON_PERMISSION_DENIED.
- service.ts: Expose the new optional caller parameter on the public
CronService methods (update, remove, run, enqueueRun).
- gateway/server-methods/cron.ts: Add `resolveCronCallerOptions()` that
extracts the caller sessionKey from request params and sets
ownerOverride=true when the client holds the operator.admin scope.
Pass the resolved caller opts into cron.list, cron.update, cron.remove,
and cron.run. Respond with PERMISSION_DENIED on CRON_PERMISSION_DENIED.
- gateway/protocol/schema/error-codes.ts: Add PERMISSION_DENIED error code.
- service.session-isolation.test.ts: 19 new tests covering listPage
filtering, and remove/update/enqueueRun ownership enforcement including
admin bypass (ownerOverride) and legacy job backward compatibility.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>