REST & SSE
HTTP API
HTTP và JSON thuần, mô tả bằng OpenAPI 3.1. Câu trả lời có thể chờ trọn vẹn hoặc stream bằng Server-Sent Events.
Kết nối
- Base URL:
http://127.0.0.1:8765(mặc định trên loopback). - Header:
Authorization: Bearer <token>, với token làlocalTokentrong<data-dir>/identity.json. - Lỗi:
{ "code": "SOME_CODE", "message": "..." }kèm mã trạng thái HTTP. Thiếu hoặc sai token:401 UNAUTHENTICATED.
export CLARKCANT_URL="http://127.0.0.1:8765"
export CLARKCANT_TOKEN="<token>" # localToken from <data-dir>/identity.json
Route công khai (không cần token)
| Route | Trả về |
|---|---|
GET /health | Tình trạng sống, nền tảng runtime, giao thức đã thương lượng. Không kèm danh tính node. |
GET /.well-known/clarkcant.json | Tài liệu khám phá, liệt kê mọi giao diện (api, mcp, websocket, cli) và endpoint của chúng. |
GET /openapi.json | Mô tả OpenAPI 3.1 của phần REST ổn định. |
GET /connections/callback/{packageId} | Nơi provider đưa trình duyệt của bạn quay về sau khi bạn kết nối tài khoản của một gói. state dùng một lần do node của bạn cấp xác thực route này. Trả về một trang ngắn không bao giờ lặp lại mã hay state. |
Các route (bề mặt v1 ổn định)
| Method | Path | Body | Ghi chú |
|---|---|---|---|
| GET | /node | – | Id node, nhãn, fingerprint và model đã cấu hình (model là null khi chưa đặt). |
| GET | /conversations | – | Danh sách cuộc trò chuyện. |
| POST | /conversations | { title? } | 201 { conversationId, homeNodeId } |
| POST | /conversations/{id}/messages | { text, attachmentIds?, references? } | Chờ câu trả lời; trả về { resolution, taskId, messageIds, timeline }, hoặc 202 khi việc tiếp tục chạy nền (đọc timeline sau). |
| POST | /conversations/{id}/messages/stream | { text, attachmentIds?, references? } | Stream câu trả lời bằng Server-Sent Events (xem bên dưới). |
| POST | /conversations/{id}/stop | { source? } | Chỉ dừng câu trả lời mà cuộc trò chuyện này đang viết. Phần đã viết được giữ lại, gắn nhãn đã dừng. Trả về { stopped }, là false khi không có câu trả lời nào đang chạy. Mỗi lần dừng được ghi audit kèm nguồn (chat, voice, hoặc api khi bỏ trống). |
| GET | /conversations/{id}/timeline?after=N | – | Tin nhắn (các block: text/markdown, hoạt động của tool, suy luận, widget…), mục đã ghim, các instance widget. |
| POST | /conversations/{id}/questions/{questionId}/answer | { text?, optionIds?, confirmed? } | Trả lời một ask_user_question; bắt đầu một lượt mới. |
| POST | /conversations/{id}/questions/{questionId}/cancel | – | Huỷ câu hỏi. |
| POST | /conversations/{id}/widgets/{instanceId}/export | { sort?, query?, filters?, columns? } | Tải một widget bảng xuống thành file CSV (text/csv, tên file nằm trong Content-Disposition). Các dòng lấy từ chính dataset của bảng: cách sắp xếp, từ khoá tìm và bộ lọc bạn gửi chọn dòng dựa trên mọi cột mà bảng hiển thị, giống hệt trên màn hình, còn columns chỉ quyết định file chứa những cột nào trong số đó. Request không thể mang dòng dữ liệu. Ô nào mà bảng tính sẽ chạy như công thức đều được thêm tiền tố '. Chỉ chủ của bảng mới xuất được, và relay WebSocket, MCP cùng clarkcant api từ chối nó với 403 PERSON_ONLY. |
| POST | /conversations/{id}/widgets/{instanceId}/actions | { actionBindingId, expectedRevision, expectedBindingDigest, input, invocationId, variant?, sequence? } | Bấm một hành động đã gắn trên widget, giống hệt khi bấm chuột hay nói yêu cầu. 200 là outcome: "done"; 202 là "approval-required" hoặc "background". Khi bị từ chối, body mang code và message (tiếng Anh, dành cho log và agent), và khi liên quan có thêm outcome (uncertain, partial, refused), mayHaveRun, recorded, readOnly, taskId, retryAfterMs, limit và báo cáo workflow cho từng bước. Widget không nằm trong cuộc trò chuyện được nêu thì nhận 404 INSTANCE_UNKNOWN; vượt giới hạn mỗi phút của binding thì nhận 429 RATE_LIMITED. Cuộc gọi có thể thay đổi dữ liệu được ghi vào sổ effect trước khi gửi; nếu không ghi được, lần bấm nhận 503 LEDGER_UNAVAILABLE và không có gì được gửi. Cuộc gọi đã gửi mà không nhận được câu trả lời đáng tin (504 SERVICE_TIMED_OUT, 409 SERVICE_CANCELLED, 504 SERVICE_UNREACHABLE, 502 SERVICE_TOOL_FAILED) trả về outcome: "uncertain" với mayHaveRun: true, trở thành effect chưa rõ, và hộp thư chỉ hỏi về nó, kèm taskId, khi recorded là true. Một lần read chưa xong thì không thay đổi gì và mang readOnly: true. Cùng một invocationId không bao giờ được gửi hai lần: lần lặp lại nhận câu trả lời đã lưu, kể cả sau khi khởi động lại, và lần bấm bị việc khởi động lại cắt ngang thì nhận ACTION_INTERRUPTED. contextRefs của hành động agent do node đọc và chỉ đến model như dữ liệu, không bao giờ như chỉ dẫn; yêu cầu không mang chữ nào của riêng nó. variant: "view-state" là lần ghi chỉ trạng thái cho trạng thái phát của một trình phát do host giữ (canvas.video@1, canvas.audio@1) và cần một sequence, số nguyên dương tăng dần theo mỗi lần ghi; giá trị vượt đồng hồ của node hơn một ngày nhận 400 INVALID_INPUT. Nó qua cùng các bước kiểm tra chủ sở hữu, binding, revision, digest và input, rồi trả về 200 { variant, duplicate, instanceId, revision, stateRevision, state } mà không có timeline và không làm revision của instance tăng lên. Lần ghi có sequence không mới hơn lần cuối được nhận thì không ghi gì và, trước các bước kiểm tra revision và digest, được trả về duplicate: true cùng trạng thái node đang giữ; lần thử lại với đúng invocationId của lần ghi mới nhất được trả về đúng như vậy, còn mọi lần ghi khác thuộc loại này mang thêm stale: true. invocationId bắt đầu bằng view-state: được dành riêng cho bản ghi của chính node và nhận 400 INVALID_SCHEMA, giống như sequence không kèm variant, variant không kèm sequence, hay bất kỳ giá trị variant nào khác; variant trên một binding khác thì nhận 400 UNSUPPORTED_ACTION. |
| GET | /composer/suggestions?trigger=/|@&q=&conversationId= | – | Những gì nên gợi ý sau / (kỹ năng) hoặc @ (dự án, tệp, thư mục, dịch vụ, cuộc trò chuyện, việc nền). Tối đa 8 dòng; mỗi dòng mang ref để gửi đi, hoặc disabledReason. Xem bên dưới. |
| POST | /stop | – | Dừng khẩn cấp: huỷ các lệnh đang chạy, ngắt lượt trả lời, dừng việc chạy nền. |
Tạo cuộc trò chuyện
curl -s -X POST "$CLARKCANT_URL/conversations" \
-H "Authorization: Bearer $CLARKCANT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "title": "From my app" }'
# 201 → { "conversationId": "…", "homeNodeId": "…" }
Gửi tin nhắn và chờ trả lời
curl -s -X POST "$CLARKCANT_URL/conversations/<conversationId>/messages" \
-H "Authorization: Bearer $CLARKCANT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "text": "Summarise the notes in my Documents folder" }'
# → { "resolution": …, "taskId": "…", "messageIds": […], "timeline": … }
Một câu gõ như “open settings” được host hiểu là ý định với ứng dụng (appIntent trong response) thay vì gửi tới model.
Trong ô soạn tin, tệp đã chọn vẫn nằm cùng draft khi đang kiểm tra tải lên. Nút Gửi bị vô hiệu trong lúc đó; nhấn Enter không gửi hay xoá nội dung và tệp. Khi tải lên xong, hãy gửi tin nhắn như bình thường. Tệp tải lỗi vẫn giữ lý do đã nêu và không được gửi cùng tin nhắn.
Ô soạn tin đánh dấu Markdown cơ bản ngay khi bạn gõ: chữ đậm, chữ nghiêng, gạch ngang, code trong dòng, liên kết, tiêu đề, trích dẫn, danh sách và khối code có rào. Ô nhập vẫn là văn bản thuần, nên gõ, chọn, hoàn tác, kiểm tra chính tả và bộ gõ vẫn hoạt động như trước. Các ký tự Markdown vẫn hiện, và tin nhắn được gửi đúng như bạn đã gõ.
Dòng dưới ô soạn tin bắt đầu bằng model mà tin nhắn tiếp theo sẽ chạy và mức thinking của nó (thinking: mặc định khi node không đặt mức nào), sau đó là độ đầy của ngữ cảnh, tỉ lệ cache, tốc độ viết và chi phí của lượt gần nhất. Model được đọc từ GET /node, nơi model phản ánh model đã chọn trong Cài đặt kể từ khi node khởi động, và dòng này cập nhật ngay khi bạn chọn model khác. Mỗi lượt trả lời dừng sau 5 phút theo mặc định (CC_MODEL_MAX_WALL_CLOCK_MS); việc dài hơn nên chạy nền.
Việc phê duyệt do con người quyết định trên giao diện của chính họ. Route đó nằm ngoài mô tả ổn định và cố ý không có tool MCP.
Tin nhắn gửi khi Clark đang trả lời
Một tin nhắn ngắn bạn gõ khi Clark vẫn đang trả lời sẽ được nhập (steer) vào câu trả lời đó. Tin nhắn có tệp, tham chiếu hoặc phê duyệt, tin nhắn nói, hoặc tin nhắn từ script hay ứng dụng khác sẽ chờ và được trả lời riêng. Stop cũng huỷ các tin nhắn còn đang chờ.
Trên route /messages thường, một tin nhắn tới khi có lượt đang trả lời được quyết định ngay tại đó: nhập (steer) vào lượt đang chạy, ngắt lượt đó, hoặc chạy nền. Một tin nhắn có nguồn gốc khác với lượt đang chạy, hoặc một tin nhắn gõ trong lúc đang có lượt nói bằng giọng, không bao giờ được nhập (steer) vào lượt đó. Khi steer được chọn cho một tin nhắn như vậy, nó cũng không ngắt lượt đang chạy: nó chờ và được trả lời thành một lượt riêng với nguồn gốc của chính nó. Lượt đang chạy chỉ bị ngắt khi bộ quyết định chọn ngắt, khi tin nhắn mới có tham chiếu, hoặc khi chạy nền mà không có worker nào nhận.
Mọi cách khác để bắt đầu một lượt (route stream, phần tiếp theo sau một phê duyệt, một câu hỏi đã được trả lời, giọng nói) không bao giờ gửi prompt thứ hai cho lượt đang chạy. Chữ thuần cùng nguồn gốc và cùng kênh, không có tệp đính kèm, tham chiếu, chỉ dẫn hay dữ liệu, được nhập (steer) vào lượt trong lúc câu trả lời của lượt đó đang được viết. Câu trả lời đó trả lời luôn tin nhắn này, và response cùng sự kiện done của stream báo resolution: "steered" không kèm id tin nhắn. Chữ gõ không bao giờ nhập vào một lượt nói, và tin nhắn có tệp đính kèm chờ tới lượt riêng của nó. Mọi tin nhắn khác chờ lượt đang chạy kết thúc rồi thành một lượt riêng, đọc tệp đính kèm và tham chiếu của chính nó; request (hoặc stream) của nó vẫn mở cho tới khi lượt đó trả lời xong.
Một lượt chỉ đang được chuẩn bị (session của nó còn đang được tạo, hoặc việc đổi model còn đang chờ) thì chưa trả lời: tin nhắn mới không bao giờ dừng nó và được trả lời sau nó. Stop, nút dừng khẩn cấp hoặc việc node tắt sẽ huỷ mọi tin nhắn còn đang chờ, nên không tin nhắn nào trong số đó bắt đầu; thẻ trả lời của nó cho biết nó đã được dừng trước khi bắt đầu, và tin nhắn vẫn được lưu.
Chỉ tới một kỹ năng, một dự án hay một tệp
Ngoài phần chữ, một tin nhắn có thể mang tối đa 8 tham chiếu có kiểu: một kỹ năng, một dự án, một tệp hay thư mục trong dự án đã được lập chỉ mục, một dịch vụ MCP, một cuộc trò chuyện khác hoặc một việc nền. Hãy lấy chúng từ GET /composer/suggestions thay vì tự dựng, rồi gửi ref của từng dòng bạn chọn. Hộp thư của ứng dụng cũng gửi theo cách đó một loại nữa, là thông báo, khi bạn dùng Hỏi Clark hoặc Thêm vào ngữ cảnh trên nó.
curl -s "$CLARKCANT_URL/composer/suggestions?trigger=@&q=myapp/src/" \
-H "Authorization: Bearer $CLARKCANT_TOKEN"
# → { "trigger": "@", "query": "myapp/src/", "suggestions": [ { "kind": "file", "label": "myapp/src/app.ts", "ref": { … } } ] }
curl -s -X POST "$CLARKCANT_URL/conversations/<conversationId>/messages" \
-H "Authorization: Bearer $CLARKCANT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "text": "Xem giúp @myapp/src/app.ts", "references": { "version": 1, "items": [ <ref đó> ] } }'
Tham chiếu là con trỏ, không phải quyền. Node kiểm lại từng tham chiếu khi tin nhắn tới: kỹ năng đã bị sửa hay gỡ, tệp không còn, dự án nằm ngoài các thư mục được phép, cuộc trò chuyện không còn tồn tại, hay thông báo không còn trong hộp thư đều khiến cả tin nhắn bị từ chối với 400 REFERENCE_NOT_AVAILABLE, có nêu tên tham chiếu, và không có gì được lưu. Tham chiếu được chấp nhận sẽ được lưu trên tin nhắn thành block reference, và lượt trả lời được báo về nó bằng đường dẫn tương đối với thư mục được phép, không bao giờ bằng đường dẫn tuyệt đối; nội dung của một thông báo được trích lại dưới dạng dữ liệu, không bao giờ như chỉ dẫn. Muốn đọc, chạy hay thay đổi thứ nó chỉ tới thì vẫn đi qua công cụ và chính sách như thường lệ.
Stream câu trả lời (SSE)
curl -N -X POST "$CLARKCANT_URL/conversations/<conversationId>/messages/stream" \
-H "Authorization: Bearer $CLARKCANT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "text": "What changed in my project today?" }'
| Sự kiện | Ý nghĩa |
|---|---|
delta | Thêm một đoạn trả lời: { text }. |
reasoning | Phần suy luận của model, tách khỏi những gì nó nói với người dùng. |
tool-start, tool-end | Một lần gọi tool bắt đầu hoặc kết thúc. |
host-control | Agent yêu cầu ứng dụng thay đổi một thứ gì đó: mở Settings ở một tab, đổi model, mở hoặc kết thúc chế độ giọng nói, quay lại cuộc trò chuyện. Xem bên dưới. |
error | Lượt trả lời gặp lỗi. |
done | Sự kiện cuối: { resolution, taskId, messageIds, timeline }. |
Từ JavaScript:
// EventSource cannot POST or send headers, so read the stream with fetch.
const res = await fetch(`${url}/conversations/${conversationId}/messages/stream`, {
method: "POST",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({ text: "hello" }),
});
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
for (;;) {
const { value, done } = await reader.read();
if (done) break;
console.log(value); // raw Server-Sent Events: delta, tool-start, …, done
}
Khi agent thay đổi ứng dụng
Sự kiện host-control do agent yêu cầu mang một controlId. Ứng dụng thực hiện hành động, rồi trả lời bằng POST /app-intents/host-control/<controlId> { "ran": true|false, "say": "…" }. Agent nhận đúng câu trả lời đó, nên Clark chỉ nói ứng dụng đã thay đổi khi ứng dụng báo là đã làm, và chuyển lại lý do của ứng dụng khi không làm được. Nếu không có câu trả lời trong vài giây, agent được báo là hành động chưa được xác nhận. Mỗi controlId chỉ được trả lời một lần, và route chờ câu trả lời (/messages) không bao giờ chờ nó. Việc trả lời thuộc bề mặt của con người: relay WebSocket, MCP và clarkcant api từ chối nó với 403 PERSON_ONLY.
Trong bản ghi audit, một hành động agent yêu cầu khi đang trả lời điều bạn nói được đánh dấu voice-agent, tách biệt với lệnh do chính bạn nói (voice).
Đọc một cuộc trò chuyện
curl -s "$CLARKCANT_URL/conversations/<conversationId>/timeline?after=0" \
-H "Authorization: Bearer $CLARKCANT_TOKEN"
Timeline trả về mọi bước đúng như đã lưu. Khi ứng dụng hiển thị một câu trả lời, từ ba bước làm việc liên tiếp trở lên (lệnh gọi tool, suy luận và các bước kiểm tra đi kèm) được gộp thành một dòng, “Đã làm N bước”, bấm vào để mở ra mọi bước theo đúng thứ tự. Nếu có bước lỗi, dòng đó thêm “· N bước lỗi” màu đỏ, và bên trong, bước lỗi mở sẵn ở lý do của nó. Khi câu trả lời còn đang được viết, bước mới nhất của nó nằm ngoài phần gộp.
Sau khi bạn duyệt một lệnh trên thẻ, Clark bắt đầu một lượt mới để đọc kết quả của lệnh và làm tiếp. Không ai gõ tin nhắn người dùng mà lượt đó trả lời, nên tin nhắn ấy mang hostWritten: { "kind": "host-continuation", "version": 1 }. Hãy hiển thị tin nhắn như vậy thành một dòng của ClarkCant ("Đã duyệt — Clark tiếp tục"), không phải lời người dùng đã viết, và làm y như vậy với mọi kind hoặc version bạn không nhận ra. Nội dung của nó là thứ model đã đọc. Nó không xuất hiện trong kết quả tìm kiếm, và clarkcant read in nó thành (approved, Clark carries on).
Trả lời câu hỏi của Clark
Khi cần bạn quyết định điều gì, Clark hỏi bằng ask_user_question rồi kết thúc lượt. Câu trả lời của bạn mở một lượt mới. Gửi text, optionIds hoặc confirmed tuỳ theo câu hỏi, hoặc huỷ bằng …/cancel.
curl -s -X POST "$CLARKCANT_URL/conversations/<conversationId>/questions/<questionId>/answer" \
-H "Authorization: Bearer $CLARKCANT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "optionIds": ["<optionId>"] }'
Yêu cầu duyệt do task đang chạy tạo ra
GET /inbox có các yêu cầu duyệt đang chờ do task đang chạy tạo ra. Quyết định bằng POST /tasks/{taskId}/approvals/{approvalId}/decide với { "decision": "granted" | "denied", "digest": "<operationDigest shown in the inbox>" }, dùng đúng digest đã hiện trong hộp thư. Duyệt cho phép task chạy lại; redispatched cho biết node đã nhận lượt chạy đó hay chưa. Nếu chưa nhận, chưa có gì chạy và bạn có thể yêu cầu lại. Từ chối hoặc hết hạn sẽ kết thúc task. Đây là quyết định chỉ bạn được thực hiện: MCP, relay yêu cầu qua WebSocket và clarkcant api từ chối route bằng 403 PERSON_ONLY, để AI client không tự duyệt thao tác của nó.
Ai đã yêu cầu: nguồn gốc của một lượt
Mỗi lượt ghi lại ai đã bắt đầu nó, thành origin của tin nhắn người dùng: person (trang của chính bạn, một tin nhắn nói, hoặc một thẻ bạn trả lời hoặc một hành động widget bạn bấm trong trang), mcp (MCP ask_clark), relay (relay WebSocket), cli-api (clarkcant api, một script hoặc mọi người giữ token khác), automation (một tự động hoá đã lên lịch hoặc thường trực) hoặc peer (tác vụ một node khác uỷ cho). Node đọc giá trị này từ những gì gateway đã biết, không bao giờ từ thân request, và ghi nó trên thẻ duyệt mà lượt đó tạo ra, trên dòng của lượt trong GET /activity và trong nhật ký kiểm toán.
Mặc định chính sách đối xử với mọi nguồn gốc như với chính bạn: một lượt do ứng dụng AI bắt đầu qua MCP được quyết định y như tin nhắn của bạn. Để các chương trình phải hỏi trước, hãy bật tuỳ chọn trong Cài đặt → Điều khiển → Yêu cầu từ chương trình khác, tức đặt preference execution.machineTurns thành "ask". Khi đó một bước rủi ro (external-write, destructive, financial, communication, media-capture) trong lượt bắt đầu từ mcp, relay hoặc cli-api sẽ hỏi trước, ở chỗ chính sách lẽ ra đã cho chạy. Quy tắc từ chối hoặc lệnh cấm vẫn thắng.
Duyệt một bước không có nghĩa là duyệt phần còn lại: sau khi bạn duyệt một thẻ do chương trình tạo ra, bước rủi ro tiếp theo của nó vẫn được hỏi lại. Lưu ý: chương trình nào giữ token của node này vẫn có thể đổi cài đặt này, hoặc gọi thẳng HTTP API mà xưng là bạn.
Xử lý một thông báo
GET /inbox liệt kê các thông báo của bạn, và mỗi thông báo mang theo các actions hợp với điều nó nói tới ngay lúc này. Node tính lại chúng mỗi lần đọc, nên một thao tác không còn làm được vẫn được liệt kê, kèm lý do unavailable (conversation-gone, work-gone, package-gone hoặc already-current), thay vì thành một nút bấm vào thì lỗi. Mỗi thao tác đi tới một route riêng:
| Thao tác | Route | Tác dụng |
|---|---|---|
| Chạy lại | POST /work/{id}/retry | Việc nền bị lỗi, bị dừng hoặc bị cắt ngang do khởi động lại sẽ chạy lại với đúng yêu cầu cũ, trong cùng hội thoại, thành một việc mới có id riêng. Mỗi lần chạy chỉ chạy lại được một lần: bấm lần thứ hai trả về 409 ALREADY_RETRIED. Việc đã xong hoặc còn đang chạy trả về 409 WORK_NOT_RETRYABLE, còn node đang bận trả về 429 BACKGROUND_BUSY và để lần chạy đó vẫn chạy lại được sau. |
| Hỏi lại | POST /conversations/{id}/questions/{questionId}/ask-again | Một câu hỏi đã hết hạn mà chưa ai trả lời quay lại thành câu hỏi mới với cùng lời và cùng lựa chọn, và thẻ cũ ghi rằng nó đã được hỏi lại. Câu hỏi còn đang chờ trả về 409 QUESTION_OPEN, câu đã được trả lời hoặc đã huỷ trả về 409 QUESTION_CLOSED, và câu đã được hỏi lại trả về 409 ALREADY_ASKED_AGAIN. |
| Cập nhật | POST /packages/install | Lần cài đặt bình thường với phiên bản thông báo nêu, nên mọi bước kiểm tra khi cài vẫn áp dụng. Nếu bị từ chối, bản bạn đang cài vẫn giữ nguyên. |
| Bỏ qua bản này | POST /inbox/notices/{id}/skip-version | Ngừng báo phiên bản đó của một gói, cùng các bản cũ hơn; bản mới hơn vẫn được báo. Phiên bản được đọc từ thông báo, không từ yêu cầu gửi lên. …/unskip-version hoàn tác việc này. Thông báo không phải về bản cập nhật trả về 409 NOT_AN_UPDATE. |
Thông báo rằng bộ máy của Clark (Pi SDK) có bản mới không cần bạn làm gì: bản mới sẽ có trong bản ClarkCant tiếp theo. Tiêu đề gọi theo bộ máy của Clark, phần nội dung vẫn ghi tên Pi SDK và các phiên bản, và nút đầu tiên là Bỏ, cạnh đó là Bỏ qua phiên bản này, không có nút Hỏi Clark.
Các phiên bản bạn đã bỏ qua nằm trong skippedVersions của GET /inbox, mới nhất trước. DELETE /inbox/skipped-versions/{kind}/{name}/{version} lấy lại một bản kể cả khi thông báo của nó đã không còn, với kind là package hoặc pi và mỗi phần được mã hoá URL, nên @scope/name vẫn dùng được.
curl -s -X DELETE "$CLARKCANT_URL/inbox/skipped-versions/package/%40scope%2Fname/1.4.0" \
-H "Authorization: Bearer $CLARKCANT_TOKEN"
Một route cho mọi thao tác của một thông báo
POST /inbox/notices/{id}/actions/{action} thực hiện bất kỳ thao tác nào ở trên theo tên. Đây là route mà các nút trong hộp thư của ứng dụng, một yêu cầu gõ hoặc nói, các agent của chính Clark, MCP tool act_on_notice và clarkcant api đều dùng. Thao tác là một trong mark-read, mark-unread, dismiss, restore (hoàn tác việc bỏ trong năm phút), snooze, unsnooze, suppress, unsuppress, retry, update, skip-version hoặc ask-again. Body là {}; snooze cần { "until": "<ISO instant>" }, tối đa 30 ngày sau.
curl -s -X POST "$CLARKCANT_URL/inbox/notices/<noticeId>/actions/dismiss" \
-H "Authorization: Bearer $CLARKCANT_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
Node đối chiếu thao tác với những gì thông báo đưa ra ngay lúc đó, và không đổi gì khi từ chối:
400 UNKNOWN_ACTION: tên đó không phải thao tác của thông báo. Tên thao tác được đọc đúng như viết trong đường dẫn và không bao giờ được giải mã, nên một tên mã hoá phần trăm bị từ chối ở đây.403 PERSON_ONLY:reconcile-confirmedvàreconcile-failed. Đây là câu trả lời của bạn về một hành động không ai thấy kết thúc, và chúng có route riêng (bên dưới).403 PERSON_ONLY:updatedo các agent của Clark, MCP hoặc relay WebSocket yêu cầu. Cài bản cập nhật là quyết định của riêng bạn, bằng nút Cập nhật trên thông báo, nênclarkcant api, relay vàcallcủa MCP cũng từ chối route này.409 SURFACE_ACTION:open,ask-clark,add-to-context,review-updatevàcopy-details. Chúng đổi thứ đang hiện hoặc đang giữ trên màn hình của bạn, nên chỉ ứng dụng làm được.404 RESOURCE_NOT_FOUND: thông báo không còn nữa.409 ACTION_NOT_OFFERED: lúc này thông báo không đưa ra thao tác đó.409 ACTION_UNAVAILABLE, kèm lý dounavailable: thông báo có liệt kê thao tác đó, nhưng lúc này chưa làm được.409 ACTION_IN_PROGRESS: bản cập nhật của thông báo đó đang được cài.409 UNDO_EXPIRED:restoređến sau khi đã bỏ quá năm phút.
Khi làm được, câu trả lời là { "noticeId", "action", "outcome": "done" }, cùng những gì thao tác tạo ra: snoozedUntil, workId mới, version hoặc questionId mới. Một bản cập nhật đã cài còn cho biết bao nhiêu quyền nó yêu cầu đang chờ bạn duyệt (pendingCapabilities) và bao nhiêu bị chế độ thực thi của bạn từ chối (deniedCapabilities). update trả nguyên mọi lời từ chối của bước cài đặt. Nếu chế độ thực thi của bạn yêu cầu hỏi trước khi cài, nó trả 202 với "outcome": "approval-required" và không cài gì; yêu cầu lại cùng phiên bản sẽ dùng lại yêu cầu duyệt vẫn đang chờ. Mỗi lần gọi mà route này xử lý được ghi vào nhật ký của node kèm bề mặt nó đến từ đó. Các route trong bảng vẫn dùng được như trước.
Các route hộp thư dùng cùng token nhưng chưa có trong /openapi.json và có thể thay đổi.
Cho biết một hành động đã có hiệu lực hay chưa
Khi một lệnh tác động ra ngoài máy của bạn, chẳng hạn một lần push, hết thời gian hoặc bị dừng trước khi báo kết quả, không ai biết nó đã được thực hiện hay chưa. Tác vụ chờ ở trạng thái chưa rõ, không tự làm lại hành động đó, và hộp thư của bạn nhận một thông báo về nó với hai nút: “Đã có hiệu lực” và “Chưa có hiệu lực”. Câu trả lời được ghi lại cùng người trả lời và thời điểm, không thể đổi về sau, và quyết định kết cục của tác vụ: tác vụ chỉ thành công khi hành động đã có hiệu lực và lần chạy đã kiểm tra kết quả của nó.
curl -s -X POST "$CLARKCANT_URL/effects/<effectId>/reconcile" \
-H "Authorization: Bearer $CLARKCANT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "outcome": "confirmed" }'
# outcome: "confirmed" (đã có hiệu lực) hoặc "failed" (chưa có hiệu lực)
source (click, chat hoặc voice) là nhãn tuỳ chọn cho biết câu trả lời được đưa ra ở đâu; nhãn được lưu đúng như gửi và không chứng minh điều gì. Một hành động thuộc tác vụ của người khác, của node khác, hoặc không tồn tại trả về 404 RESOURCE_NOT_FOUND; một hành động không còn ở trạng thái chưa rõ trả về 409 EFFECT_NOT_UNKNOWN. Route này ghi lại quyết định của một người, nên relay WebSocket, MCP và clarkcant api từ chối nó với 403 PERSON_ONLY: một AI client tự nói được rằng lần push của nó đã thành công thì cũng tự báo được thành công của chính nó.
Một tác vụ website chạy nền ghi các lần gửi biểu mẫu và những cú nhấp khác có thể làm thay đổi điều gì đó vào cùng sổ này. Khi một biểu mẫu đã được gửi đi mà không nhận được trả lời, kết quả là chưa rõ, biểu mẫu không bị gửi lại, và nó dẫn tới đây.
Chọn chủ đề
Chủ đề quy định màu sắc và độ bo góc của Clark, và từ appearance API 2 là cả phần còn lại của diện mạo (xem bên dưới). Nó tách biệt với chế độ màu (sáng, tối hoặc theo hệ thống), vốn vẫn áp dụng bên trong mọi chủ đề. Clark Default có sẵn, và các chủ đề khác đến từ những gói đã cài có khai báo facet themes. Chủ đề chỉ là dữ liệu: nó không chạy được mã, không thêm được style toàn cục, không với ra ngoài gói của nó và không tải được gì. Mọi chủ đề đều phải qua cùng phép kiểm tra độ tương phản như Clark Default và một phép kiểm tra giữ các trạng thái được bảo vệ tách biệt nhau, và chủ đề trượt một trong hai sẽ không được đưa ra để chọn. Các route này dùng cùng token nhưng chưa có trong /openapi.json và có thể thay đổi.
| Method | Đường dẫn | Body | Ghi chú |
|---|---|---|---|
| GET | /themes | – | { themes, problems, unchecked }. Clark Default đứng đầu, sau đó là từng chủ đề của gói kèm themeRef (package:<package id>#<theme id>) và nơi cung cấp: package id, phiên bản, digest, làn tin cậy và nguồn. problems nêu từng chủ đề không thể đưa ra và lý do; chủ đề trượt kiểm tra độ tương phản có kèm danh sách contrast các cặp màu không đạt. unchecked nêu từng gói mà node này không đọc được tệp. |
| GET | /appearance | – | { selectedRef, appliedRef, theme, provider, fallback }: điều bạn đã chọn và điều đang được vẽ. Khi lựa chọn không vẽ được, Clark Default được vẽ thay, lựa chọn của bạn vẫn được giữ, và fallback cho biết lý do bằng THEME_NOT_INSTALLED, THEME_INVALID, THEME_LOW_CONTRAST, THEME_PROTECTED, THEME_UNAVAILABLE hoặc THEME_UNKNOWN. Cài lại gói, hoặc lùi bản cập nhật đã làm hỏng nó, sẽ đưa chủ đề trở lại mà không cần chọn lại. |
| PUT | /preferences/experience.themeRef | { value } | Chọn một chủ đề: builtin:clark, hoặc một themeRef từ /themes. Tham chiếu mà node này không vẽ được sẽ bị từ chối với 409 cùng mã và lý do tương ứng, và không có gì được lưu. Giá trị hoàn toàn không phải là tham chiếu trả về 400 PREFERENCE_INVALID. |
Các trường message bằng tiếng Anh, dành cho log. Hãy hiển thị lỗi từ code và contrast bằng ngôn ngữ của người đọc, như ứng dụng vẫn làm. Không có gì được đẩy xuống khi một gói thay đổi, nên client đọc lại /appearance sau khi nó thay đổi một gói và khi cửa sổ của nó được mở lại. Trang đổi giao diện tại chỗ, không tải lại, và giữ nguyên cuộc trò chuyện, tin nhắn đang soạn và các khung đã ghim.
curl -s -X PUT "$CLARKCANT_URL/preferences/experience.themeRef" \
-H "Authorization: Bearer $CLARKCANT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "value": "package:com.example.theme-dusk#dusk" }'
Gói chủ đề tham chiếu
Pixel Arcade và Neo Brutalism là ví dụ chỉ chứa dữ liệu trong checkout ClarkCant. Cài qua vòng đời package hiện có, rồi chọn trong Cài đặt → Trải nghiệm hoặc xem trước trong Theme Lab. Tham chiếu của chúng là package:org.clarkcant.pixel-arcade#pixel-arcade và package:org.clarkcant.neo-brutalism#neo-brutalism. Đây là gói trong checkout; xuất bản trên marketplace vẫn là bước riêng.
Pixel Arcade có khung vuông, nút vát cạnh, bóng cứng, đường quét có giới hạn, chuyển động theo bước và Orb Plasma mặc định. Neo Brutalism có viền dày, bóng lệch, tiêu đề đậm, nút nổi và Orb Glass mặc định. Cả hai thích ứng qua cùng đường diện mạo của host, widget built-in/khai báo, snapshot widget cách ly và cửa sổ tách rời. Lựa chọn Orb riêng thắng các mặc định này; giảm chuyển động luôn thắng. Clark Default giữ nguyên.
Mỗi gói ghi giấy phép Apache-2.0 và nguồn. Phông là profile do host sở hữu với fallback hệ thống: Pixel Arcade dùng mono cho tiêu đề, typewriter cho mã và system dễ đọc cho nội dung; Neo Brutalism dùng system cho nội dung/tiêu đề và stack mono của host. Các gói không đóng gói và không tải tệp phông nào. Phông lấy từ stack hệ thống có thể khác nhau giữa hệ điều hành.
Xem trước và tùy chỉnh
Mở Cài đặt → Trải nghiệm → Duyệt chủ đề để vào Theme Lab. Nó dùng component sản phẩm thật với các ví dụ cục bộ được ghi rõ. Xem trước chủ đề đã cài giữ hội thoại, draft và focus; chỉ Dùng chủ đề này mới lưu lựa chọn. Chủ đề dùng gần đây do cùng bộ ghi preference lưu lại (tối đa sáu tham chiếu khác nhau); tham chiếu không còn trên node này không hiện thành nút gần đây.
GET /appearance?themeRef=<tham chiếu đã encode> phân giải chủ đề đã cài và kiểm tra để xem trước, không đổi preference. Tham chiếu sai hình dạng trả 400; chủ đề không có trả fallback Clark Default thông thường. Diện mạo cá nhân trả thêm customization: { accent, density, font, codeFont } khi có. Accent là null để dùng màu của chủ đề hoặc { dark: "#7AA2F7", light: "#2453A8" }; density là comfortable hoặc compact. font là phông giao diện cho tin nhắn, tiêu đề và mọi nút bấm, một trong các profile body bên dưới; codeFont là phông cho khối code, code trong dòng, đường dẫn và số liệu căn cột, một trong các profile mono. null giữ phông của chủ đề. Ghi qua PUT /preferences/experience.accent, experience.density, experience.font và experience.codeFont với { value }. Trong Cài đặt → Trải nghiệm, mỗi lựa chọn phông được vẽ bằng chính phông đó, nên bạn chọn bằng mắt. Mỗi màu nhấn hiện thành một ô màu cạnh mã hex, và bấm vào ô màu sẽ mở bảng chọn màu của hệ điều hành, nên bạn chọn được màu mà không cần biết mã của nó. Màu nhấn khó đọc hoặc che trạng thái được bảo vệ trả 409 trước khi đổi preference. Compiler chọn màu chữ dễ đọc trên nút nhấn; mật độ gọn giữ nguyên typography và giới hạn bố cục tối thiểu. Cả hai lựa chọn tới widget và cửa sổ detached qua cùng snapshot appearance.
Nếu bản cập nhật package khiến màu nhấn đã lưu không đạt kiểm tra, màu của chủ đề được vẽ và customizationFallback giải thích lý do; preference đã lưu vẫn được giữ. Đặt lại tùy chỉnh chủ đề đưa màu nhấn, mật độ, phông chữ, chuyển động và cá nhân hóa Orb về mặc định, giữ chủ đề đã chọn, chế độ sáng/tối và ngôn ngữ. Lựa chọn giảm chuyển động của hệ điều hành luôn thắng. Xem tạo chủ đề cho init, dev, test và pack.
Phần còn lại của diện mạo (appearance API 2)
Từ appearance API 2 ("appearanceApi": { "min": 2, "max": 2 }), chủ đề có thể đặt toàn bộ diện mạo chứ không chỉ màu sắc. Mỗi thiết lập là một cái tên hoặc một con số có giới hạn, và ClarkCant tự viết CSS:
typography:bodyvàdisplaychọn trongclark,system,serif,rounded,mono,inter,geist;monochọn trongclark,typewriter,jetbrains,geist-mono;headingWeighttừ 400 đến 800. ClarkCant tự mang theo Inter, Geist, JetBrains Mono và Geist Mono, nên chúng hiển thị giống nhau trên mọi hệ điều hành; các profile còn lại là stack phông của hệ thống.border:widthtừ 1 đến 3 px,stylelàsolidhoặcdashed.shadow:stylelàsoft,hardhoặcnone. Với bóng cứng,offsettừ 1 đến 8 px vàcolorlàtext,borderhoặcaccent.motion:speedtừ 0,5 đến 2,easinglàstandard,snappy,linearhoặcstepped.icons.stroke: từ 1 đến 2,5.radius.field: từ 0 đến 2 rem.recipes:buttonlà quiet, outlined, solid, raised hoặc beveled;cardlà flat, outlined hoặc raised;inputlà quiet, filled, outlined hoặc underlined;modallà floating hoặc framed;badgelà pill, rounded hoặc square;composerlà floating, integrated hoặc framed.effects:backdrop.kindlà dot-grid, hard-grid, scanlines, grain hoặc paper, vớiintensitytừ 0 đến 1 vàscaletừ 8 đến 48 px;surface.kindlà glass, soft-glow, paper hoặc grain, vớiintensitytừ 0 đến 1. Ánh sáng theo con trỏ trên lớp nền mạnh theo cường độ của lớp nền và không bao giờ sáng hơn ánh sáng của chính Clark. Kính phủ thẻ widget và ô soạn tin bằng một lớp mờ đục; chỉ hộp thoại là trong suốt và làm mờ nền.orb:profileOrb mặc định vàpalettetuỳ chọn gồm các bộ ba màu từ 0 đến 1 cho mọi kênh của Orb trừcanvas, kênh do trang luôn tự cung cấp. Nó chỉ có tác dụng cho đến khi bạn tự chọn Orb.
Chủ đề không được chứa CSS, selector, URL, tệp font hay ảnh, và trường, recipe hay hiệu ứng lạ, hoặc giá trị ngoài giới hạn, đều bị từ chối. Chủ đề phải qua hai phép kiểm tra ở cả chế độ sáng và tối: kiểm tra độ tương phản và kiểm tra trạng thái được bảo vệ. Phép kiểm tra trạng thái được bảo vệ yêu cầu:
- màu nguy hiểm, cảnh báo, thành công và màu nhấn phải khác nhau, và mỗi màu trạng thái phải khác chữ thường;
- vòng focus phải khác màu viền, và chữ bị vô hiệu hoá phải khác chữ đang dùng được;
- viền thẻ phải thấy được trên thẻ và trên nền trang;
- chữ, màu trạng thái, màu nhấn và vòng focus phải đọc được trên mọi bề mặt có hiệu ứng, và trên nền trang dưới lớp nền cùng ánh sáng theo con trỏ. Hộp thoại được đo trên nền trang sáng nhất và tối nhất mà lớp phủ của nó có thể che, nên kính quá mạnh hay hoa văn dày được chiếu sáng sẽ bị từ chối;
- ánh sáng của Orb phải thấy được trên nền trang, nên bảng màu tối đến mức làm Orb biến mất sẽ bị từ chối.
Chủ đề không đạt sẽ không được đưa ra để chọn. /themes liệt kê nó trong problems kèm từng mục không đạt, và chọn nó sẽ nhận 409 với THEME_LOW_CONTRAST (kèm danh sách contrast) hoặc THEME_PROTECTED (kèm danh sách protected gồm { scheme, check, first, second, value, minimum }). check là status-distinct, status-vs-text, focus-vs-border, disabled-distinct, edge-visible, surface-readable hoặc orb-visible, và first là "orb" với orb-visible. THEME_PROTECTED cũng có thể là mã fallback của /appearance. Hãy diễn đạt từng mã bằng ngôn ngữ của người đọc, và diễn đạt một mã bạn không biết bằng một câu chung.
Dù chủ đề nói gì, có những thứ luôn do ClarkCant kiểm soát:
- vòng focus và các điều khiển bị vô hiệu hoá;
- các thẻ duyệt, thông tin xác thực và kết nối của chính host: đường viền và bề mặt trơn của chúng, không kính, không vân, không phát sáng, không bóng;
- các nút trong những thẻ ấy và trong hộp thư, cùng nút Dừng. Đó là nút của chính ClarkCant mang màu của chủ đề, nên nút Phê duyệt luôn được tô màu nhấn còn nút Từ chối bên cạnh vẫn trơn;
- chế độ giảm chuyển động, dù đến từ hệ điều hành hay từ Cài đặt → Trải nghiệm → Chuyển động → Giảm. Khi đó mọi thời lượng bằng không, ánh sáng theo con trỏ trên nền tắt và Orb đứng yên.
Đổi diện mạo bằng cách yêu cầu
Bạn cũng đổi được diện mạo bằng cách nói với Clark, bằng giọng nói, hoặc qua POST /app-intents với appearance.set-theme (themeRef), appearance.set-color-scheme (colorScheme: light, dark hoặc system), appearance.reset (Clark Default, theo hệ thống) hoặc appearance.open-theme-gallery. Mỗi yêu cầu lưu lựa chọn giống như bộ chọn trong Cài đặt, và không yêu cầu nào hỏi xác nhận. Chủ đề được đối chiếu với các chủ đề của node này và bị từ chối bằng một câu nói rõ khi không vẽ được. Một yêu cầu làm việc, như "make a dark theme", được chuyển cho Clark thay vì đổi chế độ màu. Các câu có sẵn ("switch to dark mode", "chuyển giao diện sang tối") giữ nguyên nghĩa kể cả khi có chủ đề đã cài tên "Dark"; chủ đề như vậy được chọn từ bộ chọn hoặc qua agent.
Diện mạo cho tác giả widget
Widget built-in và Mini App khai báo theo chủ đề hiện tại bằng token semantic. Widget cách ly nhận cùng AppearanceSnapshot công khai đã kiểm tra, với chế độ sáng/tối đã phân giải, token có giới hạn, giảm chuyển động và revision. Nó không nhận tệp theme thô, thông tin xác thực hay quyền truy cập host.
api.appearance.current() trả về snapshot đóng băng sâu, hoặc undefined khi host cũ không cung cấp extension. api.appearance.subscribe(handler) nhận revision thay đổi và trả về hàm huỷ đăng ký. Snapshot ban đầu đến trước khi mount; đổi chủ đề giữ nguyên iframe, props và state, không ghi semantic hay tạo lượt model. Nội dung lịch sử, props/state đã lưu, nguồn gốc và văn bản dự phòng giữ nguyên.
import { bindAppearance } from "@clarkcant/widget-sdk/dom";
const unbind = bindAppearance(document.documentElement, api.appearance);
api.lifecycle.onDispose(unbind);
SDK core độc lập với DOM. Adapter tuỳ chọn chỉ ghi biến --cc-* chuẩn và thuộc tính diện mạo trên phần tử được truyền vào, giữ nguyên biến riêng của tác giả. Runtime /widget-runtime.js do host phục vụ cũng export bindAppearance và applyAppearanceToElement.
Bridge phiên bản 2 mang appearance tuỳ chọn trong init kèm appearance@1, rồi gửi { kind: "appearance.changed", nonce, revision, appearance } với revision trùng khớp và kiểm tra source/nonce hiện có. SDK mới nhận host phiên bản 1 không có diện mạo. SDK cũ phiên bản 1 đóng gói trong widget phải được nâng cấp để chạy với host phiên bản 2; hãy dùng runtime do host cung cấp hoặc build lại bằng SDK hiện tại. Composition tách cửa sổ nhận cùng revision đã phân giải qua relay chỉ đọc của desktop, không truy vấn theme hay nhận thông tin xác thực.
Định nghĩa widget mặc định dùng appearanceMode: "adaptive"; widget có thể khai báo "fixed" cho hệ thống giao diện riêng. Lab và thẻ chi tiết ghi rõ widget fixed. Directory có thể cung cấp widgetAppearance: [{ id, mode }] để hiển thị trên marketplace, nhưng định nghĩa đã cài và kiểm tra digest vẫn là nguồn quyết định. Widget fixed vẫn nhận diện mạo và phải tôn trọng giảm chuyển động; cả hai chế độ đều không thể đổi giao diện của host.
Ô bản đồ
Bản đồ vẽ một bản đồ nền ngoại tuyến và không yêu cầu ô bản đồ nào cho tới khi một nhà cung cấp được nêu tên trong preference maps.tilePolicy, vốn mặc định là null (xem Bản đồ). Bạn đặt nó trong Cài đặt → Tiện ích → Ô bản đồ, hoặc Clark đặt nó qua công cụ của mình theo execution policy của bạn. Việc ghi hoặc hoàn tác trực tiếp chính sách, cũng như nhập hoặc xóa khóa, là quyết định của riêng bạn: MCP, relay WebSocket và clarkcant api từ chối các route đó với 403 PERSON_ONLY. Các route ô bản đồ có trong /openapi.json.
| Method | Đường dẫn | Body | Ghi chú |
|---|---|---|---|
| PUT | /preferences/maps.tilePolicy | { value } | Đặt chính sách thành một nhà cung cấp, hoặc thành null để tắt ô bản đồ. origin đúng dạng scheme://host[:port], dùng https, hoặc http chỉ với địa chỉ loopback. template là một đường dẫn trên origin đó chứa {z}, {x} và {y}, mỗi cái đúng một lần. attribution là một dòng dài tối đa 200 ký tự, và maxZoom là số nguyên tối đa 19. template dài tối đa 300 ký tự, bắt đầu bằng / và chỉ dùng chữ cái, chữ số, / . _ ~ - = & ? cùng các ký hiệu giữ chỗ. Trong phần truy vấn của template, tên tham số nào chứa key, token, secret, sig, auth, pass, credential hoặc session đều bị từ chối: khóa thuộc về credential. credential tùy chọn là { secret: "maps:tiles" } kèm đúng một trong hai trường header hoặc query; chính sách nêu tên bất kỳ secret nào khác đều bị từ chối. Giá trị không hợp lệ trả về 400 PREFERENCE_INVALID, và không có gì được lưu. Chỉ dành cho người dùng: 403 PERSON_ONLY trên giao diện máy. |
| POST | /preferences/maps.tilePolicy/undo | – | Hoàn tác lần ghi cuối và trở về giá trị mà nó đã thay: { undone, preference }. undone: false, kèm reason, nghĩa là không có gì để hoàn tác. Chỉ dành cho người dùng: 403 PERSON_ONLY trên giao diện máy. |
| GET | /map-tiles | – | { provider: { origin, attribution, maxZoom } | null, offline? }. Khi provider là null, offline cho biết lý do: no-provider (mặc định), key-unavailable (chính sách cần khóa nhưng chưa lưu khóa dùng được) hoặc key-origin-mismatch (khóa đã lưu được nhập cho origin khác, nên node không gửi nó tới origin này). Không bao giờ trả về mẫu đường dẫn hay bất cứ phần nào của khóa. |
| GET | /map-tiles/key | – | { key: { origin? } | null }: đã lưu khóa hay chưa, và origin duy nhất mà khóa được gửi tới. Không bao giờ trả về giá trị. |
| PUT | /map-tiles/key | { origin, value } | Lưu khóa thành secret maps:tiles của node, gắn với origin; node chỉ gửi khóa tới đó. Nhập lại khóa cho origin khác sẽ chuyển ràng buộc, và không gì khác chuyển được. Trả về { key: { origin } }, không bao giờ trả về giá trị. Chỉ dành cho người dùng: 403 PERSON_ONLY trên giao diện máy. |
| DELETE | /map-tiles/key | – | Xóa khóa: { removed }. Chỉ dành cho người dùng: 403 PERSON_ONLY trên giao diện máy. |
| GET | /map-tiles/{z}/{x}/{y} | – | Một ô bản đồ từ nhà cung cấp của chính sách, do node tải về: image/png hoặc image/webp, được kiểm tra bằng loại nội dung của nhà cung cấp và chính các byte, tối đa 512 KiB, kèm nosniff. Chuyển hướng không được đi theo. Các lỗi từ chối: 404 MAP_TILES_OFF khi không có chính sách, 400 MAP_TILE_OUT_OF_BOUNDS khi zoom vượt maxZoom hoặc x và y nằm ngoài lưới của mức zoom đó, 429 MAP_TILES_RATE_LIMITED khi vượt 12 yêu cầu mỗi giây với đợt dồn 48, 404 MAP_TILE_MISSING khi nhà cung cấp không có ô nào ở địa chỉ đó, 502 MAP_TILE_FAILED hoặc MAP_TILE_REFUSED, và 503 MAP_TILE_KEY_UNAVAILABLE khi không dùng được khóa, thường kèm cùng lý do offline (trừ khi chính secret broker từ chối khóa). Tối đa 256 ô hoặc 24 MiB được lưu đệm trong một giờ. |
Khóa luôn là secret riêng của node, maps:tiles, với consumer duy nhất là maps:tiles@<origin>, tức origin mà khóa được nhập cho. Node thêm khóa, thông qua secret broker, dưới dạng header hoặc tham số truy vấn mà chính sách nêu tên, và chỉ vào các yêu cầu tới origin đó. Clark có thể nêu tên một nhà cung cấp nhưng không chuyển được khóa, và biểu mẫu credential chung (POST /credentials) từ chối tên maps:tiles cùng mọi consumer maps:tiles. Khóa không bao giờ tới trang, props, trạng thái widget, log, khóa bộ đệm, thông báo lỗi hay mô hình.
Ví dụ dưới đây gọi node qua HTTP trực tiếp bằng token của chính node. clarkcant api, MCP và relay WebSocket từ chối cùng lệnh gọi đó với 403 PERSON_ONLY.
curl -s -X PUT "$CLARKCANT_URL/preferences/maps.tilePolicy" \
-H "Authorization: Bearer $CLARKCANT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "value": { "origin": "https://tiles.example.com", "template": "/styles/basic/{z}/{x}/{y}.png", "attribution": "© Example contributors", "maxZoom": 17, "credential": { "secret": "maps:tiles", "header": "x-api-key" } } }'
Chính sách nội dung media
Trình phát âm thanh có thể phát một tệp từ web, nhưng trang không bao giờ tự tải nó: node của bạn tải nó, một lần, khi trình phát được đặt (xem Trình phát âm thanh và xem trước tài liệu). Không có route HTTP nào cho việc này; chính sách là thiết lập khởi động CC_MEDIA_ORIGINS của node, một danh sách các origin https trần phân tách bằng dấu phẩy, tối đa 32 mục, mặc định để trống. Một danh sách có bất kỳ mục nào không đúng dạng đó sẽ bị bỏ qua toàn bộ, node không cho phép gì, và nói lý do một lần khi khởi động.
CC_MEDIA_ORIGINS=https://media.example.com,https://cdn.example.org
Mỗi lần từ chối kết thúc bằng (media policy rule: <rule>) trong thông báo mà mô hình đọc:
| Quy tắc | Điều bị từ chối |
|---|---|
origin-not-allowed | Một origin không có trong CC_MEDIA_ORIGINS. |
https-only | Bất kỳ scheme nào ngoài https, trong URL hoặc trong một lần chuyển hướng. |
credentials-in-url | Tên người dùng hoặc mật khẩu trong URL hoặc trong một lần chuyển hướng. |
private-address | Một tên phân giải ra địa chỉ loopback, riêng tư, link-local hoặc địa chỉ dành riêng khác, được kiểm tra trên đúng địa chỉ thực sự được kết nối. Một origin được viết dưới dạng địa chỉ hoặc localhost chỉ đúng địa chỉ đó và được cho phép. |
redirect-off-origin, too-many-redirects | Chuyển hướng sang origin khác, hoặc quá 3 lần. |
type-not-allowed | Loại khai báo không phải audio/mpeg, audio/ogg, audio/wav hay audio/webm. |
type-mismatch | Các byte không đúng loại đã khai báo, được kiểm tra bằng cách nhận dạng định dạng chứa, hoặc truyền tải có nén. |
too-large | Lớn hơn 25 MiB, dựa trên độ dài khai báo hoặc ngay khi phần thân vượt quá mức đó. |
too-long, duration-unknown | Dài hơn một giờ, hoặc định dạng chứa không nêu độ dài của nó. |
timeout, not-found, fetch-failed | Lần tải lâu hơn 30 giây, tệp không tồn tại, hoặc không kết nối được. |
source | Không có đúng một nguồn, hoặc một ID mà node không cấp. |
Yêu cầu không mang cookie, thông tin xác thực hay referrer và dùng một kết nối mới. Tệp vượt qua kiểm tra được lưu thành artifact của cuộc hội thoại, tính vào hạn mức lưu trữ của bạn, và trang phát nó từ node; chính sách nội dung của trang vẫn là media-src 'self' blob:. Các bước kiểm tra loại tệp, kích thước và độ dài cũng áp dụng cho tệp âm thanh mà người dùng đã có sẵn.
Cài một gói
POST /packages/install cài gói mà bạn đã chọn. Chỉ ứng dụng của chính bạn gọi được route này: MCP, relay WebSocket và clarkcant api nhận 403 PERSON_ONLY, và không agent hay tool nào của mô hình cài được gói.
Với một gói được liệt kê bằng đường dẫn trên máy này, node của bạn tự tính digest các tệp của nó; bạn không cần gửi digest. Listing trên marketplace mang contentDigest, digest của các tệp đó lúc danh sách được tạo, và nút Cài gửi lại nó. Nếu các tệp đã đổi từ đó, lần cài bị từ chối với 409 DIGEST_MISMATCH, không có gì được cài hay được hỏi, và ứng dụng mời tìm lại thay vì cùng nút Cài. Lần cài sao chép các tệp vào bộ nhớ đệm gói của node và gói chạy từ bản sao đó, nên những chỉnh sửa sau này với các tệp của nó không thay đổi gì cho tới khi bạn cài lại, và lần cài lại kiểm tra chúng từ đầu. Trong lúc bạn đang làm một gói, clark widget dev là vòng chỉnh sửa trực tiếp. Nếu listing thay đổi dưới một gói đã cài (cùng phiên bản được liệt kê với digest khác, hoặc một phiên bản khác), các tệp, frame và lượt đọc widget của nó trả về 409 NOT_INSTALLED cho tới khi bạn cài lại, và các widget của nó giữ nguyên trạng thái. Một đường dẫn không sao chép được các tệp (không đọc được, chứa liên kết tượng trưng hay liên kết cứng, hoặc hơn 5.000 tệp, 5.000 thư mục, thư mục lồng sâu 64 cấp hay 64 MiB) bị từ chối với 400 LOCAL_SOURCE_UNREADABLE, còn các tệp thay đổi trong lúc đang được sao chép thì nhận 409 DIGEST_MISMATCH, nên bản sao không bao giờ là sự pha trộn của hai phiên bản. Một bản sao mà node không ghi được vào bộ nhớ đệm của chính nó (ví dụ khi ổ đĩa đầy) bị từ chối với 503 PACKAGE_CACHE_UNAVAILABLE; các tệp của bạn không bị thay đổi, và bạn có thể thử lại. Các kiểm tra này chạy trước khi chế độ thực thi của bạn quyết định. Client nào gửi localDigest vẫn hoạt động như trước.
Khi chế độ thực thi của bạn yêu cầu hỏi trước khi cài, node của bạn không cài gì và trả 202 kèm { "code": "APPROVAL_REQUIRED", "message", "approvalId" }. Lần cài đó chờ bạn trong hộp thư: GET /inbox liệt kê nó trong waiting với kind: "install-approval", kèm gói, phiên bản, những quyền gói xin, mức rủi ro, operationDigest, requestedAt và expiresAt. Mục này chỉ được liệt kê khi thư mục gói vẫn còn công bố đúng gói, đúng phiên bản và đúng digest đó.
Bạn quyết định bằng POST /packages/approvals/{approvalId}/decision với { "decision": "granted" | "denied", "digest" }, gửi đúng digest mà mục đã hiện. Đây cũng là route chỉ bạn dùng được để duyệt quyền của gói.
- Từ chối: không cài gì, và các gói của bạn vẫn giữ nguyên.
- Duyệt: node của bạn chạy đúng lần cài đó, qua mọi bước kiểm tra khi cài, gắn với digest đó.
Một quyết định duyệt vẫn có thể bị từ chối:
409 DIGEST_MISMATCH: trang gói đã đổi sau khi hỏi. Câu hỏi vẫn mở, và chưa cài gì.409 DIGEST_MISMATCH, với gói được liệt kê bằng một đường dẫn trên máy bạn: các tệp của gói đã đổi sau khi hỏi. Chưa cài gì và câu hỏi rời khỏi hộp thư. Hãy cài lại để được hỏi về các tệp như hiện tại.400 LOCAL_SOURCE_UNREADABLE: không đọc được các tệp ở đường dẫn cục bộ, nên bị từ chối ngay khi định hỏi.403 POLICY_REFUSED: chế độ thực thi của bạn giờ cấm cài.APPROVAL_FORGED: digest gửi lên không phải digest đã hỏi.APPROVAL_ALREADY_DECIDED: câu hỏi đã được quyết định rồi, nên bấm lần hai không bao giờ cài hai lần.APPROVAL_EXPIRED: đã quá mười phút.
Lần cài không ai quyết định sẽ hết hạn sau mười phút. Không cài gì, và một thông báo nói rõ điều đó. Mọi kết quả (đã hỏi, đã cài, đã từ chối, hết hạn, bị từ chối, thất bại) được ghi vào luồng sự kiện package.install-approval. Nút Cập nhật trên thông báo và nút Cài trong marketplace đều dẫn tới mục chờ này và nói nơi để quyết định.
Trước khi bạn cài, thẻ thư mục gói và câu hỏi cài đặt liệt kê những gì gói chạm tới bên ngoài vùng cách ly (xem những gì một gói chạm tới). Một artifact có manifest khai báo phạm vi khác với những gì trang gói đã hiện sẽ bị từ chối với 409 DECLARED_REACH_MISMATCH trước khi có bất cứ điều gì được ghi lại.
Dừng mọi thứ
curl -s -X POST "$CLARKCANT_URL/stop" \
-H "Authorization: Bearer $CLARKCANT_TOKEN"
Xoá hội thoại
Gõ “xoá hội thoại này”, hoặc yêu cầu bằng giọng nói. Clark dùng policy thực thi hiện tại của bạn: thực hiện yêu cầu rõ ràng, hỏi trước hoặc từ chối. Khi policy yêu cầu xác nhận, hội thoại hiện Giữ hội thoại và Xoá hội thoại. Không giữ bản sao để Hoàn tác vì mục tiêu là giải phóng tệp và hạn mức.
POST /conversations/{id}/delete nhận {} trên node gốc của hội thoại. Đây là hành động của người dùng: MCP, relay WebSocket và clarkcant api từ chối route này cùng route xác nhận bằng 403 PERSON_ONLY; app intent do agent yêu cầu cũng không được xoá.
202trả về{ deleted: false, decision: { kind: "needs-confirmation", intent, readBack, confirmationToken } }khi policy hỏi. Chưa xoá gì.- Trả lời bằng
POST /app-intents/confirmvà{ confirmationToken, decision: "granted" | "denied" }. Từ chối giữ nguyên mọi thứ. Duyệt trả về decision cóintent.deletionPermit. - Gửi
{ deletionPermit }tới route xoá ban đầu. Quyền này thuộc đúng người và hội thoại, hết hạn sau hai phút và chỉ dùng một lần. Policy và hoạt động được kiểm tra lại; từ chối mới luôn thắng quyền đã duyệt.
200 trả về { deleted: true, conversationId, attachments, artifacts, pendingFiles, readBack }. Các row của hội thoại, tệp đính kèm, tệp widget (kể cả tệp đã hoàn tất nhưng chưa đính kèm), grant và hàng đợi dọn tệp commit cùng nhau, với foreign key vẫn bật. Tệp vật lý bị xoá sau commit; byte còn dùng ở nơi khác được giữ. Tệp đang khoá chờ lần khởi động hoặc lượt dọn định kỳ tiếp theo; pendingFiles báo số tệp còn chờ.
409 trả về { deleted: false, decision: { kind: "refused", say } }, giải thích cái gì được giữ và bước tiếp theo. Task chưa kết thúc, đang tạm dừng hoặc chưa rõ kết quả, lượt trả lời, công việc nền và thao tác widget còn đang xử lý kết quả đều chặn việc xoá. Hãy hoàn tất, huỷ hoặc đối soát công việc đó rồi thử lại.
Bộ nhớ đã lưu, tài nguyên độc lập, nhật ký phiên và lịch sử kiểm toán/đồng bộ chỉ ghi thêm được giữ lại. Sau khi xác nhận thành công, ứng dụng bắt đầu hội thoại mới. Nếu mất phản hồi mạng thì chưa rõ kết quả: tải lại trước khi thử lại.
Tệp mà một widget giữ
Widget không bao giờ nhận được đường dẫn. Nó giữ một tệp dưới dạng artifactRef, { v, artifactId, kind, mimeType, sizeBytes, name, digest? }, và máy của bạn kiểm lại quyền của widget mỗi lần ref được dùng, nên một ref bị chép sang chỗ khác không mở được gì. Một quyền kéo dài 24 giờ kể từ lúc chọn tệp hoặc lần ghi gần nhất của widget. Sau đó, widget vẫn đọc được tệp do chính nó tạo và đã hoàn tất, còn tệp người dùng đã chọn thì phải được chọn lại. Một tệp còn đang ghi dở bị xoá 24 giờ sau lần ghi gần nhất. Mỗi tệp tối đa 25 MiB, và mỗi widget giữ tối đa 128 MiB trong 1 GiB của bạn. Phần đó tính các tệp widget đã tạo và những gì nó đính kèm từ các tệp ấy, nhưng không tính các tệp bạn đã chọn cho nó. Widget chỉ bỏ được tệp do chính nó tạo. Mọi câu trả lời là một ref, các byte, hoặc một lời từ chối có mã nói rõ điều gì sai. Tên tệp được gửi đi dưới cả hai dạng filename và filename* (RFC 6266), nên một tên có dấu được tải xuống đúng như khi viết.
Xoá hội thoại giải phóng tệp widget và grant của nó, kể cả tệp đã hoàn tất nhưng chưa đính kèm. Byte dùng chung được giữ; việc dọn tệp vật lý chờ database commit.
Các route của widget bắt đầu bằng /conversations/{id}/widgets/{instanceId}/artifacts, viết tắt là … ở bảng dưới.
| Method | Path | Body | Ghi chú |
|---|---|---|---|
| GET | /artifacts/{artifactId} | – | Artifact, kèm artifactRef của nó. Artifact của người khác trả về 404 ARTIFACT_NOT_FOUND. |
| GET | /artifacts/{artifactId}/content | – | Các byte của một tệp đã hoàn tất. 409 ARTIFACT_NOT_FINALIZED khi tệp vẫn đang được ghi. |
| POST | /artifacts/{artifactId}/export | { suggestedName? } | Lưu thành: các byte được tải xuống dưới một tên tệp, không bao giờ là một đường dẫn. Tên giữ phần mở rộng theo kiểu của tệp. Relay WebSocket, MCP và clarkcant api từ chối nó với 403 PERSON_ONLY. |
| POST | … | { mimeType, name? } | Một tệp mới mà widget này được ghi. 201 { artifactRef }. |
| POST | …/pick | { name, mimeType, contentBase64, accept? } | Một tệp người dùng đã chọn, trao cho widget này. Bị các relay đó từ chối với 403 PERSON_ONLY. |
| GET | …/{artifactId}/content?offset=&length= | – | { artifactRef, offset, eof, contentBase64 }, tối đa 262.144 byte mỗi lần đọc. |
| POST | …/{artifactId}/chunks | { offset, contentBase64 } | Tối đa 262.144 byte, bắt đầu từ chỗ tệp đang kết thúc. 409 ARTIFACT_INSTANCE_QUOTA_EXCEEDED khi widget đã giữ 128 MiB. |
| POST | …/{artifactId}/finalize | – | Chốt các byte sau khi kiểm chúng với kiểu đã khai báo. |
| POST | …/{artifactId}/attach | { name? } | 201 { artifactRef, attachmentRef }. Tệp chờ trong ô soạn tin; người dùng gửi nó cùng tin nhắn tiếp theo. name đề xuất tên tệp cho tệp đính kèm. Node làm sạch tên đó, và cả tên riêng của artifact khi không có name: node bỏ phần đường dẫn cùng các ký tự không an toàn hoặc vô hình, giới hạn độ dài, và đặt phần mở rộng theo kiểu của tệp. Khi không còn gì dùng được, node dùng tên mặc định như untitled.png. Lần đính kèm đầu tiên quyết định tên: đính kèm lại cùng một tệp sẽ trả về đúng tệp đính kèm đó. |
| DELETE | …/{artifactId} | – | Bỏ một tệp do widget này tạo: { discarded: true, artifactId }. Các byte của nó bị xoá khi không còn gì dùng tới. Tệp của widget khác, hoặc tệp người dùng đã chọn, trả về 403 ARTIFACT_NOT_CREATOR. |
Các route này có trong /openapi.json. Chọn một tệp và Lưu thành là hành động của chính người dùng, nên widget phải nhờ host làm và không tự làm được. Thay tệp mà widget đã mở chỉ có trong ứng dụng desktop, chỉ bằng một tệp cùng kiểu, và nút này nêu tên cả hai tệp. Trên trình duyệt, Lưu thành là một lượt tải xuống, nên ứng dụng báo “Đã bắt đầu tải xuống” chứ không nói tệp đã được lưu.
Thao tác ghi tệp của widget từ bề mặt máy
Trên bề mặt máy (MCP, relay WebSocket, clarkcant api), năm thao tác ghi tệp của widget (tạo, ghi chunk, chốt, đính kèm, bỏ) đi qua chính sách thực thi của người dùng, và mọi lần đều được ghi nhật ký kiểm toán, không kèm byte. Mỗi lần là một hiệu ứng local-write. Ngoại lệ là bỏ một tệp không phải tệp đang ghi dở do chính bên yêu cầu đó bắt đầu: việc này có thể xoá bản duy nhất của người dùng, nên nó là destructive và được hỏi ở cả chế độ Có rào lẫn chế độ Tự chủ. Chính sách hoặc cho chạy, hoặc từ chối với 403 POLICY_REFUSED, hoặc hỏi. Khi người dùng đã chọn được hỏi về các lượt từ bề mặt máy (machineTurns: "ask"), mỗi lần ghi được quyết định như một lượt do bề mặt đó yêu cầu: khi đó một lần bỏ tệp destructive vẫn được hỏi kể cả khi có quy tắc cho chạy hiệu ứng destructive, và thẻ nói rõ ai đã yêu cầu.
Khi hỏi, một thẻ phê duyệt do host sở hữu xuất hiện trong hội thoại, bằng ngôn ngữ của người dùng, và bên gọi nhận 202 { outcome: "approval-required", approvalRequired: { approvalId } }. Chỉ người dùng quyết định được thẻ đó. Hỏi lại đúng việc đó sẽ nhận lại thẻ đang chờ. Quá 8 thẻ đang chờ từ một bề mặt trong một hội thoại, câu trả lời là 429 APPROVALS_PENDING. Số đếm này nằm trong bộ nhớ, nên khởi động lại sẽ đặt lại nó. Mỗi thẻ là cho một tệp, không bao giờ cho từng chunk, và không bao giờ chứa byte.
Duyệt một lệnh tạo, hoặc quyền ghi vào một tệp đang ghi dở, sẽ cấp quyền ghi 15 phút trên đúng tệp đó: các chunk và lệnh chốt chạy mà không cần thẻ khác. Quyền kết thúc khi tệp được chốt, bị bỏ hoặc hết hạn. Qua relay WebSocket, chỉ đúng kết nối đã yêu cầu giữ quyền này. Lời gọi MCP và các lần chạy clarkcant api không có danh tính riêng của từng client, nên ở đó mọi client của bề mặt đó đều giữ quyền, không chỉ client đã yêu cầu. Thẻ và biên nhận nói rõ điều này.
Job chạy lâu của package
Một capability của package có công việc kéo dài hơn một lần bấm khai báo "execution": { "kind": "job", "version": 1 } trong tools facet. Một lần bấm vào nó không chờ service: api.actions.invoke trả về một JobRef, là một id job_… không mang nghĩa, và máy của bạn chạy lời gọi ở nền tối đa 30 phút. Widget giữ JobRef trong state của nó, nên vẫn theo dõi được đúng job đó sau khi được mở lại.
const jobId = await api.actions.invoke("binding_notes_export", { steps: 12, stepMs: 1500 }, invocationId);
await api.state.update((state) => ({ ...state, exportJob: jobId }), { exportJob: jobId });
const stop = api.jobs.subscribe(jobId, (job) => render(job));
api.jobs.get(ref) đọc trạng thái của job (queued, running, waiting, completed, failed hoặc cancelled), tiến độ, output, lỗi và các tệp kết quả. api.jobs.subscribe(ref, handler) kiểm tra mỗi giây, chỉ chuyển đi những thay đổi, và tự dừng khi job kết thúc. api.jobs.cancel(ref) dừng job. Tiến độ chỉ là những gì chính service đã báo, và tệp kết quả đến dưới dạng artifactRef, không bao giờ là đường dẫn.
Liệt kê các job của widget (jobs.list@1). api.jobs.list() trả về các job do chính các nút của widget này khởi động, mới nhất trước và tối đa 20, kể cả các job Clark hay giọng nói khởi động qua cùng nút đó, nên widget có thể hiện công việc không bắt đầu từ một cú bấm. Việc liệt kê ra đời sau jobs@1 và được cung cấp như một extension riêng, jobs.list@1, bên cạnh nó; api.jobs.canList() cho biết ứng dụng có cung cấp hay không, và api.jobs.list() từ chối mà không gửi gì khi ứng dụng không cung cấp. Widget có liệt kê thì kiểm tra trước: không có danh sách, nó hiện các job được khởi động trong lúc nó đang mở và nói rõ rằng các job trước đó không được hiện. Một job trong danh sách có cùng các trường như job đọc bằng api.jobs.get.
Một job thất bại có thể mang chính lời của service. Khi service báo lỗi, error của job trích lời đó bên trong câu của chính node: The package service reported an error: “…”. Its effect may have happened; review before retrying. Phần trong ngoặc kép là của service, và có thể là của provider mà service gọi: các ký tự điều khiển và ký tự định dạng vô hình (kể cả ký tự đảo chiều bidi) bị loại bỏ và phần đó bị cắt ở 400 ký tự, nhưng ngoài ra không có gì bảo đảm cho nó. Hãy hiển thị nó như lời service đã nói, bên trong một câu của riêng bạn. Khoá provider mà provider gửi lại đã được thay bằng [redacted] ở đó (cách một service tới provider của nó).
JobRef là con trỏ, không phải quyền. Máy của bạn chỉ trả lời widget có chính nút bấm đã khởi động job, trong cùng hội thoại, với cùng phiên bản package và capability; mọi trường hợp khác, kể cả một ref bị chép sang widget khác, đều là 404 JOB_NOT_FOUND, giống hệt một job chưa từng tồn tại. Nếu chế độ thực thi của bạn cần hỏi trước khi capability chạy, lần bấm sẽ hiện thẻ phê duyệt của ứng dụng trước, và job chỉ bắt đầu sau khi bạn phê duyệt ở đó.
Một job chạy tối đa 30 phút; quá thời hạn đó nó kết thúc ở trạng thái thất bại. Trong lúc chạy, nó nằm trong danh sách công việc đang chạy của node (GET /work), và việc dừng nó ở đó (POST /work/{id}/cancel), Dừng khẩn cấp và việc tắt node đều huỷ nó. Nút Dừng của hội thoại kết thúc câu trả lời, không kết thúc một job đã chạy, giống như các công việc nền khác. Vì service có thể đã làm xong việc trước khi nhận được lệnh huỷ, job sẽ báo rằng nó “có thể đã hoàn tất tác động” thay vì khẳng định không có gì xảy ra. Job vẫn đang chạy khi node khởi động lại được đánh dấu thất bại và không bao giờ tự chạy lại. Khi job kết thúc, hội thoại và hộp thư đều báo, nêu tối đa ba tệp kết quả. Hội thoại còn job đang chạy thì không xoá được cho tới khi job kết thúc hoặc bị huỷ.
| Method | Path | Body | Ghi chú |
|---|---|---|---|
| GET | /conversations/{id}/widgets/{instanceId}/jobs | – | { jobs: [...] }, mới nhất trước và tối đa 20: các job do chính các nút của widget này khởi động. Mỗi mục có cùng các trường như job của route một job và qua cùng bước kiểm tra chủ sở hữu. Instance không thuộc hội thoại là 404 INSTANCE_UNKNOWN. Trong widget, đây là api.jobs.list(). |
| GET | /conversations/{id}/widgets/{instanceId}/jobs/{jobId} | – | { job: { jobId, status, progress?, resultRefs, output?, error?, createdAt, startedAt?, endedAt? } }. |
| POST | /conversations/{id}/widgets/{instanceId}/jobs/{jobId} | – | Huỷ job: 202 { accepted, jobId }. Job đã kết thúc trả về 409 JOB_NOT_RUNNING và vẫn đọc được. |
Các route này có trong /openapi.json. Node không chạy được job trả về 503 JOB_UNAVAILABLE. POST /stop đếm các job đã huỷ trong stopped.jobs. Theo thiết kế, các route này, kể cả route liệt kê, mở cho client máy như client MCP hay relay, không chỉ cho màn hình của chính bạn: một client máy có thể liệt kê các job của một instance, output của chúng và các artifactRef mà không cần biết trước JobRef nào. Nó không biết thêm gì mà nó không đọc được từng job một, và không đọc được byte của tệp qua các route này.
Lần bấm đọc điều widget đang hiển thị
Một widget cách ly mô tả điều nó đang hiển thị bằng api.semantic.publish(summary, selectedIds, values?). Ứng dụng gửi lần publish cuối của một loạt sau 250 ms, và gửi các lần publish lần lượt từng cái, đúng thứ tự. Một binding agent có thể đọc mô tả đó khi được bấm, qua contextRefs của nó (selection, widget), nên một lần bấm ngay sau một lần publish không được chạy dựa trên mô tả trước đó.
Trước khi chạy lần bấm như vậy, ứng dụng gửi lần publish còn đang chờ ổn định và chờ đến khi máy của bạn đã giữ mô tả được publish trước lần bấm, hoặc một mô tả mới hơn. Các lần publish sau lần bấm vẫn được gửi như thường nhưng không được chờ, nên một widget publish liên tục không thể giữ lần bấm lại. Các lần bấm khác chỉ chờ lần publish còn đang chờ ổn định, hoặc lần mà máy của bạn vừa từ chối.
Thời gian chờ có giới hạn: mỗi lần gửi bị bỏ sau 5 giây và toàn bộ việc chờ sau 8 giây. Mô tả bị máy của bạn từ chối được gửi lại một lần. Nếu vẫn hỏng, hoặc hết thời gian chờ, api.actions.invoke bị từ chối kèm câu của ứng dụng (“Chưa gửi kịp cho Clark điều widget đang hiển thị, nên hành động này chưa chạy…”) và lần bấm không chạy. Không có gì trong widget bị thay đổi, và người dùng có thể bấm lại. Hãy hiện lời từ chối đó ở chỗ người dùng đã bấm. Trình soạn thảo văn bản mẫu làm đúng như vậy cho nút viết lại của nó.
Hành động Clark nhờ một widget thực hiện
Một widget cách ly có thể cung cấp cho Clark những hành động do chính nó thực hiện, chẳng hạn định dạng các ô đang chọn: widget khai báo chúng trong offeredActions và xử lý từng hành động bằng api.actions.offer(name, handler) (actions.perform@1). Khi đó Clark có thể nhờ một widget đang hiện trên màn hình thực hiện một trong các hành động đó, qua chính sách thực thi của bạn, và chỉ màn hình đang hiện widget mới thực hiện nó. Clark không bao giờ tự ghi vào widget và không bao giờ tự duyệt hành động.
Khi chính sách thực thi muốn hỏi trước, Clark đặt một thẻ phê duyệt vào cuộc trò chuyện, và không có gì được gửi cho widget cho tới khi bạn phê duyệt. Thẻ hiện toàn bộ dữ liệu widget sẽ nhận; một hành động có dữ liệu quá dài để hiện đầy đủ (hơn 1.200 ký tự) bị từ chối thay vì chỉ hiện một phần. Nếu Clark yêu cầu lại cùng hành động với cùng dữ liệu trong lúc thẻ của nó còn chờ, bạn nhận lại đúng thẻ đó, không có thẻ thứ hai, và một cuộc trò chuyện giữ tối đa 8 thẻ hành động đang chờ, dù có bao nhiêu bề mặt đã yêu cầu. Phê duyệt sẽ hỏi widget trên màn hình bạn phê duyệt; phê duyệt từ hộp thư, hoặc từ một trang không hiện widget, được ghi là “đã duyệt nhưng không thực hiện”, và không có gì được gửi. Thực hiện một hành động được cung cấp là thao tác ghi cục bộ, nên riêng lựa chọn được hỏi về lượt của chương trình không làm nó bị hỏi; một quy tắc hoặc chế độ có hỏi thì sẽ hỏi. Ai đã yêu cầu được ghi trên thẻ phê duyệt và trong nhật ký kiểm toán, kể cả sau khi bạn duyệt.
Dành cho tác giả widget. Để từ chối một hành động được cung cấp, hãy throw api.actions.refuse("CODE", "lý do") trước khi thay đổi bất cứ điều gì. Mọi lỗi khác, kể cả lời từ chối của chính SDK, nghĩa là widget có thể đã thay đổi điều gì đó: Clark nói rằng không biết hành động đã có hiệu lực hay chưa và không thử lại.
Tài nguyên mà một gói được chạy cùng
Các service của một gói chạy trong một container trên máy của bạn. Gói xin kích thước cho container đó bằng cách nêu tên một resource profile trong manifest, không bao giờ bằng con số:
"resources": { "version": 1, "profile": "interactive-heavy" }
Node của bạn giữ bảng profile và quyết định cấp gì. Gói không nêu profile nào chạy với interactive-light, đúng giới hạn mà mọi service đã chạy trước khi có profile, từng giá trị một, nên một gói có sẵn vẫn chạy y như trước.
| Profile | Bộ nhớ / CPU / tiến trình / /tmp | Mỗi lệnh gọi | Mỗi job | Số job cùng lúc | Khi ra khỏi màn hình |
|---|---|---|---|---|---|
interactive-light (mặc định) | 256 MiB / 1 / 128 / 16 MiB | 60 giây | 30 phút | 4 | Bị gỡ khỏi màn hình |
interactive-heavy | 1 GiB / 2 / 256 / 64 MiB | 120 giây | 30 phút | 2 | Bị gỡ khỏi màn hình |
media-workstation | 4 GiB / 4 / 512 / 512 MiB | 300 giây | 2 giờ | 1 | Có thể tiếp tục phát |
background-compute | 2 GiB / 2 / 256 / 256 MiB | 60 giây | 4 giờ | 2 | Bị gỡ khỏi màn hình |
Mọi profile đều chạy không có mạng (--network none) và với /tmp ở chế độ noexec. Profile đặt bộ nhớ, số CPU, giới hạn tiến trình và kích thước /tmp của container, thời hạn của lệnh gọi và của job, và số job chạy cùng lúc. Service và job vẫn chạy dù widget của chúng có đang hiện trên màn hình hay không. Tệp lớn nhất mà một service trả về vẫn bằng giới hạn tệp đính kèm, nên luôn đính kèm được vào hội thoại.
Node của bạn quyết định theo thứ tự sau:
- Không bao giờ cấp GPU: node không chuyển GPU vào container.
interactive-lightluôn được cấp.- Lời từ chối của policy thực thi luôn thắng.
- Một profile không được cấp khi nó cần nhiều CPU hơn số container engine báo, hoặc hơn một nửa bộ nhớ của engine. Với Docker Desktop, đó là dung lượng của máy ảo Linux của nó.
Một profile không cấp được thì không bao giờ bị đổi sang profile nhỏ hơn. Thay vào đó gói ở trạng thái suy giảm: các service của nó không khởi động, và mỗi capability hiện lý do. Cài đặt → Tiện ích hiện profile bên cạnh mỗi gói đã cài: các giới hạn khi được cấp, hoặc “Đã xin … nhưng không được cấp: …” khi không.
Podman rootless không được uỷ quyền các controller cgroup v2 chấp nhận giới hạn bộ nhớ và CPU nhưng không áp dụng chúng. Node của bạn vẫn cấp profile ở đó, và chi tiết gói ghi rằng container engine ở đây không áp giới hạn bộ nhớ và CPU, thay vì khẳng định có giới hạn.
Một widget bị gỡ khỏi màn hình khi nó cuộn ra ngoài vùng nhìn thấy. Chỉ widget có gói được cấp media-workstation mới có nút bật “Tiếp tục phát khi cuộn đi” trong khung do chính ứng dụng vẽ. Khi bật, widget vẫn được giữ lại và ứng dụng báo “tên widget vẫn đang chạy ngoài màn hình”. Nút này tắt với mỗi lần widget được gắn mới, widget không tự bật được, và nút Dừng vẫn kết thúc nó.
Cách một service gọi tới provider của nó
Container của service vẫn ở --network none, và service không bao giờ giữ khoá của provider. Tools facet của gói khai báo các khoá nó cần và các origin nó được gọi tới, mỗi thứ kèm mục đích:
"egress": {
"version": 1,
"secrets": [{ "name": "LOOKUP_API_KEY", "purpose": "Signs the lookups in with the provider." }],
"origins": [{
"origin": "https://api.example.com",
"purpose": "Looks up the words you ask about.",
"credential": { "secret": "LOOKUP_API_KEY", "header": "authorization", "scheme": "bearer" }
}]
}
Node của bạn là MCP client của từng service của gói, qua stdio. Với service có khai báo egress, lệnh initialize của node đề nghị capabilities.experimental["clarkcant/egress"] với version: 1. Khi đó service gửi cho node request clarkcant/egress.fetch với { version: 1, url, method?, headers?, body? } trên cùng kết nối, và chính node thực hiện request HTTP:
- Chỉ tới origin đã khai báo. Origin phải khớp chính xác một origin đã khai báo, và URL có chứa thông tin đăng nhập bị từ chối. Node không đi theo chuyển hướng, nên khoá không bao giờ bị gửi sang origin khác.
- Không tới origin loopback hay mạng riêng.
localhost, địa chỉ loopback, mạng riêng và link-local (IPv4, IPv6 và dạng IPv4-mapped) bị từ chối kể cả khi đã khai báo, trừ khi bạn khởi động node vớiCC_EGRESS_ALLOW_PRIVATE_NETWORK=1. Mặc định tắt, và không trường manifest nào bật được nó. Việc kiểm tra đọc host của URL; nó không kiểm tra một tên miền công khai phân giải ra địa chỉ nào. - Chỉ khi có lệnh gọi đang chạy. Request chỉ được trả lời khi node của bạn đang có một lệnh gọi tới service đó. Request dừng khi lệnh gọi cuối cùng kết thúc, bị huỷ, hoặc service bị dừng.
- Lệnh gọi chỉ đọc thì chỉ được đọc. Khi mọi lệnh gọi đang chạy đều được quyết định là
readhoặclocal-write, request chỉ được dùngGEThoặcHEAD. Phương thức khác cần một lệnh gọi được quyết định làexternal-write,destructive,financialhoặccommunication, loại mà bước kiểm tra rủi ro của policy thực thi sẽ hỏi. - Giới hạn tần suất. Mỗi service đang chạy được dồn tối đa 30 request, nạp lại 10 request mỗi giây.
- Node tự gắn khoá. Node thêm header đã khai báo từ khoá bạn lưu cho consumer
package:<id>; header cùng tên do service đặt bị bỏ. Header cookie, proxy, chuyển tiếp và framing bị loại bỏ. Mỗi request gửi tối đa 1 MiB, nhận tối đa 2 MiB và kéo dài tối đa 30 giây. - Che khoá bí mật. Khoá được thay bằng
[redacted]trong header và body trả về, ở dạng nguyên bản và ở các dạng JSON-escaped, URL-encoded, base64 và base64url. Đây là lớp bảo vệ cố gắng hết mức trước provider lặp lại khoá, không phải bảo đảm: khoá trả về ở dạng khác, như bị tách, băm hoặc mã hoá, vẫn tới được service.
Lời từ chối là lỗi JSON-RPC từ -32010 tới -32018: origin chưa khai báo, không có lệnh gọi đang chạy, khoá không dùng được, quá lớn, không kết nối được provider, đã dừng, phương thức không được các lệnh gọi đang chạy cho phép, quá nhiều request, và origin mà node này không gọi tới. -32019 là request bị từ chối vì một lệnh gọi đang chạy giữ một tệp bạn đã chọn (tệp mà một service đọc). Mỗi request được ghi kiểm toán là egress với gói, phương thức, origin, tên khoá, trạng thái và kết quả, không bao giờ ghi đường dẫn, body hay giá trị. Phương thức này chỉ nằm giữa node và các service của chính nó; nó không có trên POST /mcp.
Cho tới khi khoá đã khai báo được lưu, các capability của gói được xem là chưa đăng nhập (authenticated: false) kèm lý do, ví dụ “the secret LOOKUP_API_KEY has not been provided on this node”. Một lần bấm, agent và giọng nói đều bị từ chối với 409 CAPABILITY_NOT_AUTHENTICATED. Đây là trạng thái “cần đăng nhập” của một service; trạng thái vòng đời widget needs_auth không được đặt cho nó. Lưu khoá cho package:<id> bằng POST /credentials sẽ đăng nhập service mà không cần khởi động lại, và xoá khoá sẽ đăng xuất nó. Khoá lưu cho consumer command: không được dùng cho egress, nên hãy lưu một khoá riêng cho gói.
Chưa có mạng qua proxy cho service cần socket thô. Cho container một mạng sẽ làm yếu một mặc định cách ly.
Tệp mà một service đọc
Một service có thể làm việc trên tệp mà một widget giữ mà không bao giờ nhận đường dẫn hay handle tới tệp đó. Một capability nêu các trường đối số mang id artifact trong facet tools của nó:
{ "tool": "render_audio", "ref": "com.example.media.render@1", "effectCategory": "read",
"execution": { "kind": "job", "version": 1 },
"inputArtifacts": { "version": 1, "fields": ["source"] } }
Một lệnh gọi nêu tệp ở một trong các trường đó chỉ có thể đến từ nút của chính instance widget đang giữ tệp. Cùng lệnh gọi đó từ Clark, giọng nói, MCP hay CLI bị từ chối với 403 ARTIFACT_INPUT_REFUSED, vì không ai trong số đó giữ quyền của widget. Trước khi gửi lệnh gọi, node của bạn kiểm tra từng tệp được nêu:
- instance widget đã bấm giữ quyền trên tệp, và tệp đã hoàn tất, không còn đang được ghi (
403 ARTIFACT_INPUT_REFUSED); - tệp thuộc về hội thoại nơi lần bấm diễn ra, và hội thoại đó vẫn còn giữ widget (
403 ARTIFACT_INPUT_REFUSED); - node xác định được resource profile đã cấp cho gói; gói không có profile nào bị từ chối thay vì chạy mà không có giới hạn (
403 ARTIFACT_INPUT_REFUSED); - tệp nằm trong giới hạn đầu vào của profile đó (
413 ARTIFACT_INPUT_TOO_LARGE).
Lệnh gọi bị từ chối không gửi gì tới service. Khi chính sách thực thi của bạn hỏi trước một lệnh gọi như vậy, thẻ phê duyệt nêu widget và nút mà lần bấm đến từ đó, và điều bạn phê duyệt bao gồm cả hai. Phê duyệt sẽ chạy lệnh gọi như chính lần bấm đó: node kiểm tra lại rằng hội thoại vẫn giữ widget và nút vẫn gọi capability này, rồi thực hiện mọi bước kiểm tra ở trên. Thẻ có widget đã mất bị từ chối với APPROVAL_STALE.
Các giới hạn đến từ profile đã được cấp, không bao giờ từ manifest. Manifest nêu trường, không nêu kích thước, nên không thể nâng giới hạn nào:
| Profile | Tệp đầu vào | Độ dài media |
|---|---|---|
interactive-light | 8 MiB | 2 phút |
interactive-heavy | 16 MiB | 10 phút |
media-workstation | 25 MiB | 2 giờ |
background-compute | 25 MiB | 1 giờ |
Node của bạn cung cấp việc đọc tệp trong initialize của MCP dưới dạng capabilities.experimental["clarkcant/artifacts"]: { version: 1, methods: ["clarkcant/artifacts.read"], chunkBytes: 262144, maxInputBytes, maxMediaSeconds, maxResultBytes }. Trong lúc lệnh gọi diễn ra, và chỉ khi đó, service gửi clarkcant/artifacts.read với { version: 1, artifactId, offset, length } và nhận lại { artifactId, offset, bytes, eof, sizeBytes, mimeType }, với byte ở dạng base64:
- Đọc có giới hạn. Mỗi lần đọc trả về tối đa 256 KiB; một khoảng dài hơn bị từ chối với
-32602. Node kiểm tra lại quyền ở mỗi lần đọc. - Chỉ các tệp được nêu, chỉ trong lệnh gọi. Một id mà lệnh gọi không nêu trong trường đã khai báo bị từ chối với
-32020, và một lần đọc có câu trả lời đến sau khi lệnh gọi đã kết thúc cũng vậy; byte của nó không được trao đi. - Các lời từ chối khác. Quyền đã bị thu hồi, tệp không còn, hoặc lệnh gọi đã dùng hết ngân sách đọc là
-32021. Ngân sách là bốn lượt qua các tệp của lệnh gọi, với tối đa bốn lần đọc cho mỗi phần 256 KiB, cộng thêm 16 lần đọc cho header và thao tác dò vị trí. - Độ dài media do service tự kiểm soát. Chỉ service đọc được độ dài của một đoạn, nên nó nên từ chối đoạn dài hơn
maxMediaSecondstrước khi làm bất cứ việc gì.maxResultByteslà tệp lớn nhất một kết quả được mang, khoảng 2,95 MiB. Tệp được trả về chỉ trở thành artifact khi job hoàn tất, nên job bị huỷ hay thất bại không để lại tệp nào được trình bày như đã xong. - Không có egress khi đang giữ tệp. Trong lúc một lệnh gọi giữ tệp đang chạy,
clarkcant/egress.fetchcủa service bị từ chối với-32019, trừ khi lệnh gọi đó được quyết định làexternal-write,communication,destructivehoặcfinancial, những tác động mà bước kiểm tra rủi ro của chính sách thực thi hỏi tới. MộtGETcó thể mang byte trong URL của nó, nên lệnh gọireadhaylocal-writeđang giữ tệp không có egress nào cho tới khi nó kết thúc. Capability vừa đọc tệp của bạn vừa gửi nó tới một provider phải khai báoexternal-writetrở lên, để chính sách của bạn quyết định việc gửi đó.
Công cụ dựng media mẫu được xây trên cơ chế này: widget của nó chọn một đoạn WAV, và service của nó đọc đoạn đó theo từng phần dưới profile background-compute.
Kết nối một tài khoản
Một service làm việc trên tài khoản của bạn ở một provider, chẳng hạn danh sách công việc hay lịch của bạn, khai báo một connection trên facet tools của nó. Node của bạn làm mọi việc chạm tới thông tin xác thực của tài khoản: service thấy câu trả lời của provider và widget thấy trạng thái. Không bên nào giữ access token, refresh token hay mã ủy quyền, và trang web hay container của service cũng vậy.
"connection": {
"version": 1,
"provider": "fake.tasks",
"displayName": "Fake Tasks (test fixture)",
"flow": "oauth-pkce",
"authorization": {
"authorizationEndpoint": "http://127.0.0.1:8880/oauth/authorize",
"tokenEndpoint": "http://127.0.0.1:8880/oauth/token",
"revocationEndpoint": "http://127.0.0.1:8880/oauth/revoke",
"clientId": "connected-app-dev"
},
"scopes": [
{ "scope": "tasks.read", "purpose": "Lists your tasks." },
{ "scope": "tasks.write", "purpose": "Renames a task when you ask." }
],
"endpoints": ["http://127.0.0.1:8880"],
"probe": { "url": "http://127.0.0.1:8880/api/me" }
}
Luồng duy nhất là authorization code với PKCE, nên một package không bao giờ mang client secret, và client id là công khai. Mọi URL phải là HTTPS, trừ địa chỉ loopback. Probe phải nằm trên một endpoint đã khai báo. Một package khai báo tối đa một kết nối, và một endpoint không thể đồng thời là một origin egress, nên mỗi origin chỉ có một thông tin xác thực. Mỗi capability nêu các scope nó cần trong requiredScopes, và mỗi scope đó phải là scope mà kết nối xin.
- Kết nối. Bạn bấm Kết nối trên package trong Cài đặt → Tiện ích. Đây là giao diện của chính ứng dụng, không bao giờ là của widget.
POST /packages/{id}/connectiontạo một cặp PKCE (S256) và mộtstatedùng một lần, giữ trong bộ nhớ mười phút, rồi trả về URL ủy quyền của provider để ứng dụng mở trong trình duyệt hệ thống. Chỉ người dùng mới kết nối được: qua MCP hoặc relay, yêu cầu bị từ chối với403 PERSON_ONLY, nên một AI client không thể tự bắt đầu kết nối cho chính nó. Yêu cầu cũng bị từ chối với409 CONNECT_ON_THIS_MACHINEtrừ khi nó tới node của bạn qua loopback, vì provider đưa trình duyệt quay về chính máy đó. - Quay về. Provider chuyển hướng về
http://127.0.0.1:<port>/connections/callback/<package id>. Mỗi package có đường callback riêng, và node của bạn kiểm tra rằng state được cấp cho đúng package mà đường dẫn nêu trước khi gửi mã đi bất cứ đâu, nên một mã gửi về cho package này không bao giờ được đổi cho package khác (tấn công OAuth mix-up). Node đổi mã tại token endpoint đã khai báo, so các scope được cấp với các scope đã khai báo và gọi probe. Chỉ khi đó node mới giữ kết nối, với các token nằm trong bảng riêng. Trang mà trình duyệt tới không bao giờ lặp lại mã hay state và không gửi referrer, còn một callback bị phát lại hoặc giả mạo sẽ bị từ chối. - Ký yêu cầu. Service vẫn chạy không có mạng. Khi nó xin một URL trên một endpoint của kết nối bằng
clarkcant/egress.fetch, node của bạn tự thêmauthorization: Bearer …, chỉ khi một lệnh gọi tới service đang chạy, bỏ mọi headerauthorizationdo service đặt, và xoá token khỏi câu trả lời trả lại cho service. Khi không có token dùng được, yêu cầu bị từ chối với-32012. Mỗi yêu cầu được ghi kiểm toán làegresskèm provider của kết nối, không bao giờ kèm token. - Làm mới. Token sắp hết hạn được làm mới trước, và lần gia hạn giữ nguyên các scope đã được cấp lúc đồng ý, trừ khi provider nói khác. Một phản hồi
401của provider cho chính token đã gửi được làm mới một lần. Sau đó chỉ400hoặc401từ token endpoint mới thu hồi kết nối;408,429hay5xxgiữ nguyên kết nối. Một yêu cầu không bao giờ được thử lại. - Sẵn sàng. Trạng thái là
not-connected,connected,partial,expiredhoặcrevoked, kèm các scope đã xin, đã cấp và còn thiếu, cùng một lý do. Một capability chưa sẵn sàng khi kết nối chưa có, đã hết hạn hoặc đã bị thu hồi, hoặc không cấp một scope trongrequiredScopescủa nó, và lý do nói rõ trường hợp nào, ví dụ “the Fake Tasks (test fixture) account did not grant tasks.write; reconnect it in Settings and allow it”. Widget đọc trạng thái này từactions.availability(); một lần bấm, agent và giọng nói bị từ chối vớiCAPABILITY_NOT_AUTHENTICATEDcùng lý do đó. Capability có scope đã được cấp vẫn hoạt động trên một kết nối cấp một phần. - Thu hồi và kết nối lại. Thu hồi gọi revocation endpoint của provider khi có khai báo, xoá các token, và trạng thái đọc là
revokedtrước khi yêu cầu trả lời, kể cả khi đang có một lần gia hạn dở dang. Kết nối lại chạy lại bước Kết nối. Gỡ package, từ Cài đặt hoặc bằng cách nhờ Clark, cũng quên kết nối theo cách đó, và một lần ủy quyền còn đang chờ callback không thể hoàn tất nữa. - Phiên bản mới. Node của bạn giữ một dấu vân tay về nơi mà khai báo cho phép token của tài khoản đi tới: provider, client id, các endpoint ủy quyền, token và thu hồi, các endpoint và probe. Nếu một bản cập nhật hoặc một lần quay về phiên bản cũ thay đổi bất kỳ mục nào trong số đó, kết nối đọc là
revoked, các token bị xoá mà không được gửi tới các địa chỉ mới, và bạn kết nối lại theo khai báo mới.
Trong Cài đặt. Cài đặt → Tiện ích hiện tài khoản của từng package: trạng thái, các scope đã cấp và lý do nó chưa dùng được đầy đủ. Nút Kết nối hiện khi chưa kết nối, còn lại là Kết nối lại, và Thu hồi hiện khi đã kết nối hoặc kết nối một phần. Trong lúc bạn đăng nhập ở provider, Cài đặt báo đang chờ và đọc lại trạng thái mỗi 1,5 giây, tối đa năm phút.
Một capability, một nhật ký kiểm toán. Kết nối không thay đổi cách một capability chạy. Nút của widget, invoke_capability của Clark và một câu lệnh nói vẫn tới cùng capability, cùng chính sách thực thi của bạn, cùng thẻ phê duyệt của chính ứng dụng và cùng nhật ký kiểm toán. Một lần ghi không nhận được câu trả lời trước hạn chót được ghi là chưa rõ kết quả và không được thử lại.
| Method | Đường dẫn | Trả về | Từ chối |
|---|---|---|---|
| GET | /packages/{id}/connection | 200 { connection }: trạng thái ở trên, không bao giờ có token. | 404 NOT_INSTALLED, 404 NO_CONNECTION, 409 MANIFEST_UNREADABLE, 503 CONNECTIONS_UNAVAILABLE |
| POST | /packages/{id}/connection | 200 { authorizationUrl }. Chỉ người dùng. | Các mã ở trên, cộng thêm 403 PERSON_ONLY, 409 CONNECT_ON_THIS_MACHINE, và 409 ENDPOINT_REFUSED cho địa chỉ provider loopback hoặc mạng riêng trên một node khởi động không có CC_EGRESS_ALLOW_PRIVATE_NETWORK=1 |
| POST | /packages/{id}/connection/revoke | 200 { connection }, đã ở trạng thái revoked. | Như với GET |
| GET | /connections/callback/{packageId} | Công khai. 200 “Connected to …”, hoặc 400 “Not connected” kèm điều gì thất bại và rằng chưa có gì được kết nối. | 503 khi node không kết nối tài khoản |
GET /packages cũng mang trạng thái connection của từng package đã cài. Thu hồi không giới hạn cho người dùng: bất kỳ giao diện nào giữ token của node bạn đều có thể thu hồi, một cách có chủ ý, vì thu hồi chỉ thu hẹp quyền truy cập.
Ứng dụng kết nối tài khoản mẫu chứng minh điều này với fake connector của nó, một fixture kiểm thử và phát triển, không phải provider thật. Chưa có provider thật nào (digitopvn/clarkcant#333). Ứng dụng desktop chỉ mở địa chỉ HTTPS trong trình duyệt hệ thống, nên một provider trên địa chỉ http loopback, như fake connector, được kết nối từ trình duyệt. Chỉ luồng authorization code với PKCE được xây dựng; nhiều tài khoản cho một package, hay một kết nối dùng chung giữa các package, thì chưa. Cài lại một package sau khi gỡ hiện để service của nó ở trạng thái không hoạt động, còn khôi phục thì vẫn được (digitopvn/clarkcant#400).
Token trình duyệt cho widget (tokens@1)
Một số SDK của nhà cung cấp chỉ chạy được với token trong trình duyệt. UI facet của gói có thể khai báo các provider mà widget cần token, tối đa 8 provider, mỗi provider 16 scope:
"browserTokens": {
"version": 1,
"providers": [{ "provider": "example.maps", "scopes": ["tiles:read"], "purpose": "Draws the map tiles." }]
}
Chỉ widget có gói đã khai báo token trình duyệt mới được đề nghị tokens@1 trong init.extensions:
if (api.tokens.available()) {
const token = await api.tokens.request({ provider: "example.maps", scopes: ["tiles:read"], ttlSeconds: 300 });
sdk.setAccessToken(token.value); // { provider, value, scopes, expiresAt }
}
Token gắn với một instance widget và một phiên. Mỗi lần widget được gắn là một phiên riêng, một id ngẫu nhiên do ứng dụng giữ, và token bị thu hồi khi lần gắn đó kết thúc, khi gói bị gỡ, quay về bản cũ hoặc cập nhật sang mã mới, và khi node dừng, ở nơi provider hỗ trợ thu hồi. Token chỉ được cấp khi provider và mọi scope đều đã khai báo, node của bạn có adapter cho provider đó có thể tạo token giới hạn theo đúng các scope ấy, và thời hạn nằm trong 30–3600 giây và trong mức tối đa của chính provider (900 giây khi không xin thời hạn). Request bị từ chối chứ không bao giờ bị thu hẹp. Mỗi phiên giữ tối đa 8 token; token thứ chín thu hồi token cũ nhất.
Node của bạn chỉ giữ id token của provider, và ghi kiểm toán provider, instance và kết quả là browser-token, không bao giờ ghi giá trị. SDK và ứng dụng đều từ chối, với TOKEN_NOT_ALLOWED, một lần cập nhật state, semantic publish, action, ghi tệp hoặc liên kết ngoài có chứa token đúng như khi được cấp. Lớp bảo vệ này chỉ cố gắng hết mức: mã của widget giữ giá trị token, nên điều giới hạn rò rỉ là token có scope hẹp, thời hạn ngắn và bị thu hồi khi widget đóng.
| Method | Path | Body | Ghi chú |
|---|---|---|---|
| POST | /conversations/{id}/widgets/{instanceId}/browser-tokens | { session, request: { provider, scopes, ttlSeconds? } } | { token: { provider, token, scopes, expiresAt } }. Chỉ người dùng: ứng dụng xin thay cho widget mà nó đã gắn. |
| DELETE | /conversations/{id}/widgets/{instanceId}/browser-tokens/{session} | – | { ended: true, revoked }. Request sau đó cho phiên này là 409 TOKEN_SESSION_ENDED. |
Các route này có trong /openapi.json. Lời từ chối: 403 TOKEN_PROVIDER_NOT_DECLARED hoặc TOKEN_SCOPE_NOT_DECLARED, 409 TOKEN_PACKAGE_NOT_ACTIVE hoặc TOKEN_SESSION_ENDED, 422 TOKEN_PROVIDER_UNSCOPED, TOKEN_SCOPE_NOT_SUPPORTED hoặc TOKEN_TTL_TOO_LONG, 502 TOKEN_ISSUE_FAILED và 503 TOKEN_PROVIDER_UNAVAILABLE. ClarkCant chưa kèm adapter provider nào, nên hiện mọi request đều là 503 TOKEN_PROVIDER_UNAVAILABLE.
Những gì một gói chạm tới, hiện trước khi cài
Một mục trong thư mục gói nêu phạm vi của gói trong declaredReach, { origins, secrets, browserTokens, connections }; trang gói không có trường này nghĩa là gói không chạm tới gì. Trước khi cấp bất cứ quyền gì, thẻ thư mục gói trong hội thoại, câu hỏi cài đặt trong hộp thư và chi tiết gói trong Cài đặt → Tiện ích liệt kê từng origin kèm mục đích, từng khoá theo tên kèm mục đích (không bao giờ hiện giá trị), từng provider token trình duyệt kèm scope và mục đích, và từng kết nối tài khoản kèm provider, scope và endpoint.
Node đọc một mục trong thư mục gói có những trường nó không biết bằng cách bỏ qua các trường đó thay vì từ chối cả mục. Thẻ thư mục gói khi đó cho biết có bao nhiêu thông tin mà phiên bản Clark này không đọc được, và nêu tên những trường có tên là định danh thuần; một bản Clark mới hơn sẽ hiển thị chúng.
Một artifact có manifest khai báo phạm vi khác với những gì trang gói đã hiện sẽ bị từ chối với 409 DECLARED_REACH_MISMATCH trước khi có bất cứ điều gì được ghi lại, nên những gì bạn đồng ý đúng là những gì bạn nhận. Resource profile được hiện trong chi tiết gói sau khi gói đã cài.
Những gì một bản cập nhật cho thấy. Thông báo cập nhật gói, và câu hỏi cài đặt mà một bản cập nhật đưa ra khi chế độ thực thi hỏi trước, so sánh trang gói của phiên bản mới với manifest của phiên bản đang cài và mang reachChange: từng origin, khoá, scope token trình duyệt, scope tài khoản và endpoint tài khoản mà phiên bản mới thêm vào hay bỏ đi; từng origin mà một khoá giờ được gửi tới hoặc không còn được gửi tới; một yêu cầu GPU, thứ node này không bao giờ cấp; từng giới hạn tài nguyên bị thay đổi, kèm cả hai giá trị; và cách gói hoạt động khi khuất màn hình nếu cách đó thay đổi. Kết luận là wider khi có thứ được thêm vào, có giới hạn tăng lên hoặc cách hoạt động khi khuất màn hình chuyển từ suspend sang authorized-playback, narrower khi chỉ có thứ bị bỏ đi hoặc hạ xuống, và unchanged trong các trường hợp còn lại. Nó không có mặt khi gói chưa được cài, và là { verdict: "unknown" }, hiện thành lời báo không so sánh được với bản đang cài, khi không đọc được manifest đang cài hoặc trang gói mới. Mỗi danh sách giữ tối đa 32 mục và đếm phần còn lại. Nó chỉ giúp bạn quyết định chứ không quyết định gì: một bản cập nhật vẫn do chính sách thực thi quyết định như mọi lần cài.
Các node đã ghép cặp
Hai node ClarkCant có thể được ghép cặp với nhau, ví dụ laptop của bạn và một máy chủ. Một node đã ghép cặp có thể báo cho node kia biết chuyện gì đã xảy ra, đặt một thông báo vào hộp thư của chủ node kia, và giao cho nó một việc, mỗi thứ chỉ trong phạm vi mà chủ của node nhận cho phép.
Các route này dùng cùng token nhưng chưa có trong /openapi.json và có thể thay đổi. Xác nhận một peer và viết một grant là quyết định của chủ node: relay WebSocket, MCP và clarkcant api từ chối chúng với 403 PERSON_ONLY. Hiện nay các node nói chuyện với gateway của nhau bằng HTTP thuần, nên một node phải truy cập được qua HTTP mới ghép cặp được, và con người kiểm khoá của node kia bằng cách so fingerprint.
Ghép cặp hai node
Một node mời, node kia nhận lời mời, rồi một người xác nhận trên cả hai node. Khi cả hai chưa cùng xác nhận, peer vẫn đang chờ: không có gì được gửi tới nó và tin nhắn của nó bị từ chối.
| Method | Path | Body | Ghi chú |
|---|---|---|---|
| POST | /peers/invites | { endpoint } | Trên node mời, kèm địa chỉ mà node kia sẽ dùng để gọi tới. 201 { invite } gồm inviteId, issuerNodeId, endpoint, fingerprint và expiresAt. Chỉ dùng một lần, có hiệu lực 10 phút. |
| POST | /peers/claim | { inviteId, node } | Node nhận lời mời gọi route này trên node mời, không cần token của node đó. node là { nodeId, label, endpoint, publicKey, fingerprint, tokenHash } của node nhận lời mời. 200 { issuer, tokenHash }, và node nhận lời mời được ghi lại ở trạng thái chờ. 404 INVITE_UNKNOWN; 409 INVITE_EXPIRED hoặc INVITE_ALREADY_CLAIMED; 400 FINGERPRINT_MISMATCH khi fingerprint không khớp với khoá được đưa ra. |
| POST | /peers/record | { node } | Trên node nhận lời mời: issuer trong câu trả lời của lượt nhận lời mời, kèm endpoint bạn đã dùng để gọi tới nó và tokenHash trong câu trả lời. 201 { nodeId, trustedAt }, vẫn đang chờ (trustedAt là null). |
| GET | /peers | – | nodeId, endpoint, fingerprint, pairedAt, trustedAt và revokedAt của từng peer. |
| POST | /peers/{nodeId}/confirm | – | Quyết định của một người, sau khi so fingerprint: lời mời mang fingerprint của node mời, còn GET /node cho thấy fingerprint của chính từng máy. 200 { nodeId, trusted: true }; 404 PEER_UNKNOWN. |
| POST | /peers/{nodeId}/revoke | – | Kết thúc việc ghép cặp trên node này: node không nhận thêm tin nhắn nào từ peer đó và không gửi gì cho nó nữa. 200 { nodeId, revoked: true }; 404 PEER_UNKNOWN. |
Không có token nào đi qua mạng khi ghép cặp. Mỗi node đưa cho peer một token dẫn xuất từ localToken của chính nó (HMAC-SHA256 dạng base64url của peer:<peerNodeId>, với khoá là localToken), còn tokenHash là SHA-256 dạng hex của token đó. Chỉ có hash được lưu lại.
curl -s -X POST "$CLARKCANT_URL/peers/invites" \
-H "Authorization: Bearer $CLARKCANT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "endpoint": "http://laptop.local:8765" }'
# 201 → { "invite": { "inviteId": "…", "fingerprint": "…", "expiresAt": "…", … } }
# Sau khi node kia đã nhận lời mời và bạn đã so fingerprint:
curl -s -X POST "$CLARKCANT_URL/peers/<nodeId>/confirm" \
-H "Authorization: Bearer $CLARKCANT_TOKEN"
Viết một grant
POST /grants nhận một grant: { grantId, ownerPrincipalId, senderNodeId, receiverNodeId, capabilityRefs, resources, allowedDataClasses, expiresAt, budget?, maxDelegationDepth, allowedEffectCategories? }. Grant ghi node này là bên gửi, chủ của node này (ownerPrincipalId trong identity.json) là chủ, và một peer đã xác nhận là bên nhận. Grant được lưu và xếp hàng gửi tới peer trong cùng một lần ghi, và từ đó peer nhận những việc node này giao cho nó theo grant đó, cho tới khi grant hết hạn hoặc bị thu hồi. Peer vẫn chỉ chạy những gì chủ của nó cho phép: nó lấy giao của grant với mức cho phép của chính nó, nên một grant có thể thu hẹp những gì chạy ở đó nhưng không bao giờ mở rộng.
201 { grantId, receiverNodeId, allowedDataClasses }.400 INVALID_SCHEMA, hoặc400 GRANT_NOT_OURSkhi bên gửi không phải node này;403 NOT_THE_OWNER;404 PEER_UNKNOWNkhi bên nhận chưa được ghép cặp và xác nhận.
Hiếm khi bạn phải tự viết grant. Khi bạn thiết lập trong cuộc trò chuyện một tự động hoá chạy trên node đã ghép cặp, grant được viết cho đúng các thư mục, repository và tác động của nó, và việc tạm dừng hay xoá tự động hoá đó thu hồi grant trên cả hai node.
Trước khi giao việc, bạn có thể xem node kia sẽ chạy được gì cho bạn. Hãy nhờ Clark liệt kê các node đã ghép cặp (list_peers): với mỗi node, Clark cho biết chủ của node đó có cho phép node của bạn điều gì không, và trong những năng lực được cho phép, năng lực nào node đó chạy được ngay lúc này. Node của bạn chỉ biết đến vậy: các thư mục và mọi thứ khác mà quyền cho phép bao gồm vẫn ở lại node kia. Khi bạn thiết lập một tự động hoá chạy ở đó, Clark cảnh báo nhưng không từ chối nếu node đó chưa cho phép node của bạn, không cho phép điều tác vụ cần, hoặc chưa chạy được nó. Một node không hỏi được, vì đang chạy ClarkCant cũ hơn hoặc không trả lời trong 5 giây, được báo là chưa rõ. Lần kiểm tra khi tác vụ tới nơi vẫn là bên quyết định.
Một tác vụ tới nơi trước khi node đó chạy được điều nó cần, chẳng hạn khi một gói còn đang nạp, sẽ chờ ở đó thay vì bị từ chối, và cả hai chủ node đều được báo nó đang chờ năng lực nào. Việc chờ không có giới hạn thời gian: tác vụ tự bắt đầu khi năng lực đó sẵn sàng, và dừng nó ở nơi bạn thiết lập sẽ kết thúc nó ngay. Với cặp ghép được tạo trước tính năng này, cho tới khi mỗi node đã gửi được gì đó cho node kia, tác vụ như vậy vẫn bị từ chối ngay, và lời cảnh báo nói rõ điều đó.
Một tác vụ giao cho node đã ghép cặp có thể mang về các tệp mà lần chạy đã ghi, nhưng chỉ khi cả hai chủ node cho phép. Về phía bạn, hãy đặt cho tự động hoá một hạn mức byte bằng maxArtifactBytes trong create_automation, tối đa 16 MiB. Về phía bên kia, chủ của node đó cho node của bạn một hạn mức bằng maxArtifactBytes trong allow_peer_tasks. Thiếu một trong hai thì tệp ở lại node kia, và kết quả sẽ nói rõ điều đó. Tệp được mang về trong phạm vi hạn mức nhỏ hơn, tối đa 8 tệp và 4 MiB mỗi tệp. Chỉ các tệp được ghi bằng công cụ ghi tệp dự án bên trong các thư mục của tác vụ mới được gửi, không bao giờ có HTML, SVG hay script. Node của bạn kiểm tra từng tệp theo chính grant của bạn cho tác vụ đó, tải về một lần và kiểm tra digest trước khi giữ lại. Tệp được mang về hiển thị cùng kết quả trong cùng cuộc trò chuyện, và tệp nào bị bỏ ra hoặc bị từ chối đều được liệt kê kèm lý do.
Báo cho node đã ghép cặp biết chuyện gì đã xảy ra
POST /peers/{nodeId}/signals { id, topic, payload?, subject?, occurredAt? }, gửi tới chính node của bạn bằng token của nó. id là tên bạn đặt cho sự kiện. Signal được xếp hàng trước khi gửi, nên không bị mất khi peer đang ngoại tuyến, và peer chỉ ghi nhận nó một lần dù được gửi lại bao nhiêu lần.
202 { messageId, queued: true }.404 PEER_UNKNOWNkhi peer chưa được ghép cặp và xác nhận;400cho các trường hợp khác.
Peer ghi nhận nó thành peer.<topic> từ node mà kênh đã xác thực chỉ ra, nên nó không thể giả làm một lần gửi từ GitHub, một webhook có chữ ký hay một bộ hẹn giờ. Signal là một sự việc, không phải mệnh lệnh: nó không cần grant và không đòi hỏi gì, và chỉ những yêu cầu thường trực mà chủ của peer đã thiết lập ở đó mới quyết định nó khởi động việc gì, nếu có.
curl -s -X POST "$CLARKCANT_URL/peers/<nodeId>/signals" \
-H "Authorization: Bearer $CLARKCANT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "id": "build-812", "topic": "build.finished", "payload": { "status": "failed" } }'
# 202 → { "messageId": "…", "queued": true }
Đặt thông báo vào hộp thư của chủ node kia
POST /peers/{nodeId}/notices { id, title, body?, category?, severity? }, gửi tới chính node của bạn bằng token của nó. id là tên bạn đặt cho sự kiện. category là result, update, message (mặc định) hoặc alert; severity là info (mặc định), success, warning hoặc error. Hiện chưa có gì trên node tự gửi thông báo, nên route này là cách một script, CLI, một client MCP hay một tự động hoá báo cho một Clark đã ghép cặp.
202 { messageId, queued: true }: đã xếp hàng, peer chưa nhận.404 PEER_UNKNOWNkhi peer chưa được ghép cặp và xác nhận.409 NOTICES_UNSUPPORTEDkhi peer chưa cho biết nó nhận thông báo. Node biết điều đó từ câu trả lời của peer cho bất cứ thứ gì nó gửi tới, nên hãy gửi cho peer đó một thứ trước, ví dụ một signal, rồi thử lại. Nếu peer chạy ClarkCant từ trước khi có thông báo, hãy cập nhật nó ở máy đó.400cho mọi trường hợp khác.
Thông báo chỉ là chữ, không hơn: id tối đa 160 ký tự, tiêu đề tối đa 120 và nội dung tối đa 500, không có hành động, liên kết hay chủ thể. Node hiển thị nó quyết định có thể làm gì với nó, và xem nội dung của nó là dữ liệu. Nó được ghi nhận là của node bạn, mỗi id một lần, nên gửi lại hay phát lại cũng chỉ hiện một lần. Mỗi peer giữ tối đa 20 thông báo đang chờ ở đó, cái cũ nhất bị bỏ trước, mà không đẩy ra thông báo của chính node đó hay của peer khác.
Node nhận chỉ nhận thông báo theo quyết định của chính chủ nó là làm việc với node của bạn: một grant còn hiệu lực mà chủ đó đã viết cho node của bạn, hoặc việc họ cho phép node của bạn chạy việc ở đó. Grant do node của bạn viết không tính, và chỉ ghép cặp thôi cũng không đủ. Nó cũng chỉ nhận tối đa 30 thông báo mỗi phút từ một peer. Lời từ chối là quyết định cuối cùng, không phải lần gửi thất bại: node của bạn không gửi lại thông báo đó, và báo cho bạn trong hộp thư của chính bạn, mỗi peer và mỗi lý do một lần, thông báo nào không tới, vì sao, và điều gì sẽ thay đổi được việc đó.
curl -s -X POST "$CLARKCANT_URL/peers/<nodeId>/notices" \
-H "Authorization: Bearer $CLARKCANT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "id": "backup-2026-09-30", "title": "Sao lưu đêm qua thất bại", "severity": "error" }'
# 202 → { "messageId": "…", "queued": true }
Trong hộp thư
- Thông báo có thể đến từ một node đã ghép cặp. Chúng được đánh dấu là của node đó, và không mang hành động nào từ nó.
- Khi việc gửi tới một node đã ghép cặp thất bại liên tục hơn 10 phút, bạn nhận một thông báo cho lần mất kết nối đó, không phải mỗi tin nhắn một thông báo; nếu vấn đề thay đổi, một thông báo mới thay cho thông báo cũ. Thông báo nói rõ vấn đề: thiết bị không trả lời từ một thời điểm nào đó, thiết bị trả lời nhưng từ chối, ClarkCant ở đó không xử lý được tin nhắn, hoặc việc gửi đã dừng. Nó cho biết những gì còn nợ có còn nằm trong hàng đợi và vẫn được thử gửi lại hay không. Thông báo tự biến mất ngay khi node đó xác nhận đã nhận bất cứ thứ gì, hoặc khi bạn thu hồi việc ghép cặp.
- Khi một node bỏ một tin nhắn gửi tới node đã ghép cặp, những tin gửi sau đó vẫn được chuyển tới, đúng thứ tự. Chủ của cả hai node nhận một thông báo. Thông báo nói rõ việc gì thất bại, các tin sau vẫn được giữ, điều gì xảy ra với những việc liên quan và nên làm gì, rồi mới liệt kê những gì đã mất và thuộc việc nào. Một lần giao việc hoặc lệnh dừng bị mất sẽ chốt việc đó, là thất bại nếu nó chưa từng rời node của bạn. Một kết quả bị mất khiến việc đã giao hiện là chưa rõ trong hội thoại của nó, vì không ai xác nhận được việc đó kết thúc ra sao ở bên kia. Câu hỏi, câu trả lời hay việc duyệt bị mất sẽ không được gửi lại, và bên đang chờ sẽ chờ tới khi nó hết hạn.
- Nếu node đã ghép cặp chạy ClarkCant bản trước thay đổi này, nó không vượt qua được tin nhắn đã mất. Bạn nhận một thông báo rằng việc ghép cặp đang bị kẹt và cập nhật ClarkCant trên thiết bị đó sẽ gỡ kẹt. Khi đã cập nhật, thiết bị đó tự bắt kịp và thông báo tự biến mất.
Sinh client
Phần ổn định được mô tả bằng OpenAPI 3.1, nên công cụ OpenAPI 3.1 nào cũng sinh được client có kiểu từ đó:
curl -s "$CLARKCANT_URL/openapi.json" -o clarkcant-openapi.json
File đính kèm và ảnh là dữ liệu nhị phân: gửi chúng qua HTTP. Các route này cũng gọi được qua WebSocket, MCP và CLI.