Lewati ke konten

Multiplayer — identitas, keanggotaan, dan kehadiran

Control Center bekerja untuk satu operator, tetapi model di baliknya bersifat multi-aktor sejak awal. Manusia dan agen mengirim pesan di space yang sama, menyunting tiket yang sama, dan muncul pada daftar kehadiran yang sama. Membuat itu aman dan mudah dipahami bermuara pada tiga keputusan: siapa yang dihitung sebagai aktor, apa yang menggerbangi akses, dan state mana yang dibagikan secara tahan lama versus sekadar disiarkan langsung.

Prinsipal: pengguna dan agen adalah aktor yang setara

Bagian berjudul “Prinsipal: pengguna dan agen adalah aktor yang setara”

Segala sesuatu yang mencatat “siapa melakukan ini” — pesan, tiket, tinjauan, rencana, log run, entri audit — diselesaikan melalui sebuah Principal, union tersegel dari UserPrincipal dan AgentPrincipal dengan bentuk wire yang ringkas (user:<id> / agent:<id>). Atribusi tidak pernah meleburkan N manusia (atau N agen) menjadi satu akun sentinel, sehingga tinjauan yang ditinggalkan agen dan tinjauan yang ditinggalkan manusia dapat dibedakan di mana pun keduanya muncul.

Karena prinsipal adalah unit identitas, ia juga unit pembatasan:

  • Batas laju bersifat per prinsipal, bukan per sesi. Pengaman banjir pada panggilan alat jarak jauh menganggarkan per pengguna, bukan per koneksi: semua perangkat seorang anggota mengambil dari satu jendela geser bersama (120 panggilan per menit, yang paling banyak 30 di antaranya boleh mengubah data), sehingga membuka tiga perangkat tidak melipattigakan jatah siapa pun dan satu anggota yang berisik tidak bisa membuat yang lain kelaparan.
  • Pembatalan bersifat per prinsipal. Jurnal tindakan setiap klien hanya mencatat mutasi yang dilakukan prinsipal itu. Anda dapat ⌘Z edit tiket Anda sendiri; Anda tidak bisa membatalkan milik rekan satu tim — saat Anda melihatnya, jalur tahan lama sudah menyelesaikannya (lihat di bawah).

Keanggotaan adalah batas akses — bukan kunci pemasangan

Bagian berjudul “Keanggotaan adalah batas akses — bukan kunci pemasangan”

Dua kredensial terpisah menjawab dua pertanyaan yang terpisah. Kunci pemasangan (lihat Kontrol jarak jauh dan seluler) mengautentikasi sebuah perangkat ke server. Keanggotaan memberi wewenang kepada pengguna perangkat itu untuk bertindak di dalam sebuah ruang kerja. Perangkat yang dipasangkan tetapi penggunanya tidak memegang keanggotaan di suatu ruang kerja tidak dapat membaca maupun mengubahnya.

Keanggotaan adalah satu baris per (ruang kerja, pengguna), yang disimpan di berkas basis data milik ruang kerja itu sendiri — jadi “siapa saja yang ada di ruang kerja ini?” adalah pembacaan bercakupan biasa, sedangkan pertanyaan kebalikannya, “saya berada di ruang kerja mana saja?”, pasti melintasi berkas dan merupakan salah satu dari sedikit fan-out antar-ruang kerja yang diizinkan. Itulah yang dijalankan oleh pemilih ruang kerja sebelum ruang kerja mana pun dipilih.

Setiap baris membawa peran dari tangga lima tingkat yang diurutkan berdasarkan peringkat:

Peran Boleh
owner Kendali penuh, termasuk penghapusan ruang kerja; tepat satu per ruang kerja
admin Mengelola anggota, undangan, pengaturan, operasi destruktif
member Baca-tulis sehari-hari: pesan, tiket, run agen
viewer Hanya-baca di seluruh ruang kerja; glob pengecualian rahasia menyembunyikan path yang cocok pada permukaan yang memuat kode
guest Tingkat hanya-baca yang sama dengan viewer, pada peringkat terendah di tangga

Setiap keputusan akses bermuara pada “apakah peran pemanggil setidaknya X”. Tingkat lantainya diturunkan dari jenis operasi kecuali operasi itu mendeklarasikannya secara eksplisit: pembacaan membutuhkan guest, mutasi membutuhkan member, operasi destruktif membutuhkan admin. ACL per objek di luar pemberian izin per repo di bawah ini sengaja berada di luar cakupan.

