Skip to content

Commit 087f044

Browse files
committed
adding runtimes
add runtime to crates
1 parent 79f9130 commit 087f044

8 files changed

Lines changed: 1158 additions & 75 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"adapters": [
3+
{
4+
"id": "coven-code",
5+
"label": "Coven Code",
6+
"executable": "coven-code",
7+
"interactive_prompt_prefix_args": [],
8+
"non_interactive_prompt_prefix_args": ["--print"],
9+
"install_hint": "npm install -g @opencoven/coven (or download a release archive from https://github.com/OpenCoven/coven-code/releases), then ensure `coven-code` is on PATH.",
10+
"system_prompt_flag": "--append-system-prompt",
11+
"model_flag": "--model",
12+
"capabilities": {
13+
"stream": true,
14+
"preassigned_session_id": true,
15+
"think": true,
16+
"speed": true
17+
},
18+
"sandbox": {
19+
"flag": "--permission-mode",
20+
"full": "bypass-permissions",
21+
"read_only": "plan"
22+
},
23+
"stream_args": {
24+
"prefix_args": ["--print", "--input-format", "stream-json", "--output-format", "stream-json"],
25+
"session_id_flag": "--session-id",
26+
"resume_flag": "--resume"
27+
},
28+
"version": "1.0.0",
29+
"homepage": "https://github.com/OpenCoven/coven-code",
30+
"description": "Coven Code adapter. One-shot via `--print <prompt>`, JSONL streaming via `--print --input-format stream-json --output-format stream-json` (long-lived: one turn per stdin user frame, exits on stdin EOF; the positional prompt is ignored in this mode, matching Claude Code). Session pre-assignment/resume via `--session-id`/`--resume`. Think maps to `--thinking <TOKENS>`, speed to `--effort <LEVEL>`. Interactive launches must not append a prompt: a positional prompt switches coven-code into print mode."
31+
}
32+
]
33+
}

src-rust/Cargo.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-rust/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ tokio-tungstenite = { version = "0.24", features = ["native-tls-vendored"] }
9999
# JSON Schema
100100
schemars = { version = "0.8", features = ["derive"] }
101101

102+
# Runtime registry
103+
coven-runtime-spec = { git = "https://github.com/OpenCoven/coven-runtimes", rev = "9af3e4cf2fe66e95aa1a3addc867524822ea28ab" }
104+
coven-runtime-registry = { git = "https://github.com/OpenCoven/coven-runtimes", rev = "9af3e4cf2fe66e95aa1a3addc867524822ea28ab" }
105+
102106
# Workspace crates
103107
claurst-core = { path = "crates/core" }
104108
claurst-acp = { path = "crates/acp" }

src-rust/crates/cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ urlencoding = "2"
4949
xxhash-rust = { version = "0.8", features = ["xxh64"] }
5050

5151
[dev-dependencies]
52+
coven-runtime-spec = { workspace = true }
5253
tempfile = { workspace = true }
5354

5455
[build-dependencies]

src-rust/crates/cli/src/main.rs

Lines changed: 172 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
mod codex_oauth_flow;
1212
mod headless;
1313
mod oauth_flow;
14+
mod stream_mode;
1415
mod upgrade;
1516

