@@ -109,7 +109,8 @@ export type AttemptRecord = {
109109export function buildWebappPayload ( input : {
110110 target : TargetHandle ;
111111 controls : RunControls ;
112- workspaceRoot : string ;
112+ /** Omit for the Daytona transport: the sandbox-side launcher defaults this to its own root. */
113+ workspaceRoot ?: string ;
113114 contributors ?: number ;
114115} ) : Record < string , unknown > {
115116 const webapp = webappTargetMetadata ( input . target ) ;
@@ -132,7 +133,7 @@ export function buildWebappPayload(input: {
132133 secret : webapp . secret ,
133134 secretUploadingUrl : webapp . secretUploadingUrl ,
134135 } ,
135- workspaceRoot : input . workspaceRoot ,
136+ ... ( input . workspaceRoot !== undefined ? { workspaceRoot : input . workspaceRoot } : { } ) ,
136137 model : input . controls . model ,
137138 contributors : input . contributors ?? input . controls . contributors ,
138139 ...buildGuardrails ( input . controls ) ,
@@ -301,6 +302,12 @@ export function buildReadAttemptsScript(attacksDir: string): string {
301302 * `runDaytonaEngagement`'s `afterEngagement` hook (before sandbox cleanup) -- by the time
302303 * `runDaytonaEngagement` itself resolves, the sandbox is already deleted.
303304 *
305+ * The `<workspaceRoot>/workspace/attacks/<attempt>/*.json` layout is a modality-agnostic
306+ * autobrin-flue convention (both `repo` and `webapp` modalities call the same `prepareWorkspace()`
307+ * layout code), so this reads attempts back the same way regardless of `payload.modality` -- this
308+ * mirrors the local-transport reader (`extractClaimFromWorkspace`), which is likewise never gated
309+ * on modality.
310+ *
304311 * Deliberately does NOT tolerate a script-level failure (non-zero exit, unparsable/non-array
305312 * output) by falling back to `[]`: the script's own per-file reads already tolerate an
306313 * individual attempt missing evaluate/report/disclosure.json (still mid-run, a legitimate `{}`),
@@ -312,8 +319,6 @@ export function buildReadAttemptsScript(attacksDir: string): string {
312319 * contract (cleanup still runs, the overall call rejects) instead of corrupting the claim.
313320 */
314321export async function fetchAttemptsFromSandbox ( sandbox : Sandbox , payload : EngagementPayload ) : Promise < AttemptRecord [ ] > {
315- if ( payload . modality !== 'repo' ) return [ ] ;
316-
317322 const attacksDir = `${ engagementWorkspaceDir ( payload ) } /attacks` ;
318323 const script = [ 'set -euo pipefail' , "python3 - <<'PY'" , buildReadAttemptsScript ( attacksDir ) , 'PY' ] . join ( '\n' ) ;
319324 const response = await executeChecked ( sandbox , script , '/' , 60 ) ;
@@ -450,11 +455,14 @@ async function runViaLocalNpx(input: RunInput): Promise<NormalizedResult> {
450455async function runViaDaytona ( input : RunInput ) : Promise < NormalizedResult > {
451456 const { contenderId, config, contributors, task, target, controls, context } = input ;
452457
453- if ( target . modality !== 'repo' || ! target . repo ) {
458+ if ( target . modality !== 'repo' && target . modality !== 'webapp' ) {
454459 throw new Error (
455- `autobrin contender "${ contenderId } ": transport "daytona" currently only supports modality "repo" (got " ${ target . modality } ") ` ,
460+ `autobrin contender "${ contenderId } ": transport "daytona" does not support modality "${ target . modality } "` ,
456461 ) ;
457462 }
463+ if ( target . modality === 'repo' && ! target . repo ) {
464+ throw new Error ( `autobrin contender "${ contenderId } ": modality "repo" requires target.repo` ) ;
465+ }
458466
459467 const started = Date . now ( ) ;
460468 const engagementDir = path . join (
@@ -465,8 +473,14 @@ async function runViaDaytona(input: RunInput): Promise<NormalizedResult> {
465473 await mkdir ( context . resultsDir , { recursive : true } ) ;
466474
467475 // No workspaceRoot: the sandbox-side launcher defaults it to its own root (BENCHPRESS_ROOT),
468- // never this (local-only) engagementDir.
469- const payload = buildRepoPayload ( { target, controls, contributors } ) ;
476+ // never this (local-only) engagementDir. Webapp targets skip repo-modality-specific target
477+ // materialization entirely: there is no target repo to clone into the sandbox, only a URL the
478+ // sandbox reaches over the network (runDaytonaEngagement's own modality branch calls
479+ // prepareWebappTarget instead of prepareRepoTarget -- see src/daytona/bootstrap.ts).
480+ const payload =
481+ target . modality === 'webapp'
482+ ? buildWebappPayload ( { target, controls, contributors } )
483+ : buildRepoPayload ( { target, controls, contributors } ) ;
470484
471485 const stdoutChunks : string [ ] = [ ] ;
472486 const stderrChunks : string [ ] = [ ] ;
0 commit comments