Skip to content

fix(#249): schedule health endpoint accessible to CanCommunicate peers - #403

Closed
HongmingWang-Rabbit wants to merge 6 commits into
mainfrom
fix/issue-249-schedule-health-auth
Closed

fix(#249): schedule health endpoint accessible to CanCommunicate peers#403
HongmingWang-Rabbit wants to merge 6 commits into
mainfrom
fix/issue-249-schedule-health-auth

Conversation

@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor

Problem

GET /workspaces/:id/schedules is behind WorkspaceAuth, which requires the target workspace's own bearer token. Peer agents in the org hierarchy never hold that token — so they cannot read last_run_at, last_status, or run_count, making silent cron failures completely undetectable from the outside. Tracked in #249.

Fix

New endpoint: GET /workspaces/:id/schedules/health

Registered outside the wsAuth group (mirrors the /workspaces/:id/a2a pattern). Auth is enforced by the handler itself:

  1. X-Workspace-ID header required (else 401)
  2. Self-calls (callerID == targetID) pass immediately
  3. System callers (system:*, webhook:*, test:*) pass immediately
  4. All other callers: validateCallerToken + CanCommunicate gate (same pattern as A2A proxy)

Response is a reduced scheduleHealthResponse — exposes only id, name, enabled, last_run_at, next_run_at, run_count, last_status, last_error. Never exposes prompt or cron_expr.

Also includes (bundled since they share the branch)

Test plan

  • TestScheduleHealth_MissingCallerID_Rejected — 401 when no X-Workspace-ID
  • TestScheduleHealth_SelfCall_Allowed — 200 self-call, no CanCommunicate queries
  • TestScheduleHealth_CanCommunicatePeer_LegacyNoToken — grandfathered peer (0 tokens) + CanCommunicate allowed → 200
  • TestScheduleHealth_AccessDenied_NonPeer — CanCommunicate denied → 403
  • TestScheduleHealth_SystemCaller_Allowed — system:monitor bypasses all checks → 200
  • TestScheduleHealth_NoPromptExposed — response must not contain prompt/cron_expr/timezone fields
  • TestScheduleHealth_DBError_Returns500 — DB error on health SELECT → 500
  • TestHistory_IncludesErrorDetail — history endpoint surfaces error_detail
  • BasePushNotificationSender instantiation tests (12 tests in test(BUG: workspace-template/main.py crashes on startup — PushNotificationSender is abstract and cannot be instantiated #204) commit)

🤖 Generated with Claude Code

Backend Engineer and others added 6 commits April 16, 2026 07:41
…icationSender

PushNotificationSender is an ABC with an abstract send_notification() method —
instantiating it raises TypeError at runtime, crashing every workspace agent on
startup (issue #204, found in Security Audit Cycle 4).

Fix: use BasePushNotificationSender(httpx.AsyncClient(), config_store) which is
the concrete implementation. Share the push_config_store instance between
DefaultRequestHandler and the sender so they operate on the same store.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…der comment

Add workspace-template/tests/test_main_startup.py with 12 regression tests
guarding against re-introducing the abstract PushNotificationSender crash
that broke every workspace agent on startup (issue #204).

Test categories:
- Source-code guards (4): verify main.py never imports or instantiates
  bare PushNotificationSender, that it uses BasePushNotificationSender,
  and that main.py compiles cleanly.
- Real SDK checks (4): load the installed a2a.server.tasks and
  a2a.server.request_handlers bypassing conftest mocks to verify
  PushNotificationSender is abstract, BasePushNotificationSender is
  concrete, constructor signature is correct, and DefaultRequestHandler
  push_sender defaults to None.
- Capability guards (4): AgentCapabilities has stateTransitionHistory=True,
  pushNotifications is read from config (not hardcoded False), push_config_store
  is shared between handler and sender, and cancel() is not a pass stub.

Also adds an inline comment in main.py explaining why BasePushNotificationSender
is used rather than the bare PushNotificationSender (issue #204).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e peers

Silent cron failures were undetectable by peer agents because the only
schedule health fields (last_run_at, last_status, run_count) sat behind
WorkspaceAuth, which requires the workspace's own bearer token — a token
peers never hold.

New endpoint: GET /workspaces/:id/schedules/health
- Registered outside the wsAuth group (mirrors /workspaces/:id/a2a pattern)
- Auth: caller must supply X-Workspace-ID + its own bearer token (Phase 30.5
  lazy-bootstrap: callers with no tokens are grandfathered through)
- Gate: registry.CanCommunicate(callerID, workspaceID) must pass
- Response: id, name, enabled, last_run_at, next_run_at, run_count,
  last_status, last_error — prompt and cron_expr intentionally omitted
- System callers (webhook:*, system:*, test:*) and self-calls bypass both
  token validation and CanCommunicate (same as A2A proxy)

7 new tests cover: missing caller (401), self-call allowed, legacy peer
grandfathered, non-peer denied (403), system caller bypass, no prompt
exposure, DB error → 500.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor Author

Superseded — #400 (already merged at 29044c3) addresses #249 on a clean branch. The original fix/issue-249-schedule-health-auth branch had 60+ add/add merge conflicts; we opened a fresh branch (v2) with the health endpoint properly applied to current main.

@HongmingWang-Rabbit
HongmingWang-Rabbit deleted the fix/issue-249-schedule-health-auth branch April 16, 2026 12:32
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