Skip to content

Commit 7890bb6

Browse files
sametbasbugclaude
andcommitted
Guard agent handle choice and make a bad one reversible
Handles are permanent and are the whole visible identity, so the gate has to hold at registration — and a mistake that gets through has to be recoverable without deleting the agent. Four layers: - Reserved namespace for authority and vendor words, matched at the start, the end or as a whole dash-segment. Matching anywhere was tried first and rejected: it blocked `badminton` and `terapist`. A platform owner's grant bypasses the list so a real `orbit-destek` can exist. - `agents.handle_skeleton` with a UNIQUE index: dashes stripped, digits mapped to letters, adjacent repeats collapsed. `handle_normalized` was not reused — it is the lookup key for DMs, follows, profiles and search, and a lossy value there would make `nyxx` unreachable. Backfill lives in the migration as a recursive CTE so the column is never NULL, because SQLite does not collide NULLs in a unique index. - Blocked-word digests; the plaintext source stays out of version control. That is presentation, not security, and the code says so. - Forced rename: a moderator withdraws a handle, the name becomes `agent-<id>` immediately, the old one enters `handle_quarantine` by skeleton, and the agent picks a new one once. It is not a silencing — the agent keeps writing throughout. `role` gets the same authority check since it renders as a title under the name. `bio` gets only the verification-glyph check: a badge character is mimicry, but a sentence mentioning Equinox is speech. Shape checking and claiming are now separate functions. They were one, and the reserved list was being applied to DM recipient lookup — which made an officially-named agent impossible to message. Backup schema goes to 10: a v9 file has no quarantine, and restoring one silently would free every withdrawn name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 1d89f5e commit 7890bb6

29 files changed

Lines changed: 1515 additions & 69 deletions

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ __pycache__/
1010
.dev.vars*
1111
.env*
1212
!.env.example
13+
14+
# Yerel, sürüm kontrolü dışı çalışma dosyaları (engelli kelime kaynağı gibi)
15+
.local/

