Skip to content

Commit d4c7519

Browse files
committed
ci(mantis): allow fork telegram proof
1 parent e6b0b37 commit d4c7519

4 files changed

Lines changed: 104 additions & 5 deletions

File tree

.github/codex/prompts/mantis-telegram-desktop-proof.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Inputs are provided as environment variables:
2424
- `BASELINE_SHA`
2525
- `CANDIDATE_REF`
2626
- `CANDIDATE_SHA`
27+
- `MANTIS_CANDIDATE_TRUST`
2728
- `MANTIS_OUTPUT_DIR`
2829
- `MANTIS_INSTRUCTIONS`
2930
- `CRABBOX_PROVIDER`
@@ -44,6 +45,12 @@ Required workflow:
4445
`.artifacts/qa-e2e/mantis/telegram-desktop-proof-worktrees/baseline` and
4546
`.artifacts/qa-e2e/mantis/telegram-desktop-proof-worktrees/candidate`, then
4647
install and build each worktree with the repo's normal `pnpm` commands.
48+
If `MANTIS_CANDIDATE_TRUST` is `maintainer-approved-fork-pr-head`, treat the
49+
candidate worktree as untrusted fork code: do not pass GitHub, OpenAI,
50+
Crabbox, Convex, or other workflow secrets into candidate install, build, or
51+
runtime commands. The candidate SUT may receive only the proof runner's
52+
short-lived Telegram bot token, generated local config/state paths, and mock
53+
model key needed for this isolated proof.
4754
5. In each worktree, run the real-user Telegram Crabbox proof flow from the
4855
skill with `$OPENCLAW_TELEGRAM_USER_PROOF_CMD`; do not run
4956
`pnpm qa:telegram-user:crabbox` directly. The proof command comes from the

.github/workflows/mantis-telegram-desktop-proof.yml

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ on:
3535
description: Optional existing Crabbox desktop lease id or slug to reuse
3636
required: false
3737
type: string
38+
allow_fork_candidate:
39+
description: Allow a fork PR head candidate when pr_number points at that PR
40+
required: false
41+
default: false
42+
type: boolean
3843

