Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,53 @@ jobs:
- name: Checkout
uses: actions/checkout@v7

- name: Detect dependency graph capability
id: dependency-graph
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
response_file="${RUNNER_TEMP}/dependency-graph-response.json"
status="$({ curl --silent --show-error \
--output "${response_file}" \
--write-out '%{http_code}' \
--header 'Accept: application/vnd.github+json' \
--header "Authorization: Bearer ${GH_TOKEN}" \
--header 'X-GitHub-Api-Version: 2022-11-28' \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/dependency-graph/sbom"; } || true)"

if [[ "${status}" == '200' ]]; then
echo 'enabled=true' >> "${GITHUB_OUTPUT}"
exit 0
fi

message="$(jq -r '.message // empty' "${response_file}" 2>/dev/null || true)"
if [[ "${status}" == '404' ]]; then
echo 'enabled=false' >> "${GITHUB_OUTPUT}"
echo "::warning::仓库 Dependency Graph/SBOM 接口不可用(HTTP 404)。Dependency Review 已跳过;启用 Dependency Graph 后会自动恢复漏洞门禁。"
exit 0
fi
if [[ "${status}" == '403' \
&& "${message,,}" == *'dependency graph'* \
&& "${message,,}" == *'not enabled'* ]]; then
echo 'enabled=false' >> "${GITHUB_OUTPUT}"
echo "::warning::${message} Dependency Review 已跳过;启用仓库 Dependency Graph 后会自动恢复漏洞门禁。"
exit 0
fi

echo "Dependency Graph capability probe failed with HTTP ${status}: ${message}" >&2
cat "${response_file}" >&2 || true
exit 1

- name: Review dependency changes
if: steps.dependency-graph.outputs.enabled == 'true'
uses: actions/dependency-review-action@v5
with:
fail-on-severity: high
fail-on-scopes: 'runtime, development'
comment-summary-in-pr: always

- name: Report unavailable dependency review
if: steps.dependency-graph.outputs.enabled != 'true'
run: echo 'Dependency Graph 未启用或 SBOM 接口不可用;本次没有运行依赖差异漏洞分析。'
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,8 +480,10 @@ WS 控制面必须使用命令注册表,而不是单个 `handleMessage()` 大

### 6.1 核心原则

- OpenAI Agents SDK `Runner` 是单次 run 内唯一的 Agent 编排状态机。不得在平台层复制 turn loop、handoff loop、工具并发调度或创建并行 Runner
- SDK `RunState` 是唯一恢复载荷;`AgentState` 只保存平台工作流、审批、UI 和审计投影,不能反向驱动一套重复的 Agent 状态机
- 地理智能平台 `RunEngine` 是持久 Run、Turn、Runner segment、输入邮箱、终态竞争和 child Run 生命周期的唯一控制面;这些领域事实写入 PostgreSQL,不从 SDK Session 或 UI 反推
- OpenAI Agents SDK `Runner` 是单个 Runner segment 内唯一的模型、工具、handoff 与 Agent-as-tool 微循环。平台不得复制原始 Responses/function-call loop
- 同一 Run 任一时刻最多有一个活动 Runner segment;不同持久 child Run 只能在根控制面的深度、并发和预算限制内并行
- SDK `RunState` 只保存单个 Runner segment 的公开恢复载荷;`AgentState`/后续 reducer snapshot 保存平台工作流、审批、UI 和审计状态,不得解析 SDK checkpoint 的内部 JSON 布局
- PostgreSQL Transcript Entry、Conversation Item、Run Event、Run Input、Workflow、Approval 与 Tool Value 是运行历史事实;内存索引和前端状态都是可重建投影
- 内容寻址存储中的 SDK checkpoint 只保存恢复载荷;checkpoint 的 run 归属、schema 版本、SDK 版本、运行配置摘要和状态必须由 PostgreSQL 记录约束
- 运行时入口是 facade。SDK 装配、SDK 执行、上下文预算和平台投影必须保持独立职责,调用方不得穿透这些边界
Expand Down Expand Up @@ -529,7 +531,8 @@ WS 控制面必须使用命令注册表,而不是单个 `handleMessage()` 大

### 6.6 多智能体与工具并发

