-
Notifications
You must be signed in to change notification settings - Fork 2
sec(workflows): harden permissions, pin SHAs, and fix prompt injection #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
213a0c2
1f94692
cc4e806
34e3f5f
e0d9854
2bf2676
f902245
2d0cb7e
d99cdcb
7b75c90
977fdc7
10b348c
96e8e83
c550569
1f7961b
6384cc4
b924a4c
de42a6d
5187991
5e69e63
6dc9159
aafbdca
bd5d501
7d02ff1
b7858dd
469d3f2
cacdf9f
38338a3
99d8281
0e439bc
f6935fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,8 @@ concurrency: | |
| group: linear-feedback-${{ github.event.pull_request.number }} | ||
| cancel-in-progress: false | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| sync-linear: | ||
| if: | | ||
|
|
@@ -46,70 +48,108 @@ jobs: | |
| env: | ||
| LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| uses: actions/github-script@v7 | ||
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 | ||
| with: | ||
| script: | | ||
| const key = process.env.LINEAR_API_KEY; | ||
| if (!key) { | ||
| core.warning('LINEAR_API_KEY not set — skip Linear subtask sync'); | ||
| return; | ||
| } | ||
| const pr = context.payload.pull_request; | ||
| const prNumber = pr.number; | ||
| const prUrl = pr.html_url; | ||
| const bot = | ||
| context.payload.comment?.user?.login || | ||
| context.payload.review?.user?.login || | ||
| 'bot'; | ||
| const path = context.payload.comment?.path || ''; | ||
| const commentBody = ( | ||
| context.payload.comment?.body || | ||
| context.payload.review?.body || | ||
| '' | ||
| ).slice(0, 3500); | ||
| const commentUrl = | ||
| context.payload.comment?.html_url || | ||
| context.payload.review?.html_url || | ||
| prUrl; | ||
| try { | ||
| const key = process.env.LINEAR_API_KEY; | ||
| if (!key) { | ||
| core.warning('LINEAR_API_KEY not set — skip Linear subtask sync'); | ||
| return; | ||
| } | ||
| const pr = context.payload.pull_request; | ||
| const prNumber = pr.number; | ||
| const prUrl = pr.html_url; | ||
| const bot = | ||
| context.payload.comment?.user?.login || | ||
| context.payload.review?.user?.login || | ||
| 'bot'; | ||
| const path = context.payload.comment?.path || ''; | ||
| const commentBody = ( | ||
| context.payload.comment?.body || | ||
| context.payload.review?.body || | ||
| '' | ||
| ).slice(0, 3500); | ||
| const commentUrl = | ||
| context.payload.comment?.html_url || | ||
| context.payload.review?.html_url || | ||
| prUrl; | ||
|
|
||
| async function linear(query, variables) { | ||
| const res = await fetch('https://api.linear.app/graphql', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: key, | ||
| }, | ||
| body: JSON.stringify({ query, variables }), | ||
| }); | ||
| const json = await res.json(); | ||
| if (json.errors) { | ||
| throw new Error(JSON.stringify(json.errors)); | ||
| async function linear(query, variables) { | ||
| const res = await fetch('https://api.linear.app/graphql', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: key, | ||
| }, | ||
| body: JSON.stringify({ query, variables }), | ||
| }); | ||
| const json = await res.json(); | ||
| if (json.errors) { | ||
| throw new Error(JSON.stringify(json.errors)); | ||
| } | ||
| return json.data; | ||
| } | ||
| return json.data; | ||
| } | ||
|
|
||
| const teamId = '1e672463-31de-40b2-a378-3c03fd8f7e3b'; | ||
| const parentTitle = `PR #${prNumber} agent feedback rollup`; | ||
| const teamId = '1e672463-31de-40b2-a378-3c03fd8f7e3b'; | ||
| const parentTitle = `PR #${prNumber} agent feedback rollup`; | ||
|
|
||
| const search = await linear( | ||
| `query($filter: IssueFilter!) { | ||
| issues(filter: $filter, first: 5) { | ||
| nodes { id identifier title } | ||
| } | ||
| }`, | ||
| { | ||
| filter: { | ||
| title: { eq: parentTitle }, | ||
| team: { id: { eq: teamId } } | ||
| const search = await linear( | ||
| `query($filter: IssueFilter!) { | ||
| issues(filter: $filter, first: 5) { | ||
| nodes { id identifier title } | ||
| } | ||
| }`, | ||
| { | ||
| filter: { | ||
| title: { eq: parentTitle }, | ||
| team: { id: { eq: teamId } } | ||
| } | ||
| } | ||
| ); | ||
| let parentId = search.issues?.nodes?.find( | ||
| n => n.title === parentTitle | ||
| )?.id; | ||
|
|
||
| if (!parentId) { | ||
| const created = await linear( | ||
| `mutation($input: IssueCreateInput!) { | ||
| issueCreate(input: $input) { | ||
| success | ||
| issue { id identifier url } | ||
| } | ||
| }`, | ||
| { | ||
| input: { | ||
| teamId, | ||
| title: parentTitle, | ||
| description: [ | ||
| '**Agent: Jules | Grok**', | ||
| '', | ||
| `Parent rollup for automated review feedback on ${prUrl}`, | ||
| '', | ||
| 'Subtasks are created by GHA `agent-feedback-linear-sync`.', | ||
| 'Jules auto-resolve via `agent-review-auto-jules`.', | ||
| '', | ||
| 'Signed-off-by: Grok <grok@x.ai>', | ||
| ].join('\n'), | ||
| }, | ||
| } | ||
| ); | ||
| parentId = created.issueCreate.issue.id; | ||
| core.info(`Created parent ${created.issueCreate.issue.identifier}`); | ||
| } | ||
| ); | ||
| let parentId = search.issues?.nodes?.find( | ||
| n => n.title === parentTitle | ||
| )?.id; | ||
|
|
||
| if (!parentId) { | ||
| const created = await linear( | ||
| const labels = await linear( | ||
| `query { issueLabels(filter: { name: { eq: "agent-feedback" } }) { nodes { id } } }` | ||
| ); | ||
| const labelId = labels.issueLabels?.nodes?.[0]?.id; | ||
|
|
||
| const subTitle = path | ||
| ? `[${bot}] PR #${prNumber}: ${path}` | ||
| : `[${bot}] PR #${prNumber}: review feedback`; | ||
|
|
||
| const sub = await linear( | ||
| `mutation($input: IssueCreateInput!) { | ||
| issueCreate(input: $input) { | ||
| success | ||
|
|
@@ -119,73 +159,49 @@ jobs: | |
| { | ||
| input: { | ||
| teamId, | ||
| title: parentTitle, | ||
| parentId, | ||
| title: subTitle.slice(0, 200), | ||
| description: [ | ||
| '**Agent: Jules | Grok**', | ||
| '', | ||
| `Parent rollup for automated review feedback on ${prUrl}`, | ||
| `Source: ${commentUrl}`, | ||
| `PR: ${prUrl}`, | ||
| `Bot: ${bot}`, | ||
| path ? `Path: \`${path}\`` : '', | ||
| '', | ||
| 'Subtasks are created by GHA `agent-feedback-linear-sync`.', | ||
| 'Jules auto-resolve via `agent-review-auto-jules`.', | ||
| '### Comment', | ||
| commentBody, | ||
| '', | ||
| 'Signed-off-by: Grok <grok@x.ai>', | ||
| ].join('\n'), | ||
| 'Auto-synced by agent-feedback-linear-sync.yml', | ||
| ].filter(Boolean).join('\n'), | ||
| ...(labelId ? { labelIds: [labelId] } : {}), | ||
| }, | ||
| } | ||
| ); | ||
| parentId = created.issueCreate.issue.id; | ||
| core.info(`Created parent ${created.issueCreate.issue.identifier}`); | ||
| } | ||
|
|
||
| const labels = await linear( | ||
| `query { issueLabels(filter: { name: { eq: "agent-feedback" } }) { nodes { id } } }` | ||
| ); | ||
| const labelId = labels.issueLabels?.nodes?.[0]?.id; | ||
|
|
||
| const subTitle = path | ||
| ? `[${bot}] PR #${prNumber}: ${path}` | ||
| : `[${bot}] PR #${prNumber}: review feedback`; | ||
| core.info(`Subtask ${sub.issueCreate.issue.identifier} ${sub.issueCreate.issue.url}`); | ||
|
|
||
| const sub = await linear( | ||
| `mutation($input: IssueCreateInput!) { | ||
| issueCreate(input: $input) { | ||
| success | ||
| issue { id identifier url } | ||
| if (context.payload.comment?.id) { | ||
| try { | ||
| await github.rest.reactions.createForPullRequestReviewComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| comment_id: context.payload.comment.id, | ||
| content: 'eyes', | ||
| }); | ||
| } catch (e) { | ||
| core.info(String(e)); | ||
| } | ||
| }`, | ||
| { | ||
| input: { | ||
| teamId, | ||
| parentId, | ||
| title: subTitle.slice(0, 200), | ||
| description: [ | ||
| '**Agent: Jules | Grok**', | ||
| '', | ||
| `Source: ${commentUrl}`, | ||
| `PR: ${prUrl}`, | ||
| `Bot: ${bot}`, | ||
| path ? `Path: \`${path}\`` : '', | ||
| '', | ||
| '### Comment', | ||
| commentBody, | ||
| '', | ||
| 'Auto-synced by agent-feedback-linear-sync.yml', | ||
| ].filter(Boolean).join('\n'), | ||
| ...(labelId ? { labelIds: [labelId] } : {}), | ||
| }, | ||
| } | ||
| ); | ||
| core.info(`Subtask ${sub.issueCreate.issue.identifier} ${sub.issueCreate.issue.url}`); | ||
|
|
||
| if (context.payload.comment?.id) { | ||
| try { | ||
| await github.rest.reactions.createForPullRequestReviewComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| comment_id: context.payload.comment.id, | ||
| content: 'eyes', | ||
| }); | ||
| } catch (e) { | ||
| core.info(String(e)); | ||
| } catch (err) { | ||
| const errMsg = String(err); | ||
| if ( | ||
| errMsg.includes('USAGE_LIMIT_EXCEEDED') || | ||
| errMsg.includes('usage limit exceeded') || | ||
| errMsg.includes('free issue limit') || | ||
| errMsg.includes('exceeded the free issue limit') | ||
| ) { | ||
| core.warning(`Linear workspace free issue limit exceeded. Unable to sync comment. Error details: ${errMsg}`); | ||
| return; | ||
| } | ||
| throw err; | ||
|
Comment on lines
+195
to
+206
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: New try/catch swallows only Linear quota errors but wraps the GitHub reaction call too Wrapping the whole script in try/catch is fine, but note the inner reaction call already has its own catch, so the outer handler mainly covers Linear API errors. Non-quota errors are re-thrown, preserving prior failure behavior. Also Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,8 @@ concurrency: | |
| group: jules-issue-${{ github.event.issue.number || github.run_id }} | ||
| cancel-in-progress: false | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| jules-on-label: | ||
| if: | | ||
|
|
@@ -40,7 +42,7 @@ jobs: | |
| steps: | ||
| - name: Acknowledge (👀) + inventory open agent PRs | ||
| id: coord | ||
| uses: actions/github-script@v7 | ||
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- repository conventions ---'
find /tmp/coderabbit-repo-knowledge/timerloggedout-spec-termux-monorepo-75290edd -path '*/.github*' -o -path '*/learnings/*' 2>/dev/null | head -50
for f in /tmp/coderabbit-repo-knowledge/timerloggedout-spec-termux-monorepo-75290edd/*/*.md; do
case "$f" in
*github*|*workflow*|*learnings*) printf '\n--- %s ---\n' "$f"; cat "$f";;
esac
done
printf '%s\n' '--- workflow excerpts ---'
cat -n .github/workflows/agent-jules-on-issues.yml | sed -n '1,60p;130,200p;210,230p'Repository: timerloggedout-spec/termux-monorepo Length of output: 7614 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- all github-script invocations and token fields ---'
awk '
/uses: actions\/github-script@/ { start=NR; print "\ninvocation at line " NR }
NR >= start && NR <= start+8 { print NR ":" $0 }
' .github/workflows/agent-jules-on-issues.yml
printf '%s\n' '--- github.rest calls in this workflow ---'
rg -n 'github\.rest|github-token' .github/workflows/agent-jules-on-issues.ymlRepository: timerloggedout-spec/termux-monorepo Length of output: 3083 🌐 Web query:
💡 Result: Yes, the official actions/github-script action defines the default value of the github-token input as ${{ github.token }} [1][2][3]. The github-token input is optional, and the action uses the provided token to create an authenticated GitHub client [4][5]. By default, this mechanism utilizes the GITHUB_TOKEN associated with the workflow, which is scoped to the current repository [4][5]. Users typically only need to provide a different token (such as a personal access token) via the github-token input if they require access to resources or APIs that the default GITHUB_TOKEN does not have permissions to access [4][5]. The ${{ github.token }} context is equivalent to ${{ secrets.GITHUB_TOKEN }} [1][6]. Citations:
Pass Each 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| with: | ||
| script: | | ||
| const issue = context.payload.issue.number; | ||
|
|
@@ -102,15 +104,18 @@ jobs: | |
| if: ${{ secrets.JULES_API_KEY != '' }} | ||
| continue-on-error: true | ||
| uses: google-labs-code/jules-invoke@v1 | ||
| env: | ||
| ISSUE_TITLE: ${{ github.event.issue.title }} | ||
| ISSUE_BODY: ${{ github.event.issue.body }} | ||
| with: | ||
| jules_api_key: ${{ secrets.JULES_API_KEY }} | ||
| starting_branch: master-staging | ||
| prompt: | | ||
| You are Jules working on termux-monorepo. Read AGENTS.md and GEMINI.md if present. | ||
|
|
||
| ## Issue #${{ github.event.issue.number }}: ${{ github.event.issue.title }} | ||
|
|
||
| ${{ github.event.issue.body }} | ||
| Please analyze and resolve the issue provided in the environment variables: | ||
| - Issue Title: $ISSUE_TITLE | ||
| - Issue Body: $ISSUE_BODY | ||
|
Comment on lines
113
to
+118
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Require an These prompts tell Jules to analyze and resolve an issue, but they do not require an applicable row in As per coding guidelines: “Do not invent work outside Also applies to: 247-251 🤖 Prompt for AI AgentsSource: Coding guidelines
Comment on lines
+107
to
+118
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Treat the PR inventory as untrusted data.
Also applies to: 240-251 🤖 Prompt for AI Agents🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- applicable repository convention files ---'
find /tmp/coderabbit-repo-knowledge/timerloggedout-spec-termux-monorepo-75290edd -type f -path '*/specs/*.md' -o -path '*/conventions/*.md' 2>/dev/null | sort | while read -r f; do
case "$f" in
*github*|*workflow*|*action*|*ci*|*security*) printf '\n### %s\n' "$f"; cat "$f";;
esac
done
printf '%s\n' '--- workflow locations and surrounding source ---'
cat -n .github/workflows/agent-jules-on-issues.yml | sed -n '95,125p;228,258p'
printf '%s\n' '--- branch and Jules references in this workflow ---'
rg -n -C 2 'starting_branch|master-staging|Jules|jules' .github/workflows/agent-jules-on-issues.ymlRepository: timerloggedout-spec/termux-monorepo Length of output: 10545 Use Both Jules invocation blocks set 🤖 Prompt for AI AgentsSource: Coding guidelines 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: printf '%s\n' '--- workflow ---'
sed -n '90,125p' .github/workflows/agent-jules-on-issues.yml
sed -n '225,260p' .github/workflows/agent-jules-on-issues.yml
printf '%s\n' '--- workflow declarations and secret references ---'
sed -n '1,40p' .github/workflows/agent-jules-on-issues.yml
rg -n -C 3 'JULES_API_KEY|env:|uses:.*jules|prompt:' .github/workflows/agent-jules-on-issues.yml
printf '%s\n' '--- applicable repository guidance ---'
head -5 /tmp/coderabbit-repo-knowledge/timerloggedout-spec-termux-monorepo-75290edd/*/*.mdRepository: timerloggedout-spec/termux-monorepo Length of output: 9946 🌐 Web query:
💡 Result: In GitHub Actions, you cannot directly reference secrets in Citations:
Map GitHub Actions does not support direct secret references in 🤖 Prompt for AI Agents🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- applicable repository conventions ---'
find /tmp/coderabbit-repo-knowledge/timerloggedout-spec-termux-monorepo-75290edd -maxdepth 2 -type f -name '*.md' -print
printf '%s\n' '--- workflow lines ---'
cat -n .github/workflows/agent-jules-on-issues.yml | sed -n '90,125p;225,260p'
printf '%s\n' '--- Jules action references and local action definitions ---'
rg -n --glob '*.yml' --glob '*.yaml' --glob 'action.yml' --glob 'action.yaml' \
'jules_api_key|starting_branch|prompt\.txt|inputs\.prompt|uses:.*jules|ISSUE_TITLE|COMMENT_BODY' .github . 2>/dev/null | head -160
printf '%s\n' '--- convention content ---'
for f in /tmp/coderabbit-repo-knowledge/timerloggedout-spec-termux-monorepo-75290edd/*/*.md; do
printf '\n### %s\n' "$f"
cat "$f"
doneRepository: timerloggedout-spec/termux-monorepo Length of output: 13312 🌐 Web query:
💡 Result: In the google-labs-code/jules-invoke GitHub Action, the prompt input is a required string that defines the task for the Jules AI agent to perform on your codebase [1][2][3]. When the action runs, it processes this prompt input by writing it to a file named prompt.txt within the runner's environment [2]. The action then optionally appends additional context to this file—such as the content of the last commit or the commit log—depending on the values of the include_last_commit and include_commit_log inputs [1][2]. Finally, the content of prompt.txt is sent as part of the payload to the Jules API [2]. You can provide this input directly in your workflow YAML using the with keyword [1][3]: jobs: run-jules: runs-on: ubuntu-latest steps: - uses: google-labs-code/jules-invoke@v1 with: prompt: | Your instructions for Jules go here. jules_api_key: ${{ secrets.JULES_API_KEY }} Citations:
Use workflow expressions for event values.
🤖 Prompt for AI Agents |
||
|
|
||
| ## Open agent / related PRs (DO NOT overlap files) | ||
| ${{ steps.coord.outputs.prior_prs }} | ||
|
|
@@ -136,7 +141,7 @@ jobs: | |
|
|
||
| - name: Fallback @jules ping (App path) | ||
| if: ${{ secrets.JULES_API_KEY == '' }} | ||
| uses: actions/github-script@v7 | ||
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 | ||
| with: | ||
| script: | | ||
| const issue = context.payload.issue.number; | ||
|
|
@@ -180,7 +185,7 @@ jobs: | |
| steps: | ||
| - name: React 👀 on comment + inventory PRs | ||
| id: coord | ||
| uses: actions/github-script@v7 | ||
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 | ||
| with: | ||
| script: | | ||
| try { | ||
|
|
@@ -214,7 +219,7 @@ jobs: | |
| core.setOutput('prior_prs', inventory); | ||
|
|
||
| - name: Ensure jules label | ||
| uses: actions/github-script@v7 | ||
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 | ||
| with: | ||
| script: | | ||
| try { | ||
|
|
@@ -232,16 +237,18 @@ jobs: | |
| if: ${{ secrets.JULES_API_KEY != '' }} | ||
| continue-on-error: true | ||
| uses: google-labs-code/jules-invoke@v1 | ||
| env: | ||
| ISSUE_TITLE: ${{ github.event.issue.title }} | ||
| ISSUE_BODY: ${{ github.event.issue.body }} | ||
| COMMENT_BODY: ${{ github.event.comment.body }} | ||
| with: | ||
| jules_api_key: ${{ secrets.JULES_API_KEY }} | ||
| starting_branch: master-staging | ||
| prompt: | | ||
| Issue #${{ github.event.issue.number }}: ${{ github.event.issue.title }} | ||
|
|
||
| ${{ github.event.issue.body }} | ||
|
|
||
| User request: | ||
| ${{ github.event.comment.body }} | ||
| Please analyze and resolve the issue and user comment provided in the environment variables: | ||
| - Issue Title: $ISSUE_TITLE | ||
| - Issue Body: $ISSUE_BODY | ||
| - User Comment: $COMMENT_BODY | ||
|
|
||
| ## Open agent / related PRs (DO NOT overlap files) | ||
| ${{ steps.coord.outputs.prior_prs }} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Pass
OPERATOR_TOKENexplicitly to GitHub API calls.The
actions/github-scriptsteps fall back toGITHUB_TOKENwhengithub-tokenis omitted. Setgithub-token: ${{ secrets.OPERATOR_TOKEN }}for the reaction request here and for the corresponding calls inagent-jules-on-issues.ymlso authenticated operations continue using the intended token under the new permission restrictions.📍 Affects 2 files
.github/workflows/agent-feedback-linear-sync.yml#L51-L53(this comment).github/workflows/agent-jules-on-issues.yml#L45-L45🤖 Prompt for AI Agents
Source: Coding guidelines