@@ -42,6 +42,14 @@ export interface Agent {
4242 origin ?: "in-memory" | "registry" ;
4343 registeredBy ?: string | null ;
4444 lastSeen ?: string | null ;
45+ /** True when the agent is archived: kept (with all its history) but refused
46+ * by every execution path. The UI lists archived agents in a separate,
47+ * greyed section and disables chat/run for them. Unset/false ⇒ active. */
48+ archived ?: boolean ;
49+ archivedAt ?: string | null ;
50+ /** Owning group (visibility) + owning user id (mutate/delete). */
51+ ownerGroup ?: string | null ;
52+ ownerUser ?: string | null ;
4553}
4654
4755export interface RegisterAgentInput {
@@ -51,6 +59,8 @@ export interface RegisterAgentInput {
5159 source ?: string ;
5260 model ?: string ;
5361 registeredBy ?: string ;
62+ /** The group the new agent belongs to (one of the creator's groups). */
63+ ownerGroup ?: string ;
5464}
5565
5666/** Result of `displaySource(agent.source)`. Drives `<SourceBadge>` rendering. */
@@ -219,29 +229,134 @@ export interface NewSchedule {
219229 minuteUtc ?: number ;
220230}
221231
232+ // API keys — minted + stored (hashed) by the server; the plaintext is returned
233+ // exactly once on create. List/responses are redacted (prefix + last4 only).
234+ export interface ApiKey {
235+ _id : string ;
236+ prefix : string ;
237+ last4 : string ;
238+ label : string ;
239+ /** Display label of the group/role the key acts as. */
240+ group ?: string | null ;
241+ /** Roles the key inherits → resolved to permissions via the role map. */
242+ roleIds ?: string [ ] ;
243+ scopes ?: string [ ] ; // DEPRECATED
244+ createdBy : string ;
245+ createdAt : string ;
246+ expiresAt ?: string | null ;
247+ lastUsedAt ?: string | null ;
248+ revoked : boolean ;
249+ revokedAt ?: string | null ;
250+ }
251+
252+ // Current principal, from GET /me. Drives the SPA's permission gating.
253+ export interface Me {
254+ id : string ; // principal id (Keycloak sub) — compare to resource ownerUser
255+ user : string ;
256+ displayName ?: string | null ;
257+ source : "oidc" | "api-key" | "cookie" | "dev" ;
258+ kind : "user" | "service" ;
259+ roles : string [ ] ;
260+ groups : string [ ] ;
261+ permissions : string [ ] ;
262+ }
263+
264+ // Editable role → permission map (Settings → Roles).
265+ export interface Role {
266+ _id : string ;
267+ description : string ;
268+ permissions : string [ ] ;
269+ builtin : boolean ;
270+ updatedAt ?: string ;
271+ }
272+ export interface PermissionDef {
273+ key : string ;
274+ description : string ;
275+ }
276+
277+ // Groups — read-only, sourced from Keycloak (Okta/Keycloak own groups + membership).
278+ export interface Group {
279+ id : string ;
280+ name : string ;
281+ path : string ;
282+ }
283+ export interface GroupMember {
284+ id : string ;
285+ username : string | null ;
286+ email : string | null ;
287+ name : string | null ;
288+ roles : string [ ] ;
289+ }
290+
291+ // ── Auth: reactive token refresh ─────────────────────────────────────────────
292+ // The BFF session cookie is short-lived (it tracks the Keycloak access-token
293+ // expiry, ~5 min). When a dashboard request 401s we silently POST /auth/refresh
294+ // (which rotates the server-held refresh token and re-signs the cookie) and
295+ // replay the original request once. All concurrent 401s share ONE in-flight
296+ // refresh — refresh tokens rotate and can be spent only once, so a stampede
297+ // would invalidate itself. On a hard refresh failure the session is truly gone:
298+ // we notify AuthContext (→ SSO sign-in screen).
299+
300+ let refreshInFlight : Promise < boolean > | null = null ;
301+ let onAuthLost : ( ( ) => void ) | null = null ;
302+
303+ /** AuthContext registers a callback here to flip to the anonymous/login state
304+ * when the refresh token is dead (idle timeout / revocation / logout). */
305+ export function setAuthLostHandler ( fn : ( ( ) => void ) | null ) : void {
306+ onAuthLost = fn ;
307+ }
308+
309+ function tryRefresh ( ) : Promise < boolean > {
310+ if ( ! refreshInFlight ) {
311+ refreshInFlight = fetch ( `/api/v1/auth/refresh` , {
312+ method : "POST" ,
313+ headers : { accept : "application/json" } ,
314+ credentials : "include" ,
315+ } )
316+ . then ( ( r ) => r . ok )
317+ . catch ( ( ) => false )
318+ . finally ( ( ) => {
319+ refreshInFlight = null ;
320+ } ) ;
321+ }
322+ return refreshInFlight ;
323+ }
324+
325+ /** fetch against the dashboard API with credentials. On 401, refresh once and
326+ * replay; if refresh fails, signal auth-lost and return the 401 response. */
327+ async function authedFetch ( path : string , init : RequestInit ) : Promise < Response > {
328+ const url = `/api/v1${ path } ` ;
329+ const opts : RequestInit = { credentials : "include" , ...init } ;
330+ let r = await fetch ( url , opts ) ;
331+ if ( r . status === 401 ) {
332+ const ok = await tryRefresh ( ) ;
333+ if ( ok ) {
334+ r = await fetch ( url , opts ) ;
335+ } else {
336+ onAuthLost ?.( ) ;
337+ }
338+ }
339+ return r ;
340+ }
341+
222342async function getJSON < T > ( path : string ) : Promise < T > {
223- const r = await fetch ( `/api${ path } ` , {
224- headers : { accept : "application/json" } ,
225- credentials : "include" ,
226- } ) ;
343+ const r = await authedFetch ( path , { headers : { accept : "application/json" } } ) ;
227344 if ( ! r . ok ) throw new Error ( `${ path } → ${ r . status } ` ) ;
228345 return r . json ( ) as Promise < T > ;
229346}
230347async function postJSON < T > ( path : string , body : unknown ) : Promise < T > {
231- const r = await fetch ( `/api ${ path } ` , {
348+ const r = await authedFetch ( path , {
232349 method : "POST" ,
233350 headers : { "content-type" : "application/json" , accept : "application/json" } ,
234- credentials : "include" ,
235351 body : JSON . stringify ( body ) ,
236352 } ) ;
237353 if ( ! r . ok ) throw new Error ( `${ path } → ${ r . status } ` ) ;
238354 return r . json ( ) as Promise < T > ;
239355}
240356async function reqJSON < T > ( method : string , path : string , body ?: unknown ) : Promise < T > {
241- const r = await fetch ( `/api ${ path } ` , {
357+ const r = await authedFetch ( path , {
242358 method,
243359 headers : { "content-type" : "application/json" , accept : "application/json" } ,
244- credentials : "include" ,
245360 ...( body !== undefined ? { body : JSON . stringify ( body ) } : { } ) ,
246361 } ) ;
247362 if ( ! r . ok ) throw new Error ( `${ path } → ${ r . status } ` ) ;
@@ -345,6 +460,13 @@ export const api = {
345460 reqJSON < DeleteResult > ( "DELETE" , `/agents/${ encodeURIComponent ( agentId ) } ` ) ,
346461 patchAgent : ( agentId : string , fields : Partial < Omit < RegisterAgentInput , "name" > > ) =>
347462 reqJSON < { ok : boolean } > ( "PATCH" , `/agents/${ encodeURIComponent ( agentId ) } ` , fields ) ,
463+ archiveAgent : ( agentId : string ) =>
464+ reqJSON < { ok : boolean ; disposed : number ; schedulesDisabled : number ; warnings : string [ ] } > (
465+ "POST" ,
466+ `/agents/${ encodeURIComponent ( agentId ) } /archive` ,
467+ ) ,
468+ unarchiveAgent : ( agentId : string ) =>
469+ reqJSON < { ok : boolean } > ( "POST" , `/agents/${ encodeURIComponent ( agentId ) } /unarchive` ) ,
348470 logs : ( agentId ?: string , limit = 100 ) =>
349471 getJSON < { logs : LogEntry [ ] } > ( `/logs?limit=${ limit } ${ agentId ? `&agentId=${ encodeURIComponent ( agentId ) } ` : "" } ` ) . then ( ( d ) => d . logs ) ,
350472 sessions : ( agentId ?: string , limit = 100 ) =>
@@ -363,9 +485,9 @@ export const api = {
363485 logWebTurn : ( entry : { bot : string ; sessionId : string ; query : string ; reply : string ; ok : boolean } ) =>
364486 postJSON < { ok : boolean } > ( "/logs" , { ...entry , requester : "web" } ) ,
365487 // SSE chat — caller reads the stream. Path goes through the same /api proxy.
366- chatStreamUrl : ( sandboxId : string ) => `/api/sandboxes/${ encodeURIComponent ( sandboxId ) } /chat` ,
488+ chatStreamUrl : ( sandboxId : string ) => `/api/v1/ sandboxes/${ encodeURIComponent ( sandboxId ) } /chat` ,
367489 // SSE one-shot run (deepagents). Server builds the /run body from {message}.
368- runStreamUrl : ( agentId : string ) => `/api/agents/${ encodeURIComponent ( agentId ) } /run` ,
490+ runStreamUrl : ( agentId : string ) => `/api/v1/ agents/${ encodeURIComponent ( agentId ) } /run` ,
369491 // Schedules
370492 schedules : ( agentId ?: string ) =>
371493 getJSON < { schedules : Schedule [ ] } > ( `/schedules${ agentId ? `?agentId=${ encodeURIComponent ( agentId ) } ` : "" } ` ) . then ( ( d ) => d . schedules ) ,
@@ -397,6 +519,45 @@ export const api = {
397519 deleteOpaPolicy : ( id : string ) =>
398520 reqJSON < { success ?: boolean } > ( "DELETE" , `/opa-policies/${ encodeURIComponent ( id ) } ` ) ,
399521
522+ // Current principal + session.
523+ auth : {
524+ me : ( ) => getJSON < Me > ( "/me" ) ,
525+ logout : ( ) => postJSON < { ok : boolean ; logoutUrl ?: string } > ( "/logout" , { } ) ,
526+ loginUrl : ( ) => "/api/v1/auth/login" ,
527+ } ,
528+
529+ // Roles — editable role→permission map + the permission catalog.
530+ roles : {
531+ list : ( ) => getJSON < { roles : Role [ ] } > ( "/roles" ) . then ( ( d ) => d . roles ) ,
532+ permissions : ( ) => getJSON < { permissions : PermissionDef [ ] } > ( "/permissions" ) . then ( ( d ) => d . permissions ) ,
533+ create : ( body : { name : string ; description ?: string ; permissions : string [ ] } ) =>
534+ postJSON < { role : Role } > ( "/roles" , body ) . then ( ( d ) => d . role ) ,
535+ update : ( id : string , body : { description ?: string ; permissions ?: string [ ] } ) =>
536+ reqJSON < { role : Role } > ( "PUT" , `/roles/${ encodeURIComponent ( id ) } ` , body ) . then ( ( d ) => d . role ) ,
537+ remove : ( id : string ) => reqJSON < { ok : boolean } > ( "DELETE" , `/roles/${ encodeURIComponent ( id ) } ` ) ,
538+ } ,
539+
540+ // Groups — read-only window into Keycloak (creation/membership live in Okta/KC).
541+ groups : {
542+ list : ( ) => getJSON < { groups : Group [ ] } > ( "/groups" ) . then ( ( d ) => d . groups ) ,
543+ members : ( id : string ) =>
544+ getJSON < { members : GroupMember [ ] ; truncated : boolean } > ( `/groups/${ encodeURIComponent ( id ) } /members` ) ,
545+ } ,
546+
547+ // API keys — mint (plaintext returned once), list (redacted), revoke. A key is
548+ // minted bound to a group/role and inherits its permissions.
549+ apiKeys : {
550+ list : ( ) => getJSON < { apiKeys : ApiKey [ ] } > ( "/api-keys" ) . then ( ( d ) => d . apiKeys ) ,
551+ create : ( label : string , opts ?: { expiresAt ?: string | null ; group ?: string | null ; roleIds ?: string [ ] } ) =>
552+ postJSON < { key : string ; apiKey : ApiKey } > ( "/api-keys" , {
553+ label,
554+ ...( opts ?. expiresAt ? { expiresAt : opts . expiresAt } : { } ) ,
555+ ...( opts ?. group ? { group : opts . group } : { } ) ,
556+ ...( opts ?. roleIds ?. length ? { roleIds : opts . roleIds } : { } ) ,
557+ } ) ,
558+ revoke : ( id : string ) => reqJSON < { ok : boolean } > ( "DELETE" , `/api-keys/${ encodeURIComponent ( id ) } ` ) ,
559+ } ,
560+
400561 // Evals — suite CRUD + run trigger + run readback.
401562 evals : {
402563 listSuites : ( ) => getJSON < { suites : EvalSuite [ ] } > ( "/evals/suites" ) . then ( ( d ) => d . suites ) ,
0 commit comments