Documentation
¶
Overview ¶
Package skillinject is the L11 plugin wrapper around the host-filesystem skill installer in internal/skillinject. The installer itself has no daemon coupling — it scans known agent-tool directories and writes SKILL.md/heartbeat references — so the data layer lives utility-tier where both the daemon plugin and cmd/pilotctl can call it.
Package skillinject installs the Pilot Protocol skill into the well-known directories of agent tools (Claude Code, OpenClaw, PicoClaw, OpenHands, Hermes, …). The configuration — what to inject, where, and what marker content to upsert into each tool's heartbeat file — is fetched at runtime from the pilot-skills repository on GitHub. There is no embedded fallback: a tick that cannot reach the network is logged and skipped; the next tick retries.
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: GetMode (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.
Index ¶
- Constants
- func GetMode(home string) string
- func IsEnabled(home string) booldeprecated
- func ParseFileMode(s string) os.FileMode
- func Run(ctx context.Context, cfg Config)
- func SetEnabled(home string, enabled bool) errordeprecated
- func SetMode(home, mode string) error
- func SetUpdateConfig(home string, cfg UpdateConfig) error
- type Action
- type Config
- type EnabledFlag
- type FileKind
- type Manifest
- type ManifestHelper
- type ManifestPlugin
- type ManifestPluginAllowList
- type ManifestPluginFile
- type ManifestTool
- type ModeFlag
- type Outcome
- type Removal
- type RemovalKind
- type RemovalReport
- type Report
- type Service
- type State
- type UpdateConfig
Constants ¶
const ( // ModeManual: install once + refresh on Pilot update, no 15-min ticker. ModeManual = "manual" // ModeAuto: current always-live behaviour (15-min reconcile ticker). ModeAuto = "auto" // ModeDisabled: remove skills + no ticks. ModeDisabled = "disabled" )
Mode names for the tri-state skill_inject.mode setting.
const BackupSuffix = ".pilot-bak"
BackupSuffix is appended to the original config file path before mergePluginAllowList overwrites it. Kept distinct from openclaw's own .bak / .bak.N rotation so we can identify our own snapshots and not interfere with the tool's rolling backup chain.
const DefaultInterval = 15 * time.Minute
DefaultInterval is how often the daemon re-runs the scan/reconcile pass after the initial startup tick.
const DefaultManifestURL = "https://raw.githubusercontent.com/TeoSlayer/pilot-skills/main/inject-manifest.json"
DefaultManifestURL is the canonical raw GitHub URL for the inject manifest. Overridable via Config.ManifestURL (test hook).
const DefaultRepoBaseURL = "https://raw.githubusercontent.com/TeoSlayer/pilot-skills/main/"
DefaultRepoBaseURL is the prefix used to fetch any path the manifest references (skills/<name>/SKILL.md, heartbeats/<tool>.md). Overridable via Config.RepoBaseURL.
Variables ¶
This section is empty.
Functions ¶
func GetMode ¶ added in v0.2.3
GetMode returns the current skill_inject mode. Defaults to ModeAuto when the flag isn't present, so existing installs keep their current live-ticker behaviour. New installs will be set to ModeManual on the first call to SetMode (configured by the daemon's startup path).
func IsEnabled
deprecated
IsEnabled returns whether skill injection is on. Returns true for ModeManual and ModeAuto, false for ModeDisabled. Defaults to true (opt-out, not opt-in) when the flag isn't present, so fresh installs get the feature without any extra step.
Deprecated: use GetMode to get the full tri-state.
func ParseFileMode ¶
ParseFileMode parses an octal mode string like "0755". Empty input returns the default 0o755 (executable). Invalid input returns 0o755 with no error so a malformed manifest doesn't break the tick.
func Run ¶
Run blocks running scan/reconcile ticks until ctx is cancelled. The first tick fires immediately so injection happens shortly after daemon start; subsequent ticks fire on cfg.Interval (unless mode is manual, in which case only the initial tick runs).
func SetEnabled
deprecated
func SetMode ¶ added in v0.2.3
SetMode persists the skill_inject mode. Reads the existing config (if any), updates only the skill_inject key, writes back atomically. Accepts ModeManual, ModeAuto, or ModeDisabled. Empty string is treated as ModeAuto for backward compatibility.
func SetUpdateConfig ¶ added in v0.2.3
func SetUpdateConfig(home string, cfg UpdateConfig) error
SetUpdateConfig persists the update subkey. Reads the existing config (if any), updates only the update key, writes back atomically.
Types ¶
type Action ¶
type Action string
Action is what the reconcile loop chose to do in response to a State.
type Config ¶
type Config struct {
// Home overrides the user home dir (test hook).
Home string
// Interval between scan ticks after the initial startup tick.
Interval time.Duration
// ManifestURL overrides the canonical raw GitHub URL for inject-manifest.json.
ManifestURL string
// RepoBaseURL overrides the prefix used to resolve relative paths in
// the manifest (skills/<name>/SKILL.md, heartbeats/<tool>.md).
RepoBaseURL string
// HTTPClient overrides the HTTP client used for fetching.
HTTPClient *http.Client
// ManifestPublicKey, when set, enables Ed25519 detached-signature
// verification on manifest + all fetched repo files. The daemon
// fetches <url>.sig alongside each resource and verifies before
// accepting. Nil (default) preserves the pre-verification behavior.
ManifestPublicKey ed25519.PublicKey
}
Config tunes the injector. Zero values use sensible defaults.
type EnabledFlag ¶
type EnabledFlag struct {
Enabled bool `json:"enabled"`
}
EnabledFlag describes the persisted opt-out state (legacy format). Stored at ~/.pilot/config.json under "skill_inject" → {"enabled": bool}. Deprecated: use ModeFlag with GetMode/SetMode instead.
type FileKind ¶
type FileKind string
FileKind names which of a target's managed surfaces an Outcome is about.
type Manifest ¶
type Manifest struct {
Version int `json:"version"`
Entrypoint string `json:"entrypoint"`
Description string `json:"description,omitempty"`
Tools []ManifestTool `json:"tools"`
Helpers []ManifestHelper `json:"helpers,omitempty"`
}
Manifest mirrors inject-manifest.json. Field tags match the upstream schema. Unknown fields are ignored (forward-compat with new tool fields).
type ManifestHelper ¶
type ManifestHelper struct {
Name string `json:"name"`
// Src is a repo-relative path fetched via fetchRepoFile, e.g.
// "workflow-injection/pilot-ask".
Src string `json:"src"`
// Dst is the absolute install target. Supports ~/ expansion, e.g.
// "~/.pilot/bin/pilot-ask".
Dst string `json:"dst"`
// Mode is the file mode in octal string form, e.g. "0755". Empty
// defaults to 0755 (helpers are executables).
Mode string `json:"mode,omitempty"`
}
ManifestHelper is one helper script the daemon installs at a well-known path so any AI tool on the host can invoke it. Used to ship pilot-ask (the directory + specialist round-trip wrapper).
Helpers are tool-agnostic — they live under ~/.pilot/bin/ and are referenced by every tool's heartbeat directive.
type ManifestPlugin ¶
type ManifestPlugin struct {
// ID matches openclaw.plugin.json's "id" field. Used as the
// allow-list key + directory name.
ID string `json:"id"`
// InstallPath is where the plugin directory is written
// (e.g. "~/.openclaw/extensions/pilotprotocol-prompt-injector").
InstallPath string `json:"installPath"`
// Files lists the plugin source files the daemon copies in.
// Order doesn't matter — each file is reconciled independently.
Files []ManifestPluginFile `json:"files"`
// AllowList, if set, tells the daemon to ensure the plugin id
// appears in the tool's plugin allow-list + entries map. Nil
// disables the JSON-merge step (e.g. for tools without an
// explicit allow-list concept).
AllowList *ManifestPluginAllowList `json:"allowList,omitempty"`
}
ManifestPlugin describes a per-tool plugin that the daemon writes onto disk (alongside the heartbeat + skill copy) and tracks in the tool's own plugin allow-list. Today this is openclaw-only — the plugin registers a `before_prompt_build` hook that prepends the pilot directive into the system prompt on every turn. SKILL.md and the heartbeat file are loaded by their tools' own lifecycles (workspace bootstrap / periodic), neither fires per-turn, so the plugin is the only reliable per-prompt injection surface.
type ManifestPluginAllowList ¶
type ManifestPluginAllowList struct {
// ConfigPath is the JSON file the daemon merges into
// (e.g. "~/.openclaw/openclaw.json").
ConfigPath string `json:"configPath"`
// AllowListJsonPath is a dotted path to the trust array. Created
// if absent. Daemon appends the plugin id iff not already present.
AllowListJsonPath string `json:"allowListJsonPath"`
// EntriesJsonPath is a dotted path to the per-plugin entries
// object (e.g. "plugins.entries"). The daemon ensures
// `entries.<id>.enabled` is `true`.
EntriesJsonPath string `json:"entriesJsonPath"`
}
ManifestPluginAllowList describes how the daemon merges its plugin id into a tool's configuration to mark the plugin as trusted/enabled. Today targets openclaw.json with paths `plugins.allow` (string array) and `plugins.entries.<id>.enabled` (bool).
type ManifestPluginFile ¶
type ManifestPluginFile struct {
// Name is the filename relative to InstallPath (e.g.
// "openclaw.plugin.json", "index.mjs").
Name string `json:"name"`
// Src is a repo-relative path fetched via fetchRepoFile
// (e.g. "workflow-injection/openclaw-plugin/index.mjs").
Src string `json:"src"`
}
ManifestPluginFile is one file the daemon writes into the plugin install directory. Mirrors ManifestHelper but scoped to a plugin.
type ManifestTool ¶
type ManifestTool struct {
Name string `json:"name"`
RootDir string `json:"rootDir"`
SkillsDir string `json:"skillsDir"`
HeartbeatPath string `json:"heartbeatPath,omitempty"`
HeartbeatTemplate string `json:"heartbeatTemplate,omitempty"`
SkillNaming string `json:"skillNaming,omitempty"` // "" = "directory" (default), "flat" = single-file
SelfHeartbeat bool `json:"selfHeartbeat,omitempty"`
Plugin *ManifestPlugin `json:"plugin,omitempty"`
}
ManifestTool is one tool target row.
type ModeFlag ¶ added in v0.2.3
type ModeFlag struct {
Mode string `json:"mode,omitempty"`
}
ModeFlag describes the persisted tri-state mode. Stored at ~/.pilot/config.json under "skill_inject" → {"mode": "manual"|"auto"|"disabled"}.
type Outcome ¶
type Outcome struct {
Tool string `json:"tool"`
Kind FileKind `json:"kind"`
Path string `json:"path"`
State State `json:"state"`
Action Action `json:"action"`
Hash string `json:"hash,omitempty"`
Err string `json:"err,omitempty"`
}
Outcome records one reconcile decision.
type Removal ¶
type Removal struct {
Tool string `json:"tool"`
Kind FileKind `json:"kind"`
Path string `json:"path"`
Action RemovalKind `json:"action"`
Err string `json:"err,omitempty"`
}
Removal is one entry in the disable report. Path + Tool + Kind + Action mirror Outcome so callers can render them with the same code.
type RemovalKind ¶
type RemovalKind string
RemovalKind names the action Uninstall took on one path. Mirrors Action / Kind in state.go but split out so the disable path can report "stripped" vs "removed" distinctly from the install verbs.
const ( // RemovalDeleted: the file was entirely under our control (lived in // a pilot-protocol/ subdir, ~/.pilot/bin, etc.) and is now gone. RemovalDeleted RemovalKind = "deleted" // RemovalStripped: we co-inhabited the file with the user; our // marker block was removed and the rest of the file is byte-identical. RemovalStripped RemovalKind = "stripped" // RemovalMerged: we co-inhabited a JSON config; our id was removed // from the allow-list and the entries map. Other keys preserved. RemovalMerged RemovalKind = "merged" // RemovalRestored: a .pilot-bak snapshot from install time was found // and we restored from it byte-for-byte (preferred over a manual // inverse-merge when available). RemovalRestored RemovalKind = "restored" // RemovalNoop: nothing on disk to remove for this path. RemovalNoop RemovalKind = "noop" // RemovalError: a removal attempt failed; see Err. RemovalError RemovalKind = "error" )
type RemovalReport ¶
type RemovalReport struct {
At time.Time `json:"at"`
Removals []Removal `json:"removals"`
ManifestOffline bool `json:"manifest_offline,omitempty"`
}
RemovalReport is what Uninstall returns to its caller.
func Uninstall ¶
func Uninstall(ctx context.Context, cfg Config) (*RemovalReport, error)
Uninstall is the inverse of Tick: it removes every file/marker the daemon has ever written via the skillinject manifest. Safety rules:
- Files in subdirs we own (~/.<tool>/skills/pilot-protocol/, ~/.pilot/bin/, plugin install paths) are DELETED outright.
- Files we co-inhabit with the user (CLAUDE.md, AGENTS.md, AGENT.md, SOUL.md) have ONLY our marker block stripped — the file is left on disk even if it becomes empty. Never delete a user-owned file.
- Plugin allow-list JSON (openclaw.json) is restored from .pilot-bak when present, otherwise we inverse-merge: remove our id from the allow array and delete entries.<id>.
Network failures are tolerated: if the manifest can't be fetched we fall back to the cached copy under ~/.pilot/skills-cache/. If that's also missing we return an error — fresh boxes that have never run a tick have nothing to remove anyway.
func (*RemovalReport) Counts ¶
func (r *RemovalReport) Counts() map[RemovalKind]int
Counts returns how many removals hit each RemovalKind.
type Report ¶
type Report struct {
At time.Time `json:"at"`
Outcomes []Outcome `json:"outcomes"`
Skipped []string `json:"skipped,omitempty"`
// Disabled is true if the tick was a no-op because the user has
// `pilotctl skills disable`'d injection.
Disabled bool `json:"disabled,omitempty"`
}
Report is the result of one Tick.
func ForceTick ¶ added in v0.2.3
ForceTick is like Tick but skips the IsEnabled() gate. It performs a full scan + reconcile pass regardless of the persisted skill_inject flag. Intended for one-shot use after a manual update (pilotctl update) so skills reconcile even in manual mode where the periodic ticker is not running.
func Tick ¶
Tick performs one scan + reconcile pass and returns a Report. Network failures abort the tick and return an error — there is no embedded fallback. Exposed for tests, one-shot use, and `pilotctl skills check`.
If the user has set skill injection to disabled mode via `pilotctl skills disable` (persisted in ~/.pilot/config.json), Tick returns an empty report without touching disk or the network. Use ForceTick to run even when skill injection is disabled (e.g. post-update reconcile in manual mode).
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the L11 plugin adapter. The daemon (L7) holds plugins only as coreapi.Service interfaces — no skillinject import in core. Concrete construction happens in cmd/daemon/main.go (L12).
func NewService ¶
NewService returns a Service ready for daemon.RegisterPlugin.
type State ¶
type State string
State is the classification of one managed file at the start of a tick.
type UpdateConfig ¶ added in v0.2.3
type UpdateConfig struct {
// Auto controls whether the updater checks for and applies new
// versions. True (default) = auto-update enabled.
Auto *bool `json:"auto,omitempty"`
// Pin locks the updater to a specific release tag (e.g. "v1.10.5").
// Empty string (default) means no pin — follow latest stable.
Pin string `json:"pin,omitempty"`
// Interval is the minimum time between update checks, in Go duration
// string format ("15m", "1h"). Empty string (default) means use the
// updater's built-in default check interval.
Interval string `json:"interval,omitempty"`
}
UpdateConfig describes the runtime-tunable auto-update settings. Stored at ~/.pilot/config.json under "update" → {...}. Zero values leave the updater default in place.
func GetUpdateConfig ¶ added in v0.2.3
func GetUpdateConfig(home string) UpdateConfig
GetUpdateConfig reads the update subkey from ~/.pilot/config.json. Returns a zero-value UpdateConfig when the subkey is absent, so the caller can apply its own defaults.