-
Notifications
You must be signed in to change notification settings - Fork 1
139 lines (131 loc) · 6.6 KB
/
Copy pathframework-agent.yml
File metadata and controls
139 lines (131 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# The workflow ActionsDriver dispatches (#610): one run is one `prompt()` turn.
#
# The driver cannot learn a run id from the dispatch API (it answers 204 with no body), so
# the correlation id it generates is echoed into `run-name` and into the artifact name. That
# is how the driver finds its own run, and it is why both must keep interpolating it.
name: framework-agent
run-name: framework-agent ${{ inputs.correlation_id }}
on:
workflow_dispatch:
inputs:
prompt:
description: What the agent should do. Passed to the action verbatim, never through a shell.
required: true
correlation_id:
description: How the driver finds this run. Must appear in run-name and in the artifact name.
required: true
model:
description: Model id, e.g. claude-opus-4-8. Empty means the action's default.
required: false
resume_session_id:
description: A prior agent session id to continue instead of starting fresh.
required: false
branch:
description: Branch to push the run's work to, so the driver can read it back and continue on it (#1085).
required: false
# The workflow pushes the run branch and the agent may open a PR. `id-token: write` is not
# optional: the action exchanges an OIDC token to authenticate the subscription OAuth token,
# and without it every run fails with "Could not fetch an OIDC token".
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
agent:
runs-on: ubuntu-latest
# Well under the 6h job cap. A turn that runs this long has gone wrong.
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
# Full history: the agent reads the log to understand what it is changing.
fetch-depth: 0
# Built from environment variables rather than by interpolating ${{ }} into the shell,
# so a crafted input cannot become a command. The driver validates them too.
- name: Compose agent arguments
id: args
env:
MODEL: ${{ inputs.model }}
RESUME: ${{ inputs.resume_session_id }}
run: |
set -euo pipefail
# Agent mode grants no permissions by default, so an unattended run cannot edit
# or run anything without this. The runner is disposable, which is what makes it safe.
args="--dangerously-skip-permissions"
# Written as `if` rather than `[ .. ] && ..`, which returns non-zero when the input
# is empty and would fail the step under `set -e`.
if [ -n "$MODEL" ]; then args="$args --model $MODEL"; fi
if [ -n "$RESUME" ]; then args="$args --resume $RESUME"; fi
echo "value=$args" >> "$GITHUB_OUTPUT"
- name: Run the agent
id: claude
uses: anthropics/claude-code-action@v1
with:
# A `claude setup-token` OAuth token: the run spends the subscription, not an API key (#495).
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: ${{ inputs.prompt }}
claude_args: ${{ steps.args.outputs.value }}
# The runner and its checkout vanish when the job ends, so the only way the driver can
# read the agent's work (diffs, file contents) or run the next turn on top of it is a
# branch on origin. The action does not create one for a workflow_dispatch run, so we
# push it here, to the name the driver chose. This mirrors the local flow, where the
# framework (not the agent) pushes the session branch (#799); the agent just commits.
- name: Push the run branch
if: always()
id: runbranch
env:
RUN_BRANCH: ${{ inputs.branch }}
DISPATCH_REF: ${{ github.ref_name }}
# Authenticate the push explicitly. The agent step runs its own git setup and leaves
# the checkout's persisted credentials unusable, so a plain `git push origin` fails
# with "Authentication failed"; push through a tokenized URL instead.
GH_PUSH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if [ -z "$RUN_BRANCH" ]; then echo "no run branch requested"; exit 0; fi
git config user.name "framework-agent"
git config user.email "framework-agent@users.noreply.github.com"
# Commit anything the agent left uncommitted so it is not lost with the runner.
if [ -n "$(git status --porcelain)" ]; then
git add -A
git commit -m "framework agent run ($RUN_BRANCH)" --quiet
fi
# Don't create an empty branch for a no-op turn: only push when the run advanced
# past the ref it checked out. If that ref can't be resolved, err toward pushing.
base="$(git rev-parse --verify --quiet "origin/$DISPATCH_REF" || true)"
if [ -n "$base" ] && [ "$(git rev-parse HEAD)" = "$base" ]; then
echo "no new commits; nothing to push"
exit 0
fi
git push "https://x-access-token:${GH_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:refs/heads/$RUN_BRANCH"
echo "branch=$RUN_BRANCH" >> "$GITHUB_OUTPUT"
# The only REST-readable channel out of a run. `always()` so a failed turn still
# returns its transcript, which is exactly when we most want to read it.
- name: Collect the transcript
if: always()
env:
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
# The branch we actually pushed above, not the action's branch_name, which stays
# empty for a workflow_dispatch agent run (#1085).
BRANCH: ${{ steps.runbranch.outputs.branch }}
SESSION_ID: ${{ steps.claude.outputs.session_id }}
run: |
set -euo pipefail
# Not dot-prefixed: upload-artifact@v4 defaults include-hidden-files to false and
# drops every file under a hidden path segment, so a `.framework-run/` dir uploads
# nothing and the driver finds no artifact to read.
mkdir -p framework-run
# An empty array still parses, so a crashed action yields an empty turn, not a driver error.
if [ -n "$EXECUTION_FILE" ] && [ -f "$EXECUTION_FILE" ]; then
cp "$EXECUTION_FILE" framework-run/execution.json
else
echo '[]' > framework-run/execution.json
fi
jq -n --arg branch "$BRANCH" --arg session_id "$SESSION_ID" \
'{branch: $branch, session_id: $session_id}' > framework-run/meta.json
- uses: actions/upload-artifact@v4
if: always()
with:
name: framework-run-${{ inputs.correlation_id }}
path: framework-run
retention-days: 7