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
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Gated tools: a new optional manifest key, `gatedTools`, for skill
targets whose directory is too generic to detect by existence alone.
A gated tool is active only while its `requireMarker` file exists and
names the tool's target, and that file must be inside `~/.pilot`. The
marker holds `skills_dir=<dir>` and `skill_format=<muse|canonical>`
lines; the tool is on only when `skills_dir` is the row's `skillsDir`
(after resolving symlinks) and `skill_format` its `skillFormat`. An empty
marker, one for another folder, and one for the canonical frontmatter
turn nothing on, so an installer run for another agent's skills folder,
or with the Muse frontmatter turned off, never makes the daemon write or
rewrite `~/workspace/skills`. The marker must be a regular file of at
most 4 KiB. Without a matching marker nothing under the tool's `rootDir`
is read, written or removed, by a tick or by `Uninstall`.
Only the entrypoint skill copy is installed; there is no heartbeat or
plugin. `rootDir` must be inside the home directory, `skillsDir` inside
`rootDir`. Every file operation goes through an `os.Root` on `rootDir`,
and a symlink at the skill file or at any directory between `rootDir`
and it is refused with an error row. Writes use an `O_EXCL` temp file
with a random name. A gated row that resolves to a regular tool's skill
copy is refused, so the two never rewrite each other. The key is new on
purpose: every released version decodes the manifest with plain
`json.Unmarshal` into a struct without it, so released daemons ignore
these rows. As a `tools` row, the same entry would be installed by every
released daemon on any host where the directory exists, with no marker
check.
- `skillFormat: "muse"` (`SkillFormatMuse`) for Meta Muse, which loads
skills from `~/workspace/skills`: the entrypoint SKILL.md frontmatter is
rewritten to `name: "<entrypoint, - replaced by _>"` and a one-line
quoted `description` (folded values joined, capped at 1024 bytes), and
every other key is dropped. The body is unchanged. The output is byte
for byte what the pilot-skills Muse installer (`muse/install.sh`) writes,
so the two do not rewrite each other's copy.
`TestMuseSkillMD_MatchesInstaller` runs the installer's own shell
function against the Go port.

- `Config.ProxyCommand` and credential refresh for the default HTTP
client. Egress proxies that rotate the credentials in `HTTPS_PROXY`
(Meta Muse) answered every tick after the first rotation with 407,
because the daemon keeps its launch-time environment. When
`Config.HTTPClient` is nil, the client is now a
`netproxy.RefreshingTransport` whose refresh command is
`Config.ProxyCommand`, else `$PILOT_PROXY_CMD`, else `"proxy_cmd"` in
`~/.pilot/config.json`, the same settings pilot-daemon reads. It runs at
the start of each tick, once a minute while it runs, and on a 407, and
the refused request is retried once. `PILOT_PROXY` or `config.json`
`"proxy"` set to off/none/no/false/direct turns it off. Without a
command the client is the plain one, and a 407 reported as a
`*netproxy.ConnectError` (pilot-daemon's `http.DefaultTransport` does so
after refreshing its own credentials) is retried once; a rejection the
proxy garbles ("malformed HTTP status code", Meta Muse's form, which never
reaches the transport's CONNECT hook) is retried once after 2s, when the
transport's resolver has had a chance to re-read the credentials in the
background.

### Changed

- `canonicalPath` resolves a path that does not exist yet through its
nearest existing ancestor, not only its parent directory, so two paths
that will name the same file compare equal before either is written.

## [v0.2.4] - 2026-09-23

### Fixed

- Heartbeat blocks are inserted as literal text. Rewrites went through
Expand Down
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

Skill injector plugin for the Pilot Protocol daemon. Installs and keeps
current the `SKILL.md` files in each detected agent tool's well-known
directory (Claude Code, OpenClaw, PicoClaw, OpenHands, Hermes).
directory (Claude Code, OpenClaw, PicoClaw, OpenHands, Hermes, and Meta
Muse on hosts marked as Muse targets).
Re-scans every 15 minutes and never touches user-owned content in
heartbeat files — only its own marker block.

Expand Down Expand Up @@ -57,6 +58,25 @@ to be earned with full transparency, so here is the whole story:
rewritten. The retired plugin's `index.mjs` is replaced with a no-op
instead, and the daemon log and report say so once. Every removal is
logged with its path.
- **Generic directories need an explicit opt-in.** Tools listed under the
manifest's `gatedTools` key (today Meta Muse, which loads skills from
`~/workspace/skills`) are installed only on hosts that carry the tool's
marker file under `~/.pilot`, and only when that marker names the tool's
skills directory and format. The Muse installer writes
`~/.pilot/targets/muse` when it installs the Muse-format skills into
`~/workspace/skills`:

