Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ application-secret.properties

# --- Claude Code / agent state ---
# Per-project agent memory + runtime metadata. Personal to whoever drove the
# session; project-wide docs that should be shared belong in AGENT.md.
# session; project-wide docs that should be shared belong in AGENTS.md.
.claude/
MEMORY.md

Expand Down
5 changes: 3 additions & 2 deletions AGENT.md → AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ changes; it captures the conventions that aren't obvious from the code.
This file is deliberately tool agnostic. Coding agents look for a project guide under various names
(`AGENT.md`, `AGENTS.md`, `CLAUDE.md`); this is the canonical one, and if your tool wants a different
filename, point it here rather than forking the content. A second copy is a second source of truth,
and the stale one always wins an argument eventually.
and the stale one always wins an argument eventually. The `CLAUDE.md` beside this file is exactly
that: a one-line `@AGENTS.md` import, because Claude Code reads `CLAUDE.md` and not `AGENTS.md`.

Quick starts: `QUICKSTART-COMPOSE.md` (containers) · `QUICKSTART-MANUAL.md` (native + systemd).
Production proxy (nginx/haproxy, sizing, the SameSite gotcha): `frontend.md`.
Expand Down Expand Up @@ -47,7 +48,7 @@ App: http://localhost:8080 · Keycloak: http://localhost:8081 · Test users: `al
## Layout

The tree below is a sketch — `service/` and `web/dto/` keep growing; treat the directory listing
as authoritative and AGENT.md as a starting orientation.
as authoritative and AGENTS.md as a starting orientation.