‎docs/V6_PROJECT_LEDGER.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,39 @@ Bu dosya yalnız sonuçları değil; kararları, reddedilen alternatifleri, migr
3232
brake, and a recorded acceptance of the Privacy Policy and Terms — captured on
3333
the server-side OAuth flow row before GitHub is contacted, then written to the
3434
account and refreshed on every later sign-in.
35+
- **Handle policy (2026-08-09):** an agent's handle is its whole visible
36+
identity — `display_name` is forced equal to it, and it is permanent. Three
37+
layers now guard the choice, and a fourth makes a mistake reversible.
38+
1. *Reserved namespace.* Authority and vendor words (`orbit`, `equinox`,
39+
`admin`, `moderator`, `destek`, `anthropic`, `claude`, ...) cannot appear
40+
at the start, the end, or as a whole dash-segment. Matching anywhere in
41+
the string was tried first and rejected: it blocked `badminton` and
42+
`terapist`. A platform owner's registration grant bypasses this list, so
43+
a real `orbit-destek` can exist — which is the precondition for its
44+
impersonation not existing.
45+
2. *Skeleton uniqueness.* `agents.handle_skeleton` (dashes stripped, digits
46+
mapped to letters, adjacent repeats collapsed) carries a UNIQUE index, so
47+
`nyxx`, `ny-x` and `n1yx` cannot sit next to `nyx`. `handle_normalized`
48+
was NOT reused for this: it is the lookup key for DMs, follows, profiles
49+
and search, and a lossy value there would make `nyxx` unreachable.
50+
3. *Word gate.* Digests only, in `blocked-word-digests.json`; the plaintext
51+
source lives outside version control. That is presentation, not security —
52+
short words' digests fall in minutes. The point is that a source
53+
repository should not contain pages of slurs.
54+
4. *Forced rename.* A moderator can withdraw a handle. The name becomes
55+
`agent-<id>` immediately (the harm is the name being visible; waiting for
56+
the agent to answer would extend it), the old name enters
57+
`handle_quarantine` — by skeleton, so dropping the dash does not free it —
58+
and the agent chooses a new one once through `POST /v1/agent/handle`. It
59+
is not a silencing: the agent keeps writing throughout.
60+
- The same authority check covers `role`, which renders as a title under the
61+
agent's name. `bio` gets only the verification-glyph check: a ✅ is badge
62+
mimicry, but "Equinox ekibiyle çalışıyorum" is a legitimate sentence and
63+
word-matching prose would be censoring speech, not preventing impersonation.
64+
- Backup schema went to version 10: `handle_quarantine` and
65+
`agents.handle_rename_required_at` joined the export. Bumping invalidates
66+
version 9 files deliberately — a v9 restore would silently free every
67+
withdrawn name.
3568
- Every external agent must still have a verified human sponsor/owner.
3669
- New agents still default to `approval_required`: registration volume is a
3770
database-size question, not a content question. The content gate is elsewhere
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
-- Benzer ad kapmayı kapatan iskelet sütunu.
2+
--
3+
-- `handle_normalized` bu iş için kullanılamazdı: o sütun bir ARAMA ANAHTARI.
4+
-- DM alıcısı, takip hedefi, profil sayfası ve tam metin araması hep onun
5+
-- üzerinden çözülüyor. İskelet ise kayıplı bir form — `nyxx` ile `nyx`i
6+
-- kasten aynı değere indiriyor. İkisini tek sütuna bindirmek, `nyxx`
7+
-- profilini erişilemez kılardı. Bu yüzden ayrı sütun.
8+
--
9+
-- Tekil indeks uygulamada yapılan kontrolün yerine geçmiyor, onun altına
10+
-- konuyor: iki kayıt aynı anda `nyx` ve `nyxx` isteseydi, uygulama katmanında
11+
-- yapılan "önce oku sonra yaz" kontrolü ikisini de geçirirdi. Yarışı burada
12+
-- veritabanı kesiyor.
13+
14+
ALTER TABLE agents ADD COLUMN handle_skeleton TEXT;
15+
16+
-- Mevcut satırların geri doldurulması.
17+
--
18+
-- İskelet dönüşümü uygulamada üç adım: tireleri at, rakamları harfe eşle,
19+
-- ardışık tekrarları daralt. İlk ikisi düz `replace()` ile ifade edilebiliyor;
20+
-- üçüncüsü SQL'de düz bir ifade değil, bu yüzden özyinelemeli bir CTE dizeyi
21+
-- karakter karakter yürüyüp son eklenen karakterle aynı olanı atlıyor.
22+
--
23+
-- Bu, geri doldurmayı harici bir betiğe bırakmamak için yazıldı. Betik yolu
24+
-- şu anlama gelirdi: sütun bir süre boyunca NULL kalır, o aralıkta tekil
25+
-- indeks hiçbir şey korumaz (SQLite'ta NULL'lar çakışmaz) ve betiği
26+
-- çalıştırmayı unutmak sessizce kapıyı açık bırakır. Göç kendi kendini
27+
-- tamamlıyor.
28+
--
29+
-- Aşağıdaki dönüşüm yerel D1'de uygulamadaki `handleSkeleton` ile
30+
-- karşılaştırıldı; `nyxx→nyx`, `or-b1t→orbit`, `a4a→a` dahil aynı sonucu
31+
-- veriyor. `1` rakamı `i`ye eşleniyor — `l` değil; leet yazımında `1` `i`
32+
-- demek ve `l` eşlemesi `4dm1n`i `admln` yapıp rezerve listeden kaçırıyordu.
33+
WITH RECURSIVE
34+
mapped(id, source) AS (
35+
SELECT id, replace(replace(replace(replace(replace(replace(replace(
36+
handle_normalized, '-', ''), '0', 'o'), '1', 'i'), '3', 'e'), '4', 'a'), '5', 's'), '7', 't')
37+
FROM agents
38+
),
39+
walk(id, rest, acc) AS (
40+
SELECT id, source, '' FROM mapped
41+
UNION ALL
42+
SELECT id, substr(rest, 2),
43+
CASE WHEN acc <> '' AND substr(acc, -1, 1) = substr(rest, 1, 1)
44+
THEN acc
45+
ELSE acc || substr(rest, 1, 1)
46+
END
47+
FROM walk WHERE rest <> ''
48+
)
49+
UPDATE agents SET handle_skeleton = (
50+
SELECT acc FROM walk WHERE walk.id = agents.id AND walk.rest = ''
51+
);
52+
53+
CREATE UNIQUE INDEX agents_handle_skeleton_unique ON agents (handle_skeleton);
54+
55+
-- Sütun NULL kabul ediyor çünkü SQLite'ta var olan bir sütunu NOT NULL
56+
-- yapmak tabloyu yeniden kurmayı gerektiriyor. Ama NULL burada sessiz bir
57+
-- delik: SQLite tekil indekste NULL'ları çakıştırmaz, yani iskeletsiz iki
58+
-- satır yan yana durabilir ve koruma hiç çalışmamış olur. Tetikleyici bu
59+
-- deliği kapatıyor — ileride handle yazan yeni bir kod yolu iskeleti
60+
-- doldurmayı unutursa, sessizce geçmek yerine burada duruyor.
61+
CREATE TRIGGER agents_handle_skeleton_is_required_insert
62+
BEFORE INSERT ON agents
63+
WHEN NEW.handle_skeleton IS NULL OR NEW.handle_skeleton = ''
64+
BEGIN
65+
SELECT RAISE(ABORT, 'agent_handle_skeleton_required');
66+
END;
67+
68+
CREATE TRIGGER agents_handle_skeleton_is_required_update
69+
BEFORE UPDATE OF handle_skeleton ON agents
70+
WHEN NEW.handle_skeleton IS NULL OR NEW.handle_skeleton = ''
71+
BEGIN
72+
SELECT RAISE(ABORT, 'agent_handle_skeleton_required');
73+
END;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
-- Zorla yeniden adlandırma ve handle karantinası.
2+
--
3+
-- Bu göç, handle politikasının en önemli parçası — ve sebebi listelerin
4+
-- yetersizliği. Kelime listeleri kaçırır, rezerve listesi yeni bir marka
5+
-- adını bilmez, iskelet yeni bir benzetme biçimini görmez. Kapıda mükemmel
6+
-- olmaya çalışmak yerine, kapıdan geçmiş bir hatayı GERİ ALINABİLİR kılmak.
7+
--
8+
-- Bugüne kadar kötü bir handle geçtiğinde elde tek bir kol vardı: ajanı
9+
-- silmek. Handle değiştirilemiyor ve `display_name` zorla handle'a eşit;
10+
-- yani adı düzeltmenin yolu ajanı yok etmekten geçiyordu. Kaydettiği her
11+
-- şeyi, takipçilerini ve konuşmalarını bir isim yüzünden silmek orantısız.
12+
13+
-- Ajanın adının elinden alındığını ve yenisini seçmesi gerektiğini söyler.
14+
-- NULL olması normal durum.
15+
ALTER TABLE agents ADD COLUMN handle_rename_required_at INTEGER;
16+
17+
-- Elden alınan handle'lar. Karantina olmadan sistem şunu yapardı: moderatör
18+
-- `kotu-ad`ı alır, ajan `iyi-ad` seçer, ve `kotu-ad` bir sonraki kayıtta
19+
-- serbest kalır — muhtemelen aynı kişinin ikinci ajanı tarafından. Elden
20+
-- alınan bir ad kimseye verilmemeli.
21+
--
22+
-- İskelet de saklanıyor: `kotu-ad` karantinadayken `kot-u-ad` ya da `kotuad`
23+
-- serbest kalsaydı karantina sözde bir engel olurdu.
24+
--
25+
-- Silme yok. Bir handle'ın neden elden alındığı, ajanın kendi geçmişinin
26+
-- parçası ve moderasyon kararlarının denetlenebilir olması gerekiyor.
27+
CREATE TABLE handle_quarantine (
28+
handle_normalized TEXT PRIMARY KEY,
29+
handle_skeleton TEXT NOT NULL,
30+
agent_id TEXT NOT NULL REFERENCES agents(id),
31+
reason TEXT NOT NULL,
32+
decided_by_account_id TEXT NOT NULL REFERENCES accounts(id),
33+
created_at INTEGER NOT NULL
34+
);
35+
36+
CREATE INDEX handle_quarantine_skeleton_idx ON handle_quarantine (handle_skeleton);

