Skip to content
Draft
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
28 changes: 28 additions & 0 deletions .github/workflows/agent-source-export.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: agent-source-export

on:
push:
branches:
- agent-source-export
pull_request:
branches:
- devin-rust-core

permissions:
contents: read

jobs:
export:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Package source
run: tar --exclude=.git --exclude=target -czf "$RUNNER_TEMP/pi_agent_rust-source.tar.gz" .
- name: Upload source
uses: actions/upload-artifact@v4
with:
name: pi-agent-rust-source
path: ${{ runner.temp }}/pi_agent_rust-source.tar.gz
if-no-files-found: error
retention-days: 1
3 changes: 3 additions & 0 deletions .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ jobs:
steps:
- name: Skip unmatched profile
if: env.SHOULD_RUN != 'true'
# The default working-directory is the not-yet-checked-out repo path, so
# this step must run from the workspace root to avoid a shell failure.
working-directory: ${{ github.workspace }}
run: echo "Skipping ${{ matrix.name }} for event/profile selection"

- name: Free disk space
Expand Down
67 changes: 67 additions & 0 deletions .github/workflows/devin-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: devin-ci

on:
workflow_dispatch:
push:
branches:
- devin-rust-core
- "devin-*"
pull_request:
paths:
- ".github/workflows/devin-ci.yml"
- "src/agent.rs"
- "src/devin/**"
- "src/lib.rs"
- "src/session.rs"
- "tests/devin_*.rs"
- "tests/fixtures/devin_cli/**"
- "Cargo.toml"
- "Cargo.lock"

permissions:
contents: read

jobs:
rust:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
CI: "true"
VCR_MODE: "playback"
RUST_BACKTRACE: "1"
CARGO_INCREMENTAL: "0"
CARGO_PROFILE_DEV_DEBUG: "line-tables-only"
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
fd-find \
libxcb1-dev \
libxcb-render0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
ripgrep
sudo ln -sf "$(command -v fdfind)" /usr/local/bin/fd

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@nightly
with:
components: clippy, rustfmt

- name: Check formatting
run: cargo fmt --all -- --check

- name: Lint default features
run: cargo clippy --all-targets -- -D warnings

- name: Test Devin contract and session policy
run: |
cargo test --test devin_contract --test devin_session_state
cargo test devin::

- name: Build release binary
run: cargo build --release
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,11 @@ src/bin/test_time.rs
/fix_tools_write.rs

# ACFS ephemeral patterns (auto-generated agent artifacts)
core
core.*
# Anchored: an unanchored `core` also excluded every nested directory named
# `core`, which silently dropped the vendored pi-mono
# packages/coding-agent/src/core/** tree the TS conformance oracle imports.
/core
/core.*
cline.mcp.json
codex.mcp.json
cursor.mcp.json
Expand Down
65 changes: 65 additions & 0 deletions docs/devin-rust-progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Pi-Devin Rust parity progress

## Baseline

- Upstream: `Dicklesworthstone/pi_agent_rust`
- Pinned commit: `590d61899ae64e172f15d919632a9134ddec6fb6`
- Upstream version: `0.1.23`
- Implementation branch: `devin-rust-core`
- Writable fork: `OnlineChefGroep/pi_agent_rust`

The pristine `--all-features` check exposed an upstream `wasm-host` failure:
21 `Future + Send` errors converge on an `asupersync::sync::MutexGuard` held
across `await` in `src/extensions.rs`. This predates Pi-Devin changes. The
default-feature gate was stopped before completion when the project policy
changed to CI-only Rust validation.

No further Cargo, rustc, clippy, rustfmt, or release builds run on the local
workstation. Rust verification is performed by GitHub Actions.

## Implemented evidence

- Four ATIF-v1.7 transcripts exported by Devin `3000.2.17` expose the same 28
function-calling tools. The installed binary at extraction time was
`3000.3.22`; no current-version transcript was available.
- All four transcripts have identical JSON-schema hashes for every tool.
- `AgentMode` and `PermissionMode` are independent, session-scoped values.
- Devin mode, sandbox, workspace, and scope state round-trips through versioned
custom session entries.
- Plan and Ask mode restrictions are policy decisions, not prompt decoration.
- Autonomous mode rejects activation without an active OS sandbox.
- Tool policy validates object arguments, classifies effects and risk, checks
workspace/scoped paths, rejects traversal and symlink escapes, and returns
allow, ask, deny, or sandbox.
- Native agent tool execution can use the same central policy gate before
approvals, extension hooks, and tool execution.
- Audit records retain per-log salted argument hashes instead of raw arguments.

## Remaining gaps

1. Expose persisted Devin state through TUI, ACP, and RPC.
2. Register full transcript-derived schemas and migrate the existing eight
tools behind the same canonical registry.
3. Implement process supervision and persistent plan/todo tools.
4. Implement managed subagents, MCP, skills, hooks, and web/browser adapters.
5. Add disabled-by-default cloud XML parsing with no direct execution path.
6. Complete file mutation hashes/diffs and persistent audit/recovery sinks.
7. Run the end-to-end repository, plan, edit, background process, subagent,
and MCP smoke test in CI.

## CI reproduction

```bash
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo test --test devin_contract
cargo test devin::
cargo test --all-targets
cargo build --release
```

The optional full-feature upstream defect remains separately reproducible with:

```bash
cargo clippy --all-targets --all-features -- -D warnings
```
40 changes: 40 additions & 0 deletions scripts/e2e/run_all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -478,9 +478,49 @@ if [[ -z "$CORRELATION_ID" ]]; then
fi
export CI_CORRELATION_ID="$CORRELATION_ID"

