adventure-table

P3 — 開發設計方針

Phase:P3 — Exploration + Roll + AI
本文件是 P3 的具體實作契約。產品行為以 實作規格.md 與根目錄 規格企劃.md 為準;測試與 closeout evidence以 測試指南.md 為準。

最後更新:2026-09-08


1. P3 Subphase 順序

三份 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執行。


2. 開工時真實 codebase 與永久依賴方向

P3 開工時多人核心已存在於 app.domain.rooms / app.persistence.rooms / app.api.rooms 一帶,重要既有契約包括:

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。

2.1 P2 caller identity 的真實限制

P2 雖然 schema 已接受 controller_kind='ai' 這個 enum shape,但目前真正能進 Session gameplay authorization 的 caller 是 Human-only。開工時必須把這個事實視為正式 migration 前提,而不是假設 AI 只要有 token 就能沿用現有入口:

因此 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正式接上同一層。

2.2 Acting actor 與 subject Seat 分離

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

規則:


3. P3 multiplayer module 建議 layout

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一建立,就同步擴充:

其中 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才補。


4. Alembic 與 persistence track

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

規則不變:

P3 migration tests要在真 PostgreSQL驗 fresh + P2 legacy upgrade,不能只有 SQLite metadata.create_all


5. P3-A — Session Table Runtime & Event Stream

5.1 canonical tables

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_eventsP3 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 無鎖計算。

5.2 event payload規則

Event payload只放該 event重播/呈現需要的 immutable fact,不複製整份 Character / Session snapshot。

P3 event payload從第一天就要有 schema/version discipline:

5.3 audience model

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。

5.4 event query / wait API

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不變。

5.5 Resume integration

P2 SessionResumeService保留「組合 canonical truth」原則。P3擴充時建議建立:

SessionTableResumeDTO

由既有 Resume + P3 current runtime projection組成,而不是把 P2 DTO變成第二份 persistence snapshot。

初次載入至少回:

P2已知 N+1問題在 P3-A 必須處理:

5.6 End / Abandon lifecycle

Session End / Abandon:

End / Abandon不是「先改 status,之後背景工作清token」。詳見 §12 atomic lifecycle boundary。


6. P3-B — Exploration, Chat & Actions

6.1 canonical message / stage tables

建議新增:

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後送的舊內容蓋掉新內容而無提示。

6.2 最小 Stage asset

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。

6.3 TableActorContext / exploration permission

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通過既有函式而偽造。

一般權限:

DM proxy

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

授權:

6.4 Slash command parser ownership

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:

6.5 Web UI

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原樣保留。


7. P3-C — Roll, Check & PendingAction

7.1 tables

建議新增:

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。

7.2 RNG

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自己的角色資料。

7.3 formal / physical / quick roll

Physical dice submit endpoint只接受 pending roll_request_id + raw die value(s);Server重新計 modifier / total。Client不得只提交 final total。

Quick Dice獨立記錄,不完成 formal request。

7.4 idempotency / concurrency

Formal roll completion必須有:

Race結果:只有一個commit成功;後來者取得既有resolved result或stable conflict,不能產生第二結果。

7.5 PendingAction

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。

7.6 /check

P3-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。

7.7 Character state mutation / DM proxy

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雙步驟。


8. P3-D — AI Controller, Scoped Token & Handoff

8.1 TableActorContext 與既有授權入口 migration

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:

不得用「AI token驗過,所以隨便找一個 Human access session id代入」的相容層。

既有 P2 Human 行為必須保持向後相容;同步保留/更新 live character scope、Session Start/End/Abandon、Late Join、API authorization與controller persistence regression。

8.2 controller persistence / P2 CHECK constraint migration

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。

8.3 AI controller grant model

建議新增:

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

欄位規則:

Token plaintext只在create/rotate response返回一次。Persistence不可存plaintext。

Current AI authorization invariant

每次 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。

Session scope lifecycle

Unbound pre-session AI DM grant 的能力面

session_id=NULL 時,它不是 current Session DM。允許面只包含:

明確禁止:

Unbound grant revoke / expiry lifecycle

pre-session DM grant在以下事件發生時立即失去授權能力;有對應 mutation transaction時應在同一 UoW revoke:

8.4 token format

採至少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。

8.5 Player Human → AI / AI → Human

Let AI Control request:

seat_id
temporary_instruction nullable

transaction:

  1. lock Seat controller row / equivalent。
  2. verify caller是目前Human controller,且 seat.controller_access_session_id == caller.access_session_id
  3. verify active Session participant / Active Character一致。
  4. ensure沒有不可安全切換的in-flight commit;已commit transaction不回滾。
  5. revoke previous AI grant(若有)。
  6. increment seat.controller_epoch
  7. mint new session-bound grant,generation = seat.controller_epoch,並保存 handoff_return_access_session_id = caller.access_session_id + optional Temporary Instruction。
  8. switch Seat controller to AI並bind該grant id。
  9. append controller-changed event(Instruction本身不必做public event)。
  10. commit。

Temporary Handoff Instruction:

Take Back Control self-service identity

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:

  1. lock同一Seat/controller state。
  2. revalidate current grant id + epoch/generation + return access identity。
  3. revoke AI grant。
  4. increment Seat controller_epoch,使任何舊grant generation stale。
  5. switch Seat回 Human,controller_access_session_id = handoff_return_access_session_id,清 AI grant binding。
  6. append controller-changed audit event。
  7. commit。

