Skip to content

Hardening pass + 4 capability additions (review-driven)#1

Merged
gauravpsingh07 merged 11 commits into
mainfrom
feature/hardening-and-features
Jul 7, 2026
Merged

Hardening pass + 4 capability additions (review-driven)#1
gauravpsingh07 merged 11 commits into
mainfrom
feature/hardening-and-features

Conversation

@gauravpsingh07

Copy link
Copy Markdown
Owner

Implements every issue and feature surfaced by the deep code review, one focused commit each (11 total).

Fixes / hardening

  • fix(actions): GET /api/agent-actions now filters/sorts/paginates in the DB via a JPA Specification (was loading the whole table into memory).
  • perf(policy): cache compiled regex patterns + batch-load conditions (findByPolicyIdIn) — removes per-request recompilation and an N+1.
  • perf(agents): stamp lastUsedAt with a targeted @Modifying update, off the optimistic-lock path.
  • refactor(messaging): emit Kafka/RabbitMQ events via @TransactionalEventListener(AFTER_COMMIT) — no more phantom events on rollback.

Features

  • feat(approvals): scheduled expiry sweeper — PENDING past deadline → EXPIRED, action failed closed to DENIED, APPROVAL_EXPIRED audit event (wires up previously-dead status/audit/filter).
  • feat(actions): POST /api/agent-actions/{id}/complete — agent reports outcome → COMPLETED/FAILED.
  • feat(security): dependency-free fixed-window rate limiting on login (per IP) + ingestion (per key), 429 + Retry-After.
  • feat(messaging): Kafka + RabbitMQ consumers projecting into an in-memory LiveMetricsService (topics/queues finally have a reader).
  • feat(realtime): SSE GET /api/stream; Angular approval inbox live-reloads with a Live/Offline badge.
  • feat(policy): field-level before/after diff in POLICY_UPDATED audit entries.
  • docs: new endpoints, config keys, and the stateless-JWT / localStorage tradeoff.

Verification

  • Backend mvnw verify: 147 tests green (51 unit + 96 integration, H2).
  • Frontend: 57 Karma specs green; production build clean.
  • Secret scan of the branch diff: no secret-like files or content.

Caveats (as flagged during planning): the live Kafka/RabbitMQ broker round-trip (commit 8) and live end-to-end SSE (commit 9) require the docker stack + both apps running and are not exercised by the automated suites — those verify compilation, conditional guards, unit/handler logic, and auth.

🤖 Generated with Claude Code

gauravpsingh07 and others added 11 commits July 7, 2026 18:18
The GET /api/agent-actions list endpoint loaded the entire
action_requests table into memory with findAll(), then filtered,
sorted, and paginated in the application layer. This is inconsistent
with the audit and approval endpoints (which already use
Specifications) and does not survive real data volume.

Introduce ActionRequestSpecifications and have the repository extend
JpaSpecificationExecutor so filtering, ordering, and pagination are
all pushed to the database. Existing ActionIngestionIT coverage
(adminCanListActions, filterByStatus) exercises the new path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The policy evaluator sits on the action-ingestion hot path yet did two
wasteful things per request: it recompiled every resource-pattern and
condition regex from scratch, and it fired one findByPolicyId query per
enabled policy (an N+1).

Cache compiled Patterns by their source string in both PolicyEvaluator
and ConditionEvaluator, and batch-load all conditions for the enabled
policy set with a single findByPolicyIdIn, grouping them in memory.
Behaviour is unchanged; the existing 35 policy tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
markUsed() previously did a findById + setter + save on every action
submission. Because Agent extends the @Version-locked BaseEntity, that
read-modify-write put the hottest write in the system on the optimistic-
locking path, so two actions from the same agent could collide on the
version column.

Replace it with a @Modifying UPDATE that sets last_used_at (and
updated_at) directly by id, leaving the version column untouched. The
query flushes and clears automatically so no stale managed instance
lingers in the persistence context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kafka events and RabbitMQ tasks were published inside the ingestion and
approval transactions. A rollback after a send left the broker holding an
event describing state that never became durable — a classic dual-write
hazard.

Introduce internal Spring application events (ActionReceived/Decided,
ApprovalRequested, ActionCompleted) that the services publish in-band. A
new TransactionalMessagingForwarder relays them to the existing publishers
under @TransactionalEventListener(AFTER_COMMIT), so nothing reaches a
broker unless the transaction commits. publishReceived now stamps the
RECEIVED status as a constant rather than reading the entity (whose status
is mutated later in the same transaction), keeping the deferred payload
correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The EXPIRED approval status, the APPROVAL_EXPIRED audit event, and the
dashboard's EXPIRED filter were all defined but unreachable: nothing ever
transitioned a past-deadline PENDING request, so it stayed PENDING forever.

