Skip to content
Draft
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
41 changes: 22 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
github-token: ${{ github.token }}
```

The chat runs under the Coder user linked to the GitHub user who applied the label. Set `coder-username` for service-account workflows.
The action resolves the acting user from the GitHub user who applied the label (used for org pick and the per-user reuse label). The chat itself is owned by the user the `coder-token` belongs to. Set `acting-coder-username` to override the acting user; see [Identity](#identity-resolution) and [Security](#security-model) for the full model.

## Inputs

Expand All @@ -49,8 +49,8 @@ The chat runs under the Coder user linked to the GitHub user who applied the lab
| `chat-prompt` | yes | | Prompt to send to the agent. |
| `github-url` | yes | | Issue or pull request URL. |
| `github-token` | yes | | Used to post and update comments. |
| `coder-username` | no | | Run the chat as this Coder user. Mutually exclusive with `github-user-id`. Bypasses the [trust gate](#security-model). |
| `github-user-id` | no | | Resolve to a Coder user by linked GitHub id. Mutually exclusive with `coder-username`. |
| `acting-coder-username` | no | | Override the acting Coder user used for org pick and the per-user reuse label. Mutually exclusive with `acting-github-user-id`. Bypasses the [trust gate](#security-model). Does NOT change the chat owner; the chat is always owned by the `coder-token` holder. |
| `acting-github-user-id` | no | | Resolve the acting Coder user from a linked GitHub id. Mutually exclusive with `acting-coder-username`. Does NOT change the chat owner. |
| `coder-organization` | no | | Coder organization name. Recommended for multi-org users. |
| `workspace-id` | no | | Pin the chat to an existing workspace. |
| `model-config-id` | no | | Model configuration to use. |
Expand All @@ -70,7 +70,7 @@ The chat runs under the Coder user linked to the GitHub user who applied the lab
| `chat-created` | `true` if newly created, `false` if a message was sent to an existing chat. |
| `chat-status` | `waiting`, `pending`, `running`, `paused`, `completed`, `error`. |
| `chat-title` | Chat title. |
| `coder-username` | Coder username the chat ran as. |
| `acting-coder-username` | Acting Coder username (org pick, reuse label). The chat owner is the `coder-token` holder, which may differ. |
| `workspace-id` | Workspace UUID. |
| `pull-request-url` | PR or branch URL when the chat tracks changes. |
| `pull-request-state` | `open`, `closed`, `merged`. |
Expand All @@ -90,14 +90,15 @@ PR/diff outputs come from the chat's `diff_status` and are only reliable when th

### Identity resolution

The action picks the Coder user to run the chat as. First source wins:
The chat itself is always owned by the user the `coder-token` belongs to: `POST /api/experimental/chats` has no owner override, so the API binds ownership to the session. The action separately resolves an **acting user** used for org pick and the per-user reuse label (`coder-agents-chat-action-user`). First source wins:

1. `coder-username` input. Used directly.
2. `github-user-id` input. Looked up by linked GitHub id; deleted Coder users are filtered.
1. `acting-coder-username` input. Used directly.
2. `acting-github-user-id` input. Looked up by linked GitHub id; deleted Coder users are filtered.
3. `github.context.payload.sender.id`. Available on most webhook events.
4. `github.context.actor`. Resolved to a GitHub id via Octokit. Excluded for `schedule` events (the actor is the workflow file editor, not a triggering user).
4. `github.context.actor`. Resolved to a GitHub id via Octokit.
5. `GET /api/v2/users/me` against the configured `coder-token`. Used when no input or workflow-context signal applies (`schedule` events, `workflow_dispatch` without sender or actor, custom `repository_dispatch` chains).

If nothing resolves, the action fails and names the inputs to set.
If the acting user resolves via `acting-coder-username` or `acting-github-user-id` and the result differs from the `coder-token` owner, the action emits a `core.warning` naming both usernames. The chat is still owned by the token holder; the warning surfaces the divergence so the workflow author can confirm the token belongs to the intended user.

### Organization resolution

Expand Down Expand Up @@ -162,7 +163,7 @@ jobs:
coder-url: ${{ secrets.CODER_URL }}
coder-token: ${{ secrets.CODER_TOKEN }}
coder-organization: ${{ secrets.CODER_ORG }} # required if the bot belongs to more than one org
coder-username: doc-check-bot
acting-coder-username: doc-check-bot
chat-prompt: |
Use the doc-check skill to review PR
${{ github.event.pull_request.html_url }}.
Expand All @@ -171,7 +172,7 @@ jobs:
wait: complete
```

`pull_request_target` runs against the base repo and has access to secrets even for fork PRs. The service-account identity bypasses the trust gate so fork PRs are reviewed under a known bot.
`pull_request_target` runs against the base repo and has access to secrets even for fork PRs. The service-account identity bypasses the trust gate so fork PRs are reviewed under the bot's organization and reuse scope. The chat itself is owned by the `coder-token` holder regardless.

### Send a follow-up

