Skip to documentation
Documentation navigation

Documentation navigation

Documentation / guides

Event visibility

Keep internal events out of public event streams.

developer

The HTTP surface exposes only events that the event package marks public. This is enforced at all outward serialization boundaries, not only by the caller’s subscription filter. A private hustle event or an event with an unknown visibility value must never reach a status response, journal response, or live SSE stream.

Public predicate

The serve package applies the event package’s authoritative predicate to a whole-session filter:

// The serve package uses this equivalent boundary for every outward delivery.
func isPublicEvent(ev event.Event) bool {
	return event.ShouldDeliver(event.EventFilter{
		Ephemeral: event.LoopScope{All: true},
		Enduring:  event.LoopScope{All: true},
	}, ev)
}

The filter selects the whole session. The predicate still rejects an event whose visibility is event.Internal or an unknown value. Public event types can then be encoded using the durable event codec or the explicit live DTOs.

Read boundaries

Status and journal reads validate event-bearing DTOs again immediately before writing JSON:

BoundaryPrivate event behaviorClient result
GET /v1/sessions/{sid}/status with private LastTurn or LastStepvalidateSessionStatus returns *NonPublicEventErrorgeneric 500 internal
GET /v1/sessions/{sid}/journal with private eventvalidateEventJournalPage rejects the pagegeneric 500 internal
StatusEvent.MarshalJSONvalidateStatusEvent rejects before event.MarshalEventmarshal error, classifiable with errors.As

The error retains only the visibility value:

var visibilityErr *serve.NonPublicEventError
if errors.As(err, &visibilityErr) {
	// Treat this as a server-side projection or adapter violation. Do not send
	// the event type or payload to a client while diagnosing it.
	log.Printf("non-public event visibility=%d", visibilityErr.Visibility)
}

The response body remains generic. Tests explicitly check that private hustle names, prompts, run descriptors, and visibility fields are not leaked.

Live boundary

The SSE encoder applies the same predicate before its Enduring or Ephemeral type switch. A private delivery is skipped and the stream continues. An unrecognized public delivery is also skipped if its wire shape cannot be represented safely. This is fail closed: no ad hoc JSON fallback exists.

%%{init: {"theme":"dark"}}%%
flowchart LR
    D[event.Delivery] --> V{public visibility?}
    V -- no --> X[skip or reject at boundary]
    V -- yes --> C{known class and wire shape?}
    C -- no --> Y[skip without lossy JSON]
    C -- yes --> O[public status, journal, or SSE frame]

The pkg/sessionstore public event replayer is another defense in depth for durable reads, but serve still validates the returned DTO because its Reader interface is intentionally adapter-neutral.

Source and runnable proof

go test ./pkg/serve -run 'Test(StatusEvent|ReadHandlers|EncodeDelivery|HandleEventsSkipsNonPublic)'

← back to documentation