原 handoff Human若換裝置/清 localStorage而拿到新的 Room access session,即使 display name相同,也不能走 self-service Take Back。這不是 P3 可安全推導的同一個人。

Administrative Player Seat reassignment

如果原 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;必須:

普通 member、相同 display name的新 access session、知道Room password的人都不能用這條 admin path。

8.6 AI DM Start / Session lifecycle

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 token revoke

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仍有效的窗口。

8.7 connection / presence

AI Connection與controller ownership分開。

AI request / wait event成功可更新 last_seen_at,但:

pre-session TTL與presence不同。 pre_session_expires_at 到期是credential authorization expiry,必須拒絕 Start / pre-session context;不能因 last_seen_at 最近就延長或忽略 TTL。

P3不假設Server可主動喚醒外部AI conversation。


9. P3-E — AI Tool Surface & Event Delivery

9.1 protocol baseline

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:

Gameplay Session、Seat scope與event cursor全部仍由 Adventure Table application state決定,不能因 MCP transport變 stateless 就搬進 process-local transport state。

9.2 MCP endpoint / auth / TLS

建議:

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。

傳輸要求固定:

9.3 tool naming

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

9.4 structured response

所有tool回 structured content + stable error code。AI control flow判斷 code,不是解析localized message。

9.5 get_session_context

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。

9.6 event wait

wait_for_event(timeout) wrapper只呼叫P3-A TableEventService.wait_after

未綁 Session 的 pre-session DM grant不得呼叫 event wait。

9.7 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破壞。

9.8 Site Tools / future agent

未來transport:

new transport adapter
→ same TableActorContext resolver
→ same application services

不得複製rules / permission。


10. Capability / routing / frontend guard

P3每新增一個可導航多人surface,同一commit至少處理:

現有 apps/server/app/api/meta.py::CapabilityFlags 使用 extra="forbid",且已經存在 combattimelineai_actor。P3 capability contract固定:

Standalone:不mount P3 routers、不mount /mcpai_actor=false,任何新增 P3 multiplayer capability也必須 false。


11. stable error / localization SSOT

P3延續P2 error discipline:

新增可預期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不能靠英文句子判斷。


12. Transaction boundary / Unit of Work

P3至少以下必須 atomic:

可以使用現有SQLAlchemy Session / repository UoW;不要求P3建立完整 GameTransaction persistence entity。

禁止 application service內先commit一次、再呼另一service第二次commit來模擬atomic。


13. Security boundary

永久規則:

  1. audience filter在Server projection。
  2. token plaintext不落DB / log。
  3. Authorization header redaction。
  4. MCP tool args私密文字不在exception telemetry全文輸出。
  5. Stage upload驗magic bytes、大小、allowlist。
  6. cross-Room / Seat / Session scope每個write/read都從canonical relation重驗。
  7. AI DM沒有Owner destructive endpoint。
  8. AI Player看不到其他Player whisper / dm-only result。
  9. revoked / expired / stale generation / wrong-session token在所有tool共用auth resolver被拒絕。
  10. Current AI authority以 Seat current grant id + controller_epoch 為準;grant status只是必要條件之一,不是充分條件。
  11. 未綁Session的AI DM grant只可做minimal pre-session context + Start,不可取得任何active Session gameplay/private state。
  12. Human / AI actor不得用偽造Human access-session完成AI authorization。
  13. Take Back Control只能由grant保存的原 handoff Human access session self-service;其他member、相同display name、新access session一律不能冒充。遺失原session時只走Owner/DM administrative reassignment。
  14. Temporary Handoff Instruction不得進public event、Character export或Campaign permanent fact;失效後不再出現在AI context。
  15. DM proxy只有fixed current DM actor可用;持DM Key但不是本場DM Controller不得proxy。
  16. remote MCP bearer credential不得經非 TLS 公網/LAN 明文傳輸;external gate必須走 §9.2 的 HTTPS/TLS 入口。

14. Performance / polling

最低要求:

SEAT_PRIVATE.recipient_seat_ids 的 index contract依實際 storage/query strategy決定,但不能含糊:

P3不要求Redis / Kafka / websocket broker;PostgreSQL + process-local wake notification + DB truth即可,但correctness不能依賴單process memory。


15. P3-F closeout wiring

P3-F不新增新的產品domain;補整合與證據:

如果P3-F發現為了測試方便需要新增產品行為,先回對應Subphase contract,不偷塞closeout。


16. P4 / P6 / P7 handoff boundary

給 P4

P4可直接重用:TableActorContext / TableActionContext、DM proxy、event cursor / visibility、RollGroup / Request / Result、PendingAction、atomic table action UoW。

給 P6

P6讓AI DM取得Adventure / world context、persistent facts、NPC / Scene runtime,並首次完整交付高階 resolve_action() orchestration。P3只提供細粒度table actions與安全write primitive,不用同名半套tool占住contract。

給 P7

P7把P3 session_events / messages / rolls / actions,以及P6高階resolved transactions,投影成正式跨Session Timeline,並建立Snapshot / Restore / broader Export。P3 event substrate不等於P7完成。