@@ -6,6 +6,21 @@ import { runtimeReadyPredicateSql } from "./machineRepo";
66
77const parseAgent = < T extends Agent > ( row : T ) => parseJsonFields ( row , [ "skills" , "subagents" , "handoff_to" ] ) ;
88
9+ async function sha1 ( value : string ) : Promise < string > {
10+ const bytes = new TextEncoder ( ) . encode ( value ) ;
11+ const hash = await crypto . subtle . digest ( "SHA-1" , bytes ) ;
12+ return [ ...new Uint8Array ( hash ) ] . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) ) . join ( "" ) ;
13+ }
14+
15+ async function nextVersion ( db : D1 , username : string , builtin : boolean ) : Promise < string > {
16+ if ( builtin ) return "latest" ;
17+ const row = await db
18+ . prepare ( "SELECT MAX(CAST(version AS INTEGER)) as version FROM agents WHERE username = ? AND version != 'latest'" )
19+ . bind ( username )
20+ . first < { version : number | null } > ( ) ;
21+ return String ( ( row ?. version ?? 0 ) + 1 ) ;
22+ }
23+
924export interface PreparedAgent extends Agent {
1025 privateKeyJwk : JsonWebKey ;
1126}
@@ -26,21 +41,24 @@ export async function prepareAgent(
2641) : Promise < PreparedAgent > {
2742 const { id, publicKeyBase64, fingerprint, privateKeyJwk } = identity ;
2843 const now = new Date ( ) . toISOString ( ) ;
44+ const soul = input . soul ?? null ;
2945 return {
3046 id,
3147 owner_id : ownerId ,
3248 name : input . name || input . username ,
3349 username : input . username ,
3450 gpg_subkey_id : null ,
3551 bio : input . bio ?? null ,
36- soul : input . soul ?? null ,
52+ soul,
3753 role : input . role ?? null ,
3854 kind : input . kind ?? "worker" ,
3955 handoff_to : input . handoff_to ?? null ,
4056 runtime : input . runtime ,
4157 model : input . model ?? null ,
4258 skills : input . skills ?? null ,
4359 subagents : input . subagents ?? null ,
60+ version : await nextVersion ( db , input . username , builtin ) ,
61+ soul_sha1 : await sha1 ( soul ?? "" ) ,
4462 public_key : publicKeyBase64 ,
4563 fingerprint,
4664 builtin : builtin ? 1 : 0 ,
@@ -56,8 +74,8 @@ export async function insertAgent(db: D1, agent: PreparedAgent, extras?: { mailb
5674 const handoffJson = agent . handoff_to ? JSON . stringify ( agent . handoff_to ) : null ;
5775 await db
5876 . prepare ( `
59- INSERT INTO agents (id, owner_id, name, username, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents, public_key, private_key, fingerprint, builtin, mailbox_token, gpg_subkey_id, created_at, updated_at)
60- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
77+ INSERT INTO agents (id, owner_id, name, username, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents, version, soul_sha1, public_key, private_key, fingerprint, builtin, mailbox_token, gpg_subkey_id, created_at, updated_at)
78+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
6179 ` )
6280 . bind (
6381 agent . id ,
@@ -73,6 +91,8 @@ export async function insertAgent(db: D1, agent: PreparedAgent, extras?: { mailb
7391 agent . model ,
7492 skillsJson ,
7593 subagentsJson ,
94+ agent . version ,
95+ agent . soul_sha1 ,
7696 agent . public_key ,
7797 JSON . stringify ( agent . privateKeyJwk ) ,
7898 agent . fingerprint ,
@@ -127,6 +147,7 @@ export async function listAgents(db: D1, ownerId: string): Promise<AgentWithActi
127147 const result = await db
128148 . prepare ( `
129149 SELECT a.id, a.owner_id, a.name, a.username, a.gpg_subkey_id, a.bio, a.soul, a.role, a.kind, a.handoff_to, a.runtime, a.model, a.skills, a.subagents,
150+ a.version, a.soul_sha1,
130151 a.public_key, a.fingerprint, a.builtin, a.created_at, a.updated_at,
131152 CASE WHEN EXISTS (
132153 SELECT 1 FROM machines m, json_each(m.runtimes) rt
@@ -159,6 +180,7 @@ export async function getAgent(db: D1, agentId: string, ownerId: string): Promis
159180 return db
160181 . prepare ( `
161182 SELECT a.id, a.owner_id, a.name, a.username, a.gpg_subkey_id, a.bio, a.soul, a.role, a.kind, a.handoff_to, a.runtime, a.model, a.skills, a.subagents,
183+ a.version, a.soul_sha1,
162184 a.public_key, a.fingerprint, a.builtin, a.created_at, a.updated_at,
163185 CASE WHEN EXISTS (
164186 SELECT 1 FROM machines m, json_each(m.runtimes) rt
@@ -192,7 +214,7 @@ export async function updateAgent(
192214) : Promise < Agent | null > {
193215 const agent = await db
194216 . prepare (
195- "SELECT id, owner_id, name, username, gpg_subkey_id, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents, public_key, fingerprint, builtin, created_at, updated_at FROM agents WHERE id = ?" ,
217+ "SELECT id, owner_id, name, username, gpg_subkey_id, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents, version, soul_sha1, public_key, fingerprint, builtin, created_at, updated_at FROM agents WHERE id = ?" ,
196218 )
197219 . bind ( agentId )
198220 . first < Agent > ( ) ;
@@ -213,6 +235,12 @@ export async function updateAgent(
213235 ( applied as any ) [ field ] = val ;
214236 }
215237 }
238+ if ( "soul" in updates && updates . soul !== undefined ) {
239+ const digest = await sha1 ( updates . soul ?? "" ) ;
240+ sets . push ( "soul_sha1 = ?" ) ;
241+ binds . push ( digest ) ;
242+ applied . soul_sha1 = digest ;
243+ }
216244
217245 binds . push ( agentId ) ;
218246 await db
@@ -222,6 +250,106 @@ export async function updateAgent(
222250 return parseAgent ( { ...agent , ...applied , updated_at : now } ) ;
223251}
224252
253+ type AgentSnapshot = Agent & { private_key : string ; mailbox_token : string | null } ;
254+
255+ function jsonOrNull ( value : unknown [ ] | null ) : string | null {
256+ return value ? JSON . stringify ( value ) : null ;
257+ }
258+
259+ async function findLatestAgentId ( db : D1 , username : string ) : Promise < string | null > {
260+ const row = await db . prepare ( "SELECT id FROM agents WHERE username = ? AND version = 'latest'" ) . bind ( username ) . first < { id : string } > ( ) ;
261+ return row ?. id ?? null ;
262+ }
263+
264+ async function updateLatestAgent ( db : D1 , latestId : string , source : AgentSnapshot , updatedAt : string ) : Promise < void > {
265+ await db
266+ . prepare ( `
267+ UPDATE agents
268+ SET name = ?, gpg_subkey_id = ?, bio = ?, soul = ?, role = ?, kind = ?, handoff_to = ?, runtime = ?, model = ?,
269+ skills = ?, subagents = ?, soul_sha1 = ?, public_key = ?, private_key = ?, fingerprint = ?, builtin = ?, mailbox_token = ?, updated_at = ?
270+ WHERE id = ?
271+ ` )
272+ . bind (
273+ source . name ,
274+ source . gpg_subkey_id ,
275+ source . bio ,
276+ source . soul ,
277+ source . role ,
278+ source . kind ,
279+ jsonOrNull ( source . handoff_to ) ,
280+ source . runtime ,
281+ source . model ,
282+ jsonOrNull ( source . skills ) ,
283+ jsonOrNull ( source . subagents ) ,
284+ source . soul_sha1 ,
285+ source . public_key ,
286+ source . private_key ,
287+ source . fingerprint ,
288+ source . builtin ,
289+ source . mailbox_token ,
290+ updatedAt ,
291+ latestId ,
292+ )
293+ . run ( ) ;
294+ }
295+
296+ async function insertLatestAgent ( db : D1 , ownerId : string , source : AgentSnapshot , now : string ) : Promise < string > {
297+ const latestId = crypto . randomUUID ( ) ;
298+ await db
299+ . prepare ( `
300+ INSERT INTO agents (id, owner_id, name, username, gpg_subkey_id, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents, version, soul_sha1, public_key, private_key, fingerprint, builtin, mailbox_token, created_at, updated_at)
301+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'latest', ?, ?, ?, ?, ?, ?, ?, ?)
302+ ` )
303+ . bind (
304+ latestId ,
305+ ownerId ,
306+ source . name ,
307+ source . username ,
308+ source . gpg_subkey_id ,
309+ source . bio ,
310+ source . soul ,
311+ source . role ,
312+ source . kind ,
313+ jsonOrNull ( source . handoff_to ) ,
314+ source . runtime ,
315+ source . model ,
316+ jsonOrNull ( source . skills ) ,
317+ jsonOrNull ( source . subagents ) ,
318+ source . soul_sha1 ,
319+ source . public_key ,
320+ source . private_key ,
321+ source . fingerprint ,
322+ source . builtin ,
323+ source . mailbox_token ,
324+ now ,
325+ now ,
326+ )
327+ . run ( ) ;
328+ return latestId ;
329+ }
330+
331+ export async function publishAgent ( db : D1 , username : string , agentId : string , ownerId : string ) : Promise < Agent | null > {
332+ const target = await db
333+ . prepare (
334+ "SELECT id, owner_id, name, username, gpg_subkey_id, bio, soul, role, kind, handoff_to, runtime, model, skills, subagents, version, soul_sha1, public_key, private_key, fingerprint, builtin, mailbox_token FROM agents WHERE id = ? AND owner_id = ?" ,
335+ )
336+ . bind ( agentId , ownerId )
337+ . first < Agent & { private_key : string ; mailbox_token : string | null } > ( ) ;
338+ if ( ! target ) return null ;
339+ const source = parseAgent ( target ) as AgentSnapshot ;
340+ if ( source . username !== username ) return null ;
341+ if ( source . version === "latest" ) return getAgent ( db , agentId , ownerId ) ;
342+
343+ const now = new Date ( ) . toISOString ( ) ;
344+ const latestId = await findLatestAgentId ( db , source . username ) ;
345+ if ( latestId ) {
346+ await updateLatestAgent ( db , latestId , source , now ) ;
347+ return getAgent ( db , latestId , ownerId ) ;
348+ }
349+
350+ return getAgent ( db , await insertLatestAgent ( db , ownerId , source , now ) , ownerId ) ;
351+ }
352+
225353export async function deleteAgent ( db : D1 , agentId : string ) : Promise < boolean > {
226354 await db . prepare ( "UPDATE tasks SET assigned_to = NULL WHERE assigned_to = ? AND status IN ('todo', 'in_progress')" ) . bind ( agentId ) . run ( ) ;
227355 const result = await db . prepare ( "DELETE FROM agents WHERE id = ?" ) . bind ( agentId ) . run ( ) ;
0 commit comments