Bỏ qua để đến nội dung

Triển khai và máy khách

Control Center không phải là một chương trình duy nhất. Nó là một kiến trúc máy khách mỏng: một máy chủ không giao diện sở hữu dữ liệu của bạn, cùng một tập hợp máy khách dựng giao diện qua một kết nối RPC duy nhất tới máy chủ đó. Hiểu được sự tách bạch này sẽ lý giải vì sao một số thứ chạy ở nơi chúng chạy, và mỗi máy khách có thể với tới hay không với tới được những gì.

Một tiến trình có tên cc_server nắm giữ các cơ sở dữ liệu — một tệp global.db nhỏ cho trạng thái toàn máy chủ, cộng thêm một tệp SQLite cho mỗi không gian làm việc, được mở theo kiểu lười — và chạy những công việc dài hạn: các engine quy trình, bộ đối chiếu, bề mặt công cụ MCP và trình lắng nghe điều phối. Mọi thứ cần tồn tại lâu hơn một cửa sổ, hoặc mà hai máy khách phải thấy giống hệt nhau, đều nằm ở đó.

Không máy khách nào tự mở tệp cơ sở dữ liệu. Thay vào đó, mỗi máy khách giữ một RemoteRpcClient và phát các thao tác kho mã cùng các gói đăng ký qua nó. Máy chủ xác thực kết nối dựa trên khóa chia sẻ trước của thiết bị đã ghép nối — rồi không quan tâm tới kết nối đó nữa. Một phiên không bị ràng buộc vào một không gian làm việc. Bộ điều phối là phi trạng thái: mọi lời gọi thuộc phạm vi không gian làm việc đều mang workspace_id của riêng nó trong tham số, và lời gọi nào thiếu trường đó sẽ bị từ chối.

Đó là chủ ý, và chính điều này cho phép hai máy khách trên cùng một máy chủ nằm ở các không gian làm việc khác nhau mà không cần máy chủ giữ một “không gian làm việc hiện tại” để chúng mâu thuẫn với nhau. Thay vào đó, việc cô lập được thi hành theo từng lời gọi, qua bốn bước:

  1. Thao tác tự khai báo liệu nó có thuộc phạm vi không gian làm việc hay không; nếu có mà không có workspace_id nào được gửi tới, lời gọi sẽ bị từ chối.
  2. Id được nêu tên phải là một không gian làm việc đã đăng ký, và được kiểm tra trước khi bất cứ thứ gì mở cơ sở dữ liệu — nếu không, một id không xác định sẽ tạo ra một workspace.db rỗng ma trên mỗi yêu cầu. Tra không ra thì trả về not-found.
  3. WorkspaceRole của bên gọi phải đạt mức tối thiểu của thao tác, vốn được suy ra từ loại thao tác: đọc cần guest, thay đổi cần member, phá hủy cần admin.
  4. Một thao tác để lộ nội dung kho mã còn đòi hỏi quyền cấp theo từng kho ở đúng mức mà nó khai báo — none / read / review / write — để tư cách thành viên không gian làm việc không bao giờ âm thầm vượt quyền máy chủ mã.

Câu cần nhớ: giữ khóa ghép nối không phải là ranh giới truy cập; tư cách thành viên mới là.

Ba sức ép đã đẩy kiến trúc về phía một máy chủ:

  1. Một nguồn sự thật duy nhất. Một máy tính và một điện thoại, hay một laptop và một máy tính, phải thấy cùng một trạng thái đội máy. Nếu mỗi bên sở hữu cơ sở dữ liệu riêng, chúng sẽ lệch nhau. Chỉ một máy chủ nghĩa là mọi máy khách đọc cùng một dữ liệu trực tiếp.
  2. Web không thể tự phục vụ. Trình duyệt không thể sinh tiến trình con hay mở tệp cục bộ, nên một bản dựng web không bao giờ có thể sở hữu cơ sở dữ liệu hay chạy agent. Nó buộc phải nói chuyện với một máy chủ làm được những việc đó. Thay vì dựng thêm một đường dẫn dữ liệu thứ hai chỉ dành cho web, ứng dụng máy tính được đưa lên cùng đường dẫn đó.
  3. Vận hành không giao diện. Một máy chủ bạn có thể chạy trên một chiếc máy nhỏ — với tới được từ điện thoại trên tàu hay từ trình duyệt ở bất cứ đâu — hữu ích hơn một GUI mà bạn phải để mở.

