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
1 change: 1 addition & 0 deletions .github/workflows/hosted-canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ jobs:
SOURCE_SHA: ${{ github.event.pull_request.head.sha }}
CANARY_QUALIFIED_SOURCE_SHA: ${{ github.event.pull_request.head.sha }}
CANARY_TRUSTED_WORKFLOW_SHA: ${{ github.event.pull_request.base.sha }}
CANARY_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
run: |
set -euo pipefail
test -n "$GH_TOKEN"
Expand Down
115 changes: 115 additions & 0 deletions scripts/ship-canary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,118 @@ test("candidate config cannot redirect trusted canary targets", () => {
})
})

test("trusted template base admits exact first-consumer canary targets", () => {
const driver = canaryFixture()
const candidate = canaryFixture()
writeFileSync(
join(driver.temporaryRoot, "plugin.config.json"),
readFileSync(join(root, "plugin.config.json"), "utf8"),
)

const result = runTrustedCanary(
driver.temporaryRoot,
driver,
candidate.temporaryRoot,
"--dry-run",
{
GITHUB_REPOSITORY: "myagentdojo/dojo-hello",
CANARY_HEAD_REPOSITORY: "myagentdojo/dojo-hello",
GITHUB_WORKFLOW_REF:
"myagentdojo/dojo-hello/.github/workflows/hosted-canary.yml@refs/heads/main",
},
)

expect(result.exitCode, result.stderr.toString()).toBe(0)
expect(JSON.parse(result.stdout.toString())).toMatchObject({
identity: "myagentdojo",
targets: [
{ repository: "myagentdojo/dojo-hello-public-canary", visibility: "PUBLIC" },
{ repository: "myagentdojo/dojo-hello-private-canary", visibility: "PRIVATE" },
],
})
})

interface MutableBootstrapCandidate {
template: boolean
repository: string
canary: {
owner: string
actor: string
publicRepository: string
privateRepository: string
}
}

test.each([
[
"redirected target",
(config: MutableBootstrapCandidate) => {
config.canary.publicRepository = "attacker-public-canary"
},
{},
],
[
"wrong actor",
(config: MutableBootstrapCandidate) => {
config.canary.actor = "another-actor"
},
{},
],
[
"wrong repository URL",
(config: MutableBootstrapCandidate) => {
config.repository = "https://github.com/myagentdojo/another-repository"
},
{},
],
[
"partially initialized candidate",
(config: MutableBootstrapCandidate) => {
config.template = true
},
{},
],
[
"fork head",
(_config: MutableBootstrapCandidate) => {},
{ CANARY_HEAD_REPOSITORY: "fork-owner/dojo-hello" },
],
])("trusted template bootstrap rejects %s", (_name, mutate, environment) => {
const driver = canaryFixture()
const candidate = canaryFixture()
writeFileSync(
join(driver.temporaryRoot, "plugin.config.json"),
readFileSync(join(root, "plugin.config.json"), "utf8"),
)
const candidateConfigPath = join(candidate.temporaryRoot, "plugin.config.json")
const candidateConfig = JSON.parse(
readFileSync(candidateConfigPath, "utf8"),
) as MutableBootstrapCandidate
mutate(candidateConfig)
writeFileSync(candidateConfigPath, `${JSON.stringify(candidateConfig, null, 2)}\n`)

const result = runTrustedCanary(
driver.temporaryRoot,
driver,
candidate.temporaryRoot,
"--dry-run",
{
GITHUB_REPOSITORY: "myagentdojo/dojo-hello",
CANARY_HEAD_REPOSITORY: "myagentdojo/dojo-hello",
GITHUB_WORKFLOW_REF:
"myagentdojo/dojo-hello/.github/workflows/hosted-canary.yml@refs/heads/main",
...environment,
},
)

expect(result.exitCode).toBe(1)
expect(JSON.parse(result.stdout.toString())).toMatchObject({
category: "canary_target_mismatch",
retrySafe: false,
})
expect(existsSync(driver.log)).toBe(false)
})