- 返回主智能体的子智能体使用 SDK `Agent.asTool()`,转交所有权使用 SDK `handoff()`;不得增加自定义 batch 工具、手动并行 Runner 或第二套工具调度队列
- 短小且同步返回父智能体的任务使用 SDK `Agent.asTool()`,同一 Run 内的对话所有权转移使用 SDK `handoff()`;需要独立追问、取消、恢复、预算或并行的长任务使用持久 child Run
- 同一 Run 禁止并行启动多个 Runner;不同 child Run 的 Runner 由根控制面统一限额,父子通信必须经过持久消息与输入邮箱,不共享可变 RunState
- 无副作用、非破坏、免审批的只读工具默认进入共享并发通道;确有线程不安全实现时用 `parallelSafe=false` 显式退出。写工具、审批工具和无法证明只读的能力进入独占通道
- 写工具、审批工具、MCP 工具和不能证明安全的子智能体始终独占执行;安全声明不是对权限、审批和审计的豁免
- 并发数量由 SDK Runner 的工具并发上限约束;地理智能平台 执行闸门只实施业务安全约束,不负责重新调度模型返回的调用
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@geo-agent-platform/shared-types": "file:../../packages/shared-types",
"@hono/node-server": "^2.1.1",
"@hono/zod-validator": "^0.9.0",
"@openai/agents": "0.15.0",
"@openai/agents": "0.16.1",
"@turf/turf": "^7.4.0",
"ajv": "^8.20.0",
"better-auth": "1.6.29",
Expand Down
27 changes: 5 additions & 22 deletions apps/server/src/agent/agentsCheckpointService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,50 +12,33 @@
import { describe, expect, it } from 'vitest'

import { SDK_STATE_SCHEMA_VERSION } from './agentsRuntimeMetadata.js'
import {
assertCheckpointCompatibility,
toolCallResultIdsFromSerializedState,
} from './agentsCheckpointService.js'
import { assertCheckpointCompatibility } from './agentsCheckpointService.js'

const validCheckpoint = {
orchestrationEngine: 'openai_agents',
sdkStateSchemaVersion: SDK_STATE_SCHEMA_VERSION,
agentsSdkVersion: '0.11.8',
agentsSdkVersion: '0.16.1',
runtimeConfigDigest: 'sha256:config',
}

