Skip to content

Commit 04d0c29

Browse files
author
Ovtcharov
committed
feat(skills): let the agent install and sign in to a skill's CLI
Asking GAIA to triage GitHub issues on a machine without the GitHub CLI ended the conversation: the agent said "install gh and run gh auth login", and the user went and did it by hand. It can now do both itself, after asking. Three tools on the flagship agent, driven by data on the binary policy rather than by new branches: - check_cli_setup reports one of five states, each with a different remedy. Splitting them matters most for env_token — gh authenticated from $GH_TOKEN works fine, and refuses to store a credential while that variable is set, so reporting it as "logged out" sends the user to run a command gh will reject. - install_cli runs the platform package manager. Linux is deliberately absent: gh is not in stock Debian or Fedora repositories, and adding GitHub's apt/dnf repo is a root-level trust change that belongs to the user. - sign_in_cli drives the browser device flow, hands over the one-time code and URL, and waits on the CLI's own exit rather than on a second approval prompt. Watching the child observes GitHub completing the grant instead of the user reporting that it did, and avoids stranding a polling process behind a confirmation channel that either expires in a minute or blocks forever. Both mutating tools show the exact command and cannot be pre-authorized: no "always" grant is offered, and a loaded skill grant exempts only read-only run_shell_command. The command argument is display-only and verified against the policy table before anything runs, so the prompt cannot show one command and execute another. Also narrows the gh auth grant. `gh auth token` was already refused as a credential surface, but `gh auth status --show-token` prints the same token and ran at the tier that asks nobody; it is now denied on the flag, ahead of action classification. Scopes requested are repo and read:org — what the triage skill's command table can reach, and nothing beyond it. The token and the one-time code are never logged, returned, or persisted by GAIA.
1 parent 1ae7dad commit 04d0c29

14 files changed

Lines changed: 2666 additions & 42 deletions

File tree

docs/guides/gaia.mdx

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ It answers **200** when ready and **503** when not, with a `hint` naming the fix
8585

8686
## Tool surface
8787

88-
The default construction registers **67 tools** — the 55 of the Chat agent's `full` profile, plus 7 skill-library tools, 4 code-search tools, and the `load_tools` escape hatch. Grouped by what they're for:
88+
The default construction registers **70 tools** — the 58 of the Chat agent's `full` profile, plus 7 skill-library tools, 4 code-search tools, and the `load_tools` escape hatch. Grouped by what they're for:
8989

9090
| Area | Tools |
9191
|---|---|
@@ -96,22 +96,55 @@ The default construction registers **67 tools** — the 55 of the Chat agent's `
9696
| Memory | `remember`, `recall`, `forget`, `update_memory`, `search_past_conversations` |
9797
| Images | `analyze_image`, `answer_question_about_image` |
9898
| Desktop & system | `take_screenshot`, `list_windows`, `get_system_info`, `notify_desktop`, `read_clipboard`, `write_clipboard`, `text_to_speech`, `run_shell_command` |
99+
| CLI setup | `check_cli_setup`, `install_cli`, `sign_in_cli` |
99100
| Skills | `list_skills`, `search_skill_hub`, `install_skill`, `remove_skill`, `load_skill`, `unload_skill`, `skill_status` |
100101
| Code search | `index_codebase`, `search_code_index`, `get_index_status`, `clear_code_index` |
101102
| Run control | `request_user_input`, `set_loop_state`, `load_tools` |
102103

103104
That count is what the agent *can* do, not what it sees at once: per-turn tool selection sends the model a subset of the registry on any one call, and `load_tools` is the escape hatch it calls mid-turn to pull in a bundle the selector missed. No capability is lost either way.
104105

