diff --git a/docs/CODING_WORKFLOW.md b/docs/CODING_WORKFLOW.md index 5fe558be2..a1d6517d3 100644 --- a/docs/CODING_WORKFLOW.md +++ b/docs/CODING_WORKFLOW.md @@ -59,7 +59,7 @@ that the current model retained either material. When bootstrap or discovery returns `project_ref`, reuse it as the `project` selector on ordinary Project-scoped calls. The canonical `agent::` identity remains visible for diagnostics and explicit addressing, but the model does not need to mechanically repeat it. A short ref is Server-owned, durable and principal-scoped, carries no authority, and is reauthorized against its pinned canonical Project/root identity on every call. -When `work_on_project`, `start_session`, `session_summary`, or an explicit handoff returns `session_ref`, prefer that short selector for later business `session_id` inputs. The canonical `wc_sess_*` identity remains valid and authoritative. The ref is principal-scoped convenience only: every use resolves the pinned exact Session and reruns the normal Project/Session authorization, lifecycle and guard checks. It does not apply to `recording_session_id` and never creates sticky recorder context. +When `work_on_project`, `start_session`, `session_summary`, or an explicit handoff returns `session_ref`, prefer that short selector for later explicit Session selection. Business `session_id` and wrapper `recording_session_id` remain separate contracts, but either may explicitly carry the already-issued ref: Runtime canonicalizes it to the pinned `wc_sess_*` before the role-specific authorization and lifecycle/guard logic runs. The canonical identity remains valid and authoritative. The ref is principal-scoped convenience only; omission never infers a recorder and no sticky recorder context is created. ## Tool strategy guidance diff --git a/docs/CODING_WORKFLOW.zh-CN.md b/docs/CODING_WORKFLOW.zh-CN.md index 391f2602d..7cd8bd73c 100644 --- a/docs/CODING_WORKFLOW.zh-CN.md +++ b/docs/CODING_WORKFLOW.zh-CN.md @@ -24,7 +24,7 @@ work_on_project 它的 primary output 默认保持紧凑,不重复静态 instruction/workflow 正文;当前模型上下文缺少这些材料时,分别显式请求 `context_request=["project.instructions"]` 和/或 `context_request=["webcodex.workflow"]`。Workflow Session identity 不证明当前模型仍保留这些上下文。 Bootstrap 或 discovery 返回 `project_ref` 后,普通 Project-scoped tool call 的 `project` 应优先复用这个短 selector。Canonical `agent::` 仍保留用于 diagnostic 与显式 addressing,但模型无需机械重复。`project_ref` 由 Server 持久维护、按 principal 隔离,不携带 authority;每次调用都会根据其钉住的 canonical Project/root identity 重新授权。 -当 `work_on_project`、`start_session`、`session_summary` 或显式 handoff 返回 `session_ref` 时,后续 business `session_id` 优先复用这个短 selector。Canonical `wc_sess_*` 仍然有效,并继续作为持久化、审计、诊断和内部关联的权威身份。`session_ref` 只是按 principal 隔离的便利选择器:每次使用都会解析到钉住的 exact Session,并重新执行原有 Project/Session 授权、生命周期和 guard 检查。它不适用于 `recording_session_id`,也不会形成隐式或粘滞的 recorder context。 +当 `work_on_project`、`start_session`、`session_summary` 或显式 handoff 返回 `session_ref` 时,后续显式 Session 选择可优先复用这个短 selector。Business `session_id` 与 wrapper `recording_session_id` 仍是两套独立语义,但都可以显式携带已签发的 ref:Runtime 会先把它还原为钉住的 canonical `wc_sess_*`,再执行各自原有的授权、生命周期或 guard 逻辑。Canonical identity 仍是持久化、审计、诊断和内部关联的权威身份。`session_ref` 只是按 principal 隔离的便利选择器;省略 recorder 时不会自动推断,也不会形成隐式或粘滞的 recorder context。 默认情况下,它还会返回一个很小且有界的 `extensions` selection catalog:Skill metadata 来自 canonical 的 project / Runner-configured `skills.roots` / Runner-managed Skill Store 三类来源;Plugin metadata 只包含 configured working directory 与当前 Project root 匹配、且已 ready/committed 的 provider。该 metadata 不授予任何 authority,也不会自动读取 Skill body 或创建 Plugin binding;模型选择后使用 `skill_read_file` 读取 Skill 文本,`run_skill_resource` 只执行可信 Runner-configured live `scripts/` resource(由 `expected_definition_revision` fence definition)或 Runner-installed managed resource(另由 `expected_package_revision` fence package),Plugin 则走 `plugin_tool describe -> call`。Configured resource bytes 会一直保持 live 到实际执行时,并不会预先被 package revision 固定。只有当前模型上下文仍明确保留这些 discovery metadata 时,才应设置 `include_extension_catalog=false`。 ## 工具策略 guidance diff --git a/docs/agent/session-model.md b/docs/agent/session-model.md index 052b5f992..45731c4a0 100644 --- a/docs/agent/session-model.md +++ b/docs/agent/session-model.md @@ -73,13 +73,13 @@ Workflow Session lifecycle is independent from the durable `wc_goal_*` Goal doma | ID form | Canonical `wc_sess_*` (`SESSION_ID_PREFIX`); model-facing bootstrap/discovery/handoff may additionally issue principal-scoped `session_ref` values such as `~s1` | | Business field | `session_id` on tools that take a workflow session as input; model-facing business calls may pass either the canonical id or a Server-issued `session_ref` | | Coding resume field | `session_id` on canonical external `work_on_project`; the internal startup primitive's `resume_session_id` is not a wire/API field | -| Recorder field | `recording_session_id` on generic wrappers, including the stateless MCP 2026 tool-argument projection (metadata only; canonical `wc_sess_*` only and stripped before concrete tool dispatch) | +| Recorder field | `recording_session_id` on generic wrappers, including the stateless MCP 2026 tool-argument projection (metadata only; accepts canonical `wc_sess_*` or an explicitly supplied principal-scoped `session_ref`, then canonicalizes before recorder authorization and concrete dispatch) | ### Storage and ownership A `session_ref` is only a short model selector. The Server owns a durable mapping scoped to the authenticated principal and pins the ref to one exact canonical Workflow Session incarnation. Resolving it produces the canonical `wc_sess_*` before business authorization/dispatch; the ordinary Project visibility, Session authority, lifecycle, and guard checks then run unchanged. The ref is not a credential, bearer capability, recorder identity, or "current/recent Session" inference. If the pinned Session disappears or its exact incarnation cannot be proven, the old ref fails closed and is never recycled or silently retargeted. Canonical Session ids remain authoritative for persistence, audit, diagnostics, internal joins, and explicit API/CLI consumers. -`recording_session_id` remains a separate provenance contract. A `session_ref` never creates sticky recorder context and is not accepted as an implicit recorder selector. +`recording_session_id` remains a separate provenance contract. It may explicitly carry the same principal-scoped `session_ref` returned for that exact Session; Runtime canonicalizes the ref before recorder authorization and durable provenance recording. This does not make recorder provenance a business Session target, and omission never selects a recorder implicitly or creates sticky recorder context. Stateless MCP 2026 never derives a Workflow Session recorder identity from transport/window continuity. ChatGPT may supply `_meta["openai/session"]` as a hashed `ClientWindow`, but that identity is intentionally not a Workflow Session selector or trusted provenance source. Its `tools/list` schema therefore projects `recording_session_id` as explicit wrapper metadata for runtime tools. A call may carry `recording_session_id=W` while the concrete tool body carries business `session_id=C`; the MCP adapter removes the recorder field before concrete parsing and the kernel independently authorizes `W` before it can record evidence or supply trusted collaboration provenance. This does not revive legacy `mcp-session-id`, grant target authority, or infer a recorder from credentials, project identity, connection state, or `ClientWindow`. @@ -139,7 +139,7 @@ with its canonical authority and replay fences; standalone tools remain valid. Executable todo completion is assignment-fenced in 0.4. A worker first calls `get_session_assignment` for the exact coordinator `session_id + message_id`; one atomic store snapshot returns the open todo, every retained direct reply within the bound, and an opaque Session/todo-bound `assignment_fence`. Current `complete_session_message` requests require that exact token as `expected_assignment_fence` together with the independent caller `completion_key`. Assignment-local semantic changes stale the fence before mutation; unrelated Session traffic and ACK bookkeeping do not. A stale result has `state_changed=false` and includes the current assignment plus a fresh durable fence only when that exact current state remains provable. Retention loss or an oversized direct-reply set is non-completable from stale context. -The fence and completion key are different identity domains: the fence proves the semantic assignment snapshot, while the completion key correlates one accepted intent across uncertain retries. `ack_session_message_ids` and compact Session `ack_ref` are request-scoped ACK evidence; message-observation tokens are a separate cursor domain. `session_ref` is only a principal-scoped business Session selector and never ACK evidence. None substitutes for assignment/completion identity. Business `session_id` remains the independently authorized target; `recording_session_id` remains provenance only and never supplies business guards or execution defaults. Current persisted rows must carry the assignment-history and completion-fence tracking metadata required by the v2 format; restore never invents a missing fence, fingerprint, or tracking state, and a row that cannot prove the current shape is discarded rather than admitted for replay. +The fence and completion key are different identity domains: the fence proves the semantic assignment snapshot, while the completion key correlates one accepted intent across uncertain retries. `ack_session_message_ids` and compact Session `ack_ref` are request-scoped ACK evidence; message-observation tokens are a separate cursor domain. `session_ref` is a principal-scoped exact-Session selector and never ACK evidence. The field carrying it determines the role: business `session_id` remains the independently authorized target, while `recording_session_id` remains provenance only and never supplies business guards or execution defaults. None substitutes for assignment/completion identity. Current persisted rows must carry the assignment-history and completion-fence tracking metadata required by the v2 format; restore never invents a missing fence, fingerprint, or tracking state, and a row that cannot prove the current shape is discarded rather than admitted for replay. ### Explicit handoff recovery and internal snapshot fencing diff --git a/docs/agent/tool-contract-guidelines.md b/docs/agent/tool-contract-guidelines.md index 5509ccea2..2a75a100c 100644 --- a/docs/agent/tool-contract-guidelines.md +++ b/docs/agent/tool-contract-guidelines.md @@ -181,7 +181,7 @@ Runtime Project identity remains canonical as `agent::`. Resolving a `project_ref` must always look up the pinned canonical identity and then run the ordinary current Project resolution/authorization path again. The ref is not a credential, bearer token or capability. If the canonical Project disappears, becomes invisible, loses stable identity, or the same canonical address is later registered for a different root, the old ref fails closed. Never recycle or silently retarget an issued ref. Discovery/bootstrap may expose both `project_ref` and canonical identity; ordinary hot-path results should not repeat them when no model decision depends on that duplication. -The same typed durable-reference layer may issue a principal-scoped `session_ref` such as `~s1` for one exact Workflow Session incarnation. Canonical `wc_sess_*` remains authoritative for Session persistence, audit, diagnostics and internal joins. Business `session_id` inputs may use the short selector, but Runtime canonicalizes it before Session-aware authorization/dispatch and then reruns the ordinary Project visibility, Session authority, lifecycle and guard checks. `recording_session_id` is a separate recorder-provenance contract and remains canonical-only; a Session ref never creates ambient or sticky recorder state. Bootstrap, discovery and handoff may expose both identities, while ordinary hot-path results should avoid redundant duplication. +The same typed durable-reference layer may issue a principal-scoped `session_ref` such as `~s1` for one exact Workflow Session incarnation. Canonical `wc_sess_*` remains authoritative for Session persistence, audit, diagnostics and internal joins. Business `session_id` and wrapper `recording_session_id` remain separate semantic roles, but either may explicitly carry an already-issued Session ref. Runtime canonicalizes the selector before the role-specific authorization/dispatch path: business targeting still reruns Project visibility, Session authority, lifecycle and guards, while recorder provenance still reruns its independent recorder authorization and never supplies business authority. A Session ref never creates ambient or sticky recorder state. Bootstrap, discovery and handoff may expose both identities, while ordinary hot-path results should avoid redundant duplication. ### Model-projection deletion test diff --git a/src/mcp/discovery.rs b/src/mcp/discovery.rs index c4a514096..f97d46e12 100644 --- a/src/mcp/discovery.rs +++ b/src/mcp/discovery.rs @@ -3,6 +3,8 @@ use serde_json::Value; +use super::tools::RECORDING_SESSION_SELECTOR_SCHEMA_PATTERN; + // MCP discovery targets, independent of GPT Actions' importer limits. Keep // purpose, the nearest selection boundary, and essential continuation guidance. pub(super) const TOOL_DESCRIPTION_MAX_CHARS: usize = 420; @@ -45,7 +47,7 @@ pub(super) fn compact_tool(tool: &mut Value) { // session_id keeps its own canonical-derived copy and requiredness. // Nested IDs/resolution have no description; keep their type hints here. for (pointer, description) in [ - ("/properties/recording_session_id", "Optional explicit wc_sess_* recorder for one exact Workflow Session; never execution/business authority. If omitted, authorized same-Window affinity may still deliver and ACK Session collaboration without recording."), + ("/properties/recording_session_id", "Optional explicit recorder for one exact Workflow Session (wc_sess_* or issued ~sN); never execution/business authority. Omission may still allow authorized same-Window attention without recording."), ("/properties/ack_session_message_ids", "ACK-required wc_msg_* IDs retained in model context; Session uses recorder/window affinity; never resolves or authorizes."), ("/properties/ack_ref", "Compact exact Session ACK-set evidence returned in session_attention; Session-only, request-scoped, non-authoritative, and never resolves messages."), ("/properties/session_message_resolution", "Resolve one handled non-todo recorder message by exact wc_msg_*; ACK separately if required. Independent of call success."), @@ -97,7 +99,7 @@ fn compact_discovery_validation_annotations(schema: &mut Value) { for (pointer, pattern) in [ ( "/properties/recording_session_id", - "^wc_sess_([A-Za-z0-9_-]{16}|[0-9a-f]{32})$", + RECORDING_SESSION_SELECTOR_SCHEMA_PATTERN, ), ( "/properties/ack_session_message_ids/items", diff --git a/src/mcp/tools.rs b/src/mcp/tools.rs index 46a62a0f5..de9393045 100644 --- a/src/mcp/tools.rs +++ b/src/mcp/tools.rs @@ -558,6 +558,9 @@ fn insert_stateless_collaboration_ack_property(properties: &mut serde_json::Map< ); } +pub(super) const RECORDING_SESSION_SELECTOR_SCHEMA_PATTERN: &str = + "^(~s[1-9][0-9]{0,19}|wc_sess_([A-Za-z0-9_-]{16}|[0-9a-f]{32}))$"; + pub(super) fn add_stateless_workflow_recorder_metadata(payload: &mut Value) { let Some(tools) = payload.get_mut("tools").and_then(Value::as_array_mut) else { return; @@ -582,8 +585,8 @@ pub(super) fn add_stateless_workflow_recorder_metadata(payload: &mut Value) { crate::tool_runtime::sessions::TOOL_CALL_RECORDING_SESSION_ID_FIELD.to_string(), json!({ "type": "string", - "pattern": "^wc_sess_([A-Za-z0-9_-]{16}|[0-9a-f]{32})$", - "description": "Optional explicit recorder provenance for one exact Workflow Session. Never execution authority or a business Session target. When omitted, authorized same-Window affinity may still deliver and ACK Session collaboration without recording this call." + "pattern": RECORDING_SESSION_SELECTOR_SCHEMA_PATTERN, + "description": "Optional explicit recorder provenance for one exact Workflow Session; accepts canonical wc_sess_* or issued principal-scoped ~sN. Never execution/business authority. Omission may still allow authorized same-Window attention without recording." }), ); insert_stateless_collaboration_ack_property(properties); @@ -1445,6 +1448,15 @@ pub(super) fn strip_recording_session_id(arguments: &mut Value) -> Result