1617
use headless::{RunOutcome, SessionBrief};
@@ -881,6 +882,31 @@ async fn main() -> anyhow::Result<()> {
881882

882883
// --print / headless mode
883884
if is_headless {
885+
// Long-lived stream-json mode: the Coven runtime stream protocol.
886+
// Turns arrive as stdin JSON frames; the process exits on stdin EOF.
887+
// GitHub App runs (--context) never use this path — their prompt
888+
// comes from the session brief.
889+
if cli.input_format == CliInputFormat::StreamJson && github_context.is_none() {
890+
if cli.prompt.is_some() {
891+
eprintln!(
892+
"Warning: positional prompt is ignored with --input-format stream-json; \
893+
send user frames on stdin."
894+
);
895+
}
896+
let run = stream_mode::run_stream_loop(stream_mode::StreamLoopParams {
897+
client,
898+
tools,
899+
tool_ctx,
900+
query_config,
901+
cost_tracker,
902+
model: claurst_api::effective_model_for_config(&config, &model_registry),
903+
resume_id: cli.resume.clone(),
904+
})
905+
.await;
906+
cron_cancel.cancel();
907+
return run;
908+
}
909+
884910
// For a coven-github run, configure git auth from COVEN_GIT_TOKEN before
885911
// the agent starts. This installs an env-backed credential helper in the
886912
// workspace (the token stays in the environment, never on disk) so the
@@ -1606,75 +1632,29 @@ async fn run_headless(
16061632
use tokio_util::sync::CancellationToken;
16071633

16081634
// Build initial messages list from input.
1609-
// --input-format stream-json: stdin is newline-delimited JSON, each line is
1610-
// {"role":"user"|"assistant","content":"..."} (mirrors TS --input-format stream-json).
1611-
// --input-format text (default): read prompt from positional arg or entire stdin as text.
1612-
let mut messages: Vec<claurst_core::types::Message> =
1613-
if cli.input_format == CliInputFormat::StreamJson {
1614-
use tokio::io::{self, AsyncBufReadExt, BufReader};
1615-
let stdin = io::stdin();
1616-
let mut reader = BufReader::new(stdin);
1617-
let mut line = String::new();
1618-
let mut parsed: Vec<claurst_core::types::Message> = Vec::new();
1619-
loop {
1620-
line.clear();
1621-
let n = reader.read_line(&mut line).await?;
1622-
if n == 0 {
1623-
break;
1624-
}
1625-
let trimmed = line.trim();
1626-
if trimmed.is_empty() {
1627-
continue;
1628-
}
1629-
match serde_json::from_str::<serde_json::Value>(trimmed) {
1630-
Ok(v) => {
1631-
let role = v.get("role").and_then(|r| r.as_str()).unwrap_or("user");
1632-
let content = v
1633-
.get("content")
1634-
.and_then(|c| c.as_str())
1635-
.unwrap_or("")
1636-
.to_string();
1637-
if role == "assistant" {
1638-
parsed.push(claurst_core::types::Message::assistant(content));
1639-
} else {
1640-
parsed.push(claurst_core::types::Message::user(content));
1641-
}
1642-
}
1643-
Err(e) => {
1644-
eprintln!(
1645-
"Warning: skipping malformed JSON line: {} ({:?})",
1646-
trimmed, e
1647-
);
1648-
}
1649-
}
1650-
}
1651-
if parsed.is_empty() {
1652-
// Also check positional arg as fallback
1653-
if let Some(ref p) = cli.prompt {
1654-
parsed.push(claurst_core::types::Message::user(p.clone()));
1655-
}
1656-
}
1657-
parsed
1635+
// --input-format stream-json runs never reach here: main routes them to
1636+
// stream_mode::run_stream_loop (the long-lived Coven runtime protocol)
1637+
// before calling run_headless. This path reads the prompt from the
1638+
// positional arg, the session brief, or stdin as plain text.
1639+
let mut messages: Vec<claurst_core::types::Message> = {
1640+
let prompt = if let Some(ref p) = cli.prompt {
1641+
p.clone()
1642+
} else if let Some(brief) = github_context {
1643+
brief.to_prompt()
16581644
} else {
1659-
// Plain text mode
1660-
let prompt = if let Some(ref p) = cli.prompt {
1661-
p.clone()
1662-
} else if let Some(brief) = github_context {
1663-
brief.to_prompt()
1664-
} else {
1665-
use tokio::io::{self, AsyncReadExt};
1666-
let mut stdin = io::stdin();
1667-
let mut buf = String::new();
1668-
stdin.read_to_string(&mut buf).await?;
1669-
buf.trim().to_string()
1670-
};
1645+
use tokio::io::{self, AsyncReadExt};
1646+
let mut stdin = io::stdin();
1647+
let mut buf = String::new();
1648+
stdin.read_to_string(&mut buf).await?;
1649+
buf.trim().to_string()
1650+
};
16711651

1672-
if prompt.is_empty() {
1673-
anyhow::bail!("No prompt provided. Use --print <prompt> or pipe text to stdin.");
1674-
}
1652+
if prompt.is_empty() {
1653+
anyhow::bail!("No prompt provided. Use --print <prompt> or pipe text to stdin.");
1654+
}
16751655

1676-
vec![claurst_core::types::Message::user(prompt)]
1677-
};
1656+
vec![claurst_core::types::Message::user(prompt)]
1657+
};
16781658

16791659
// --prefill: inject a partial assistant turn before the query so the model
16801660
// continues from that text (mirrors TS --prefill flag).
@@ -5475,4 +5455,130 @@ mod tests {
54755455
// NOTE: the coven-github headless-contract types + behavior moved to the
54765456
// `headless` module; their conformance tests live in `headless.rs`
54775457
// (`#[cfg(test)] mod tests`), pinned to the vendored golden fixtures.
5458+
5459+
// ── coven-runtimes manifest drift guard ─────────────────────────────────
5460+
//
5461+
// `spec/runtime-manifest/coven-code.json` is the runtime adapter manifest
5462+
// accepted into the OpenCoven/coven-runtimes canonical registry. Registry
5463+
// versions are immutable: if one of these tests fails, a CLI flag the
5464+
// manifest depends on drifted. Fix the flag or publish a new manifest
5465+
// version and re-accept it in coven-runtimes (`conjure registry add`) —
5466+
// do not silently edit the shipped 1.0.0 definition.
5467+
5468+
const RUNTIME_MANIFEST: &str =
5469+
include_str!("../../../../spec/runtime-manifest/coven-code.json");
5470+
5471+
fn runtime_adapter() -> coven_runtime_spec::RuntimeAdapter {
5472+
let manifest = coven_runtime_spec::AdapterManifest::from_json(RUNTIME_MANIFEST)
5473+
.expect("runtime manifest parses");
5474+
assert_eq!(manifest.adapters.len(), 1, "exactly one adapter expected");
5475+
manifest.adapters.into_iter().next().expect("one adapter")
5476+
}
5477+
5478+
#[test]
5479+
fn runtime_manifest_passes_spec_validation() {
5480+
let adapter = runtime_adapter();
5481+
let errors = coven_runtime_spec::validate_adapter(&adapter);
5482+
assert!(errors.is_empty(), "spec violations: {errors:?}");
5483+
assert_eq!(adapter.id, "coven-code");
5484+
assert_eq!(adapter.executable, "coven-code");
5485+
}
5486+
5487+
#[test]
5488+
fn runtime_manifest_declares_full_stream_capabilities() {
5489+
let caps = runtime_adapter().capabilities;
5490+
assert!(caps.stream, "stream mode is implemented in stream_mode.rs");
5491+
assert!(caps.preassigned_session_id);
5492+
assert!(caps.think, "think maps to --thinking");
5493+
assert!(caps.speed, "speed maps to --effort");
5494+
}
5495+
5496+
#[test]
5497+
fn runtime_manifest_stream_launch_args_parse() {
5498+
let adapter = runtime_adapter();
5499+
let stream = adapter.stream_args.as_ref().expect("stream_args declared");
5500+
5501+
let mut argv: Vec<String> = vec!["coven-code".into()];
5502+
argv.extend(stream.prefix_args.iter().cloned());
5503+
let session_flag = stream
5504+
.session_id_flag
5505+
.as_deref()
5506+
.expect("session_id_flag declared");
5507+
argv.push(session_flag.to_string());
5508+
argv.push("00000000-0000-0000-0000-000000000000".into());
5509+
argv.push(adapter.model_flag.clone().expect("model_flag declared"));
5510+
argv.push("claude-sonnet-4-5".into());
5511+
argv.push(
5512+
adapter
5513+
.system_prompt_flag
5514+
.clone()
5515+
.expect("system_prompt_flag declared"),
5516+
);
5517+
argv.push("identity preamble".into());
5518+
5519+
let cli = Cli::try_parse_from(&argv).expect("stream launch argv parses");
5520+
assert!(cli.print);
5521+
assert_eq!(cli.input_format, CliInputFormat::StreamJson);
5522+
assert_eq!(cli.output_format, CliOutputFormat::StreamJson);
5523+
assert_eq!(
5524+
cli.session_id_flag.as_deref(),
5525+
Some("00000000-0000-0000-0000-000000000000")
5526+
);
5527+
}
5528+
5529+
#[test]
5530+
fn runtime_manifest_resume_flag_parses() {
5531+
let adapter = runtime_adapter();
5532+
let stream = adapter.stream_args.as_ref().expect("stream_args declared");
5533+
let resume_flag = stream.resume_flag.as_deref().expect("resume_flag declared");
5534+
5535+
let mut argv: Vec<String> = vec!["coven-code".into()];
5536+
argv.extend(stream.prefix_args.iter().cloned());
5537+
argv.push(resume_flag.to_string());
5538+
argv.push("11111111-1111-1111-1111-111111111111".into());
5539+
5540+
let cli = Cli::try_parse_from(&argv).expect("resume argv parses");
5541+
assert_eq!(
5542+
cli.resume.as_deref(),
5543+
Some("11111111-1111-1111-1111-111111111111")
5544+
);
5545+
}
5546+
5547+
#[test]
5548+
fn runtime_manifest_sandbox_mapping_parses_for_both_policies() {
5549+
let adapter = runtime_adapter();
5550+
let sandbox = adapter.sandbox.as_ref().expect("sandbox declared");
5551+
5552+
for (permission, expected) in [
5553+
(
5554+
coven_runtime_spec::Permission::Full,
5555+
CliPermissionMode::BypassPermissions,
5556+
),
5557+
(
5558+
coven_runtime_spec::Permission::ReadOnly,
5559+
CliPermissionMode::Plan,
5560+
),
5561+
] {
5562+
let mut argv: Vec<String> = vec!["coven-code".into()];
5563+
argv.extend(adapter.non_interactive_prompt_prefix_args.iter().cloned());
5564+
argv.extend(sandbox.args(permission));
5565+
argv.push("do the thing".into());
5566+
5567+
let cli = Cli::try_parse_from(&argv)
5568+
.unwrap_or_else(|e| panic!("{permission:?} sandbox argv rejected: {e}"));
5569+
assert_eq!(cli.permission_mode, expected);
5570+
}
5571+
}
5572+
5573+
#[test]
5574+
fn runtime_manifest_one_shot_launch_parses() {
5575+
let adapter = runtime_adapter();
5576+
let mut argv: Vec<String> = vec!["coven-code".into()];
5577+
argv.extend(adapter.non_interactive_prompt_prefix_args.iter().cloned());
5578+
argv.push("hello".into());
5579+
5580+
let cli = Cli::try_parse_from(&argv).expect("one-shot argv parses");
5581+
assert!(cli.print);
5582+
assert_eq!(cli.prompt.as_deref(), Some("hello"));
5583+
}
54785584
}

0 commit comments

Comments
 (0)