Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/runtime-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export interface ArtifactReview {
runtimeEpisodeTrace?: string
runtimeReferenceManifest?: string
runtimeReplayReferenceIndex?: string
previewEvidence?: string
agentResult?: string
transcript?: string
}
Expand Down
58 changes: 58 additions & 0 deletions packages/runtime-core/src/runtime-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,63 @@ export interface ArtifactPreview {
holdSeconds?: number
}

export interface ArtifactPreviewUrlRef {
kind: "preview-url"
availability: "reviewer-safe" | "local-only" | "unavailable"
reviewerSafe: boolean
url?: string
reason?: string
}

export interface ArtifactPreviewEvidence {
schema: "wp-codebox/preview-evidence/v1"
createdAt: string
session: {
kind: "browser-playground-session"
id: string
runtimeId: string
backend: RuntimeBackendKind
environment: {
kind: EnvironmentSpec["kind"]
name: string
version: string
}
}
run: RuntimeEpisodeTraceRef
preview: {
status: ArtifactPreview["status"] | "unavailable"
lifecycle: ArtifactPreview["lifecycle"] | "not-started"
source?: ArtifactPreview["source"]
createdAt?: string
expiresAt?: string
holdSeconds?: number
url: ArtifactPreviewUrlRef
publicUrl?: ArtifactPreviewUrlRef
localUrl?: ArtifactPreviewUrlRef
siteUrl?: ArtifactPreviewUrlRef
}
readiness: {
ready: boolean
status: BrowserStartupProgressEvent["status"] | "not-started"
phase?: BrowserStartupProgressEvent["phase"]
events: Array<{
id: string
phase: BrowserStartupProgressEvent["phase"]
status: BrowserStartupProgressEvent["status"]
label?: string
elapsed_ms?: number
timestamp: string
}>
}
components: {
packages?: ArtifactPackageProvenance
runtime: {
backend: RuntimeBackendKind
wordpressVersion?: string
}
}
}