Đánh đổi là máy chủ giờ trở thành một phụ thuộc: nếu nó không chạy, mọi thứ đều không hoạt động. Câu trả lời mặc định của ứng dụng máy tính cho điều đó là tự sinh một máy chủ.

Năm máy khách kết nối tới máy chủ, mỗi loại có hình dạng và mức độ tin cậy khác nhau.

Ứng dụng máy tính viết bằng Flutter (macOS, Windows, Linux) là máy khách đầy đủ. Trong lần khởi chạy đầu tiên, nó hỏi bạn muốn chạy theo cách nào, rồi ghi nhớ lựa chọn đó:

  • Cục bộ: sinh và giám sát một cc_server trên chính máy này — thiết lập mặc định, khép kín, dành cho một người dùng. Ứng dụng máy tính khởi chạy máy chủ; máy chủ sở hữu cơ sở dữ liệu nằm dưới thư mục gốc app-support và trao đổi với ứng dụng qua loopback.
  • Từ xa: kết nối tới một cc_server đang chạy ở nơi khác qua WebSocket bảo mật. Dữ liệu nằm trên máy chủ đó; máy tính này thuần túy chỉ dựng giao diện.

Trong cả hai trường hợp, máy khách RPC thu được sẽ thay thế nhà cung cấp dữ liệu của ứng dụng, nên toàn bộ giao diện — mọi màn hình, mọi tính năng — đều đọc và ghi qua máy chủ. Ứng dụng máy tính không bao giờ chạm trực tiếp vào tệp cơ sở dữ liệu.

Bản dựng web cũng chính là ứng dụng Flutter đó, được biên dịch để chạy trong trình duyệt và lưu trữ tại app.usectrl.dev, nên không có gì phải cài đặt. Trình duyệt không thể sinh tiến trình con, vì vậy máy khách web luôn ở chế độ từ xa: nó quay số tới một cc_server qua wss:// và dựng toàn bộ giao diện máy tính. Sau khi kết nối, nó hiển thị đúng những màn hình đó trên cùng một đội máy, bởi vì đó là cùng một đoạn mã chạy trên cùng một RPC. Những khoảng trống chỉ là vài chỗ cần một năng lực gốc mà trình duyệt không có — lớp chất lượng ghi âm cuộc họp ở phần dưới là chỗ chính, và thông báo hệ điều hành là chỗ khác.

Đây là lý do “không cần tải về” và “không cần máy chủ” là hai khẳng định khác nhau. Máy khách web loại bỏ việc cài đặt; nó không loại bỏ máy chủ. Thứ nó quay số tới có thể là một cc_server trên chính chiếc laptop đó — ws://localhost:9030, vốn không cần chứng chỉ vì trình duyệt coi loopback là đáng tin — hoặc một máy chủ trên máy khác, khi đó phải với tới được qua wss:// với chứng chỉ mà trình duyệt tin cậy. cc_server từ chối bind một địa chỉ không phải loopback nếu không có TLS, nên máy chủ từ xa đồng nghĩa với việc nó có chứng chỉ riêng, một proxy kết thúc TLS, hoặc một tunnel hay VPN cung cấp chứng chỉ đó.

Bản bundle được lưu trữ tách biệt khỏi máy chủ mà nó nói chuyện cùng. Một cc_server độc lập phục vụ điểm cuối RPC, bề mặt MCP và các proxy của nó, nhưng không phục vụ bundle web — đường dẫn tệp tĩnh có tồn tại trong máy chủ, song không có web root nào được cấu hình trên tệp nhị phân độc lập, nên nó không có gì để phục vụ tại /. Ảnh container cc-webapp đã phát hành là thứ lưu trữ bundle, và scripts/build_web.sh là thứ dựng nó. Máy chủ chỉ chấp nhận các origin của trình duyệt nằm trong danh sách --allowed-origins, mặc định là https://app.usectrl.dev, nên tự lưu trữ bundle nghĩa là phải thêm origin của nó vào danh sách.

Phương tiện (ảnh đại diện, ảnh bảng tin, ảnh chụp PR) đi qua điểm cuối proxy của máy chủ thay vì gọi thẳng tới các máy chủ thượng nguồn, nên trình duyệt không bao giờ tải từ một origin tùy ý. Máy khách đã triển khai gắn một Content-Security Policy riêng cho từng yêu cầu, lấy từ một cookie không nhạy cảm, để chỉ origin của máy chủ đang kết nối được phép.

