Column visibility:兩種機制,一個 hidden set
欄位可見性有兩個互補機制:table-level column_acl 是全域 hide-map;每筆 grant 的 visible_columns 是 allowlist。伺服器把兩者折疊成一個 effective hidden_columns 集合,所有讀寫入口都以同一集合執行。
機制一:table-level column_acl
curl -X PATCH \
"$BASE_URL/private/module/custom_tables/department/11111111-1111-4111-8111-111111111111/tables/22222222-2222-4222-8222-222222222222/column-acl" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"column_acl": {
"col_dddddddd_dddd_4ddd_8ddd_dddddddddddd": { "read": "managers" },
"col_eeeeeeee_eeee_4eee_8eee_eeeeeeeeeeee": { "read": "all" }
}
}'儲存的 map key 是 live schema 的 internal column key。輸入可使用目前 column_mapping 的 display-name key,伺服器會正規化成 internal key;未知 key 或同一欄位別名的衝突 policy 會被拒絕。read 只接受 all 或 managers。對非 manager,標成 managers 的欄位加入 hidden set;透過 REST 時 manager 的 table-level hidden set 永遠為空(在部門表的 agent session 中,房間天花板會把 manager 當成非 manager,所以那裡同樣會隱藏 managers-only 欄位)。送出空 map 可清除 table-level hide 設定。
這個設定適合「不論從哪種 grant 進來,成本欄都只有管理員可見」的硬底線。端點見 columnAcl.update。
自 2026-07-28 版起,這個寫入在資料表列鎖下合併。兩個併發的 settings PATCH——例如欄位 ACL 儲存撞上 default-permissions 儲存,或 IaC apply 撞上 UI 編輯——以前兩邊都回 200,晚 commit 的無聲蓋掉早的那個 key;對這個設定來說,代表收緊過的欄位 ACL 可能退回全員可見。現在競爭時 PATCH 會等鎖,並可能回併發合約裡可重試的 409;拿到 200 就表示合併真的成立。
機制二:grant visible_columns
{
"user_id": "33333333-3333-4333-8333-333333333333",
"can_read": "all",
"can_insert": true,
"can_edit": "own",
"visible_columns": [
"col_aaaaaaaa_aaaa_4aaa_8aaa_aaaaaaaaaaaa",
"col_bbbbbbbb_bbbb_4bbb_8bbb_bbbbbbbbbbbb",
"col_dddddddd_dddd_4ddd_8ddd_dddddddddddd"
]
}visible_columns 是 allowlist:所有未列出的 live columns 都加入 hidden set。它可放在 user、department、chatroom 與 client grants;省略或 null 代表 grant 本身不縮小欄位,仍會套 table-level column_acl。空 list 會被拒絕,不用它表達「看不到任何欄位」。Table defaults 沒有 visible_columns。補集是對每一個現行鍵值計算的,包括內建的 id 欄位:沒有列出 id 的白名單也會隱藏 data.id(資料列本身最外層的 id 不受影響),用戶端需要那個欄位時請把它列進去。
Note
visible_columns只接受 internal keys。這讓欄位 rename 後 grant 仍穩定;建立 grant UI 時,請從 schema 顯示名稱映射到 internal key。
合併公式
hidden_columns
= column_acl 中 read=managers 的 keys(非 manager)
∪(所有 live schema keys − grant.visible_columns)假設 live columns 是內建的 id,加上 name、status、cost、internal_note:
| 來源 | 結果 |
|---|---|
column_acl 把 cost 設為 managers-only | hide cost |
Grant allowlist 是 name, status, cost | hide id, internal_note |
| Effective union | hide id, cost, internal_note |
Allowlist 不能重新打開 table-level hide;即使列出 cost,非 manager 仍看不到。Manager 不需要也不能有 explicit grant,因此透過 REST 時不會被 grant allowlist 縮小;在部門表的 agent session 中,session 房間的授權列與其 visible_columns 同樣會限制 manager。
現行 ACL 同樣約束 history
已合併 #1987,來源 b1e5a13d0e209b41cca460fbe6ba0dc7c7b301a8,將目前的 canonical denial 與 column ceiling 套用到 record/table history、stored version 與 version diff。受限 principal——包括 grant 欄位 allowlist 或 Agent/session ceiling——不會取得目前 live schema 之外的已移除欄位 key;即使 allowlist 包含全部剩餘 live 欄位,也不例外。未受欄位限制的 history 讀者仍可能取得已移除的 stored field;不得全面抹除這些欄位,也不能推論所有歷史欄位都可讀。
Hidden 別名、link cell 的 expansion/truncation marker,以及從 hidden 或已不再授權的 source column 衍生的 computed result/provenance 仍會被過濾。Version diff 檢查兩邊 snapshot 的來源證據,包括 predecessor evidence;缺少證據時,依賴它的 computed value 會 fail closed。這是讀取投影,不會改寫儲存的 audit row。請見歷程與as-of 讀取;as-of 仍只回傳目前可見欄位。此來源說明不是部署或座標出勤驗收聲明。
Hidden 代表 absent,不是 null
Record response 會完全移除 hidden field,而不是回傳 null。Filter、sort、search、aggregate 或 write 若命中 hidden key,伺服器會把它當成欄位不存在來拒絕:400 Column '<name>' does not exist in table schema 是寫入時對不存在欄位使用的字句。自後端 5.21.0 起,舊式的鍵值遵守同一條規則:GET .../records 與 GET .../view 的 sort_by,以及 POST .../records/search 與 POST .../records/export 的 filters、sort_by 與 sort,只要命中 hidden key,得到的回應和不存在的 key 完全相同,都是 400 Filter column '<name>' does not exist in table schema. Valid columns: […](排序則是 Sort column …),而且那份 Valid columns 清單只列出你有權讀取的欄位,再加上 created_at、id 與 updated_at。存在性檢查先於欄位型別檢查,所以 hidden 的 link、rollup、lookup 或 formula 欄位不會被當成 Filtering on link/rollup/lookup/formula column … is not supported 拒絕。computed_filters(包含建立在 hidden 輸入之上、本身可見的 formula)、stored_filters 與 any_of,對 hidden 與不存在的欄位一律回 Column '<name>' does not exist in table schema。aggregate 的 group_by 是例外:真正不存在的 key 仍回它自己的 group_by column '<name>' not found in table schema。自後端 5.22.0 起,MCP 的 records_aggregate 工具對 group_by 中的顯示名稱(包含建立在 hidden 欄位之上的 formula)也遵守這條規則:hidden 名稱回 not_found,與不存在的名稱完全相同;以前回的是 invalid_argument: Column 'col_…' does not exist in table schema,等於把 hidden 欄位的內部 key 放進了回應。呼叫者若讀不到某個 saved view 提到的欄位,套用該 view 時,舊式 filters 或 sort_by key 也會被當成不存在的 key 處理,見 saved view。自後端 5.22.0 起,上述規則在 hidden 欄位的顯示名稱是 created_at、updated_at、created_by 或 created_by_client 時同樣適用:在 saved view(REST 套用與 MCP 的 views_apply)以及 MCP records_query 的舊式 filters 中,這四個名稱的解析與沒有該欄位的資料表相同(對其中任一個的 filter,或 created_at、updated_at 的 sort_by,指向紀錄本身的欄位;created_by 或 created_by_client 的 sort_by 則退回依建立順序排序)。以前這些名稱會被換成沒有任何紀錄擁有的 key,所以以 created_at 排序的 view 會照紀錄 id 順序回傳,而 created_at、updated_at 或 created_by 的 filter 什麼都比對不到。嚴格的讀取(GET .../records、POST .../records/search 與 .../export)和多欄位 sort key 不在此列:它們仍會把這種名稱解析成 hidden 欄位,並以 400 拒絕。agent 工具 custom_tables_query_records 自 5.22.0 起,對 computed_filters 中的 hidden 名稱也給出與不存在名稱相同的回答(見 query_records 的 computed_filters)。Exports、views、history 與依賴欄位的讀取也必須沿用伺服器結果,前端不應自行補回 schema 欄位。
被隱藏的 link 欄位會連同附著在它身上的一切一起消失。對該欄位被隱藏的讀取者——不論是被資料表的欄位 ACL,或是被 grant 的 visible_columns 隱藏——回應中不會有它的儲存格、"<column>::truncated" 旗標(agent 用來標示目標超過 20 個的記號),開啟 expand_links 時也不會有它的 expanded 項目;REST、MCP 以及 agent 與 Analyst 的讀取行為一致。反向連結的讀取(REST 的 incoming-links 與 hydrated 路由、MCP 的 records_incoming_links 與 records_hydrated 工具)也會略過該欄位在來源資料表上會形成的群組。該欄位對其未隱藏的讀取者——在 REST 與 MCP 上包括每一位 manager——看到的內容不變。
被隱藏的是「值」,不是「名稱」。 只有 manager 能讀的欄位,其名稱仍會出現在 schema_definition.columns 中,被扣住的只有值。Hidden set 是套用在準備好的 record 字典上,computed 欄位則只是被標註 "restricted": true;兩條路徑都不會裁剪 schema 清單。切勿從欄位是否出現在 schema 推論可讀性:這麼做的前端會渲染出一個永遠沒有值的欄位,或送出一個必然 4xx 的 filter。
因此,以下程式會把「不存在」視為正確狀態:
const hasCost = Object.prototype.hasOwnProperty.call(record.data, '成本')
if (hasCost) {
renderMoney(record.data['成本'])
}不要用 record.data['成本'] ?? 0,那會把「無權限」錯誤呈現成真實的零值。
想知道自己看得到哪些欄位,請讀 permissions.me 的 visible_columns(null = 全部、[] = 沒有),不要從 schema 推論。兩個機制合併後如何參與 grant resolution,見 effective permissions。