export interface ArtifactBundle {
id: string
directory: string
Expand Down Expand Up @@ -542,6 +599,7 @@ export interface ArtifactBundle {
runtimeReferenceManifestPath?: string
runtimeReferenceIndexPath?: string
runtimeReplayReferenceIndexPath?: string
previewEvidencePath?: string
preview?: ArtifactPreview
contentDigest: string
createdAt: string
Expand Down
158 changes: 158 additions & 0 deletions packages/runtime-playground/src/artifact-bundle-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import {
type ArtifactManifest,
type ArtifactManifestFile,
type ArtifactPreview,
type ArtifactPreviewEvidence,
type ArtifactReviewBrowserSummary,
type ArtifactSpec,
type BrowserStartupProgressEvent,
type ExecutionResult,
type LifecycleEvent,
type MountSpec,
type ObservationResult,
type RuntimeCreateSpec,
type RuntimeEpisodeTraceRef,
type RuntimeInfo,
type Snapshot,
} from "@automattic/wp-codebox-core"
Expand Down Expand Up @@ -101,6 +104,7 @@ export class ArtifactBundleBuilder {
const runtimeReferenceManifestPath = join(filesDirectory, "runtime-reference-manifest.json")
const runtimeReferenceIndexPath = join(filesDirectory, "runtime-reference-index.json")
const runtimeReplayReferenceIndexPath = join(filesDirectory, "runtime-replay-index.json")
const previewEvidencePath = join(filesDirectory, "preview-evidence.json")
const redactor = new ArtifactRedactor(source.spec.secretEnv)

await source.redactBrowserArtifacts(redactor)
Expand Down Expand Up @@ -132,6 +136,20 @@ export class ArtifactBundleBuilder {
context: source.spec.metadata ?? {},
mounts: source.mounts,
})
const artifactBundleRef: RuntimeEpisodeTraceRef = {
kind: "artifact-bundle",
id: bundleId,
digest: { algorithm: "sha256", value: contentDigest },
path: "manifest.json",
}
const previewEvidence = buildPreviewEvidence({
createdAt,
runtime,
preview,
events: source.events,
packages: provenance.packages,
artifactBundleRef,
})
const metadata: Record<string, unknown> = {
id: bundleId,
contentDigest: contentDigestMetadata,
Expand Down Expand Up @@ -163,6 +181,7 @@ export class ArtifactBundleBuilder {
runtimeCreatedAt: source.runtimeCreatedAt,
mounts: source.mounts,
preview,
previewEvidencePath: "files/preview-evidence.json",
browser,
diagnosticsPath: "files/diagnostics.json",
})
Expand All @@ -184,6 +203,7 @@ export class ArtifactBundleBuilder {
runtimeReferenceManifest: relative(source.artifactRoot, runtimeReferenceManifestPath),
runtimeReferenceIndex: relative(source.artifactRoot, runtimeReferenceIndexPath),
runtimeReplayReferenceIndex: relative(source.artifactRoot, runtimeReplayReferenceIndexPath),
previewEvidence: relative(source.artifactRoot, previewEvidencePath),
mountDiffs: relative(source.artifactRoot, diffsPath),
...(runtimeSnapshotFiles.length > 0 ? { runtimeSnapshots: runtimeSnapshotFiles.map((file) => relative(source.artifactRoot, file.path)) } : {}),
...(browser ? { browser: browser.probes.find((probe) => probe.summaryFile)?.summaryFile ?? "files/browser/summary.json" } : {}),
Expand Down Expand Up @@ -225,6 +245,7 @@ export class ArtifactBundleBuilder {
artifactManifestFile(runtimeReferenceManifestPath, "runtime-reference-manifest", "application/json"),
artifactManifestFile(runtimeReferenceIndexPath, "runtime-reference-index", "application/json"),
artifactManifestFile(runtimeReplayReferenceIndexPath, "runtime-replay-index", "application/json"),
artifactManifestFile(previewEvidencePath, "preview-evidence", "application/json"),
...source.browserManifestFiles(),
...source.observationManifestFiles(),
...source.pluginCheckManifestFiles(),
Expand Down Expand Up @@ -253,6 +274,7 @@ export class ArtifactBundleBuilder {
await writeFile(patchPath, redactedPatch)
await writeRedactedArtifact(redactor, diagnosticsPath, source.artifactRoot, `${JSON.stringify(diagnostics, null, 2)}\n`)
await writeRedactedArtifact(redactor, testResultsPath, source.artifactRoot, `${JSON.stringify(testResults, null, 2)}\n`)
await writeRedactedArtifact(redactor, previewEvidencePath, source.artifactRoot, `${JSON.stringify(previewEvidence, null, 2)}\n`)
const redaction = redactor.summary()
if (redaction.total > 0) {
review.redaction = redaction
Expand Down Expand Up @@ -351,13 +373,149 @@ export class ArtifactBundleBuilder {
runtimeReferenceManifestPath,
runtimeReferenceIndexPath,
runtimeReplayReferenceIndexPath,
previewEvidencePath,
...(preview ? { preview } : {}),
contentDigest,
createdAt,
}
}
}

function buildPreviewEvidence({
artifactBundleRef,
createdAt,
events,
packages,
preview,
runtime,
}: {
artifactBundleRef: RuntimeEpisodeTraceRef
createdAt: string
events: LifecycleEvent[]
packages?: ArtifactPreviewEvidence["components"]["packages"]
preview?: ArtifactPreview
runtime: RuntimeInfo
}): ArtifactPreviewEvidence {
const progressEvents = events.flatMap((event) => {
if (event.type !== "runtime.browser-startup-progress") {
return []
}

const progress = event.data?.event
if (!isBrowserStartupProgressEvent(progress)) {
return []
}

return [{
id: event.id,
phase: progress.phase,
status: progress.status,
label: progress.label,
elapsed_ms: progress.elapsed_ms,
timestamp: event.timestamp,
}]
})
const lastProgress = progressEvents.at(-1)
const ready = progressEvents.some((event) => event.phase === "preview:ready" && event.status === "complete")

return {
schema: "wp-codebox/preview-evidence/v1",
createdAt,
session: {
kind: "browser-playground-session",
id: `browser-playground-session-${runtime.id}`,
runtimeId: runtime.id,
backend: runtime.backend,
environment: {
kind: runtime.environment.kind,
name: runtime.environment.name ?? runtime.environment.kind,
version: runtime.environment.version ?? "unknown",
},
},
run: artifactBundleRef,
preview: {
status: preview?.status ?? "unavailable",
lifecycle: preview?.lifecycle ?? "not-started",
source: preview?.source,
createdAt: preview?.createdAt,
expiresAt: preview?.expiresAt,
holdSeconds: preview?.holdSeconds,
url: safePreviewUrlRef(preview?.url),
...(preview?.publicUrl ? { publicUrl: safePreviewUrlRef(preview.publicUrl) } : {}),
...(preview?.localUrl ? { localUrl: safePreviewUrlRef(preview.localUrl) } : {}),
...(preview?.siteUrl ? { siteUrl: safePreviewUrlRef(preview.siteUrl) } : {}),
},
readiness: {
ready,
status: lastProgress?.status ?? "not-started",
phase: lastProgress?.phase,
events: progressEvents,
},
components: {
packages,
runtime: {
backend: runtime.backend,
wordpressVersion: runtime.environment.version,
},
},
}
}

function isBrowserStartupProgressEvent(value: unknown): value is BrowserStartupProgressEvent {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false
}

const candidate = value as Partial<BrowserStartupProgressEvent>
return typeof candidate.phase === "string" && typeof candidate.status === "string"
}

function safePreviewUrlRef(url: string | undefined): ArtifactPreviewEvidence["preview"]["url"] {
if (!url) {
return {
kind: "preview-url",
availability: "unavailable",
reviewerSafe: false,
reason: "preview-url-unavailable",
}
}

try {
const parsed = new URL(url)
if (isLocalPreviewHost(parsed.hostname)) {
return {
kind: "preview-url",
availability: "local-only",
reviewerSafe: false,
reason: "loopback-url-omitted",
}
}
} catch {
return {
kind: "preview-url",
availability: "unavailable",
reviewerSafe: false,
reason: "invalid-url-omitted",
}
}

return {
kind: "preview-url",
availability: "reviewer-safe",
reviewerSafe: true,
url,
}
}

function isLocalPreviewHost(hostname: string): boolean {
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "")
return normalized === "localhost"
|| normalized === "0.0.0.0"
|| normalized === "127.0.0.1"
|| normalized === "::1"
|| normalized.startsWith("127.")
}

async function writeJsonLines(path: string, records: unknown[], redactor: ArtifactRedactor, artifactRoot: string): Promise<void> {
await writeRedactedArtifact(redactor, path, artifactRoot, records.length > 0 ? `${records.map((record) => JSON.stringify(record)).join("\n")}\n` : "")
}
Expand Down
3 changes: 3 additions & 0 deletions packages/runtime-playground/src/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ export function buildArtifactReview({
runtimeCreatedAt,
mounts,
preview,
previewEvidencePath,
browser,
diagnosticsPath,
}: {
Expand All @@ -283,6 +284,7 @@ export function buildArtifactReview({
runtimeCreatedAt: string
mounts: MountSpec[]
preview?: ArtifactPreview
previewEvidencePath?: string
browser?: ArtifactReviewBrowserSummary
diagnosticsPath?: string
}): ArtifactReview {
Expand Down Expand Up @@ -366,6 +368,7 @@ export function buildArtifactReview({
testResults: "files/test-results.json",
runtimeReferenceManifest: "files/runtime-reference-manifest.json",
runtimeReplayReferenceIndex: "files/runtime-replay-index.json",
...(previewEvidencePath ? { previewEvidence: previewEvidencePath } : {}),
},
...(browser ? { browser } : {}),
riskFlags: suspiciousFullFileRewriteRiskFlags(patch),
Expand Down
Loading