Skip to content

Add experimental workspace presence and host user directories - #147

Open
molefrog wants to merge 46 commits into
mainfrom
codex/collab
Open

molefrog wants to merge 46 commits into
mainfrom
codex/collab

Conversation

@molefrog

@molefrog molefrog commented Sep 21, 2026 •

Copy link
Copy Markdown
Owner

Adds opt-in workspace presence through moi/collab: connected peers, user lookup, cursors, focus, and selection. Batiok publishes one global account and workspace member lists atomically, so applets can resolve offline users and distinguish loading from an empty directory. Application-data synchronization is outside this PR.

<PresenceGroup id={`task:${task.id}`}>
  <PresenceFrame id="title">
    <Input value={task.title} onChange={updateTitle} />
  </PresenceFrame>
  <PresenceGutter id="notes">
    <Textarea value={task.notes} onChange={updateNotes} />
  </PresenceGutter>
</PresenceGroup>

Enable presence with moi start --experimental-collab; install applet guidance/types with moi init --experimental-collab. One workspace engine/provider supplies all applets. Profiles require ID and color; name, email, and avatar are optional. Applets store user IDs and resolve current profiles through hooks and components.

Architecture: collab RFC. Host integration: Batiok bridge. Applet authoring: collaboration guide.

Review focus

  • Host snapshots replace identity and directories together. Missing hosted directories remain loading, ready-empty is authoritative, and sign-out clears identity and members. Arbitrary IDs, including __proto__, remain ordinary IDs. Batiok owns authentication and access enforcement.
  • Presence is scoped to page/applet and stable nested target IDs. Frame/gutter each require one child; changing or removing an ID releases its markers. Missing/disposed bridges warn once, return empty hook values, and render no collaboration components. Removing a workspace cancels pending connections and stops its active room.
  • Personal chat selection stays tab-local through first sends, provider ID changes, and applet-triggered messages. Pending applet attachments watch the matching selection scope. Rooms allow 64 connections and 128 registrations per connection; reconnect restores publications.

Verification

bun test: 1,914 passed, 4 skipped, 0 failures. Typecheck, full lint (existing warnings), formatting, and registry validation pass. CLI dev-mode signal cleanup passed 50 repeated runs. Earlier two-browser checks covered grouped focus/selection, reorder/removal, ID changes, cursor anchors, profiles, directory loading, sign-out, and away/return behavior.

molefrog and others added 12 commits September 21, 2026 17:54
A person's id now resolves to a name and face after they leave. The
collab worker keeps a `people` table (schema v2, migrated from v1),
upserts it on join and identity change, sends it in `welcome.people`,
and announces new or changed profiles with a `people` message. The
client store merges the directory with live participants.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Applets refer to a person by id (`id`, or `ids` for several) and the
components resolve name, face, and presence themselves: `Person` (with
`avatarOnly` and `showStatus`), `Facepile`, `Cursor`, `PresenceFrame`,
`PresenceGutter`, and the `usePerson` hook. `PresenceFrame`, and with it
`PresenceField` and `Selection`, hugs the single element it wraps and
copies its corner radius. `SyncStatus` is gone; applets render
`isSaving` and `error` from the shared-state hooks.

Hooks and components now talk to a `CollabBackend` contract from
context instead of the socket client. The workspace provides the live
backend; `createFakeBackend` is an in-memory room behind the same store.
`/dev/collab-kit` runs on it: every component with made-up people, then
the connected components and the exported hooks against scripted
participants.

The registry Avatar gains an `xs` size and expresses its default size as
a variant so applet-scoped CSS cannot override it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The people directory commit picked up an unrelated removal of
`CollabCapability` from the working tree, while the code that stops
using it is not committed yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-21T16:05:04.157673Z 1893769 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18937693f1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/features/chat/sessions/useSelectedSession.ts Outdated
Comment thread server/collab/manager.ts
Comment thread client/features/collab/store.ts Outdated
Remove persistent shared-state synchronization, expose connected peers and separate presence reads/publications, and resolve profiles through authoritative workspace directories supplied by Batiok. Built-in indicators publish only while active; disabled applets keep inert APIs.

Validated with 1744 passing tests (4 platform skips), production build, lint, changed-file formatting, and typecheck excluding pre-existing invalid untracked sketch.tsx. Equivalent checks ran manually because the pre-commit hook formats the whole tree and would touch unrelated files.
@molefrog molefrog changed the title Add experimental workspace collaboration Add experimental workspace presence and host user directories Sep 26, 2026
Show only connected people in the workspace header. Export the full workspace directory hook with status filtering, clarify applet/page scoping, and support one presence wrapper around keyed list or form children.