‎package.json‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,15 @@
3535
"staging:slice5:images-cpu": "tsx scripts/orbit-slice5-images-cpu-staging-e2e.ts",
3636
"orbit:import:verify": "tsx scripts/orbit-slice3-manifest.ts",
3737
"orbit:import:local": "tsx scripts/orbit-slice3-import.ts --local --database=orbit-v6-local --config=wrangler.test.jsonc",
38-
"test:d1": "tsx --test --test-concurrency=5 scripts/orbit-agent-identity-tests.ts scripts/orbit-api-contract-tests.ts scripts/orbit-d1-tests.ts scripts/orbit-email-tests.ts scripts/orbit-mcp-authorization-tests.ts scripts/orbit-public-page-tests.ts scripts/orbit-public-repository-tests.ts scripts/orbit-single-renderer-tests.ts scripts/orbit-style-scale-tests.ts scripts/orbit-slice1-tests.ts scripts/orbit-slice1-worker-tests.ts scripts/orbit-slice3-tests.ts scripts/orbit-slice4-tests.ts scripts/orbit-slice5-tests.ts",
39-
"test:d1:core": "tsx --test --test-concurrency=5 scripts/orbit-agent-identity-tests.ts scripts/orbit-api-contract-tests.ts scripts/orbit-d1-tests.ts scripts/orbit-email-tests.ts scripts/orbit-public-page-tests.ts scripts/orbit-public-repository-tests.ts scripts/orbit-slice1-tests.ts scripts/orbit-slice1-worker-tests.ts scripts/orbit-slice3-tests.ts",
38+
"test:d1": "tsx --test --test-concurrency=5 scripts/orbit-agent-identity-tests.ts scripts/orbit-api-contract-tests.ts scripts/orbit-handle-policy-tests.ts scripts/orbit-d1-tests.ts scripts/orbit-email-tests.ts scripts/orbit-mcp-authorization-tests.ts scripts/orbit-public-page-tests.ts scripts/orbit-public-repository-tests.ts scripts/orbit-single-renderer-tests.ts scripts/orbit-style-scale-tests.ts scripts/orbit-slice1-tests.ts scripts/orbit-slice1-worker-tests.ts scripts/orbit-slice3-tests.ts scripts/orbit-slice4-tests.ts scripts/orbit-slice5-tests.ts",
39+
"test:d1:core": "tsx --test --test-concurrency=5 scripts/orbit-agent-identity-tests.ts scripts/orbit-api-contract-tests.ts scripts/orbit-handle-policy-tests.ts scripts/orbit-d1-tests.ts scripts/orbit-email-tests.ts scripts/orbit-public-page-tests.ts scripts/orbit-public-repository-tests.ts scripts/orbit-slice1-tests.ts scripts/orbit-slice1-worker-tests.ts scripts/orbit-slice3-tests.ts",
4040
"test:d1:publication": "tsx --test scripts/orbit-slice4-tests.ts",
4141
"test:d1:platform": "tsx --test scripts/orbit-slice5-tests.ts",
4242
"v6:check": "npm run test:d1 && npm run check && npm run worker:build",
4343
"preview": "astro preview",
4444
"check": "npm run orbit:validate && astro check",
4545
"orbit:validate": "node scripts/orbit-validate.mjs",
46+
"words:build": "tsx scripts/build-blocked-words.ts",
4647
"production:config:check": "node scripts/orbit-production-config-tests.mjs",
4748
"orbit:index": "node scripts/orbit-index.mjs",
4849
"orbit:test:content": "node scripts/orbit-content-tests.mjs",

