model persisted to /state; stop auto-allowing claude-code unfree
model persistence: /model <name> now writes to /state/hyperhive-model (in-container), Bus::new reads it on init. operator override survives harness restart and container rebuild; gone on --purge like every other piece of agent state. path overridable via HYPERHIVE_MODEL_FILE for tests. failure to persist is a warn, not fatal — runtime override still applies, just won't survive a restart. unfree opt-in: drop the auto-allowUnfreePredicate from harness-base.nix and the claude-unstable overlay. operator now has to set nixpkgs.config.allowUnfree (or a predicate listing claude-code) in their own host config. silent unfree bypass was sketchy; this is honest. readme + gotchas updated to spell out the snippet. todo: drops model-persistence + container-crash + journald (all shipped); adds per-agent send allow-list (constrain who an agent can message).
This commit is contained in:
parent
58c3cd853b
commit
8b9f7d21b7
6 changed files with 84 additions and 19 deletions
11
README.md
11
README.md
|
|
@ -91,6 +91,17 @@ hive-c0re will then:
|
||||||
- auto-create the manager container (`hm1nd`) if missing,
|
- auto-create the manager container (`hm1nd`) if missing,
|
||||||
- auto-rebuild any managed container whose hyperhive rev is stale.
|
- auto-rebuild any managed container whose hyperhive rev is stale.
|
||||||
|
|
||||||
|
`claude-code` is unfree; hyperhive does not auto-allow it for you.
|
||||||
|
Add to your host config:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
nixpkgs.config.allowUnfreePredicate =
|
||||||
|
pkg: builtins.elem (nixpkgs.lib.getName pkg) [ "claude-code" ];
|
||||||
|
```
|
||||||
|
|
||||||
|
(or `nixpkgs.config.allowUnfree = true`, your call). Each per-agent
|
||||||
|
container inherits this through the same nixpkgs evaluation.
|
||||||
|
|
||||||
## Build / deploy
|
## Build / deploy
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
|
||||||
20
TODO.md
20
TODO.md
|
|
@ -3,6 +3,17 @@
|
||||||
Pick anything from here when relevant. Cross-cutting design notes live in
|
Pick anything from here when relevant. Cross-cutting design notes live in
|
||||||
[CLAUDE.md](CLAUDE.md); high-level project intro in [README.md](README.md).
|
[CLAUDE.md](CLAUDE.md); high-level project intro in [README.md](README.md).
|
||||||
|
|
||||||
|
## Permissions / policy
|
||||||
|
|
||||||
|
- **Per-agent send allow-list.** Today any agent can `send` to any
|
||||||
|
other recipient (peer, manager, operator). Add a per-agent
|
||||||
|
policy that constrains the `to` field — declared in `agent.nix`,
|
||||||
|
e.g. `hyperhive.allowedRecipients = [ "manager" "alice" ]`.
|
||||||
|
Broker rejects with an `Err { message }` when the policy denies.
|
||||||
|
Default: unrestricted (back-compat). The manager can still
|
||||||
|
always send anywhere. Useful for sandboxing untrusted sub-agents
|
||||||
|
so they can only talk to the manager, not other sub-agents.
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
- **Unprivileged containers (userns mapping).** Today the nspawn container
|
- **Unprivileged containers (userns mapping).** Today the nspawn container
|
||||||
|
|
@ -31,15 +42,6 @@ Pick anything from here when relevant. Cross-cutting design notes live in
|
||||||
derived from the same config so the operator stays in control of
|
derived from the same config so the operator stays in control of
|
||||||
what's exposed.
|
what's exposed.
|
||||||
|
|
||||||
## Per-agent settings
|
|
||||||
|
|
||||||
- **Model override persistence.** `/model <name>` already switches
|
|
||||||
the model at runtime via `Bus::set_model`; the chip on the agent
|
|
||||||
page reflects the current value. Override is in-memory only and
|
|
||||||
resets on harness restart — by design for now, but consider
|
|
||||||
optional persistence (`/state/model` file?) so an operator-set
|
|
||||||
model survives a rebuild.
|
|
||||||
|
|
||||||
## UI / UX
|
## UI / UX
|
||||||
|
|
||||||
- **Terminal: `/model` slash command.** Operator-typeable model
|
- **Terminal: `/model` slash command.** Operator-typeable model
|
||||||
|
|
|
||||||
|
|
@ -53,11 +53,15 @@ socket without needing a clean reinstall.
|
||||||
|
|
||||||
## `claude-code` is unfree
|
## `claude-code` is unfree
|
||||||
|
|
||||||
`harness-base.nix` allow-list's it specifically. The flake pins it to
|
The flake pins it to **nixpkgs-unstable** via
|
||||||
**nixpkgs-unstable** via `overlays.claude-unstable` (stable lags too
|
`overlays.claude-unstable` (stable lags too far). The overlay
|
||||||
far). The overlay imports unstable with its own
|
imports unstable inheriting the user's `nixpkgs.config`, so the
|
||||||
`allowUnfreePredicate` so the access inside the overlay doesn't
|
operator must opt in by setting `allowUnfree = true` (or an
|
||||||
itself trip.
|
`allowUnfreePredicate` that whitelists `claude-code`) on their host
|
||||||
|
config. hyperhive deliberately does NOT auto-allow — silent unfree
|
||||||
|
bypass would be sketchy, and the error message is clear enough that
|
||||||
|
the operator can fix it once and forget about it. Same on the
|
||||||
|
per-agent containers (they inherit through the same nixpkgs).
|
||||||
|
|
||||||
## Claude credentials are per-agent
|
## Claude credentials are per-agent
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,14 @@
|
||||||
claude-unstable =
|
claude-unstable =
|
||||||
final: prev:
|
final: prev:
|
||||||
let
|
let
|
||||||
|
# Inherit the *user's* nixpkgs config so allowUnfree (or an
|
||||||
|
# `allowUnfreePredicate` they set on their flake) propagates
|
||||||
|
# into the unstable import. hyperhive does not silently
|
||||||
|
# bypass the unfree gate — if the operator hasn't opted in,
|
||||||
|
# this overlay's `claude-code` access fails honestly.
|
||||||
unstable = import nixpkgs-unstable {
|
unstable = import nixpkgs-unstable {
|
||||||
inherit (prev.stdenv.hostPlatform) system;
|
inherit (prev.stdenv.hostPlatform) system;
|
||||||
config.allowUnfreePredicate = pkg: builtins.elem (prev.lib.getName pkg) [ "claude-code" ];
|
config = prev.config;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,36 @@ const HISTORY_CAPACITY: usize = 2000;
|
||||||
/// `HYPERHIVE_EVENTS_DB` env var (used in tests and one-shot tools).
|
/// `HYPERHIVE_EVENTS_DB` env var (used in tests and one-shot tools).
|
||||||
const DEFAULT_EVENTS_DB: &str = "/state/hyperhive-events.sqlite";
|
const DEFAULT_EVENTS_DB: &str = "/state/hyperhive-events.sqlite";
|
||||||
|
|
||||||
|
/// Persisted model name file. Same lifecycle as the events db —
|
||||||
|
/// survives destroy/recreate, gone on purge. Empty / missing file
|
||||||
|
/// falls back to `DEFAULT_MODEL`.
|
||||||
|
const DEFAULT_MODEL_FILE: &str = "/state/hyperhive-model";
|
||||||
|
|
||||||
|
/// Path to the persisted model file. Overridable via
|
||||||
|
/// `HYPERHIVE_MODEL_FILE` for dev / tests.
|
||||||
|
fn model_file_path() -> PathBuf {
|
||||||
|
std::env::var_os("HYPERHIVE_MODEL_FILE")
|
||||||
|
.map_or_else(|| PathBuf::from(DEFAULT_MODEL_FILE), PathBuf::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_model() -> Option<String> {
|
||||||
|
let s = std::fs::read_to_string(model_file_path()).ok()?;
|
||||||
|
let name = s.trim();
|
||||||
|
if name.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(name.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn persist_model(name: &str) -> std::io::Result<()> {
|
||||||
|
let path = model_file_path();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
std::fs::write(path, format!("{name}\n"))
|
||||||
|
}
|
||||||
|
|
||||||
fn now_unix() -> i64 {
|
fn now_unix() -> i64 {
|
||||||
std::time::SystemTime::now()
|
std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
|
@ -177,11 +207,12 @@ impl Bus {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (tx, _) = broadcast::channel(CHANNEL_CAPACITY);
|
let (tx, _) = broadcast::channel(CHANNEL_CAPACITY);
|
||||||
|
let initial_model = load_model().unwrap_or_else(|| DEFAULT_MODEL.to_owned());
|
||||||
Self {
|
Self {
|
||||||
tx: Arc::new(tx),
|
tx: Arc::new(tx),
|
||||||
store,
|
store,
|
||||||
state: Arc::new(Mutex::new((TurnState::Idle, now_unix()))),
|
state: Arc::new(Mutex::new((TurnState::Idle, now_unix()))),
|
||||||
model: Arc::new(Mutex::new(DEFAULT_MODEL.to_owned())),
|
model: Arc::new(Mutex::new(initial_model)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,9 +224,16 @@ impl Bus {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Switch the model for future turns. The current turn (if any)
|
/// Switch the model for future turns. The current turn (if any)
|
||||||
/// keeps the model it was already running.
|
/// keeps the model it was already running. Persisted to
|
||||||
|
/// `/state/hyperhive-model` so the override survives harness
|
||||||
|
/// restart and container rebuild (gone on `--purge`, matching
|
||||||
|
/// every other piece of agent state).
|
||||||
pub fn set_model(&self, name: impl Into<String>) {
|
pub fn set_model(&self, name: impl Into<String>) {
|
||||||
*self.model.lock().unwrap() = name.into();
|
let value: String = name.into();
|
||||||
|
self.model.lock().unwrap().clone_from(&value);
|
||||||
|
if let Err(e) = persist_model(&value) {
|
||||||
|
tracing::warn!(error = ?e, "model: persist failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the harness's authoritative turn-loop state. Records
|
/// Update the harness's authoritative turn-loop state. Records
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,12 @@
|
||||||
|
|
||||||
boot.isNspawnContainer = true;
|
boot.isNspawnContainer = true;
|
||||||
|
|
||||||
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ];
|
# `claude-code` is unfree. hyperhive intentionally does NOT auto-allow
|
||||||
|
# it — the operator opts in by setting
|
||||||
|
# `nixpkgs.config.allowUnfreePredicate` (or `allowUnfree = true`) in
|
||||||
|
# their own host config / agent.nix. Without that, the per-agent
|
||||||
|
# build fails on this package and the operator sees an honest "this
|
||||||
|
# is unfree, are you sure?" error.
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
hyperhive
|
hyperhive
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue