Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/rune-artifact/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The tree is rooted at `agent/`:

- `agent/manifest.json` — `format: rune-artifact-v1`, optional `initiative: open-agent`, `tag` (from `rune artifact build --tag`, default `latest`), agent name, model, RFC3339 `created_at`, and a sorted `files` list (paths relative to `agent/`, excluding `manifest.json`).
- `agent/Runefile`.
- `agent/tools/**`, `agent/skills/**`, `agent/schemas/**` — included when present under the source agent directory (so scripts, WASM modules, skill markdown, and JSON schemas ship with the bundle).

## Loading with `AgentPackage`

Expand Down
8 changes: 6 additions & 2 deletions crates/rune-artifact/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
//! Portable **rune-artifact** bundles: a versioned `manifest.json` plus packable agent files under
//! an `agent/` tree (same layout [`rune_spec::AgentPackage::load`] expects).
//!
//! Packed paths (relative to `agent/`) include `Runefile` and files under `tools/`, `skills/`, and
//! `schemas/` so process tools, local skills, and JSON schemas referenced from tool YAML are
//! shipped with the bundle.
//!
//! **Open Agent Initiative (OAI)** is the project’s name for this portable agent bundle format.
//! It is **not** an [Open Container Initiative](https://opencontainers.org/) (OCI) **container**
//! image; the payload is a deterministic bundle for agents, not a container runtime image.
//!
//! **Remote skills:** refs listed in `Runefile` but not present under `skills/` are **not**
//! embedded (MVP packs on-disk files only). After load, `missing_skills` is populated the same as
//! loading from a dev tree.
//! embedded (only on-disk files under `skills/` are packed). After load, `missing_skills` is
//! populated the same as loading from a dev tree.
//!
//! Typical flow: [`materialize_agent_bundle`](crate::materialize_agent_bundle) → ship directory or
//! [`export_bundle_to_tar_gz_file`](crate::export_bundle_to_tar_gz_file) for a `.tar.gz` →
Expand Down
28 changes: 25 additions & 3 deletions crates/rune-artifact/src/pack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,16 @@ struct PreparedBundle {
files: Vec<(String, Vec<u8>)>,
}

/// Pack `Runefile` plus files under `tools/`, `skills/`, and `schemas/` (relative to agent root).
fn allowed_relative_path(rel: &str) -> bool {
rel == "Runefile"
if rel.contains("..") || rel.starts_with('/') {
return false;
}
if rel == "Runefile" {
return true;
}
const PREFIXES: &[&str] = &["tools/", "skills/", "schemas/"];
PREFIXES.iter().any(|p| rel.starts_with(*p))
}

/// Collect (full_path, unix-style relative path) for every file that belongs in the artifact.
Expand Down Expand Up @@ -329,9 +337,17 @@ models:
assert!(!allowed_relative_path("README.md"));
assert!(!allowed_relative_path(".gitignore"));
assert!(!allowed_relative_path("workflow.yaml"));
assert!(!allowed_relative_path("tools/search.yaml"));
assert!(!allowed_relative_path("skills/x.md"));
assert!(!allowed_relative_path("src/main.rs"));
assert!(!allowed_relative_path("../Runefile"));
assert!(!allowed_relative_path("tools/../Runefile"));
}

#[test]
fn allows_tools_skills_schemas_trees() {
assert!(allowed_relative_path("tools/search.yaml"));
assert!(allowed_relative_path("tools/sum.py"));
assert!(allowed_relative_path("skills/foo/SKILL.md"));
assert!(allowed_relative_path("schemas/in.json"));
}

// --- collect_packable_files ---
Expand All @@ -348,11 +364,14 @@ models:
let dir = tempfile::tempdir().unwrap();
write_minimal_agent(dir.path());
std::fs::write(dir.path().join("README.md"), "# readme").unwrap();
std::fs::create_dir_all(dir.path().join("tools")).unwrap();
std::fs::write(dir.path().join("tools/sum.py"), b"print(1)").unwrap();

let files = collect_packable_files(dir.path()).unwrap();
let rels: Vec<&str> = files.iter().map(|(_, r)| r.as_str()).collect();

assert!(rels.contains(&"Runefile"));
assert!(rels.contains(&"tools/sum.py"));
assert!(!rels.contains(&"README.md"));
}

Expand All @@ -379,12 +398,15 @@ models:
write_minimal_agent(dir.path());
std::fs::write(dir.path().join("README.md"), "# readme").unwrap();
std::fs::write(dir.path().join(".gitignore"), "target/\n").unwrap();
std::fs::create_dir_all(dir.path().join("tools")).unwrap();
std::fs::write(dir.path().join("tools/sum.py"), b"# tool").unwrap();

let mut buf = Vec::new();
pack_agent_dir(dir.path(), &mut buf, PackOptions::default()).unwrap();

