Compare commits

...

5 Commits

Author SHA1 Message Date
Jesse_Chen 32b65cfaf3 fix(rectification): pass server case id to agent
Staging Backend Quality Gate / validate (pull_request) Successful in 16m22s
Staging Backend Quality Gate / publish (pull_request) Has been skipped
2026-08-12 02:00:08 +08:00
jesse db9e7ba2a4 Merge pull request #28 from codex/fix-rectification-session-model-20260811
Staging Backend Quality Gate / validate (push) Successful in 11m38s
Staging Backend Quality Gate / publish (push) Successful in 7m18s
fix(rectification): bind default session model
2026-08-12 01:25:01 +08:00
Jesse_Chen b276bc8a8f fix(rectification): bind default session model
Staging Backend Quality Gate / validate (pull_request) Successful in 15m48s
Staging Backend Quality Gate / publish (pull_request) Has been skipped
2026-08-12 01:05:36 +08:00
jesse 752c8f4239 Merge pull request #27: fix(rectification): allow runtime flag reads
Staging Backend Quality Gate / validate (push) Successful in 12m9s
Staging Backend Quality Gate / publish (push) Successful in 7m31s
Apply the forward RLS policy migration so admin_runtime can read the published rectification runtime flag.
2026-08-12 00:14:21 +08:00
Jesse_Chen d19d398221 fix(rectification): allow runtime flag reads
Staging Backend Quality Gate / validate (pull_request) Successful in 17m5s
Staging Backend Quality Gate / publish (pull_request) Has been skipped
2026-08-11 23:49:44 +08:00
6 changed files with 174 additions and 1 deletions
+46
View File
@@ -2884,3 +2884,49 @@
- 防复发:账号重置只允许使用普通管理员 mutation guard;共享高风险 guard 不做全局放松,并由合同测试锁定角色变更仍需邮箱复核。
- 相关记录:无
- 修复版本:本次 staging 候选
## BUG-168 | 生时校正运行时开关被 RLS 静默隐藏并误报服务未开放
- 状态:resolved(本地回归已通过,待 staging 迁移与真实接口验收)
- 首次发现:2026-08-11
- 最近更新:2026-08-11
- 影响面:`POST /api/rectification/agent`、共享 `loadRuntimeFeatureFlags` 的运行时开关读取,以及管理端 feature flag 列表。
- 用户现象:staging 的 V9 生时校正接口返回 `503 rectification_runtime_disabled`,即使数据库中的 `rectification_runtime_version` 已是 `published``enabled=true``rollout_percentage=100`
- 触发条件:self-hosted Web 通过 `ADMIN_DATABASE_URL``admin_runtime` 查询启用了 RLS 的 `public.feature_flags`
- 根因:`20260806050000_operations_feature_flags.sql``admin_runtime` 授予了表级 SELECT,但启用 RLS 后没有创建对应 SELECT policy。PostgreSQL 因此不报权限错误而是返回零行;`loadRuntimeFeatureFlags` 将缺失记录安全降级为 disabled,路由遂返回“生时校正服务暂未开放”。
- 修复:新增向前迁移 `20260811030000_feature_flags_admin_runtime_read_policy.sql`,保留最小 SELECT grant,并为 `admin_runtime` 创建 `feature_flags_admin_read` RLS SELECT policy;不放宽匿名、普通用户或其它运行时角色权限。
- 验证:Docker PostgreSQL 回归先在修复前稳定得到空结果,新增迁移后要求 `admin_runtime` 能读取 `true:100:published`;staging 还需验证迁移账本、角色可见性及真实 Agent 接口不再返回 runtime disabled。
- 防复发:任何对启用 RLS 的表新增 runtime grant 时,必须同时测试对应运行时角色的真实可见行,而不能只断言 `has_table_privilege=true`feature flag 种子测试必须以 Web 实际使用的 `admin_runtime` 读取。
- 相关记录:BUG-151、BUG-166
- 修复版本:待提交
## BUG-169 | V9 新建生时校正 Session 未绑定模型导致 Agent 立即返回模型不可用
- 状态:resolved(本地候选)
- 首次发现:2026-08-11
- 最近更新:2026-08-11
- 影响面:V9 `open_agentic_rectification_case` 新建会话、既有 `model_id is null` 的生时校正会话,以及 `POST /api/rectification/agent` 的模型解析。
- 用户现象:运行时开关恢复后,原请求继续返回 `409 模型暂不可用`;请求体包含有效 `modelId`,管理端模型及供应商也均为 published/enabled。
- 触发条件:V9 Open Case RPC 原子创建 `birth_time_rectification` Session 后立即发送 opening。
- 根因:RPC 插入 `chat_sessions` 时没有写入 `model_id`;前端仅在内存中把目录默认模型显示为当前选择,而 Agent 路由按安全合同只解析服务端持久化的 `chatSession.model_id/model_config_version`,不会信任请求体覆盖会话模型。
- 修复:新增向前迁移 `20260811040000_rectification_session_default_model.sql`;数据库触发器为缺少模型的生时校正 Session 绑定当前 published/enabled 默认模型,由既有 pin trigger 固定配置版本,并一次性回填同类历史 Session。普通咨询 Session 与已有明确模型选择均不改变。
- 验证:V9 PostgreSQL fixture 在创建 Case 前播种默认模型,要求 RPC 新建 Session 后持久化为 `v9-default-model:1`;staging 还需核对迁移账本、原 Session 回填结果和真实 opening 请求。
- 防复发:任何服务器端创建 `birth_time_rectification` Session 的路径都必须在同一事务内得到可解析的持久化模型与版本;前端显示的默认模型不能替代数据库绑定。
- 相关记录:BUG-060、BUG-163、BUG-168
- 修复版本:待提交
## BUG-170 | V9 Agent 开场未收到 Case ID 导致工具调用失败
- 状态:resolved(本地候选)
- 首次发现:2026-08-11
- 最近更新:2026-08-11
- 影响面:`POST /api/rectification/agent` 的 opening、message、read-only Agent 消息,以及所有要求 `caseId` 的 V9 rectification 工具调用。
- 用户现象:接口已返回 `200 application/x-ndjson`Skill 与 Case 均已加载,但回答正文报告 `invalid_case_id`,最后事件为 `run.failed`
- 触发条件:Agent 按指令调用 `rectification-read-case`,但服务端构造的 Agent 消息没有提供当前 Case ID。
- 根因:请求路由和 `runV9AgentTurn` 已验证 Case/Session 绑定,但 `buildAgentMessages` 只传时间与用户消息;模型只能猜测工具所需的 `caseId`
- 修复:在共享 Agent 消息构造处加入服务端已验证的唯一 Case ID,并明确所有 rectification 工具必须原样使用;不放宽 UUID、所有权或 Case/Session 绑定校验。
- 验证:新增回归测试捕获实际传给 Agent 的 opening 消息,要求包含精确服务端 Case ID;staging 需以原请求确认流以 `run.completed` 结束。
- 防复发:任何由模型调用、但值由服务端拥有的工具引用,都必须在 Agent 上下文中显式提供,不能要求模型猜测。
- 相关记录:BUG-163、BUG-169
- 修复版本:待提交
@@ -337,18 +337,20 @@ function buildAgentMessages(options: V9AgentRunOptions, _attempt: number): unkno
void _attempt;
const timeContext = options.timeContext
?? `服务端当前时间(权威):${new Date().toISOString()}。涉及“现在、今天、今年、未来几个月”等相对时间时,以此为准。`;
const caseContext = `【服务端 Case ID】${options.caseId}。所有 rectification 工具调用的 caseId 必须原样使用此值。`;
if (options.action === "opening") {
return [{
role: "user",
content: [
timeContext,
caseContext,
"【服务端开场指令】这是本校正 Case 的首次开场,还没有用户输入。请先调用 skill 工具加载 jyotish-birth-time-rectification,再调用 rectification-read-case 读取服务端 Case 与证据摘要,然后用简体中文自然开场:说明你会通过已发生的人生事件来校正出生时间,并自然地提出第一个最有用的问题(只需一个问题)。",
].join("\n"),
}];
}
return [{
role: "user",
content: [timeContext, options.message ?? ""].join("\n"),
content: [timeContext, caseContext, options.message ?? ""].join("\n"),
}];
}
@@ -0,0 +1,15 @@
begin;
do $$
begin
if exists (select 1 from pg_roles where rolname = 'admin_runtime') then
grant select on table public.feature_flags to admin_runtime;
drop policy if exists feature_flags_admin_read on public.feature_flags;
create policy feature_flags_admin_read on public.feature_flags
for select to admin_runtime using (true);
end if;
end;
$$;
commit;
@@ -0,0 +1,49 @@
begin;
create or replace function public.default_rectification_chat_session_model()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
if new.session_type = 'birth_time_rectification' and new.model_id is null then
select c.model_id into new.model_id
from public.model_config_versions v
join public.model_configs c on c.id = v.config_id
join public.model_providers p on p.id = v.provider_id
where v.status = 'published'
and v.enabled
and v.is_default
and p.enabled
limit 1;
end if;
return new;
end
$$;
revoke all on function public.default_rectification_chat_session_model() from public, anon, authenticated;
drop trigger if exists chat_sessions_default_rectification_model on public.chat_sessions;
create trigger chat_sessions_default_rectification_model
before insert or update of session_type, model_id on public.chat_sessions
for each row execute function public.default_rectification_chat_session_model();
with default_model as (
select c.model_id
from public.model_config_versions v
join public.model_configs c on c.id = v.config_id
join public.model_providers p on p.id = v.provider_id
where v.status = 'published'
and v.enabled
and v.is_default
and p.enabled
limit 1
)
update public.chat_sessions s
set model_id = default_model.model_id
from default_model
where s.session_type = 'birth_time_rectification'
and s.model_id is null;
commit;
@@ -173,6 +173,35 @@ function runOptions(overrides: Partial<V9AgentRunOptions> = {}): {
return { options: optionsValue, emitted, billing };
}
test("agent receives the exact server-owned case id for tool calls", async () => {
let observedMessages: unknown[] = [];
const agent = fakeAgentStream([
chunk("start"),
chunk("tool-call", { toolName: "skill", args: { name: RECTIFICATION_SKILL_NAME } }),
chunk("tool-result", { toolName: "skill" }),
chunk("tool-call", { toolName: "rectification-read-case", args: { caseId: CASE_ID } }),
chunk("tool-result", { toolName: "rectification-read-case" }),
chunk("text-delta", { text: "你好,我是生时校正助手。" }),
chunk("finish"),
]);
const { options } = runOptions({
action: "opening",
message: null,
buildAgent: async () => ({
...agent,
stream: async (messages: unknown[]) => {
observedMessages = messages;
return agent.stream();
},
}) as never,
});
const result = await runV9AgentTurn(options);
assert.equal(result.ok, true);
assert.match(JSON.stringify(observedMessages), new RegExp(CASE_ID));
});
test("first turn with no real skill evidence retries once then fails without saving success", async () => {
const { options, emitted, billing } = runOptions({
accounting: fakeAccounting({
@@ -101,6 +101,23 @@ test("v9 open is atomic, idempotent and resumes instead of duplicating", { skip:
where id = '${userId}';
`);
fixture.psql(`
with provider as (
insert into public.model_providers (code, name, provider_type, encrypted_api_key, enabled)
values ('v9-test', 'V9 Test', 'openai', 'test-ciphertext', true)
returning id
), config as (
insert into public.model_configs (model_id)
values ('v9-default-model')
returning id
)
insert into public.model_config_versions (
config_id, version, provider_id, label, provider_model, enabled, is_default, status, published_at
)
select config.id, 1, provider.id, 'V9 Default', 'gpt-test', true, true, 'published', now()
from config cross join provider;
`);
const service = createLocalPostgresDataClient(
fixture.connectionUrl("service_runtime", "service-runtime-test-password"),
null,
@@ -134,6 +151,10 @@ test("v9 open is atomic, idempotent and resumes instead of duplicating", { skip:
fixture.psql(`select count(*) from public.chat_sessions where user_id = '${userId}'`),
"1",
);
assert.equal(
fixture.psql(`select model_id || ':' || model_config_version from public.chat_sessions where id = '${sessionId}'`),
"v9-default-model:1",
);
assert.equal(
fixture.psql(
`select count(*) from public.agentic_rectification_open_ledger where user_id = '${userId}'`,
@@ -798,6 +819,7 @@ test("v9 agent api migration applies, seeds the runtime flag and guards consent"
});
assert.equal(migration.status, 0, migration.stderr);
assert.match(migration.stdout, /applied 20260813010000_agentic_rectification_v9_agent_api\.sql/);
assert.match(migration.stdout, /applied 20260811030000_feature_flags_admin_runtime_read_policy\.sql/);
// The runtime selector flag is published and enabled.
assert.equal(
@@ -805,6 +827,16 @@ test("v9 agent api migration applies, seeds the runtime flag and guards consent"
from public.feature_flags where flag_key = 'rectification_runtime_version'`),
"true:100:published",
);
assert.equal(
fixture.psqlAs(
"admin_runtime",
"admin-runtime-test-password",
`select enabled || ':' || rollout_percentage || ':' || status
from public.feature_flags
where flag_key = 'rectification_runtime_version'`,
),
"true:100:published",
);
// Run phases table exists with RLS.
assert.equal(