skillinject

package
v1.10.1-rc3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

Documentation

Overview

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.

Index

Constants

View Source
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.

View Source
const DefaultInterval = 15 * time.Minute

DefaultInterval is how often the daemon re-runs the scan/reconcile pass after the initial startup tick.

View Source
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).

View Source
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 IsEnabled

func IsEnabled(home string) bool

IsEnabled returns whether skill injection is on. Defaults to TRUE (opt-out, not opt-in) when the flag isn't present, so fresh installs get the feature without any extra step.

func ParseFileMode

func ParseFileMode(s string) os.FileMode

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

func Run(ctx context.Context, cfg Config)

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.

func SetEnabled

func SetEnabled(home string, enabled bool) error

SetEnabled persists the opt-out flag. Reads the existing config (if any), updates only the skill_inject key, writes back atomically.

Types

type Action

type Action string

Action is what the reconcile loop chose to do in response to a State.

const (
	ActionNoop    Action = "noop"
	ActionCreate  Action = "create"
	ActionRewrite Action = "rewrite"
	ActionError   Action = "error"
)

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
}

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. Stored at ~/.pilot/config.json under "skill_inject" → {"enabled": bool}.

type FileKind

type FileKind string

FileKind names which of a target's managed surfaces an Outcome is about.

const (
	KindSkill           FileKind = "skill"
	KindMarker          FileKind = "marker"
	KindHelper          FileKind = "helper"
	KindPluginFile      FileKind = "plugin_file"
	KindPluginAllowList FileKind = "plugin_allowlist"
	KindWebhookRoute    FileKind = "webhook_route"
	KindWebhookURL      FileKind = "webhook_url"
)

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 is the single-plugin slot. Kept for backwards compat with
	// pre-multi-plugin manifests. Prefer Plugins for new entries.
	Plugin *ManifestPlugin `json:"plugin,omitempty"`
	// Plugins is the multi-plugin slot — one tool can install N plugins
	// (e.g. openclaw gets both pilotprotocol-prompt-injector and
	// pilotprotocol-webhook-receiver). Each entry reconciles identically
	// to a single Plugin block. If both Plugin and Plugins are set, the
	// daemon reconciles Plugin first then iterates Plugins.
	Plugins []ManifestPlugin `json:"plugins,omitempty"`
	// WebhookRoutes is the per-tool list of routes the daemon merges
	// into a YAML config (today hermes — see ManifestWebhookRoute). One
	// tool can declare any number; each reconciles independently.
	WebhookRoutes []ManifestWebhookRoute `json:"webhookRoutes,omitempty"`
	// WebhookURL, if set, tells the daemon to write this URL into
	// ~/.pilot/webhook_url so its webhook plugin POSTs events to the
	// tool's receiver. Set per-tool with the canonical default port and
	// path (e.g. openclaw → http://127.0.0.1:18789/pilot-webhook).
	// When multiple tools declare a URL, the daemon picks the first one
	// whose RootDir exists on disk — manifest order is the tiebreaker.
	// Operators wanting multi-tool delivery can run pilotctl set-webhook
	// explicitly.
	WebhookURL string `json:"webhookURL,omitempty"`
}

ManifestTool is one tool target row.

type ManifestWebhookRoute

type ManifestWebhookRoute struct {
	// ConfigPath is the YAML file the daemon merges into
	// (e.g. "~/.hermes/config.yaml").
	ConfigPath string `json:"configPath"`
	// RoutesYamlPath is the dotted path to the routes map. Created if
	// absent; intermediate maps are materialized.
	// e.g. "platforms.webhook.extra.routes".
	RoutesYamlPath string `json:"routesYamlPath"`
	// RouteName is the key under RoutesYamlPath where our entry goes
	// (e.g. "pilot-events"). The daemon owns this key.
	RouteName string `json:"routeName"`
	// Route is the YAML body to assign at RoutesYamlPath.RouteName.
	// Free-form so future schema additions don't require a Go change —
	// the daemon passes it through verbatim.
	Route map[string]interface{} `json:"route"`
}

ManifestWebhookRoute describes a route the daemon adds to a YAML config file so the tool's built-in webhook receiver accepts pilot events at a known path. Modeled on hermes-agent's platforms.webhook.extra.routes schema (one named route entry per agent integration, HMAC-signed, optional event allow-list, optional prompt template).

The daemon owns the named route — operators should not hand-edit the same RouteName under RoutesYamlPath; the reconcile loop will overwrite it. Other keys in the file (including other routes) are preserved.

Comment-preservation caveat: the current implementation parses YAML to a generic map and re-marshals, which loses inline comments on any node it touches. Comments on keys outside the modified subtree survive only if the YAML library happens to preserve them across the round-trip — yaml.v3's default does not. A future upgrade to yaml.Node-mode editing would close this gap.

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 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 Tick

func Tick(ctx context.Context, cfg Config) (*Report, error)

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 disabled skill injection via `pilotctl skills disable` (persisted in ~/.pilot/config.json), Tick returns an empty report without touching disk or the network.

func (*Report) Counts

func (r *Report) Counts() map[Action]int

Counts returns how many outcomes hit each Action.

type State

type State string

State is the classification of one managed file at the start of a tick.

const (
	// File (or marker block) does not exist on disk.
	StateAbsent State = "absent"
	// File/marker exists and matches the canonical we want.
	StateIdentical State = "identical"
	// File/marker exists but the content/hash differs from canonical.
	StateDrifted State = "drifted"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL