Skip to content

Commit df85529

Browse files
committed
fix(agent): map installation identity from gh token
1 parent 86efc71 commit df85529

File tree

1 file changed

+92
-1
lines changed

1 file changed

+92
-1
lines changed

.github/workflows/agent.yml

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2990,6 +2990,80 @@ jobs:
29902990
fs.writeFileSync(".ubiquityos.issue.md", markdown);
29912991
NODE
29922992
2993+
- name: Map GitHub App installation token
2994+
if: steps.parse.outputs.allowed == 'true' && steps.parse.outputs.privileged == 'true'
2995+
id: app_token_map
2996+
env:
2997+
GH_TOKEN: ${{ inputs.authToken }}
2998+
REPO: ${{ steps.parse.outputs.repo }}
2999+
EXPECTED_INSTALLATION_ID: ${{ steps.parse.outputs.installation_id }}
3000+
run: |
3001+
set -euo pipefail
3002+
3003+
repo="${REPO:?Missing REPO}"
3004+
gh api "repos/$repo" > "$RUNNER_TEMP/ubq_repo_from_token.json"
3005+
3006+
# Preferred mapping source for installation tokens.
3007+
if gh api "installation" > "$RUNNER_TEMP/ubq_installation_from_token.json"; then
3008+
echo "Resolved installation metadata via /installation."
3009+
else
3010+
echo "Could not read /installation with current token; falling back to webhook installation id."
3011+
printf '{}' > "$RUNNER_TEMP/ubq_installation_from_token.json"
3012+
fi
3013+
3014+
node <<'NODE'
3015+
const fs = require("node:fs");
3016+
3017+
const expectedRaw = String(process.env.EXPECTED_INSTALLATION_ID || "").trim();
3018+
const expectedInstallationId = Number.parseInt(expectedRaw, 10);
3019+
const expectedId = Number.isFinite(expectedInstallationId) && expectedInstallationId > 0 ? expectedInstallationId : null;
3020+
3021+
const readJson = (path, fallback) => {
3022+
try {
3023+
return JSON.parse(fs.readFileSync(path, "utf8"));
3024+
} catch {
3025+
return fallback;
3026+
}
3027+
};
3028+
3029+
const repoInfo = readJson(`${process.env.RUNNER_TEMP}/ubq_repo_from_token.json`, {});
3030+
const installationInfo = readJson(`${process.env.RUNNER_TEMP}/ubq_installation_from_token.json`, {});
3031+
3032+
const mappedInstallationId = typeof installationInfo.id === "number" && Number.isFinite(installationInfo.id) ? installationInfo.id : expectedId;
3033+
if (expectedId && mappedInstallationId && expectedId !== mappedInstallationId) {
3034+
console.error(`GitHub App installation mismatch: expected ${expectedId}, mapped ${mappedInstallationId}.`);
3035+
process.exit(1);
3036+
}
3037+
3038+
const tokenMap = {
3039+
tokenType: "github_app_installation",
3040+
installationId: mappedInstallationId,
3041+
expectedInstallationId: expectedId,
3042+
appSlug: typeof installationInfo.app_slug === "string" ? installationInfo.app_slug : "",
3043+
accountLogin: typeof installationInfo?.account?.login === "string" ? installationInfo.account.login : "",
3044+
repository: typeof repoInfo.full_name === "string" ? repoInfo.full_name : "",
3045+
};
3046+
3047+
const outputLines = [];
3048+
const setOutput = (k, v) => outputLines.push(`${k}<<EOF\n${String(v)}\nEOF\n`);
3049+
3050+
setOutput("map_json", JSON.stringify(tokenMap));
3051+
setOutput("installation_id", tokenMap.installationId ? String(tokenMap.installationId) : "");
3052+
setOutput("app_slug", tokenMap.appSlug);
3053+
setOutput("account_login", tokenMap.accountLogin);
3054+
setOutput("repo", tokenMap.repository);
3055+
3056+
fs.appendFileSync(process.env.GITHUB_OUTPUT, outputLines.join(""));
3057+
3058+
const printable = {
3059+
installationId: tokenMap.installationId,
3060+
appSlug: tokenMap.appSlug || null,
3061+
accountLogin: tokenMap.accountLogin || null,
3062+
repository: tokenMap.repository || null,
3063+
};
3064+
process.stdout.write(`GitHub App token mapping: ${JSON.stringify(printable)}\n`);
3065+
NODE
3066+
29933067
- name: Fetch marketplace plugin registry (best-effort)
29943068
if: steps.parse.outputs.allowed == 'true' && steps.parse.outputs.privileged == 'true'
29953069
continue-on-error: true
@@ -3034,6 +3108,7 @@ jobs:
30343108
CONVERSATION_KEY: ${{ steps.parse.outputs.conversation_key }}
30353109
ENVIRONMENT: ${{ steps.parse.outputs.environment }}
30363110
CONFIG_PATH_CANDIDATES_JSON: ${{ steps.parse.outputs.config_path_candidates }}
3111+
APP_TOKEN_MAP_JSON: ${{ steps.app_token_map.outputs.map_json }}
30373112
run: |
30383113
node <<'NODE'
30393114
const fs = require("node:fs");
@@ -3045,12 +3120,27 @@ jobs:
30453120
const conversationKey = String(process.env.CONVERSATION_KEY || "").trim();
30463121
const environment = String(process.env.ENVIRONMENT || "").trim();
30473122
const configPathCandidatesJson = String(process.env.CONFIG_PATH_CANDIDATES_JSON || "").trim();
3123+
const appTokenMapJson = String(process.env.APP_TOKEN_MAP_JSON || "").trim();
30483124
let configPathCandidates = [];
3125+
let appTokenMap = null;
30493126
try {
30503127
configPathCandidates = configPathCandidatesJson ? JSON.parse(configPathCandidatesJson) : [];
30513128
} catch {
30523129
configPathCandidates = [];
30533130
}
3131+
try {
3132+
appTokenMap = appTokenMapJson ? JSON.parse(appTokenMapJson) : null;
3133+
} catch {
3134+
appTokenMap = null;
3135+
}
3136+
3137+
const authMappingLine =
3138+
privileged && appTokenMap && typeof appTokenMap === "object"
3139+
? [
3140+
"- GH token mapping (resolved from this run token):",
3141+
` installation_id=${appTokenMap.installationId || "unknown"}, app_slug=${appTokenMap.appSlug || "unknown"}, account=${appTokenMap.accountLogin || "unknown"}, repo_access=${appTokenMap.repository || "unknown"}.`,
3142+
].join("\n")
3143+
: null;
30543144
30553145
const contextLines = [
30563146
"- The full issue/PR thread (including collaborators' comments) is available in `.ubiquityos.issue.md`.",
@@ -3062,13 +3152,14 @@ jobs:
30623152
? `- Config path candidates (edit the active one): ${configPathCandidates.join(", ")}`
30633153
: null,
30643154
"- If present, the marketplace plugin registry is in `.ubiquityos.marketplace.json` (best-effort; may be empty if access is unavailable).",
3155+
authMappingLine,
30653156
].filter(Boolean);
30663157
30673158
const operationsLines = privileged
30683159
? [
30693160
"GitHub operations (privileged):",
30703161
"- The invoker is an OWNER/MEMBER/COLLABORATOR; you may use the GitHub CLI `gh` for GitHub operations (issues/PRs/labels/comments).",
3071-
"- Authentication: `GH_TOKEN` is set to a GitHub App installation token. `gh auth status` may display `actions-user` (expected for non-user tokens). Do not run `gh auth login`. If you need a sanity check, run: `gh api repos/<owner>/<repo> -q .full_name`.",
3162+
"- Authentication: `GH_TOKEN` is a GitHub App installation token. Treat the GH token mapping in this prompt as source of truth. `gh auth status` may display `actions-user` (expected for non-user tokens). Do not run `gh auth login`.",
30723163
"- Marketplace auth (optional): if you need to create/update repos under the marketplace org, `UOS_MARKETPLACE_GH_TOKEN` may be set and `UOS_MARKETPLACE_ORG` is provided.",
30733164
' - For marketplace ops, prefix commands like: `GH_TOKEN=\"$UOS_MARKETPLACE_GH_TOKEN\" gh ... --repo \"$UOS_MARKETPLACE_ORG/<repo>\"`.',
30743165
"- Sandbox: outbound network for tool commands is enabled in this run.",

0 commit comments

Comments
 (0)