Skip to content

Threats to the runs and the workflows

← Security design · Threats and countermeasures

What can go wrong around the AI, in the runs of the workflows, their packages and their logs, with the countermeasure and the evidence of each.

Threat Countermeasure Evidence
A package of the Gemini CLI or the Copilot CLI, or one that they depend on, is replaced on npm's registry, or a new release of one is broken or malicious analyze installs only the lock files of engines/, with npm ci, which takes each package at its locked version, refuses one whose SHA-512 hash isn't the locked one and runs no script of a package; an update of a lock file merges only once the required check engine contracts of its pull request passed, which verifies the signatures of npm's registry and the attestations of every package that npm installs from it on GitHub's Ubuntu x64 runner, and that npm refuses another hash, as the packages of the engines shows test_the_lock_file_pins_every_package_with_its_hash, test_analyze_runs_the_engine_with_its_secret_alone, test_a_failed_install_says_what_npm_said in tests/test_engines.py; test_the_lock_file_passes_when_its_signatures_verify_and_npm_refuses_a_hash in tests/test_engine_contract.py
An issue keeps the assistant busy, so that no analysis of the repository finishes, because each one reads many issues; or a repository's tens of thousands of issues and pull requests do An analysis reads every open issue but only the max_issues closed issues and the max_discussions discussions that changed last, in pages of a hundred without pull requests, and a reply only its issue. A first analysis learns from the max_analyzed issues analyzed last, from at most 5 pages of the comments written last, and asks for the permission of at most 20 people behind a 👎, never of a bot. Each text is read up to GitHub's 65,536 characters, by patterns that look at each character a bounded number of times; the AI reads at most 20,000 characters of each text of its issue, and HTML comments are removed as GitHub hides them, also one that starts a line and never ends test_the_context_reads_every_open_issue_and_the_closed_ones_that_changed_last, test_the_discussions_end_at_their_limit, test_no_text_makes_reading_an_issue_slow, test_no_text_makes_the_cleaning_slow, test_forms_read_as_they_always_did in tests/test_assistant.py; property tests against the earlier patterns in tests/test_properties.py; fuzzing with Atheris in fuzz/fuzz_issue_text.py, which fails on a reading that takes longer than half a second
Someone steers the assistant with a command: mutes it, closes an issue as a duplicate of their own, asks the AI to post something, or makes it analyze again and again A command counts only from a maintainer or the reporter of the issue; from anyone else, it is an ordinary comment. The reporter may only say not-duplicate and ask once per issue for a new analysis, while it waits for triage, within the limits of the day, which record the run in the marker of the assistant's own analysis. The question of /assistant ask reaches the AI in comment.md, never in the prompt, and its answer passes every check of apply, once for each question; the event job runs no AI test_a_command_is_the_first_line_of_a_maintainer_s_or_the_reporter_s_comment, test_a_command_of_anyone_else_is_an_ordinary_comment, test_the_reporter_may_ask_for_a_new_analysis_once, test_the_reporter_s_again_counts_for_the_limits_of_the_day, test_the_answer_to_a_maintainer_s_question_is_checked_and_posted_once, test_an_answer_goes_only_to_a_maintainer_s_question in tests/test_assistant.py; test_a_whole_comment_mentions_nobody_and_links_only_allowed_places in tests/test_properties.py
A label name or the model of config.toml adds arguments to the command line of the engine, or breaks it, so that no issue gets an analysis The schema writes an apostrophe of a label name as \u0027, which ends no quotes and reads back as the same JSON. check and the engine command refuse a model that is not a name of letters, digits and ._:@/[]-, and one that starts with a dash, which the engine's action would read as another flag test_the_schemas_fit_into_the_engine_arguments, test_a_model_that_would_add_arguments_is_refused in tests/test_assistant.py; test_a_model_that_would_add_arguments_fails_the_check_and_the_engine in tests/test_setup.py
Anyone opens many issues or discussions, also from many accounts and all at once, or comments again and again after a release, so that the paid engine runs without end or uses up a subscription that other repositories share (denial of wallet) The event job starts an analysis, a first answer to a discussion, a follow-up or a reply after a release only while the 24 hours before hold fewer than max_per_day new issues and discussions of the categories that get answers of people who don't maintain the repository, follow-ups and replies after a release; a new issue counts every issue opened before it, also one that a limit kept from the AI, so neither a flood at once nor issues that keep the AI from answering get past the count. A new issue also needs its reporter to have opened fewer than max_per_author_per_day, and the AI reads only release_replies comments of the reporter after each release. The event job records each follow-up and reply after a release as it starts, in the marker of the assistant's own comment that the run answers, which only the assistant can change, so neither runs that still go on nor deleted comments get past the count. Over a limit, the issue keeps its labels and needs-triage for the maintainer, and the run says so in a notice. A limit that is no whole number fails check and stops the run instead of lifting the limit test_a_new_issue_beyond_the_daily_limit_waits_for_the_maintainer, test_only_the_new_issues_of_reporters_before_this_one_count, test_the_answers_of_the_day_count_toward_the_daily_limit, test_a_follow_up_counts_while_it_still_runs, test_one_reporter_gets_three_analyses_a_day, test_the_ai_reads_two_comments_of_the_reporter_after_the_release, test_deleted_comments_after_the_release_get_no_more_replies, test_maintainers_are_never_limited, test_a_limit_that_is_no_whole_number_stops_the_run in tests/test_assistant.py; test_config_problems_are_found in tests/test_setup.py
Someone comments while a reply of the AI waits in line, so that it judges their comment instead of the one that started it: a "still broken for me too" that reopens an issue whose reporter just confirmed the fix, or a question of someone else that marks the issue as waiting for its reporter The context step and the apply job read the comment of the run's event. The AI judges that comment alone, in comment.md, and apply reads it again and acts only when its author is the reporter (release-reply) or a maintainer (maintainer-reply); a run by hand judges the newest such comment test_a_reply_reads_the_issue_and_the_comment_that_started_it, test_a_release_reply_acts_only_on_the_reporter_s_comment_that_it_judged, test_a_maintainer_reply_acts_only_on_the_maintainer_s_comment_that_it_judged, test_context_and_apply_judge_the_comment_of_the_event in tests/test_assistant.py
A job that writes to discussions breaks the analyses of a repository whose template grants discussions only reading, or a compromised step of another job writes there A called workflow that asks for more than its caller grants doesn't start, so the jobs of the issues only read discussions, and reusable-discussion-assistant.yml, which only discussion-assistant.yml calls, has the one job that writes there: apply, which runs no AI, behind Harden-Runner. Its job analyze is the one of the issues, which a test keeps equal test_only_the_apply_job_of_the_discussions_writes_to_discussions, test_the_discussions_analyze_as_the_issues_do in tests/test_templates.py
Text of an issue runs as a shell command in a workflow Every input goes through the environment; no expression of untrusted text stands in a run: script test_untrusted_text_never_reaches_a_shell_script, test_the_action_passes_every_input_through_the_environment
A pull request runs its code with the permissions of the lifecycle The lifecycle checks out the default branch and runs no code of the pull request test_merges_and_releases_start_the_lifecycle_without_running_pull_request_code
A compromised action or tool in a job with a write token sends the token elsewhere Harden-Runner blocks every host but api.github.com and github.com in every job without the AI, before any other step, and turns off sudo and containers so that no step lifts the block; the Action test runs the action behind the same block test_jobs_without_the_ai_reach_only_github, test_the_action_runs_in_ci_behind_the_block_of_the_templates in tests/test_templates.py
A public log or timeline reveals a possible vulnerability The Findings workflow names an open alert by number and link only, and can only dismiss alerts. The comment on a possible security report only points to private reporting, the timeline shows a fixed reason for its labels, it gets no priority, and the prompt asks for one neutral sentence without details in its summary, analysis and reason test_other_findings_stay_open_and_name_only_number_and_link, test_the_findings_workflow_may_only_dismiss_alerts_and_runs_no_ai, test_a_possible_vulnerability_is_not_discussed_in_public, test_a_security_report_gets_no_public_priority
GitHub renames or ends its egress-firewall runner, a technical preview, so every analysis waits in the queue for a day, and a repository moves the AI to a runner without any block The variable ISSUE_ASSISTANT_AI_RUNNER names a runner in its place without a release: the job event accepts only GitHub's runners of Ubuntu with a fixed version and names the hosts of .github/egress-firewall.yaml for Harden-Runner, which blocks every other host as the first step of analyze; the next step fails the job when requests to example.com still get through after a minute. The Canary fails when its analyze waits for longer than 30 minutes test_the_ai_runs_on_another_runner_only_behind_harden_runner, test_the_ai_runs_only_where_the_block_holds in tests/test_templates.py; test_harden_runner_gets_the_hosts_of_the_firewall_policy, test_a_runner_where_nothing_would_block_the_hosts_is_refused in tests/test_setup.py; tests/test_canary_queue.py
A repository's copy of a workflow drifts into something weaker check compares every workflow with its template, and the tests pin the security rules of the templates tests/test_templates.py