# 模块调用上下文

了解模块方法当前可以读取的 Runtime、调用方、用户和请求上下文字段。

模块调用上下文是平台在解析并校验一次模块方法调用时生成的 JSON 对象。它与页面或调用方提交的业务参数
相互独立，用于向方法实现提供当前 Runtime、调用方、访问用户和请求路由信息。

模块不能假定所有字段在所有入口都存在。方法只应读取本页列出的稳定字段，并根据调用入口决定哪些字段
必须存在。没有建立对应身份的调用不会伪造默认值，也不会从请求体中的同名字段回退。

## 稳定字段

| 字段                     | 当前类型               | 何时存在                      | 含义                                                                          |
| ---------------------- | ------------------ | ------------------------- | --------------------------------------------------------------------------- |
| `applicationRuntimeId` | `string`           | 所有已经解析到 Runtime 的模块后端调用   | 当前应用 Runtime ID。平台根据实际执行的 Runtime 写入，并覆盖调用入口提供的同名上下文值。                      |
| `callerBusinessId`     | `string`           | 已验证出调用方业务身份时              | 调用当前方法的源模块或服务 `businessId`。模块通过服务引用调用另一模块时使用该字段区分调用方；直接用户调用或没有业务身份的入口可能不存在。 |
| `targetBusinessId`     | `string`           | 按模块或服务 `businessId` 路由方法时 | 当前被调用模块或服务的 `businessId`。按全局方法名解析时不存在。                                      |
| `targetMethodName`     | `string`           | 已成功解析方法时                  | 当前目标方法名。                                                                    |
| `userId`               | `number \| string` | 当前入口完成用户身份校验并返回用户时        | 当前平台用户 ID。未登录、可选认证未建立身份或服务调用未携带用户身份时不存在。                                    |
| `workspaceId`          | `number \| string` | 当前入口完成工作区身份校验时            | 当前可信工作区 ID。它不会仅根据请求参数或请求体中的同名字段生成。                                          |
| `httpMethod`           | `string`           | HTTP 入口                   | 进入 Runtime 的 HTTP 方法，例如 `GET` 或 `POST`。                                     |
| `requestPath`          | `string`           | HTTP 入口                   | 进入 Runtime 的原始请求路径。                                                         |

`number | string` 表示当前不同鉴权入口可能保留数字 ID，也可能保留字符串形式。ID 用作比较、存储键或
HTTP Header 前应先规范化为非空字符串，不要进行算术运算。

`userId` 是目标方法可读取的已验证上下文字段。外部模块后端继续通过 ServiceReference 调用下游服务时，
不能把这个数字重新放进业务参数或自定义用户 Header；应转发 Runtime 为本次请求注入的不透明
`X-Runtime-Actor-Assertion`。该 assertion 不属于本表的 JSON 上下文字段，也不由 `methodBody` 映射，完整
规则见 [Runtime 用户委托](/development/bundle-development/module-development/actor-delegation/)。

> **Runtime 与模块级隔离**
>
> 需要按 Runtime 和调用模块隔离数据时，同时把 `applicationRuntimeId` 和 `callerBusinessId` 声明为
> 必需上下文。若数据还属于工作区，再同时要求 `workspaceId`。任一必需字段缺失时应拒绝调用，不能回退
> 到仅工作区、仅 Runtime 或公共目录。

## 映射到 HTTP 请求

上下文不会自动完整转发给模块后端。HTTP 方法必须在 `methodBody` 中显式选择字段，并映射到 Header、
Path、Query 或 Body。以下示例把 Runtime、调用模块、工作区和可选用户映射为模块自己的业务 Header：

```json
{
  "header": {
    "X-Module-Runtime-Id": {
      "position": "context",
      "key": "applicationRuntimeId",
      "required": true
    },
    "X-Module-Caller-Business-Id": {
      "position": "context",
      "key": "callerBusinessId",
      "required": true
    },
    "X-Module-Workspace-Id": {
      "position": "context",
      "key": "workspaceId",
      "required": true
    },
    "X-Module-User-Id": {
      "position": "context",
      "key": "userId",
      "required": false
    }
  }
}
```

`position: "context"` 按 `key` 精确读取上下文字段。输出字段名与上下文键不同时必须写 `key`；省略
`key` 时使用输出字段名查找。`required: true` 的字段缺失会终止调用，不应使用固定值或更宽作用域代替。

需要转换字段或组装请求体时可以使用表达式：

```json
{
  "body": {
    "runtimeId": {
      "expression": "context.applicationRuntimeId",
      "required": true
    },
    "callerBusinessId": {
      "expression": "context.callerBusinessId",
      "required": true
    }
  }
}
```

表达式中的 `context` 只包含当前调用已经建立的字段。不要在表达式中从业务参数回退身份字段。

## Rust / Axum 接入

Rust 后端不会自动获得一个 Runtime SDK 对象、请求扩展或完整上下文 JSON。Runtime 只按当前模块方法的
`methodBody` 映射发送选中的字段；Rust 服务读取的是映射后的普通 HTTP Header、Path、Query 或 Body。
因此，Rust 代码中的字段名由模块自己的 HTTP 契约决定；除非映射时刻意使用同名输出字段，否则不要假定
`applicationRuntimeId` 等网关内部键会直接出现在服务请求中。

使用上面的 Header 映射时，Axum 可以在 HTTP 入口立即转换成类型化上下文：

```rust
use axum::{
    http::{HeaderMap, StatusCode},
    response::{IntoResponse, Response},
};

#[derive(Debug)]
struct RuntimeCallContext {
    application_runtime_id: String,
    caller_business_id: String,
    workspace_id: String,
    user_id: Option<String>,
}

#[derive(Debug)]
enum ContextError {
    Missing(&'static str),
    Invalid(&'static str),
}

impl IntoResponse for ContextError {
    fn into_response(self) -> Response {
        let message = match self {
            Self::Missing(name) => format!("missing required runtime context: {name}"),
            Self::Invalid(name) => format!("invalid runtime context: {name}"),
        };
        (StatusCode::BAD_REQUEST, message).into_response()
    }
}

fn read_header(
    headers: &HeaderMap,
    name: &'static str,
) -> Result<Option<String>, ContextError> {
    let Some(value) = headers.get(name) else {
        return Ok(None);
    };
    let value = value.to_str().map_err(|_| ContextError::Invalid(name))?;
    let value = value.trim();
    if value.is_empty() {
        return Err(ContextError::Invalid(name));
    }
    Ok(Some(value.to_owned()))
}

fn required_header(
    headers: &HeaderMap,
    name: &'static str,
) -> Result<String, ContextError> {
    read_header(headers, name)?.ok_or(ContextError::Missing(name))
}

impl TryFrom<&HeaderMap> for RuntimeCallContext {
    type Error = ContextError;

    fn try_from(headers: &HeaderMap) -> Result<Self, Self::Error> {
        Ok(Self {
            application_runtime_id: required_header(headers, "x-module-runtime-id")?,
            caller_business_id: required_header(headers, "x-module-caller-business-id")?,
            workspace_id: required_header(headers, "x-module-workspace-id")?,
            user_id: read_header(headers, "x-module-user-id")?,
        })
    }
}

async fn handle_module_call(headers: HeaderMap) -> Result<StatusCode, ContextError> {
    let context = RuntimeCallContext::try_from(&headers)?;

    // 从这里开始，业务逻辑只使用类型化 context，不再直接读取 HeaderMap。
    let _ = (
        context.application_runtime_id,
        context.caller_business_id,
        context.workspace_id,
        context.user_id,
    );
    Ok(StatusCode::NO_CONTENT)
}
```

映射到 Header 后，Runtime 会把数字或字符串形式的 ID 都序列化为 Header 字符串；上面的 Rust 类型统一
使用非空 `String`，不会对 ID 做算术运算。可选的 `userId` 未建立时不会发送对应 Header，Rust 中保持
`None`。如果业务要求当前用户，应该同时把 `X-Module-User-Id` 的 `required` 改为 `true`，并把
`user_id` 改为必需字段。

> **上下文不是 Endpoint 凭据**
>
> `X-Module-*` 是模块自己定义的 HTTP 字段，不是平台凭据。只有请求确实经过受控 Runtime 调用链，并且
> Endpoint 已按[环境配置与服务鉴权](/development/backend-development/configuration-and-auth/)限制直接访问时，
> 这些映射值才能作为该次 Runtime 调用的上下文使用。对外公开的 Endpoint 不能只凭这些 Header 完成鉴权。

> **Actor Header 不是方法自定义 Header**
>
> 模块定义不得声明、固定或通过表达式生成 `X-Runtime-Actor-Assertion`。Runtime 只在已建立最终用户身份时
> 注入该请求级值；外部后端只把收到的值用于当前同步调用链，不解析、不记录、不持久化。

## 信任与缺失规则

- `params`、Query、普通 Header 和请求体都是业务输入；其中的同名字段不会自动成为可信上下文。
- `applicationRuntimeId` 来自当前实际执行的 Runtime，不由页面或模块自行选择。
- `callerBusinessId` 只在平台验证出调用方业务身份后存在；不能由目标模块根据请求内容猜测。
- `userId`、`workspaceId` 只代表当前入口已经建立的身份。字段缺失表示该身份没有建立，不表示值为 `0`、空字符串或系统管理员。
- 外部服务转发的 Actor assertion 必须经过 Runtime 验证；它不能由普通 Header、请求体或已保存的
  `userId` 重新构造。服务链进入新的目标后，下一跳 assertion 由 Runtime 自动轮换。
- 方法依赖某个字段时应声明 `required: true` 并失败关闭。不要设置固定管理员、默认工作区、公共 Runtime 或其他兼容回退。
- 日志和错误信息可以记录字段是否存在，但不应输出完整身份声明、凭据或敏感模块属性。
