可攜身分 token
原始的 user、department、client 或 chatroom ID 都綁定特定環境。在身分 token 出現之前,只要文件指名了某個 principal,它就被釘死在匯出它的那套安裝上:ref 可攜,principal 不可攜。$user: / $dept: / $smc: / $room: 補上了這個缺口。凡是過去必須填環境專屬 ID 的位置,現在都能改填 token;它們在 plan 與 apply 時以公司為錨點解析並 fail-closed;export 也會輸出 token——因此一份 export 可以進 Git 審查,再套用到另一個 scope,不必動手改 ID。
四種 token 家族
| Token | 指名對象 | 自然鍵 |
|---|---|---|
$user:<username> | 使用者 | Username,在公司內唯一 |
$dept:<department name> | 部門 | 部門名稱,在公司內唯一 |
$smc:<platform>:<platform_user_id> | 社群媒體用戶 | 公司底下各 chatroom 內的 (platform, platform user id) |
$room:<chatroom name> | 聊天室 | 聊天室名稱——不唯一 |
$room:<department name>/<chatroom name> | 聊天室 | 以部門限定的聊天室名稱 |
Token 主體是 1 到 150 個字元的自由文字,可以包含空白與冒號,因為真實的部門與聊天室名稱本來就會有。唯一被排除的是 JSONL 本身就禁止的換行。因此 $dept:Front Desk 與 $smc:line:U8f2c1b 都是合法寫法。
Chatroom.name 完全沒有唯一性限制,所以在多數租戶裡,裸寫 $room:<name> 都是真的有歧義。請把 $room:<department name>/<chatroom name> 當成預設寫法,裸名只是捷徑,而不是反過來。
各介面接受哪些家族
| 介面 | 可用 token |
|---|---|
grant 的 principal.id | 與 principal.type 相符的那一種:user → $user:、department → $dept:、client → $smc:、chatroom → $room: |
insight_selection 的 chatroom | 只有 $room:——填 $user:/$dept:/$smc: 會在 parse 階段被拒 |
record 中 principal 欄位的 data cell | user 欄位填 $user:,social_client 欄位填 $smc:,principal 欄位可填 $user:/$smc:/$room:——principal cell 會存成帶標籤形式(user:<id>/smc:<id>/room:<id>)。任何 cell 裡的 $dept: 都是逐行 kind 錯誤 |
trigger 中 principal 欄位的 when predicate 值 | user/social_client 欄位填 $user:、$smc:(解析成裸 ID);principal 欄位填 $user:、$smc:、$room:(解析成帶標籤 cell) |
型別為 identity:user / identity:social_media_client 的 invoke_command 輸入 | $user:、$smc: |
table 的 spec.moderators | 只有 $user:;$dept:/$smc:/$room: 會在 parse 階段被拒 |
目標表自然鍵為 user、social_client 或 principal 欄位的 link cell | 與該自然鍵欄位相符的那一種:user 鍵用 $user:、social_client 鍵用 $smc:、principal 鍵可用 $user:/$smc:/$room:。查詢用的值就是目標列實際儲存的形式:舊型別自然鍵是裸 ID,principal 自然鍵是帶標籤 cell |
Moderator 使用下列只允許 user 的窄版 pattern。原始 user ID 仍可供同環境往返使用;可攜形式則是 $user:<username>:
^([a-z0-9][a-z0-9_.-]{0,35}|\$user:[^\r\n]{1,150})$Moderator 清單最多 50 項,而且是 full-set PUT 語意。Token 會在 plan 依文件所屬公司解析、在 apply 再解析一次;last-applied state 只儲存 canonical user ID,authored token 永不持久化。
放寬後的 principal.id 契約
IacGrantPrincipal.id 現在是 1 到 160 個字元,且符合:
^([a-z0-9][a-z0-9_.-]{0,35}|\$(user|dept|smc|room):[^\r\n]{1,150})$這是兩個並列選項,不是把單一 pattern 放寬:
- 原始 ID 仍遵守 token 出現前的契約——最多 36 個字元、不含冒號與空白,這樣衍生出的
{table}.grant:{type}:{id}ref 才會合法。原始 ID 綁定環境。 - 可攜身分 token 以公司內唯一的自然鍵指名 principal。它的主體是 1 到 150 個字元的自由文字,所以含空白與冒號的部門、聊天室名稱都寫得出來。
前綴必須與 type 相符;前綴交錯是 per-line 錯誤,而不是去解析成另一種 principal。
insight_selection.chatroom 使用同一組 pattern 中僅限 $room: 的窄版:
^([a-z0-9][a-z0-9_.-]{0,35}|\$room:[^\r\n]{1,150})$解析以公司為錨點並 fail-closed
每個 token 都在 diff 執行之前,對文件 scope 所屬的公司解析;apply 也會依 live state 重驗帶 token 的 mutation。唯一的例外是 principal 欄位在同一份文件較早處宣告的 record 行:plan 階段無法翻譯它,因此整行會帶著 warning 延後,token 改由 executor 在 apply 時解析——見 record line kind。沒有跨租戶 fallback,也沒有「最接近的比對」。找不到、有歧義、已軟刪除、跨租戶——四種情況都讓該行失敗,都不會靜默略過該行,也不會挑第一個候選。
解析發生在衍生 ref 建立之前,所以 grant:{type}:{id} 與 insight.{uuid} 一定帶著解析後的原始 ID。這就是為什麼一份文件可以從原始 ID 改寫成 token 而收斂為 noop,而不是新增一筆重複 grant:兩種寫法會落在同一列 state。
當 token 無法解析時,plan action 仍然需要一個 ref,而 IacPlanAction.ref 有 pattern 限制且不接受 $。伺服器會代入 <prefix>unresolved-<8 個十六進位字元> 形式的佔位符,例如 insight.unresolved-1a2b3c4d。請把它當成錯誤標籤,絕不是資源身分——出錯的 token 就寫在 error detail 裡。
語法錯誤
括號裡的提示標示的是該通道自己的文法。Cell 通道——record cell、trigger when 值、invoke_command 輸入、link 自然鍵,以及 table.spec.moderators——提供三種前綴;grant 通道——grant.principal.id 與 insight_selection.chatroom——提供四種,因為 $dept: 只用於 grant。
malformed principal token '$user:': empty username (expected '$user:<username>', '$smc:<platform>:<platform_user_id>', '$room:<chatroom name>' or '$room:<department name>/<chatroom name>')
malformed principal token '$smc:line': needs platform AND platform_user_id (expected '$user:<username>', '$smc:<platform>:<platform_user_id>', '$room:<chatroom name>' or '$room:<department name>/<chatroom name>')
malformed principal token '$room:': empty chatroom name (expected '$user:<username>', '$smc:<platform>:<platform_user_id>', '$room:<chatroom name>' or '$room:<department name>/<chatroom name>')
unrecognized principal token '$team:sales' (expected '$user:<username>', '$smc:<platform>:<platform_user_id>', '$room:<chatroom name>' or '$room:<department name>/<chatroom name>')
malformed principal token '$dept:': empty department name (expected '$user:<username>', '$dept:<department name>', '$smc:<platform>:<platform_user_id>', '$room:<chatroom name>' or '$room:<department name>/<chatroom name>')
principal token '$smc:line:U123' does not match the column's type: a user column expects $user:<...> or a raw id
principal token '$dept:Sales' does not match the column's type: a principal column expects $user:<...> or $smc:<...> or $room:<...> or a raw id最後兩則是 kind 閘門。它在任何查詢之前就執行,所以 token 帶的名稱不必存在;它列出的期待值就是該欄位或該 grant 自己的前綴集合,以 or 串接——user 與 social_client 各一種、principal 三種、每種 grant 型別一種。Cell 裡的 $dept: 一律是這則錯誤:只有直接呼叫 cell 解析器才會回報成 unrecognized principal token '$dept:Sales' (…)。
解析錯誤
principal token '$user:bob': no user with username 'bob' in this company
principal token '$user:bob': user 'bob' is deleted
principal token '$dept:Sales': no department named 'Sales' in this company
principal token '$dept:Sales': department 'Sales' is deleted
principal token '$room:Support': no live chatroom named 'Support' in this company
principal token '$room:Support': ambiguous — 2 live chatrooms are named 'Support' (['<id-a>', '<id-b>']); qualify it as '$room:<department name>/Support'
principal token '$room:Support Desk/Tier 1': no live chatroom named 'Tier 1' in department 'Support Desk' in this company
principal token '$room:Support Desk/Tier 1': ambiguous — 2 live chatrooms match (['<id-a>', '<id-b>'])
principal token '$smc:line:U123': no social client with platform user id 'U123' on platform 'line' in this company
principal token '$smc:line:U123': ambiguous — 2 social clients match, in chatrooms ['<id-a>', '<id-b>']
cannot resolve this scope's company for principal token resolution其中兩點值得強調。軟刪除的使用者或部門會得到明確錯誤,而不是「找不到」——離職會讓新的 apply 大聲失敗,而不是靜默改綁到別人。另外,$smc: 的歧義是正常的租戶樣貌,不是防禦性分支:social client 的唯一性是以 chatroom 為單位,所以同一個 (platform, platform user id) 出現在同公司的兩個 chatroom 時,永遠無法被無歧義地 token 化。軟刪除的聊天室則是第一條規則的例外:兩個聊天室分支都在 SQL 裡過濾存活狀態,所以被墓碑化的房間回報的是上面那則「找不到」,永遠不會是 is deleted。
Trigger predicate 內的 token 會加上位置前綴:
trigger when column 'assignee': principal token '$user:bob': user 'bob' is deletedToken 與 plan_hash
解析後的 token 綁定會以 (principal, type, authored, resolved) 指紋折入 plan_hash——但只有 grant principal 與 insight_selection.chatroom 會綁定。record cell、trigger when 值與 link 自然鍵裡的 token 走的是同一組 helper,卻永遠不會折入 hash,所以改掉一位使用者或一個聊天室的名稱,並不會重新蓋章一份 token 只出現在 cell 裡的 plan。就會綁定的那兩者而言,由此得到兩個都很關鍵的結果:
- 有釘住
plan_hash時,審查與 apply 之間的改名會重新蓋章,該次 apply 會被409 plan_stale拒絕。這是名稱易主之後唯一的保護。 - 沒有
plan_hash時,apply 會依設計綁到當下持有該名稱的人。省略這個 query parameter,現在放棄的東西比以前更多。
Export 會輸出 token,但不是一律如此
Export 會把原始 principal ID 反向對應成 token,依種類批次查詢並限定租戶。principal cell 會先被解析,再用它自己的標籤去對應的表查——user: 查使用者表、smc: 查 social client 表、room: 查聊天室表;user/social_client cell、grant principal 與 insight_selection.chatroom 則直接用裸 ID 查。當 token 會出錯或不可用時,export 刻意退回已儲存的值:principal 欄位退回原始帶標籤 cell(user:<id>、smc:<id>、room:<id>),其他位置退回原始 UUID。
- 裸名重複的 chatroom 會退回限定形式
$room:<department name>/<chatroom name>。若兩種形式都模稜兩可,就完全不輸出 token。 - 名稱本身含有 token 分隔字元
/的 chatroom 維持原樣;裸名重複、且其部門名稱也含有/的 chatroom 同樣維持原樣,因為限定形式會被切錯。$dept:grant principal 不受影響:部門名稱是整串比對,所以$dept:Sales/EMEA會被輸出,也解析得回來。 - 被軟刪除或懸空的 chatroom 不在聊天室對應表裡,因此保持原樣——這正是讓該匯出重新 plan 得到 0 差異、而不會靜默改綁到另一個存活房間的原因。
(platform, platform user id)在公司內對到多列的 social client 永遠不會被 token 化,因為那個 token 套用不了。- 懸空、跨租戶或 username 空白的 ID 以原樣匯出。
- 軟刪除的使用者仍會被 token 化,好讓重新 apply 在解析器大聲失敗,而不是把一個死掉的原始 ID 帶下去。
因此,export 的可攜程度取決於該租戶名稱有多唯一。Export 裡出現原始 ID 或原始帶標籤 cell 是正確行為,不是 bug——但那也正是套用到別處之前必須人工修正的那幾行。同一個未變動系統的兩次匯出是位元相同的;帶著這些退路的匯出,在原本的 scope 重新 plan 依然是 0 差異。
$row.<column ref> 樣板 token
第二層可攜性與前者無關,處理的是 rule 與 trigger spec 內的樣板。Store 存的是 $row.<col_hex>,文件寫的是 $row.<column ref>。Export 把內部 key 改寫成 ref,plan/apply 再把 ref 翻回內部 key。
以下欄位會帶 $row. 樣板:
api_callaction 的url、headers與bodynotify與send_channel_messageaction 的訊息line_flex字串invoke_command的輸入值,於觸發當下依觸發列渲染
$row. 的翻譯刻意不是 fail-closed。無法解析的 token 會原樣保留,好讓舊有以內部 key 撰寫的文件、以及只是「看起來像 token」的自由文字繼續運作。因此 api_call body 或 notify 訊息裡打錯的 column ref 會靜默通過 plan,並在觸發時渲染成字面文字。伺服器 plan 抓不到它;實際觸發一次才會。
各種 $ 語法互不相干
不要把某一種語法的 token 帶進另一種。它們長得像,但毫無共通之處:
| 語法 | Token | 使用位置 |
|---|---|---|
| IaC 身分 token | $user:、$dept:、$smc:、$room: | 只在 authored IaC 文件 |
| ACL row-policy token | $me、$me.department、$today、$today±Nd、$now | Grant 與 client-access 的 read_filter/edit_filter predicate 值 |
| Command DSL 參照 | $input、$ctx、$row、$rel、$new、$item | Command definition |
此外還有三種位置專屬的 $ 語法——link 比對的 $self、trigger 樣板的 $row、rule 參照的 $row. / $same——同樣各自獨立;row policy token 列出了全部六種。
Grant filter 永遠不接受 $input。Command definition 永遠不接受把 $dept: 當 filter 值。而 IaC 身分 token 只屬於 IaC:把 $user:alice 送進 REST record API 會被當成原始 ID,並得到 Unknown or inaccessible user ids: ['$user:alice']。
Public-read 的 read_filter 只取 row-policy 語法中不含主體的子集,詳見 public_read。
動手試試
在 IaC 工作台寫一個 department grant,principal 填 {"type": "department", "id": "$dept:Front Desk"},再把前綴改成 $user:,確認本機 parser 接受該 pattern、而伺服器 plan 會回報種類不符。執行唯讀 plan,並確認 grant action 的 ref 帶的是解析後的 UUID,而不是 token。