Expand Down Expand Up @@ -224,8 +225,8 @@ The action sets `chat-error-kind` and `chat-error-message` on failure, posts a c
| `chat-error-kind` | What happened | What to do |
| ----------------- | ------------- | ---------- |
| `spend_exceeded` | Chat spend limit reached. Spent and limit are in the comment. | Wait for reset or raise the deployment's per-user limit. |
| `user_not_found` | No Coder user matched the GitHub identity. | Pass `coder-username`, or have the user link their GitHub account in Coder. |
| `user_ambiguous` | Multiple live Coder users share the GitHub id. | Set `coder-username` to disambiguate. |
| `user_not_found` | No Coder user matched the GitHub identity. | Pass `acting-coder-username`, or have the user link their GitHub account in Coder. |
| `user_ambiguous` | Multiple live Coder users share the GitHub id. | Set `acting-coder-username` to disambiguate. |
| `org_not_found` | Org missing or the user has no memberships. The comment names which. | Fix or set `coder-organization`. |
| `api_error` | Any other Coder API error. The comment includes the underlying message; wrapped errors carry the original `CoderAPIError` via `Error.cause` and the workflow log renders the full cause chain. | Common causes: bad token, bad `workspace-id`, deployment unreachable. |
| `timeout` | `wait: complete` didn't reach terminal in time. | Raise `wait-timeout-seconds`, or split the work. |
Expand All @@ -239,16 +240,18 @@ Branch on the kind without parsing the message:

## Security model

Identity auto-resolve binds the Coder user matching the GitHub event sender to the chat. The trust gate refuses to auto-resolve when the trigger is untrusted:
The **chat owner** is fixed by the `coder-token`: `POST /api/experimental/chats` has no owner override, so every chat the action creates is owned by the user the token belongs to. Workflows running fork PRs with `secrets.CODER_TOKEN` available (the `pull_request_target` pattern) execute under the workflow's Coder identity, end of story. The primary mitigation against attacker-controlled prompts under your token is GitHub's own rule that `secrets.*` is unavailable to `pull_request` events from forks. Use `pull_request_target` only when you've gated execution accordingly.

- Fork PRs (`head.repo` null, `head.repo.fork === true`, or `head.repo.full_name !== base.repo.full_name`).
- Comment or review events whose `comment.author_association` or `review.author_association` is not `OWNER`, `MEMBER`, or `COLLABORATOR`.
The **acting user** is the Coder identity resolved for org pick and the per-user reuse label (`coder-agents-chat-action-user`). It is NOT the chat owner. The trust gate protects this acting user from pollution by untrusted triggers, layered on top of (not in place of) GitHub's event-permission model. The gate refuses to auto-resolve when:

The gate doesn't read `issue.author_association` or `pull_request.author_association` because those describe the resource opener, not the event sender (a MEMBER labeling a NONE user's issue is fine).
- The trigger is a fork pull request (`head.repo` null, `head.repo.fork === true`, or `head.repo.full_name !== base.repo.full_name`).
- The trigger is a comment or review whose `comment.author_association` or `review.author_association` is not `OWNER`, `MEMBER`, or `COLLABORATOR`.

For other events the action defers to GitHub's own event-permission model. Setting `coder-username` or `github-user-id` explicitly bypasses the gate; the workflow author has chosen the identity.
Without the gate, an attacker who happens to have a linked Coder identity could open a fork PR or drop a drive-by comment and the action would attribute the chat (org pick, reuse label) to that identity. On refusal, the action does not fall back to `users/me`: a hostile trigger should not silently collapse onto the token owner. Setting `acting-coder-username` or `acting-github-user-id` bypasses the gate; the workflow author has chosen the identity explicitly.

Independent of the gate: fork PRs that need secrets must run under `pull_request_target`, not `pull_request`.
The gate does not read `issue.author_association` or `pull_request.author_association` because those describe the resource opener, not the event sender (a MEMBER labeling a NONE user's issue is fine).

Independent of the gate: if your workflow uses `pull_request_target` to run against fork PRs, gate execution on author trust separately (label gating, manual approval). The trust gate covers the auto-resolved acting user only.

## Limitations

Expand Down
12 changes: 6 additions & 6 deletions action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ inputs:
description: "GitHub token used to post and update issue comments."
required: true

github-user-id:
description: "GitHub user ID to resolve to a Coder user. Mutually exclusive with coder-username."
acting-github-user-id:
description: "GitHub user ID. Resolves the acting Coder user (used for org pick and the per-user reuse label) by linked GitHub id. Does NOT change the chat owner; the chat is always owned by the `coder-token` holder. Mutually exclusive with acting-coder-username."
required: false

coder-username:
description: "Coder username to use directly. Mutually exclusive with github-user-id; useful for service-account workflows."
acting-coder-username:
description: "Override the acting Coder user used for org pick and the per-user reuse label. Mutually exclusive with acting-github-user-id. Does NOT change the chat owner; the chat is always owned by the `coder-token` holder. Useful when the workflow's token belongs to one user but the action should attribute the run to another."
required: false

coder-organization:
Expand Down Expand Up @@ -76,8 +76,8 @@ inputs:
default: "false"

outputs:
coder-username:
description: "The Coder username resolved from the GitHub user."
acting-coder-username:
description: "The acting Coder username (used for org pick and the per-user reuse label). The chat owner is the `coder-token` holder, which may differ."

chat-id:
description: "The chat ID."
Expand Down
Loading
Loading