@@ -94,32 +94,55 @@ export namespace Server {
9494 if ( ! CoreFlag . ALTIMATE_WORKSPACE ) {
9595 return { status : 409 , body : { ok : false , error : "Workspace mode is not enabled for this server." } }
9696 }
97+ return browserOriginRefusal ( "Workspace actions" , origin , host , password , fetchSite )
98+ }
99+ /** Why a browser-originated call to a local-only `/altimate/*` route must be refused, or undefined
100+ * when it may run. `subject` names the routes in the error ("Workspace actions", "Traces"). */
101+ export function browserOriginRefusal (
102+ subject : string ,
103+ origin : string | undefined ,
104+ host : string | undefined ,
105+ password : string | undefined = Flag . OPENCODE_SERVER_PASSWORD ,
106+ fetchSite ?: string ,
107+ ) : { status : 403 ; body : { ok : false ; error : string } } | undefined {
97108 // A browser labels every request it sends, including Origin-less ones such as an `<img>` GET
98109 // from another site. Native clients send no such header, so only a browser's cross-site request
99110 // is refused here; the Origin rules below handle the rest.
100111 if ( fetchSite && fetchSite !== "same-origin" && fetchSite !== "none" ) {
101- log . warn ( "refused cross-site workspace action " , { fetchSite } )
102- return { status : 403 , body : { ok : false , error : "Workspace actions cannot be run from another site." } }
112+ log . warn ( "refused cross-site request " , { subject , fetchSite } )
113+ return { status : 403 , body : { ok : false , error : ` ${ subject } cannot be run from another site.` } }
103114 }
104115 if ( ! origin ) return undefined
105116 if ( ! password ) {
106- log . warn ( "refused browser-originated workspace action on an unsecured server" , { origin } )
117+ log . warn ( "refused browser-originated request on an unsecured server" , { subject , origin } )
107118 return {
108119 status : 403 ,
109120 body : {
110121 ok : false ,
111- error : "Workspace actions cannot be run from a browser origin on an unsecured server. Set OPENCODE_SERVER_PASSWORD." ,
122+ error : ` ${ subject } cannot be run from a browser origin on an unsecured server. Set OPENCODE_SERVER_PASSWORD.` ,
112123 } ,
113124 }
114125 }
115126 // With a password set, basicAuth has vetted the credentials — but a browser replays cached
116- // Basic credentials on a cross-site form POST too, so only this server's own pages may call.
127+ // Basic credentials on a cross-site request too, so only this server's own pages may call.
117128 if ( ! sameOrigin ( origin , host ) ) {
118- log . warn ( "refused cross-origin workspace action " , { origin } )
119- return { status : 403 , body : { ok : false , error : "Workspace actions cannot be run from another origin." } }
129+ log . warn ( "refused cross-origin request " , { subject , origin } )
130+ return { status : 403 , body : { ok : false , error : ` ${ subject } cannot be run from another origin.` } }
120131 }
121132 return undefined
122133 }
134+ const TRACE_PAGE_SIZE = 50
135+ const TRACE_SESSION_ID = / ^ [ A - Z a - z 0 - 9 _ - ] { 1 , 128 } $ /
136+ /** The traces directory, honoring `tracing.dir` like the CLI and TUI; a config that fails to load
137+ * falls back to the default rather than hiding every trace. */
138+ async function tracesDir ( ) : Promise < string | undefined > {
139+ try {
140+ const { Config } = await import ( "../config/config" )
141+ return ( await Config . get ( ) ) . tracing ?. dir
142+ } catch {
143+ return undefined
144+ }
145+ }
123146 /** The skill registry this instance serves, reloaded so a skill written since it loaded is found
124147 * — also one in a skills directory that did not exist at boot. Same in-context path as
125148 * `refreshSkillRegistry` in session/prompt.ts: the facade's invalidate keeps the stale root list
@@ -1249,6 +1272,77 @@ export namespace Server {
12491272 }
12501273 } )
12511274 // altimate_change end
1275+ // altimate_change start — GET /altimate/trace, GET /altimate/trace/:sessionID/view
1276+ // The TUI's `/traces` for the IDE extension, which runs this CLI headless: a page of the
1277+ // session traces and one trace's self-contained viewer page. Trace content carries prompts and
1278+ // tool output, so a browser origin is refused on the same terms as the workspace routes.
1279+ . get ( "/altimate/trace" , async ( c ) => {
1280+ const refused = browserOriginRefusal (
1281+ "Traces" ,
1282+ c . req . header ( "origin" ) ,
1283+ c . req . header ( "host" ) ,
1284+ undefined ,
1285+ c . req . header ( "sec-fetch-site" ) ,
1286+ )
1287+ if ( refused ) return c . json ( refused . body , refused . status )
1288+ try {
1289+ const { Trace } = await import ( "../altimate/observability/tracing" )
1290+ const page = await Trace . listTracesPaginated ( await tracesDir ( ) , {
1291+ offset : Number ( c . req . query ( "offset" ) ?? 0 ) ,
1292+ limit : Number ( c . req . query ( "limit" ) ?? TRACE_PAGE_SIZE ) ,
1293+ } )
1294+ return c . json ( {
1295+ ok : true as const ,
1296+ total : page . total ,
1297+ offset : page . offset ,
1298+ limit : page . limit ,
1299+ traces : page . traces . map ( ( { sessionId, trace } ) => ( {
1300+ sessionID : sessionId ,
1301+ title : trace . metadata . title || trace . metadata . prompt || sessionId ,
1302+ startedAt : trace . startedAt ,
1303+ status : trace . summary . status ,
1304+ duration : trace . summary . duration ,
1305+ totalTokens : trace . summary . totalTokens ,
1306+ totalCost : trace . summary . totalCost ,
1307+ totalToolCalls : trace . summary . totalToolCalls ,
1308+ } ) ) ,
1309+ } )
1310+ } catch ( err ) {
1311+ const error = err instanceof Error ? err . message : String ( err )
1312+ log . error ( "trace list: failed" , { error } )
1313+ return c . json ( { ok : false , error } , 500 )
1314+ }
1315+ } )
1316+ . get ( "/altimate/trace/:sessionID/view" , async ( c ) => {
1317+ const refused = browserOriginRefusal (
1318+ "Traces" ,
1319+ c . req . header ( "origin" ) ,
1320+ c . req . header ( "host" ) ,
1321+ undefined ,
1322+ c . req . header ( "sec-fetch-site" ) ,
1323+ )
1324+ if ( refused ) return c . json ( refused . body , refused . status )
1325+ const sessionID = c . req . param ( "sessionID" )
1326+ // Trace files are `<sessionID>.json` in the traces dir; anything outside this alphabet
1327+ // could name a path elsewhere.
1328+ if ( ! TRACE_SESSION_ID . test ( sessionID ) ) {
1329+ return c . json ( { ok : false , error : `Invalid sessionID: ${ sessionID } ` } , 400 )
1330+ }
1331+ try {
1332+ const [ { Trace } , { renderTraceViewer } ] = await Promise . all ( [
1333+ import ( "../altimate/observability/tracing" ) ,
1334+ import ( "../altimate/observability/viewer" ) ,
1335+ ] )
1336+ const trace = await Trace . loadTrace ( sessionID , await tracesDir ( ) )
1337+ if ( ! trace ) return c . json ( { ok : false , error : `Trace not found: ${ sessionID } ` } , 404 )
1338+ return c . html ( renderTraceViewer ( trace , { embedded : true } ) )
1339+ } catch ( err ) {
1340+ const error = err instanceof Error ? err . message : String ( err )
1341+ log . error ( "trace view: failed" , { error } )
1342+ return c . json ( { ok : false , error } , 500 )
1343+ }
1344+ } )
1345+ // altimate_change end
12521346 . all ( "/*" , async ( c ) => {
12531347 const path = c . req . path
12541348
0 commit comments