test("public publication uses sanitized bytes while private publication uses the source checkout", async () => {
const driver = canaryFixture()
const candidate = canaryFixture()
Expand Down Expand Up @@ -1060,6 +1172,9 @@ test("privileged canary workflow executes trusted code and treats the PR checkou
expect(workflow).toContain("environment: hosted-canary-qualification")
expect(workflow).toContain("CANARY_QUALIFIED_SOURCE_SHA: ${{ github.event.pull_request.head.sha }}")
expect(workflow).toContain("CANARY_TRUSTED_WORKFLOW_SHA: ${{ github.event.pull_request.base.sha }}")
expect(workflow).toContain(
"CANARY_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}",
)
expect(workflow).toContain("GH_TOKEN: ${{ secrets.CANARY_GH_TOKEN }}")
expect(workflow).toContain("CANARY_SSH_KNOWN_HOSTS: ${{ secrets.CANARY_SSH_KNOWN_HOSTS }}")
expect(workflow).toContain("CANARY_SSH_PRIVATE_KEY: ${{ secrets.CANARY_SSH_PRIVATE_KEY }}")
Expand Down
98 changes: 72 additions & 26 deletions scripts/ship-canary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
import { tmpdir } from "node:os"
import { join, resolve } from "node:path"

import { loadPluginConfig } from "./plugin-config"
import { type PluginConfig, loadPluginConfig } from "./plugin-config"
import { deterministicPluginArchive, payloadInventorySha256 } from "./plugin-files"
import { copyMarketplaceDistribution, proveHostedHarnessInstall } from "./prove-harness-install"

Expand Down Expand Up @@ -1097,22 +1097,34 @@ function installCandidate(target: Target, sourceSha: string): CandidateInstallEv
}
}

function candidateCanaryTargets(sourceRoot: string): {
owner?: unknown
actor?: unknown
publicRepository?: unknown
privateRepository?: unknown
} {
interface CandidateCanaryConfig {
template?: unknown
repository?: unknown
canary: {
owner?: unknown
actor?: unknown
publicRepository?: unknown
privateRepository?: unknown
}
}

function candidateCanaryConfig(sourceRoot: string): CandidateCanaryConfig {
try {
const candidate = JSON.parse(readFileSync(join(sourceRoot, "plugin.config.json"), "utf8")) as {
template?: unknown
repository?: unknown
canary?: {
owner?: unknown
actor?: unknown
publicRepository?: unknown
privateRepository?: unknown
}
}
return candidate.canary ?? {}
return {
template: candidate.template,
repository: candidate.repository,
canary: candidate.canary ?? {},
}
} catch (error) {
throw new CanaryError(
"candidate_config_invalid",
Expand All @@ -1123,6 +1135,52 @@ function candidateCanaryTargets(sourceRoot: string): {
}
}

function trustedCanaryTargets(
trustedConfig: PluginConfig,
candidate: CandidateCanaryConfig,
identity: string,
environment: Record<string, string | undefined> = process.env,
): PluginConfig["canary"] {
const exactMatch =
candidate.canary.owner === trustedConfig.canary.owner &&
candidate.canary.actor === trustedConfig.canary.actor &&
candidate.canary.publicRepository === trustedConfig.canary.publicRepository &&
candidate.canary.privateRepository === trustedConfig.canary.privateRepository
if (exactMatch) return trustedConfig.canary

const repository = environment.GITHUB_REPOSITORY ?? ""
const [owner, name, extra] = repository.split("/")
const expected = {
owner,
actor: identity,
publicRepository: `${name}-public-canary`,
privateRepository: `${name}-private-canary`,
}
const protectedBootstrap =
trustedConfig.template === true &&
environment.GITHUB_ACTIONS === "true" &&
Boolean(owner && name && !extra && identity) &&
environment.CANARY_HEAD_REPOSITORY === repository &&
environment.GITHUB_WORKFLOW_REF?.startsWith(
`${repository}/.github/workflows/hosted-canary.yml@`,
) === true &&
/^[0-9a-f]{40}$/.test(environment.CANARY_TRUSTED_WORKFLOW_SHA ?? "") &&
candidate.template === false &&
candidate.repository === `https://github.com/${repository}` &&
candidate.canary.owner === expected.owner &&
candidate.canary.actor === expected.actor &&
candidate.canary.publicRepository === expected.publicRepository &&
candidate.canary.privateRepository === expected.privateRepository
if (protectedBootstrap) return expected

throw new CanaryError(
"canary_target_mismatch",
"candidate canary targets differ from the trusted driver checkout",
"restore the trusted targets, or initialize the exact same-repository template through its protected hosted-canary workflow",
false,
)
}

function assertCandidateInstall(
target: Target,
evidence: CandidateInstallEvidence,
Expand Down Expand Up @@ -1223,21 +1281,9 @@ export async function qualifyTargets(

function preflight(options: PublishOptions): Preflight {
const trustedConfig = loadPluginConfig(root)
const candidateCanary = candidateCanaryTargets(options.sourceRoot)
if (
candidateCanary.owner !== trustedConfig.canary.owner ||
candidateCanary.actor !== trustedConfig.canary.actor ||
candidateCanary.publicRepository !== trustedConfig.canary.publicRepository ||
candidateCanary.privateRepository !== trustedConfig.canary.privateRepository
) {
throw new CanaryError(
"canary_target_mismatch",
"candidate canary targets differ from the trusted driver checkout",
"restore plugin.config.json canary targets to the trusted base values",
false,
)
}
const candidateConfig = candidateCanaryConfig(options.sourceRoot)
const identity = processResult(["gh", "api", "user", "--jq", ".login"]) || ""
const canary = trustedCanaryTargets(trustedConfig, candidateConfig, identity)
const dirty = processResult(
["git", "status", "--porcelain", "--untracked-files=no"],
false,
Expand Down Expand Up @@ -1327,17 +1373,17 @@ function preflight(options: PublishOptions): Preflight {
const boundTransport = bindTransportIdentity(
identity,
resolvedTransport.identity,
trustedConfig.canary.actor,
canary.actor,
resolvedTransport.kind,
)
const transportIdentity = { ...boundTransport, host: resolvedTransport.host }
const publicCandidate = createSanitizedPublicCandidate(options.sourceRoot, sourceSha)
try {
const targets = buildTargets(
origin,
trustedConfig.canary.owner,
trustedConfig.canary.publicRepository,
trustedConfig.canary.privateRepository,
canary.owner,
canary.publicRepository,
canary.privateRepository,
sourceSha,
options.sourceRoot,
publicCandidate,
Expand Down
Loading