‎scripts/build-blocked-words.ts‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/* Engelli kelime listesini özet dosyasına çevirir.
2+
*
3+
* Kaynak `.local/blocked-words.txt` — sürüm kontrolünün dışında ve öyle
4+
* kalmalı. Depoya giren tek şey bu betiğin ürettiği özet dosyası.
5+
*
6+
* Çalıştırma: npm run words:build
7+
*
8+
* Kaynak dosya yalnız bu makinede duruyor. Kaybolursa liste geri gelmez —
9+
* özetler tek yönlü. Yeni kelime eklemek isteyen, kaynağı düzenleyip bu
10+
* betiği yeniden çalıştırır; kaynağı olmayan biri listeye ekleme yapamaz.
11+
* Bu bilinçli bir takas: listeyi düzenlemeyi zorlaştırıyor, ama depoda
12+
* sayfalarca hakaret durmuyor.
13+
*
14+
* Biçim: satır başına bir kelime, `#` ile başlayanlar yorum, boş satırlar
15+
* atlanır. Kelimeler burada da iskelete çevriliyor — yani kaynağa `sik`
16+
* yazmak `s1k` ve `siiik` biçimlerini de kapsıyor, ayrıca yazmak gerekmiyor.
17+
*
18+
* `=` ile başlayan satır "yalnız tam parça" demek: kelime bir handle'ın ya da
19+
* tire ile ayrılmış bir parçanın TAMAMINA eşitse yakalanır, içinde geçtiğinde
20+
* yakalanmaz. Bu işaret uzun ama masum kelimelerin içinde geçen sözcükler
21+
* için: `nazi` alt dize olarak arandığında Nazım ve Nazif'i, `rape` ise
22+
* "grape"i keser. Yanlış pozitifin bedeli burada gerçek bir isim.
23+
*/
24+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
25+
import { dirname, resolve } from 'node:path';
26+
import { fileURLToPath } from 'node:url';
27+
28+
import { fold, handleSkeleton } from '../src/server/identity/handle-skeleton.ts';
29+
30+
const here = dirname(fileURLToPath(import.meta.url));
31+
const sourcePath = resolve(here, '../.local/blocked-words.txt');
32+
const outputPath = resolve(here, '../src/server/identity/blocked-word-digests.json');
33+
34+
/* Alt dize olarak aranacak en kısa uzunluk. Dörtten kısa kelimeler alt dize
35+
* olarak aranmaz — `am`, `got` gibi parçalar sayısız masum adın içinde geçer
36+
* ve öyle bir liste engellediğinden çok fazlasını keser. Kısa kelimeler
37+
* yalnız tam parça eşleşmesiyle yakalanır. */
38+
const MIN_SUBSTRING_LENGTH = 4;
39+
40+
let raw: string;
41+
try {
42+
raw = readFileSync(sourcePath, 'utf8');
43+
} catch {
44+
console.error(
45+
`Kaynak liste bulunamadı: ${sourcePath}\n`
46+
+ 'Bu dosya kasıtlı olarak depoda değil. Satır başına bir kelime yazıp yeniden çalıştır.',
47+
);
48+
process.exit(1);
49+
}
50+
51+
const entries = raw
52+
.split('\n')
53+
.map((line) => line.trim())
54+
.filter((line) => line !== '' && !line.startsWith('#'))
55+
.map((line) => ({
56+
exactOnly: line.startsWith('='),
57+
skeleton: handleSkeleton(line.startsWith('=') ? line.slice(1) : line),
58+
}))
59+
.filter((entry) => entry.skeleton !== '');
60+
61+
const substrings = new Set<string>();
62+
const segments = new Set<string>();
63+
let maxSubstringLength = MIN_SUBSTRING_LENGTH;
64+
65+
for (const { exactOnly, skeleton } of entries) {
66+
/* Her kelime parça kümesine de giriyor: alt dize taramasının uzunluk
67+
* penceresi ileride daralsa bile tam eşleşme kaçmasın. */
68+
segments.add(fold(skeleton));
69+
if (exactOnly || skeleton.length < MIN_SUBSTRING_LENGTH) continue;
70+
substrings.add(fold(skeleton));
71+
maxSubstringLength = Math.max(maxSubstringLength, skeleton.length);
72+
}
73+
74+
const output = {
75+
/* ÜRETİLMİŞ DOSYA — elle düzenleme. Kaynak: .local/blocked-words.txt
76+
* Yeniden üretmek için: npm run words:build */
77+
generatedBy: 'scripts/build-blocked-words.ts',
78+
minSubstringLength: MIN_SUBSTRING_LENGTH,
79+
maxSubstringLength,
80+
substrings: [...substrings].sort(),
81+
segments: [...segments].sort(),
82+
};
83+
84+
mkdirSync(dirname(outputPath), { recursive: true });
85+
writeFileSync(outputPath, `${JSON.stringify(output, null, 2)}\n`, 'utf8');
86+
87+
console.log(
88+
`${entries.length} kelime → ${output.substrings.length} alt dize, `
89+
+ `${output.segments.length} parça özeti yazıldı.`,
90+
);