```
src/main/java/ai/intellistream/chat/
Expand Down
8 changes: 4 additions & 4 deletions AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ Each finding is tagged by severity: **[H]** high, **[M]** medium, **[L]** low,

#### Next steps
- Decide the API auth story (bearer-only vs. cookie-bearer hybrid) and document it in
`AGENT.md`. Add an IT that calls `/api/channels` with no Authorization header and asserts
`AGENTS.md`. Add an IT that calls `/api/channels` with no Authorization header and asserts
the documented behaviour.
- Tighten cookie attributes (`SameSite=Strict`, `Secure` when HTTPS).

Expand Down Expand Up @@ -826,7 +826,7 @@ Each finding is tagged by severity: **[H]** high, **[M]** medium, **[L]** low,
| # | Item | Severity | Status |
|---|------|----------|--------|
| 1 | STOMP SUBSCRIBE authorization (block snooping on private channels) | **H** | ✅ done — `StompAuthorizationConfig` |
| 2 | API auth story (cookie vs. bearer) — document & test | **H** | ✅ documented in `AGENT.md` |
| 2 | API auth story (cookie vs. bearer) — document & test | **H** | ✅ documented in `AGENTS.md` |
| 3 | Per-user rate limiting (REST + STOMP) | **H** | ✅ done — `RateLimiter` (in-memory; replace for multi-instance) |
| 4 | Default security headers (CSP, nosniff, HSTS, Referrer-Policy) | **H** | ✅ done in `SecurityConfig` |
| 5 | MIME sniffing on upload + nosniff on download | **M** | ✅ done — `AttachmentService.sniffContentType` + `X-Content-Type-Options` header |
Expand All @@ -835,8 +835,8 @@ Each finding is tagged by severity: **[H]** high, **[M]** medium, **[L]** low,
| 8 | Service-level invariant: thread reply inherits parent channel | **M** | ✅ enforced by `MessageService.replyInThread` (`channel = parent.getChannel()`); covered by `SecurityBoundaryIT` |
| 9 | Cookie attributes (`SameSite=Strict`, `Secure`) | **M** | ✅ `SameSite=Strict` on JSESSIONID + CSRF cookie; `Secure` should be added in HTTPS deploys via `server.servlet.session.cookie.secure=true` |
| 10 | Strict CSP — extract inline `<script>` first | **M** | ✅ done — moved to `theme-loader.js` / `profile.js` |
| 11 | Decide public-channel read posture (member-only vs. anyone-authenticated) | **M** | ✅ documented in `AGENT.md`; current behavior kept |
| 12 | OWASP dependency scanner + dependency upgrades | **M** | ⚠️ deferred — listed under roadmap in `AGENT.md` for opt-in |
| 11 | Decide public-channel read posture (member-only vs. anyone-authenticated) | **M** | ✅ documented in `AGENTS.md`; current behavior kept |
| 12 | OWASP dependency scanner + dependency upgrades | **M** | ⚠️ deferred — listed under roadmap in `AGENTS.md` for opt-in |
| 13 | Validate usernames during OIDC provisioning | **L** | ✅ done — `UserService.sanitizeUsername` |
| 14 | Generic error envelope, redact identifiers | **L** | ✅ done — `ApiExceptionHandler` returns `{code, message, traceId}` with full detail logged server-side only |
| 15 | Reject unknown multipart fields with 400 | **L** | ✅ done in `AttachmentRestController` |
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!-- Canonical agent instructions live in AGENTS.md, which agent tooling reads by
convention. Claude Code reads CLAUDE.md rather than AGENTS.md, so this file imports it.
Keep this a one-line import and edit AGENTS.md instead. -->
@AGENTS.md
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ sizes are welcome.

## Ground rules

- **Read `AGENT.md` first.** It captures the conventions that aren't obvious from the code —
- **Read `AGENTS.md` first.** It captures the conventions that aren't obvious from the code —
the two security filter chains, the read-vs-write channel access split, the STOMP
authorization model, the server-side Markdown sanitization, the embedded-Lucene search, and
the "don'ts" (no SPA framework, no ILIKE search, no H2 in tests, no `ddl-auto=update`).
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ codebase is small, conventional and covered:
- **981 tests across 101 classes** (47 integration, 54 unit), running in about six minutes.
Integration tests run against a real PostgreSQL via Testcontainers — never H2, which silently
accepts SQL that Postgres rejects.
- **Conventions are written down.** [`AGENT.md`](AGENT.md) documents the decisions you cannot infer
- **Conventions are written down.** [`AGENTS.md`](AGENTS.md) documents the decisions you cannot infer
from the code: the two security filter chains, `requireMember` vs `requireWriteAccess`, why
broadcast happens after commit, why there is no SockJS. Read it before your first change.
- **Security posture is explicit.** A strict CSP with no inline script, two separate filter chains,
Expand Down Expand Up @@ -227,7 +227,7 @@ scratch. A feature typically touches one service, one controller, one migration
| `intellistream-chat` | Gradle artifact, systemd unit, `/opt` path |

Then regenerate `V1__init.sql`.
2. **Read `AGENT.md` and keep it current.** It is the conventions document for the project, and it
2. **Read `AGENTS.md` and keep it current.** It is the conventions document for the project, and it
is worth more to a new contributor than any amount of generated API documentation. Update it as
your fork diverges.
3. **Write the change down before writing it.** Acceptance criteria beat prose: *"polls auto-close
Expand Down
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,7 @@ <h3><span class="num">1</span>Fork and rename</h3>
</div>

<div class="qs-step">
<h3><span class="num">2</span>Read (and own) <code>AGENT.md</code></h3>
<h3><span class="num">2</span>Read (and own) <code>AGENTS.md</code></h3>
<p class="qs-note">Claude Code reads it on every invocation. It codifies the conventions that aren't obvious from the code, the two filter chains, read-vs-write access checks, server-side Markdown render, the strict CSP, embedded Lucene, Testcontainers + real Postgres. Keep it in sync as your fork diverges; Claude follows whatever's in there.</p>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
*
* <p><b>Why this exists at all.</b> Two enforcement points cannot afford the database. The STOMP
* inbound interceptor runs on the message send path, which is deliberately query-free (see
* AGENT.md) — a {@code select} per frame would undo the work that made it fast. The servlet filter
* AGENTS.md) — a {@code select} per frame would undo the work that made it fast. The servlet filter
* runs before the request reaches anything that resolves a domain {@code User}, so it has a token
* and nothing else; resolving one there would double the user lookup every page load already pays.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,7 @@ public void requireWriteAccess(Channel channel, User user) {
* WebSocket send paths, thread replies, edits, reactions, attachments, typing pings, invites,
* poll votes and {@code /remind} are nine or ten separate entry points across five classes, and
* a rule enforced in ten places is a rule enforced in nine as soon as an eleventh is written.
* AGENT.md already tells new write endpoints to call {@code requireWriteAccess}; hanging this off
* AGENTS.md already tells new write endpoints to call {@code requireWriteAccess}; hanging this off
* that instruction means a new one inherits the rule without its author having heard of
* archiving. The two paths that genuinely cannot use it — {@link #join}, where you are not a
* member yet, and {@link #rename} — call this directly, and they are the exceptions precisely
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ static Broadcast broadcastAmong(Set<String> handles) {
* <p>Not {@code mentionRepo.save()} in a loop. {@code MessageMention.id} is {@code IDENTITY},
* which disables Hibernate's insert batching outright — every {@code save} is its own round
* trip, so a 1,000-member channel would turn one message into 1,000 of them on the send path,
* which AGENT.md is explicit is the hot path. The {@code select … from users} form exists so
* which AGENTS.md is explicit is the hot path. The {@code select … from users} form exists so
* the id list can travel as a single expanded parameter, and {@code on conflict do nothing}
* makes the statement idempotent against a personal mention of the same user in the same body.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
*
* <p>State resets on app restart by design; the persisted custom status is the only thing that
* survives. Single-instance only — multi-node would swap this for shared state (see the
* horizontal-scaling notes / AGENT.md's RateLimiter migration path).
* horizontal-scaling notes / AGENTS.md's RateLimiter migration path).
*/
@Component
public class PresenceTracker {
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/static/js/chat/mention-autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ function avatarFor(item) {

/**
* Stand-in for the avatar on a broadcast row. From the SVG sprite, not an emoji: the sprite icons
* inherit currentColor and so follow the theme, which a glyph cannot (see AGENT.md).
* inherit currentColor and so follow the theme, which a glyph cannot (see AGENTS.md).
*/
function broadcastIcon() {
const wrap = document.createElement('span');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ void theCleanupReconcileRewritesAMissingConversationDocumentWithItsFilenames()

@Test
void aReconcileSweepLeavesAnUpToDateDocumentWithFilesAlone() throws java.io.IOException {
// The failure AGENT.md warns about, in its quietest form: a sweep that decided a document
// The failure AGENTS.md warns about, in its quietest form: a sweep that decided a document
// it did not recognise was stale, or that rewrote a healthy one from a projection missing
// half its fields. Either way the file is gone from search an hour after it was uploaded,
// and nothing anywhere says so.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

/**
* Static guard for the "Channel members" panel in {@code chat.js}. The project deliberately
* doesn't run JS in tests (no Node, no headless browser — see AGENT.md), so this is the
* doesn't run JS in tests (no Node, no headless browser — see AGENTS.md), so this is the
* closest we can get to catching a class of regression where the panel block refers to a
* {@code window.ChatKit} symbol before chat.js has destructured it locally.
*
Expand Down
Loading