let paths = tar_entry_paths(&buf);
assert!(paths.iter().any(|p| p == "agent/Runefile"));
assert!(paths.iter().any(|p| p == "agent/tools/sum.py"));
assert!(!paths.iter().any(|p| p.contains("README.md")));
assert!(!paths.iter().any(|p| p.contains(".gitignore")));
}
Expand Down
2 changes: 2 additions & 0 deletions crates/rune-cmd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ tracing-opentelemetry = { workspace = true }
anyhow = { workspace = true }
uuid = { workspace = true }
reqwest = { workspace = true }
futures-util = { workspace = true }
tokio-util = { workspace = true, features = ["io"] }
tempfile = { workspace = true }
nix = { workspace = true }
chrono = { workspace = true }
Expand Down
43 changes: 15 additions & 28 deletions crates/rune-cmd/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ pub enum Command {
#[command(subcommand)]
cmd: DaemonCommand,
},
/// Stop a deployment (scale to 0 replicas)
Stop(StopAgentArgs),
/// Remove a deployment
Rm(RmAgentArgs),
/// Stop a deployment by agent name (scale to 0 replicas)
Stop(AgentStopArgs),
/// Remove a deployment by agent name
Rm(AgentRmArgs),
/// Manage Raft cluster membership
Cluster {
#[command(subcommand)]
Expand Down Expand Up @@ -149,19 +149,20 @@ pub struct AgentRmArgs {

#[derive(clap::Args)]
pub struct RunArgs {
/// Path to a Runefile (YAML). Mutually exclusive with AGENT_SPEC.
#[arg(short = 'f', long = "file", conflicts_with = "agent_spec")]
pub file: Option<std::path::PathBuf>,

/// Local path (agent dir or Runefile), artifact agent name from `rune artifact ls`, or git://...
#[arg(required_unless_present = "file")]
pub agent_spec: Option<String>,
/// Agent name (artifact manifest `agent_name` under ~/.rune/artifacts; see `rune artifact build` / `rune artifact ls`)
pub name: String,
/// Bundle tag (materialized dir or `{name}-{tag}.tar.gz`)
#[arg(long, default_value = "latest")]
pub tag: String,
#[arg(long, default_value = "dev")]
pub namespace: String,
#[arg(long, default_value = "stable")]
pub alias: String,
#[arg(long, default_value = "http://localhost:8081")]
pub control_plane: String,
/// When the artifact is not local, GET `{base}/{name}-{tag}.tar.gz` (also env RUNE_ARTIFACT_REGISTRY_URL)
#[arg(long, env = "RUNE_ARTIFACT_REGISTRY_URL")]
pub artifact_registry: Option<String>,
}

#[derive(clap::Args)]
Expand All @@ -171,6 +172,9 @@ pub struct ChatArgs {
/// Gateway base URL (default matches `rune daemon start`)
#[arg(long, default_value = "http://localhost:8080")]
pub gateway: String,
/// Stream SSE events (token, tool_start, tool_done, thinking, …). Requires ANTHROPIC_API_KEY or OPENAI_API_KEY on the gateway.
#[arg(long)]
pub stream: bool,
}

#[derive(clap::Args)]
Expand Down Expand Up @@ -231,23 +235,6 @@ pub struct StopArgs {
pub pid_file: String,
}

#[derive(clap::Args)]
pub struct StopAgentArgs {
pub deployment_id: String,
#[arg(long, default_value = "http://localhost:8081")]
pub control_plane: String,
}

#[derive(clap::Args)]
pub struct RmAgentArgs {
pub deployment_id: String,
/// Force delete even when cascade fails (disables foreign key checks)
#[arg(long)]
pub force: bool,
#[arg(long, default_value = "http://localhost:8081")]
pub control_plane: String,
}

#[derive(Subcommand)]
pub enum DaemonCommand {
/// Launch the gateway and control-plane servers
Expand Down
22 changes: 0 additions & 22 deletions crates/rune-cmd/commands/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,28 +50,6 @@ pub fn resolve_agent_source(
}
}

/// `rune run -f /path/to/Runefile.yaml`
pub fn resolve_agent_from_runefile_path(
path: &Path,
) -> Result<(Option<tempfile::TempDir>, rune_spec::AgentPackage)> {
use rune_spec::AgentPackage;
let pkg = AgentPackage::load_runefile(path)?;
Ok((None, pkg))
}

/// Local path, `git://...`, or stored artifact name (see `rune artifact ls`).
pub fn resolve_agent_source_or_artifact(
agent_spec: &str,
) -> Result<(Option<tempfile::TempDir>, rune_spec::AgentPackage)> {
if agent_spec.starts_with("git://") {
return resolve_agent_source(agent_spec);
}
if Path::new(agent_spec).exists() {
return resolve_agent_source(agent_spec);
}
crate::commands::artifact::load_by_stored_agent_name(agent_spec)
}

// ---------------------------------------------------------------------------
// Runtime management helpers
// ---------------------------------------------------------------------------
Expand Down
Loading
Loading