describe('Agents checkpoint compatibility', () => {
it('仅从已序列化的 function_call_result 提取可关闭账本的 callId', () => {
expect(toolCallResultIdsFromSerializedState(JSON.stringify({
context: {
untrusted: { type: 'function_call_result', callId: 'call_forged' },
},
generatedItems: [
{ type: 'tool_call_item', rawItem: { type: 'function_call', callId: 'call_pending' } },
{ type: 'tool_call_output_item', rawItem: { type: 'function_call_result', callId: 'call_done' } },
{ type: 'handoff_output_item', rawItem: { type: 'function_call_result', callId: 'call_handoff' } },
{ type: 'tool_call_output_item', rawItem: { type: 'function_call_result', callId: 'call_done' } },
],
}))).toEqual(['call_done', 'call_handoff'])
})

it('接受完全匹配的 SDK 检查点', () => {
expect(() => assertCheckpointCompatibility(validCheckpoint, {
runId: 'run_1',
sdkVersion: '0.11.8',
sdkVersion: '0.16.1',
configDigest: 'sha256:config',
})).not.toThrow()
})

it.each([
[{ ...validCheckpoint, orchestrationEngine: null }, '不是 OpenAI Agents SDK 检查点'],
[{ ...validCheckpoint, sdkStateSchemaVersion: null }, 'SDK 状态 schema 不匹配'],
[{ ...validCheckpoint, agentsSdkVersion: '0.11.7' }, 'SDK 版本不匹配'],
[{ ...validCheckpoint, agentsSdkVersion: '0.15.0' }, 'SDK 版本不匹配'],
[{ ...validCheckpoint, runtimeConfigDigest: 'sha256:other' }, '运行配置已变化'],
])('拒绝不兼容检查点', (checkpoint, message) => {
expect(() => assertCheckpointCompatibility(checkpoint, {
runId: 'run_1',
sdkVersion: '0.11.8',
sdkVersion: '0.16.1',
configDigest: 'sha256:config',
})).toThrow(message)
})
Expand Down
34 changes: 2 additions & 32 deletions apps/server/src/agent/agentsCheckpointService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { Agent, RunContext, RunState } from '@openai/agents'

import type { AgentsExecutionContext } from './agentsToolBridge.js'
import { toolCallResultIdsFromHistory } from './agentsSdkStateBoundary.js'
import { SDK_STATE_SCHEMA_VERSION } from './agentsRuntimeMetadata.js'
import type { RunSteeringRecord } from '../schemas/types.js'
import type { AgentRuntimeStore } from '../store/runtimePorts.js'
Expand Down Expand Up @@ -41,7 +42,7 @@ export class AgentsCheckpointService {
inputLeaseId: string | null = null,
): Promise<AgentsCheckpointCommit> {
const serializedState = state.toString()
const terminalToolCallIds = toolCallResultIdsFromSerializedState(serializedState)
const terminalToolCallIds = toolCallResultIdsFromHistory(state.history)
const acknowledgedInputs = await this.store.saveAgentsSdkState(runId, serializedState, {
agentsSdkVersion: metadata.sdkVersion,
runtimeConfigDigest: metadata.configDigest,
Expand Down Expand Up @@ -77,37 +78,6 @@ export class AgentsCheckpointService {
}
}

/**
* 只有已进入当前可恢复 RunState 的 function_call_result 才能关闭
* 工具副作用账本。平台 tool result/transcript 先落盘不足以证明 SDK
* 恢复点不会重放该调用。
*/
export function toolCallResultIdsFromSerializedState(serializedState: string): string[] {
const parsed: unknown = JSON.parse(serializedState)
const callIds = new Set<string>()
if (isRecord(parsed) && Array.isArray(parsed.generatedItems)) {
collectFunctionCallResults(parsed.generatedItems, callIds)
}
return [...callIds]
}

function collectFunctionCallResults(value: unknown, callIds: Set<string>): void {
if (Array.isArray(value)) {
for (const item of value) collectFunctionCallResults(item, callIds)
return
}
if (!isRecord(value)) return
if (value.type === 'function_call_result') {
const callId = value.callId
if (typeof callId === 'string' && callId) callIds.add(callId)
}
for (const nested of Object.values(value)) collectFunctionCallResults(nested, callIds)
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}

export function assertCheckpointCompatibility(
checkpoint: {
orchestrationEngine: string | null
Expand Down
27 changes: 27 additions & 0 deletions apps/server/src/agent/agentsRuntimeMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// +-------------------------------------------------------------------------
//
// 地理智能平台 - Agents SDK 运行元数据测试
//
// 文件: agentsRuntimeMetadata.test.ts
//
// 日期: 2026年08月18日
// 作者: JamesLinYJ
// 协助: OpenAI Codex:GPT-5.6 Pro
// --------------------------------------------------------------------------

import { describe, expect, it } from 'vitest'

import {
agentsSdkVersion,
assertAgentsSdkVersionSupported,
SUPPORTED_AGENTS_SDK_VERSION,
} from './agentsRuntimeMetadata.js'

describe('Agents SDK runtime metadata', () => {
it('锁定经过契约验证的 SDK 版本', async () => {
const installed = await agentsSdkVersion()
expect(installed).toBe(SUPPORTED_AGENTS_SDK_VERSION)
expect(() => assertAgentsSdkVersionSupported(installed)).not.toThrow()
expect(() => assertAgentsSdkVersionSupported('0.15.0')).toThrow('不支持的')
})
})
9 changes: 9 additions & 0 deletions apps/server/src/agent/agentsRuntimeMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '../schemas/types.js'

export const SDK_STATE_SCHEMA_VERSION = AGENTS_SDK_STATE_SCHEMA_VERSION
export const SUPPORTED_AGENTS_SDK_VERSION = '0.16.1'

let versionPromise: Promise<string> | null = null

Expand All @@ -26,6 +27,14 @@ export function agentsSdkVersion(): Promise<string> {
return versionPromise
}

export function assertAgentsSdkVersionSupported(version: string): void {
if (version !== SUPPORTED_AGENTS_SDK_VERSION) {
throw new Error(
`不支持的 @openai/agents 版本 '${version}';要求 '${SUPPORTED_AGENTS_SDK_VERSION}'`,
)
}
}

export function runtimeConfigDigest(config: AgentRuntimeConfig): string {
return createHash('sha256').update(stableStringify(config)).digest('hex')
}
Expand Down
51 changes: 51 additions & 0 deletions apps/server/src/agent/agentsSdkBoundaryArchitecture.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// +-------------------------------------------------------------------------
//
// 地理智能平台 - Agents SDK 防腐边界架构守卫
//
// 文件: agentsSdkBoundaryArchitecture.test.ts
//
// 日期: 2026年08月18日
// 作者: JamesLinYJ
// 协助: OpenAI Codex:GPT-5.6 Pro
// --------------------------------------------------------------------------

import { readdir, readFile } from 'node:fs/promises'
import path from 'node:path'
import { describe, expect, it } from 'vitest'

describe('Agents SDK anti-corruption boundary', () => {
it('把 SDK 内部状态接触限制在单一防腐文件,并禁止解析 checkpoint 内部布局', async () => {
const sourceRoot = path.join(process.cwd(), 'src')
const files = await collectProductionTypescript(sourceRoot)
const internalInputToken = '.' + '_' + 'originalInput'
const generatedItemsToken = 'generated' + 'Items'
const allowedInternalInputFile = 'agent/agentsSdkStateBoundary.ts'

for (const file of files) {
const relative = path.relative(sourceRoot, file).replace(/\\/gu, '/')
const source = await readFile(file, 'utf8')
if (source.includes(internalInputToken)) {
expect(relative).toBe(allowedInternalInputFile)
}
expect(source.includes(generatedItemsToken), relative).toBe(false)
expect(
/JSON\.parse\([^)]*\.toString\(\)/u.test(source),
relative,
).toBe(false)
}
})
})

async function collectProductionTypescript(root: string): Promise<string[]> {
const output: string[] = []
for (const entry of await readdir(root, { withFileTypes: true })) {
const absolute = path.join(root, entry.name)
if (entry.isDirectory()) {
output.push(...await collectProductionTypescript(absolute))
continue
}
if (!entry.isFile() || !entry.name.endsWith('.ts') || entry.name.endsWith('.test.ts')) continue
output.push(absolute)
}
return output
}
99 changes: 99 additions & 0 deletions apps/server/src/agent/agentsSdkStateBoundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// +-------------------------------------------------------------------------
//
// 地理智能平台 - Agents SDK 状态防腐边界测试
//
// 文件: agentsSdkStateBoundary.test.ts
//
// 日期: 2026年08月18日
// 作者: JamesLinYJ
// 协助: OpenAI Codex:GPT-5.6 Pro
// --------------------------------------------------------------------------

import type { AgentInputItem } from '@openai/agents'
import { describe, expect, it } from 'vitest'

import {
stageRunInputsInSdkState,
toolCallResultIdsFromHistory,
type AgentsSdkSerializableInputState,
} from './agentsSdkStateBoundary.js'

describe('Agents SDK state boundary', () => {
it('幂等接纳带持久 sequence 的 steering 输入', () => {
const state: AgentsSdkSerializableInputState = { _originalInput: 'initial' }
const item = runInput('run_1', 1, '继续分析')

stageRunInputsInSdkState(state, 'run_1', [item])
stageRunInputsInSdkState(state, 'run_1', [structuredClone(item)])

expect(state._originalInput).toEqual([
{ type: 'message', role: 'user', content: 'initial' },
item,
])
})

it('拒绝跨 Run、缺 marker 和同 sequence 不同内容', () => {
const state: AgentsSdkSerializableInputState = { _originalInput: [] }
expect(() => stageRunInputsInSdkState(
state,
'run_1',
[runInput('run_2', 1, 'cross-run')],
)).toThrow('缺少可序列化 marker')

expect(() => stageRunInputsInSdkState(state, 'run_1', [{
type: 'message',
role: 'user',
content: 'missing marker',
}])).toThrow('缺少可序列化 marker')

stageRunInputsInSdkState(state, 'run_1', [runInput('run_1', 1, 'first')])
expect(() => stageRunInputsInSdkState(
state,
'run_1',
[runInput('run_1', 1, 'changed')],
)).toThrow('内容不一致')
})

it('只从公开 history 中提取 function call 结果', () => {
const history: AgentInputItem[] = [
{
type: 'function_call',
name: 'lookup',
callId: 'call_pending',
status: 'completed',
arguments: '{}',
},
{
type: 'function_call_result',
name: 'lookup',
callId: 'call_done',
status: 'completed',
output: { type: 'text', text: 'ok' },
},
{
type: 'function_call_result',
name: 'lookup',
callId: 'call_done',
status: 'completed',
output: { type: 'text', text: 'same' },
},
]

expect(toolCallResultIdsFromHistory(history)).toEqual(['call_done'])
})
})

function runInput(runId: string, inputSequence: number, content: string): AgentInputItem {
return {
type: 'message',
role: 'user',
content,
providerData: {
geoAgentRunInput: {
runId,
inputId: `input_${inputSequence}`,
inputSequence,
},
},
}
}
Loading
Loading