mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-07 16:19:09 +00:00
Compare commits
3 commits
98fa659303
...
421290d066
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
421290d066 | ||
|
|
a52ebed77c | ||
|
|
2335ac59c0 |
12 changed files with 238 additions and 20 deletions
2
.github/CODEOWNERS
vendored
Normal file
2
.github/CODEOWNERS
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Global review assignment
|
||||
* @amoussa1229
|
||||
36
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
name: Bug Report
|
||||
description: Report a defect or unexpected behavior.
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to file a bug report.
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
description: A concise description of the bug.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Reproduction
|
||||
description: Steps to reproduce the behavior, including code or commands if applicable.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: What you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Python version, OS, and any relevant dependency versions.
|
||||
validations:
|
||||
required: false
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Sea Haven Security
|
||||
url: mailto:security@seahavenind.com
|
||||
about: Report security concerns privately.
|
||||
29
.github/ISSUE_TEMPLATE/feature.yml
vendored
Normal file
29
.github/ISSUE_TEMPLATE/feature.yml
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
name: Feature Request
|
||||
description: Propose a new feature or enhancement.
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for suggesting an improvement.
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
description: What is the feature or enhancement?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: motivation
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: Why is this needed? What problem does it solve?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives
|
||||
description: Any alternative approaches you considered.
|
||||
validations:
|
||||
required: false
|
||||
29
.github/ISSUE_TEMPLATE/task.yml
vendored
Normal file
29
.github/ISSUE_TEMPLATE/task.yml
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
name: Task
|
||||
description: Track a maintenance, documentation, or engineering task.
|
||||
labels: [task]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Use this template for work that is not a bug or feature request.
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
description: What needs to be done?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: acceptance
|
||||
attributes:
|
||||
label: Acceptance criteria
|
||||
description: How will we know this task is complete?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Notes
|
||||
description: Any additional context, dependencies, or links.
|
||||
validations:
|
||||
required: false
|
||||
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
## Summary
|
||||
|
||||
<!-- What changed and why — 1-3 sentences. Explain the motivation, not just the diff. -->
|
||||
|
||||
## Validation
|
||||
|
||||
<!-- How you verified it works — commands run, steps taken, screenshots if UI. -->
|
||||
|
||||
## Tests
|
||||
|
||||
<!-- What tests were added, updated, or run. If no automated tests, explain manual testing. -->
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Anything reviewers should know — migration steps, deploy order, follow-ups, breaking changes. Omit this section if empty. -->
|
||||
13
README.md
13
README.md
|
|
@ -13,11 +13,12 @@
|
|||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://opensource.org/licenses/MIT" target="_blank"><img src="https://img.shields.io/github/license/langchain-ai/open-swe" alt="License"></a>
|
||||
<a href="https://github.com/langchain-ai/open-swe/stargazers" target="_blank"><img src="https://img.shields.io/github/stars/langchain-ai/open-swe" alt="GitHub Stars"></a>
|
||||
<a href="https://github.com/Sea-Haven-Industries/open-swe/actions/workflows/ci.yml" target="_blank"><img src="https://github.com/Sea-Haven-Industries/open-swe/actions/workflows/ci.yml/badge.svg?branch=dev" alt="CI"></a>
|
||||
<a href="https://opensource.org/licenses/MIT" target="_blank"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
||||
<a href="https://www.python.org/" target="_blank"><img src="https://img.shields.io/badge/Python-3.11+-3776AB.svg" alt="Python 3.11+"></a>
|
||||
<a href="https://www.typescriptlang.org/" target="_blank"><img src="https://img.shields.io/badge/TypeScript-6.0+-3178C6.svg" alt="TypeScript 6.0+"></a>
|
||||
<a href="https://github.com/langchain-ai/langgraph" target="_blank"><img src="https://img.shields.io/badge/Built%20on-LangGraph-blue" alt="Built on LangGraph"></a>
|
||||
<a href="https://github.com/langchain-ai/deepagents" target="_blank"><img src="https://img.shields.io/badge/Built%20on-Deep%20Agents-blue" alt="Built on Deep Agents"></a>
|
||||
<a href="https://x.com/langchain" target="_blank"><img src="https://img.shields.io/twitter/url/https/twitter.com/langchain.svg?style=social&label=Follow%20%40LangChain" alt="Twitter / X"></a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
|
@ -27,7 +28,7 @@ Elite engineering orgs like Stripe, Ramp, and Coinbase are building their own in
|
|||
Open SWE is the open-source version of this pattern. Built on [LangGraph](https://langchain-ai.github.io/langgraph/) and [Deep Agents](https://github.com/langchain-ai/deepagents), it gives you the same architecture those companies built internally: cloud sandboxes, Slack and Linear invocation, subagent orchestration, and automatic PR creation — ready to customize for your own codebase and workflows.
|
||||
|
||||
> [!NOTE]
|
||||
> 💬 Read the **announcement blog post [here](https://blog.langchain.com/open-swe-an-open-source-framework-for-internal-coding-agents/)**
|
||||
> Read the **announcement blog post [here](https://blog.langchain.com/open-swe-an-open-source-framework-for-internal-coding-agents/)**
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -103,7 +104,7 @@ Open SWE's orchestration has two layers:
|
|||
All three companies in the article converge on **Slack as the primary invocation surface**. Open SWE does the same:
|
||||
|
||||
- **Slack** — Mention the bot in any thread. Supports `repo:owner/name` syntax to specify which repo to work on. The agent replies in-thread with status updates and PR links.
|
||||
- **Linear** — Comment `@openswe` on any issue. The agent reads the full issue context, reacts with 👀 to acknowledge, and posts results back as comments.
|
||||
- **Linear** — Comment `@openswe` on any issue. The agent reacts with 👀 to acknowledge, reads the full issue context, and posts results back as comments.
|
||||
- **GitHub** — Tag `@openswe` in PR comments on agent-created PRs to have it address review feedback and push fixes to the same branch.
|
||||
|
||||
Each invocation creates a deterministic thread ID, so follow-up messages on the same issue or thread route to the same running agent.
|
||||
|
|
@ -136,7 +137,7 @@ This is an area where you can extend Open SWE for your org: add deterministic CI
|
|||
## Features
|
||||
|
||||
- **Trigger from Linear, Slack, or GitHub** — mention `@openswe` in a comment to kick off a task
|
||||
- **Instant acknowledgement** — reacts with 👀 the moment it picks up your message
|
||||
- **Instant acknowledgement** — acknowledges the moment it picks up your message
|
||||
- **Message it while it's running** — send follow-up messages mid-task and it'll pick them up before its next step
|
||||
- **Run multiple tasks in parallel** — each task runs in its own isolated cloud sandbox
|
||||
- **GitHub OAuth built-in** — authenticates with your GitHub account automatically
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# Security Policy
|
||||
|
||||
For any security concerns, please contact us at security@langchain.dev.
|
||||
For any security concerns, please contact us at security@seahavenind.com.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import asyncio
|
|||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import threading
|
||||
|
|
@ -472,22 +473,44 @@ async def _approval_state(request: ToolCallRequest, change: WorkflowPushChange)
|
|||
|
||||
async def _run_with_workflow_token(
|
||||
thread_id: str,
|
||||
request: ToolCallRequest,
|
||||
run: Callable[[], Awaitable[ToolMessage | Command]],
|
||||
) -> ToolMessage | Command:
|
||||
sandbox_type = os.getenv("SANDBOX_TYPE", "langsmith")
|
||||
if sandbox_type != "langsmith":
|
||||
return await run()
|
||||
|
||||
elevated = await refresh_proxy_token(
|
||||
thread_id, permissions=WORKFLOW_RUNTIME_PROXY_TOKEN_PERMISSIONS
|
||||
)
|
||||
if not elevated:
|
||||
logger.error(
|
||||
"Workflow push approved for thread %s, but proxy token elevation to workflows:write "
|
||||
"failed; the sandbox cannot push workflow files without an elevated token.",
|
||||
thread_id,
|
||||
)
|
||||
error_message = ToolMessage(
|
||||
content=json.dumps(
|
||||
{
|
||||
"status": "error",
|
||||
"error_type": "WorkflowPushElevationFailed",
|
||||
"error": (
|
||||
"Workflow push approved, but the sandbox could not obtain a "
|
||||
"workflows-scoped token. Please retry the push or check the "
|
||||
"GitHub proxy / token minting configuration."
|
||||
),
|
||||
}
|
||||
),
|
||||
tool_call_id="",
|
||||
status="error",
|
||||
)
|
||||
return _tool_message_for_request(error_message, request)
|
||||
try:
|
||||
return await run()
|
||||
finally:
|
||||
if elevated:
|
||||
restored = await refresh_proxy_token(
|
||||
thread_id, permissions=RUNTIME_PROXY_TOKEN_PERMISSIONS
|
||||
)
|
||||
if not restored:
|
||||
await refresh_proxy_token(
|
||||
thread_id, permissions=BASE_RUNTIME_PROXY_TOKEN_PERMISSIONS
|
||||
)
|
||||
restored = await refresh_proxy_token(thread_id, permissions=RUNTIME_PROXY_TOKEN_PERMISSIONS)
|
||||
if not restored:
|
||||
await refresh_proxy_token(thread_id, permissions=BASE_RUNTIME_PROXY_TOKEN_PERMISSIONS)
|
||||
|
||||
|
||||
class WorkflowPushGuardMiddleware(AgentMiddleware):
|
||||
|
|
@ -519,7 +542,7 @@ class WorkflowPushGuardMiddleware(AgentMiddleware):
|
|||
state = await _approval_state(request, change)
|
||||
if state == "approved" and thread_id:
|
||||
safe_request = _override_execute_command(request, change.fixed_command)
|
||||
return await _run_with_workflow_token(thread_id, lambda: handler(safe_request))
|
||||
return await _run_with_workflow_token(thread_id, request, lambda: handler(safe_request))
|
||||
return _tool_message_for_request(
|
||||
_blocked_message(change, already_rejected=state == "rejected"), request
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
|
@ -273,3 +274,77 @@ async def test_non_workflow_push_runs_without_approval(monkeypatch: pytest.Monke
|
|||
assert called is True
|
||||
assert isinstance(result, ToolMessage)
|
||||
assert result.content == "pushed"
|
||||
|
||||
|
||||
async def test_approved_workflow_push_aborts_when_elevation_fails(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
guard.SANDBOX_BACKENDS["thread-1"] = _Backend()
|
||||
|
||||
async def fake_approved(thread_id: str, fingerprint: str) -> bool:
|
||||
return True
|
||||
|
||||
refresh_calls: list[dict[str, str]] = []
|
||||
|
||||
async def fake_refresh(*args: Any, **kwargs: Any) -> bool:
|
||||
refresh_calls.append(dict(kwargs.get("permissions", {})))
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(guard, "workflow_push_approved", fake_approved)
|
||||
monkeypatch.setattr(guard, "refresh_proxy_token", fake_refresh)
|
||||
|
||||
called = False
|
||||
request = _Request()
|
||||
|
||||
async def handler(_request: Any) -> ToolMessage:
|
||||
nonlocal called
|
||||
called = True
|
||||
return ToolMessage(content="pushed", tool_call_id="call-1")
|
||||
|
||||
result = await guard.WorkflowPushGuardMiddleware().awrap_tool_call(request, handler)
|
||||
|
||||
assert called is False
|
||||
assert isinstance(result, ToolMessage)
|
||||
assert result.tool_call_id == "call-1"
|
||||
assert result.status == "error"
|
||||
payload = json.loads(str(result.content))
|
||||
assert payload["status"] == "error"
|
||||
assert payload["error_type"] == "WorkflowPushElevationFailed"
|
||||
assert "workflows-scoped token" in payload["error"]
|
||||
assert len(refresh_calls) == 1
|
||||
assert refresh_calls[0].get("workflows") == "write"
|
||||
|
||||
|
||||
async def test_approved_workflow_push_runs_on_non_langsmith_providers(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
guard.SANDBOX_BACKENDS["thread-1"] = _Backend()
|
||||
|
||||
async def fake_approved(thread_id: str, fingerprint: str) -> bool:
|
||||
return True
|
||||
|
||||
refresh_calls: list[dict[str, str]] = []
|
||||
|
||||
async def fake_refresh(*args: Any, **kwargs: Any) -> bool:
|
||||
refresh_calls.append(dict(kwargs.get("permissions", {})))
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(guard, "workflow_push_approved", fake_approved)
|
||||
monkeypatch.setattr(guard, "refresh_proxy_token", fake_refresh)
|
||||
monkeypatch.setattr(guard, "os", os)
|
||||
|
||||
called = False
|
||||
|
||||
async def handler(request: Any) -> ToolMessage:
|
||||
nonlocal called
|
||||
called = True
|
||||
return ToolMessage(content="pushed", tool_call_id=request.tool_call["id"])
|
||||
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setenv("SANDBOX_TYPE", "local")
|
||||
result = await guard.WorkflowPushGuardMiddleware().awrap_tool_call(_Request(), handler)
|
||||
|
||||
assert called is True
|
||||
assert isinstance(result, ToolMessage)
|
||||
assert result.content == "pushed"
|
||||
assert refresh_calls == []
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ import { StrictMode } from "react"
|
|||
import { hydrateRoot } from "react-dom/client"
|
||||
import { registerSW } from "virtual:pwa-register"
|
||||
|
||||
// prompt mode: a new SW installs in the background and takes over on the next
|
||||
// load, so deploys never reload a tab mid-agent-run. Skip in dev — the SW
|
||||
// precaches production-only build artifacts that 404 against the dev server.
|
||||
// autoUpdate: vite-plugin-pwa generates a SW that calls skipWaiting and
|
||||
// clientsClaim, so a new deploy activates for existing tabs. Open dashboard
|
||||
// tabs may reload when the service worker takes over; this is acceptable for
|
||||
// a dev/internal dashboard where silent updates are preferred over stale
|
||||
// bundles. Skip in dev — the SW precaches production-only build artifacts that
|
||||
// 404 against the dev server.
|
||||
if (import.meta.env.PROD) {
|
||||
registerSW()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ const config = defineConfig({
|
|||
tanstackStart({ spa: { enabled: true } }),
|
||||
VitePWA({
|
||||
injectRegister: false,
|
||||
registerType: "prompt",
|
||||
registerType: "autoUpdate",
|
||||
outDir: ".output/public",
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue