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
57 changes: 57 additions & 0 deletions .agent/knowledge/data-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -706,3 +706,60 @@ Document API and data-shape assumptions that must stay compatible over time.
- Evidence (schema/tests/path): `activities/mobcode/shared/types.ts`; `activities/mobcode/server/routes.ts`; `activities/mobcode/server/routes.test.ts`; `activities/mobcode/client/manager/MobCodeManager.tsx`; `activities/mobcode/client/student/MobCodeStudent.tsx`
- Follow-up action: Keep any new student-visible payload derived from the published instructor version and participant-scoped workspace snapshot.
- Owner: Codex

## Learn SyncDeck identity-fingerprint cross-system correlation contract

- Date: 2026-08-07
- Surface: internal module | Learn SyncDeck integration logging
- Contract: ActiveBits' only session identity is `mappingId = HMAC-SHA256(sharedSecret, "syncdeck\n<provider>\n<resourceLinkId>")` (`mappingId()` in `activities/syncdeck/server/learnIntegration.ts`) — there is no `context_id` or "slot" concept on the ActiveBits side; any such composition happens upstream, in whatever `provider`/`resourceLinkId` Learn sends per request. To let Learn and ActiveBits correlate requests without either side logging raw identity values, ActiveBits computes three **non-reversible** log fingerprints using the same shared HMAC secret used for Learn request signing: `providerFingerprint = HMAC-SHA256(sharedSecret, "learn-provider|" + provider).hex.slice(0,16)`, `resourceLinkFingerprint = HMAC-SHA256(sharedSecret, "learn-resource-link|" + resourceLinkId).hex.slice(0,16)`, `mappingFingerprint = HMAC-SHA256(sharedSecret, "learn-mapping|" + mappingId).hex.slice(0,16)` (see `identityFingerprint`/`identityFingerprints` in `learnIntegration.ts`; the HMAC input is `parts.join('|')` over `[domainTag, value]`). A `sessionFingerprint` for the internal SyncDeck session id uses the domain tag `"learn-session"` the same way. Learn must use the identical domain-separation strings, `|`-joined two-part HMAC input, secret, and 16-hex-char truncation to produce fingerprints that match ActiveBits' logs byte-for-byte for the same underlying value.
- Compatibility constraints: These are log-correlation identifiers only, never returned in any API response and never used for authorization or session lookup (`mappingId` remains the actual lookup key). Changing the domain-separation tags, the join character (`|`), the HMAC algorithm, or the truncation length breaks correlation with Learn's matching implementation and with historical logs — treat this as a cross-system contract, not an internal implementation detail, and coordinate any change with the Learn team.
- Validation rules: `status`, `student-entry`, `start`, and `stop` each emit a `learn-identity-resolved` log line (or, for the in-flight `start` retry case, a schema-compatible `learn-instructor-session-start-pending` line) with `operation`, `state`, `reused` (where applicable), the three identity fingerprints, and `sessionFingerprint`. Pending starts always emit `operation: "start"`, `reused: false`, and `sessionFingerprint: null`. None of these lines include raw `provider`, `resourceLinkId`, the internal mapping id, internal session ids, handoff/launch URLs, or browser tokens.
- Evidence (schema/tests/path): `activities/syncdeck/server/learnIntegration.ts`; `activities/syncdeck/server/learnIntegration.test.ts`; `.agent/knowledge/security-notes.md` (2026-08-07 entry).
- Follow-up action: Once Learn ships its matching fingerprint implementation, verify a sample of correlated `provider`/`resourceLinkId` values produce identical fingerprints across both systems' logs before relying on this for incident tracing.
- Owner: Codex

## Generic session-store `linkedSessionId` keepalive contract

- Date: 2026-08-08
- Surface: internal module | `server/core/sessions.ts` (`SessionStore.touch`)
- Contract: Any session record may declare `data.linkedSessionId: string` pointing at another record in the same store. `SessionStore.touch(id)` — for both `InMemorySessionStore` and the Valkey-wrapped store returned by `createSessionStore()` — checks the touched record's `data.linkedSessionId` and, when present and not self-referential, directly refreshes that linked record without inspecting its own link (single hop, not recursive/multi-hop; mirrors the existing `embeddedParentSessionId` propagation already used by `get`/`consumeSessionDataToken`/`refreshSessionExpiry`, but that field is intentionally kept separate and is not itself propagated by `touch`). Any activity-agnostic caller that already touches session A on real activity (e.g. `server/core/wsRouter.ts`'s 30s websocket ping/pong/message loop, which calls `sessions.touch(ws.sessionId)`) will therefore also keep session B alive for as long as A has a connected client, with no changes needed to `wsRouter.ts` or to whatever drives A's own touches.
- Compatibility constraints: This is a generic store-level primitive, not owned by any one activity — the field name and propagation behavior must stay activity-agnostic per the Activity Containment Policy. First consumer: `activities/syncdeck/server/learnIntegration.ts` stamps `data.linkedSessionId` on a live SyncDeck session (`type: 'syncdeck'`) with its Learn entry-mapping id (`type: 'syncdeck-learn-entry'`) via `linkLiveSessionToEntry()`, so the entry's lifetime tracks the session's actual connectivity instead of depending solely on Learn re-polling `status`/`start`. Any future activity needing "this record should stay alive exactly as long as that other record has real activity" should reuse this field rather than adding a new one.
- Validation rules: `touch()` only propagates one hop and only when `linkedSessionId !== id`; the direct-refresh helper never follows the linked record's own link, so chains stop after one hop and two-record cycles complete safely. Only `touch()` propagates `linkedSessionId` — `get()`/`refreshSessionExpiry()`/`consumeSessionDataToken()` do not, since reading a session (as opposed to genuine touch/keepalive activity) is not the right signal for "keep the linked record alive."
- Evidence (schema/tests/path): `server/core/sessions.ts` (`getLinkedSessionId`, `InMemorySessionStore.touch`, the Valkey-wrapped `touch` closure); `server/sessionStore.test.ts`; `activities/syncdeck/server/learnIntegration.ts` (`linkLiveSessionToEntry`); `activities/syncdeck/server/learnIntegration.test.ts`; `.agent/knowledge/security-notes.md` (2026-08-08 entry).
- Follow-up action: If a second activity adopts `linkedSessionId`, consider whether single-hop propagation is still sufficient before extending it to chains.
- Owner: Codex

## Learn active-entry lifetime and fingerprint regression vector

- Date: 2026-08-08
- Contract: A Learn entry in `waiting` state is bounded by `data.expiresAt`; once `active`, its backing session-store TTL is the lifetime authority and is refreshed through the live session's `linkedSessionId`. Active-entry reads must not reject the mapping solely because its historical `data.expiresAt` has passed.
- Observability: Learn lifecycle logs use the documented HMAC fingerprints for provider, resource, mapping, and session identity. The exact shared test vector in `.agent/plans/learn-syncdeck-session-integration.md` is asserted in `activities/syncdeck/server/learnIntegration.test.ts`; change it only through a coordinated cross-system migration.
- Owner: Codex

## Activity normalizers preserve generic keepalive links

- Date: 2026-08-08
- Contract: An activity normalizer that reconstructs `session.data` must retain a valid generic `data.linkedSessionId`. SyncDeck's normalizer preserves the trimmed non-empty string so its Learn live session can keep the linked entry mapping alive.
- Validation: `server/sessionStore.test.ts` registers the real SyncDeck normalizer, writes a `syncdeck` record through `createSessionStore()`, and verifies both persistence and linked touch propagation.
- Owner: Codex

## Learn live-session link lifecycle

- Date: 2026-08-08
- Contract: A Learn-created SyncDeck session carries `linkedSessionId` only while its entry mapping is active. Learn stop and either instructor-activation rollback clear the link, but only if it still matches that mapping, so a stale socket cannot refresh a recreated entry mapping.
- Validation: `activities/syncdeck/server/learnIntegration.test.ts` verifies the link is absent from the live session after a Learn stop.
- Owner: Codex

## Cross-instance linked-session invalidation

- Date: 2026-08-08
- Contract: In the Valkey-backed store, a cached source record is re-read from Valkey at most once per five seconds before its `linkedSessionId` is propagated. This makes a remote stop/unlink authoritative within that bounded interval without a Valkey read for every websocket touch.
- Validation: `server/sessionStore.test.ts` uses two independent wrapped stores over one fake Valkey record map and verifies the second store does not refresh a target after the first store removes the link, while an immediate subsequent touch performs no extra read.
- Owner: Codex

## Learn lifecycle failure logging

- Date: 2026-08-08
- Contract: Instructor-start lifecycle errors use an allowlisted schema of event, operation, requestId, stable errorCode, identity fingerprints, and `sessionFingerprint`; error messages and arbitrary context values are excluded. Error logs never include raw Learn resource identifiers or internal session IDs.
- Validation: `activities/syncdeck/server/learnIntegration.test.ts` scans both captured info and error lifecycle logs, including a forced instructor-start failure containing sentinel resource, session, URL, and token values.
- Owner: Codex
95 changes: 95 additions & 0 deletions .agent/knowledge/security-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,101 @@ Track security-relevant boundaries, risks, and mitigation decisions.
- Follow-up action: Use a short class/day expiry from Learn when generating links.
- Owner: Codex

- Date: 2026-08-07
- Area: Learn SyncDeck identity-fingerprint logging (session-split investigation)
- Threat or risk: A session-split incident (instructor editor showed an active session/join
code; a student's launch with the same intended placement was handed off to a waiting
room; the instructor's later reopen landed in a separate session the student had started)
could not be traced after the fact, because `status`/`student-entry` logged nothing on
success and no log line captured `provider`, `resourceLinkId`, or the computed mapping id
needed to tell whether the three requests resolved to the same internal identity.
- Control or mitigation: Added `learn-identity-resolved` log lines (and inline fingerprint
fields on the existing `learn-instructor-session-start-pending` log) to `status`,
`student-entry`, `start`, and `stop`. Each line records `operation`, resolved `state`,
`reused` (for `student-entry`/`start`), and three **non-reversible** fingerprints —
`providerFingerprint`, `resourceLinkFingerprint`, `mappingFingerprint` — plus a
`sessionFingerprint` for the internal SyncDeck session, all derived via a domain-separated
HMAC-SHA256 (truncated to 16 hex chars) keyed by the same shared Learn HMAC secret used for
request signing. Raw `provider`, `resourceLinkId`, the internal mapping id, and internal
session ids are never included in these lines; handoff/launch URLs and browser tokens are
never logged. See `identityFingerprint`/`identityFingerprints`/`logIdentityResolution` in
`activities/syncdeck/server/learnIntegration.ts`.
- Residual risk: Fingerprints are stable only as long as the shared HMAC secret is unchanged;
rotating the secret makes historical and post-rotation fingerprints for the same identity
incomparable (expected — same tradeoff as `mappingId` itself). Fingerprints reveal identity
*equality/inequality* across log lines, not the underlying values, by design.
- Validation (test/review/path): `activities/syncdeck/server/learnIntegration.test.ts` (asserts
fingerprint stability across `student-entry`/`start`/`status`/`stop` calls for the same
resource, divergence for a different `resourceLinkId`, and that `learn-identity-resolved`
lines never contain the raw resourceLinkId, session id, or handoff/token material).
- Follow-up action: Learn is adding a matching fingerprint scheme on their side (see the
`learn-provider`/`learn-resource-link`/`learn-mapping` domain-separation contract in
`.agent/knowledge/data-contracts.md`) so calls can be correlated across both systems without
either side logging raw identity values. If a future incident needs finer-grained tracing,
extend `logIdentityResolution` call sites rather than logging raw values.
- Owner: Codex

- Date: 2026-08-08
- Area: Learn SyncDeck entry-mapping TTL (session-split root cause)
- Threat or risk: Root-caused the session-split class from the entry above to two compounding
bugs, neither requiring any `provider`/`resourceLinkId` mismatch: (1) the active-entry TTL
fallback `sessions.ttlMs ?? WAITING_TTL_MS` in `learnIntegration.ts` silently resolved to the
10-minute `WAITING_TTL_MS` in production, because the Valkey-backed store returned by
`createSessionStore()` never exposes a top-level `ttlMs` (only `InMemorySessionStore`, used
without `VALKEY_URL`, has one; `server/routes/statusRoute.ts` already had to work around this
same gap). (2) Even at the intended ~1h value, the entry's expiry was refreshed only by Learn
REST calls (`status`/`student-entry`/`start`/`stop` -> `loadEntry`), never by the live
SyncDeck session's own websocket activity — so an entry could silently expire out of the store
mid-class while the session it pointed at was genuinely live and busy (confirmed against an
incident where an embedded videosync child had just run under the "orphaned" session). Once
the entry was gone, the next student launch created a fresh waiting entry under the same
identity, and the instructor's next `start`/reopen created a brand-new session under that
entry — splitting the class exactly as observed, entirely independent of the identity-mismatch
hypothesis in the entry above.
- Control or mitigation: (1) Added `resolveActiveEntryTtlMs()` in `learnIntegration.ts`, which
checks `sessions.ttlMs`, then `sessions.valkeyStore?.ttlMs`, before falling back to
`WAITING_TTL_MS`, replacing all three `sessions.ttlMs ?? WAITING_TTL_MS` call sites (`start`'s
create branch, the substitute-instructor-link create branch, and `loadEntry`'s refresh calc).
(2) Added a generic, activity-agnostic `linkedSessionId` convention to `server/core/sessions.ts`
(parallel to the existing `embeddedParentSessionId` pattern): any session may declare
`data.linkedSessionId`, and `SessionStore.touch()` (both `InMemorySessionStore` and the
Valkey-wrapped store) propagates a touch to that linked record. `learnIntegration.ts` now
stamps the live SyncDeck session with `data.linkedSessionId = <entry mapping id>` via
`linkLiveSessionToEntry()` whenever `start` or the substitute-link route creates a session.
Since `wsRouter.ts`'s existing 30s ping loop already calls `sessions.touch(ws.sessionId)` on
any connected instructor/student/embedded-child socket, the entry now stays alive for as long
as anyone is actually connected to the class session, independent of whether Learn ever polls
`status` again.
- Residual risk: Sessions created before this change lack `linkedSessionId` and won't benefit
from propagation until they naturally end and a new one is created (no migration needed — the
old poll-driven refresh path still applies to them). The entry still has no live-session
connection during the window between `start` creating it and the first client connecting; the
TTL value fix (1) covers that window, not propagation (2).
- Validation (test/review/path): `server/sessionStore.test.ts` (`linkedSessionId` touch
propagation, and an entry surviving past its own ttl purely via a linked session being
touched, with a negative control); `activities/syncdeck/server/learnIntegration.test.ts`
(`linkedSessionId` stamped on the live session after `start`; active-entry `expiresAt`
resolves from `valkeyStore.ttlMs` when the wrapped store's top-level `ttlMs` is undefined).
- Follow-up action: If a future activity needs the same "entry mapping tied to a live session's
actual connectivity" shape, reuse the `linkedSessionId` convention rather than inventing a new
one — see the generic contract in `.agent/knowledge/data-contracts.md`.
- Owner: Codex

## Learn pending-start logging and linked-session safety

- Date: 2026-08-08
- Finding: The `learn-instructor-session-start-pending` event previously included raw `resourceLinkId`, despite the fingerprint-only logging contract. The linked-session helper also needed to avoid recursive traversal so malformed `A -> B -> C` and `A <-> B` links cannot refresh beyond one hop or recurse indefinitely.
- Resolution: The pending event now retains `requestId`, state, and identity fingerprints but omits the raw resource identifier. Both in-memory and Valkey-backed session stores use a non-propagating direct refresh for `linkedSessionId` targets.
- Validation: `activities/syncdeck/server/learnIntegration.test.ts` parses the pending event and verifies the raw field is absent; `server/sessionStore.test.ts` covers chain and two-record-cycle behavior plus a widened expiry margin.
- Owner: Codex

## Learn active-entry expiry and lifecycle logging

- Date: 2026-08-08
- Contract: Waiting Learn entries retain their bounded `data.expiresAt` gate. Active entries rely on the underlying session-store TTL, refreshed through the single-hop live-session link, so a stale logical timestamp cannot split an active class from its mapping.
- Privacy: Learn lifecycle audit events use provider/resource/mapping/session fingerprints rather than raw identifiers. Exact tests lock the cross-system HMAC vector from `.agent/plans/learn-syncdeck-session-integration.md`.
- Owner: Codex

- Date: 2026-07-15
- Area: waiting-room student display-name persistence
- Threat or risk: Remembering a student's lobby name across days in browser persistence could inadvertently expand into storing participant IDs, credentials, or activity-specific form data.
Expand Down
15 changes: 15 additions & 0 deletions .agent/knowledge/testing-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ Capture reusable test setup patterns, common failure modes, and reliability guid
- Follow-up action: Keep the terminal-running assertion as the popup readiness check and use the longer timeout only for actual execution completion.
- Owner: Codex

## Valkey session-wrapper coverage without an external service

- Date: 2026-08-08
- Pattern: Pass a minimal in-memory `ValkeySessionStore` test double as the optional third argument to `createSessionStore()` when a test must execute the real cache/wrapped-store branch. Assert through its record map and touch call list after `flushCache()` for cached touches, and immediately for uncached touches.
- Why it helps: This exercises production cache/direct-touch sequencing without requiring a running Valkey instance or opening a network connection in unit tests.
- Evidence: `server/sessionStore.test.ts`.
- Owner: Codex

## Session-store selection logging

- Date: 2026-08-08
- Pattern: Treat session-store startup selection as a structured server event, and capture `console.info` in a focused unit test to assert its stable discriminator fields for both in-memory and Valkey-backed modes.
- Evidence: `server/core/sessions.ts`; `server/sessionStore.test.ts`.
- Owner: Codex

- Date: 2026-07-27
- Scope: typecheck | activities
- Pattern: Run the activities TypeScript check one activity directory at a time through `scripts/typecheck-activities.mjs`. The runner reuses `activities/tsconfig.json` compiler options and shared contracts, but narrows source inclusion to the current activity.
Expand Down
Loading