Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
896dfc6194 | ||
|
|
291abe6e41 | ||
|
|
1d3f22805f | ||
|
|
d207299d0f | ||
|
|
2bd1b0a3d5 | ||
|
|
bc83fde4ad | ||
|
|
4d885df9ad | ||
|
|
2ad4b43118 | ||
|
|
b08176f089 |
15 changed files with 437 additions and 12 deletions
16
claude-plugins/.claude-plugin/marketplace.json
Normal file
16
claude-plugins/.claude-plugin/marketplace.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
||||
"name": "hyperhive",
|
||||
"description": "hyperhive-authored Claude Code plugins, shipped hive-wide via the default hyperhive.claudeMarketplaces/claudePlugins nix options (nix/agent-modules/claude-settings.nix). Built as a nix store path and added with `claude plugin marketplace add <path>` - no forge repo or git remote needed. Add a new plugins/<name>/ subtree + one entry below to ship another.",
|
||||
"owner": {
|
||||
"name": "hyperhive"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "base",
|
||||
"description": "hyperhive's base plugin - skills every agent needs, regardless of role. Currently: state-hygiene (read-before-write discipline and dated-archive convention for durable agent notes/state files). Add a new skill here (not a new plugin) if it applies to all agents; give it its own specialized plugin if it doesn't.",
|
||||
"source": "./plugins/base",
|
||||
"category": "productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
claude-plugins/plugins/base/.claude-plugin/plugin.json
Normal file
7
claude-plugins/plugins/base/.claude-plugin/plugin.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "base",
|
||||
"description": "hyperhive's base plugin - skills every agent needs, regardless of role.",
|
||||
"author": {
|
||||
"name": "hyperhive"
|
||||
}
|
||||
}
|
||||
49
claude-plugins/plugins/base/skills/state-hygiene/SKILL.md
Normal file
49
claude-plugins/plugins/base/skills/state-hygiene/SKILL.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
name: state-hygiene
|
||||
description: Enforces safe handling of an agent's own durable state/notes files (e.g. state/notes.md, state/TODO.md, or anything else under state/ meant to survive a container restart) - read the file before archiving, overwriting, or pruning it, split resolved work into small dated archive files instead of growing one giant live file, and never shell-pipe-append (>>) into a notes file. Use this whenever you are about to write to a durable notes/state file, especially right before a context-compaction checkpoint, when told to "flush state" or "write down anything relevant", or when a live notes file has grown large and needs an archive pass.
|
||||
---
|
||||
|
||||
# State Hygiene
|
||||
|
||||
Durable notes/state files (your `CLAUDE.md`, `state/notes.md`, `state/TODO.md`,
|
||||
and anything else under `state/`) are your memory across container resets and
|
||||
context compactions. They only work if you actually read them before you
|
||||
change them - writing blind is how history gets silently destroyed or
|
||||
duplicated.
|
||||
|
||||
## Before writing to any durable notes file
|
||||
|
||||
1. **Read it first.** Never overwrite or append to a notes/state file based
|
||||
on memory or assumption of its current contents - the file may have moved
|
||||
on since your last read (another turn, another agent touched shared
|
||||
space, a prior session ended mid-edit). Read (or re-read the relevant
|
||||
section) immediately before editing.
|
||||
2. **Never pipe-append.** Don't do `echo "..." >> state/notes.md` (or
|
||||
equivalent) from a shell command. That's a blind write with no chance to
|
||||
see - and reconcile with - what's already there. Use your file-edit tool
|
||||
(read, then edit/write) instead.
|
||||
3. **Prefer a targeted edit over a full rewrite** for anything long-lived: an
|
||||
edit is less likely to accidentally clobber unrelated content than
|
||||
regenerating the whole file from your current context.
|
||||
|
||||
## Keeping a notes file lean
|
||||
|
||||
- Split into a **live file** (only what's currently active/gated/relevant -
|
||||
this is what you re-read every turn, keep it short) and an **archive
|
||||
subdirectory** (e.g. `state/notes-archive/`) for resolved/closed work,
|
||||
moved out of the hot path, never deleted. Don't drop archived files
|
||||
loose at the top level of `state/` alongside the live files - a
|
||||
dedicated subdir keeps `ls state/` showing only what's live.
|
||||
- Archive in **dated, per-topic chunks** - `YYYY-MM-DD-<topic>.md` - not one
|
||||
ever-growing archive blob. `ls` on the archive subdir should read like a
|
||||
timeline.
|
||||
- When you finish a piece of work, collapse its entry in the live file to a
|
||||
one-line "done" summary and move the full detail into a new dated
|
||||
archive file.
|
||||
- A live file well past ~500 lines (or into four figures) is overdue for an
|
||||
archive pass - do it at a natural checkpoint (finishing a task, a
|
||||
context-compaction prompt, or whenever you notice it).
|
||||
|
||||
If your environment ships its own hive-wide/repo-wide notes convention doc,
|
||||
treat that as the canonical source and this skill as a reminder to follow
|
||||
it - don't duplicate its rationale into your own notes either.
|
||||
|
|
@ -84,6 +84,47 @@ scoped, only this one package. This is needed because each per-agent
|
|||
operator's host-level `allowUnfree` does **not** propagate in.
|
||||
Operators don't need to set anything on their side.
|
||||
|
||||
That same isolation is why an agent can't pick a claude out of a
|
||||
*different* nixpkgs by itself: a container only ever sees the one
|
||||
nixpkgs the meta flake injects, so an `agent.nix` naming the host's
|
||||
`nixpkgs-unstable` has nothing to name. A release channel can trail
|
||||
unstable by weeks on this package, which is what
|
||||
`services.hyperhive.c0re.claudeCodePackage` is for — set it host-side
|
||||
and every agent runs that build.
|
||||
|
||||
What crosses is the **store path**, not the derivation. Containers
|
||||
share the host's `/nix/store`, so the binary is already reachable
|
||||
inside them with its whole closure; hive-c0re writes the path into each
|
||||
agent's flake as a string literal and the agent module symlinks
|
||||
`bin/claude` onto PATH. Two things rule out the obvious alternatives: a
|
||||
`path:/nix/store/<pkg>` flake input is re-copied into the store as a
|
||||
reference-less `-source` (so the runtime closure never arrives), and
|
||||
`lib.types.package` fed a bare path runs `builtins.storePath`, which
|
||||
pure evaluation rejects. `hyperhive.docs.source` gets away with being
|
||||
an input only because a docs tree has no runtime dependencies.
|
||||
|
||||
The `storePath` trap is worth spelling out, because it is not confined
|
||||
to options the operator writes: **any** option of type `package` fed a
|
||||
store-path *string* coerces through `lib.toDerivation`, i.e.
|
||||
`builtins.storePath`. `environment.systemPackages` and
|
||||
`systemd.services.<name>.path` both do it (the latter takes plain
|
||||
strings like `/run/wrappers` happily, but anything under
|
||||
`builtins.storeDir` is treated as a package). So a path handed to the
|
||||
container as text has to be wrapped in a real derivation — a symlink
|
||||
farm built from the interpolated string — before it can go anywhere a
|
||||
package is expected.
|
||||
|
||||
The catch is that a path written into a generated flake is text, not a
|
||||
reference — the container's closure does not keep the binary alive.
|
||||
The **host** does: the package is interpolated into
|
||||
`/etc/hyperhive/serve.json`, so it lands in the host's system closure
|
||||
and is gc-rooted by the running generation. `builtins.toJSON` preserves
|
||||
string context, which is the load-bearing detail; discard the context
|
||||
anywhere on that path and `nix-collect-garbage` will eventually take
|
||||
the hive's `claude` out from under it. The price of the root is that an
|
||||
old `claude-code` can't be reclaimed until every agent has rebuilt past
|
||||
it and the old generations are gone.
|
||||
|
||||
## Claude credentials are per-agent
|
||||
|
||||
`/var/lib/hyperhive/agents/<name>/claude/` bind-mounts to
|
||||
|
|
|
|||
|
|
@ -21,6 +21,17 @@ auto-reset / retry decisions in `drive_turn`. The lib returns everything it
|
|||
parsed from a turn (usage, cost, context window, resolved model) as
|
||||
`Telemetry`, which the policy layer applies to the bus.
|
||||
|
||||
**Which `claude` binary.** The bare name `claude`, resolved off the
|
||||
harness unit's PATH. By default that's the `claude-code` in the agent's
|
||||
own nixpkgs (the meta flake's `nixpkgs` input) via
|
||||
`environment.systemPackages`. Since that's usually a release channel and
|
||||
this package moves fast, the operator can pin one hive-wide with
|
||||
`services.hyperhive.c0re.claudeCodePackage`: its store path is written
|
||||
into each agent's flake, and `claude` on PATH becomes a symlink to it
|
||||
instead of the container's own `claude-code` — so there's only ever one
|
||||
`claude` in the container. Agents pick up a new build on their
|
||||
next rebuild, not live. See docs/gotchas.md::`claude-code` is unfree.
|
||||
|
||||
Hive-enforced settings ship at `/etc/claude-code/managed-settings.json`
|
||||
(claude-code's canonical managed-settings path — precedence #1,
|
||||
read-only, un-overridable), wired in `nix/agent-modules/claude-settings.nix`
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@
|
|||
assets
|
||||
frontend
|
||||
reference-docs
|
||||
claude-plugins
|
||||
;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -62,6 +62,17 @@ pub struct Coordinator {
|
|||
/// "nixpkgs"` is set in the host flake). Empty string = legacy
|
||||
/// `follows = "hyperhive/nixpkgs"` behaviour.
|
||||
pub nixpkgs_flake: String,
|
||||
/// Store path of the `claude-code` build every agent runs, written
|
||||
/// into each per-agent flake as `hyperhive.claudeCodePath`. Set by
|
||||
/// the NixOS module option `services.hyperhive.c0re.claudeCodePackage`
|
||||
/// (which resolves the package and hands us its path). `None` = every
|
||||
/// agent keeps the `claude-code` from its own nixpkgs.
|
||||
///
|
||||
/// A path rather than a flake input because containers share the
|
||||
/// host's `/nix/store`: the binary is already reachable inside them,
|
||||
/// closure and all. The host module is what keeps it from being
|
||||
/// garbage-collected — see that option.
|
||||
pub claude_code_path: Option<String>,
|
||||
/// TCP port the host's hive-c0re dashboard listens on. Inlined into
|
||||
/// each per-agent flake so the agent's web UI can build the right
|
||||
/// rebuild-button URL pointing back at the dashboard.
|
||||
|
|
@ -213,6 +224,11 @@ pub struct HiveEnv {
|
|||
/// so a doc edit only re-locks this input, not the whole source.
|
||||
pub hyperhive_docs_flake: String,
|
||||
pub nixpkgs_flake: String,
|
||||
/// Store path of the `claude-code` agents run, or `None` for "each
|
||||
/// agent keeps the one out of its own nixpkgs". Travels into the
|
||||
/// container as `hyperhive.claudeCodePath` — a plain string, kept
|
||||
/// alive host-side by the module that resolved it.
|
||||
pub claude_code_path: Option<String>,
|
||||
pub dashboard_port: u16,
|
||||
pub operator_pronouns: String,
|
||||
pub context_window_tokens: std::collections::HashMap<String, u64>,
|
||||
|
|
@ -235,6 +251,7 @@ impl Default for HiveEnv {
|
|||
hyperhive_flake: "/etc/hyperhive".to_string(),
|
||||
hyperhive_docs_flake: String::new(),
|
||||
nixpkgs_flake: String::new(),
|
||||
claude_code_path: None,
|
||||
dashboard_port: 7000,
|
||||
operator_pronouns: "she/her".to_string(),
|
||||
context_window_tokens: std::collections::HashMap::from([
|
||||
|
|
@ -474,6 +491,7 @@ impl Coordinator {
|
|||
hyperhive_flake,
|
||||
hyperhive_docs_flake,
|
||||
nixpkgs_flake,
|
||||
claude_code_path,
|
||||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
|
|
@ -520,6 +538,7 @@ impl Coordinator {
|
|||
hyperhive_flake,
|
||||
hyperhive_docs_flake,
|
||||
nixpkgs_flake,
|
||||
claude_code_path,
|
||||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
|
|
@ -553,6 +572,7 @@ impl Coordinator {
|
|||
hyperhive_flake: self.hyperhive_flake.clone(),
|
||||
hyperhive_docs_flake: self.hyperhive_docs_flake.clone(),
|
||||
nixpkgs_flake: self.nixpkgs_flake.clone(),
|
||||
claude_code_path: self.claude_code_path.clone(),
|
||||
dashboard_port: self.dashboard_port,
|
||||
operator_pronouns: self.operator_pronouns.clone(),
|
||||
context_window_tokens: self.context_window_tokens.clone(),
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ pub async fn sync_agents(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<()> {
|
|||
&hive.hyperhive_flake,
|
||||
&hive.hyperhive_docs_flake,
|
||||
&hive.nixpkgs_flake,
|
||||
hive.claude_code_path.as_deref(),
|
||||
hive.dashboard_port,
|
||||
&hive.operator_pronouns,
|
||||
&hive.context_window_tokens,
|
||||
|
|
@ -650,6 +651,7 @@ fn render_flake(
|
|||
hyperhive_flake: &str,
|
||||
docs_flake: &str,
|
||||
nixpkgs_flake: &str,
|
||||
claude_code_path: Option<&str>,
|
||||
dashboard_port: u16,
|
||||
operator_pronouns: &str,
|
||||
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||
|
|
@ -660,6 +662,7 @@ fn render_flake(
|
|||
hyperhive_flake,
|
||||
docs_flake,
|
||||
nixpkgs_flake,
|
||||
claude_code_path,
|
||||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
|
|
@ -937,6 +940,7 @@ fn render_flake_with_lookup<F>(
|
|||
hyperhive_flake: &str,
|
||||
docs_flake: &str,
|
||||
nixpkgs_flake: &str,
|
||||
claude_code_path: Option<&str>,
|
||||
dashboard_port: u16,
|
||||
operator_pronouns: &str,
|
||||
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||
|
|
@ -1051,6 +1055,18 @@ where
|
|||
if !docs_flake.is_empty() {
|
||||
out.push_str(" hyperhive.docs.source = inputs.\"hyperhive-docs\".outPath;\n");
|
||||
}
|
||||
// The `claude-code` agents run, as a bare store path rather than a
|
||||
// flake input: containers share the host's `/nix/store`, so the
|
||||
// binary is already there with its closure and has nothing to
|
||||
// travel. A string literal is also the only shape that evaluates —
|
||||
// `lib.types.package` on a bare path runs `builtins.storePath`,
|
||||
// which pure eval rejects. The agent module puts its `bin/` on the
|
||||
// harness PATH; the host module holds the gc root, since a path
|
||||
// spelled out here is text and references nothing.
|
||||
// `None` = no override; agents keep their own nixpkgs' `claude-code`.
|
||||
if let Some(path) = claude_code_path {
|
||||
let _ = writeln!(out, " hyperhive.claudeCodePath = \"{path}\";");
|
||||
}
|
||||
// CA trust: embed every hive-trusted CA so each agent validates them at
|
||||
// build time. The list is the hive's own self-signed CA (when active)
|
||||
// plus every peer-hive root CA (`swarm.peers.<d>.caCert`) — a peer CA is
|
||||
|
|
@ -1561,6 +1577,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1605,6 +1622,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1617,6 +1635,57 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_flake_pins_claude_path_without_adding_an_input() {
|
||||
// The host-pinned claude travels as a bare store path assigned to
|
||||
// an option — deliberately NOT as a flake input. Containers share
|
||||
// the host store, so the build is already reachable; making it an
|
||||
// input would re-copy it as a reference-less `-source` and strip
|
||||
// the closure the binary actually needs.
|
||||
let out = render_flake(
|
||||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
Some("/nix/store/cccc-claude-code-2.1.220"),
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
assert!(
|
||||
out.contains("hyperhive.claudeCodePath = \"/nix/store/cccc-claude-code-2.1.220\";"),
|
||||
"claude path assigned as a plain string literal:\n{out}"
|
||||
);
|
||||
assert!(
|
||||
!out.contains("claude-code-2.1.220\".url"),
|
||||
"the pinned claude must not become a flake input:\n{out}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_flake_omits_claude_path_when_unset() {
|
||||
// `None` = no host-level pin: the option is left undefined so the
|
||||
// agent module keeps its own nixpkgs' `claude-code` (and keeps it
|
||||
// in `environment.systemPackages`, which is what makes the
|
||||
// unpinned case self-contained).
|
||||
let out = render_flake(
|
||||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
assert!(
|
||||
!out.contains("claudeCodePath"),
|
||||
"no claude assignment when unpinned:\n{out}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_flake_falls_back_to_follows_when_nixpkgs_flake_empty() {
|
||||
// Empty nixpkgs_flake → legacy follows behaviour (backward compat
|
||||
|
|
@ -1625,6 +1694,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1655,6 +1725,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1688,6 +1759,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1723,6 +1795,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1761,6 +1834,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1803,6 +1877,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1886,6 +1961,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1971,6 +2047,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -2013,6 +2090,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
|
|||
|
|
@ -228,7 +228,14 @@ in
|
|||
|
||||
options.hyperhive.claudeMarketplaces = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ "anthropics/claude-plugins-official" ];
|
||||
default = [
|
||||
"anthropics/claude-plugins-official"
|
||||
"${config.hyperhive.packages.claude-plugins}"
|
||||
];
|
||||
defaultText = lib.literalMD ''
|
||||
`[ "anthropics/claude-plugins-official" "''${hyperhive.packages.claude-plugins}" ]`
|
||||
(the flake's own local-path plugin marketplace)
|
||||
'';
|
||||
example = [
|
||||
"anthropics/claude-plugins-official"
|
||||
"anthropics/claude-plugins-community"
|
||||
|
|
@ -242,14 +249,24 @@ in
|
|||
reference a marketplace (e.g. `foo@claude-plugins-official`).
|
||||
Rendered to `/etc/hyperhive/claude-marketplaces.json`.
|
||||
|
||||
Defaults to Anthropic's official marketplace; agents get it
|
||||
out of the box without any per-agent.nix wiring.
|
||||
Defaults to Anthropic's official marketplace plus hyperhive's
|
||||
own `claude-plugins` nix package (see
|
||||
`nix/packages/claude-plugins.nix`) — a local-path marketplace
|
||||
built as a plain nix store path, registered under the name
|
||||
`hyperhive` (from its `marketplace.json`, not the store path
|
||||
itself, so plugin specs stay stable across rebuilds). No forge
|
||||
repo or git remote needed to ship a hive-authored skill; agents
|
||||
get both marketplaces out of the box without any per-agent.nix
|
||||
wiring.
|
||||
'';
|
||||
};
|
||||
|
||||
options.hyperhive.claudePlugins = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ "skill-creator@claude-plugins-official" ];
|
||||
default = [
|
||||
"skill-creator@claude-plugins-official"
|
||||
"base@hyperhive"
|
||||
];
|
||||
example = [
|
||||
"formatter@my-marketplace"
|
||||
"thinking-tools@anthropics"
|
||||
|
|
@ -264,15 +281,22 @@ in
|
|||
`/etc/hyperhive/claude-plugins.json`; the harness reads it via
|
||||
`plugins::install_configured`.
|
||||
|
||||
Defaults to Anthropic's `skill-creator`, which teaches an agent
|
||||
to write, refine, and evaluate its own skills — agents get it
|
||||
out of the box, matching the default marketplace above.
|
||||
Defaults to Anthropic's `skill-creator` (teaches an agent to
|
||||
write, refine, and evaluate its own skills) plus hyperhive's own
|
||||
`base` plugin — one plugin bundling every skill that applies to
|
||||
*all* agents regardless of role (currently just `state-hygiene`,
|
||||
read-before-write + dated-archive discipline for durable
|
||||
notes/state files; more all-agent skills land as additional
|
||||
skills inside this same plugin, not new plugins — a skill that
|
||||
only some agents need gets its own specialized plugin instead).
|
||||
Agents get both out of the box, matching the default
|
||||
marketplaces above.
|
||||
|
||||
Note that a per-agent definition REPLACES this default rather
|
||||
than extending it (ordinary NixOS list-option semantics, same as
|
||||
`claudeMarketplaces`). An agent that wants extra plugins AND the
|
||||
default should list `skill-creator@claude-plugins-official`
|
||||
alongside them.
|
||||
defaults should list both `skill-creator@claude-plugins-official`
|
||||
and `base@hyperhive` alongside them.
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,33 @@
|
|||
'';
|
||||
};
|
||||
|
||||
options.hyperhive.claudeCodePath = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/nix/store/…-claude-code-2.1.220";
|
||||
description = ''
|
||||
Store path of the `claude-code` this agent runs, or `null` (the
|
||||
default) to use the `claude-code` from the container's own
|
||||
nixpkgs.
|
||||
|
||||
Set by the generated meta flake when the operator sets
|
||||
`services.hyperhive.c0re.claudeCodePackage` host-side, so a hive
|
||||
can run a claude built from a *different* nixpkgs than the one
|
||||
its agents evaluate against — a release channel can trail
|
||||
unstable by weeks on this one package. It arrives as a path and
|
||||
not a package because agents share the host's `/nix/store`, so
|
||||
the build is already reachable here with its whole closure and
|
||||
has nothing to travel.
|
||||
|
||||
When set, `claude` on PATH is a symlink to this path's
|
||||
`bin/claude` and the container's own `claude-code` is dropped, so
|
||||
there is only ever one claude in the container. Note that neither
|
||||
the symlink nor anything else in the container's closure *refers*
|
||||
to the target — keeping it alive is the host's job, see
|
||||
`services.hyperhive.c0re.claudeCodePackage`.
|
||||
'';
|
||||
};
|
||||
|
||||
config = {
|
||||
assertions = [
|
||||
# Guard the inputs-routed-as-output pattern: the agent flake.nix is
|
||||
|
|
@ -183,8 +210,31 @@
|
|||
environment.systemPackages = [
|
||||
config.hyperhive.packages.hive-metric
|
||||
]
|
||||
++ [
|
||||
(
|
||||
if config.hyperhive.claudeCodePath == null then
|
||||
pkgs.claude-code
|
||||
else
|
||||
# Host-pinned claude: a symlink farm around a path the
|
||||
# container was handed as text. It has to be a derivation —
|
||||
# `environment.systemPackages` coerces a store-path *string*
|
||||
# with `toDerivation`, i.e. `builtins.storePath`, which pure
|
||||
# evaluation rejects (`systemd.services.*.path` does the same,
|
||||
# which is why the harness gets this via PATH like everything
|
||||
# else rather than a unit-level entry). Interpolating the path
|
||||
# into the builder is just text, so it evaluates anywhere.
|
||||
#
|
||||
# The symlink registers no store reference — the target isn't
|
||||
# among this derivation's inputs, so nothing here keeps the
|
||||
# binary alive. That is deliberate and it is the host's job:
|
||||
# see `services.hyperhive.c0re.claudeCodePackage`.
|
||||
pkgs.runCommandLocal "claude-code-pinned" { } ''
|
||||
mkdir -p "$out/bin"
|
||||
ln -s ${config.hyperhive.claudeCodePath}/bin/claude "$out/bin/claude"
|
||||
''
|
||||
)
|
||||
]
|
||||
++ (with pkgs; [
|
||||
claude-code
|
||||
bashInteractive
|
||||
coreutils-full
|
||||
# procps for pkill — used by the web UI's /api/cancel to SIGINT the
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
per-binary daemon/CLI packages (`hive-agent`, `hive-agent-mcp`,
|
||||
`hive-bash-daemon`,
|
||||
`hive-forge`, `hive-forge-notify`, `hive-matrix-daemon`,
|
||||
`hive-metric`, `hive-screen-mcp`) plus the `assets`, `frontend` and
|
||||
`reference-docs` trees. Wired by the flake's agent-base/ruth
|
||||
`hive-metric`, `hive-screen-mcp`) plus the `assets`, `frontend`,
|
||||
`reference-docs` and `claude-plugins` trees. Wired by the flake's agent-base/ruth
|
||||
nixosModules to `hyperhive.packages.<system>.*`; override an
|
||||
individual key per-agent to swap in a patched binary.
|
||||
'';
|
||||
|
|
|
|||
|
|
@ -76,6 +76,20 @@ let
|
|||
hyperhive_flake = cfg.hyperhiveFlake;
|
||||
hyperhive_docs_flake = cfg.hyperhiveDocs;
|
||||
nixpkgs_flake = cfg.nixpkgsFlake;
|
||||
# The `claude-code` every agent runs, or null for "each agent keeps
|
||||
# the one out of its own nixpkgs". `builtins.toJSON` serialises a
|
||||
# derivation as its out path (and null as null), so the package goes
|
||||
# in whole rather than interpolated — that is also the ONLY thing
|
||||
# gc-rooting it: the resulting string carries store context, so this
|
||||
# /etc entry genuinely references the package and the host's system
|
||||
# closure holds it alive. Nothing container-side can — meta.rs writes
|
||||
# the path into each agent's generated flake as a plain string
|
||||
# literal, and text is not a reference. Hence the assertion below: do
|
||||
# NOT discard this context, and do not route the path through
|
||||
# anything that drops it. The failure mode is a garbage-collected
|
||||
# `claude` and a hive that can't take a turn, weeks after the commit
|
||||
# that caused it.
|
||||
claude_code_path = cfg.claudeCodePackage;
|
||||
dashboard_port = cfg.dashboardPort;
|
||||
operator_pronouns = cfg.operatorPronouns;
|
||||
context_window_tokens = cfg.contextWindowTokens;
|
||||
|
|
@ -94,6 +108,28 @@ in
|
|||
];
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
# The pinned claude reaches agents as a bare path, so this
|
||||
# string's store context is the whole reason the binary survives
|
||||
# a `nix-collect-garbage`. Losing it is invisible at eval and at
|
||||
# deploy — it only shows up as every agent failing to spawn
|
||||
# `claude`, at whatever unrelated moment the gc runs. Cheap
|
||||
# enough to just check.
|
||||
assertion =
|
||||
cfg.claudeCodePackage == null || builtins.hasContext (builtins.toJSON cfg.claudeCodePackage);
|
||||
message = ''
|
||||
services.hyperhive.c0re.claudeCodePackage lost its store
|
||||
context on the way into /etc/hyperhive/serve.json, so the
|
||||
package is no longer gc-rooted by the system closure and
|
||||
`nix-collect-garbage` may delete the claude every agent runs.
|
||||
Something on that path discarded the context (e.g.
|
||||
builtins.unsafeDiscardStringContext, toString, or reading the
|
||||
path back out of a plain file) — undo it.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
environment.systemPackages = [
|
||||
cfg.package
|
||||
pkgs.git
|
||||
|
|
|
|||
|
|
@ -128,6 +128,45 @@
|
|||
of the host's channel.
|
||||
'';
|
||||
};
|
||||
claudeCodePackage = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.package;
|
||||
default = null;
|
||||
example = lib.literalExpression "inputs.nixpkgs-unstable.legacyPackages.x86_64-linux.claude-code";
|
||||
description = ''
|
||||
The `claude-code` build every agent runs, or `null` (the
|
||||
default) to leave each agent on the `claude-code` from its own
|
||||
nixpkgs — i.e. whatever `nixpkgsFlake` resolves to.
|
||||
|
||||
This is the one binary the whole hive is built around, and it
|
||||
moves fast enough that a release channel routinely trails
|
||||
unstable by weeks on it. An agent cannot fix that for itself:
|
||||
agents evaluate against the single nixpkgs hive-c0re injects,
|
||||
so an `agent.nix` has no other tree to reach for. Set this from
|
||||
a second nixpkgs in the host flake and every agent follows,
|
||||
without moving the nixpkgs the rest of the container is built
|
||||
from.
|
||||
|
||||
What travels into the container is the **store path**, not the
|
||||
derivation: agents share the host's `/nix/store`, so the binary
|
||||
and its full closure are already reachable there — nothing
|
||||
needs rebuilding or copying. hive-c0re writes the path into
|
||||
each agent's generated flake as a plain string literal (a bare
|
||||
path fed to `lib.types.package` would run `builtins.storePath`,
|
||||
which is illegal under pure evaluation) and the agent module
|
||||
puts its `bin/` on the harness's PATH.
|
||||
|
||||
The flip side of a plain string is that nothing in the agent's
|
||||
own closure refers to it, so the container cannot keep it
|
||||
alive. The **host** does that instead: this package is
|
||||
interpolated into `/etc/hyperhive/serve.json`, which puts it in
|
||||
the host's system closure — so it is gc-rooted by the running
|
||||
generation for exactly as long as that generation is the one
|
||||
the agents were rendered from. The cost is that
|
||||
`nix-collect-garbage` cannot reclaim an old `claude-code` until
|
||||
every agent has been rebuilt past it and the old generations
|
||||
are gone.
|
||||
'';
|
||||
};
|
||||
dashboardPort = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 7000;
|
||||
|
|
|
|||
48
nix/packages/claude-plugins.nix
Normal file
48
nix/packages/claude-plugins.nix
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
stdenv,
|
||||
}:
|
||||
|
||||
# hyperhive-authored Claude Code plugin marketplace (source tree at
|
||||
# ../../claude-plugins), shipped as a standalone derivation and spliced
|
||||
# into the *default* `hyperhive.claudeMarketplaces`/`claudePlugins` lists
|
||||
# (nix/agent-modules/claude-settings.nix) as a local marketplace source.
|
||||
# `claude plugin marketplace add` accepts a plain filesystem path (verified:
|
||||
# `claude plugin marketplace add --help` → "Add a marketplace from a URL,
|
||||
# path, or GitHub repo"), and registers it under the marketplace.json's own
|
||||
# `name` field ("hyperhive") rather than the path — so
|
||||
# `<plugin>@hyperhive` is a stable plugin spec regardless of the store
|
||||
# path's hash. The built store path IS the marketplace: no forge repo, no
|
||||
# git remote, no PAT/PR ceremony needed to ship a skill hive-wide.
|
||||
#
|
||||
# Every hive-authored skill lives under its own `plugins/<name>/` subtree
|
||||
# in the source tree; add one there plus a matching entry in
|
||||
# `.claude-plugin/marketplace.json`'s `plugins` list to ship another.
|
||||
#
|
||||
# Pure data: nothing to build, `$out` is the tree as-is (mirrors
|
||||
# ./reference-docs.nix). `cp -r . "$out"` (not `cp -r ./* $out`) because
|
||||
# the marketplace/plugin manifests live under `.claude-plugin/` — a
|
||||
# hidden directory a shell glob (`*`) would silently skip.
|
||||
stdenv.mkDerivation {
|
||||
pname = "hyperhive-claude-plugins";
|
||||
version = "0.1.0";
|
||||
# Narrow src keeps this derivation's input hash decoupled from the rest
|
||||
# of the tree — a skill edit only re-hashes this.
|
||||
src = ../../claude-plugins;
|
||||
|
||||
dontBuild = true;
|
||||
dontConfigure = true;
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
mkdir -p $out
|
||||
cp -r . "$out"/
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
|
||||
meta = {
|
||||
description = "hyperhive-authored Claude Code plugin marketplace (skills shipped hive-wide)";
|
||||
homepage = "https://forge.darkest.space/hyperhive/hyperhive";
|
||||
};
|
||||
}
|
||||
|
|
@ -139,6 +139,11 @@ in
|
|||
# so the binary derivation stays cached when a prompt edit ripples
|
||||
# through.
|
||||
assets = pkgs.callPackage ./assets.nix { };
|
||||
# hyperhive-authored Claude Code plugin marketplace (source tree at
|
||||
# ../../claude-plugins) — a local-path marketplace spliced into the
|
||||
# default `hyperhive.claudeMarketplaces`/`claudePlugins` lists. See
|
||||
# ./claude-plugins.nix for the "why a store path, not a repo" rationale.
|
||||
claude-plugins = pkgs.callPackage ./claude-plugins.nix { };
|
||||
# The repo docs/ markdown tree as a standalone derivation —
|
||||
# agents read it in-container (added as a claude additional
|
||||
# directory) and the website repo reuses it as a flake input,
|
||||
|
|
|
|||
Loading…
Reference in a new issue