1- import { createHash , randomUUID } from "node:crypto" ;
1+ import { randomUUID } from "node:crypto" ;
22import type { spawn } from "node:child_process" ;
33import { once } from "node:events" ;
44import { createWriteStream } from "node:fs" ;
@@ -27,6 +27,8 @@ export type FindResult = {
2727
2828export type CaptureMode = "semantic" | "keyword" ;
2929export type ScopeName = "user" | "agent" ;
30+ export type AgentScopeMode = "user_agent" | "agent" ;
31+ export type ServerAuthMode = "api_key" | "trusted" ;
3032export type RuntimeIdentity = {
3133 userId : string ;
3234 agentId : string ;
@@ -171,17 +173,20 @@ export const localClientCache = new Map<string, LocalClientCacheEntry>();
171173export const localClientPendingPromises = new Map < string , PendingClientEntry > ( ) ;
172174
173175const MEMORY_URI_PATTERNS = [
174- / ^ v i k i n g : \/ \/ u s e r \/ (?: [ ^ / ] + \/ ) ? m e m o r i e s (?: \/ | $ ) / ,
175- / ^ v i k i n g : \/ \/ a g e n t \/ (?: [ ^ / ] + \/ ) ? m e m o r i e s (?: \/ | $ ) / ,
176+ / ^ v i k i n g : \/ \/ u s e r \/ (?: [ ^ / ] + (?: \/ a g e n t \/ [ ^ / ] + ) ? \/ ) ? m e m o r i e s (?: \/ | $ ) / ,
177+ / ^ v i k i n g : \/ \/ a g e n t \/ (?: [ ^ / ] + (?: \/ u s e r \/ [ ^ / ] + ) ? \/ ) ? m e m o r i e s (?: \/ | $ ) / ,
176178] ;
177- const USER_STRUCTURE_DIRS = new Set ( [ "memories" ] ) ;
178- const AGENT_STRUCTURE_DIRS = new Set ( [ "memories" , "skills" , "instructions" , "workspaces" ] ) ;
179+ const USER_STRUCTURE_DIRS = new Set ( [ "memories" , "profile.md" , ".abstract.md" , ".overview.md" ] ) ;
180+ const AGENT_STRUCTURE_DIRS = new Set ( [
181+ "memories" ,
182+ "skills" ,
183+ "instructions" ,
184+ "workspaces" ,
185+ ".abstract.md" ,
186+ ".overview.md" ,
187+ ] ) ;
179188const REMOTE_RESOURCE_PREFIXES = [ "http://" , "https://" , "git@" , "ssh://" , "git://" ] ;
180189
181- function md5Short ( input : string ) : string {
182- return createHash ( "md5" ) . update ( input ) . digest ( "hex" ) . slice ( 0 , 12 ) ;
183- }
184-
185190export function isMemoryUri ( uri : string ) : boolean {
186191 return MEMORY_URI_PATTERNS . some ( ( pattern ) => pattern . test ( uri ) ) ;
187192}
@@ -211,25 +216,57 @@ async function cleanupUploadTempPath(path?: string): Promise<void> {
211216}
212217
213218export class OpenVikingClient {
214- private spaceCache = new Map < string , Partial < Record < ScopeName , string > > > ( ) ;
215219 private identityCache = new Map < string , RuntimeIdentity > ( ) ;
216220
217221 constructor (
218222 private readonly baseUrl : string ,
219223 private readonly apiKey : string ,
220224 private readonly defaultAgentId : string ,
221225 private readonly timeoutMs : number ,
226+ private readonly serverAuthMode : ServerAuthMode = "api_key" ,
222227 /** When set (or defaulted), sent so ROOT key can access tenant-scoped APIs. */
223228 private readonly accountId : string = "" ,
224229 private readonly userId : string = "" ,
225230 /** When set, logs routing for find + session writes (tenant headers + paths; never apiKey). */
226231 private readonly routingDebugLog ?: ( message : string ) => void ,
232+ private readonly isolateUserScopeByAgent = false ,
233+ private readonly isolateAgentScopeByUser = true ,
227234 ) { }
228235
229236 getDefaultAgentId ( ) : string {
230237 return this . defaultAgentId ;
231238 }
232239
240+ async getResolvedIdentity ( agentId ?: string ) : Promise < RuntimeIdentity > {
241+ return this . getRuntimeIdentity ( agentId ) ;
242+ }
243+
244+ private resolveTenantHeaders ( ) :
245+ | { apiKey ?: string ; accountId ?: string ; userId ?: string }
246+ {
247+ const apiKey = this . apiKey . trim ( ) ;
248+ const accountId = this . accountId . trim ( ) ;
249+ const userId = this . userId . trim ( ) ;
250+ if ( this . serverAuthMode === "trusted" ) {
251+ return {
252+ ...( apiKey ? { apiKey } : { } ) ,
253+ accountId : accountId || "default" ,
254+ userId : userId || "default" ,
255+ } ;
256+ }
257+ if ( apiKey ) {
258+ return {
259+ apiKey,
260+ ...( accountId ? { accountId } : { } ) ,
261+ ...( userId ? { userId } : { } ) ,
262+ } ;
263+ }
264+ return {
265+ accountId : accountId || "default" ,
266+ userId : userId || "default" ,
267+ } ;
268+ }
269+
233270 private async emitRoutingDebug (
234271 label : string ,
235272 detail : Record < string , unknown > ,
@@ -240,16 +277,17 @@ export class OpenVikingClient {
240277 }
241278 const effectiveAgentId = agentId ?? this . defaultAgentId ;
242279 const identity = await this . getRuntimeIdentity ( agentId ) ;
280+ const tenantHeaders = this . resolveTenantHeaders ( ) ;
243281 this . routingDebugLog (
244282 `openviking: ${ label } ` +
245283 JSON . stringify ( {
246284 ...detail ,
247285 X_OpenViking_Agent : effectiveAgentId ,
248- X_OpenViking_Account : this . accountId . trim ( ) || "default" ,
249- X_OpenViking_User : this . userId . trim ( ) || "default" ,
286+ X_OpenViking_Account : tenantHeaders . accountId ?? null ,
287+ X_OpenViking_User : tenantHeaders . userId ?? null ,
250288 resolved_user_id : identity . userId ,
251289 session_vfs_hint : detail . sessionId
252- ? `viking://session/${ identity . userId } / ${ String ( detail . sessionId ) } `
290+ ? `viking://session/${ String ( detail . sessionId ) } `
253291 : undefined ,
254292 } ) ,
255293 ) ;
@@ -266,11 +304,16 @@ export class OpenVikingClient {
266304 const timer = setTimeout ( ( ) => controller . abort ( ) , requestTimeoutMs ?? this . timeoutMs ) ;
267305 try {
268306 const headers = new Headers ( init . headers ?? { } ) ;
269- if ( this . apiKey ) {
270- headers . set ( "X-API-Key" , this . apiKey ) ;
307+ const tenantHeaders = this . resolveTenantHeaders ( ) ;
308+ if ( tenantHeaders . apiKey ) {
309+ headers . set ( "X-API-Key" , tenantHeaders . apiKey ) ;
310+ }
311+ if ( tenantHeaders . accountId ) {
312+ headers . set ( "X-OpenViking-Account" , tenantHeaders . accountId ) ;
313+ }
314+ if ( tenantHeaders . userId ) {
315+ headers . set ( "X-OpenViking-User" , tenantHeaders . userId ) ;
271316 }
272- headers . set ( "X-OpenViking-Account" , this . accountId . trim ( ) || "default" ) ;
273- headers . set ( "X-OpenViking-User" , this . userId . trim ( ) || "default" ) ;
274317 if ( effectiveAgentId ) {
275318 headers . set ( "X-OpenViking-Agent" , effectiveAgentId ) ;
276319 }
@@ -306,14 +349,6 @@ export class OpenVikingClient {
306349 await this . request < { status : string } > ( "/health" ) ;
307350 }
308351
309- private async ls ( uri : string , agentId ?: string ) : Promise < Array < Record < string , unknown > > > {
310- return this . request < Array < Record < string , unknown > > > (
311- `/api/v1/fs/ls?uri=${ encodeURIComponent ( uri ) } &output=original` ,
312- { } ,
313- agentId ,
314- ) ;
315- }
316-
317352 private async getRuntimeIdentity ( agentId ?: string ) : Promise < RuntimeIdentity > {
318353 const effectiveAgentId = agentId ?? this . defaultAgentId ;
319354 const cached = this . identityCache . get ( effectiveAgentId ) ;
@@ -334,54 +369,18 @@ export class OpenVikingClient {
334369 }
335370 }
336371
337- private async resolveScopeSpace ( scope : ScopeName , agentId ?: string ) : Promise < string > {
338- const effectiveAgentId = agentId ?? this . defaultAgentId ;
339- const agentScopes = this . spaceCache . get ( effectiveAgentId ) ;
340- const cached = agentScopes ?. [ scope ] ;
341- if ( cached ) {
342- return cached ;
343- }
344-
372+ private async buildCanonicalRoot ( scope : ScopeName , agentId ?: string ) : Promise < string > {
345373 const identity = await this . getRuntimeIdentity ( agentId ) ;
346- const fallbackSpace =
347- scope === "user" ? identity . userId : md5Short ( `${ identity . userId } :${ identity . agentId } ` ) ;
348- const reservedDirs = scope === "user" ? USER_STRUCTURE_DIRS : AGENT_STRUCTURE_DIRS ;
349- const preferredSpace =
350- scope === "user" ? identity . userId : md5Short ( `${ identity . userId } :${ identity . agentId } ` ) ;
351-
352- const saveSpace = ( space : string ) => {
353- const existing = this . spaceCache . get ( effectiveAgentId ) ?? { } ;
354- existing [ scope ] = space ;
355- this . spaceCache . set ( effectiveAgentId , existing ) ;
356- } ;
357-
358- try {
359- const entries = await this . ls ( `viking://${ scope } ` , agentId ) ;
360- const spaces = entries
361- . filter ( ( entry ) => entry ?. isDir === true )
362- . map ( ( entry ) => ( typeof entry . name === "string" ? entry . name . trim ( ) : "" ) )
363- . filter ( ( name ) => name && ! name . startsWith ( "." ) && ! reservedDirs . has ( name ) ) ;
364-
365- if ( spaces . length > 0 ) {
366- if ( spaces . includes ( preferredSpace ) ) {
367- saveSpace ( preferredSpace ) ;
368- return preferredSpace ;
369- }
370- if ( scope === "user" && spaces . includes ( "default" ) ) {
371- saveSpace ( "default" ) ;
372- return "default" ;
373- }
374- if ( spaces . length === 1 ) {
375- saveSpace ( spaces [ 0 ] ! ) ;
376- return spaces [ 0 ] ! ;
377- }
378- }
379- } catch {
380- // Fall back to identity-derived space when listing fails.
374+ if ( scope === "user" ) {
375+ const root = this . isolateUserScopeByAgent
376+ ? `viking://user/${ identity . userId } /agent/${ identity . agentId } `
377+ : `viking://user/${ identity . userId } ` ;
378+ return root ;
381379 }
382-
383- saveSpace ( fallbackSpace ) ;
384- return fallbackSpace ;
380+ const root = this . isolateAgentScopeByUser
381+ ? `viking://agent/${ identity . agentId } /user/${ identity . userId } `
382+ : `viking://agent/${ identity . agentId } ` ;
383+ return root ;
385384 }
386385
387386 private async normalizeTargetUri ( targetUri : string , agentId ?: string ) : Promise < string > {
@@ -405,8 +404,8 @@ export class OpenVikingClient {
405404 return trimmed ;
406405 }
407406
408- const space = await this . resolveScopeSpace ( scope , agentId ) ;
409- return `viking:// ${ scope } / ${ space } /${ parts . join ( "/" ) } ` ;
407+ const root = await this . buildCanonicalRoot ( scope , agentId ) ;
408+ return `${ root } /${ parts . join ( "/" ) } ` ;
410409 }
411410
412411 async find (
@@ -427,12 +426,13 @@ export class OpenVikingClient {
427426 } ;
428427 const effectiveAgentId = agentId ?? this . defaultAgentId ;
429428 const identity = await this . getRuntimeIdentity ( agentId ) ;
429+ const tenantHeaders = this . resolveTenantHeaders ( ) ;
430430 this . routingDebugLog ?.(
431431 `openviking: find POST ${ this . baseUrl } /api/v1/search/find ` +
432432 JSON . stringify ( {
433433 X_OpenViking_Agent : effectiveAgentId ,
434- X_OpenViking_Account : this . accountId . trim ( ) || "default" ,
435- X_OpenViking_User : this . userId . trim ( ) || "default" ,
434+ X_OpenViking_Account : tenantHeaders . accountId ?? null ,
435+ X_OpenViking_User : tenantHeaders . userId ?? null ,
436436 resolved_user_id : identity . userId ,
437437 target_uri : normalizedTargetUri ,
438438 target_uri_input : options . targetUri ,
@@ -645,21 +645,27 @@ export class OpenVikingClient {
645645 } > ,
646646 agentId ?: string ,
647647 createdAt ?: string ,
648+ roleId ?: string ,
648649 ) : Promise < void > {
649650 const body : {
650651 role : string ;
652+ role_id ?: string ;
651653 parts : typeof parts ;
652654 created_at ?: string ;
653655 } = { role, parts } ;
654656 if ( createdAt ) {
655657 body . created_at = createdAt ;
656658 }
659+ if ( roleId ) {
660+ body . role_id = roleId ;
661+ }
657662 await this . emitRoutingDebug (
658663 "session message POST (with parts)" ,
659664 {
660665 path : `/api/v1/sessions/${ encodeURIComponent ( sessionId ) } /messages` ,
661666 sessionId,
662667 role,
668+ role_id : roleId ?? null ,
663669 partCount : parts . length ,
664670 created_at : createdAt ?? null ,
665671 } ,
0 commit comments