Require fresh active Agent metadata for approved work

This commit is contained in:
2026-09-30 08:08:39 +08:00
parent 58f23660b8
commit 5da33c0d5b
3 changed files with 44 additions and 0 deletions
@@ -37,6 +37,7 @@
- D 的部署文件读取已独立于旧代次 Schema:Agent 清单仅接受**一个本机端点**;OSS 配置必须匹配本 D 身份、指定本机 HTTPS 目标、15 分钟授权与显式资产上限,凭据只经受控环境变量引用。超大文件、重复/未知字段、多个 Agent、非本机目标、缺失凭据和错误 D 归属均拒绝;JSON 唯一键及凭据引用基础函数已移出旧配置文件。`go test ./internal/config -count=1` 通过。此处仅验证部署数据,不代表 Agent 实际加载、官方 SDK 已签发目标,亦未接入主 `dispatcher` 命令。
- D 的 Agent 会话激活现在必须显式携带规范 UUID v4 的本 D 身份,并写入 Agent 授权绑定;预配置了对应 D 身份的隔离 Agent 会拒绝错误归属,不能再靠 D 自报空身份放行。`go test ./internal/dispatcher -count=1` 通过;主 `dispatcher` 命令尚未使用这条会话链路,不代表本机 D↔A 执行已验收。
- D 的已授权调用元数据现在只从该 Agent **最新且未过期**的会话产生,每次生成不同操作身份;未注册、会话缺失或过期均明确拒绝。`TestAgentCoordinatorProbesBeforeActivation` 覆盖这些边界,不能代替实际 D 主进程的执行与录音联测。
- Agent 增加独立的无代次环境预检:Agent/Cell 与预授权 D 的身份、会话与私有恢复路径、隔离 Mock 场景和已加载 SIP 测试事实、D gRPC 目标及 mTLS 文件/指纹均须显式提供;仅允许本机监听和本机 D 端点,mixed/real 在访问文件或网络前拒绝。缺失项不继承旧 `FromEnv` 默认值,凭据/地址不回显;`go test ./internal/config -run '^TestLoadAgentEnvironment' -count=1` 通过。此检查已接入根命令可达的唯一 Agent Mock 入口;本机双向 TLS 监听可由受信 D 证书探测,另一张同 CA 证书被指纹门禁拒绝。本地测试用批准的 D 身份激活会话,错误 D UUID 即使携带受信证书也在写入会话日志前拒绝;尚未执行呼叫、验证 Agent→D 录音或真实 Asterisk 加载。
- 会话栅栏修复:Agent 重启后 `session_generation=0` 由持久化代际高水位递增,旧显式代际仍拒绝,代际耗尽明确阻断;D 每次新激活产生独立操作身份,同 boot/epoch 的下一次激活不再误回放旧会话。隔离重启/重复激活测试通过,未作为主 Dispatcher 进程重启或真实部署证据。
+24
View File
@@ -253,6 +253,30 @@ func (c *AgentCoordinator) clientAndSession(agentID string) (agentpb.AgentContro
return client, session, nil
}
// ApprovedMeta supplies a fresh operation identity from the currently active
// session. Expired or unregistered sessions cannot authorize new work.
func (c *AgentCoordinator) ApprovedMeta(ctx context.Context, agentID string) (*agentpb.RequestMeta, error) {
if c == nil || ctx == nil || agentID == "" {
return nil, errors.New("registered Agent and request context are required")
}
if err := ctx.Err(); err != nil {
return nil, err
}
_, session, err := c.clientAndSession(agentID)
if err != nil {
return nil, err
}
if session.AgentID != agentID || session.CellID == "" || session.BootID == "" || session.DispatcherEpoch == "" ||
session.SessionGeneration == 0 || session.ExpiresAtUnixMs <= c.now().UnixMilli() {
return nil, errors.New("approved Agent session is incomplete or expired")
}
identity, err := uuid.NewRandom()
if err != nil {
return nil, fmt.Errorf("create Agent operation identity: %w", err)
}
return c.meta(session, "approved:"+identity.String(), ""), nil
}
// AuthorizeInboundMeta accepts an Agent→Dispatcher fact only from the current
// activated Endpoint session. An old boot's durable fact can be replayed, but
// its request metadata must use the newly activated boot and generation.
+19
View File
@@ -68,6 +68,25 @@ func TestAgentCoordinatorProbesBeforeActivation(t *testing.T) {
if first.SessionGeneration != 1 || second.SessionGeneration != 2 || third.SessionGeneration != 3 {
t.Fatalf("a new activation replayed an old session: first=%d second=%d third=%d", first.SessionGeneration, second.SessionGeneration, third.SessionGeneration)
}
one, err := coordinator.ApprovedMeta(context.Background(), "agent-1")
if err != nil {
t.Fatal(err)
}
two, err := coordinator.ApprovedMeta(context.Background(), "agent-1")
if err != nil {
t.Fatal(err)
}
if one.AgentId != "agent-1" || one.CellId != "cell-1" || one.BootId != status.BootId ||
one.DispatcherEpoch != "epoch-2" || one.SessionGeneration != 3 || one.RequestId == two.RequestId || one.OperationId == two.OperationId {
t.Fatal("approved metadata did not bind the latest unique Agent session")
}
if _, err := coordinator.ApprovedMeta(context.Background(), "unknown-agent"); err == nil {
t.Fatal("an unregistered Agent inherited another session")
}
coordinator.now = func() time.Time { return time.Date(2026, 9, 18, 1, 11, 0, 0, time.UTC) }
if _, err := coordinator.ApprovedMeta(context.Background(), "agent-1"); err == nil {
t.Fatal("expired Agent session authorized another operation")
}
}
func TestAgentCoordinatorActivatesExecutesAndControlsWithoutRetryingOriginate(t *testing.T) {