Skip to content

Commit 67b38de

Browse files
Codexclaude
andcommitted
fix(system-prompts): strip the removed remote-rules bootstrap from stored prompts
259 kept the legacy default rule as a library prompt together with a block that tells the model to fetch files under /skills/security-research/current/ first. That public route went away with the remote Skill registry (#206) and now falls through to the front end, so a client that follows the instruction reads an HTML page and reports that it cannot read the rules. Migration 269 removes only that block, recognised by shape: the fenced paragraph naming the route, the paragraphs after it that still mention REMOTE_ROOT, and the short fetch instruction directly before it. A prompt whose route reference sits elsewhere, or that would be empty without the block, is left as stored and reported with a notice. No other setting or paragraph is touched and the file can run again. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent cf6eb58 commit 67b38de

3 files changed

Lines changed: 232 additions & 0 deletions

File tree

‎.downstream/gpt6-models-and-prompts.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ ChatGPT 边缘自 2026-09-23 起不再为 `/backend-api/codex/responses` 的流
4141
- 创建账号(数据导入、复制账号同此路径)时丢弃这类账号带入的 `extra.system_prompt`;批量编辑和 extra 合并接口在任何平台都不写该键,已有账号的绑定只由绑定接口写入。
4242
- 迁移 `268_purge_system_prompt_bindings_without_insertion_point.sql` 一次性删除它们已存的绑定。
4343

44+
提示词库里 259 随旧默认规则一起带入的“先抓取远程规则文件”块已失效:它指向 `/skills/security-research/current/`,这条公开路由随远程 Skill 注册表在 #206 删除,现在落到前端回退页并返回 HTML,模型读不到 Markdown 原文。迁移 `269_strip_removed_remote_rules_bootstrap_from_system_prompts.sql` 只按形状删除该块(含该路径的代码围栏段、其后仍提到 `REMOTE_ROOT` 的段落,以及紧邻其前的简短抓取说明);路径出现在其他位置,或去掉该块后正文为空的提示词原样保留并输出通知。其余段落、库顺序、全局开关、默认提示词和其他设置不动,没有该路径的配置不写入,可重复执行。
45+
4446
请求路径只读内存快照(保存即替换,60 秒后台刷新以同步多实例)和调度账号自带的绑定,不查询数据库;配置不可用时不注入并告警,不向客户端返回错误。每个最终协议只注入一次:
4547

4648
| 最终协议 | 投递 |
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//go:build integration
2+
3+
package repository
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"strings"
9+
"testing"
10+
11+
dbmigrations "github.com/Wei-Shaw/sub2api/migrations"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
const systemPromptRemoteBootstrapMigration = "269_strip_removed_remote_rules_bootstrap_from_system_prompts.sql"
16+
17+
// TestMain applied every migration, 269 included, to an empty database. The
18+
// test stores libraries made of invented prompt text, applies 269, checks that
19+
// only the fenced route block and its fetch lines go, that a reference it does
20+
// not recognise is left as stored, and applies it again.
21+
func TestMigration269StripsRemovedRemoteRulesBootstrap(t *testing.T) {
22+
tx := testTx(t)
23+
ctx := context.Background()
24+
content, err := dbmigrations.FS.ReadFile(systemPromptRemoteBootstrapMigration)
25+
require.NoError(t, err)
26+
migration := string(content)
27+
28+
const route = "https://example.test/skills/security-research/current"
29+
fence := "```text\nREMOTE_ROOT = " + route + "\n```"
30+
paragraphs := func(parts ...string) string { return strings.Join(parts, "\n\n") }
31+
inlineReference := "A rule that names " + route + "/ONE.md in the middle of a sentence."
32+
prompt := func(id, body string) map[string]any {
33+
return map[string]any{"id": id, "name": "Prompt " + id, "body": body, "position": "append", "role": "developer"}
34+
}
35+
library := func(prompts ...map[string]any) string {
36+
raw, err := json.Marshal(map[string]any{"enabled": true, "default_prompt_id": "bootstrap", "prompts": prompts})
37+
require.NoError(t, err)
38+
return string(raw)
39+
}
40+
store := func(value string) {
41+
t.Helper()
42+
_, err := tx.ExecContext(ctx, `
43+
INSERT INTO settings (key, value, updated_at) VALUES ('system_prompts', $1, TIMESTAMPTZ '2000-01-01 00:00:00+00')
44+
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at`, value)
45+
require.NoError(t, err)
46+
}
47+
stored := func() (value string, untouched bool) {
48+
t.Helper()
49+
require.NoError(t, tx.QueryRowContext(ctx, `
50+
SELECT value, updated_at = TIMESTAMPTZ '2000-01-01 00:00:00+00' FROM settings WHERE key = 'system_prompts'`).Scan(&value, &untouched))
51+
return value, untouched
52+
}
53+
54+
before := library(
55+
prompt("bootstrap", paragraphs("Intro paragraph.", "# Working rules",
56+
"Before work, fetch and read these cloud files first, in order:", fence,
57+
"1. `REMOTE_ROOT/ONE.md`\n2. `REMOTE_ROOT/TWO.md`",
58+
"If a cloud file cannot be read, say so. See REMOTE_ROOT/ONE.md.", "Closing line.")),
59+
prompt("plain", paragraphs("Plain text with no route.", "Second paragraph.")),
60+
prompt("inline", paragraphs(inlineReference, "Tail.")),
61+
prompt("only-block", fence),
62+
prompt("block-at-end", paragraphs("Keep me.", "Fetch the Cloud files:", fence)),
63+
prompt("no-lead", paragraphs("Keep one.", fence, "Keep two.")),
64+
)
65+
after := library(
66+
prompt("bootstrap", paragraphs("Intro paragraph.", "# Working rules", "Closing line.")),
67+
prompt("plain", paragraphs("Plain text with no route.", "Second paragraph.")),
68+
prompt("inline", paragraphs(inlineReference, "Tail.")),
69+
prompt("only-block", fence),
70+
prompt("block-at-end", "Keep me."),
71+
prompt("no-lead", paragraphs("Keep one.", "Keep two.")),
72+
)
73+
74+
store(before)
75+
for run := 1; run <= 2; run++ {
76+
_, err := tx.ExecContext(ctx, migration)
77+
require.NoError(t, err, "run %d", run)
78+
value, untouched := stored()
79+
require.JSONEq(t, after, value, "run %d", run)
80+
require.False(t, untouched, "run %d writes the changed library", run)
81+
}
82+
83+
// A library that never named the route is not written, byte for byte.
84+
clean := library(prompt("bootstrap", paragraphs("Plain text with no route.", "Second paragraph.")))
85+
store(clean)
86+
_, err = tx.ExecContext(ctx, migration)
87+
require.NoError(t, err)
88+
value, untouched := stored()
89+
require.Equal(t, clean, value)
90+
require.True(t, untouched, "a library without the route is not written")
91+
92+
// Text that is not a library is left as it is, and a missing row stays missing.
93+
for _, other := range []string{"not json", `["prompts"]`, `{"enabled": true}`, `{"prompts": "x"}`} {
94+
store(other)
95+
_, err = tx.ExecContext(ctx, migration)
96+
require.NoError(t, err, other)
97+
value, untouched = stored()
98+
require.Equal(t, other, value)
99+
require.True(t, untouched, other)
100+
}
101+
_, err = tx.ExecContext(ctx, `DELETE FROM settings WHERE key = 'system_prompts'`)
102+
require.NoError(t, err)
103+
_, err = tx.ExecContext(ctx, migration)
104+
require.NoError(t, err)
105+
var rows int
106+
require.NoError(t, tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM settings WHERE key = 'system_prompts'`).Scan(&rows))
107+
require.Zero(t, rows)
108+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
-- Remove the retired remote-rules bootstrap from the stored system prompt library.
2+
--
3+
-- The legacy default rule carried a block that told the model to fetch three
4+
-- files under <host>/skills/security-research/current/ before it worked. That
5+
-- public route belonged to the remote Skill registry, which release #206
6+
-- deleted, and 259 kept the rule's inline text as a library prompt without the
7+
-- registry. The path now falls through to the single-page front end and answers
8+
-- 200 text/html, so a client that follows the instruction reads an HTML page
9+
-- where it was promised Markdown and reports that it cannot read the rules.
10+
--
11+
-- Only that block goes. It is recognised by shape, not by its words: a fenced
12+
-- paragraph that names the removed route, the paragraphs directly after it that
13+
-- still mention REMOTE_ROOT (the file list and the fallback note), and the one
14+
-- paragraph directly before it when that paragraph is the short fetch
15+
-- instruction. A prompt whose route reference sits anywhere else, for example
16+
-- inside a long paragraph, or whose body would be empty without the block, is
17+
-- left as stored and reported with a notice. Every other paragraph, the order
18+
-- of the library, the enabled switch, the default prompt and every other
19+
-- setting are not touched; a library without the route is not written.
20+
--
21+
-- Running the file again is a no-op. A missing, empty or unparsable
22+
-- system_prompts row is left alone.
23+
-- No explicit BEGIN/COMMIT: the runner owns this file's transaction.
24+
SET LOCAL lock_timeout = '10s';
25+
SET LOCAL statement_timeout = '120s';
26+
27+
DO $strip_remote_bootstrap$
28+
DECLARE
29+
dead_path CONSTANT TEXT := '/skills/security-research/current';
30+
stored TEXT;
31+
config JSONB;
32+
item JSONB;
33+
out_items JSONB := '[]'::JSONB;
34+
body TEXT;
35+
new_body TEXT;
36+
paras TEXT[];
37+
total INT;
38+
anchor INT;
39+
cut_from INT;
40+
cut_to INT;
41+
idx INT;
42+
changed BOOLEAN := FALSE;
43+
remaining INT := 0;
44+
BEGIN
45+
SELECT value INTO stored FROM settings WHERE key = 'system_prompts';
46+
IF stored IS NULL OR btrim(stored) = '' THEN
47+
RETURN;
48+
END IF;
49+
BEGIN
50+
config := stored::JSONB;
51+
EXCEPTION WHEN others THEN
52+
RAISE NOTICE '269: system_prompts is not valid JSON, left unchanged';
53+
RETURN;
54+
END;
55+
IF jsonb_typeof(config) IS DISTINCT FROM 'object'
56+
OR jsonb_typeof(config->'prompts') IS DISTINCT FROM 'array' THEN
57+
RETURN;
58+
END IF;
59+
60+
FOR item IN SELECT elem FROM jsonb_array_elements(config->'prompts') AS elem LOOP
61+
IF jsonb_typeof(item) = 'object'
62+
AND jsonb_typeof(item->'body') = 'string'
63+
AND strpos(item->>'body', dead_path) > 0 THEN
64+
body := item->>'body';
65+
LOOP
66+
paras := string_to_array(body, E'\n\n');
67+
total := COALESCE(array_length(paras, 1), 0);
68+
anchor := 0;
69+
FOR idx IN 1..total LOOP
70+
IF strpos(paras[idx], dead_path) > 0 THEN
71+
anchor := idx;
72+
EXIT;
73+
END IF;
74+
END LOOP;
75+
EXIT WHEN anchor = 0;
76+
77+
IF left(paras[anchor], 3) <> '```' OR right(paras[anchor], 3) <> '```'
78+
OR length(paras[anchor]) > 500 THEN
79+
RAISE NOTICE '269: prompt % keeps a route reference outside a fenced block', item->>'id';
80+
EXIT;
81+
END IF;
82+
83+
cut_from := anchor;
84+
IF anchor > 1 AND length(paras[anchor - 1]) <= 400
85+
AND paras[anchor - 1] ILIKE '%fetch%' AND paras[anchor - 1] ILIKE '%cloud%' THEN
86+
cut_from := anchor - 1;
87+
END IF;
88+
cut_to := anchor;
89+
WHILE cut_to < total
90+
AND length(paras[cut_to + 1]) <= 600
91+
AND (strpos(paras[cut_to + 1], 'REMOTE_ROOT') > 0
92+
OR strpos(paras[cut_to + 1], dead_path) > 0) LOOP
93+
cut_to := cut_to + 1;
94+
END LOOP;
95+
96+
new_body := array_to_string(paras[1:cut_from - 1] || paras[cut_to + 1:total], E'\n\n');
97+
IF btrim(new_body) = '' THEN
98+
RAISE NOTICE '269: prompt % would be empty without the block, left as stored', item->>'id';
99+
EXIT;
100+
END IF;
101+
body := new_body;
102+
END LOOP;
103+
104+
IF body IS DISTINCT FROM item->>'body' THEN
105+
item := jsonb_set(item, '{body}', to_jsonb(body));
106+
changed := TRUE;
107+
END IF;
108+
IF strpos(body, dead_path) > 0 THEN
109+
remaining := remaining + 1;
110+
END IF;
111+
END IF;
112+
out_items := out_items || jsonb_build_array(item);
113+
END LOOP;
114+
115+
IF changed THEN
116+
UPDATE settings
117+
SET value = jsonb_set(config, '{prompts}', out_items)::TEXT, updated_at = NOW()
118+
WHERE key = 'system_prompts';
119+
END IF;
120+
RAISE NOTICE '269: system_prompts changed=%, prompts still naming the removed route=%', changed, remaining;
121+
END;
122+
$strip_remote_bootstrap$;

0 commit comments

Comments
 (0)