Undangan adalah cara keanggotaan diberikan, dari Pengaturan → Ruang kerja → Anggota. Seorang admin menerbitkan kode sekali pakai (berupa tautan atau QR) yang kedaluwarsa setelah tujuh hari secara bawaan; server hanya menyimpan hash SHA-256 dari kode itu dan kodenya sendiri ditampilkan tepat sekali. Menukarkannya menyediakan pengguna tersebut secara JIT, mencatat keanggotaan pada peran undangan (tidak pernah owner — kepemilikan dialihkan secara eksplisit, tidak pernah diberikan lewat undangan) dan menyerahkannya ke pemasangan perangkat. Kredensial perangkat yang diterbitkan setelah pemasangan pun dibatasi waktunya 30 hari, jadi pemasangan bukan kunci permanen.

Pemberian izin repo berada di bawah keanggotaan. Karena server memegang checkout penuh, keanggotaan ruang kerja saja tidak boleh pernah memberi hak istimewa melebihi forge: setiap permukaan yang memuat kode — berkas, diff, tinjauan PR, pencarian kode — memeriksa izin anggota pada repo itu. Pemberian izin bersifat per (ruang kerja, pengguna, repo) dan diperingkat none → read → review → write, di mana write berarti menjalankan run agen yang menulis ke repo. Tidak adanya baris berarti none; pemilik dan admin secara implisit memegang write pada setiap repo yang tertaut dan tidak membawa baris sama sekali.

Control Center memisahkan state bersama ke dalam dua jalur dengan aturan ketahanan yang berlawanan, karena kedua jenis state itu gagal dengan cara yang berlawanan.

Kehadiran — siapa yang online, di mana mereka berada, apa yang sedang mereka ketik, apa yang sedang dilakukan agen saat ini — tidak bernilai begitu menjadi basi. Seandainya dipersistenkan, setiap penyambungan ulang harus merekonsiliasi “basis data mengatakan Sam sedang menyunting tiket ini” dengan kenyataan. Jadi kehadiran tidak pernah ditulis ke basis data. Ia hidup di hub kehadiran dalam memori di server: klien memublikasikan dengan presence.update, peer berlangganan dengan presence.watch, dan entri kedaluwarsa otomatis setelah tiga heartbeat yang terlewat (heartbeat berjalan setiap 10 detik; hub menyapu setiap 5 detik). Tidak ada yang perlu direkonsiliasi — hanya dibiarkan luntur.

Dua sifat menjaga jalur ini tetap dapat dipercaya:

  • Identitas berasal dari sesi, tidak pernah dari argumen klien. Server memetakan perangkat terautentikasi milik koneksi ke sebuah id pengguna; klien hanya pernah bisa memperbarui kehadirannya sendiri, sehingga tidak ada yang dapat memalsukan status atau lokasi peserta lain. Pembaruan offline yang eksplisit (terputus atau tombol tak terlihat) menghapus entri itu seketika.
  • Daftar kehadiran dipusatkan di server. Peer tidak pernah bertukar kehadiran secara langsung, sehingga setiap peserta melihat daftar yang sama apa pun topologinya. Agen tidak punya klien, jadi server menyintesis entri mereka dari state run langsung — berpikir, berjalan, terblokir di gerbang persetujuan, selesai — dengan satu run perwakilan yang menentukan state dan lokasi seorang agen agar run yang bersamaan tidak membuat daftar itu bergetar.

Fan-out bertingkat per konsumen: desktop dan web menempuh tingkat penuh (paling banyak satu emisi daftar per 100 ms), ponsel menempuh tingkat ringkasan (per 500 ms), sehingga pembaruan secepat kursor tidak bisa melelehkan PWA.

Lokasi seorang peserta — tempat mereka berada saat ini, hingga ke artefaknya — adalah union tersegel: Space, File (repo + path + baris opsional), Pr, Ticket, PlanNode. Tidak setiap lokasi punya tempat untuk dituju: tidak ada rute penampil berkas, jadi lokasi File dirender sebagai label alih-alih tujuan. Jalur ini juga membawa klaim lunak (“Sam sedang menyunting tiket ini”, “seorang agen memegang worktree ini”): visibilitas konflik, bukan kunci. Jenis lokasi yang tidak dikenal diuraikan menjadi null, sehingga klien lama sekadar tidak merender apa yang tidak dipahaminya.

Catatan yang harus bertahan — tiket, dokumen Catatan space bersama, pesan — menempuh jalur sebaliknya: otoritatif, terurut, dan dipersistenkan. Edit yang bersamaan diselesaikan dengan last-writer-wins dalam urutan penerimaan server, bukan berdasarkan jam klien. Penyelesaiannya sengaja dibuat berbutir halus: edit tiket bersifat per kolom, sehingga dua orang yang menyunting bidang berbeda pada tiket yang sama sama-sama diterapkan dan tidak ada yang menimpa yang lain (transisi alur kerja tetap memakai penguncian optimistis); dokumen Catatan space adalah satu kolom, sehingga seluruh dokumen bersifat LWW dengan klaim lunak di jalur kehadiran yang membuat penyuntingan bersamaan terlihat alih-alih terkunci. Ini sengaja bukan CRDT — beban pembuktian ada pada CRDT, dan LWW berdasar urutan penerimaan plus klaim yang terlihat mencakup pola kolaborasi yang nyata.

