Closes the last three outstanding gaps from the post-wave-11 research + policy-bridge follow-ups. JouleClaw now has full parity with NVIDIA OpenShell's agent-infrastructure primitives, with every piece kept all-Rust, deterministic, and dep-light. ## Wave-19 — jouleclaw-routing::privacy (proposal #3) Orthogonal sensitivity axis on top of the joules-first cascade. NVIDIA OpenShell's Privacy Router primitive, JouleClaw-native. Surface: - SensitivityLevel: Public / Internal / Confidential / Restricted - PrivacyClassifier trait — deterministic, pluggable - HeuristicPrivacyClassifier — dep-free reference impl detecting email / phone / SSN / credential markers / a tunable custom-keyword list - TierSensitivityPolicy — per JouleClass ceiling, default locks Wire to Public-only and Model to Internal - PrivacyRouter — wraps any inner Router, filters its plan to permitted tiers, prepends a reasoning line carrying the classifier verdict 17 new tests: classifier (6) + policy (4) + router (5) + the fallback / coord-aware path (2). ## Wave-20 — jouleclaw-workspace (proposal #4) Persistent structured workspace for long-running L6 agents. NVIDIA-OpenShell File-System parity, all-Rust + signed. Surface: - Workspace<B: WorkspaceBackend> handle with put/get/has/remove - WorkspaceBackend trait — content-addressed bytes (BLAKE3) - MemoryWorkspaceBackend / FileWorkspaceBackend reference impls - WorkspaceManifest — sorted entries, agent-owned, BLAKE3 content_hash over the canonical bytes - SignedWorkspaceManifest + sign_manifest / verify_signed_manifest using the AgentIdentity from jouleclaw-attest - Path normalisation rejects absolute paths, `..` segments, empty segments, NUL bytes 20 new tests: path normalisation (4) + put/get round-trip (3) + manifest determinism (3) + sign/verify (3) + resume across sessions (1) + file backend persistence + content-address dedupe (2) + bad-input rejection (3) + EntryKind wire (1). ## Wave-21 — jouleclaw-policy-ipc (final follow-up) Out-of-process transport for PolicyEngine. NVIDIA-OpenShell's "agent process cannot in-process patch the policy" primitive. Surface: - PolicyServer<E: PolicyEngine> — wraps an engine, serves LD-JSON requests on any (BufRead, Write) pair - PolicyClient — owns a spawned child process, implements PolicyEngine over the wire (evaluate / audit / audit_since / policy_version / install_policy) - jclaw-policyd binary — wires PolicyServer to stdin/stdout, with optional --policy <path> startup - Request / Response wire envelopes with stable LD-JSON shapes Required one trait extension: install_policy is now a defaulted method on PolicyEngine (defaulted to EnginePoisoned for backends that don't support live updates), so the wire path and the in-process path share a single trait method. InMemoryPolicyEngine delegates the trait method to its inherent impl — same code path for both transports. 5 unit tests (in-memory wire round-trip) + 3 integration tests (child-process spawn → real wire round-trip → install + evaluate + audit through the kernel boundary). ## Workspace 104 crates (+2: workspace + policy-ipc), 2,574 lib tests pass (+42 across waves 19-21), 0 failures. The full original adoption plan + the policy-bridge follow-ups are now shipped.
73 lines
2.4 KiB
Rust
73 lines
2.4 KiB
Rust
//! End-to-end integration test: spawn `jclaw-policyd` as a child
|
|
//! process and drive it through PolicyClient. The binary is built by
|
|
//! Cargo as a sibling of the test artefact under `target/<profile>/`.
|
|
|
|
use jouleclaw_policy::{
|
|
Action, AgentId, AgentScope, FsMode, Policy, PolicyEngine, Rule,
|
|
};
|
|
use jouleclaw_policy_ipc::PolicyClient;
|
|
use std::path::PathBuf;
|
|
|
|
fn binary_path() -> PathBuf {
|
|
// CARGO_BIN_EXE_<name> is set by Cargo when running integration
|
|
// tests of a crate that declares a [[bin]] target.
|
|
let p = env!("CARGO_BIN_EXE_jclaw-policyd");
|
|
PathBuf::from(p)
|
|
}
|
|
|
|
#[test]
|
|
fn evaluate_round_trips_through_a_child_process() {
|
|
let client = PolicyClient::spawn(binary_path()).expect("spawn jclaw-policyd");
|
|
// Server boots with the empty deny-by-default policy.
|
|
let decision = client.evaluate(
|
|
&AgentId::new("did:key:zA"),
|
|
&Action::filesystem("/x", FsMode::Read),
|
|
);
|
|
assert!(decision.is_deny(), "empty policy → deny-by-default");
|
|
}
|
|
|
|
#[test]
|
|
fn install_policy_over_ipc_then_evaluate_uses_new_rules() {
|
|
let client = PolicyClient::spawn(binary_path()).expect("spawn jclaw-policyd");
|
|
let new = Policy::new_at_version(
|
|
vec![Rule::allow("read-workspace", AgentScope::Any)
|
|
.filesystem("/workspace/**", [FsMode::Read])],
|
|
2,
|
|
);
|
|
let v = client.install_policy(new).expect("install");
|
|
assert_eq!(v, 2);
|
|
assert_eq!(client.policy_version(), 2);
|
|
|
|
let allowed = client.evaluate(
|
|
&AgentId::new("did:key:zA"),
|
|
&Action::filesystem("/workspace/notes.md", FsMode::Read),
|
|
);
|
|
assert!(allowed.is_allow());
|
|
|
|
let denied = client.evaluate(
|
|
&AgentId::new("did:key:zA"),
|
|
&Action::filesystem("/etc/passwd", FsMode::Read),
|
|
);
|
|
assert!(denied.is_deny());
|
|
}
|
|
|
|
#[test]
|
|
fn audit_log_is_visible_over_ipc() {
|
|
let client = PolicyClient::spawn(binary_path()).expect("spawn jclaw-policyd");
|
|
let _ = client.evaluate(
|
|
&AgentId::new("did:key:zA"),
|
|
&Action::filesystem("/x", FsMode::Read),
|
|
);
|
|
let _ = client.evaluate(
|
|
&AgentId::new("did:key:zA"),
|
|
&Action::filesystem("/y", FsMode::Read),
|
|
);
|
|
let log = client.audit();
|
|
assert_eq!(log.len(), 2);
|
|
assert_eq!(log[0].sequence, 1);
|
|
assert_eq!(log[1].sequence, 2);
|
|
|
|
let after = client.audit_since(1);
|
|
assert_eq!(after.len(), 1);
|
|
assert_eq!(after[0].sequence, 2);
|
|
}
|