105-
`run_shell_command` is restricted to a read-only command allowlist (`ls`, `cat`, `grep`, `find`, `stat`, `df`, `uname`, and similar) and rate-limited. Anything that writes, installs, or executes an arbitrary binary is refused.
106+
`run_shell_command` is restricted to a read-only command allowlist (`ls`, `cat`, `grep`, `find`, `stat`, `df`, `uname`, and similar) and rate-limited. Anything that writes, installs, or executes an arbitrary binary is refused — a package manager is not on that allowlist and cannot be talked onto it. Installing the specific CLIs a skill needs goes through [its own tools](#setting-up-a-cli-a-skill-needs) instead.
106107

107108
**Image *generation* is deliberately off.** Drawing a picture would pull a second model into Lemonade and evict the resident chat model mid-conversation — not a trade a document agent should make silently. A custom agent can still compose the [`sd` tool mixin](/sdk/mixins/tool-mixins#95-stable-diffusion-mixin) when that trade-off is acceptable.
108109

110+
### Setting up a CLI a skill needs
111+
112+
Some skills run a command-line tool rather than calling an API — [`github-triage`](/guides/starter-skills#github-triage) needs the GitHub CLI, which owns its own login. When that tool is missing or logged out, the agent sets it up for you instead of handing you instructions and stopping.
113+
114+
Ask for it in plain words — *"set up the GitHub CLI"* — and three tools do the work:
115+
116+
| Tool | What it does |
117+
|---|---|
118+
| `check_cli_setup` | Reads the CLI's own status. Changes nothing and never prompts. |
119+
| `install_cli` | Installs it with your package manager, after you approve the exact command. |
120+
| `sign_in_cli` | Starts the browser sign-in, after you approve the exact command. |
121+
122+
The check reports one of five states, and only one of them means "sign in":
123+
124+
| State | What it means | What fixes it |
125+
|---|---|---|
126+
| `ready` | Installed, signed in, permissions present. | Nothing. |
127+
| `missing` | Not on `PATH`. | Install it. |
128+
| `unauthenticated` | Installed, no working credential. | Sign in. |
129+
| `insufficient_scopes` | Signed in, but the token lacks a permission the skill needs. | Sign in again — the flow re-requests it. |
130+
| `env_token` | Signed in from a `GH_TOKEN` / `GITHUB_TOKEN` environment variable. **This works.** | Nothing, unless you *want* a browser login — then clear the variable first, because `gh` refuses to store its own credential while it is set. |
131+
132+
**You approve every install and every sign-in, one at a time.** Both are in the confirmation set below, and neither offers an "always" — there is no way to pre-authorize them, and loading a skill does not exempt them.
133+
134+
**Signing in is a handoff, not automation.** GAIA starts `gh auth login --web`, shows you the one-time code and the device URL, and waits. You enter the code in your browser; the agent cannot type it for you. Afterwards it re-checks and confirms the permissions actually arrived, rather than trusting the exit code. GAIA requests exactly the scopes the skill's commands can reach — `repo` and `read:org` for GitHub — and never sees the token.
135+
136+
If any step can't finish — no code appears, the code expires, the browser sign-in fails — it says so and gives you the command to run by hand. It never reports a setup that didn't happen.
137+
138+
<Note>
139+
**Linux has no automated install, on purpose.** The GitHub CLI is not in the stock Debian or Fedora repositories, and the real install adds GitHub's apt/dnf repository — a root-level trust decision that is yours, not an agent's. On Linux you get [cli.github.com](https://cli.github.com) instead. Windows uses `winget`, macOS uses `brew`.
140+
</Note>
141+
109142
### Tools that need your approval
110143

111-
Six tools mutate your machine and are gated behind an explicit confirmation: **`write_file`**, **`edit_file`**, **`run_shell_command`**, **`execute_python_file`**, and — because installing a skill writes third-party code under `~/.gaia/skills` and removing one deletes it — **`install_skill`** and **`remove_skill`**. Everything else — reading, indexing, querying, web fetching, memory — runs without asking.
144+
Eight tools mutate your machine and are gated behind an explicit confirmation: **`write_file`**, **`edit_file`**, **`run_shell_command`**, **`execute_python_file`**, **`install_cli`** and **`sign_in_cli`** (they install software and sign you in to an account), and — because installing a skill writes third-party code under `~/.gaia/skills` and removing one deletes it — **`install_skill`** and **`remove_skill`**. Everything else — reading, indexing, querying, web fetching, memory, and `check_cli_setup` — runs without asking.
112145

113146
<Warning>
114-
**Over the sidecar's `/query` stream, all six are refused rather than prompted.** That
147+
**Over the sidecar's `/query` stream, all eight are refused rather than prompted.** That
115148
surface has no way to collect an approval, so instead of pretending to, the run stops
116149
with a message saying which action it declined. If your workflow depends on the agent
117150
writing files, drive it from a surface that can prompt you, or perform the write yourself.

docs/guides/starter-skills.mdx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -179,14 +179,14 @@ duplicates, ranks by severity × reach, and posts the reply once you approve it.
179179
grants the GitHub CLI to this skill's session on this agent, restricted to the
180180
subcommands in GAIA's policy table (see
181181
[Bring your own CLI](/plans/skill-format#bring-your-own-cli)).
182-
- **Configure**
183-
184-
```bash
185-
gh auth login # gh owns its own authGAIA never sees the token
186-
gh auth status
187-
```
188-
189-
If `gh` is not on PATH the skill refuses to load and names the install command.
182+
- **Configure** nothing, if you let GAIA do it. Ask it to set up the GitHub CLI
183+
and it checks, installs, and signs in for you, showing you each command first —
184+
see [Setting up a CLI a skill needs](/guides/gaia#setting-up-a-cli-a-skill-needs).
185+
`gh` still owns its own auth; GAIA never sees the token. To do it by hand
186+
instead, `gh auth login` then `gh auth status` works exactly as before.
187+
188+
If `gh` is not on PATH the skill refuses to load rather than answering from no
189+
data — and the refusal now offers the setup rather than ending there.
190190
Reads run without a per-call approval prompt — loading the skill is the consent.
191191
The useful writes (`gh issue create|comment|edit`, `gh pr comment`,
192192
`gh label create|edit`) show you the exact command and wait for a yes, a no, or
@@ -238,9 +238,11 @@ gaia skill info gpu-restock-watch
238238

239239
- **Permissions are declarations, and v1 only honors some of them.**
240240
`network:*` and `mcp:connect:<connector-id>` bridge to the connector model and
241-
work today. `filesystem`, `shell`, `database`, `desktop`, and `env` need a
242-
sandbox that has not shipped, so a skill declaring one is **refused at load**
243-
rather than loaded unenforced ([#1019](https://github.com/amd/gaia/issues/1019)).
241+
work today, and so does `shell:execute:<binary>` — that is the grant
242+
`github-triage` uses. Everything else local — `filesystem`, `database`,
243+
`desktop`, `env`, and a bare unscoped `shell:execute` — needs a sandbox that has
244+
not shipped, so a skill declaring one is **refused at load** rather than loaded
245+
unenforced ([#1019](https://github.com/amd/gaia/issues/1019)).
244246
Do not add a permission to look thorough — add it only if the skill needs it.
245247
- **`tools` and `tools_required` are different fields.** `tools` are `@tool`
246248
functions your skill *provides* (and must exist in its `tools.py`).

docs/plans/skill-format.mdx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -385,10 +385,14 @@ Four rules make the grant safe enough to bridge before the sandbox exists:
385385
write rejects `--body-file` (uploads a local file's contents), `--editor`
386386
(hangs a stdin-less agent), and `--web`. The flag is checked before the
387387
action, so a write carrying one never reaches a prompt.
388-
4. **The binary must be installed.** `load_skill` checks `PATH` and refuses with
389-
the install command when it is missing. A skill that loads without the tool it
390-
documents produces confident answers from no data — the failure this bridge
391-
exists to prevent.
388+
4. **The binary must be installed** — though a missing one is no longer a dead
389+
end. `load_skill` still checks `PATH` and still refuses, because a skill that
390+
loads without the tool it documents produces confident answers from no data.
391+
What changed is the remedy: a `BinarySetup` entry on the policy lets the agent
392+
install the CLI and drive its browser sign-in itself, each behind its own
393+
approval prompt, so the refusal names an action instead of ending the
394+
conversation. See
395+
[Setting up a CLI a skill needs](/guides/gaia#setting-up-a-cli-a-skill-needs).
392396

393397
The existing shell guardrails still apply on top: command chaining and
394398
redirection (`&&`, `;`, `>`, backticks, `$()`) are rejected before the policy

docs/sdk/mixins/tool-mixins.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Each row is the exact `name → module.Class` mapping the registry resolves:
3838
| `file_search` | `from gaia.agents.tools.file_tools import FileSearchToolsMixin` | Fuzzy/glob file search |
3939
| `file_io` | `from gaia.agents.tools.file_io_tools import FileIOToolsMixin` | Read/write/edit files |
4040
| `shell` | `from gaia.agents.tools.shell_tools import ShellToolsMixin` | Sandboxed shell commands |
41+
| `cli_setup` | `from gaia.agents.tools.cli_setup_tools import CliSetupToolsMixin` | Install and sign in to a skill's CLI ([how it works](/guides/gaia#setting-up-a-cli-a-skill-needs)) |
4142
| `screenshot` | `from gaia.agents.tools.screenshot_tools import ScreenshotToolsMixin` | Screen capture |
4243
| `filesystem` | `from gaia.agents.tools.filesystem_tools import FileSystemToolsMixin` | File system navigation |
4344
| `scratchpad` | `from gaia.agents.tools.scratchpad_tools import ScratchpadToolsMixin` | SQL scratchpad tables for data analysis |

hub/agents/chat/python/gaia_agent_chat/agent.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from gaia.agents.tools import ScratchpadToolsMixin # Structured data analysis
4343
from gaia.agents.tools import ( # Web browsing and search; Shared tools
4444
BrowserToolsMixin,
45+
CliSetupToolsMixin,
4546
FileIOToolsMixin,
4647
FileSearchToolsMixin,
4748
FileToolsMixin,
@@ -179,6 +180,7 @@ class ChatAgent(
179180
RAGToolsMixin,
180181
FileToolsMixin,
181182
ShellToolsMixin,
183+
CliSetupToolsMixin,
182184
FileSystemToolsMixin,
183185
ScratchpadToolsMixin,
184186
BrowserToolsMixin,
@@ -449,9 +451,10 @@ def __init__(self, config: Optional[ChatAgentConfig] = None):
449451
# never register RAG tools and can't use this restore — never trigger
450452
# the lazy RAG build via ``self.rag`` below; ``and`` short-circuits
451453
# before evaluating it.
452-
_uses_rag = "doc_rag" in get_profile_spec(
453-
getattr(config, "prompt_profile", "full")
454-
).tool_groups
454+
_uses_rag = (
455+
"doc_rag"
456+
in get_profile_spec(getattr(config, "prompt_profile", "full")).tool_groups
457+
)
455458
if _uses_rag and config.ui_session_id and self.rag:
456459
loaded = self.session_manager.load_session(config.ui_session_id)
457460
if loaded:
@@ -1293,11 +1296,16 @@ def _register_tools(self) -> None:
12931296
if spec.early_return:
12941297
# Minimal: only shell for system queries
12951298
self.register_shell_tools()
1299+
# Registered on every profile, this one included: "can you install
1300+
# the GitHub CLI?" is a conversational question, and the answer has
1301+
# to be yes before any skill needing that CLI can even load.
1302+
self.register_cli_setup_tools()
12961303
self._register_external_tools_conditional()
12971304
return
12981305

12991306
# All other profiles get at least shell tools
13001307
self.register_shell_tools()
1308+
self.register_cli_setup_tools()
13011309
self.register_memory_tools() # Persistent memory tools
13021310

13031311
for _group_name in spec.tool_groups:

hub/skills/github-triage/SKILL.md

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name: github-triage
33
description: Triage GitHub work with the gh CLI — your unread notification inbox, or one repository's issue backlog. Groups what arrived, judges what is urgent, and posts the reply once you approve it. Use when asked to triage issues or notifications, check the GitHub inbox, see what needs attention or what you are blocking, review a backlog, comment on or label an issue, or work out what to fix first.
44
license: MIT
5-
version: 2.1.0
5+
version: 2.2.0
66
metadata:
77
gaia:
88
security_tier: community
@@ -25,21 +25,33 @@ Run every GitHub command through `run_shell_command`.
2525

2626
## Setup
2727

28-
Check before you conclude anything is missing. A command that failed is not
29-
proof `gh` is absent:
28+
Call `check_cli_setup("gh")` first. It is read-only, prompts nobody, and returns
29+
the one thing worth acting on — a `state`. Never infer setup from a `gh` command
30+
that failed, and never run `gh --version` to guess.
3031

31-
```bash
32-
gh --version # installed? ("gh version", no dashes, is refused)
33-
gh auth status # logged in? prints the account and its scopes
34-
```
35-
36-
- `gh --version` fails → not installed. Offer to install it: `winget install
37-
GitHub.cli` (Windows), `brew install gh` (macOS), else https://cli.github.com.
38-
That is not a `gh` command, so the user approves it per-call — ask, then run it.
39-
- `gh auth status` fails → installed but not logged in. `gh auth login` is
40-
interactive and you cannot drive it; hand that one step to the user.
41-
- Any **other** failure is a bug in the command you sent, not a missing `gh`.
42-
Quote the error and fix the command. Never diagnose it as "not installed".
32+
| `state` | Remedy |
33+
|---|---|
34+
| `ready` | None. Start the triage. |
35+
| `missing` | `install_cli`. Linux has no automated install — give the user https://cli.github.com. |
36+
| `unauthenticated` | `sign_in_cli`. |
37+
| `insufficient_scopes` | `sign_in_cli` — signing in again re-requests `repo` and `read:org`. |
38+
| `env_token` | **Not a sign-in.** `gh` already works on a `GH_TOKEN`/`GITHUB_TOKEN` from the environment, and refuses to store its own credential while that is set. Say so and move on. |
39+
40+
Pass `command` back exactly as `check_cli_setup` returned it — `install_command`
41+
or `sign_in_command`. That string is what the user sees in the approval prompt,
42+
and one that does not match is refused rather than run.
43+
44+
Both tools stop and ask, every time: the user sees the exact command and answers
45+
before anything runs. There is no "always", and loading this skill pre-approves
46+
neither. Signing in is a **handoff** — GAIA starts the flow and shows a one-time
47+
code and a URL, then waits while the user enters the code in their browser. You
48+
cannot type it for them.
49+
50+
Report a setup failure as a failure. Both tools name the manual command when they
51+
give up; hand that to the user rather than claiming a step succeeded.
52+
53+
Any **other** `gh` failure is a bug in the command you sent, not a missing `gh`.
54+
Quote the error and fix the command. Never diagnose it as "not installed".
4355

4456
## What the grant allows
4557

src/gaia/agents/base/agent.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,13 @@ def __init__(self, tool_name: str, timeout: float):
198198
TOOLS_REQUIRING_CONFIRMATION = {
199199
"run_shell_command",
200200
"run_cli_command",
201+
# Installs software, and signs a CLI in to the user's account. Both mutate
202+
# the machine on the user's behalf, and neither can ever be pre-authorized:
203+
# the only grant that skips this gate covers ``run_shell_command`` reads
204+
# (``ShellToolsMixin.skill_grant_covers_call``), and ``grant_scope`` offers
205+
# no "always" for either, so every call is asked about on its own.
206+
"install_cli",
207+
"sign_in_cli",
201208
# Runs a .py file in a subprocess — arbitrary code execution, and unlike
202209
# run_shell_command there is no read-only allowlist behind it.
203210
"execute_python_file",

src/gaia/agents/registry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"file_search": ("gaia.agents.tools.file_tools", "FileSearchToolsMixin"),
4444
"file_io": ("gaia.agents.tools.file_io_tools", "FileIOToolsMixin"),
4545
"shell": ("gaia.agents.tools.shell_tools", "ShellToolsMixin"),
46+
"cli_setup": ("gaia.agents.tools.cli_setup_tools", "CliSetupToolsMixin"),
4647
"screenshot": ("gaia.agents.tools.screenshot_tools", "ScreenshotToolsMixin"),
4748
"filesystem": ("gaia.agents.tools.filesystem_tools", "FileSystemToolsMixin"),
4849
"scratchpad": ("gaia.agents.tools.scratchpad_tools", "ScratchpadToolsMixin"),

src/gaia/agents/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99
from .browser_tools import BrowserToolsMixin
10+
from .cli_setup_tools import CliSetupToolsMixin
1011
from .code_index_tools import CodeIndexToolsMixin
1112
from .file_io_tools import FileIOToolsMixin
1213
from .file_monitor_tools import FileToolsMixin
@@ -19,6 +20,7 @@
1920

2021
__all__ = [
2122
"BrowserToolsMixin",
23+
"CliSetupToolsMixin",
2224
"CodeIndexToolsMixin",
2325
"FileIOToolsMixin",
2426
"FileSearchToolsMixin",

0 commit comments

Comments
 (0)