“Penulis terakhir” berarti urutan penerimaan server, tidak pernah jam klien — laptop dengan jam yang melenceng tidak dapat memenangkan edit yang sudah kalah. Di balik jalur tahan lama berdiri feed sinkronisasi: trigger SQLite menambahkan setiap mutasi ke log perubahan per ruang kerja di dalam transaksi penulisan, mengalokasikan nomor urut monotonik per ruang kerja dalam transaksi yang sama dengan mutasinya. Sebuah layanan server mengekor log itu menjadi frame delta berversi sesuai urutan. Klien berkembang dari snapshot penuh menjadi delta; celah dalam urutan menjatuhkan penyimpanan itu kembali ke mode snapshot alih-alih salah menerapkan frame.

Jalur kehadiran bukan sekadar daftar — ia menggerakkan verba kolaborasi:

  • Mode ikuti. Klik seorang peserta dan klien Anda mengikuti lokasinya, bernavigasi saat ia bernavigasi. Ikuti bersifat “sampai Anda bertindak”: begitu Anda bernavigasi atas kemauan sendiri, mode ikuti melepas. Spanduk yang dapat ditutup memberi tahu saat peserta lain sedang menyorot (mempresentasikan) space tempat Anda berada.
  • Arahkan. Pesan ke agen yang sedang berjalan diantrekan sebagai pengarahan; agen membacanya ulang sebelum melanjutkan giliran berikutnya, sehingga Anda dapat mendorong sebuah run di tengah jalan tanpa menghentikannya.
  • Ambil alih. Mengambil kendali atas sebuah percakapan menjeda setiap run aktif di batas giliran bersih berikutnya (run Claude Code, yang tidak punya batas aman, dihentikan) di balik snapshot git per giliran, sehingga pengambilalihan dapat dibalik. Penanda tahan lama memblokir pengiriman baru ke space itu dan bertahan melewati mulai ulang server — server tidak pernah melanjutkan otomatis ke edit manusia yang belum selesai. Satu editor aktif per worktree adalah klaim lunak: pengambilalihan kedua terlihat dan ditolak, bukan digabungkan diam-diam.
  • Kembalikan. Mengembalikan kendali memposting ringkasan diff terstruktur worktree ke space, mengantrekannya sebagai pengarahan untuk run yang dijeda, melanjutkan run itu, dan menghapus penanda.

Pengarahan dan pengambilalihan adalah intervensi setelah kejadian. Dial otonomi adalah pengaturan tetap: per space, per agen, ditetapkan dari header space, ia menentukan apa yang terjadi saat agen mencapai tindakan yang digerbangi.

Tingkat Nilai wire Efek pada panggilan alat yang digerbangi
Hanya usulkan proposeOnly Ditolak langsung, dengan alasan yang harus disampaikan agen — ia dapat menjelaskan tindakan itu tetapi tidak melakukannya
Bertindak dengan persetujuan actWithApproval Gerbang persetujuan yang fail-closed: satu konfirmasi muncul ke operator yang terhubung dan tanpa penyetuju berarti ditolak
Bertindak bebas actFreely Disetujui sebelumnya, kecuali jika aturan pagar pengaman deny yang keras berlaku — aturan deny selalu menang
(tidak ditetapkan) null Sama dengan bertindak dengan persetujuan: bawaan server

Dua hal berada di luar dial itu ke dua arah. Verba keluaran milik sebuah mode dipatok dan tidak pernah bisa ditolak, apa pun kata dial itu — mode orkestrasi pernah secara struktural tidak mampu mengeluarkan satu-satunya luarannya karena sebuah preset menolak kelas efek yang dideklarasikan verba tersebut. Dan deny keras dari pagar pengaman tindakan tidak dapat ditimpa oleh actFreely.

Semua ini bersifat aditif. Alur kerja operator solo tidak berubah: dengan satu manusia dan tanpa agen, hub kehadiran tidak punya apa pun untuk disiarkan, sehingga jalur itu memancarkan snapshot awalnya lalu menganggur tanpa lalu lintas sama sekali; mesin LWW dan sinkronisasi adalah jalur penulis tunggal yang sama seperti selama ini; dan kunci pemasangan tetap menjadi kredensial perangkat seperti selama ini. Keanggotaan sudah menjadi batas akses — dengan satu manusia hanya ada satu baris owner dan tidak ada elemen antarmuka daftar kehadiran.