Ứng dụng đồng hành trên điện thoại (cc_remote)

Phần tiêu đề “Ứng dụng đồng hành trên điện thoại (cc_remote)”

Ứng dụng Remote là một máy khách riêng, nhẹ hơn — một PWA Flutter web tại remote.usectrl.dev. Nó không phải bản ứng dụng đầy đủ, và đó là chủ ý. Nó ghép nối qua một relay WebSocket được môi giới, niêm phong đầu-cuối, và chỉ nói một lát cắt thiên về đọc của bề mặt công cụ: đọc phiếu, tin nhắn và bảng tin; gửi trả lời hoặc cập nhật một phiếu.

Một chiếc điện thoại là chủ thể có đặc quyền thấp hơn so với một agent cục bộ. Chính sách công cụ từ chối mặc định, khóa chia sẻ trước niêm phong mọi khung, một năng lực phiên khóa các thao tác đặc quyền (chẳng hạn ghép nối) khỏi máy khách đồng hành, các kiểm tra vai trò và quyền cấp theo kho ở trên, cùng việc giới hạn tần suất — tất cả giữ cho một lần ghép nối đã được duyệt (hay bị lộ) không biến thành toàn quyền điều khiển từ xa. Xem Điều khiển từ xa và di động để biết đầy đủ mô hình bảo mật.

Các công cụ bên ngoài — trình soạn thảo, agent khác, CLI — nói được Model Context Protocol sẽ kết nối tới bề mặt MCP của máy chủ và dùng sổ đăng ký công cụ có kiểu. Đây là đường tích hợp lập trình; nó không phải một GUI. Xem Dùng máy chủ MCP và Tham chiếu công cụ MCP.

Một cc_worker là tệp nhị phân Dart thuần, không giao diện, ghép nối với một cc_server, khai báo các năng lực của máy chủ nhà rồi kéo về những công việc đã được thuê để thực thi. Nó truyền sự kiện tiến trình về qua chính RPC đó và không giữ trạng thái bền vững nào: không cơ sở dữ liệu, không xác thực, không phê duyệt, không ngân sách — những thứ đó không bao giờ rời khỏi cc_server. Một worker là một cánh tay, không phải bộ não thứ hai. Một máy chủ có thẩm quyền, N cánh tay ngờ nghệch; không có đồng thuận, không có lưu lượng worker-với-worker.

Thực thi theo hợp đồng thuê vẫn chỉ là phần đường ống của việc thuê. agentRun là loại duy nhất chạy một lệnh thật (CC_JOB_COMMAND, hoặc một prompt được lặp lại nếu không có lệnh nào được cung cấp). pipelineStep, codeIndex, goldenRender, benchmark và evalBatch hiện chỉ dò bằng git --version / rev-parse HEAD để đường truyền phát được chạy thử mà không giả vờ rằng các công việc đó đã thực sự chạy.

Những gì nó quảng bá là hệ điều hành, kiến trúc CPU, số nhân, RAM, việc có với tới được một Flutter SDK hay không, và máy chủ nhà có những backend sandbox nào (không có trên Windows). Một worker chuyên dụng còn luôn quảng bá alwaysOn và acceptsParallel, và đó là những gì bộ lập lịch pin → prefer → spill dựa vào để xếp việc. Không có phép dò GPU hay ML nào.

Một máy tính chạy đơn lẻ không hề nhận ra những điều này, bởi việc điều phối thông thường hoàn toàn không đi qua đội máy. Đúng là có một worker ngầm trong cùng tiến trình được đăng ký cho máy chủ nhà, nhưng nó chỉ là một điểm nối đăng ký không có runner nào được mắc vào — một công việc thực sự được xếp lên worker cục bộ sẽ thất bại thay vì được thực thi. Hiện nay, chỉ một cc_worker thật mới thực thi các công việc được thuê.

Mối quan tâm Máy chủ (cc_server) Máy thực thi đội máy (cc_worker) Máy khách máy tính Máy khách web / điện thoại
Cơ sở dữ liệu (Drift/SQLite) sở hữu chúng — — —
Xác thực, phê duyệt, ngân sách, danh tính sở hữu — — —
Quy trình, bộ đối chiếu, trình lắng nghe điều phối chạy chúng — — —
Bề mặt công cụ MCP phục vụ chúng — — —
Thực thi agent chạy chúng (hoặc cho thuê ra ngoài) thực thi các công việc được thuê — —
Dựng giao diện — — đầy đủ đầy đủ (web) / thiên về đọc (điện thoại)
Ghi âm cuộc họp (mic + âm thanh cuộc họp) — — ghi âm gốc chỉ trên web, qua âm thanh chia sẻ màn hình
OAuth lịch (Google) lưu token — — —

Hai tầng không sandbox giống nhau. cc_server bọc những lượt chạy mà nó tự thực thi trong sandbox của hệ điều hành máy chủ nhà khi có backend — Seatbelt trên macOS, bubblewrap trên Linux và WSL2 — và lùi về các ranh giới khác trên Windows hoặc khi thiếu bwrap và socat. cc_worker hoàn toàn không sandbox. Nó quảng bá một năng lực sandbox để máy chủ có thể xếp việc theo nền tảng, nhưng không có gì trong worker áp dụng năng lực đó: một công việc được thuê được sinh thẳng ra, chỉ bị giới hạn bởi môi trường của hợp đồng thuê và worktree mà nó được cấp. Vì vậy, cho thuê công việc ra ngoài sẽ đẩy việc thực thi ra khỏi sandbox ngay cả trên một máy chủ nhà có sandbox. Xem Sandbox và bảo mật để biết điều gì giới hạn một lượt chạy trong từng trường hợp.

Việc ghi âm cuộc họp diễn ra trên máy khách vì nó cần micro và âm thanh của cuộc họp — một máy chủ nằm trong tủ đựng chổi không thể ghi lại cuộc gọi của bạn. Bản ghi sau đó được quy trình của máy chủ tóm tắt, và ghi chú được lưu ở nơi mọi máy khách đều đọc được.

Máy khách web cũng ghi âm, điều mà hình dạng của bảng có thể che khuất: nó thu micro cùng một track âm thanh chia sẻ màn hình ngay trong trình duyệt rồi truyền PCM 16 kHz tới máy chủ. Nó bắt buộc phải có track âm thanh đó, nên sẽ thất bại rõ ràng ở những nơi trình duyệt không cung cấp được. Điều thực sự chỉ có trên máy tính là lớp chất lượng quanh việc ghi âm — khử vọng ở mức tín hiệu qua bộ xử lý gốc, đồng hồ đo mức đầu vào và cảnh báo mic chết, chạy lại một bản tóm tắt, hủy xử lý, và thao tác ghi-rồi-liên kết của lịch.

Mọi máy khách chứng minh danh tính của mình bằng một khóa chia sẻ trước được cấp một lần:

  • Ứng dụng máy tính ở chế độ cục bộ tạo một khóa mới mỗi lần khởi động rồi đưa nó cho máy chủ được sinh ra qua biến môi trường, nên không có gì bí mật bị lưu lại trên máy tính.
  • Các máy khách từ xa (máy tính ở chế độ từ xa, web, điện thoại) dùng một khóa do lệnh pair của máy chủ tạo ra và lưu trong keychain của hệ điều hành (hoặc, trên điện thoại, lưu cục bộ).

Việc thu hồi diễn ra trực tiếp, không chậm trễ. Máy chủ theo dõi bảng thiết bị đã ghép nối và cắt mọi phiên đang mở mà thiết bị của nó đã rời khỏi tập đang hoạt động, trong vòng vài giây — trên cả đường WebSocket trực tiếp lẫn đường relay. Bạn không phải chờ thiết bị kết nối lại mới cắt được nó.

Khóa không bao giờ đi qua một URL mà máy chủ tĩnh có thể thấy. Khi nó đến qua liên kết — trên điện thoại hoặc trong trình duyệt — nó nằm trong URL fragment, phần sau dấu #, thứ mà trình duyệt không bao giờ gửi tới máy chủ lưu trữ trang. Đó là bất biến đáng ghi nhớ, và nó đúng cho cả hai. Máy khách web còn hai điểm vào khác: biểu mẫu kết nối, nơi bạn dán trực tiếp máy chủ và khóa, cùng một mã mời mà nó đổi lấy thông tin xác thực được tạo riêng cho trình duyệt đó.