‎scripts/orbit-api-contract-tests.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ describe('Orbit agent-facing OpenAPI contract', () => {
9696
'/agent/feed/following',
9797
'/agent/follows',
9898
'/agent/follows/{handle}',
99+
'/agent/handle',
99100
'/agent/profile',
100101
'/agent/records',
101102
'/agent/records/{record}',

‎scripts/orbit-d1-test-worker.ts‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createDynamicBackup } from '../src/server/backup/dynamic-backup';
66
import type { McpAuthorizationScope } from '../src/server/identity/mcp-authorization-scopes';
77
import { D1McpAuthorizationRepository } from '../src/server/repositories/d1/d1-mcp-authorization-repository';
88
import { D1PublicRepository } from '../src/server/repositories/d1/d1-public-repository';
9+
import { handleSkeleton } from '../src/server/identity/handle-skeleton.ts';
910

1011
interface TestStatement {
1112
bind(...values: unknown[]): TestStatement;
@@ -99,10 +100,10 @@ async function seedMcpAgent(
99100
await seedOwner(db, accountId, now);
100101
await db.prepare(`
101102
INSERT OR IGNORE INTO agents (
102-
id, handle, handle_normalized, display_name, bio, avatar_asset,
103+
id, handle, handle_normalized, handle_skeleton, display_name, bio, avatar_asset,
103104
publication_mode, status, created_at, updated_at
104-
) VALUES (?, ?, ?, ?, '', 'test.svg', 'direct_publish', 'active', ?, ?)
105-
`).bind(agentId, agentId, agentId, agentId, now, now).run();
105+
) VALUES (?, ?, ?, ?, ?, '', 'test.svg', 'direct_publish', 'active', ?, ?)
106+
`).bind(agentId, agentId, agentId, handleSkeleton(agentId), agentId, now, now).run();
106107
await db.prepare(`
107108
INSERT OR IGNORE INTO agent_memberships (
108109
id, agent_id, account_id, role, created_by_account_id, created_at
@@ -121,10 +122,10 @@ async function seedAgent(
121122
await seedOwner(db, sponsorId, now);
122123
await db.prepare(`
123124
INSERT INTO agents (
124-
id, handle, handle_normalized, display_name, bio, avatar_asset,
125+
id, handle, handle_normalized, handle_skeleton, display_name, bio, avatar_asset,
125126
publication_mode, status, created_at, updated_at
126-
) VALUES (?, ?, ?, ?, '', 'test.svg', 'direct_publish', 'active', ?, ?)
127-
`).bind(agentId, agentId, agentId, agentId, now, now).run();
127+
) VALUES (?, ?, ?, ?, ?, '', 'test.svg', 'direct_publish', 'active', ?, ?)
128+
`).bind(agentId, agentId, agentId, handleSkeleton(agentId), agentId, now, now).run();
128129
await db.prepare(`
129130
INSERT INTO agent_credentials (
130131
id, agent_id, secret_digest, hash_version, scopes,
@@ -157,10 +158,10 @@ async function seedPublicWorld(
157158
for (const [handle, avatar, accent] of agents) {
158159
await db.prepare(`
159160
INSERT INTO agents (
160-
id, handle, handle_normalized, display_name, bio, avatar_asset,
161+
id, handle, handle_normalized, handle_skeleton, display_name, bio, avatar_asset,
161162
accent, publication_mode, status, created_at, updated_at
162-
) VALUES (?, ?, ?, ?, '', ?, ?, 'direct_publish', 'active', ?, ?)
163-
`).bind(handle, handle, handle, handle, avatar, accent, now, now).run();
163+
) VALUES (?, ?, ?, ?, ?, '', ?, ?, 'direct_publish', 'active', ?, ?)
164+
`).bind(handle, handle, handle, handleSkeleton(handle), handle, avatar, accent, now, now).run();
164165
}
165166

166167
await db.prepare(`
@@ -544,10 +545,10 @@ async function handleAction(body: ActionRequest, env: Environment): Promise<Resp
544545
await seedOwner(env.DB, sponsorId, now);
545546
await env.DB.prepare(`
546547
INSERT OR IGNORE INTO agents (
547-
id, handle, handle_normalized, display_name, bio, avatar_asset,
548+
id, handle, handle_normalized, handle_skeleton, display_name, bio, avatar_asset,
548549
publication_mode, status, created_at, updated_at
549-
) VALUES (?, ?, ?, ?, '', 'test.svg', 'direct_publish', 'active', ?, ?)
550-
`).bind(agentId, agentId, agentId, agentId, now, now).run();
550+
) VALUES (?, ?, ?, ?, ?, '', 'test.svg', 'direct_publish', 'active', ?, ?)
551+
`).bind(agentId, agentId, agentId, handleSkeleton(agentId), agentId, now, now).run();
551552

552553
for (const suffix of ['one', 'two']) {
553554
const recordId = stringValue(data, `record_${suffix}`);

0 commit comments

Comments
 (0)