Verified 1754 tests, production build, two live browser sessions, filtered project typecheck, lint, and changed-file formatting. Ran checks manually instead of the whole-tree autoformat hook to preserve the existing invalid untracked sketch.tsx unchanged.
Forward --experimental-collab directly to child servers and initialize process configuration from parsed flags. Remove the environment toggle and service handling; keep init documentation installation separate from runtime opt-in.

Verified 1755 tests, lint, changed-file formatting, and project typecheck with the pre-existing invalid sketch.tsx excluded. Ran checks manually to preserve that untracked file from the whole-tree formatting hook.
Comment thread server/collab/applet-module.ts
Return empty hooks and render nothing with one warning when an applet collab bridge is unavailable. Keep personal chat and view-builder creation from updating shared selection. Stop workspace presence, including pending connections, when the workspace is removed.

Addresses PR #147 review findings. Verified 1774 tests, production client build, lint, formatting, and project typecheck excluding the pre-existing invalid sketch.tsx. Ran checks manually to avoid the whole-tree hook modifying that unrelated untracked file.
Integrate main's applet attachments and workspace navigation while retaining
collaboration bridge access, presence lifecycle, and personal chat/tab selection.
Adapt presence locations and regression fixtures to the new tab addresses.

Validation: 1,865 tests passed, 4 skipped, 0 failed; production client build,
typecheck, lint, and changed-file formatting passed. Typecheck and lint exclude
the user's pre-existing untracked sketch.tsx parse error. The whole-tree
auto-format hook is bypassed to preserve that file; equivalent checks ran
manually. Generated dist output was removed after the build.
molefrog and others added 22 commits September 27, 2026 13:23
* Inherit collab identity from Cloudflare Access

A deployment behind Cloudflare Access (Zero Trust) can set
`cloudflareAccess` in config.json, or MOI_CLOUDFLARE_ACCESS_TEAM_DOMAIN and
MOI_CLOUDFLARE_ACCESS_AUD, so every viewer inherits their Access identity
instead of creating a dev profile.

The server verifies the Access application token (Cf-Access-Jwt-Assertion,
or the CF_Authorization cookie when a proxy drops the header): RS256
signature against the team's published keys, issuer, audience, and expiry.
The profile takes id from `sub` and email from `email`; tokens carry no
display name, so the name is the email's local part, and the color is a
stable pick from the persona palette hashed from the id.

GET /api/identity serves the verified profile and loads before the app
mounts. The browser uses it over a saved dev profile, and an outer host
still wins. Presence sockets verify the token on upgrade (401 without one)
and pin every join and identity update to the verified profile, so one
viewer cannot appear as another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK

* Leave the name out of Cloudflare Access profiles

The base now allows users without names, with built-in labels falling
back to the email. Access tokens carry no display name, so the inherited
profile keeps only id, email, and the id-derived color instead of
deriving a name from the email. The /dev/collab notice labels the
profile with userDisplayName, which also fixes the typecheck against
the optional name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK

* Condense Cloudflare Access tests

Fold the verifier rejections into one table, drop cases that repeat
other coverage (concurrent fetches, clock drift, profile helpers), and
merge the config, endpoint, socket pinning, and identity precedence
checks into fewer tests. Coverage stays on signature, issuer, audience,
expiry, key refresh throttling, header and cookie tokens, config
parsing, socket pinning, and source precedence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK

* Refuse mismatched Access identities and expire cached keys

A presence socket verified by Cloudflare Access now joins only with the
verified id. A different id, such as an outer host's own identity, is
refused with identity_mismatch instead of being rewritten to the Access
user, so peers never see a different person than the browser shows.

Cached signing keys now expire after ten minutes, so a key Cloudflare
stops publishing stops verifying tokens even when no new key id arrives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK

* Verify Cloudflare Access tokens with jose

Replace the hand-rolled JWT parsing, WebCrypto verification, and key
cache with jose's jwtVerify and createRemoteJWKSet, the library
Cloudflare's own verification examples use. Tokens must be RS256, name
the team domain as issuer and one of the configured AUD tags, and carry
an expiry, with 60 seconds of clock tolerance. jose caches the team's
keys for ten minutes and refetches for an unknown key id after a 30
second cooldown. Failed key fetches are logged; bad tokens are not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK

* Widen the Access token time margins in tests

The not-yet-valid case set nbf 61 seconds past a timestamp taken at
module load, one second outside the verifier's 60 second tolerance, so
it failed whenever key generation in beforeAll took over a second, as
on a slow CI runner. Expired and not-yet-valid tokens now sit ten
minutes outside the tolerance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK

---------

Co-authored-by: Claude <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

2 participants