# Cargo test targets declared with `required-features` cannot be built by name
# unless those features are enabled: `cargo test --test <name>` hard-errors
# instead of skipping the way `--all-targets` does. Drop them from the selection
# so a feature-gated target (e.g. the loom model checker) does not fail the
# shard it is classified into.
mapfile -t FEATURE_GATED_TARGETS < <(python3 - "$PROJECT_ROOT/Cargo.toml" <<'PY' 2>/dev/null || true
import sys, tomllib

with open(sys.argv[1], "rb") as handle:
manifest = tomllib.load(handle)
for entry in manifest.get("test", []):
if entry.get("required-features") and entry.get("name"):
print(entry["name"])
PY
)

is_feature_gated_target() {
local candidate="$1"
local gated
for gated in "${FEATURE_GATED_TARGETS[@]:-}"; do
[[ "$candidate" == "$gated" ]] && return 0
done
return 1
}

if (( ${#SELECTED_UNIT_TARGETS[@]} > 0 )); then
mapfile -t SELECTED_UNIT_TARGETS < <(printf '%s\n' "${SELECTED_UNIT_TARGETS[@]}" | awk 'NF' | LC_ALL=C sort -u)
fi
if (( ${#SELECTED_UNIT_TARGETS[@]} > 0 && ${#FEATURE_GATED_TARGETS[@]} > 0 )); then
RETAINED_UNIT_TARGETS=()
for target in "${SELECTED_UNIT_TARGETS[@]}"; do
if is_feature_gated_target "$target"; then
echo "[select] unit:$target requires opt-in cargo features, skipping"
continue
fi
RETAINED_UNIT_TARGETS+=("$target")
done
if (( ${#RETAINED_UNIT_TARGETS[@]} > 0 )); then
SELECTED_UNIT_TARGETS=("${RETAINED_UNIT_TARGETS[@]}")
else
SELECTED_UNIT_TARGETS=()
fi
fi
if (( ${#SELECTED_SUITES[@]} > 0 )); then
mapfile -t SELECTED_SUITES < <(printf '%s\n' "${SELECTED_SUITES[@]}" | awk 'NF' | LC_ALL=C sort -u)
fi
Expand Down
47 changes: 43 additions & 4 deletions src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::compaction::{self, ResolvedCompactionSettings};
use crate::compaction_worker::{
CompactionAdmissionSignals, CompactionQuota, CompactionWorkerState,
};
use crate::devin::{PolicyAction, ToolPolicyEngine, ToolRequest, ToolRequestOrigin};
use crate::error::{Error, Result};
use crate::extension_events::{
BeforeAgentStartOutcome, InputEventOutcome, SessionBeforeCompactOutcome,
Expand Down Expand Up @@ -1120,6 +1121,9 @@ pub struct Agent {
/// Agent configuration.
config: AgentConfig,

/// Session-scoped Devin policy gate, shared across frontend surfaces.
tool_policy: Option<Arc<ToolPolicyEngine>>,

/// Optional extension manager for tool/event hooks.
extensions: Option<ExtensionManager>,

Expand All @@ -1146,6 +1150,7 @@ impl Agent {
provider,
tools,
config,
tool_policy: None,
extensions: None,
messages: Vec::new(),
steering_fetchers: Vec::new(),
Expand All @@ -1155,6 +1160,16 @@ impl Agent {
}
}

/// Install the central session policy gate used before any tool executes.
pub fn set_tool_policy(&mut self, policy: Arc<ToolPolicyEngine>) {
self.tool_policy = Some(policy);
}

/// Remove the session policy gate and restore legacy approval behavior.
pub fn clear_tool_policy(&mut self) {
self.tool_policy = None;
}

/// Get the current message history.
#[must_use]
pub fn messages(&self) -> &[Message] {
Expand Down Expand Up @@ -2959,9 +2974,28 @@ impl Agent {
) -> (ToolOutput, bool) {
let extensions = self.extensions.clone();

let approval_denied_output = self
.request_tool_approval(&tool_call, Arc::clone(&on_event))
.await;
let approval_denied_output = if let Some(policy) = &self.tool_policy {
let decision = policy.evaluate(&ToolRequest {
call_id: tool_call.id.clone(),
tool_name: tool_call.name.clone(),
arguments: tool_call.arguments.clone(),
origin: ToolRequestOrigin::Native,
});
match decision.action {
PolicyAction::Allow => None,
PolicyAction::Ask => {
self.request_tool_approval(&tool_call, Arc::clone(&on_event), true)
.await
}
PolicyAction::Deny => Some(Self::tool_approval_denied_output(&decision.reason)),
PolicyAction::Sandbox => Some(Self::tool_approval_denied_output(
"sandbox execution adapter is not configured; refusing unsandboxed execution",
)),
}
} else {
self.request_tool_approval(&tool_call, Arc::clone(&on_event), false)
.await
};

let (mut output, is_error) = if let Some(output) = approval_denied_output {
(output, true)
Expand Down Expand Up @@ -3007,9 +3041,14 @@ impl Agent {
&self,
tool_call: &ToolCall,
on_event: AgentEventHandler,
required: bool,
) -> Option<ToolOutput> {
let Some(approval) = &self.config.tool_approval else {
return None;
return required.then(|| {
Self::tool_approval_denied_output(
"policy requires approval but no approval handler is available",
)
});
};

let request = ToolApprovalRequest {
Expand Down
Loading
Loading