Add ApprovalExpiryService with a @scheduled sweep (interval configurable
via agentops.approvals.expiry.*) that moves due PENDING approvals to
EXPIRED and fails the underlying action closed (DENIED with an explanatory
reason), emits the APPROVAL_EXPIRED audit event, and fans out the
completion to the brokers. Scheduling is enabled outside the test profile;
the worker is directly callable so ApprovalExpiryServiceIT verifies it
without waiting on the clock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The action lifecycle stopped at ALLOWED/APPROVED — the firewall cleared an
action but never learned whether the agent actually ran it, leaving the
ACTION_COMPLETED audit event and completed Kafka topic underused for
directly-allowed actions.

Add POST /api/agent-actions/{id}/complete: the agent reports success or
failure and the action moves to the new COMPLETED / FAILED terminal state.
Authentication reuses X-Agent-Key, verified against the action's own agent
via a new authenticate(agentId, key) overload, so only the owning agent can
close an action. Only ALLOWED/APPROVED actions are completable (else 409);
unknown ids 404, wrong keys 401. The completion is audited and fanned out
to Kafka (approval-less, so no Rabbit notification).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neither the login endpoint nor the agent-action ingestion path had any
throttling, so credential brute-force and request floods were unbounded.

Add a dependency-free fixed-window limiter (FixedWindowRateLimiter) and a
RateLimitFilter that runs ahead of the security chain: POST /api/auth/login
is limited per client IP, and agent submit/complete per hashed agent key
(IP fallback). Over-limit requests get a uniform 429 ApiError with a
Retry-After header before any auth or DB work. Limits are configurable
under agentops.security.rate-limit.* and disabled in the test profile so
the suite stays deterministic; a dedicated RateLimitFilterIT re-enables it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Kafka topics and RabbitMQ queues were write-only — nothing read them,
so the "event stream + work queue" architecture had no consumer side.

Add a Kafka @KafkaListener over the received/decided/completed topics and a
RabbitMQ @RabbitListener over the approval-request/notification queues, both
projecting events into a new in-memory LiveMetricsService (per-type counters,
a bounded recent-events buffer, and a subscriber hook the SSE feed will use).
Consumers are guarded by agentops.messaging.consumers.enabled (and, for
Rabbit, the presence of a RabbitTemplate) and disabled in the test profile
so no broker connection is attempted; the handlers are unit-tested directly.

Note: the live broker round-trip requires the docker stack and is not
exercised by the test suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dashboard and approval inbox only refreshed on user action, and the
newly-added broker consumers had no outward surface. Add a live feed so
consumed events reach the UI without polling.

Backend: GET /api/stream is an SSE endpoint that sends an initial snapshot
(counters + recent buffer) then an "activity" event per consumed broker
event, sourced from LiveMetricsService. Because EventSource cannot set an
Authorization header, JwtAuthenticationFilter also accepts the JWT as an
access_token query parameter, scoped to the stream path.

Frontend: EventStreamService wraps EventSource (token via query param) and
exposes events$/snapshot$/connected; the approval inbox connects on init,
live-reloads when an approval task flows through the queue, and shows a
Live/Offline badge.

Verified: backend IT (auth + async-start), 5 new Karma specs, production
build. Live end-to-end behaviour needs the docker brokers + both apps
running and is not exercised by the test suites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POLICY_UPDATED audit entries only noted that a policy changed, not what
changed — an admin auditing a policy edit could not see the before/after.

Snapshot the policy's mutable fields before applying an update and record a
field-level diff ({field: {from, to}}) plus a conditionsReplaced flag in the
audit details, including only fields that actually changed. Unchanged fields
are omitted. Verified by a new PolicyControllerIT case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reflect the hardening/feature work across the reference docs:

- docs/api.md — action-completion callback, SSE stream endpoint, a Rate
  limiting section, the 429 status, approval-expiry behaviour, and an
  explicit note on the stateless-JWT / localStorage tradeoff.
- docs/architecture.md — after-commit publishing, the Kafka/Rabbit
  consumers feeding LiveMetricsService, the SSE live stream, the approval
  expiry sweeper, and the completion lifecycle.
- README.md — refreshed feature summary and test counts (147 backend,
  57 Karma).
- CHANGELOG.md — Unreleased section covering all ten changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gauravpsingh07
gauravpsingh07 merged commit 8828a76 into main Jul 7, 2026
2 checks passed
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