mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 11:33:14 +00:00
Some checks failed
CI / Lint (push) Has been cancelled
CI / Format check (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Unit tests (push) Has been cancelled
CI / Playwright E2E (push) Has been cancelled
CI / Docker build smoke (push) Has been cancelled
CI / Triage ledger up to date (push) Has been cancelled
CI / ui bun.lock in sync (push) Has been cancelled
* Fix: Fix Insecure Direct Object Reference in slack_start_new_thread.py (#1788) Co-authored-by: corridor-security[bot] <203152403+corridor-security[bot]@users.noreply.github.com> (cherry picked from commit 32e81f2979a7baf11fe387df59f7d13a31889c74) * Fix PR creation guard shell bypasses (#1786) Co-authored-by: langsmith-fleet[bot] <langsmith-fleet[bot]@users.noreply.github.com> (cherry picked from commit 75fb8b487852003916c4984504a13ee7226b2ceb) * fix: add exc_info to swallowed exception in push re-review webhook (#1764) (cherry picked from commit ab85b372b4f37b7feb849054553daed10852a42c) * chore: clarify shared response image guidance (#1782) Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> (cherry picked from commit 2e8ff4b72f1148bb36c0c1181063a3abd78b15d0) * fix: match embedded review description background (#1791) Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> (cherry picked from commit 4ea2441ada1229bc414b02950d821786be2f7301) * fix: show current shared thread in sidebar (#1799) * fix: show current shared thread in sidebar Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: preserve resolved active sidebar threads Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> --------- Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com> (cherry picked from commit a77c4e475643b4a55bb2f0c93c0aa2669014fbac) * chore: switch deferred items to landed in upstream-sync triage documentation and jsonl entries (fork PR #226). Signed-off-by: Adam Moussa <adam@seahavenind.com> * harden PR guards + sidebar after security review - Mirror upstream #1786's nested-shell / executable-normalization hardening into the fork-only pr_verdict_guard.py (verdict-gating is a real fork control), keeping it in parity with pr_creation_guard.py. - Close the glued short-flag bypass (bash -c'...') in BOTH guards: a shell's -c argument can be concatenated into the same argv token, which the space-separated -c detection missed. Diverges pr_creation_guard.py from upstream #1786 by design; to be upstreamed. - Gate the new #1799 sidebar active-thread refresh on ownership so a non-owner viewing a shared thread reads last-known state without persisting a metadata write (mirrors the is_owner gate on the single-thread read path). - Fix an F821 in the #1799 cherry-pick (Mapping import / concrete dict type). Guards remain intentionally fail-open per the honest-agent threat model; docstrings narrowed to name the residual exotic-shell / stdin-fed vectors. --------- Signed-off-by: Adam Moussa <adam@seahavenind.com> Co-authored-by: corridor-security[bot] <203152403+corridor-security[bot]@users.noreply.github.com> Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com> Co-authored-by: langsmith-fleet[bot] <langsmith-fleet[bot]@users.noreply.github.com> Co-authored-by: Suraj Bayas <surajyou24@gmail.com> Co-authored-by: Ramon Nogueira <ramon.nogueira@langchain.dev> Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Johannes du Plessis <johannes@langchain.dev> Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>
136 lines
5.1 KiB
Python
136 lines
5.1 KiB
Python
"""Tool: ``save_plan``. Publish sandbox Markdown for review or sharing.
|
|
|
|
Reads the Markdown file the agent created in the sandbox and publishes it to the
|
|
plan-review page. In plan mode it is an approvable implementation plan; outside
|
|
plan mode it is read-only shared content.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Mapping
|
|
from typing import Annotated, Any
|
|
|
|
from langgraph.config import get_config
|
|
from langgraph.prebuilt import InjectedState
|
|
|
|
from ..dashboard.plan_store import (
|
|
PLAN_FILE_DIRECTORY,
|
|
PLAN_STATUS_READY,
|
|
PLAN_STATUS_SHARED,
|
|
save_plan_content,
|
|
)
|
|
from ..utils.sandbox_state import get_sandbox_backend
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_MAX_PLAN_LINES = 20_000
|
|
_MARKDOWN_EXTENSIONS = (".md", ".markdown")
|
|
|
|
|
|
async def save_plan(
|
|
plan_file_path: str,
|
|
state: Annotated[dict[str, Any] | None, InjectedState] = None,
|
|
) -> dict[str, Any]:
|
|
"""Publish a Markdown plan file from the sandbox for review.
|
|
|
|
Use this in plan mode once your plan is ready. Outside plan mode, use it to
|
|
share long Slack responses without switching the thread into plan mode. First
|
|
create a Markdown file under ``/workspace/plans/`` using a dated, descriptive
|
|
filename, then pass that file path here. The file contents are published to
|
|
the plan-review page linked in the conversation. In plan mode, the user can
|
|
comment, approve, or request changes; outside plan mode, the page is read-only
|
|
shared content.
|
|
|
|
Write the content in standard Markdown — headings, bullet/numbered lists, and
|
|
fenced code blocks all render. Shared responses persist Markdown text only;
|
|
they do not upload or serve sandbox-local or relative image files. If images
|
|
or screenshots are needed for a Slack response, post them directly in Slack
|
|
instead of relying on the shared response page.
|
|
|
|
Args:
|
|
plan_file_path: Path to the Markdown plan file in the sandbox.
|
|
|
|
Returns:
|
|
``{success: True, path}`` on success, or ``{success: False, error}``.
|
|
"""
|
|
if not isinstance(plan_file_path, str):
|
|
return {"success": False, "error": "plan_file_path must be a string"}
|
|
path = plan_file_path.strip()
|
|
if not path:
|
|
return {"success": False, "error": "plan_file_path cannot be empty"}
|
|
if not _is_markdown_path(path):
|
|
return {
|
|
"success": False,
|
|
"error": f"plan_file_path must point to a Markdown file in {PLAN_FILE_DIRECTORY}",
|
|
}
|
|
|
|
try:
|
|
config = get_config()
|
|
except Exception:
|
|
config = {}
|
|
configurable = config.get("configurable", {}) if isinstance(config, dict) else {}
|
|
thread_id = configurable.get("thread_id") if isinstance(configurable, dict) else None
|
|
if not thread_id:
|
|
return {"success": False, "error": "no thread_id in run config"}
|
|
|
|
try:
|
|
content = (await _read_plan_file(str(thread_id), path)).strip()
|
|
if not content:
|
|
return {"success": False, "error": "plan file cannot be empty"}
|
|
await _save(str(thread_id), content, path, plan_mode=_active_plan_mode(state, configurable))
|
|
except Exception as exc: # noqa: BLE001
|
|
logger.exception("save_plan failed for thread %s", thread_id)
|
|
return {"success": False, "error": f"failed to save plan: {exc}"}
|
|
return {"success": True, "path": path}
|
|
|
|
|
|
async def _save(thread_id: str, content: str, path: str, *, plan_mode: bool) -> None:
|
|
await save_plan_content(
|
|
thread_id,
|
|
markdown=content,
|
|
status=PLAN_STATUS_READY if plan_mode else PLAN_STATUS_SHARED,
|
|
plan_file_path=path,
|
|
plan_mode=plan_mode or None,
|
|
)
|
|
|
|
|
|
def _active_plan_mode(state: dict[str, Any] | None, configurable: Any) -> bool:
|
|
if isinstance(state, dict) and state.get("plan_mode") is True:
|
|
return True
|
|
return isinstance(configurable, dict) and configurable.get("plan_mode") is True
|
|
|
|
|
|
async def _read_plan_file(thread_id: str, path: str) -> str:
|
|
backend = await get_sandbox_backend(thread_id)
|
|
result = await backend.aread(path, offset=0, limit=_MAX_PLAN_LINES)
|
|
error = _value(result, "error")
|
|
if error:
|
|
raise ValueError(error)
|
|
file_data = _value(result, "file_data")
|
|
if file_data is None:
|
|
raise ValueError("plan file could not be read")
|
|
encoding = _value(file_data, "encoding")
|
|
if encoding is not None and encoding != "utf-8":
|
|
raise ValueError("plan file must be UTF-8 text")
|
|
content = _value(file_data, "content")
|
|
if not isinstance(content, str):
|
|
raise ValueError("plan file content was not text")
|
|
if content.count("\n") + 1 >= _MAX_PLAN_LINES:
|
|
raise ValueError("plan file is too large")
|
|
return content
|
|
|
|
|
|
def _value(value: Any, key: str) -> Any:
|
|
if isinstance(value, Mapping):
|
|
return value.get(key)
|
|
return getattr(value, key, None)
|
|
|
|
|
|
def _is_markdown_path(path: str) -> bool:
|
|
if "\x00" in path or not path.startswith(f"{PLAN_FILE_DIRECTORY}/"):
|
|
return False
|
|
filename = path.removeprefix(f"{PLAN_FILE_DIRECTORY}/")
|
|
if not filename or "/" in filename:
|
|
return False
|
|
return filename.lower().endswith(_MARKDOWN_EXTENSIONS)
|