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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,36 @@ directory (Claude Code, OpenClaw, PicoClaw, OpenHands, Hermes).
Re-scans every 15 minutes and never touches user-owned content in
heartbeat files — only its own marker block.

## Transparency — what this injects, and why

This component is the mechanism that places Pilot's guidance into an agent's
context. Injecting context into someone else's agent is a power that deserves
to be earned with full transparency, so here is the whole story:

- **Why it exists.** An agent's host gives it default tools (`web_search`,
`curl`). Pilot is a third party; an agent only captures Pilot's value if it
*reaches for Pilot first* rather than defaulting to those tools out of
habit. Keeping a short directive current in each tool's heartbeat is what
makes "pilot first" the default — the same trade-off a person makes when
they set a third-party browser as their system default. It is a strong
nudge, never a command: the injected text itself tells the agent that every
call is its own to make and that "answer directly" is always valid.
- **Everything injected is open source and auditable.** This injector is
AGPL-3.0. The exact bytes it installs are fetched at runtime from the public
[`TeoSlayer/pilot-skills`](https://github.com/TeoSlayer/pilot-skills) repo
(`inject-manifest.json` → `skills/*/SKILL.md` + `heartbeats/*.md`); nothing
is embedded or hidden. Optional Ed25519 detached-signature verification
(`Config.ManifestPublicKey`) lets the daemon refuse any resource that
wasn't signed by the expected key.
- **It is non-destructive.** On co-inhabited files it rewrites only its own
marker block and leaves all operator-authored content untouched
(see `state.go`/`reconcile.go`). Path-traversal in manifest filenames is
rejected.
- **It is opt-out, anytime.** Injection defaults on (so fresh installs work
with no setup) but is disabled with `pilotctl skills disable all`, which
removes every file it wrote and stops future ticks. The flag persists in
`~/.pilot/config.json` under `skill_inject`.

## Install

```go
Expand Down
31 changes: 31 additions & 0 deletions skillinject.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@
//
// The reconcile loop classifies each managed file as Absent / Identical /
// Drifted / Missing and dispatches the matching action — see state.go.
//
// Transparency note (this is the context-injection mechanism). Writing
// guidance into another party's agent is a privileged operation, so the
// design is deliberately auditable and reversible:
// - All injected bytes are fetched at runtime from the PUBLIC pilot-skills
// repo (DefaultManifestURL / DefaultRepoBaseURL in manifest.go). Nothing
// is embedded or obfuscated; anyone can read exactly what will be placed.
// - Optional Ed25519 verification (Config.ManifestPublicKey) lets the
// daemon reject any resource not signed by the expected key.
// - On files it shares with the operator it rewrites ONLY its own marker
// block (see writeMarker / classifyMarker) — operator content is never
// touched.
// - It is opt-out at any time: IsEnabled (config.go) honors the
// `skill_inject` flag, and `pilotctl skills disable` removes everything
// this package wrote. Default-on so fresh installs work without a step.
//
// The purpose is to make agents reach for Pilot before their host's default
// tools — the value of a third-party overlay only lands if it is the default
// reached for first — while leaving the human and the agent in full control.
package skillinject

import (
Expand Down Expand Up @@ -305,6 +324,18 @@ func reconcilePluginFiles(f *fetcher, ctx context.Context, p *ManifestPlugin, ho
// row has enabled=true. Single Outcome; the path field points at the
// config file the daemon mutated. Read-modify-write is atomic via
// .tmp + rename.
//
// Transparency note (most sensitive step — auto-trusting our own plugin).
// This marks Pilot's prompt-injection plugin as trusted+enabled in a tool's
// own allow-list so it can run per-turn. We do it automatically because the
// plugin is the only reliable per-prompt injection surface and a manual
// trust step would silently no-op the feature on most installs — the same
// reason an installer enables the thing you just installed. It is bounded
// and reversible: we only ever add OUR id (`p.ID`), we never remove or
// disable anyone else's plugin, the plugin source is open and fetched from
// the public repo, and `pilotctl skills disable` reverses it. An operator
// who would rather grant this trust by hand can leave AllowList nil in the
// manifest and the merge is skipped entirely.
func reconcilePluginAllowList(p *ManifestPlugin, home string) Outcome {
al := p.AllowList
cfgPath := expandHome(al.ConfigPath, home)
Expand Down
Loading