Skip to content
Merged
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
11 changes: 11 additions & 0 deletions layers/artifact-delivery/server/api/v1/resolutions/index.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
readsLinkedFiles,
requestResolution,
} from '../../../utils/request-resolution'
import { serveReadyResolution } from '../../../utils/served-resolution'
import { setSkillPageUrlHeader } from '../../../utils/skill-page'
import { getResolution } from '../../../utils/state'

Expand All @@ -42,6 +43,7 @@ export default withArtifactProblems(defineApiHandler({
body.source.repository,
)
: { _tag: 'not-found' as const }
const build = createArtifactBuildDependencies(platform.env)
const result = await requestResolution({
db: platform.db,
lookupAdmitted: fetchAdmittedSkillIdentity(event.context),
Expand All @@ -68,6 +70,15 @@ export default withArtifactProblems(defineApiHandler({
},
}),
now: () => Math.floor(Date.now() / 1000),
// A warm run takes the ready Resolution of its commit and skips the
// queue and the build.
serveReady: serveReadyResolution({
db: platform.db,
bucket: build.bucket,
trustedRoot: build.trustedRoot,
now: build.now,
resolveOnGithub: source => build.github.resolve(source),
}),
}, {
source: body.source,
idempotencyKey,
Expand Down
8 changes: 4 additions & 4 deletions layers/artifact-delivery/server/utils/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export const LEADING_BUILD_WAIT_SECONDS = 2
* root and still stored in R2. Its files and check results come from its signed
* attestation, never from unsigned columns.
*/
interface ReusableBuild {
export interface ReusableBuild {
resolutionId: string
source: ResolvedSource
artifactId: string
Expand All @@ -125,7 +125,7 @@ interface ReusableBuild {
linkedFiles: LinkedArtifactFile[]
}

type ReuseDecision
export type ReuseDecision
= { _tag: 'hit', build: ReusableBuild }
| { _tag: 'miss', reason: ReuseMissReason }

Expand Down Expand Up @@ -675,8 +675,8 @@ async function decideReuse(
* - the statement matches the stored row;
* - R2 still holds the exact bytes.
*/
async function findReusableBuild(
dependencies: ArtifactBuildDependencies,
export async function findReusableBuild(
dependencies: Pick<ArtifactBuildDependencies, 'db' | 'bucket' | 'trustedRoot' | 'now'>,
lookup: ReadyBuildLookup,
linkedFiles: boolean,
): Promise<ReuseDecision> {
Expand Down
17 changes: 15 additions & 2 deletions layers/artifact-delivery/server/utils/request-resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { H3EventContext } from 'h3'
import type { SourceRequest } from '../schemas/contracts'
import type { FetchAdmittedSkillIdentity } from './admitted-identity'
import type { ArtifactBuildOutcome } from './build'
import type { ServedResolution } from './served-resolution'
import type { CreateResolutionResult } from './state'
import { admittedSourceRequest } from './admitted-identity'
import { recordResolutionRequester } from './requester-github'
Expand All @@ -13,6 +14,12 @@ export interface ResolutionRequestDependencies {
lookupAdmitted: FetchAdmittedSkillIdentity
enqueue: (resolutionId: string) => Promise<void>
now: () => number
/**
* Answer a public request from a ready build, or name the source to build.
* `POST /api/v1/resolutions` passes it. The run sweep does not, so every
* sweep check still runs the build.
*/
serveReady?: (source: SourceRequest, linkedFiles: boolean) => Promise<ServedResolution>
}

export type ResolutionAccess
Expand Down Expand Up @@ -67,10 +74,16 @@ export async function requestResolution(
// A private build reads blobs and never links a file.
const linkedFiles = input.access.visibility === 'public' && input.linkedFiles === true
const identity = await resolutionRequestIdentity(input.source, input.idempotencyKey, accountId, linkedFiles)
const source = input.access.visibility === 'private'
const admitted = input.access.visibility === 'private'
? input.source
: await admittedSourceRequest(input.source, dependencies.lookupAdmitted)
const result = await createResolution(dependencies.db, source, identity, dependencies.now(), input.access, linkedFiles)
// A ready build serves only a CLI that reads what it holds (see `findReusableBuild`).
const served: ServedResolution = input.access.visibility === 'public' && dependencies.serveReady
? await dependencies.serveReady(admitted, linkedFiles)
: { _tag: 'build', source: admitted }
if (served._tag === 'served')
return served
const result = await createResolution(dependencies.db, served.source, identity, dependencies.now(), input.access, linkedFiles)
if (result._tag === 'created' && input.access.visibility === 'public' && input.requesterAccountId) {
await recordResolutionRequester(dependencies.db, {
resolutionId: result.row.id,
Expand Down
139 changes: 139 additions & 0 deletions layers/artifact-delivery/server/utils/served-resolution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import type { SourceRequest } from '../schemas/contracts'
import type { ReuseMissReason } from './build'
import type { ResolveSourceResult } from './github-source'
import type { ResolutionRow } from './state'
import type { TrustedRoot } from './trusted-root'
import { createWideEvent } from '@harlan-zw/nuxt-wide-events/standalone'
import { emitOperationalEvent } from '#server/utils/operational-event'
import { findReusableBuild } from './build'
import { getResolution } from './state'

/**
* How long a Resolution request waits for GitHub to name the commit of a
* branch. A slower answer builds the request as sent, and the build resolves
* the branch itself.
*/
export const SERVE_RESOLVE_TIMEOUT_MS = 2_500

export interface ServedResolutionDependencies {
db: D1Database
bucket: R2Bucket
trustedRoot: TrustedRoot
/** Unix seconds. */
now: () => number
/** Resolve a public request that names no commit, as the build would. */
resolveOnGithub: (source: SourceRequest) => Promise<ResolveSourceResult>
resolveTimeoutMs?: number
/** Receives each decision. The default emits the `artifact-resolution-served` wide event. */
report?: (report: ServedResolutionReport) => void
}

export type ServedResolution
= | { _tag: 'served', row: ResolutionRow }
/** No ready build fits. Build `source`, pinned to its commit when GitHub named one. */
| { _tag: 'build', source: SourceRequest }

export type ServedResolutionReport
= | { _tag: 'hit', reusedFrom: string }
| { _tag: 'miss', reason: ReuseMissReason | 'github-unresolved' | 'github-timeout' | 'not-ready' }

/**
* Answer a public request from the ready build of the same Repository,
* commit, Skill folder and policy, with no queue and no build.
*
* A warm run used to request a new Resolution, wait for the queue, and wait
* again while the build re-signed the same bytes: 5.6 to 9.2 s in production
* on 2026-10-07. The ready Resolution answers it now, exactly as `GET
* /api/v1/resolutions/:id` would. It passes every check a reused build passes:
* the trusted root verifies its attestation, it names the current policy, its
* check results pass, and R2 still holds its bytes.
*
* A request that names no commit resolves on GitHub first. When no ready
* build fits, the build gets the request pinned to that commit and folder, as
* a registry name already is, so it reads no branch again.
*/
export function serveReadyResolution(
dependencies: ServedResolutionDependencies,
): (source: SourceRequest, linkedFiles: boolean) => Promise<ServedResolution> {
const report = dependencies.report ?? emitServedEvent
return async (source, linkedFiles) => {
const pinned = await pinToCommit(dependencies, source)
if (pinned._tag !== 'pinned') {
report({ _tag: 'miss', reason: pinned._tag })
return { _tag: 'build', source }
}
const decision = await findReusableBuild(dependencies, {
_tag: 'pinned',
owner: pinned.source.owner,
repository: pinned.source.repository,
commitSha: pinned.commitSha,
selector: pinned.source.selector,
}, linkedFiles)
if (decision._tag === 'miss') {
report({ _tag: 'miss', reason: decision.reason })
return { _tag: 'build', source: pinned.source }
}
const row = await getResolution(dependencies.db, decision.build.resolutionId)
if (row?.state !== 'ready') {
report({ _tag: 'miss', reason: 'not-ready' })
return { _tag: 'build', source: pinned.source }
}
report({ _tag: 'hit', reusedFrom: row.id })
return { _tag: 'served', row }
}
}

type PinnedRequest
= | { _tag: 'pinned', source: SourceRequest, commitSha: string }
| { _tag: 'github-unresolved' | 'github-timeout' }

async function pinToCommit(
dependencies: ServedResolutionDependencies,
source: SourceRequest,
): Promise<PinnedRequest> {
if (source.ref?.type === 'commit')
return { _tag: 'pinned', source, commitSha: source.ref.value }
let timer: ReturnType<typeof setTimeout> | undefined
const timeout = new Promise<'timeout'>((resolve) => {
timer = setTimeout(resolve, dependencies.resolveTimeoutMs ?? SERVE_RESOLVE_TIMEOUT_MS, 'timeout')
})
const answer = await Promise.race([
dependencies.resolveOnGithub(source).catch(() => {
// Safe to drop: the build reads GitHub again with its own retries,
// and reports the failure it finds.
return null
}),
timeout,
]).finally(() => clearTimeout(timer))
if (answer === 'timeout')
return { _tag: 'github-timeout' }
if (answer?._tag !== 'resolved' || answer.source.visibility !== 'public')
return { _tag: 'github-unresolved' }
return {
_tag: 'pinned',
commitSha: answer.source.commitSha,
source: {
provider: source.provider,
owner: source.owner,
repository: source.repository,
selector: { type: 'path', path: answer.source.skillPath },
ref: { type: 'commit', value: answer.source.commitSha },
},
}
}

function emitServedEvent(report: ServedResolutionReport): void {
if (report._tag === 'hit') {
emitOperationalEvent(createWideEvent({
'operation': 'artifact-resolution-served',
'outcome': 'hit',
'artifact.reusedFrom': report.reusedFrom,
}), 'info')
return
}
emitOperationalEvent(createWideEvent({
operation: 'artifact-resolution-served',
outcome: 'miss',
reason: report.reason,
}), 'info')
}
6 changes: 5 additions & 1 deletion layers/artifact-delivery/server/utils/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,12 @@ export async function resolutionRequestIdentity(source: SourceRequest, idempoten
}
}

/**
* `served` is a ready Resolution that answers a new request for the same
* Repository, commit, Skill folder and policy. See `serveReadyResolution`.
*/
export type CreateResolutionResult
= { _tag: 'created' | 'existing', row: ResolutionRow }
= { _tag: 'created' | 'existing' | 'served', row: ResolutionRow }
| { _tag: 'idempotency-conflict' }

export async function createResolution(
Expand Down
Loading
Loading