```
skills_dir=/root/workspace/skills
skill_format=muse
```

An empty marker, or one for another folder or for the canonical
frontmatter (`skill_format=canonical`), turns nothing on. Without a
matching marker nothing in that directory is read, written or removed.
Writes stay inside the tool's `rootDir`, never follow a symlink, and only
the entrypoint `SKILL.md` is written (rewritten into the frontmatter shape
Muse loads). Releases that predate the key ignore it. See `gated.go`.
- **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
Expand Down Expand Up @@ -92,6 +112,27 @@ report, err = skillinject.Plan(ctx, skillinject.Config{ /* ... */ })
removed, err := skillinject.Uninstall(ctx, skillinject.Config{ /* ... */ })
```

### Egress proxies that rotate their credentials

Fetches follow the proxy environment (`HTTPS_PROXY`, `NO_PROXY`, ...). Some
sandboxes (Meta Muse) rotate the credentials in `HTTPS_PROXY` every few
minutes, and a long-running daemon keeps the ones it was started with. When
`Config.HTTPClient` is nil, the client re-reads them with the same refresh
command pilot-daemon uses: `Config.ProxyCommand`, else `$PILOT_PROXY_CMD`,
else `"proxy_cmd"` in `~/.pilot/config.json`. On such hosts the Pilot
installer (pilot-protocol/release#49) saves the sandbox command there,
`pilotctl daemon start` (pilotprotocol#470) and pilot-sandbox's `pilot-up.sh`
hand it to the daemon as `$PILOT_PROXY_CMD`, and pilot-mcp setup does the
same: `bash -c 'case $https_proxy in *@*) printf %s "$https_proxy";; *) printf %s "${HTTPS_PROXY:-$https_proxy}";; esac'`.
The command runs at the start of each tick, again once a minute while it
runs, and when the proxy rejects the credentials (a 407, or an answer
net/http cannot parse), after which the refused request is retried once.
Its output is never logged. `PILOT_PROXY=off` (or `config.json` `"proxy":
"off"`) turns this off. With no command in sight (a daemon given only the
`-proxy-cmd` flag) the plain client retries a 407 its transport reports
once, and a garbled rejection once after 2s, which succeeds when the
daemon's own resolver re-read the credentials meanwhile. See `proxy.go`.

## Layout

| File | What it does |
Expand All @@ -102,6 +143,9 @@ removed, err := skillinject.Uninstall(ctx, skillinject.Config{ /* ... */ })
| `reconcile.go` | Per-tick state machine: Absent → install, Drifted → rewrite, Identical → noop. |
| `state.go` | File-state classifier (sha256 + heartbeat-marker parsing). |
| `uninstall.go` | Strip-only on co-inhabited files; delete-safe in pilot-owned subdirs. |
| `gated.go` | Marker-gated targets (`gatedTools`, e.g. Meta Muse): active only while `requireMarker` names the target; contained writes. |
| `proxy.go` | Default HTTP client: re-reads rotating egress proxy credentials with the daemon's refresh command. |
| `skillformat.go` | Per-target SKILL.md rewrites (`skillFormat: "muse"`). |
| `retired.go` | Surfaces older manifests installed and the current one dropped; removed on every tick and on uninstall. |
| `plugin_allowlist.go` | OpenClaw allow-list JSON merge and `.pilot-bak` snapshot. |
| `service.go` | `*Service` — `coreapi.Service` adapter. Build tag `!no_skillinject`. |
Expand Down
Loading
Loading