Skip to content
Merged
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
8 changes: 5 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ How to pick up work and get it merged. The shared plan lives in

1. Browse [open issues](https://github.com/Faisal-Fayaz/sidekick/issues) —
they're grouped by [milestones](https://github.com/Faisal-Fayaz/sidekick/milestones)
(`v0.3.0` = now; later work parks as bullets in `ROADMAP.md`).
(later work parks as bullets in `ROADMAP.md`).
2. Comment on the issue so we don't duplicate effort, then work from a
branch (`feature/<what>` or `fix/<what>`).
3. Starting work? Assign the issue to yourself *before* coding — the
Expand Down Expand Up @@ -126,7 +126,9 @@ GitHub auto-generates release notes from merge subjects, so write them as

- [ ] Version bumped on a `release/*` branch only (CI enforces this).
- [ ] `ROADMAP.md` `Done` records the release, `Next` points past it.
- [ ] Test-count badge in `README.md` matches `pytest --collect-only`.
- [ ] Full CI green on the release PR (lint, mypy, tests × 6, badge, build).
- [ ] Full CI green on the release PR: `lint`, `test-count`, `build`, `coverage`,
`website`, tests × 6. (`coverage` is required and gated at the `fail_under`
floor in `pyproject.toml`; the old test-count *badge* gate was removed
deliberately in #319 — the count is reported, not asserted.)
- [ ] After merge: release workflow success, PyPI shows the version,
GitHub Release exists with artifacts.
50 changes: 37 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,27 +150,27 @@ The `opencode` preset points at OpenCode Zen, opencode's gateway with a set of f

| Command | What |
|---|---|
| `sk` / `sk tui [--continue]` | Fullscreen chat, fresh session each launch |
| `sk chat [--continue]` | Fallback plain-text REPL (dumb terminals, screen readers, TUI issues) |
| `sk talk [-d SECS] [--stt-model base] [--device hw:2,0]` | Push-to-talk voice chat (CPU transcription, Enter to record/stop) |
| `sk` / `sk tui [--continue] [--model M] [--allow LIST] [--deny LIST]` | Fullscreen chat, fresh session each launch. `sk --version` prints the version and exits; `--cwd DIR` and `--profile NAME` work on every command |
| `sk chat [--continue] [--session S] [-y|--yes] [--model M] [--allow LIST] [--deny LIST] [--no-stream]` | Fallback plain-text REPL (dumb terminals, screen readers, TUI issues) |
| `sk talk [-d SECS] [--stt-model base] [--device hw:2,0] [--session S] [-y] [--install]` | Push-to-talk voice chat (CPU transcription, Enter to record/stop; `--install` skips the install prompt) |
| `sk mic-test [-d SECS] [--device hw:2,0]` | Mic level check: peak dB + silent/quiet/good verdict |
| `/sessions`, `/resume <n>`, `/sessions delete <n>`, `/fork [n]` | List, switch, delete, branch past sessions |
| `/yolo`, `/confirm`, `/readonly` | Approval modes: auto-approve writes / ask every time / block all file writes (research mode) |
| `/compact [hint]`, `/diff`, `/review` | Summarize history on demand / inspect working-tree diff / review it from inside a session |
| `/plan`, `/build` | Plan mode: propose without writing (blocked writes) / back to build mode |
| `/rewind [n]` | Undo an agent file edit — snapshots write/edit/delete targets (`shell` mutations are not tracked, use git for those) |
| `sk run "task" [--yes] [--plan] [--read-only] [--deny LIST] [--model auto\|fast\|smart\|name] [--json] [--bg] [--allow LIST]` | Single-shot agent run (auto-router picks the model; `--json` emits one machine-readable document + exit codes, use with `--yes` unattended; `--bg` detaches, returns a job id, notifies on completion; `--allow shell:pytest,write_file` skips prompts for listed tools; `--plan` proposes without writing) |
| `sk run "task" [--yes] [--plan] [--read-only] [--deny LIST] [--model auto\|fast\|smart\|name] [--json] [--bg] [--allow LIST] [--session S] [--no-stream]` | Single-shot agent run (auto-router picks the model; `--json` emits one machine-readable document + exit codes, use with `--yes` unattended; `--bg` detaches, returns a job id, notifies on completion; `--allow shell:pytest,write_file` skips prompts for listed tools; `--plan` proposes without writing) |
| `sk jobs [-n N]` | List background jobs from `sk run --bg` |
| `sk brief [-p PATH] [--smart]` | Morning digest: system + git + todos + memories, instant without LLM |
| `sk digest [--force]` | Teammate pilot: brief + overnight failures, desktop nudge or log |
| `sk remember/recall/memories/forget` | Long-term memory (FTS5 search, auto-injected) |
| `sk search QUERY [--session S]` | Full-text search across past transcripts |
| `sk todo add/list/done/clear` | Todos |
| `sk history` / `sk oops` | Shell log / explain last failure |
| `sk imagine "prompt" [--out f.png]` | Generate an image via the provider images endpoint |
| `sk export [SESSION] [--out f.md]` | Session transcript as Markdown (turns + tool calls) |
| `sk imagine "prompt" [--out f.png] [--size WxH] [--model M]` | Generate an image via the provider images endpoint |
| `sk export [SESSION] [--out f.md] [--force]` | Session transcript as Markdown (turns + tool calls). **Refuses to overwrite an existing `--out` unless `--force`** |
| `sk egress [list\|allow HOST\|deny HOST\|test URL]` | Egress policy: what the model may fetch, and what it was blocked from ([docs](docs/egress.md)) |
| `sk audit [--session S] [--format md\|json]` | Compliance log: tool runs, approve/deny, local-vs-egress |
| `sk audit [--session S] [--format md\|json] [-n N]` | Compliance log: tool runs, approve/deny, local-vs-egress (`-n` clamped 1–1000) |
| `sk stats [--session S] [--format md\|json]` | Usage + cost estimates from audit rows (turns, tools, tokens); pair with spend caps for BYO-key budgets |
| `sk hook-install [--write]` | Bash/zsh logging hook |
| `sk hooks [--check]` | List event hooks + live dry-run (see `docs/hooks.md`) |
Expand Down Expand Up @@ -219,11 +219,18 @@ sk egress test https://example.com/x # check a URL without fetching it
```

This covers every destination the *model* can cause a fetch of: `read_url`,
`web_search` (including each search hit), and provider-supplied image URLs. With
an empty allowlist, all three are refused. Your model API endpoint and the MCP
servers you configured are unaffected — you chose those, they are not something
a prompt can talk the agent into adding. Every allow and deny is written to the
ledger with the host and reason, visible via `sk egress` and `sk audit`.
`web_search`, and provider-supplied image URLs. With an empty allowlist, all
three are refused. The allowlist is checked on the initial URL **and on every
redirect hop**, so an allowlisted host cannot redirect the agent somewhere
unlisted. `web_search` itself only requests `html.duckduckgo.com` and returns
result URLs as text — it never fetches a hit, so following one is `read_url`,
which is governed on its own.

Your model API endpoint and the MCP servers you configured are unaffected — you
chose those, they are not something a prompt can talk the agent into adding.
Fetch decisions are written to the ledger with the host and reason, visible via
`sk egress` and `sk audit`. (Adding or removing an allowlist entry is a config
change, not a fetch, so it is not itself a ledger row.)

**The ceiling, stated plainly:** this governs fetches made *through the agent's
tools*. A `shell` command running `curl` is not covered — no string denylist can
Expand All @@ -244,7 +251,24 @@ The eval harness (`tests/test_eval.py`) locks in every past quality bug as an of

## Config

`~/.sidekick/config.toml` (`provider`, `model`, `base_url` override, `api_key`, …). Env overrides: `SIDEKICK_PROVIDER`, `SIDEKICK_MODEL`, `SIDEKICK_BASE_URL`, `SIDEKICK_API_KEY`, `SIDEKICK_SPEND_CAP` (per-session USD cap, `0` = unlimited). Data stays home: `history.db`, `skills/`, `nudges.log`, `input_history`, `tui-errors.log`.
`~/.sidekick/config.toml` (`provider`, `model`, `base_url` override, `api_key`, …). Data stays home: `history.db`, `skills/`, `nudges.log`, `input_history`, `tui-errors.log`.

| Env var | Effect |
|---|---|
| `SIDEKICK_PROVIDER` / `SIDEKICK_MODEL` | override the configured provider/model |
| `SIDEKICK_BASE_URL` / `SIDEKICK_API_KEY` | override the endpoint / key for this process |
| `SIDEKICK_SPEND_CAP` | per-session USD cap, `0` = unlimited |
| `SIDEKICK_REASONING_EFFORT` | reasoning effort for backends that support it |
| `SIDEKICK_PROFILE` | load a named config profile (see `sk config --profiles`) |
| `OPENCODE_API_KEY` | key for the `opencode` provider |
| `SIDEKICK_TRUST_REPO=1` | **opt in to repo-supplied slash commands** |

> **`SIDEKICK_TRUST_REPO` — why your repo's commands do nothing.** A cloned repo
> ships its own `.sidekick/commands/*.md`, and a `!`cmd`` expansion inside one
> reaches `sh -c` with no approval prompt. Those templates are therefore ignored
> unless you set `SIDEKICK_TRUST_REPO=1` in your own shell. It is read from the
> environment and never from repo config, so a repository cannot self-authorize
> (#287). Commands in `~/.sidekick/commands/` are always loaded.

**History budget:** the prompt budget bounds the **whole assembled prompt**, not just history — system prompt, auto-context, tool schemas, and a reservation for the reply are all measured and subtracted, then history gets what is left (#308). `history_budget_tokens` (default 3000) caps that share; over-budget sessions compact to a rolling summary via the current model (DB history stays complete). Lower it for small-context models.

Expand Down
4 changes: 3 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ How we decide what gets in:
- **Zero-dep bias.** Prefer stdlib solutions (FTS5 over vectors, `argparse`-grade simplicity) so it runs on a 4GB box.
- **Safety gates before features.** Writes need approval, destructive patterns hard-refused, SSRF guards on fetchers. New capabilities ship with regression tests (`tests/test_security.py`).

## Next — v0.28.0 (unplanned — propose by PR)
## Next — v0.30.0 (unplanned — propose by PR)

## Later

Expand Down Expand Up @@ -74,6 +74,8 @@ Explicit non-goals: a cloud-hosted version, frontier feature parity, native mobi

Shipped, most recent first. Details in [Releases](https://github.com/Faisal-Fayaz/sidekick/releases).

- `v0.29.0` — unparsed tool-call markup is no longer posted as an answer (#367/#368): the prose is kept, the markup is dropped, and the turn is bounded at 2 corrective retries before a plain error. Alongside it on main but **not yet published**: the `~/.sidekick` 0700/0600 fix (#301) and API-key masking (#269).
- `v0.28.0` — **burned.** Existed in `src/sk/__init__.py` for three minutes and was superseded before tagging, so the number can never be published. Listed in `BURNED_VERSIONS` in `tests/test_release_integrity.py`; a new gap now fails that test.
- `v0.27.0` — **security release.** Hostile-repo RCE chain closed: project files can no longer redirect traffic (`provider`/`model`) or self-approve shell (`[project].approved_commands`), and repo-supplied commands need `SIDEKICK_TRUST_REPO=1`. Irreversible shell commands are refused *before* approval (#298), so `--yes`/`/yolo`/`--allow-writes` no longer bypass the blocklist — **behaviour change**. Protected write paths (shell rc, credential dirs, `.git/hooks`, the agent's own config) are denied (#305). Credentials redacted from the audit trail, shell log and notifications (#300). MCP gets an environment allowlist, URL validation, a fail-closed trust tier and cross-origin header protection (#302, #303). One SSRF chokepoint for every outbound fetch, closing the CGNAT/cloud-metadata gap (#304). Compaction no longer silently no-ops and no longer discards its own retained tail (#306, #307). TUI prompt no longer sent twice; no more duplicate `tool_call_id`s (#309, #310). Fixed an order-dependent test failure (#317). Tests 828 → 981; layered on #264/#265/#266 rather than duplicating them.
- `v0.26.0` — security + robustness fix batch (#220–#229), QA regression suite (#231), retry breaker (#230), tester skill (#236), session-deny + edit-first (#237, #238), research delegate (#239), partial-output errors (#240), empty-toollist fallback, folding removal.
- `v0.25.0` — image input (#178), JS extraction (#182), profiles (#189), TUI forgiveness batch (#161, #163–#168), dispatch write-block (#208), reasoning/control hygiene (#206).
Expand Down
9 changes: 8 additions & 1 deletion docs/egress.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,16 @@ Rules:
| Path | Tool |
|---|---|
| A page the model asks to read | `read_url` |
| The search backend, and every hit it follows | `web_search` |
| The search backend, and every redirect hop it follows | `web_search` |
| A URL a provider returns for an image | image download |

> **`web_search` fetches exactly one host.** It requests only
> `html.duckduckgo.com` and returns result titles and URLs *as text*. It never
> fetches a search hit, so a result pointing anywhere is not a governed
> destination — following one is `read_url`, which is governed on its own and on
> every redirect hop. An earlier version of this table said "every hit it
> follows", which described the threat model rather than the code.

**Not governed** — traffic you configured yourself:

| Path | Why |
Expand Down
35 changes: 35 additions & 0 deletions docs/mcp-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,48 @@ env = { SQLITE_MCP_RO = "1" }
| `env` | no | `{}` | extra env vars (stdio only, merged over the process env) |
| `headers` | no | `{}` | extra HTTP headers (url only) |
| `timeout` | no | `30` | seconds per request, clamped to 1–300 |
| `trust` | no | `untrusted` | `untrusted` or `full` — see below |
| `inherit_env` | no | `[]` | glob patterns of extra env vars a stdio server may also read from the parent process (#302) |

\*Exactly one of `command`/`url`. Setting both refuses the entry (fail fast —
fix the config instead of guessing).

Server names must match `[A-Za-z0-9_-]+` (no `__`); invalid entries are
skipped. Check wiring with `sk mcp-servers` (live tool count or the error).

## `trust`: why your tools are refused

This is the gate most people hit first, and it was previously undocumented —
so the error named a config key that appeared nowhere in this file.

There are **three** gates on an MCP tool call, and the first one is not
approval:

1. **`trust`** (config, default `untrusted`, fail closed)
2. **`readOnlyHint`** (tool annotation, fail closed)
3. **approval** (`--allow`, `--read-only`, `sk chat`)

On an `untrusted` server, a tool is available only if it declares a **literal**
`readOnlyHint: true`. Anything else — a missing annotation, `readOnlyHint: "true"`,
a string, an empty annotations dict — is refused, and the error tells you to set
`trust = "full"`.

```toml
[mcp_servers.db]
command = "/usr/local/bin/sqlite-mcp"
args = ["--db", "/home/you/app.db"]
trust = "full" # tools no longer need readOnlyHint: true
```

`untrusted` is the default and the right one: a spawned server inherits the
ability to do whatever the model asks of it, and only a server you wrote should
opt out of per-tool annotation. `inherit_env` has the same shape of risk —
patterns like `AWS_*` hand a stdio server credentials from your shell — so it is
also opt-in per server.

Only `full` and `untrusted` are accepted; **anything else silently normalises to
`untrusted`** rather than erroring, so a typo fails closed.

## How tools appear

Each server tool becomes `mcp__<server>__<tool>` with the server's
Expand Down
Loading
Loading