Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,16 @@ Or use `dela run` for subshell execution:
$ dela run build
```


### Allowlist.toml
The allowlist is a TOML file located at `~/.config/dela/allowlist.toml`. It stores allow and deny rules at folder, file, and task level. It gets updated when you either run a task in a new folder for the first time, or when you run `dela allow <task>` and `dela deny <task>` commands explicitly.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
## MCP Server

Dela includes an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that allows AI assistants and editors to discover and execute tasks programmatically.

The mcp executed tasks need to be already on the allowlist. The mcp server respects the allowlist, but does not give agents tools to modify it.

### Setting Up MCP in Your Editor

```sh
Expand Down
20 changes: 18 additions & 2 deletions cargo_crap_baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@
"function": "evaluate_task_against_allowlist",
"line": 75,
"cyclomatic": 17.0,
"coverage": 100.0,
"crap": 17.0
"coverage": 96.42857142857143,
"crap": 17.013165087463555
},
{
"file": "./src/commands/allow_command.rs",
Expand Down Expand Up @@ -770,6 +770,14 @@
"coverage": 100.0,
"crap": 5.0
},
{
"file": "./src/commands/mod.rs",
"function": "gate_non_interactive",
"line": 18,
"cyclomatic": 4.0,
"coverage": 70.0,
"crap": 4.432
},
{
"file": "./src/task_discovery/turbo.rs",
"function": "build_turbo_package_config_index",
Expand Down Expand Up @@ -1106,6 +1114,14 @@
"coverage": 100.0,
"crap": 3.0
},
{
"file": "./src/commands/mod.rs",
"function": "should_block",
"line": 14,
"cyclomatic": 3.0,
"coverage": 100.0,
"crap": 3.0
},
{
"file": "./src/commands/mcp.rs",
"function": "Editor::dela_marker",
Expand Down
15 changes: 10 additions & 5 deletions src/commands/allow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ use std::env;

/// Executes the 'dela allow' command to add a specific task to the allowlist.
pub fn execute(task_name: &str) -> anyhow::Result<()> {
super::gate_non_interactive("dela allow")?;
execute_inner(task_name)
}

fn execute_inner(task_name: &str) -> anyhow::Result<()> {
let current_dir = env::current_dir()
.map_err(|e| anyhow::anyhow!("Failed to get current directory: {}", e))?;
let discovered = task_discovery::discover_tasks(&current_dir);
Expand Down Expand Up @@ -128,7 +133,7 @@ test: ## Running tests
let guard = TestEnvGuard::new();
env::set_current_dir(&guard.project_dir).expect("Failed to change directory");

let result = execute("test");
let result = execute_inner("test");
assert!(result.is_ok(), "Should succeed for a single task");

// Verify it was added to the allowlist
Expand All @@ -144,7 +149,7 @@ test: ## Running tests
let guard = TestEnvGuard::new();
env::set_current_dir(&guard.project_dir).expect("Failed to change directory");

let result = execute("nonexistent");
let result = execute_inner("nonexistent");
assert!(result.is_err(), "Should fail for nonexistent task");
assert_eq!(
result.unwrap_err().to_string(),
Expand All @@ -169,7 +174,7 @@ test: ## Running tests
.write_all(makefile_content.as_bytes())
.expect("Failed to write Makefile");

let result = execute("test");
let result = execute_inner("test");
assert!(result.is_err(), "Should fail when dela is not initialized");
assert_eq!(
result.unwrap_err().to_string(),
Expand Down Expand Up @@ -203,7 +208,7 @@ test: ## Running tests
.unwrap();

// Ambiguous task name 'test' should fail
let result = execute("test");
let result = execute_inner("test");
assert!(result.is_err(), "Should fail for ambiguous task name");
assert!(
result
Expand All @@ -213,7 +218,7 @@ test: ## Running tests
);

// Suffixed/disambiguated task name should succeed
let result = execute("test-m");
let result = execute_inner("test-m");
assert!(result.is_ok(), "Should succeed with disambiguated name");

let allowlist_content =
Expand Down
15 changes: 10 additions & 5 deletions src/commands/deny.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ use std::env;

/// Executes the 'dela deny' command to add a specific task definition file to the denylist.
pub fn execute(task_name: &str) -> anyhow::Result<()> {
super::gate_non_interactive("dela deny")?;
execute_inner(task_name)
}

fn execute_inner(task_name: &str) -> anyhow::Result<()> {
let current_dir = env::current_dir()
.map_err(|e| anyhow::anyhow!("Failed to get current directory: {}", e))?;
let discovered = task_discovery::discover_tasks(&current_dir);
Expand Down Expand Up @@ -128,7 +133,7 @@ test: ## Running tests
let guard = TestEnvGuard::new();
env::set_current_dir(&guard.project_dir).expect("Failed to change directory");

let result = execute("test");
let result = execute_inner("test");
assert!(result.is_ok(), "Should succeed for a single task");

// Verify it was added to the allowlist with Deny scope and task name
Expand All @@ -144,7 +149,7 @@ test: ## Running tests
let guard = TestEnvGuard::new();
env::set_current_dir(&guard.project_dir).expect("Failed to change directory");

let result = execute("nonexistent");
let result = execute_inner("nonexistent");
assert!(result.is_err(), "Should fail for nonexistent task");
assert_eq!(
result.unwrap_err().to_string(),
Expand All @@ -169,7 +174,7 @@ test: ## Running tests
.write_all(makefile_content.as_bytes())
.expect("Failed to write Makefile");

let result = execute("test");
let result = execute_inner("test");
assert!(result.is_err(), "Should fail when dela is not initialized");
assert_eq!(
result.unwrap_err().to_string(),
Expand Down Expand Up @@ -203,7 +208,7 @@ test: ## Running tests
.unwrap();

// Ambiguous task name 'test' should fail
let result = execute("test");
let result = execute_inner("test");
assert!(result.is_err(), "Should fail for ambiguous task name");
assert!(
result
Expand All @@ -213,7 +218,7 @@ test: ## Running tests
);

// Suffixed/disambiguated task name should succeed
let result = execute("test-m");
let result = execute_inner("test-m");
assert!(result.is_ok(), "Should succeed with disambiguated name");

let allowlist_content =
Expand Down
15 changes: 15 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,18 @@ pub mod list;
pub mod mcp;
pub mod run;
pub mod run_command;

use std::io::IsTerminal;

/// Returns an error if the current session is non-interactive (no TTY).
/// This prevents scripts and agents from running `dela allow` / `dela deny`.
pub(crate) fn gate_non_interactive(command_name: &str) -> anyhow::Result<()> {
let is_terminal = std::io::stdout().is_terminal() && std::io::stdin().is_terminal();
if !is_terminal {
anyhow::bail!(
"'{}' should only be run by human users directly, and not by scripts or agents.",
command_name
);
}
Ok(())
}
10 changes: 4 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,13 +223,11 @@ async fn main() {
let result = run_command(cli.command).await;

if let Err(err) = result {
if err
.to_string()
.starts_with("dela: command or task not found")
{
eprintln!("{}", err);
let msg = err.to_string();
if msg.starts_with("dela: command or task not found") || msg.starts_with("'dela ") {
eprintln!("{}", msg);
} else {
eprintln!("Error: {}", err);
eprintln!("Error: {}", msg);
}
std::process::exit(1);
}
Expand Down
3 changes: 2 additions & 1 deletion tests/docker_noinit/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ RUN apk add --no-cache \
npm \
maven \
gradle \
docker-cli && \
docker-cli \
util-linux && \
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/v3.21/community go-task

# symlink go-task to task
Expand Down
48 changes: 41 additions & 7 deletions tests/docker_noinit/test_noinit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -844,7 +844,7 @@ echo "Initial allowlist contents:"
cat /home/testuser/.config/dela/allowlist.toml

# Test dela allow on a single valid task
dela allow print-args >/dev/null 2>&1 || {
script -qc "dela allow print-args" /dev/null >/dev/null 2>&1 || {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
echo "${RED}✗ dela allow print-args failed${NC}"
exit 1
}
Expand All @@ -865,7 +865,7 @@ else
fi

# Verify dela allow fails for nonexistent task
if dela allow nonexistent >/dev/null 2>&1; then
if script -qc "dela allow nonexistent" /dev/null >/dev/null 2>&1; then
echo "${RED}✗ dela allow nonexistent succeeded but should have failed${NC}"
exit 1
else
Expand All @@ -874,15 +874,15 @@ fi

# We have duplicate_test.json and duplicate_test.mk still present in the directory.
# So 'test' is ambiguous.
if dela allow test >/dev/null 2>&1; then
if script -qc "dela allow test" /dev/null >/dev/null 2>&1; then
echo "${RED}✗ dela allow test (ambiguous) succeeded but should have failed${NC}"
exit 1
else
echo "${GREEN}✓ dela allow test (ambiguous) failed as expected${NC}"
fi

# But dela allow test-m (disambiguated name) should succeed!
dela allow test-m >/dev/null 2>&1 || {
script -qc "dela allow test-m" /dev/null >/dev/null 2>&1 || {
echo "${RED}✗ dela allow test-m (disambiguated name) failed${NC}"
exit 1
}
Expand All @@ -892,7 +892,7 @@ echo "${GREEN}✓ dela allow test-m (disambiguated name) succeeded as expected${
echo "\nTest 32: Testing 'dela deny' command functionality"

# Deny a single valid task
dela deny another-task >/dev/null 2>&1 || {
script -qc "dela deny another-task" /dev/null >/dev/null 2>&1 || {
echo "${RED}✗ dela deny another-task failed${NC}"
exit 1
}
Expand Down Expand Up @@ -944,7 +944,7 @@ else
fi

# Verify dela deny fails for nonexistent task
if dela deny nonexistent >/dev/null 2>&1; then
if script -qc "dela deny nonexistent" /dev/null >/dev/null 2>&1; then
echo "${RED}✗ dela deny nonexistent succeeded but should have failed${NC}"
exit 1
else
Expand All @@ -954,14 +954,48 @@ fi
# Verify dela deny fails for ambiguous tasks
# We have duplicate_test.json and duplicate_test.mk still present in the directory.
# So 'test' is ambiguous.
if dela deny test >/dev/null 2>&1; then
if script -qc "dela deny test" /dev/null >/dev/null 2>&1; then
echo "${RED}✗ dela deny test (ambiguous) succeeded but should have failed${NC}"
exit 1
else
echo "${GREEN}✓ dela deny test (ambiguous) failed as expected${NC}"
fi

# Test 33: Test non-interactive blocking of 'dela allow' and 'dela deny'
echo "\nTest 33: Testing non-interactive blocking of 'dela allow' and 'dela deny'"

# Running dela allow should get blocked and print the error to stderr
exit_code=0
output=$(dela allow print-args 2>&1) || exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "${RED}✗ dela allow print-args did not fail in non-interactive session${NC}"
exit 1
fi
if echo "$output" | grep -q "'dela allow' should only be run by human users directly, and not by scripts or agents."; then
echo "${GREEN}✓ dela allow was blocked in non-interactive session as expected${NC}"
else
echo "${RED}✗ dela allow was not blocked with the expected message${NC}"
echo "Output: $output"
exit 1
fi

# Running dela deny should get blocked and print the error to stderr
exit_code=0
output=$(dela deny print-args 2>&1) || exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "${RED}✗ dela deny print-args did not fail in non-interactive session${NC}"
exit 1
fi
if echo "$output" | grep -q "'dela deny' should only be run by human users directly, and not by scripts or agents."; then
echo "${GREEN}✓ dela deny was blocked in non-interactive session as expected${NC}"
else
echo "${RED}✗ dela deny was not blocked with the expected message${NC}"
echo "Output: $output"
exit 1
fi

# Clean up test files
rm -f duplicate_test.json duplicate_test.mk list_output.txt list_output_long.txt

echo "\n${GREEN}All non-init tests completed successfully!${NC}"

Loading