Phase:P3 — Exploration + Roll + AI
本文件是 P3 的具體實作契約。產品行為以實作規格.md與根目錄規格企劃.md為準;測試與 closeout evidence以測試指南.md為準。
最後更新:2026-09-08
三份 P3 文件使用完全一致的 Subphase 名稱與順序:
P3-A — Session Table Runtime & Event Stream
P3-B — Exploration, Chat & Actions
P3-C — Roll, Check & PendingAction
P3-D — AI Controller, Scoped Token & Handoff
P3-E — AI Tool Surface & Event Delivery
P3-F — Full P3 Integration & Closeout
每個 Subphase 完成 code + tests + static review後才進下一段;需要 Actions 時依 AGENTS.md gate執行。
P3 開工時多人核心已存在於 app.domain.rooms / app.persistence.rooms / app.api.rooms 一帶,重要既有契約包括:
SessionService / Session lifecycle。SessionResumeService。active_character_session_leases。build_capabilities() 與 frontend protectedCapabilityForPath()。P3 的依賴方向固定:
Human Web UI ─┐
├─> P3 application/domain services ─> P2 Session / Character services
MCP Adapter ──┘
Persistence <─ application/domain services
Character Core ─X→ P3 multiplayer runtime
Standalone ─X→ P3 multiplayer runtime
MCP Adapter ─X→ raw repository mutation
最重要的架構原則:
Human UI 與 AI Tool 只是在入口不同;permission、roll、event、state mutation必須共用同一層 application/domain service。
不得建立 ai_* 版本的 roll engine、character-state updater或secret filter。
P2 雖然 schema 已接受 controller_kind='ai' 這個 enum shape,但目前真正能進 Session gameplay authorization 的 caller 是 Human-only。開工時必須把這個事實視為正式 migration 前提,而不是假設 AI 只要有 token 就能沿用現有入口:
apps/server/app/api/rooms/session_scope.py::require_live_character_write() 接受 RoomAccessContext,依 access_session_id 判斷,Player path並且明確要求 player_controller_kind == "human"。apps/server/app/domain/rooms/sessions.py::SessionService._is_current_dm() 明確要求 dm_controller_kind == "human" + Human RoomAccessAuthority。SessionService.start_session() / persistence start_from_lobby() 傳遞並查驗 caller_access_session_id + caller_authority。late_join persistence同樣以 Human access session識別 current DM。因此 P3 的新 service 不得再把 RoomAccessContext 當桌內 actor 的永久 abstraction。Human Room authority仍保留,但 gameplay caller要收斂成後述 TableActorContext。P3-C 可先只交付 Human controller的實際 journey;P3-C 新 service本身不能 hardcode Human-only identity,P3-D 才把 AI grant resolver正式接上同一層。
P3 需要正式支援「DM 代理 Player Seat」。因此 table action不能只保存一個模糊 actor_seat_id 就假設「執行者 = 規則作用對象」。Application layer至少要能表達:
TableActionContext
acting_actor: TableActorContext
subject_seat_id nullable
subject_character_id nullable
execution_mode = self | dm_proxy
規則:
acting_actor.seat_id == subject_seat_id,execution_mode=self。acting_actor.role=dm,subject是某個Player Seat / Active Character,execution_mode=dm_proxy。P3 可以沿用目前 rooms package,不需為了名稱漂亮重構整個 P2。建議最小新增:
apps/server/app/domain/rooms/
table_runtime.py
table_events.py
exploration.py
rolls.py
pending_actions.py
ai_controllers.py
table_actions.py
apps/server/app/persistence/rooms/
table_runtime.py or repository extension
apps/server/app/api/rooms/
table.py
rolls.py
ai.py
apps/server/app/mcp/
server.py
auth.py
tools.py
projections.py
若現有 package形狀更適合拆在其他檔名,可調整,但必須維持:
P3 新 module / table一建立,就同步擴充:
test_m03_import_boundary.py 的 forbidden module coverage。test_m03d_schema_parity.py 的 FORBIDDEN_MULTIPLAYER_TABLES。其中 app.mcp.* 必須被特別點名處理:目前 test_m03_import_boundary.py::FORBIDDEN_MODULE_RE 只匹配 room/session/seat/campaign/party_roster 類 module segment,新增 app.mcp.server / app.mcp.tools 不會自動被既有 regex 擋住。P3-E 建立 app.mcp package 的同一小階段,必須擴充 forbidden matcher(例如加入 mcp segment或等價明確 prefix gate),並加 negative fixture證明 Character/Standalone protected import graph一旦 reach app.mcp.* 會可靠失敗。
不能等 P3-F才補。
P3 全部 multiplayer schema都落在 P2 建立的 web Alembic branch。
character@head
└─ shared Character migrations only
web@head
└─ P2 Room/Campaign/Seat/Session
└─ P3 table runtime / events / rolls / AI grants
規則不變:
alembic upgrade heads。character@head。depends_on 新的 Character revision,但 Character branch永遠不能依賴 Web revision。0013_p2d_campaign_seats.py / 0014_p2e_sessions.py。 P3 對 P2 controller binding 的演進必須放在新的 web revision,用具名 constraint drop + add完成可升級 migration。P3 migration tests要在真 PostgreSQL驗 fresh + P2 legacy upgrade,不能只有 SQLite metadata.create_all。
P3-A 建議新增最小兩個核心 persistence concept:
session_table_runtime
session_events
session_table_runtime 一場 Session最多一列:
session_id PK/FK -> sessions
revision bigint, monotonic
last_event_seq bigint, monotonic
created_at
updated_at
不要先塞 Stage / Roll / AI全部欄位;各 Subphase的 canonical table各自持有真實資料。session_table_runtime只提供共用 revision / cursor coordination。
session_events 是 P3 transport/event substrate,不是 P7 Timeline UI model:
id UUID PK
session_id FK
seq bigint
kind stable string / enum
acting_seat_id nullable FK
subject_seat_id nullable FK
subject_character_id nullable FK
execution_mode self | dm_proxy | system nullable
visibility enum
recipient_seat_ids JSON/array, default []
payload JSON object
created_at
idempotency_key nullable
若實作上沿用既有 actor_seat_id 命名也可以,但必須另有方式保存 DM proxy 的 acting vs subject identity,不能丟失 audit truth。
DB constraint:
UNIQUE(session_id, seq)
UNIQUE(session_id, idempotency_key) WHERE idempotency_key IS NOT NULL
sequence配置必須是 transaction-safe。可用 runtime row SELECT ... FOR UPDATE increment,或 PostgreSQL-compatible等價做法;不能 max(seq)+1 無鎖計算。
Event payload只放該 event重播/呈現需要的 immutable fact,不複製整份 Character / Session snapshot。
P3 event payload從第一天就要有 schema/version discipline:
kind stable。P3 event visibility固定由 Server產生 caller projection。最低 enum:
public
DM_ONLY
ACTOR_AND_DM
SEAT_PRIVATE
SEAT_PRIVATE 可帶 recipient_seat_ids。讀取時規則:
不得讓 caller query傳 include_dm_only=true 自己選 audience。
Human Web 與 MCP共用 application service:
TableEventService.current_cursor(session_id, caller)
TableEventService.list_after(session_id, caller, after_seq, limit)
TableEventService.wait_after(session_id, caller, after_seq, timeout)
HTTP建議:
GET /api/rooms/{roomId}/campaigns/{campaignId}/sessions/{sessionId}/events?after=<seq>&limit=<n>
GET /api/rooms/{roomId}/campaigns/{campaignId}/sessions/{sessionId}/events/wait?after=<seq>&timeout=<seconds>
wait 的 correctness contract不是單純「可以 long-poll」;現有 Web backend 主要是同步 SQLAlchemy / sync FastAPI route,因此 wait HTTP endpoint 必須是 async def/等價非同步 request path,且等待期間不得持有 SQLAlchemy connection、ORM Session、transaction或把整段 timeout佔在 sync worker thread裡。
固定 lifecycle:
async wait request
→ 短 DB read:authorize + read cursor / immediate events
→ 關閉 DB connection / transaction
→ await process-local notifier / AnyIO-asyncio primitive
→ wake / timeout / cancellation
→ 短 DB re-read:list_after(cursor)
→ response
若 repository仍是 sync SQLAlchemy,短 DB 操作可用既有 dependency / thread offload執行;只能 offload短 query,不能把 30–60 秒等待本身丟進 threadpool。
Durability以DB為真,不可只靠 process-local queue。Process-local notifier只降低 latency;任何 lost wakeup、process restart或多 worker miss都必須能靠DB cursor補回。
Resource invariant:
P3 不需要 WebSocket 作為 correctness requirement。未來可換 transport,但 domain event cursor contract不變。
P2 SessionResumeService保留「組合 canonical truth」原則。P3擴充時建議建立:
SessionTableResumeDTO
由既有 Resume + P3 current runtime projection組成,而不是把 P2 DTO變成第二份 persistence snapshot。
初次載入至少回:
P2已知 N+1問題在 P3-A 必須處理:
Session End / Abandon:
End / Abandon不是「先改 status,之後背景工作清token」。詳見 §12 atomic lifecycle boundary。
建議新增:
session_messages
session_stage_state
room_stage_assets
session_messages canonical欄位:
id
session_id
acting_seat_id nullable
subject_seat_id nullable
subject_character_id nullable
execution_mode = self | dm_proxy | system
kind = character_dialogue | narration | action | ooc | whisper_dm
text
visibility
created_at
命名可依現有 convention微調,但 DM proxy時 acting/subject資料不能只存在display text。
Message insert與對應 session_events append必須同 transaction commit;不能出現DB有訊息但event沒送,或event出現但message row不存在。
session_stage_state:
session_id PK
text nullable
image_asset_id nullable
revision
updated_by_seat_id
updated_at
Stage更新採 optimistic revision / compare-and-swap或等價 stale-write guard,避免DM兩個browser tab後送的舊內容蓋掉新內容而無提示。
P3沒有 Asset Library,所以圖片 persistence刻意簡單。建議 room_stage_assets:
id
room_id
mime_type
byte_size
sha256
content BYTEA
created_by_controller_ref
created_at
私人朋友使用、P3只支援 Stage image時,DB BYTEA可換取最簡單的一致 transaction / backup / Room Hard Delete語意;若實作時已有正式 blob storage abstraction,可沿用,但不可因此建立完整 Asset domain。
最低限制:
圖片讀取 endpoint本身也要 Room authorization,不得因URL可猜而public。
P3 共用 caller context:
TableActorContext
actor_kind = human | ai
human identity:
access_session_id
ai identity:
ai_controller_grant_id
grant_generation
resolved common truth:
room_id
campaign_id
session_id
seat_id
role
active_character_id nullable
Human adapter可額外保有 RoomAccessAuthority 供 Room-management endpoint使用;該 authority不是 AI actor欄位,也不能為了讓AI DM通過既有函式而偽造。
一般權限:
TableActionService接受 acting_actor + subject:
acting_actor = current DM TableActorContext
subject_seat_id = target Player Seat
subject_character_id = target Active Character
execution_mode = dm_proxy
授權:
Slash command只是一層輸入 parser,不建立第二組 backend business endpoint:
/action -> typed Action
/search -> typed Action with search intent
/whisper -> typed Whisper DM
/ooc -> typed OOC
/check -> P3-C typed Check intent / DM Request Check flow
/search 不自動選skill、不自動roll。Player說「/search door」仍只是描述行動;是否需要 Investigation / Perception、DC多少由DM裁定。
/check 的產品 ownership在 P3-C:
Session page在P3-B後變成桌面shell:
┌─────────────────────────────────────────────┐
│ Player Cards │
├──────────────────────────────┬──────────────┤
│ MAIN STAGE │ Chat │
│ image/text │ Dice (P3-C) │
│ │ Log │
├──────────────────────────────┴──────────────┤
│ Exploration Input / Quick Action Bar │
└─────────────────────────────────────────────┘
Main Stage與右側panel divider可調整;layout preference只需client preference,不進game state。
Stage與Chat必須語意分離:
DM proxy UI可從Player Card或action context進入,但不得做成「Take Controller」;UI應清楚顯示這次是 Act as / Proxy for,送出後 Seat controller原樣保留。
建議新增:
roll_groups
roll_requests
roll_results
pending_actions
action_windows # optional
roll_requests最低欄位:
id
session_id
roll_group_id nullable
target_seat_id
target_character_id nullable
request_type
ability_ref nullable
skill_ref nullable
dc nullable
modifier_mode = normal | advantage | disadvantage
flat_adjustment
visibility
status = pending | resolved | cancelled
requested_by_seat_id
created_at
resolved_at nullable
version
dc 是 secret-bearing field;API projection依caller決定是否輸出,不是frontend display:none。
roll_results:
id
roll_request_id nullable # quick dice為null
session_id
acting_seat_id
subject_seat_id
subject_character_id nullable
execution_mode = self | dm_proxy
source = server | physical | quick
formula
raw_dice
kept_dice
base_modifier
flat_adjustment
total
visibility
created_at
formal request加唯一 constraint:一個 roll_request_id 最多一個 committed result。
Server formal roll使用 Python secrets.SystemRandom / OS CSPRNG或等價不可預測來源,不使用client Math.random()。
Roll engine輸出完整 audit shape:raw dice、adv/disadv kept die、derived modifier、explicit ±N、total。
規則資料從 subject Character / Rules canonical data計算;API client不能傳「我的 proficiency bonus=99」覆蓋 Server derivation。
DM proxy時同樣用 subject Player Character 的 ability / proficiency / resources,不用DM自己的角色資料。
Physical dice submit endpoint只接受 pending roll_request_id + raw die value(s);Server重新計 modifier / total。Client不得只提交 final total。
Quick Dice獨立記錄,不完成 formal request。
Formal roll completion必須有:
pending -> resolved;roll_request_id result。Race結果:只有一個commit成功;後來者取得既有resolved result或stable conflict,不能產生第二結果。
pending_actions:
id
session_id
acting_seat_id
subject_seat_id
subject_character_id nullable
execution_mode
text / typed intent payload
status
roll_request_id nullable
window_id nullable
version
created_at
updated_at
transition至少:
pending -> processing
pending/processing -> waiting_for_roll
waiting_for_roll -> processing/resolved
pending/processing/waiting_for_roll -> cancelled
processing -> resolved
非法逆轉用stable error code,不靠UI禁止。
若實作 ActionWindow:只是一次「大家現在要做什麼」的collection container,不生成exploration turn order。
/checkP3-C正式接手 slash /check:
Player /check <intent>
→ same Exploration Check intent
→ current DM decides whether formal Request Check is needed
DM /check ...
→ same RequestCheckService
→ creates RollGroup/RollRequest if valid
Player端不得因 /check 而自行選secret DC或把 Quick Dice偽裝成formal request。
P3不建立第二套 Character write API。正式table action若要扣HP / 改Temp HP / condition / resource:
TableActionService
→ resolve TableActionContext(acting_actor, subject, execution_mode)
→ actor-aware live-character authorization adapter
→ existing Character State mutation service on subject Character
→ append table event in same transaction boundary
P3-C closeout至少驗:
P3-D接上AI actor後同一 service可接受 AI Player self path / AI DM proxy path(若產品操作需要);不得另做AI state-write service。
如果既有 Character service與P3 repository不能自然共用同一DB transaction,P3-C必須明確建立application unit-of-work;不能接受「先改HP,第二個request再寫log」的best effort雙步驟。
P2 existing Seat / Session controller identity需演進為:
none
human(access_session_id)
ai(ai_controller_grant_id, grant_generation)
P3 gameplay caller正式 context:
TableActorContext
actor_kind = human | ai
human:
access_session_id
ai:
ai_controller_grant_id
grant_generation
resolved:
room_id
campaign_id
session_id
seat_id
role
active_character_id nullable
RoomAccessAuthority仍屬 Human Room-management context;AI DM的DM能力來自被Owner事前配置的 DM Seat + valid grant,不得建立假的 authority='dm'/'owner' 來騙過P2函式。
P3-D 必須把以下既有入口改成接受/resolve typed actor:
apps/server/app/api/rooms/session_scope.py::require_live_character_write() 與 live Character write scope。SessionService.start_session()。SessionService._is_current_dm()/等價 current-DM policy。SessionService.late_join()。SessionService.end_session()。SessionService.abandon_session() 的 current DM controller分支;Owner escape hatch仍走 Human Owner authority。start_from_lobby()。late_join_from_lobby()/任何 current-DM access-session comparison。不得用「AI token驗過,所以隨便找一個 Human access session id代入」的相容層。
既有 P2 Human 行為必須保持向後相容;同步保留/更新 live character scope、Session Start/End/Abandon、Late Join、API authorization與controller persistence regression。
P2 schema 已把 Human access-session binding與 controller_kind 分欄,但目前三個 binding CHECK只能表示 Human有access session、AI/None沒有,不能表示是哪個grant控制;同時也沒有一個明確的 current controller epoch SSOT。
P3-D 在新的 web Alembic revision新增/演進最低欄位:
campaign_seats
ai_controller_grant_id nullable
controller_epoch bigint NOT NULL # per-Seat authoritative monotonic SSOT
sessions
dm_controller_ai_grant_id nullable
dm_controller_generation nullable # fixed AI grant generation snapshot
session_participants
controller_ai_grant_id_at_join nullable
controller_generation_at_join nullable # join-time AI generation snapshot
campaign_seats.controller_epoch 是 唯一 current generation authority:Seat建立時有初值(例如0),每次 controller identity真正改變時在同一 transaction單調增加,包括 Human→AI、AI→Human、AI→None、Human controller換成另一 access session、AI rotate/regrant、Take Back與 administrative reassignment。它不是只在 controller_kind='ai' 時才有值。
ai_controller_grants.generation 是 mint 當下 Seat controller_epoch 的 immutable snapshot;Session / Participant generation欄位也只是 historical snapshot。不得把 grant row自己的 generation 當 current authority,也不得只因 grant.status='active' 就授權。
新的 migration 不得修改 0013_p2d_campaign_seats.py / 0014_p2e_sessions.py,而是對下列既有具名 constraint做 drop + add:
ck_campaign_seats_controller_binding
ck_sessions_dm_controller_binding
ck_session_participants_controller_binding
Seat新的 binding CHECK語意:
common:
controller_epoch IS NOT NULL
human:
human access_session_id IS NOT NULL
ai grant id IS NULL
ai:
human access_session_id IS NULL
ai grant id IS NOT NULL
none:
human access_session_id IS NULL
ai grant id IS NULL
Session / Participant snapshot CHECK維持 controller-kind aware:Human snapshot要求 Human access session、AI snapshot要求 AI grant id + generation、None兩邊皆空;這些 snapshot不反過來成為 current Seat authorization SSOT。
建議新增:
ai_controller_grants
最低欄位:
id
room_id
campaign_id
seat_id
role
session_id nullable
secret_hash
secret_prefix / display hint
generation # immutable Seat controller_epoch snapshot
status = active | revoked
pre_session_expires_at nullable
handoff_return_access_session_id nullable
temporary_instruction nullable
created_at
bound_at nullable
revoked_at nullable
last_seen_at nullable
欄位規則:
session_id 一開始就有值,handoff_return_access_session_id 必填且等於執行 Let AI Control 的 Human access session;pre_session_expires_at=NULL。session_id=NULL、handoff_return_access_session_id=NULL、pre_session_expires_at 必填且必須是有限未來時間。TTL長度可由 server config決定,但不允許「永不到期」作為預設或 production-like gate。generation 一經mint不可改;controller epoch要前進就mint新grant,不更新舊grant generation冒充新controller。temporary_instruction 若不想直接放 grant table,可拆一對一 handoff context table;但 lifecycle必須與 grant generation一致,不可進 Character / Campaign permanent notes。Token plaintext只在create/rotate response返回一次。Persistence不可存plaintext。
每次 AI request 都重新驗:
secret verifier matches
AND grant.status = active
AND scope Room/Campaign/Seat/Role valid
AND campaign_seat.controller_kind = ai
AND campaign_seat.ai_controller_grant_id = grant.id
AND campaign_seat.controller_epoch = grant.generation
AND session / pre-session scope valid
AND pre-session TTL not expired when session_id IS NULL
所以就算某個舊 grant row因故仍標 active,只要 Seat已換 controller、grant id不再匹配或 epoch前進,就必須立即拒絕。這個 Seat binding + epoch check是 N3 的 current authority SSOT。
controller_epoch,mint generation=controller_epoch 的 grant並直接綁 session_id,再把 Seat current AI grant binding指向它。generation=controller_epoch、session_id=NULL、有限 pre_session_expires_at 的 pre-session DM grant,Seat current grant binding指向它。session_id=NULL 時,它不是 current Session DM。允許面只包含:
Start Session。明確禁止:
get_session_context / Session event read / wait。pre-session DM grant在以下事件發生時立即失去授權能力;有對應 mutation transaction時應在同一 UoW revoke:
status欄位即時改掉;可在下次讀寫時 lazy persist revoke,但安全不能依賴 cleanup job。completed、archived,或現行允許的其他非active狀態)。active_campaign_id 切離該 Campaign時,若該 grant尚未綁 Session,應revoke;重新選回Campaign後重新mint,避免 dormant DM bearer長期留存。採至少128 bits entropy的opaque bearer secret,例如:
at_ai_<public-id>_<random-secret>
Server用public id定位grant,再constant-time驗hash。Room / Campaign / Seat / role / session scope全部從grant row取得,不相信tool request自行宣稱;最後一定套用 §8.3 的 Seat grant-id + controller-epoch current binding check。
Let AI Control request:
seat_id
temporary_instruction nullable
transaction:
seat.controller_access_session_id == caller.access_session_id。seat.controller_epoch。generation = seat.controller_epoch,並保存 handoff_return_access_session_id = caller.access_session_id + optional Temporary Instruction。Temporary Handoff Instruction:
Take Back Control / revoke / End / Abandon後不再出現在active context。P2目前沒有帳號級 persistent Human identity;Room access session就是可驗證的 Human credential。因此 P3-D 不得用 display_name、同一 Room member、Room password、IP/browser fingerprint等猜測「這還是原玩家」。
Take Back Control self-service固定:
caller = active Human RoomAccessContext
seat.controller_kind = ai
grant = seat.ai_controller_grant
caller.access_session_id == grant.handoff_return_access_session_id
caller access session仍屬同Room且未revoked
只有全部成立才可 Take Back。
transaction:
controller_epoch,使任何舊grant generation stale。controller_access_session_id = handoff_return_access_session_id,清 AI grant binding。原 handoff Human若換裝置/清 localStorage而拿到新的 Room access session,即使 display name相同,也不能走 self-service Take Back。這不是 P3 可安全推導的同一個人。
如果原 handoff_return_access_session_id 已遺失/revoked,恢復控制走既有 Player Seat management authority:Owner / DM可依 P2 non-DM Seat management policy把該 Player Seat重新assign給一個合法 Human access session。
這不是 Take Back Control;必須:
controller_epoch。controller_kind=human + new controller_access_session_id。普通 member、相同 display name的新 access session、知道Room password的人都不能用這條 admin path。
Human DM path仍成立;新增AI path:
Owner configures DM Seat as AI
→ increment DM Seat controller_epoch
→ create finite-TTL pre-session AI DM grant
session_id=NULL
generation=controller_epoch
→ bind DM Seat current grant id
→ external AI authenticates
→ AI may read only minimal own pre-session/start context
→ AI calls Session Start before expiry
→ Start transaction validates assigned DM Seat + current grant id + epoch/generation + TTL
→ bind grant to new session_id
→ Session snapshots grant id + generation as fixed DM Controller
Start policy:
AI DM token rotate / replace只允許 Session尚未開始。Rotate本身必須 revoke舊 pre-session grant、increment Seat epoch、mint新 finite-TTL grant;Active Session期間不能用rotate達成DM handoff。
End Session由 fixed current DM actor執行,因此 Human DM / AI DM都可依同一 policy End。
Abandon Session維持:
fixed current DM actor (human or ai)
OR
Human Room Owner escape hatch
Late Join同樣由 fixed current DM actor授權。
End與Abandon transition必須在同一 lifecycle UoW:
lock Session
→ validate actor / Owner escape path
→ mark/close pending runtime as contract requires
→ append final boundary event
→ revoke all active ai_controller_grants where session_id = this Session
→ invalidate Temporary Handoff Instruction contexts
→ release active_character_session_leases
→ set ended/abandoned status
→ commit
實際SQL順序可調,只要一個 transaction失敗就全回滾。不能留下 status=ended 但AI token仍有效的窗口。
AI Connection與controller ownership分開。
AI request / wait event成功可更新 last_seen_at,但:
last_seen_at過期只代表Offline。pre-session TTL與presence不同。 pre_session_expires_at 到期是credential authorization expiry,必須拒絕 Start / pre-session context;不能因 last_seen_at 最近就延長或忽略 TTL。
P3不假設Server可主動喚醒外部AI conversation。
P3 remote MCP以 MCP specification 2026-07-28 為設計基準;核心P3 service不可被某個SDK type污染。
P3-E 必須明確 opt in / pin 到 modern 2026-07-28 wire contract,不能讓 SDK 預設 silently fallback 到 2025-era handshake。該版本的核心是 stateless request/response:
initialize / notifications/initialized handshake。Mcp-Session-Id transport session。MCP-Protocol-Version: 2026-07-28,body _meta 內的 io.modelcontextprotocol/protocolVersion 必須與 header 一致。Mcp-Method;tools/call、resources/read、prompts/get 等 named operation另帶對應 Mcp-Name,header/body mismatch 必須拒絕。server/discover;client可選擇呼叫它取得能力,但它是 optional discovery RPC,不是 session handshake,任何合法 modern request都不能依賴先前 discover state 才能授權/執行。server/discover 與 2026-era cacheable list/read result依規格提供 ttlMs / cacheScope;P3 protocol smoke要驗 wire 上確實存在,不只驗 SDK high-level object。Gameplay Session、Seat scope與event cursor全部仍由 Adventure Table application state決定,不能因 MCP transport變 stateless 就搬進 process-local transport state。
建議:
POST /mcp
Authorization: Bearer <AI Join Token>
P3 的 AI Join Token 是 Adventure Table 私人部署的 app-level pre-shared bearer credential。P3 不宣稱因此完成 MCP Authorization specification 的完整 OAuth 2.1 / Protected Resource Metadata / authorization-server flow;MCP authorization本身是 optional,但既然 P3 選擇以 bearer secret保護 remote HTTP endpoint,就必須遵守 bearer token的傳輸安全底線。
每個request:
Bearer token
→ resolve AIControllerGrant
→ verify secret/status/TTL-if-unbound
→ verify Seat current grant binding + controller_epoch == grant.generation
→ verify Room/Campaign/Seat/Role/session-or-pre-session scope
→ build TableActorContext or restricted PreSessionAIContext
→ call shared application service
不得把上一個MCP request的scope留在global / thread-local再沿用。session_id=NULL 的 pre-session DM grant不得被resolve成假的 active-Session TableActorContext;它只能建立 restricted pre-session context,直到 Start transaction成功綁定Session。
傳輸要求固定:
http://127.0.0.1/... 只可作 loopback development / automated protocol test substrate,不能拿來當「真 external client + bearer token」closeout evidence,也不能把 bearer endpoint暴露到非loopback網路。Authorization: Bearer ...(或等價安全 header injection)。closeout記錄 client名稱、版本、配置方式與測試日期;不得只因某 client「支援 MCP」就假設它支援手動 static token。Tool是高階table action,不是CRUD table mapping。最低集合可採:
get_session_context
get_character_context
post_dialogue
post_action
post_ooc
whisper_dm
set_stage_text # DM only
request_check # DM only
roll_pending
submit_physical_roll
quick_roll
update_character_state # policy-controlled Current State action
get_pending_events
wait_for_event
另可有最小 get_pre_session_context / 等價 Start readiness tool,只接受有效未綁定 AI DM grant,且輸出限自己 DM Seat / Campaign / Start readiness,不得偷帶 active Session gameplay state。
Player AI的 get_session_context / get_character_context 要包含目前有效 Temporary Handoff Instruction(若有),但失效後不能回傳。
禁止:
execute_sql
get_table_row
patch_character_json
set_roll_total
read_dm_only_as_player
impersonate_seat
resolve_action # P3刻意不交付;見9.7
所有tool回 structured content + stable error code。AI control flow判斷 code,不是解析localized message。
Player AI:Session identity、Stage public projection、public recent events、own Seat/Character allowed state、own pending、有效Temporary Instruction、其他Player必要public card summary。
AI DM:只有 grant已成功綁 active Session後才取得DM-visible table state、pending check info、participant summaries。Pre-session AI DM token只能用 §8.3 / §9.3 的最小pre-session context。
P6前沒有Adventure Runtime,所以P3-E不得假裝提供完整world/NPC knowledge。
wait_for_event(timeout) wrapper只呼叫P3-A TableEventService.wait_after:
events=[] + cursor,屬成功。get_pending_events(after_cursor)補洞。未綁 Session 的 pre-session DM grant不得呼叫 event wait。
resolve_action() 明確延後產品 SSOT 描述的高階 AI DM:
resolve_action()
→ narration
→ state changes
→ optional runtime content
→ optional persistent facts
→ timeline
P3 不實作這個 orchestration tool。 原因不是取消產品需求,而是其完整語意跨越後續 Phase:
因此 ownership固定:
P3-E: fine-grained shared tools only
P6: first complete high-level resolve_action() orchestration + world/runtime write-back
P7: integrate the resolved transaction into formal Timeline/history/snapshot model
不得在P3做一個同名但只會 Narration + HP 的半套 resolve_action(),避免後續 contract破壞。
未來transport:
new transport adapter
→ same TableActorContext resolver
→ same application services
不得複製rules / permission。
P3每新增一個可導航多人surface,同一commit至少處理:
build_capabilities()。protectedCapabilityForPath()。現有 apps/server/app/api/meta.py::CapabilityFlags 使用 extra="forbid",且已經存在 combat、timeline、ai_actor。P3 capability contract固定:
ai_actor;不得新增 mcp / ai_tool / external_ai 等同義 flag 造成兩套真實來源。combat=false,直到 P4 真正接手 Combat。timeline=false,P3 event/log substrate 不得因此提前打開 P7 Timeline capability。table_runtime / roll 不是現有 flag。若實作真的需要比既有 session 更細的 route gate,可以新增,但必須在同一commit更新 backend CapabilityFlags、build_capabilities()、frontend capability type / protectedCapabilityForPath()與 Web/Standalone contract tests;不能只在文件或前端自行假設欄位存在。session gate已足以保護 P3-A/B/C route,優先避免為了名稱漂亮增加多餘 flag。Standalone:不mount P3 routers、不mount /mcp;ai_actor=false,任何新增 P3 multiplayer capability也必須 false。
P3延續P2 error discipline:
zh-TW / en parity test。hardcodedUiCopy.test.ts涵蓋P3 Session components。新增可預期error例如:
ai_token_revoked
ai_token_expired
ai_token_wrong_session
ai_token_not_session_bound
ai_grant_not_current_controller
controller_changed
take_back_origin_required
take_back_origin_access_revoked
proxy_subject_not_participant
proxy_subject_not_player
roll_request_already_resolved
session_not_active
名稱可依既有 convention,但 client不能靠英文句子判斷。
P3至少以下必須 atomic:
Let AI Control:Seat epoch increment + token revoke/mint + return access identity + Temporary Instruction context + Seat binding + controller changed event。Take Back Control:origin identity recheck + AI grant revoke + Seat epoch increment + Human binding restore + controller changed event。可以使用現有SQLAlchemy Session / repository UoW;不要求P3建立完整 GameTransaction persistence entity。
禁止 application service內先commit一次、再呼另一service第二次commit來模擬atomic。
永久規則:
Authorization header redaction。controller_epoch 為準;grant status只是必要條件之一,不是充分條件。Take Back Control只能由grant保存的原 handoff Human access session self-service;其他member、相同display name、新access session一律不能冒充。遺失原session時只走Owner/DM administrative reassignment。最低要求:
(session_id, seq)。SessionResumeService Active Character summary避免N+1高頻化。SEAT_PRIVATE.recipient_seat_ids 的 index contract依實際 storage/query strategy決定,但不能含糊:
recipient_seat_ids,必須建立對應 membership index(例如適合該型別/operator的 GIN 或等價設計),並有 query-plan / focused performance evidence,避免 private event查詢退化成全 Session seq scan。(session_id, seq) 做有明確 limit 的 bounded incremental window,再由 Server projection在少量 rows內判斷 recipient membership,GIN 可不強制;但必須用測試證明 bounded window / query count,不能把「桌規模很小」當無界 seq scan 的理由。(seat_id, event_id/session_id) 類 index;文件不綁死 JSONB。P3不要求Redis / Kafka / websocket broker;PostgreSQL + process-local wake notification + DB truth即可,但correctness不能依賴單process memory。
P3-F不新增新的產品domain;補整合與證據:
.github/workflows/p3-non-e2e.yml / P3 Non-E2E。.github/workflows/p3-e2e.yml / P3 Full-Stack E2E。如果P3-F發現為了測試方便需要新增產品行為,先回對應Subphase contract,不偷塞closeout。
P4可直接重用:TableActorContext / TableActionContext、DM proxy、event cursor / visibility、RollGroup / Request / Result、PendingAction、atomic table action UoW。
P6讓AI DM取得Adventure / world context、persistent facts、NPC / Scene runtime,並首次完整交付高階 resolve_action() orchestration。P3只提供細粒度table actions與安全write primitive,不用同名半套tool占住contract。
P7把P3 session_events / messages / rolls / actions,以及P6高階resolved transactions,投影成正式跨Session Timeline,並建立Snapshot / Restore / broader Export。P3 event substrate不等於P7完成。