3944
permissions:
4045
contents: write
@@ -95,6 +100,7 @@ jobs:
95100
needs: authorize_actor
96101
runs-on: ubuntu-24.04
97102
outputs:
103+
allow_fork_candidate: ${{ steps.resolve.outputs.allow_fork_candidate }}
98104
baseline_ref: ${{ steps.resolve.outputs.baseline_ref }}
99105
candidate_ref: ${{ steps.resolve.outputs.candidate_ref }}
100106
crabbox_provider: ${{ steps.resolve.outputs.crabbox_provider }}
@@ -119,6 +125,10 @@ jobs:
119125
if (eventName === "workflow_dispatch") {
120126
const inputs = context.payload.inputs ?? {};
121127
setOutput("should_run", "true");
128+
setOutput(
129+
"allow_fork_candidate",
130+
String(inputs.allow_fork_candidate) === "true" ? "true" : "false",
131+
);
122132
setOutput("baseline_ref", inputs.baseline_ref || "main");
123133
setOutput("candidate_ref", inputs.candidate_ref || "main");
124134
setOutput("pr_number", inputs.pr_number || "");
@@ -150,6 +160,7 @@ jobs:
150160
if (!requested) {
151161
core.notice("Comment mentioned Mantis but did not request Telegram desktop proof.");
152162
setOutput("should_run", "false");
163+
setOutput("allow_fork_candidate", "false");
153164
setOutput("baseline_ref", "");
154165
setOutput("candidate_ref", "");
155166
setOutput("pr_number", "");
@@ -192,8 +203,10 @@ jobs:
192203
rawCandidate && !["head", "pr", "pr-head"].includes(rawCandidate.toLowerCase())
193204
? rawCandidate
194205
: mergedCandidate || pr.head.sha;
206+
const allowForkCandidate = /\bfork[-_]ok\b/i.test(body);
195207
196208
setOutput("should_run", "true");
209+
setOutput("allow_fork_candidate", allowForkCandidate ? "true" : "false");
197210
setOutput("baseline_ref", baselineMatch?.[1] || mergedBaseline || "main");
198211
setOutput("candidate_ref", candidate);
199212
setOutput("pr_number", String(issue.number));
@@ -217,6 +230,7 @@ jobs:
217230
outputs:
218231
baseline_revision: ${{ steps.validate.outputs.baseline_revision }}
219232
candidate_revision: ${{ steps.validate.outputs.candidate_revision }}
233+
candidate_trust: ${{ steps.validate.outputs.candidate_trust }}
220234
steps:
221235
- name: Checkout harness ref
222236
uses: actions/checkout@v6
@@ -227,6 +241,7 @@ jobs:
227241
- name: Validate refs are trusted
228242
id: validate
229243
env:
244+
ALLOW_FORK_CANDIDATE: ${{ needs.resolve_request.outputs.allow_fork_candidate }}
230245
BASELINE_REF: ${{ needs.resolve_request.outputs.baseline_ref }}
231246
CANDIDATE_REF: ${{ needs.resolve_request.outputs.candidate_ref }}
232247
GH_TOKEN: ${{ github.token }}
@@ -264,25 +279,43 @@ jobs:
264279
)"
265280
if [[ "$pr_head_count" != "0" ]]; then
266281
reason="open-pr-head"
282+
elif [[ "$label" == "candidate" && "${ALLOW_FORK_CANDIDATE:-false}" == "true" && -n "${PR_NUMBER:-}" ]]; then
283+
local fork_pr_head_count
284+
fork_pr_head_count="$(
285+
gh api \
286+
-H "Accept: application/vnd.github+json" \
287+
"repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \
288+
--jq 'if .state == "open" and .head.repo.full_name != "'"${GITHUB_REPOSITORY}"'" and .head.sha == "'"${revision}"'" then 1 else 0 end'
289+
)"
290+
if [[ "$fork_pr_head_count" == "1" ]]; then
291+
reason="maintainer-approved-fork-pr-head"
292+
fi
267293
fi
268294
fi
269295
270296
if [[ -z "$reason" ]]; then
271-
echo "${label} ref '${input_ref}' resolved to ${revision}, which is not trusted for this secret-bearing Mantis run." >&2
297+
echo "${label} ref '${input_ref}' resolved to ${revision}, which is not trusted for this secret-bearing Mantis run. Add fork-ok only for a maintainer-approved fork PR head." >&2
272298
exit 1
273299
fi
274-
printf '%s\n' "$revision"
300+
printf '%s\t%s\n' "$revision" "$reason"
275301
}
276302
277303
baseline_revision="$(validate_ref baseline "$BASELINE_REF")"
304+
baseline_trust="${baseline_revision#*$'\t'}"
305+
baseline_revision="${baseline_revision%%$'\t'*}"
278306
candidate_revision="$(validate_ref candidate "$CANDIDATE_REF")"
307+
candidate_trust="${candidate_revision#*$'\t'}"
308+
candidate_revision="${candidate_revision%%$'\t'*}"
279309
echo "baseline_revision=${baseline_revision}" >> "$GITHUB_OUTPUT"
280310
echo "candidate_revision=${candidate_revision}" >> "$GITHUB_OUTPUT"
311+
echo "candidate_trust=${candidate_trust}" >> "$GITHUB_OUTPUT"
281312
{
282313
echo "baseline: \`${BASELINE_REF}\`"
283314
echo "baseline SHA: \`${baseline_revision}\`"
315+
echo "baseline trust: \`${baseline_trust}\`"
284316
echo "candidate: \`${CANDIDATE_REF}\`"
285317
echo "candidate SHA: \`${candidate_revision}\`"
318+
echo "candidate trust: \`${candidate_trust}\`"
286319
} >> "$GITHUB_STEP_SUMMARY"
287320
288321
run_telegram_desktop_proof:
@@ -375,7 +408,7 @@ jobs:
375408
printf '%s\n' 'Defaults env_keep += "CODEX_HOME CODEX_INTERNAL_ORIGINATOR_OVERRIDE"'
376409
printf '%s\n' 'Defaults env_keep += "BASELINE_REF BASELINE_SHA CANDIDATE_REF CANDIDATE_SHA"'
377410
printf '%s\n' 'Defaults env_keep += "CRABBOX_ACCESS_CLIENT_ID CRABBOX_ACCESS_CLIENT_SECRET CRABBOX_COORDINATOR CRABBOX_COORDINATOR_TOKEN CRABBOX_LEASE_ID CRABBOX_PROVIDER"'
378-
printf '%s\n' 'Defaults env_keep += "GH_TOKEN MANTIS_INSTRUCTIONS MANTIS_OUTPUT_DIR MANTIS_PR_NUMBER"'
411+
printf '%s\n' 'Defaults env_keep += "GH_TOKEN MANTIS_CANDIDATE_TRUST MANTIS_INSTRUCTIONS MANTIS_OUTPUT_DIR MANTIS_PR_NUMBER"'
379412
printf '%s\n' 'Defaults env_keep += "OPENCLAW_BUILD_PRIVATE_QA OPENCLAW_ENABLE_PRIVATE_QA_CLI OPENCLAW_QA_CONVEX_SECRET_CI OPENCLAW_QA_CONVEX_SITE_URL OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN"'
380413
printf '%s\n' 'Defaults env_keep += "OPENCLAW_TELEGRAM_USER_CRABBOX_BIN OPENCLAW_TELEGRAM_USER_CRABBOX_PROVIDER OPENCLAW_TELEGRAM_USER_DRIVER_SCRIPT OPENCLAW_TELEGRAM_USER_PROOF_CMD"'
381414
} | sudo tee /etc/sudoers.d/mantis-codex-env >/dev/null
@@ -406,6 +439,7 @@ jobs:
406439
CRABBOX_LEASE_ID: ${{ needs.resolve_request.outputs.lease_id }}
407440
CRABBOX_PROVIDER: ${{ needs.resolve_request.outputs.crabbox_provider }}
408441
GH_TOKEN: ${{ github.token }}
442+
MANTIS_CANDIDATE_TRUST: ${{ needs.validate_refs.outputs.candidate_trust }}
409443
MANTIS_INSTRUCTIONS: ${{ needs.resolve_request.outputs.instructions }}
410444
MANTIS_OUTPUT_DIR: ${{ env.MANTIS_OUTPUT_DIR }}
411445
MANTIS_PR_NUMBER: ${{ needs.resolve_request.outputs.pr_number }}

scripts/e2e/telegram-user-crabbox-proof.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,9 +417,40 @@ function optionalString(source: JsonObject, key: string) {
417417
return typeof value === "string" && value.trim() ? value.trim() : undefined;
418418
}
419419

420+
function childProcessBaseEnv() {
421+
const keys = [
422+
"CI",
423+
"COREPACK_HOME",
424+
"FORCE_COLOR",
425+
"HOME",
426+
"LANG",
427+
"LC_ALL",
428+
"NODE_OPTIONS",
429+
"OPENCLAW_BUILD_PRIVATE_QA",
430+
"OPENCLAW_ENABLE_PRIVATE_QA_CLI",
431+
"PATH",
432+
"PNPM_HOME",
433+
"SHELL",
434+
"TEMP",
435+
"TMP",
436+
"TMPDIR",
437+
"USER",
438+
"XDG_CACHE_HOME",
439+
"XDG_CONFIG_HOME",
440+
];
441+
const env: NodeJS.ProcessEnv = {};
442+
for (const key of keys) {
443+
const value = process.env[key];
444+
if (value) {
445+
env[key] = value;
446+
}
447+
}
448+
return env;
449+
}
450+
420451
function mockServerEnv(params: { mockPort: number; mockResponseText: string; requestLog: string }) {
421452
return {
422-
...process.env,
453+
...childProcessBaseEnv(),
423454
MOCK_PORT: String(params.mockPort),
424455
MOCK_REQUEST_LOG: params.requestLog,
425456
SUCCESS_MARKER: params.mockResponseText,
@@ -428,7 +459,7 @@ function mockServerEnv(params: { mockPort: number; mockResponseText: string; req
428459

429460
function gatewayEnv(params: { configPath: string; stateDir: string; sutToken: string }) {
430461
return {
431-
...process.env,
462+
...childProcessBaseEnv(),
432463
OPENAI_API_KEY: "sk-openclaw-e2e-mock",
433464
OPENCLAW_CONFIG_PATH: params.configPath,
434465
OPENCLAW_STATE_DIR: params.stateDir,

test/scripts/mantis-telegram-desktop-proof-workflow.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,31 @@ describe("Mantis Telegram Desktop proof workflow", () => {
107107
expect(prepare.run).toContain(
108108
"OPENCLAW_TELEGRAM_USER_CRABBOX_BIN OPENCLAW_TELEGRAM_USER_CRABBOX_PROVIDER OPENCLAW_TELEGRAM_USER_DRIVER_SCRIPT OPENCLAW_TELEGRAM_USER_PROOF_CMD",
109109
);
110+
expect(prepare.run).toContain("MANTIS_CANDIDATE_TRUST");
110111

111112
const prompt = readFileSync(PROMPT, "utf8");
112113
expect(prompt).toContain("$OPENCLAW_TELEGRAM_USER_PROOF_CMD");
113114
expect(prompt).toContain("do not run\n `pnpm qa:telegram-user:crabbox` directly");
114115
});
115116

117+
it("requires explicit maintainer fork approval before accepting fork PR heads", () => {
118+
const workflowText = readFileSync(WORKFLOW, "utf8");
119+
expect(workflowText).toContain("@openclaw-mantis");
120+
expect(workflowText).toContain("fork[-_]ok");
121+
expect(workflowText).toContain("ALLOW_FORK_CANDIDATE");
122+
expect(workflowText).toContain("maintainer-approved-fork-pr-head");
123+
expect(workflowText).toContain(".head.repo.full_name !=");
124+
125+
const agent = workflowStep("Run Codex Mantis Telegram agent");
126+
expect(agent.env?.MANTIS_CANDIDATE_TRUST).toBe(
127+
"${{ needs.validate_refs.outputs.candidate_trust }}",
128+
);
129+
130+
const prompt = readFileSync(PROMPT, "utf8");
131+
expect(prompt).toContain("MANTIS_CANDIDATE_TRUST");
132+
expect(prompt).toContain("untrusted fork code");
133+
});
134+
116135
it("checks the Telegram user driver before leasing credentials", () => {
117136
const proofScript = readFileSync(PROOF_SCRIPT, "utf8");
118137
const startSession = proofScript.slice(
@@ -132,4 +151,12 @@ describe("Mantis Telegram Desktop proof workflow", () => {
132151
defaultProof.indexOf("leaseCredential({ localRoot, opts, root })"),
133152
);
134153
});
154+
155+
it("does not pass the full workflow environment into the local Telegram SUT", () => {
156+
const proofScript = readFileSync(PROOF_SCRIPT, "utf8");
157+
expect(proofScript).toContain("function childProcessBaseEnv()");
158+
expect(proofScript).toContain("...childProcessBaseEnv()");
159+
expect(proofScript).not.toContain("...process.env,\n OPENAI_API_KEY");
160+
expect(proofScript).not.toContain("...process.env,\n MOCK_PORT");
161+
});
135162
});

0 commit comments

Comments
 (0)