mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-07 16:19:09 +00:00
Compare commits
13 commits
93c2eba47f
...
2a226e2cd1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a226e2cd1 | ||
|
|
b65c3a07db | ||
|
|
34c5da7328 | ||
|
|
f43606309e | ||
|
|
232f22f9d0 | ||
|
|
b129ba6654 | ||
|
|
1bea7ae2d2 | ||
|
|
a7ddd79e98 | ||
|
|
47a9a1ba89 | ||
|
|
421290d066 | ||
|
|
a52ebed77c | ||
|
|
2335ac59c0 | ||
|
|
98fa659303 |
39 changed files with 2162 additions and 248 deletions
123
.env.example
Normal file
123
.env.example
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# === LangSmith ===
|
||||
LANGSMITH_API_KEY_PROD="" # From step 4a
|
||||
# NOTE: LANGCHAIN_TRACING_V2 and LANGCHAIN_PROJECT are RESERVED on LangGraph Platform
|
||||
# (LangSmith deployments) — the platform sets them itself and rejects the deploy if
|
||||
# you provide them. Only set them for local/self-hosted runs, not in a deployment's env.
|
||||
LANGSMITH_TENANT_ID_PROD=""
|
||||
LANGSMITH_TRACING_PROJECT_ID_PROD="" # Fallback project ID for "View trace" links; graphs trace into the open-swe-agent / open-swe-review projects by name
|
||||
LANGSMITH_URL_PROD="https://smith.langchain.com"
|
||||
|
||||
# === LLM ===
|
||||
LLM_MODEL_ID="" # Default model, e.g. "bedrock_converse:us.anthropic.claude-opus-4-8"
|
||||
ANTHROPIC_API_KEY="" # Anthropic API key
|
||||
OPENAI_API_KEY="" # OpenAI API key (when using openai: models)
|
||||
GOOGLE_API_KEY="" # Google AI API key (when using google_genai: models)
|
||||
FIREWORKS_API_KEY="" # Fireworks API key (when using fireworks: models)
|
||||
# AWS credentials for Bedrock (when using bedrock_converse: models). Region defaults to us-east-1.
|
||||
AWS_ACCESS_KEY_ID=""
|
||||
AWS_SECRET_ACCESS_KEY=""
|
||||
|
||||
# === GitHub App (required) ===
|
||||
GITHUB_APP_ID="" # From step 3c
|
||||
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
|
||||
...
|
||||
-----END RSA PRIVATE KEY-----
|
||||
"
|
||||
GITHUB_APP_INSTALLATION_ID="" # From step 3d
|
||||
|
||||
# === GitHub Webhook (required) ===
|
||||
GITHUB_WEBHOOK_SECRET="" # The secret you generated in step 3b
|
||||
|
||||
# === Dashboard GitHub OAuth (required for the dashboard) ===
|
||||
# Direct GitHub OAuth used by the dashboard login flow (not via LangSmith).
|
||||
GITHUB_APP_CLIENT_ID="" # From step 3c
|
||||
GITHUB_APP_CLIENT_SECRET="" # From step 3c
|
||||
|
||||
# === Agent-runtime GitHub OAuth via LangSmith (optional) ===
|
||||
# Without these, all agent operations use the GitHub App's bot token.
|
||||
# With these, each agent run authenticates as the triggering user.
|
||||
GITHUB_OAUTH_PROVIDER_ID="" # The provider ID from steps 3a / 4b
|
||||
# Secret used to mint short-lived service JWTs that ask LangSmith to resolve a
|
||||
# specific user's GitHub token. Needed for per-user token resolution in deployed mode.
|
||||
X_SERVICE_AUTH_JWT_SECRET=""
|
||||
|
||||
# === Repo Allowlist (optional) ===
|
||||
# Comma-separated list of GitHub orgs the agent is allowed to operate on.
|
||||
# Also gates dashboard login to members of these orgs (requires the GitHub App's
|
||||
# Organization -> Members: Read-only permission; without it, all dashboard logins are rejected).
|
||||
# Leave empty to allow all orgs.
|
||||
ALLOWED_GITHUB_ORGS="" # e.g. "my-org,my-other-org"
|
||||
# Comma-separated list of specific owner/repo pairs the agent is allowed to operate on.
|
||||
# For GitHub/Linear webhooks, a repo is allowed if its org is in ALLOWED_GITHUB_ORGS OR its owner/repo is in ALLOWED_GITHUB_REPOS.
|
||||
# Slack mentions are not rejected from regex-inferred repository text; repository access is bounded by GitHub App installation permissions.
|
||||
# Leave both empty to allow all repos.
|
||||
ALLOWED_GITHUB_REPOS="" # e.g. "some-user/their-repo,another-org/specific-repo"
|
||||
|
||||
# === Default Repository ===
|
||||
# Used across all triggers when no repo is specified.
|
||||
DEFAULT_REPO_OWNER="" # Default GitHub org (e.g. "my-org")
|
||||
DEFAULT_REPO_NAME="" # Default GitHub repo (e.g. "my-repo")
|
||||
|
||||
# === Dashboard (required to run the web dashboard) ===
|
||||
# Public URL that browsers use for /dashboard/api/* and OAuth callbacks.
|
||||
# Use the FastAPI backend URL for local/cross-origin direct API calls.
|
||||
# Use the dashboard frontend URL when a same-origin frontend rewrite proxies /dashboard/api/*.
|
||||
# Its scheme drives cookie security: http:// => SameSite=Lax (local);
|
||||
# https:// => Secure + SameSite=None (production).
|
||||
DASHBOARD_API_BASE_URL="http://localhost:2024"
|
||||
# Public base URL of the dashboard frontend (the ui/ app). Default post-login redirect.
|
||||
DASHBOARD_BASE_URL="http://localhost:3000"
|
||||
# HMAC secret for dashboard JWTs (session cookie and OAuth state).
|
||||
DASHBOARD_JWT_SECRET="" # Generate with: openssl rand -hex 32
|
||||
# Comma-separated origins allowed for credentialed CORS and post-login redirects.
|
||||
# Required whenever the frontend and API are on different origins — including local
|
||||
# dev (UI :3000 -> API :2024 is cross-origin). CORS is only enabled when this is set.
|
||||
DASHBOARD_ALLOWED_ORIGINS="http://localhost:3000" # prod: your frontend origin(s)
|
||||
# Comma-separated GitHub login or email allowlist for admin dashboard endpoints.
|
||||
# Empty => nobody is an admin.
|
||||
CONFIGURED_ADMINS="" # e.g. "alice,bob@my-org.com"
|
||||
# URL of the LangGraph server the FastAPI side calls to trigger/stream runs.
|
||||
# Defaults to http://localhost:2024 locally; set to your deployment URL in prod.
|
||||
LANGGRAPH_URL="http://localhost:2024"
|
||||
|
||||
# === Linear (if using Linear trigger) ===
|
||||
LINEAR_API_KEY="" # From step 5
|
||||
LINEAR_WEBHOOK_SECRET="" # From step 5
|
||||
|
||||
# === Slack (if using Slack trigger) ===
|
||||
SLACK_BOT_TOKEN="" # From step 5
|
||||
SLACK_BOT_USER_ID=""
|
||||
SLACK_BOT_USERNAME=""
|
||||
SLACK_SIGNING_SECRET=""
|
||||
# Optional: Slack-specific default repo (falls back to DEFAULT_REPO_OWNER/NAME).
|
||||
SLACK_REPO_OWNER=""
|
||||
SLACK_REPO_NAME=""
|
||||
# Optional: "Sign in with Slack" account linking (GitHub <-> Slack). See step 5.
|
||||
SLACK_CLIENT_ID=""
|
||||
SLACK_CLIENT_SECRET=""
|
||||
SLACK_TEAM_ID="" # Optional; restrict linking to one workspace (T...)
|
||||
|
||||
# === Exa (optional — enables web search tool) ===
|
||||
EXA_API_KEY="" # From https://dashboard.exa.ai
|
||||
|
||||
# === Reviewer / Analyzer (optional) ===
|
||||
# LangSmith dataset where reviewer finding outcomes are recorded and read back by
|
||||
# the analyzer. Defaults to "openswe-reviewer-outcomes" if unset.
|
||||
REVIEWER_OUTCOMES_DATASET=""
|
||||
# Single GitHub org whose members may trigger the agent on *public* repos.
|
||||
# Empty => no public-repo gate (back-compat). Distinct from ALLOWED_GITHUB_ORGS.
|
||||
PUBLIC_REPO_ORG_GATE=""
|
||||
|
||||
# === Sandbox (optional) ===
|
||||
# Provider: langsmith (default), modal, daytona, runloop, or local. See CUSTOMIZATION.md.
|
||||
SANDBOX_TYPE="langsmith"
|
||||
DEFAULT_SANDBOX_SNAPSHOT_ID="" # Required when SANDBOX_TYPE=langsmith (see step 4c)
|
||||
DEFAULT_SANDBOX_SNAPSHOT_FS_CAPACITY_BYTES="" # Root FS size in bytes (default: 32 GiB)
|
||||
DEFAULT_SANDBOX_VCPUS="" # vCPUs per sandbox (default: 4)
|
||||
DEFAULT_SANDBOX_MEM_BYTES="" # Memory in bytes per sandbox (default: 15 GiB)
|
||||
DEFAULT_SANDBOX_IDLE_TTL_SECONDS="" # Auto-stop after N seconds idle (default: 7200; 0 disables)
|
||||
DEFAULT_SANDBOX_DELETE_AFTER_STOP_SECONDS="" # Delete N seconds after stop (default: 86400; 0 disables)
|
||||
|
||||
# === Token Encryption ===
|
||||
TOKEN_ENCRYPTION_KEY="" # Generate with: openssl rand -base64 32
|
||||
# Supports key rotation: see "Rotating TOKEN_ENCRYPTION_KEY" below
|
||||
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. -->
|
||||
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
|
|
@ -22,6 +22,15 @@ updates:
|
|||
groups:
|
||||
minor-and-patch:
|
||||
update-types: ["minor", "patch"]
|
||||
ignore:
|
||||
# @types/node must track the runtime Node major, not the latest release.
|
||||
# /ui is the web dashboard deployed on Vercel; pin @types/node to the Node
|
||||
# major Vercel builds/runs it on. Dependabot can't see that and a too-new
|
||||
# types major still compiles (passes CI, wrong at runtime). Sanctioned
|
||||
# exception to the no-blanket-ignore rule (engineering-handbook
|
||||
# github-standards Pinning Principle). Minor/patch within the major flow.
|
||||
- dependency-name: "@types/node"
|
||||
update-types: ["version-update:semver-major"]
|
||||
|
||||
# Docker — root Dockerfile
|
||||
- package-ecosystem: "docker"
|
||||
|
|
|
|||
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
|
|
@ -59,7 +59,7 @@ jobs:
|
|||
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: "24"
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- name: Install Python deps (langgraph dev runtime)
|
||||
run: uv sync --locked
|
||||
|
|
@ -87,3 +87,16 @@ jobs:
|
|||
tests/e2e/playwright-report
|
||||
tests/e2e/test-results
|
||||
retention-days: 7
|
||||
|
||||
docker-build:
|
||||
name: Docker build smoke
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
# Smoke-build the sandbox image so a bad Dockerfile pin (e.g. an
|
||||
# apt "nodejs=${NODEJS_VERSION}" that no longer resolves) fails a check
|
||||
# instead of only surfacing at sandbox-provision time. No push, no
|
||||
# registry credentials: this only proves the image builds.
|
||||
- name: Build sandbox image (no push)
|
||||
run: docker build --pull -t open-swe-sandbox:ci .
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -39,6 +39,7 @@ yarn-error.log*
|
|||
.env*.local
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
|
|
|||
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
24
|
||||
14
CLAUDE.md
14
CLAUDE.md
|
|
@ -116,3 +116,17 @@ Webhooks compute deterministic thread ids so the same Linear issue / Slack threa
|
|||
- New dashboard endpoints: add to `agent/dashboard/routes.py`. The router is auto-mounted on the FastAPI app.
|
||||
- New graphs: register the entrypoint in `langgraph.json` under `graphs`.
|
||||
- Minimal-to-no code comments — only when the *why* isn't obvious from the code.
|
||||
|
||||
## Fork maintenance — syncing `upstream/main`
|
||||
|
||||
This is a long-lived fork of `langchain-ai/open-swe` with Sea Haven customizations woven into upstream-owned files (notably `agent/prompt.py` prompt constants, `agent/webapp.py`, and the tool/middleware wiring). Merging upstream is a triage exercise, not a fast-forward. When you want upstream's clean changes but must **defer a large structural refactor** (and its entangled features), work in this order:
|
||||
|
||||
1. **Triage before resolving.** Merge-base is `git merge-base HEAD upstream/main`. The truthful conflict set is the combined merge, `git merge-tree --write-tree --name-only HEAD upstream/main` — a per-commit probe against each commit's parent *overstates* conflicts (a file a refactor merely added shows up as a phantom `modify/delete`). Decide keep-baseline vs adopt-refactor **before** resolving, and surface the choice to a human for any auth/webhook/IAM surface.
|
||||
2. **Chase the cascade, not just the textual conflicts.** The hard part is the non-conflicting files the refactor also touched. Get the refactor's file set (`git diff-tree --no-commit-id --name-status -r <refactor-sha>`) and cross-reference the files this fork modified (`git diff --name-only <fork-base> HEAD`). Files in both = hand-resolve; files only the refactor touched = mechanical.
|
||||
3. **Deferring a refactor:** default every refactor-touched file to **upstream**, except the deleted-module cluster, which stays at your **baseline (HEAD)** — and move its **paired tests to the same side**. A file goes to HEAD when its upstream version imports a module the refactor deleted, or kept code needs an old API. Bring back files the refactor deleted but you still use with `git checkout HEAD -- <file>`. Iterate `pytest --co -q` to chase import breaks one module at a time.
|
||||
4. **Two silent hazards.** (a) A thin upstream router ends with `from .webhooks.slack import process_slack_mention`; merged alongside your monolith's *local* `def process_slack_mention`, Python rebinds the name at import, so **upstream's handler runs and silently drops your fixes** — delete those re-import lines. (b) A new tool/middleware importing a deleted module crashes the whole graph at import — if you defer the feature, delete the tool file **and** all its wiring (`server.py` tool list, `tools/__init__.py`, prompt guidance, e2e harness, its test).
|
||||
5. **Keep test + impl on the same side** — a test at upstream and its impl at HEAD (or vice-versa) yields async-vs-sync or contract drift. Keep the whole vertical (backend + UI + e2e spec + fixtures) on one side.
|
||||
6. **Run CI in layers** — `ruff`/`tsc` (syntax/types) → `pytest --co` (import-time breaks) → unit tests (contract mismatches) → **E2E (Playwright + the real LangGraph dev server)**, which is the only layer that catches import-time crashes in tool/middleware *wiring* and frontend↔backend contract drift. "Unit green" is not "done" for a structural merge.
|
||||
7. **Tooling-switch fallout** — a package-manager/build-tool switch (upstream `pnpm`, this fork keeps `bun`) auto-merges into build scripts, CI, the `packageManager` field, and lockfiles even when you reject it for the product build. After merging, sweep those and never ship two lockfiles.
|
||||
|
||||
Validate on a throwaway branch with granular commits (one per cascade class) and let each CI layer prove out before promoting.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
FROM python:3.14.6-slim-trixie
|
||||
|
||||
ARG DOCKER_CLI_VERSION=5:29.1.5-1~debian.13~trixie
|
||||
ARG NODEJS_VERSION=22.22.0-1nodesource1
|
||||
ARG NODEJS_VERSION=24.18.0-1nodesource1
|
||||
ARG UV_VERSION=0.9.26
|
||||
ARG YARN_VERSION=4.12.0
|
||||
ARG GH_VERSION=2.83.1
|
||||
|
|
@ -67,7 +67,7 @@ RUN set -eux; \
|
|||
|
||||
ENV PATH=/root/.local/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
|
||||
&& apt-get install -y "nodejs=${NODEJS_VERSION}" \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& corepack enable \
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Open SWE has two runnable pieces:
|
|||
- [uv](https://docs.astral.sh/uv/) package manager
|
||||
- [LangGraph CLI](https://docs.langchain.com/langsmith/cli)
|
||||
- [ngrok](https://ngrok.com/) (for local development — exposes webhook endpoints to the internet)
|
||||
- [pnpm](https://pnpm.io/) (only if you want to run the dashboard UI locally — see step 8). Node 20+ also works, but `ui/pnpm-lock.yaml` is the canonical lockfile.
|
||||
- [bun](https://bun.sh/) (only if you want to run the dashboard UI locally — see step 8). Node 24 is required; `ui/bun.lock` is the canonical lockfile.
|
||||
|
||||
## 1. Clone and install
|
||||
|
||||
|
|
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -32,6 +32,22 @@ TEAM_SETTINGS_KEY = "default"
|
|||
ORG_GUIDELINES_MAX_CHARS = 10_000
|
||||
REVIEW_TRACING_PROJECT_MAX_CHARS = 256
|
||||
|
||||
# Sea Haven review baseline seeded as the org-wide guidelines default. Surfaces
|
||||
# in the reviewer prompt for every repo until an admin overrides it with a
|
||||
# non-empty value via the dashboard (PUT /team-settings). Keep it stack-agnostic
|
||||
# and well under ORG_GUIDELINES_MAX_CHARS.
|
||||
DEFAULT_ORG_REVIEW_GUIDELINES = """\
|
||||
Sea Haven review baseline (applies to every repo unless a repo-specific guideline overrides it):
|
||||
|
||||
- Severity: map findings to critical / high / medium / low. Reserve critical and high for correctness bugs, security issues, data loss, or broken contracts — not style.
|
||||
- Secrets & config: flag any hardcoded secret, credential, or real `.env`/config value committed to source, and any sensitive value placed outside the platform's secrets manager.
|
||||
- Security surface (raise as high): changes to authentication/authorization or access checks; IAM/policy/permission or infrastructure-access changes; changes to the exported signature or contract of a public handler/endpoint; and untrusted-input handling (request parsing, deserialization, file uploads, SSRF-prone fetches, and template/SQL/command construction — flag unescaped interpolation of dynamic or user-controlled data).
|
||||
- Tests: flag new behavior that ships without a corresponding test, and "fixes" that only silence a check (added excludes, `noqa` / `# type: ignore`, skipped or `xfail`ed tests).
|
||||
- Naming & conventions: flag resources or code that break the repo's established naming and layout conventions.
|
||||
- Deferred work: a finding the author chooses to defer must be captured in a tracked issue, not dropped silently.
|
||||
|
||||
Only file a finding that anchors to a changed line and names a concrete failure mode. Do not police pre-existing issues outside the diff or raise pure style nits."""
|
||||
|
||||
|
||||
class TeamSettingsUpdate(BaseModel):
|
||||
review_draft_prs: bool = False
|
||||
|
|
@ -152,7 +168,7 @@ def _default_settings() -> dict[str, Any]:
|
|||
"pr_summaries": True,
|
||||
"review_trace_links": True,
|
||||
"review_tracing_project": None,
|
||||
"org_guidelines": None,
|
||||
"org_guidelines": DEFAULT_ORG_REVIEW_GUIDELINES,
|
||||
"default_agent_model": fallback_model,
|
||||
"default_agent_reasoning_effort": fallback_effort,
|
||||
"default_agent_subagent_model": fallback_model,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,33 @@ async def workflow_push_approved(thread_id: str, fingerprint: str) -> bool:
|
|||
return approvals.get(fingerprint, {}).get("status") == WORKFLOW_APPROVAL_APPROVED
|
||||
|
||||
|
||||
async def workflow_push_rejected(thread_id: str, fingerprint: str) -> bool:
|
||||
approvals = await get_workflow_push_approvals(thread_id)
|
||||
return approvals.get(fingerprint, {}).get("status") == WORKFLOW_APPROVAL_REJECTED
|
||||
|
||||
|
||||
async def find_workflow_push_approval(
|
||||
thread_id: str,
|
||||
*,
|
||||
repo: str,
|
||||
branch: str,
|
||||
files: list[str],
|
||||
) -> dict[str, Any] | None:
|
||||
"""Return the most recent approved record matching identity-level keys, if any."""
|
||||
approvals = await get_workflow_push_approvals(thread_id)
|
||||
identity = (repo, branch, tuple(sorted(files)))
|
||||
matches = [
|
||||
r
|
||||
for r in approvals.values()
|
||||
if r.get("status") == WORKFLOW_APPROVAL_APPROVED
|
||||
and (r.get("repo"), r.get("branch"), tuple(sorted(r.get("files", [])))) == identity
|
||||
]
|
||||
if not matches:
|
||||
return None
|
||||
matches.sort(key=lambda r: str(r.get("decided_at", "")), reverse=True)
|
||||
return matches[0]
|
||||
|
||||
|
||||
async def ensure_workflow_push_pending(
|
||||
thread_id: str,
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||
from .oauth import require_same_origin_for_mutations, require_session
|
||||
from .plan_api import _dispatch_followup, _thread_metadata
|
||||
from .thread_api import _user_owns_thread
|
||||
from .workflow_approval import decide_workflow_push_approval
|
||||
from .workflow_approval import (
|
||||
WORKFLOW_APPROVAL_PENDING,
|
||||
decide_workflow_push_approval,
|
||||
get_workflow_push_approvals,
|
||||
)
|
||||
|
||||
workflow_approval_router = APIRouter(
|
||||
prefix="/dashboard/api/workflow-approval",
|
||||
|
|
@ -19,6 +23,33 @@ workflow_approval_router = APIRouter(
|
|||
_SESSION_DEP = Depends(require_session)
|
||||
|
||||
|
||||
def _approval_record_response(record: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"fingerprint": record.get("fingerprint"),
|
||||
"status": record.get("status"),
|
||||
"repo": record.get("repo"),
|
||||
"branch": record.get("branch"),
|
||||
"files": record.get("files"),
|
||||
"requested_at": record.get("requested_at"),
|
||||
}
|
||||
|
||||
|
||||
@workflow_approval_router.get("/{thread_id}")
|
||||
async def list_workflow_approvals_for_thread(
|
||||
thread_id: str, session: dict[str, Any] = _SESSION_DEP
|
||||
) -> dict[str, Any]:
|
||||
metadata = await _thread_metadata(thread_id)
|
||||
if not _user_owns_thread(metadata, session["sub"], session.get("email")):
|
||||
raise HTTPException(403, "only the thread owner can view workflow push approvals")
|
||||
approvals = await get_workflow_push_approvals(thread_id)
|
||||
pending = [
|
||||
_approval_record_response(record)
|
||||
for record in approvals.values()
|
||||
if record.get("status") == WORKFLOW_APPROVAL_PENDING
|
||||
]
|
||||
return {"approvals": pending}
|
||||
|
||||
|
||||
@workflow_approval_router.post("/{thread_id}/{fingerprint}/approve")
|
||||
async def approve_workflow_push(
|
||||
thread_id: str, fingerprint: str, session: dict[str, Any] = _SESSION_DEP
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import codecs
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import threading
|
||||
|
|
@ -21,8 +23,10 @@ from langgraph.types import Command
|
|||
|
||||
from ..dashboard.workflow_approval import (
|
||||
ensure_workflow_push_pending,
|
||||
find_workflow_push_approval,
|
||||
mark_workflow_push_notified,
|
||||
workflow_push_approved,
|
||||
workflow_push_rejected,
|
||||
)
|
||||
from ..tools.slack_thread_reply import build_workflow_approval_blocks
|
||||
from ..utils.github_app import (
|
||||
|
|
@ -41,6 +45,28 @@ _SHELL_OPERATORS = {";", "|", "||", "&"}
|
|||
_REF_NAME = re.compile(r"^[A-Za-z0-9._/@+-]+$")
|
||||
_GIT_OBJECT_ID = re.compile(r"^[0-9a-fA-F]{40,64}$")
|
||||
_UNSAFE_RAW_COMMAND = re.compile(r"[;|`$<>\n\r]")
|
||||
# Unquoted shell expansion/substitution (`$VAR`, `${...}`, `$(...)`, backticks) can produce
|
||||
# a `git push` whose text we cannot see; its presence alongside a push forces fail-closed.
|
||||
_EXPANSION = re.compile(r"[$`]")
|
||||
# Unquoted command separators and grouping. Splitting the unquoted command skeleton on these
|
||||
# yields the individual simple-commands the shell would run, each checked for a `git push`.
|
||||
_SEGMENT_SPLIT = re.compile(r"[;|&(){}\n]+")
|
||||
# Command wrappers that can prefix a `git` invocation (e.g. `command git push`,
|
||||
# `env FOO=bar git push`, `/usr/bin/git push`). Recognized so a wrapped or path-qualified
|
||||
# git push cannot slip past the guard as "not a git command".
|
||||
_GIT_WRAPPERS = {"command", "env", "nice", "sudo", "stdbuf", "nohup", "time", "ionice", "setsid"}
|
||||
# Git global options that consume the following token as a separate value; skipped when
|
||||
# locating the git subcommand so `git -c k=v push` is still recognized as a push.
|
||||
_GIT_VALUE_OPTIONS = {"-C", "-c", "--namespace", "--git-dir", "--work-tree", "--exec-path"}
|
||||
|
||||
|
||||
class _BlockedGitPush:
|
||||
"""Sentinel returned by the parser when a git push command is unsafe or unrecognized."""
|
||||
|
||||
__slots__ = ("reason",)
|
||||
|
||||
def __init__(self, reason: str) -> None:
|
||||
self.reason = reason
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -57,13 +83,15 @@ class WorkflowPushChange:
|
|||
fingerprint: str
|
||||
repo: str
|
||||
branch: str
|
||||
base_sha: str
|
||||
head_sha: str
|
||||
files: list[str]
|
||||
head_sha: str
|
||||
remote: str
|
||||
local_ref: str
|
||||
remote_ref: str
|
||||
fixed_command: str
|
||||
base_sha: str = ""
|
||||
blocked: bool = False
|
||||
blocked_reason: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -142,52 +170,211 @@ def _response_ok(response: Any) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
def _parse_git_push(command: str) -> ParsedGitPush | None:
|
||||
stripped = command.strip()
|
||||
if _UNSAFE_RAW_COMMAND.search(stripped) or "&" in stripped.replace("&&", ""):
|
||||
return None
|
||||
def _tokens_invoke_git_push(tokens: list[str]) -> bool:
|
||||
"""True if the token stream runs `git push` as a git subcommand (not merely mentions it).
|
||||
|
||||
Scans each `git` invocation (bareword or path-qualified, after any wrapper prefix),
|
||||
skips `-C <dir>` and option flags, and checks whether the first positional argument is
|
||||
`push`. This ignores the word "push" appearing inside a commit message or another
|
||||
command, so `git commit -m "push it" && ls` is not treated as a push, while a genuine
|
||||
`git ... push` anywhere in a chain is.
|
||||
"""
|
||||
i = 0
|
||||
while i < len(tokens):
|
||||
if tokens[i].rsplit("/", 1)[-1] != "git":
|
||||
i += 1
|
||||
continue
|
||||
j = i + 1
|
||||
while j < len(tokens):
|
||||
tok = tokens[j]
|
||||
if tok in _SHELL_OPERATORS or tok == "&&":
|
||||
break
|
||||
if (
|
||||
tok in _GIT_VALUE_OPTIONS
|
||||
and j + 1 < len(tokens)
|
||||
and tokens[j + 1] not in _SHELL_OPERATORS
|
||||
and tokens[j + 1] != "&&"
|
||||
):
|
||||
j += 2 # option consumes the next token as its value
|
||||
continue
|
||||
if tok.startswith("-"):
|
||||
j += 1
|
||||
continue
|
||||
if tok == "push":
|
||||
return True
|
||||
break # first positional is a non-push subcommand
|
||||
i = j + 1
|
||||
return False
|
||||
|
||||
|
||||
def _decode_ansi_c(body: str) -> str:
|
||||
"""Best-effort ANSI-C (`$'...'`) escape decoding, so `$'\\x70ush'` / `$'\\160ush'` reveal
|
||||
the literal `push` the shell would run. Falls back to the raw body if decoding fails."""
|
||||
if "\\" not in body:
|
||||
return body
|
||||
try:
|
||||
tokens = shlex.split(stripped)
|
||||
return codecs.decode(body, "unicode_escape")
|
||||
except Exception:
|
||||
return body
|
||||
|
||||
|
||||
def _strip_quoted(command: str) -> str:
|
||||
"""Return the command with quoted spans and escaped characters blanked to spaces.
|
||||
|
||||
Leaves only the unquoted shell structure, so metacharacters that are literal (inside
|
||||
quotes or backslash-escaped — e.g. a `;` or `&` inside a commit message) do not look
|
||||
like command separators. Plain `'...'`/`"..."` spans wrap arguments and are blanked;
|
||||
`$'...'`/`$"..."` (ANSI-C / locale quoting) form a literal *word* (e.g. `git $'push'` runs
|
||||
`git push`), so their content is kept — otherwise the push word would vanish. Unbalanced
|
||||
quotes blank the remainder (fail-closed shape).
|
||||
"""
|
||||
out: list[str] = []
|
||||
i, n = 0, len(command)
|
||||
while i < n:
|
||||
c = command[i]
|
||||
if c in "'\"":
|
||||
# `$'...'` / `$"..."` — the preceding `$` marks a literal word, so keep content
|
||||
# and drop the `$` so adjacent pieces concatenate (`$'pus'$'h'` -> `push`).
|
||||
word_quote = bool(out) and out[-1] == "$"
|
||||
if word_quote:
|
||||
out.pop()
|
||||
if c == "'":
|
||||
j = command.find("'", i + 1)
|
||||
if j == -1:
|
||||
return "".join(out) + " "
|
||||
out.append(_decode_ansi_c(command[i + 1 : j]) if word_quote else " ")
|
||||
i = j + 1
|
||||
else:
|
||||
k = i + 1
|
||||
buf: list[str] = []
|
||||
while k < n and command[k] != '"':
|
||||
if command[k] == "\\" and k + 1 < n:
|
||||
buf.append(command[k + 1])
|
||||
k += 2
|
||||
else:
|
||||
buf.append(command[k])
|
||||
k += 1
|
||||
if k >= n:
|
||||
return "".join(out) + " "
|
||||
out.append("".join(buf) if word_quote else " ")
|
||||
i = k + 1
|
||||
elif c == "\\" and i + 1 < n:
|
||||
out.append(" ")
|
||||
i += 2
|
||||
else:
|
||||
out.append(c)
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def _maybe_obfuscated_git_push(command: str) -> bool:
|
||||
"""True if the shell would run a `git push` that `_tokens_invoke_git_push` could not see.
|
||||
|
||||
Only reached when the precise token scan found no clean push. `shlex` splits on
|
||||
whitespace and performs no expansion, so a push can hide behind a fused separator
|
||||
(`true;git push`), subshell/grouping (`(git push ...)`), or expansion (`git${IFS}push`,
|
||||
`git $(printf push)`). We work on the unquoted command skeleton so literal metacharacters
|
||||
inside a commit message are ignored (no false positive on `git commit -m "push it" && ls`
|
||||
or `git log | grep push`), then fail closed if any unquoted simple-command is a `git push`
|
||||
or if unquoted expansion could produce one.
|
||||
"""
|
||||
skeleton = _strip_quoted(command)
|
||||
if "push" not in skeleton:
|
||||
# The only `push` text is inside quotes (a literal argument), so it cannot be a
|
||||
# command word. (A push spelled purely by expansion with no literal `push` anywhere
|
||||
# is left to the GitHub proxy token scope, which lacks workflows:write until approval.)
|
||||
return False
|
||||
if _EXPANSION.search(skeleton):
|
||||
return True
|
||||
return any(
|
||||
_tokens_invoke_git_push(segment.split()) for segment in _SEGMENT_SPLIT.split(skeleton)
|
||||
)
|
||||
|
||||
|
||||
def _parse_git_push(command: str) -> ParsedGitPush | _BlockedGitPush | None:
|
||||
stripped = command.strip()
|
||||
try:
|
||||
probe = shlex.split(stripped)
|
||||
except ValueError:
|
||||
probe = None
|
||||
if probe is None or not _tokens_invoke_git_push(probe):
|
||||
# No push the token scan can normalize. Fail closed if one is hidden behind shell
|
||||
# metacharacters/expansion; otherwise leave the command untouched.
|
||||
if _maybe_obfuscated_git_push(stripped):
|
||||
return _BlockedGitPush(
|
||||
"possible git push obscured by shell metacharacters; issue it as a plain "
|
||||
"`git push origin <branch>`"
|
||||
)
|
||||
return None
|
||||
if _UNSAFE_RAW_COMMAND.search(stripped) or "&" in stripped.replace("&&", ""):
|
||||
return _BlockedGitPush("unsafe shell characters in git push command")
|
||||
tokens = probe
|
||||
if not tokens:
|
||||
return None
|
||||
|
||||
if len(tokens) >= 4 and tokens[0] == "cd" and tokens[2] == "&&":
|
||||
if any(token in _SHELL_OPERATORS or token == "&&" for token in tokens[3:]):
|
||||
return None
|
||||
return _BlockedGitPush("chained shell commands in git push")
|
||||
return _parse_git_tokens(tokens[3:], repo_dir=tokens[1])
|
||||
|
||||
if any(token in _SHELL_OPERATORS or token == "&&" for token in tokens):
|
||||
return None
|
||||
return _BlockedGitPush("chained shell commands")
|
||||
return _parse_git_tokens(tokens, repo_dir=None)
|
||||
|
||||
|
||||
def _parse_git_tokens(tokens: list[str], *, repo_dir: str | None) -> ParsedGitPush | None:
|
||||
if not tokens or tokens[0] != "git":
|
||||
def _parse_git_tokens(
|
||||
tokens: list[str], *, repo_dir: str | None
|
||||
) -> ParsedGitPush | _BlockedGitPush | None:
|
||||
"""Parse tokens into a supported git push, a block sentinel, or None.
|
||||
|
||||
Recognizes bareword `git`, path invocations (`/usr/bin/git`), and wrapper-prefixed
|
||||
forms (`command`/`env`/`nice`/... git). Anything git-push-shaped that cannot be reduced
|
||||
to `git [-C <dir>] push [-u] origin <refspec>` fails closed to a block rather than a
|
||||
silent pass-through. Non-push git commands and non-git commands return None.
|
||||
"""
|
||||
git_idx = next((idx for idx, tok in enumerate(tokens) if tok.rsplit("/", 1)[-1] == "git"), None)
|
||||
if git_idx is None:
|
||||
return None
|
||||
i = 1
|
||||
while i < len(tokens) and tokens[i] != "push":
|
||||
if tokens[i] == "-C" and i + 1 < len(tokens):
|
||||
repo_dir = tokens[i + 1]
|
||||
git_args = tokens[git_idx + 1 :]
|
||||
if "push" not in git_args:
|
||||
# A non-push git command (e.g. `git status`, `git pull`).
|
||||
return None
|
||||
# Everything before the `git` executable must be a benign wrapper prefix (a known
|
||||
# wrapper, an option flag, or a NAME=VALUE assignment). An unrecognized leading token
|
||||
# (a value-taking wrapper option like the `10` in `nice -n 10`, an unknown wrapper, or
|
||||
# `git` used as an argument to another program) is ambiguous, so fail closed.
|
||||
if any(
|
||||
not (tok in _GIT_WRAPPERS or tok.startswith("-") or "=" in tok) for tok in tokens[:git_idx]
|
||||
):
|
||||
return _BlockedGitPush(
|
||||
"unrecognized wrapper before `git push`; use `git push origin <branch>`"
|
||||
)
|
||||
i = 0
|
||||
while i < len(git_args) and git_args[i] != "push":
|
||||
if git_args[i] == "-C" and i + 1 < len(git_args):
|
||||
repo_dir = git_args[i + 1]
|
||||
i += 2
|
||||
continue
|
||||
return None
|
||||
if i >= len(tokens) or tokens[i] != "push":
|
||||
return None
|
||||
return _parse_push_args(tokens[i + 1 :], repo_dir=repo_dir)
|
||||
# A git push carrying unsupported global options (e.g. `git -c k=v push`,
|
||||
# `git --git-dir=.git push`). We cannot safely normalize it, so fail closed.
|
||||
return _BlockedGitPush(
|
||||
"unsupported git options before `push`; use `git push origin <branch>`"
|
||||
)
|
||||
return _parse_push_args(git_args[i + 1 :], repo_dir=repo_dir)
|
||||
|
||||
|
||||
def _parse_push_args(tokens: list[str], *, repo_dir: str | None) -> ParsedGitPush | None:
|
||||
def _parse_push_args(
|
||||
tokens: list[str], *, repo_dir: str | None
|
||||
) -> ParsedGitPush | _BlockedGitPush | None:
|
||||
set_upstream = False
|
||||
while tokens and tokens[0] in {"-u", "--set-upstream"}:
|
||||
set_upstream = True
|
||||
tokens = tokens[1:]
|
||||
if len(tokens) != 2 or tokens[0] != "origin":
|
||||
return None
|
||||
return _BlockedGitPush("unrecognized or unsafe git push arguments")
|
||||
parsed = _parse_refspec(tokens[1])
|
||||
if parsed is None:
|
||||
return None
|
||||
return _BlockedGitPush("unrecognized or unsafe git push refspec")
|
||||
local_ref, remote_ref = parsed
|
||||
return ParsedGitPush(
|
||||
repo_dir=repo_dir,
|
||||
|
|
@ -281,6 +468,95 @@ def _run_coroutine_sync(coro: Awaitable[ToolMessage | Command]) -> ToolMessage |
|
|||
return value
|
||||
|
||||
|
||||
def _parse_ls_tree(output: str) -> list[tuple[str, str]]:
|
||||
"""Parse `git ls-tree -r` output into a sorted list of (sha, path) tuples."""
|
||||
entries: list[tuple[str, str]] = []
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
# Format: "<mode> <type> <sha>\t<path>"
|
||||
meta, _, path = line.partition("\t")
|
||||
if not path:
|
||||
continue
|
||||
parts = meta.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
entries.append((parts[2], path))
|
||||
entries.sort(key=lambda item: item[1])
|
||||
return entries
|
||||
|
||||
|
||||
def _workflow_tree_at_ref(
|
||||
backend: Any, repo_dir: str | None, ref: str
|
||||
) -> tuple[list[tuple[str, str]], str] | None:
|
||||
"""Return the sorted workflow tree entries and a stable hash for the given ref.
|
||||
|
||||
Returns an empty list when the ref has no workflow files, so callers can detect
|
||||
additions and deletions against a base ref.
|
||||
"""
|
||||
ls_tree = _run_git(backend, repo_dir, f"ls-tree -r {shlex.quote(ref)} -- .github/workflows")
|
||||
if not ls_tree.ok:
|
||||
return None
|
||||
entries = _parse_ls_tree(ls_tree.output)
|
||||
content_hash = _fingerprint({"tree": entries})
|
||||
return entries, content_hash
|
||||
|
||||
|
||||
def _fetch_remote_base(backend: Any, repo_dir: str | None, remote: str, branch: str) -> str | None:
|
||||
"""Fetch the base ref from the exact URL the push will target and return a local alias.
|
||||
|
||||
The base is fetched from the remote's effective *push* URL (`git remote get-url --push`),
|
||||
not the remote name, because git honors a separate `pushurl`: an untrusted sandbox can
|
||||
point the fetch URL at attacker/local content while the push still lands on the real
|
||||
repo. Fetching the base from the push URL keeps the base and the push destination the
|
||||
same authenticated repo, so a split cannot hide a workflow change. Uses `FETCH_HEAD`
|
||||
(freshly fetched), never a local `refs/remotes/origin/*` ref the sandbox could rewrite.
|
||||
|
||||
Returns None (which makes the caller require approval) if the push destination is
|
||||
ambiguous or rewritten: multiple `pushurl` entries (git pushes to ALL of them, so a
|
||||
single base cannot represent the destination) or any `insteadOf`/`pushInsteadOf` URL
|
||||
rewrite the sandbox could use to make the inspected URL differ from the push target.
|
||||
"""
|
||||
rewrites = _run_git(
|
||||
backend, repo_dir, "config --get-regexp " + shlex.quote(r"url\..*\.(push)?insteadof")
|
||||
)
|
||||
if rewrites.ok and rewrites.output.strip():
|
||||
# A URL rewrite means the inspected push URL may not be the real destination.
|
||||
return None
|
||||
|
||||
push_url_res = _run_git(backend, repo_dir, f"remote get-url --push --all {shlex.quote(remote)}")
|
||||
push_urls = (
|
||||
[line.strip() for line in push_url_res.output.splitlines() if line.strip()]
|
||||
if push_url_res.ok
|
||||
else []
|
||||
)
|
||||
# `git push` sends to every configured push URL; if there is not exactly one we cannot
|
||||
# represent the destination with a single base, so fail closed.
|
||||
if len(push_urls) != 1:
|
||||
return None
|
||||
push_url = push_urls[0]
|
||||
|
||||
fetch = _run_git(backend, repo_dir, f"fetch {shlex.quote(push_url)} {shlex.quote(branch)}")
|
||||
if fetch.ok:
|
||||
return "FETCH_HEAD"
|
||||
|
||||
default = _run_git(backend, repo_dir, f"ls-remote --symref {shlex.quote(push_url)} HEAD")
|
||||
default_branch = ""
|
||||
if default.ok:
|
||||
default_branch = _first_line(default.output).removeprefix("ref: refs/heads/").split("\t")[0]
|
||||
|
||||
for fallback in (default_branch, "dev", "main"):
|
||||
if not fallback or fallback == branch:
|
||||
continue
|
||||
fetch = _run_git(
|
||||
backend, repo_dir, f"fetch {shlex.quote(push_url)} {shlex.quote(fallback)}"
|
||||
)
|
||||
if fetch.ok:
|
||||
return "FETCH_HEAD"
|
||||
return None
|
||||
|
||||
|
||||
def _workflow_change_for_push(backend: Any, parsed: ParsedGitPush) -> WorkflowPushChange | None:
|
||||
root_result = _run_git(backend, parsed.repo_dir, "rev-parse --show-toplevel")
|
||||
if not root_result.ok:
|
||||
|
|
@ -301,42 +577,34 @@ def _workflow_change_for_push(backend: Any, parsed: ParsedGitPush) -> WorkflowPu
|
|||
if not head or not _GIT_OBJECT_ID.fullmatch(head):
|
||||
return None
|
||||
|
||||
remote_branch = f"refs/remotes/{parsed.remote}/{parsed.remote_ref}"
|
||||
remote_branch_exists = _run_git(
|
||||
backend, root, f"rev-parse --verify {shlex.quote(remote_branch)}"
|
||||
)
|
||||
if remote_branch_exists.ok and _first_line(remote_branch_exists.output):
|
||||
base_ref = remote_branch
|
||||
range_expr = f"{shlex.quote(base_ref)}..{shlex.quote(head)}"
|
||||
base_sha = _first_line(_run_git(backend, root, f"rev-parse {shlex.quote(base_ref)}").output)
|
||||
else:
|
||||
origin_head = _run_git(backend, root, "symbolic-ref --short refs/remotes/origin/HEAD")
|
||||
base_ref = _first_line(origin_head.output) if origin_head.ok else "origin/main"
|
||||
range_expr = f"{shlex.quote(base_ref)}...{shlex.quote(head)}"
|
||||
base_sha = _first_line(
|
||||
_run_git(
|
||||
backend, root, f"merge-base {shlex.quote(head)} {shlex.quote(base_ref)}"
|
||||
).output
|
||||
head_tree = _workflow_tree_at_ref(backend, root, head)
|
||||
if head_tree is None:
|
||||
# The workflow tree at head could not be read (an empty tree returns ([], hash), so
|
||||
# None means the `ls-tree` read genuinely failed). Fail closed rather than skipping
|
||||
# the guard, mirroring the base-read path.
|
||||
return WorkflowPushChange(
|
||||
fingerprint="",
|
||||
repo="",
|
||||
branch=branch_name,
|
||||
files=[],
|
||||
head_sha=head,
|
||||
remote=parsed.remote,
|
||||
local_ref=parsed.local_ref,
|
||||
remote_ref=parsed.remote_ref,
|
||||
fixed_command="",
|
||||
blocked=True,
|
||||
blocked_reason="could not read the workflow tree at the pushed head; blocking to be safe",
|
||||
)
|
||||
|
||||
names = _run_git(
|
||||
backend,
|
||||
root,
|
||||
f"diff --name-only --diff-filter=ACMRTD {range_expr} -- .github/workflows",
|
||||
)
|
||||
if not names.ok:
|
||||
return None
|
||||
files = sorted(
|
||||
line.strip()
|
||||
for line in names.output.splitlines()
|
||||
if line.strip().startswith(_WORKFLOW_PREFIX)
|
||||
)
|
||||
if not files:
|
||||
return None
|
||||
base_ref = _fetch_remote_base(backend, root, parsed.remote, branch_name)
|
||||
if base_ref is not None:
|
||||
base_tree = _workflow_tree_at_ref(backend, root, base_ref)
|
||||
if base_tree is not None and head_tree[0] == base_tree[0]:
|
||||
# No workflow change against the trusted remote base.
|
||||
return None
|
||||
|
||||
diff = _run_git(backend, root, f"diff --binary --full-index {range_expr} -- .github/workflows")
|
||||
if not diff.ok or not diff.output:
|
||||
return None
|
||||
head_entries, head_content_hash = head_tree
|
||||
files = sorted({path for _sha, path in head_entries})
|
||||
|
||||
remote = _run_git(backend, root, "config --get remote.origin.url")
|
||||
repo = _normalize_remote(_first_line(remote.output)) if remote.ok else ""
|
||||
|
|
@ -346,25 +614,18 @@ def _workflow_change_for_push(backend: Any, parsed: ParsedGitPush) -> WorkflowPu
|
|||
fixed_args.append("--set-upstream")
|
||||
fixed_args.extend([parsed.remote, fixed_refspec])
|
||||
fixed_command = _git_command(root, " ".join(shlex.quote(arg) for arg in fixed_args))
|
||||
payload = {
|
||||
content_payload = {
|
||||
"repo": repo,
|
||||
"branch": branch_name,
|
||||
"base_sha": base_sha,
|
||||
"head_sha": head,
|
||||
"files": files,
|
||||
"diff": diff.output,
|
||||
"remote": parsed.remote,
|
||||
"local_ref": parsed.local_ref,
|
||||
"remote_ref": parsed.remote_ref,
|
||||
"fixed_refspec": fixed_refspec,
|
||||
"content_hash": head_content_hash,
|
||||
}
|
||||
return WorkflowPushChange(
|
||||
fingerprint=_fingerprint(payload),
|
||||
fingerprint=_fingerprint(content_payload),
|
||||
repo=repo,
|
||||
branch=branch_name,
|
||||
base_sha=base_sha,
|
||||
head_sha=head,
|
||||
files=files,
|
||||
head_sha=head,
|
||||
remote=parsed.remote,
|
||||
local_ref=parsed.local_ref,
|
||||
remote_ref=parsed.remote_ref,
|
||||
|
|
@ -372,17 +633,38 @@ def _workflow_change_for_push(backend: Any, parsed: ParsedGitPush) -> WorkflowPu
|
|||
)
|
||||
|
||||
|
||||
def _blocked_message(change: WorkflowPushChange, *, already_rejected: bool = False) -> ToolMessage:
|
||||
def _blocked_message(
|
||||
change: WorkflowPushChange,
|
||||
*,
|
||||
already_rejected: bool = False,
|
||||
stale: bool = False,
|
||||
blocked: bool = False,
|
||||
) -> ToolMessage:
|
||||
status = "rejected" if already_rejected else "approval_required"
|
||||
content = {
|
||||
"status": "error",
|
||||
"error_type": "WorkflowPushApprovalRequired",
|
||||
"error": (
|
||||
if blocked:
|
||||
error = change.blocked_reason or "This git push command is not recognized as safe."
|
||||
error_type = "WorkflowPushBlocked"
|
||||
elif stale:
|
||||
error = (
|
||||
"This git push includes GitHub workflow file changes. A previous approval "
|
||||
"exists for the same branch and workflow files, but the workflow content "
|
||||
"at the pushed head has changed since that approval (for example, a rebase "
|
||||
"that changed the workflow files or an amend that edited them). The thread "
|
||||
"owner must re-approve the new fingerprint before Open SWE can push it."
|
||||
)
|
||||
error_type = "WorkflowPushApprovalRequired"
|
||||
else:
|
||||
error = (
|
||||
"This git push includes GitHub workflow file changes and requires human "
|
||||
"approval before Open SWE can push it. Retry the same standalone git push "
|
||||
"after the thread owner approves the workflow diff."
|
||||
),
|
||||
"workflow_approval_status": status,
|
||||
"after the thread owner approves the workflow files."
|
||||
)
|
||||
error_type = "WorkflowPushApprovalRequired"
|
||||
content = {
|
||||
"status": "error",
|
||||
"error_type": error_type,
|
||||
"error": error,
|
||||
"workflow_approval_status": status if not blocked else "blocked",
|
||||
"fingerprint": change.fingerprint,
|
||||
"files": change.files,
|
||||
"repo": change.repo,
|
||||
|
|
@ -413,10 +695,12 @@ def _approval_slack_message(change: WorkflowPushChange) -> str:
|
|||
branch = change.branch or "the current branch"
|
||||
return (
|
||||
"*Workflow file approval required*\n"
|
||||
f"Open SWE is trying to push changes to GitHub workflow files in `{repo}` on `{branch}`.\n\n"
|
||||
f"*Files:*\n{files}\n\n"
|
||||
f"Open SWE is trying to push the workflow files below to `{repo}` on `{branch}`.\n\n"
|
||||
f"*Files at the pushed head:*\n{files}\n\n"
|
||||
f"*Fingerprint:* `{change.fingerprint}`\n\n"
|
||||
"Approve only if this exact workflow diff is expected. If the workflow files change, "
|
||||
"Approval covers the exact workflow files and content listed above at the pushed head, "
|
||||
"including future rebases or amends that replay the same workflow tree. If the set of "
|
||||
"workflow files, the branch, or the workflow-file content at the pushed head changes, "
|
||||
"a new fingerprint will be required."
|
||||
)
|
||||
|
||||
|
|
@ -454,6 +738,23 @@ async def _approval_state(request: ToolCallRequest, change: WorkflowPushChange)
|
|||
try:
|
||||
if await workflow_push_approved(thread_id, change.fingerprint):
|
||||
return "approved"
|
||||
if await workflow_push_rejected(thread_id, change.fingerprint):
|
||||
return "rejected"
|
||||
|
||||
# If the exact identity fingerprint is not approved, check whether a prior
|
||||
# approval covers the same (repo, branch, files) identity. If so, the workflow
|
||||
# tree at the pushed head changed underneath the prior approval (rebase/amend
|
||||
# that edited workflow files), so we surface a loud re-approval message rather
|
||||
# than a fresh silent pending record.
|
||||
prior = await find_workflow_push_approval(
|
||||
thread_id,
|
||||
repo=change.repo,
|
||||
branch=change.branch,
|
||||
files=change.files,
|
||||
)
|
||||
if prior is not None:
|
||||
return "stale_approval"
|
||||
|
||||
record, _created = await ensure_workflow_push_pending(
|
||||
thread_id,
|
||||
fingerprint=change.fingerprint,
|
||||
|
|
@ -472,22 +773,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):
|
||||
|
|
@ -504,6 +827,20 @@ class WorkflowPushGuardMiddleware(AgentMiddleware):
|
|||
parsed = _parse_git_push(command)
|
||||
if parsed is None:
|
||||
return None
|
||||
if isinstance(parsed, _BlockedGitPush):
|
||||
return WorkflowPushChange(
|
||||
fingerprint="",
|
||||
repo="",
|
||||
branch="",
|
||||
files=[],
|
||||
head_sha="",
|
||||
remote="origin",
|
||||
local_ref="",
|
||||
remote_ref="",
|
||||
fixed_command="",
|
||||
blocked=True,
|
||||
blocked_reason=parsed.reason,
|
||||
)
|
||||
backend = _backend(_thread_id(request))
|
||||
if backend is None:
|
||||
return None
|
||||
|
|
@ -515,13 +852,31 @@ class WorkflowPushGuardMiddleware(AgentMiddleware):
|
|||
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
||||
change: WorkflowPushChange,
|
||||
) -> ToolMessage | Command:
|
||||
if change.blocked:
|
||||
return _tool_message_for_request(_blocked_message(change, blocked=True), request)
|
||||
thread_id = _thread_id(request)
|
||||
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))
|
||||
if state == "stale_approval":
|
||||
record, _created = await ensure_workflow_push_pending(
|
||||
thread_id,
|
||||
fingerprint=change.fingerprint,
|
||||
repo=change.repo,
|
||||
branch=change.branch,
|
||||
base_sha=change.base_sha,
|
||||
head_sha=change.head_sha,
|
||||
files=change.files,
|
||||
)
|
||||
await _post_slack_approval_if_needed(request, change, record)
|
||||
return _tool_message_for_request(
|
||||
_blocked_message(change, already_rejected=state == "rejected"), request
|
||||
_blocked_message(
|
||||
change,
|
||||
already_rejected=state == "rejected",
|
||||
stale=state == "stale_approval",
|
||||
),
|
||||
request,
|
||||
)
|
||||
|
||||
def wrap_tool_call(
|
||||
|
|
|
|||
|
|
@ -163,8 +163,12 @@ Before any task that changes code, set up the repo in your sandbox, in order:
|
|||
This authors every commit. It is required for CI (e.g. Vercel preview deploys reject commits whose author email can't be resolved to a GitHub account; this email resolves). Do NOT set any other identity, pass `--author`, or export `GIT_AUTHOR_*` / `GIT_COMMITTER_*`.
|
||||
4. **Choose your branch** — Use a Sea Haven branch name: `<prefix>/<description>`, all kebab-case. Pick the prefix by the kind of work:
|
||||
- `feature/` — new functionality or an enhancement
|
||||
- `bug/` — a defect caught before it reaches production
|
||||
- `fix/` — a defect caught before it reaches production
|
||||
- `hotfix/` — a fix for a production-impacting issue
|
||||
- `chore/` — tooling, dependencies, config, or other maintenance
|
||||
- `docs/` — documentation-only changes
|
||||
- `refactor/` — internal restructuring with no behavior change
|
||||
- `release/` — release preparation
|
||||
|
||||
Keep `<description>` short and kebab-case (e.g. `feature/add-receipt-parser`). When a ticket key is resolvable from the run context, put it first: `feature/<KEY>-add-receipt-parser`; if no key is resolvable, omit it. Never commit directly to `main`. Keep the branch thread-stable: if a branch already exists for this thread, reuse it: fetch and check it out, starting from `origin/<branch>` (not the base branch) so prior commits are preserved for review — do not recreate it.
|
||||
5. **Read `AGENTS.md`** — IMMEDIATELY after cloning, you MUST check if `AGENTS.md` exists at the repository root (`{working_dir}/<repo>/AGENTS.md`). If it exists, you MUST read it IN FULL before doing ANY other work: its contents are **mandatory rules** that OVERRIDE your default behavior — treat them with the same authority as this system prompt. Violating AGENTS.md rules is a CRITICAL FAILURE. If `AGENTS.md` does not exist, skip this step.
|
||||
|
|
@ -233,26 +237,17 @@ This applies only after you've made code changes. By default, open or update a d
|
|||
|
||||
Steps, in order:
|
||||
|
||||
1. **Lint & format.** Run the repo's lint/format commands and fix errors before submitting (Python: `make format` then `make lint`; JS/TS with `package.json`: `yarn format` then `yarn lint`; Go: find the commands from `Makefile`/`go.mod`/CI). Then review your diff for correctness and unintended changes.
|
||||
1. **Lint & format.** Run the repo's lint/format commands and fix errors before submitting:
|
||||
- Python: `make format` then `make lint`
|
||||
- Frontend / TypeScript / JavaScript (repo contains `package.json`): `yarn format` then `yarn lint`
|
||||
- Go (repo contains `.go` files): find the commands from `Makefile`/`go.mod`/CI and run them
|
||||
|
||||
2. **Push & open/update the PR.** Commit locally and `git push origin <branch>`.
|
||||
- **Open a new PR** with the `open_pull_request` tool (pass `owner`, `repo`, `head`=your branch, `base`, `title`, `body`; push BEFORE calling it) — NOT `gh pr create` — so it's attributed to the triggering user.
|
||||
- **Update an existing PR** (edit body, mark ready, etc.) with `GH_TOKEN=dummy gh pr edit`. If a PR already exists for the branch (including one the user pasted), don't open a duplicate — `open_pull_request` returns the existing URL, so switch to `gh pr edit` and add follow-up work as new commits.
|
||||
Fix any errors reported by linters before proceeding, then review your diff for correctness — verify no regressions or unintended modifications.
|
||||
|
||||
**PR Title** (<70 chars): `<type>: <concise description> [closes <TICKET>]` where type ∈ `fix`/`feat`/`chore`/`ci`. Append the resolvable ticket in brackets (e.g. `fix: handle null session [closes AB-000]`) — from the Linear-triggered run (`{linear_project_id}-{linear_issue_number}`) or a ticket referenced in the thread; omit the suffix entirely if none resolves.
|
||||
2. **Commit** locally with a message in the Sea Haven format (see **Commit message** below).
|
||||
|
||||
**Frontend / TypeScript / JavaScript** (if repo contains `package.json`):
|
||||
- `yarn format` then `yarn lint`
|
||||
|
||||
**Go** (if repo contains `.go` files):
|
||||
- Figure out the lint/formatter commands (check `Makefile`, `go.mod`, or CI config) and run them
|
||||
|
||||
Fix any errors reported by linters before proceeding.
|
||||
|
||||
2. **Review your changes**: Review the diff to ensure correctness. Verify no regressions or unintended modifications.
|
||||
|
||||
3. **Submit**: Commit locally, push with `git push origin <branch>`, then open or update the PR when a PR is requested, necessary, or required by the Always Create PRs dashboard setting.
|
||||
- **Open a new PR** with the `open_pull_request` tool (pass `owner`, `repo`, `head` = your branch, `base`, `title`, `body`). By default the PR is authored by the app (`seahaven-openswe[bot]`), like GitHub-issue-triggered runs (a user can opt back into per-user attribution via the `author_prs_as_user` profile setting). Push the branch BEFORE calling it.
|
||||
3. **Push & open/update the PR.** `git push origin <branch>`, then open or update the PR when a PR is requested, necessary, or required by the Always Create PRs dashboard setting.
|
||||
- **Open a new PR** with the `open_pull_request` tool (pass `owner`, `repo`, `head` = your branch, `base`, `title`, `body`) — NOT `gh pr create`. By default the PR is authored by the app (`seahaven-openswe[bot]`), like GitHub-issue-triggered runs (a user can opt back into per-user attribution via the `author_prs_as_user` profile setting). Push the branch BEFORE calling it.
|
||||
- **Update an existing PR** (edit the body, mark ready for review, etc.) with `GH_TOKEN=dummy gh pr edit`. If a PR already exists for the branch (including one the user pasted in), do NOT open a duplicate — `open_pull_request` returns the existing PR's URL, so switch to `gh pr edit`. For follow-up changes, add a new commit on top of the existing branch history.
|
||||
|
||||
**PR Title** (under 70 characters): the title rule is **repo-aware** — first detect whether the target repo enforces a conventional-commit PR title, then pick the matching style. The repo is already cloned, so this check is cheap.
|
||||
|
|
@ -262,13 +257,13 @@ Steps, in order:
|
|||
- a `commitlint` config wired to PR titles (`commitlint.config.*`, `.commitlintrc*`, or a `commitlint` key in `package.json`);
|
||||
- `AGENTS.md` / `CONTRIBUTING.md` states a conventional-commit title requirement.
|
||||
|
||||
*If a gate is enforced* → emit a conventional-commit title `type(scope): description` and conform to the action's configuration. This **overrides** the Sea Haven no-`type:`-prefix default. Open the workflow (e.g. `.github/workflows/pr_lint.yml`) and read the allowed `types`/`scopes` so you stay inside them; if `requireScope` is false, a scope is optional. Map the work to a type: new functionality → `feat`, defect fix → `fix`, infra/CI → `ci`/`build`/`chore`, docs → `docs`, tests → `test`, refactor → `refactor`, perf → `perf`. Examples: `feat: add retry logic for transient upstream failures` or `fix(deps): pin langgraph-cli`. Do NOT rely on an escape-hatch label (e.g. `ignore-lint-pr-title`) to dodge the check — conform to the title instead. (Note: this repo's own `PR Title Lint` and upstream `langchain-ai/open-swe` both enforce this — emit a conforming `type:` title for them.)
|
||||
*If a gate is enforced* → emit a conventional-commit title `type(scope): description` and conform to the action's configuration (its allowed `types`/`scopes` may be narrower than the Sea Haven set below). Open the workflow (e.g. `.github/workflows/pr_lint.yml`) and read the allowed `types`/`scopes` so you stay inside them; if `requireScope` is false, a scope is optional. Map the work to a type: new functionality → `feat`, defect fix → `fix`, infra/CI → `ci`/`build`/`chore`, docs → `docs`, tests → `test`, refactor → `refactor`, perf → `perf`. Examples: `feat: add retry logic for transient upstream failures` or `fix(deps): pin langgraph-cli`. Do NOT rely on an escape-hatch label (e.g. `ignore-lint-pr-title`) to dodge the check — conform to the title instead. (Note: this repo's own `PR Title Lint` and upstream `langchain-ai/open-swe` both enforce this — emit a conforming `type:` title for them.)
|
||||
|
||||
*If no gate is enforced* → use the Sea Haven imperative style: imperative mood, capitalized, describing the change — not the ticket. Do NOT use a conventional-commit `type:` prefix (no `feat:`/`fix:`/`chore:`). When a ticket key is resolvable from the run context, prefix it in square brackets; otherwise omit it entirely:
|
||||
*If no gate is enforced* → use the Sea Haven default, which is conventional-commit style: `type(scope): concise description`, where type ∈ `feat` / `fix` / `docs` / `style` / `refactor` / `perf` / `test` / `build` / `ci` / `chore` / `revert` / `release` (scope optional). Imperative mood after the type; describe the change, not the ticket. When a ticket key is resolvable from the run context, append it in square brackets; otherwise omit it:
|
||||
```
|
||||
[<KEY>] Add retry logic for transient upstream failures
|
||||
feat: add retry logic for transient upstream failures [<KEY>]
|
||||
```
|
||||
With no resolvable key, use just the imperative description: `Add retry logic for transient upstream failures`. Resolve the key from the Linear-triggered run when present (`{linear_project_id}-{linear_issue_number}`), or from a Linear ticket referenced in the Slack thread / task context.
|
||||
With no resolvable key, drop the suffix: `feat: add retry logic for transient upstream failures`. Resolve the key from the Linear-triggered run when present (`{linear_project_id}-{linear_issue_number}`), or from a Linear ticket referenced in the Slack thread / task context.
|
||||
|
||||
**PR Body** — use this structure. Omit a section only when it would be empty:
|
||||
```
|
||||
|
|
@ -292,18 +287,14 @@ Steps, in order:
|
|||
- This is the GitHub-issue analog of the Linear `Refs: <KEY>` commit trailer — placed in the PR body where GitHub's auto-close looks.
|
||||
- **Default-branch caveat (don't mistake this for a bug):** GitHub only auto-closes the linked issue when the PR merges into the repo's **default branch**. In the Sea Haven flow the agent targets `dev`, not the default branch, so `Closes #<n>` will **not** close the issue at dev-merge time — it closes when `dev` is promoted to the default branch. The link still renders, and the issue closes on promotion; this is the correct, expected outcome. On repos where the agent targets the default branch directly, it closes on merge as usual.
|
||||
|
||||
3. **Notify the source** right after pushing (and PR open/update) succeeds, with a brief summary plus the PR link (or branch URL if no PR): `linear_comment` (with an `@mention`) for Linear, `slack_thread_reply` for Slack, `GH_TOKEN=dummy gh issue comment`/`pr comment` for GitHub. Skip if there is no known source channel.
|
||||
|
||||
When the target repo is public, don't reference private repos or private PR/issue numbers in the description.
|
||||
|
||||
**Commit message** — follow the Sea Haven format:
|
||||
- Imperative mood, capitalized first letter (e.g. "Add retry logic", not "Added retry logic" or "adds retry logic").
|
||||
**Commit message** — the message for the step-2 commit follows the Sea Haven conventional-commit format:
|
||||
- Subject `type(scope): concise description`, where type ∈ `feat` / `fix` / `docs` / `style` / `refactor` / `perf` / `test` / `build` / `ci` / `chore` / `revert` / `release` (scope optional). Imperative mood after the type (e.g. "add retry logic", not "added retry logic" or "adds retry logic").
|
||||
- Subject line ≤50 characters. If you need more, add a blank line and a body wrapped at 72 characters.
|
||||
- Explain *why*, not *what* — the diff already shows what changed.
|
||||
- No generic subjects ("Fix stuff", "Update code", "WIP", "Address review comments") and no self-referential phrasing ("This commit…", "This PR…", "I refactored…").
|
||||
- No generic descriptions ("fix stuff", "update code", "WIP", "address review comments") and no self-referential phrasing ("This commit…", "This PR…", "I refactored…").
|
||||
- When a ticket key is resolvable, add a `Refs: <KEY>` trailer (combine with `#<issue>` when both apply); otherwise omit the trailer.
|
||||
|
||||
This per-commit convention is independent of the repo-aware **PR title** rule above. On a repo that requires conventional PR titles **and** squash-merges, the squash commit subject becomes the PR title (e.g. `feat: …`) and so diverges from this imperative-no-prefix commit style — that's an acceptable tradeoff (the target repo's title lint wins), not a contradiction. Your own per-commit subjects still follow the Sea Haven format here.
|
||||
This matches the repo-aware **PR title** rule above; on a squash-merge the commit subject and the PR title use the same conventional-commit form, so they stay consistent.
|
||||
|
||||
**IMPORTANT: For code-change tasks, never ask the user for permission or confirmation before pushing commits or opening/updating a draft PR. Do not say "if you want, I can proceed" or "shall I open the PR?". When implementation is done and checks pass, push autonomously, and open/update a draft PR autonomously when requested, necessary, or required by the Always Create PRs dashboard setting.**
|
||||
|
||||
|
|
@ -317,7 +308,7 @@ Steps, in order:
|
|||
|
||||
**IMPORTANT: If `git push` or `gh` returns "403", "Permission denied", or another permanent authorization failure, do not retry. Report the error to the user immediately and stop.**
|
||||
|
||||
**IMPORTANT: Workflow files (`.github/workflows/`) may be changed only when explicitly requested. Any push that includes workflow-file changes requires human approval of the exact workflow diff fingerprint before it can proceed — do not attempt to bypass it.**
|
||||
**IMPORTANT: Workflow files (`.github/workflows/`) may be changed only when explicitly requested. Any push that includes workflow files at the pushed head requires human approval before it can proceed. Approval is keyed to the repo, branch, and the exact workflow files and content present at the pushed head, so rebases or amends that replay the same workflow tree do not require a fresh approval; changing the branch, the set of workflow files, or the workflow-file content at the pushed head does require a new approval. Do not attempt to bypass it.**
|
||||
|
||||
4. **Notify the source** immediately after pushing and, when applicable, PR creation/update succeeds. Include a brief summary plus the PR link or branch URL:
|
||||
- Linear-triggered: use `linear_comment` with an `@mention` of the user who triggered the task
|
||||
|
|
@ -325,6 +316,8 @@ Steps, in order:
|
|||
- GitHub-triggered: use `GH_TOKEN=dummy gh issue comment` or `GH_TOKEN=dummy gh pr comment`
|
||||
- If the task was not triggered from a known source channel (no Slack thread, no Linear ticket, no GitHub issue context), skip the notification step.
|
||||
|
||||
When the target repo is public, don't reference private repos or private PR/issue numbers in the summary.
|
||||
|
||||
Example:
|
||||
```
|
||||
@username, I've completed the implementation and opened a PR: <pr_url>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,17 @@
|
|||
# Default Prompt
|
||||
|
||||
When a repository is not explicitly mentioned, use the repository provided in the run metadata or dashboard settings. Do not assume a hardcoded repository name.
|
||||
|
||||
These apply to every repository unless the repo's own AGENTS.md / CONTRIBUTING.md overrides them.
|
||||
|
||||
**Secrets & config.** Never hardcode secrets or commit a real `.env`. Sensitive values (API keys, tokens, passwords, connection strings) belong in the platform's secrets manager; non-sensitive config in its parameter/config store — never baked into source or committed env files. Parameterize org- or company-specific values (names, IDs, hosts) instead of hardcoding them, especially in public repos.
|
||||
|
||||
**Keep docs in sync.** When you add, remove, or change functionality, update the README (and any other affected docs) in the same commit. An out-of-date README is a defect, not a follow-up.
|
||||
|
||||
**Verify before pushing.** Run the repo's configured checks — formatter, linter, type-checker, and test suite — and make them pass before you push. Discover the commands from the repo itself (`Makefile`, `package.json` scripts, CI config); don't assume a fixed toolchain.
|
||||
|
||||
**Trust only the real gates after delegating.** If you hand work to a subagent, re-run the actual checks yourself afterward and treat the task as unverified until you have seen them pass. Be suspicious of "fixes" that only silence a check — added test excludes, `noqa` / `# type: ignore`, skipped or `xfail`ed tests, or narrowed lint scope.
|
||||
|
||||
**Confirm a convention before adopting it.** A pattern in a single repo may be a one-off. Before treating something as house style, check that it holds across the repo's own established code or several sibling repos — match the surrounding code, not an imported assumption.
|
||||
|
||||
**Writing style.** Write PR descriptions, commit messages, and channel replies as a concise senior engineer would: plain and direct, no marketing tone, no emoji. Say what changed and why. Don't overclaim completeness — if something is untested or partial, state that plainly.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ requires-python = ">=3.11"
|
|||
license = { text = "MIT" }
|
||||
dependencies = [
|
||||
"deepagents==0.6.12",
|
||||
"fastapi>=0.138.2",
|
||||
"fastapi>=0.139.0",
|
||||
"uvicorn>=0.49.0",
|
||||
"httpx>=0.28.1",
|
||||
"PyJWT>=2.13.0",
|
||||
|
|
@ -19,7 +19,7 @@ dependencies = [
|
|||
"langchain-anthropic>=1.4.6",
|
||||
"langchain-aws>=0.2.0",
|
||||
"langgraph-cli[inmem]>=0.4.30",
|
||||
"langsmith==0.9.4",
|
||||
"langsmith==0.9.6",
|
||||
"langchain-openai>=1.3.3",
|
||||
"langchain-fireworks>=1.4.3",
|
||||
# langchain-fireworks 1.4.2 pins a pre-release fireworks-ai; opt in explicitly so uv resolves it.
|
||||
|
|
@ -27,7 +27,7 @@ dependencies = [
|
|||
"langchain-daytona>=0.0.7",
|
||||
"langchain-modal>=0.0.5",
|
||||
"langchain-runloop>=0.0.6",
|
||||
"exa-py>=2.15.0",
|
||||
"exa-py>=2.16.0",
|
||||
"langchain-google-genai>=4.2.4",
|
||||
"langchain-mcp-adapters>=0.3.0",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -28,8 +28,11 @@ from langchain_core.language_models import BaseChatModel
|
|||
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage
|
||||
from langchain_core.outputs import ChatGeneration, ChatResult
|
||||
|
||||
# One shell command that does the whole git workflow. Each execute() runs in a
|
||||
# fresh shell rooted at the sandbox dir, so the clone+commit+push is bundled.
|
||||
# One shell command that clones, implements, and commits. Each execute() runs in a
|
||||
# fresh shell rooted at the sandbox dir, and the `repo` checkout persists on disk
|
||||
# between calls. The push is a separate, standalone step: the workflow-push guard only
|
||||
# inspects (and gates) pushes issued as a plain `git push origin <branch>`, so bundling
|
||||
# the push inside this heredoc would trip its fail-closed check.
|
||||
_IMPLEMENT_SCRIPT = f"""
|
||||
set -e
|
||||
rm -rf repo
|
||||
|
|
@ -44,10 +47,12 @@ def greet(name):
|
|||
EOF
|
||||
git add -A
|
||||
git commit -m "{PR_TITLE}"
|
||||
git push origin {FEATURE_BRANCH}
|
||||
echo PUSHED_OK
|
||||
echo IMPLEMENTED_OK
|
||||
""".strip()
|
||||
|
||||
# Standalone push in the inspectable form the workflow-push guard expects.
|
||||
_PUSH_SCRIPT = f"cd repo && git push origin {FEATURE_BRANCH}"
|
||||
|
||||
_PLAN_URL_RE = re.compile(r"https?://[^\s\"'<>)\]|]+/plan\b")
|
||||
_ATTRIBUTION_RE = re.compile(r"@([A-Za-z0-9-]+):")
|
||||
|
||||
|
|
@ -281,6 +286,12 @@ SCRIPT_LIBRARY: dict[str, tuple[StepSpec, ...]] = {
|
|||
{"command": _IMPLEMENT_SCRIPT},
|
||||
"call-impl",
|
||||
),
|
||||
_tool_step(
|
||||
"Pushing the branch.",
|
||||
"execute",
|
||||
{"command": _PUSH_SCRIPT},
|
||||
"call-push",
|
||||
),
|
||||
_tool_step(
|
||||
"Opening a pull request.",
|
||||
"open_pull_request",
|
||||
|
|
|
|||
|
|
@ -229,8 +229,10 @@ def test_construct_system_prompt_uses_sea_haven_conventions() -> None:
|
|||
prompt = construct_system_prompt(working_dir="/workspace")
|
||||
|
||||
# Branch naming, PR structure, and commit format follow the handbook.
|
||||
assert "feature/" in prompt and "hotfix/" in prompt
|
||||
assert "Do NOT use a conventional-commit `type:` prefix" in prompt
|
||||
assert "feature/" in prompt and "hotfix/" in prompt and "chore/" in prompt
|
||||
# Commits use the Sea Haven conventional-commit format with the allowed type list.
|
||||
assert "conventional-commit format" in prompt
|
||||
assert "revert" in prompt and "release" in prompt
|
||||
assert "## Summary" in prompt and "## Validation" in prompt
|
||||
assert "## Release Note" not in prompt
|
||||
|
||||
|
|
@ -254,8 +256,8 @@ def test_construct_system_prompt_pr_title_rule_is_repo_aware() -> None:
|
|||
assert "amannn/action-semantic-pull-request" in prompt
|
||||
assert "repo-aware" in prompt
|
||||
assert "type(scope): description" in prompt or "type(scope): …" in prompt
|
||||
# ...without dropping the no-prefix default for repos that don't enforce one.
|
||||
assert "Do NOT use a conventional-commit `type:` prefix" in prompt
|
||||
# ...and the no-gate default is itself conventional-commit style (Sea Haven standard).
|
||||
assert "the Sea Haven default, which is conventional-commit style" in prompt
|
||||
|
||||
|
||||
def test_construct_system_prompt_shell_escapes_user_name() -> None:
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import pytest
|
|||
from pydantic import ValidationError
|
||||
|
||||
from agent.dashboard.team_settings import (
|
||||
DEFAULT_ORG_REVIEW_GUIDELINES,
|
||||
ORG_GUIDELINES_MAX_CHARS,
|
||||
REVIEW_TRACING_PROJECT_MAX_CHARS,
|
||||
TeamSettingsUpdate,
|
||||
_default_settings,
|
||||
get_org_review_guidelines,
|
||||
get_team_default_model,
|
||||
get_team_review_tracing_project,
|
||||
|
|
@ -33,6 +35,14 @@ def test_org_guidelines_rejects_oversized() -> None:
|
|||
TeamSettingsUpdate(org_guidelines="x" * (ORG_GUIDELINES_MAX_CHARS + 1))
|
||||
|
||||
|
||||
def test_default_settings_seed_sea_haven_org_guidelines() -> None:
|
||||
# Unset org guidelines default to the baked Sea Haven review baseline so the
|
||||
# reviewer applies it on every repo until an admin overrides it.
|
||||
assert _default_settings()["org_guidelines"] == DEFAULT_ORG_REVIEW_GUIDELINES
|
||||
assert "Sea Haven review baseline" in DEFAULT_ORG_REVIEW_GUIDELINES
|
||||
assert len(DEFAULT_ORG_REVIEW_GUIDELINES) <= ORG_GUIDELINES_MAX_CHARS
|
||||
|
||||
|
||||
def test_review_tracing_project_blank_normalizes_to_none() -> None:
|
||||
assert TeamSettingsUpdate(review_tracing_project=" ").review_tracing_project is None
|
||||
assert TeamSettingsUpdate(review_tracing_project=None).review_tracing_project is None
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
|
@ -19,25 +20,83 @@ class _Response:
|
|||
class _Backend:
|
||||
id = "sandbox-id"
|
||||
|
||||
def __init__(self, *, workflow_files: str = ".github/workflows/ci.yml") -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
workflow_files: str = ".github/workflows/ci.yml",
|
||||
tree_entries: dict[str, str] | None = None,
|
||||
base_tree_entries: dict[str, str] | None = None,
|
||||
remote_branches: set[str] | None = None,
|
||||
default_branch: str = "main",
|
||||
push_url: str = "https://github.com/langchain-ai/open-swe.git",
|
||||
push_urls: list[str] | None = None,
|
||||
url_rewrites: str = "",
|
||||
ls_tree_head_ok: bool = True,
|
||||
) -> None:
|
||||
self.workflow_files = workflow_files
|
||||
self.tree_entries = tree_entries
|
||||
# Default base tree is empty so any head workflow file is treated as a change.
|
||||
self.base_tree_entries = base_tree_entries if base_tree_entries is not None else {}
|
||||
self.remote_branches = remote_branches or {"feature"}
|
||||
self.default_branch = default_branch
|
||||
self.push_url = push_url
|
||||
# Effective push URLs (`git remote get-url --push --all`); defaults to [push_url].
|
||||
self.push_urls = push_urls if push_urls is not None else ([push_url] if push_url else [])
|
||||
self.url_rewrites = url_rewrites
|
||||
self.ls_tree_head_ok = ls_tree_head_ok
|
||||
self.commands: list[str] = []
|
||||
self.head = "a" * 40
|
||||
self.base_sha = "b" * 40
|
||||
|
||||
def _head_tree(self) -> str:
|
||||
if self.tree_entries is not None:
|
||||
return "".join(
|
||||
f"100644 blob {sha}\t{path}\n" for path, sha in sorted(self.tree_entries.items())
|
||||
)
|
||||
files = [path for path in self.workflow_files.split("\n") if path.strip()]
|
||||
return "".join(f"100644 blob {self.head}\t{path}\n" for path in files)
|
||||
|
||||
def _base_tree(self) -> str:
|
||||
if self.base_tree_entries is not None:
|
||||
return "".join(
|
||||
f"100644 blob {sha}\t{path}\n"
|
||||
for path, sha in sorted(self.base_tree_entries.items())
|
||||
)
|
||||
return self._head_tree()
|
||||
|
||||
def _fetch_branch(self, command: str) -> _Response:
|
||||
# Format: "git [-C /repo] fetch <url> <branch>"
|
||||
idx = command.find(" fetch ")
|
||||
if idx == -1:
|
||||
return _Response("", 1)
|
||||
parts = command[idx + len(" fetch ") :].split()
|
||||
if len(parts) < 2:
|
||||
return _Response("", 1)
|
||||
branch = parts[-1]
|
||||
if branch in self.remote_branches:
|
||||
return _Response("")
|
||||
return _Response("", 1)
|
||||
|
||||
def execute(self, command: str, *, timeout: int | None = None) -> _Response:
|
||||
self.commands.append(command)
|
||||
if "rev-parse --show-toplevel" in command:
|
||||
return _Response("/repo\n")
|
||||
if "rev-parse --verify refs/remotes/origin/feature" in command:
|
||||
return _Response("", 1)
|
||||
if "symbolic-ref --short refs/remotes/origin/HEAD" in command:
|
||||
return _Response("origin/main\n")
|
||||
if f"merge-base {self.head} origin/main" in command:
|
||||
return _Response("base-sha\n")
|
||||
if "diff --name-only" in command:
|
||||
return _Response(f"{self.workflow_files}\n" if self.workflow_files else "")
|
||||
if "diff --binary --full-index" in command:
|
||||
return _Response("diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml\n")
|
||||
if "config --get-regexp" in command:
|
||||
return _Response(f"{self.url_rewrites}\n") if self.url_rewrites else _Response("", 1)
|
||||
if "remote get-url --push" in command:
|
||||
if not self.push_urls:
|
||||
return _Response("", 1)
|
||||
return _Response("".join(f"{url}\n" for url in self.push_urls))
|
||||
if "ls-tree -r" in command:
|
||||
if "FETCH_HEAD" in command:
|
||||
return _Response(self._base_tree())
|
||||
return _Response(self._head_tree()) if self.ls_tree_head_ok else _Response("", 1)
|
||||
if " fetch " in command:
|
||||
return self._fetch_branch(command)
|
||||
if "ls-remote --symref" in command:
|
||||
return _Response(
|
||||
f"ref: refs/heads/{self.default_branch}\tHEAD\n{self.base_sha}\tHEAD\n"
|
||||
)
|
||||
if "config --get remote.origin.url" in command:
|
||||
return _Response("git@github.com:langchain-ai/open-swe.git\n")
|
||||
if "rev-parse --abbrev-ref HEAD" in command:
|
||||
|
|
@ -92,11 +151,101 @@ def test_parse_git_push_supports_git_c_and_cd() -> None:
|
|||
remote_ref="feature",
|
||||
set_upstream=True,
|
||||
)
|
||||
assert guard._parse_git_push("git status && git push") is None
|
||||
assert guard._parse_git_push("git push origin feature; git push origin evil:feature") is None
|
||||
|
||||
|
||||
def test_workflow_change_for_push_fingerprints_workflow_diff() -> None:
|
||||
def test_parse_git_push_blocks_unsafe_and_unrecognized_forms() -> None:
|
||||
assert isinstance(guard._parse_git_push("git status && git push"), guard._BlockedGitPush)
|
||||
assert isinstance(
|
||||
guard._parse_git_push("git push origin feature; git push origin evil:feature"),
|
||||
guard._BlockedGitPush,
|
||||
)
|
||||
assert isinstance(
|
||||
guard._parse_git_push("git push --force origin feature"), guard._BlockedGitPush
|
||||
)
|
||||
assert isinstance(guard._parse_git_push("git push origin"), guard._BlockedGitPush)
|
||||
assert isinstance(
|
||||
guard._parse_git_push("git push origin HEAD~1:feature"), guard._BlockedGitPush
|
||||
)
|
||||
assert guard._parse_git_push("git status") is None
|
||||
|
||||
|
||||
def test_parse_git_push_ignores_commands_without_a_push() -> None:
|
||||
# Chained or heredoc commands that never `git push` are not our concern — the
|
||||
# fail-closed blocking is reserved for commands that actually push.
|
||||
assert guard._parse_git_push("echo planning && ls") is None
|
||||
assert guard._parse_git_push("git add -A && git commit -m 'x'") is None
|
||||
assert guard._parse_git_push("cat > f <<'EOF'\nhi\nEOF") is None
|
||||
# "push" inside a commit message is not a push subcommand.
|
||||
assert guard._parse_git_push("git commit -m 'push it real good' && ls") is None
|
||||
# A standalone `cd <dir> && git push origin <branch>` stays inspectable, not blocked.
|
||||
parsed = guard._parse_git_push("cd repo && git push origin feature")
|
||||
assert isinstance(parsed, guard.ParsedGitPush)
|
||||
assert parsed.repo_dir == "repo"
|
||||
assert parsed.remote_ref == "feature"
|
||||
|
||||
|
||||
def test_parse_git_push_still_catches_quote_obfuscated_push() -> None:
|
||||
# Shell-quoting that the shell would run as a real `git push` must not slip past the
|
||||
# guard just because the raw string lacks a literal "push" token: it is detected and
|
||||
# inspected, never passed through as None.
|
||||
parsed = guard._parse_git_push('git "pu""sh" origin feature')
|
||||
assert isinstance(parsed, guard.ParsedGitPush)
|
||||
assert parsed.remote_ref == "feature"
|
||||
assert guard._tokens_invoke_git_push(["git", "status", "&&", "git", "push"]) is True
|
||||
assert guard._tokens_invoke_git_push(["command", "git", "push", "origin", "x"]) is True
|
||||
assert guard._tokens_invoke_git_push(["git", "-c", "protocol.version=2", "push"]) is True
|
||||
assert guard._tokens_invoke_git_push(["git", "commit", "-m", "push"]) is False
|
||||
# A value-option must not swallow a shell operator as its "value" and thereby hide the
|
||||
# real `git push` that follows the operator.
|
||||
assert guard._tokens_invoke_git_push(["git", "-c", ";", "git", "push"]) is True
|
||||
|
||||
|
||||
def test_parse_git_push_fails_closed_on_metachar_obfuscated_push() -> None:
|
||||
# `shlex` splits only on whitespace and performs no expansion, so a push can hide behind
|
||||
# a fused separator or a variable/command expansion. Each of these is a real push the
|
||||
# sandbox shell would run, and must fail closed rather than pass through unguarded.
|
||||
for command in (
|
||||
"true;git push origin HEAD",
|
||||
"echo hi|git push origin HEAD",
|
||||
"git${IFS}push origin HEAD",
|
||||
"$(echo git) push origin HEAD",
|
||||
"git $(printf push) origin main",
|
||||
"cmd=push; git ${cmd} origin main",
|
||||
"git -c ; git push origin main",
|
||||
"git -c | git push origin main",
|
||||
"(git push origin main)",
|
||||
"(git push -u origin HEAD)",
|
||||
"{ git push origin main; }",
|
||||
"git $'push' origin main",
|
||||
"git $'pus'$'h' origin main",
|
||||
r"git $'\x70ush' origin main",
|
||||
r"git $'\160ush' origin main",
|
||||
):
|
||||
assert isinstance(guard._parse_git_push(command), guard._BlockedGitPush), command
|
||||
|
||||
|
||||
def test_parse_git_push_allows_legitimate_metachar_commands() -> None:
|
||||
# Standalone operators / redirects around a non-push git command (or a push mentioned in
|
||||
# a commit message) are common and must not be over-blocked. Metacharacters that are
|
||||
# literal because they sit inside a quoted commit message must not read as separators.
|
||||
assert guard._parse_git_push("git commit -m 'push it real good' && ls") is None
|
||||
assert guard._parse_git_push('git commit -m "push & shove (v2)" && ls') is None
|
||||
assert guard._parse_git_push('git commit -m "$MSG about push" && npm test') is None
|
||||
assert guard._parse_git_push("git log | grep push") is None
|
||||
assert guard._parse_git_push("git diff > push.txt") is None
|
||||
# A non-push git command with an unquoted variable must not be blocked just for the `$`.
|
||||
assert guard._parse_git_push("git checkout $BRANCH") is None
|
||||
assert guard._parse_git_push("git log --grep=$PATTERN | head") is None
|
||||
# Legitimate ANSI-C quoting (a tab/newline in a git format or message) is not a push.
|
||||
assert guard._parse_git_push(r"git log --pretty=$'%h\t%s'") is None
|
||||
assert guard._parse_git_push(r"git commit -m $'line1\nline2'") is None
|
||||
# The standard `cd <dir> && git push` form is recognized, not blocked as obfuscation.
|
||||
parsed = guard._parse_git_push("cd repo && git push origin feature")
|
||||
assert isinstance(parsed, guard.ParsedGitPush)
|
||||
assert parsed.remote_ref == "feature"
|
||||
|
||||
|
||||
def test_workflow_change_for_push_fingerprints_head_workflow_tree() -> None:
|
||||
backend = _Backend()
|
||||
change = guard._workflow_change_for_push(
|
||||
backend,
|
||||
|
|
@ -153,6 +302,12 @@ async def test_unapproved_workflow_push_blocks_and_posts_slack(
|
|||
async def fake_approved(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_rejected(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_find_approval(*args: Any, **kwargs: Any) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
async def fake_pending(thread_id: str, **kwargs: Any) -> tuple[dict[str, Any], bool]:
|
||||
return {"fingerprint": kwargs["fingerprint"], "status": "pending", "notified": False}, True
|
||||
|
||||
|
|
@ -168,6 +323,8 @@ async def test_unapproved_workflow_push_blocks_and_posts_slack(
|
|||
posted["notified"] = fingerprint
|
||||
|
||||
monkeypatch.setattr(guard, "workflow_push_approved", fake_approved)
|
||||
monkeypatch.setattr(guard, "workflow_push_rejected", fake_rejected)
|
||||
monkeypatch.setattr(guard, "find_workflow_push_approval", fake_find_approval)
|
||||
monkeypatch.setattr(guard, "ensure_workflow_push_pending", fake_pending)
|
||||
monkeypatch.setattr(guard, "post_slack_thread_reply_with_ts", fake_post)
|
||||
monkeypatch.setattr(guard, "mark_workflow_push_notified", fake_notified)
|
||||
|
|
@ -204,7 +361,11 @@ async def test_approved_workflow_push_elevates_and_restores(
|
|||
refreshed.append(dict(permissions))
|
||||
return True
|
||||
|
||||
async def fake_find_approval(*args: Any, **kwargs: Any) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(guard, "workflow_push_approved", fake_approved)
|
||||
monkeypatch.setattr(guard, "find_workflow_push_approval", fake_find_approval)
|
||||
monkeypatch.setattr(guard, "refresh_proxy_token", fake_refresh)
|
||||
|
||||
pushed_command = ""
|
||||
|
|
@ -240,7 +401,11 @@ async def test_workflow_push_restoration_falls_back_when_actions_read_unavailabl
|
|||
refreshed.append(dict(permissions))
|
||||
return "actions" not in permissions
|
||||
|
||||
async def fake_find_approval(*args: Any, **kwargs: Any) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(guard, "workflow_push_approved", fake_approved)
|
||||
monkeypatch.setattr(guard, "find_workflow_push_approval", fake_find_approval)
|
||||
monkeypatch.setattr(guard, "refresh_proxy_token", fake_refresh)
|
||||
|
||||
async def handler(_request: Any) -> ToolMessage:
|
||||
|
|
@ -255,7 +420,13 @@ async def test_workflow_push_restoration_falls_back_when_actions_read_unavailabl
|
|||
|
||||
|
||||
async def test_non_workflow_push_runs_without_approval(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
guard.SANDBOX_BACKENDS["thread-1"] = _Backend(workflow_files="")
|
||||
# Head contains workflow files, but they are identical to the trusted remote base,
|
||||
# so no approval is required for a code-only change.
|
||||
backend = _Backend(
|
||||
tree_entries={".github/workflows/ci.yml": "blob-sha-1"},
|
||||
base_tree_entries={".github/workflows/ci.yml": "blob-sha-1"},
|
||||
)
|
||||
guard.SANDBOX_BACKENDS["thread-1"] = backend
|
||||
called = False
|
||||
|
||||
async def fail_approval(*args: Any, **kwargs: Any) -> bool:
|
||||
|
|
@ -273,3 +444,549 @@ async def test_non_workflow_push_runs_without_approval(monkeypatch: pytest.Monke
|
|||
assert called is True
|
||||
assert isinstance(result, ToolMessage)
|
||||
assert result.content == "pushed"
|
||||
# The base is fetched from the effective push URL, not the `origin` remote name.
|
||||
assert any("remote get-url --push" in cmd for cmd in backend.commands)
|
||||
assert any(f" fetch {backend.push_url}" in cmd for cmd in backend.commands)
|
||||
|
||||
|
||||
async def test_stale_workflow_approval_is_loud_and_blocks(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
guard.SANDBOX_BACKENDS["thread-1"] = _Backend()
|
||||
posted: dict[str, Any] = {}
|
||||
|
||||
async def fake_approved(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_rejected(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_find_approval(*args: Any, **kwargs: Any) -> dict[str, Any] | None:
|
||||
return {"fingerprint": "old-fp", "status": "approved", "decided_at": "2024-01-01T00:00:00"}
|
||||
|
||||
async def fake_pending(thread_id: str, **kwargs: Any) -> tuple[dict[str, Any], bool]:
|
||||
return {"fingerprint": kwargs["fingerprint"], "status": "pending", "notified": False}, True
|
||||
|
||||
async def fake_post(
|
||||
channel_id: str, thread_ts: str, message: str, **kwargs: Any
|
||||
) -> tuple[str, None]:
|
||||
posted.update(
|
||||
channel_id=channel_id, thread_ts=thread_ts, message=message, blocks=kwargs["blocks"]
|
||||
)
|
||||
return "1700000000.000300", None
|
||||
|
||||
async def fake_notified(thread_id: str, fingerprint: str) -> None:
|
||||
posted["notified"] = fingerprint
|
||||
|
||||
monkeypatch.setattr(guard, "workflow_push_approved", fake_approved)
|
||||
monkeypatch.setattr(guard, "workflow_push_rejected", fake_rejected)
|
||||
monkeypatch.setattr(guard, "find_workflow_push_approval", fake_find_approval)
|
||||
monkeypatch.setattr(guard, "ensure_workflow_push_pending", fake_pending)
|
||||
monkeypatch.setattr(guard, "post_slack_thread_reply_with_ts", fake_post)
|
||||
monkeypatch.setattr(guard, "mark_workflow_push_notified", fake_notified)
|
||||
|
||||
async def handler(_request: Any) -> ToolMessage:
|
||||
return ToolMessage(content="pushed", tool_call_id="call-1")
|
||||
|
||||
result = await guard.WorkflowPushGuardMiddleware().awrap_tool_call(_Request(), handler)
|
||||
|
||||
assert isinstance(result, ToolMessage)
|
||||
assert result.status == "error"
|
||||
payload = json.loads(str(result.content))
|
||||
assert payload["workflow_approval_status"] == "approval_required"
|
||||
assert "changed since that approval" in payload["error"]
|
||||
assert posted["channel_id"] == "C123"
|
||||
|
||||
|
||||
async def test_rebased_workflow_push_keeps_fingerprint_for_same_tree(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# Fingerprint is based on the workflow tree at head, not on a diff against origin.
|
||||
backend = _Backend(
|
||||
tree_entries={".github/workflows/ci.yml": "blob-sha-1"},
|
||||
)
|
||||
backend.head = "b" * 40
|
||||
guard.SANDBOX_BACKENDS["thread-1"] = backend
|
||||
|
||||
async def fake_approved(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_rejected(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_find_approval(*args: Any, **kwargs: Any) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
async def fake_pending(thread_id: str, **kwargs: Any) -> tuple[dict[str, Any], bool]:
|
||||
return {"fingerprint": kwargs["fingerprint"], "status": "pending", "notified": False}, True
|
||||
|
||||
async def fake_post(*args: Any, **kwargs: Any) -> tuple[str, None]:
|
||||
return "1700000000.000300", None
|
||||
|
||||
async def fake_notified(*args: Any, **kwargs: Any) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(guard, "workflow_push_approved", fake_approved)
|
||||
monkeypatch.setattr(guard, "workflow_push_rejected", fake_rejected)
|
||||
monkeypatch.setattr(guard, "find_workflow_push_approval", fake_find_approval)
|
||||
monkeypatch.setattr(guard, "ensure_workflow_push_pending", fake_pending)
|
||||
monkeypatch.setattr(guard, "post_slack_thread_reply_with_ts", fake_post)
|
||||
monkeypatch.setattr(guard, "mark_workflow_push_notified", fake_notified)
|
||||
|
||||
async def handler(_request: Any) -> ToolMessage:
|
||||
return ToolMessage(content="pushed", tool_call_id="call-1")
|
||||
|
||||
result = await guard.WorkflowPushGuardMiddleware().awrap_tool_call(_Request(), handler)
|
||||
assert isinstance(result, ToolMessage)
|
||||
assert result.status == "error"
|
||||
payload = json.loads(str(result.content))
|
||||
assert payload["fingerprint"]
|
||||
assert payload["files"] == [".github/workflows/ci.yml"]
|
||||
|
||||
# Rebase with a new head but the same workflow tree -> fingerprint stays stable.
|
||||
backend.head = "c" * 40
|
||||
result2 = await guard.WorkflowPushGuardMiddleware().awrap_tool_call(_Request(), handler)
|
||||
assert isinstance(result2, ToolMessage)
|
||||
payload2 = json.loads(str(result2.content))
|
||||
assert payload2["fingerprint"] == payload["fingerprint"]
|
||||
|
||||
# A content change to the same workflow file produces a different fingerprint.
|
||||
backend.tree_entries = {".github/workflows/ci.yml": "blob-sha-2"}
|
||||
result3 = await guard.WorkflowPushGuardMiddleware().awrap_tool_call(_Request(), handler)
|
||||
assert isinstance(result3, ToolMessage)
|
||||
payload3 = json.loads(str(result3.content))
|
||||
assert payload3["fingerprint"] != payload["fingerprint"]
|
||||
|
||||
|
||||
async def test_deleted_workflow_file_requires_approval(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# Deleting a workflow file means it is no longer in the head workflow tree. The
|
||||
# guard should still trigger if there are other workflow files at head; if the last
|
||||
# workflow file is deleted, the push is no longer workflow-guarded.
|
||||
backend = _Backend(
|
||||
tree_entries={
|
||||
".github/workflows/ci.yml": "blob-sha-1",
|
||||
".github/workflows/other.yml": "blob-sha-2",
|
||||
},
|
||||
)
|
||||
guard.SANDBOX_BACKENDS["thread-1"] = backend
|
||||
|
||||
async def fake_approved(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_rejected(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_find_approval(*args: Any, **kwargs: Any) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
async def fake_pending(thread_id: str, **kwargs: Any) -> tuple[dict[str, Any], bool]:
|
||||
return {"fingerprint": kwargs["fingerprint"], "status": "pending", "notified": False}, True
|
||||
|
||||
async def fake_post(*args: Any, **kwargs: Any) -> tuple[str, None]:
|
||||
return "1700000000.000300", None
|
||||
|
||||
async def fake_notified(*args: Any, **kwargs: Any) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(guard, "workflow_push_approved", fake_approved)
|
||||
monkeypatch.setattr(guard, "workflow_push_rejected", fake_rejected)
|
||||
monkeypatch.setattr(guard, "find_workflow_push_approval", fake_find_approval)
|
||||
monkeypatch.setattr(guard, "ensure_workflow_push_pending", fake_pending)
|
||||
monkeypatch.setattr(guard, "post_slack_thread_reply_with_ts", fake_post)
|
||||
monkeypatch.setattr(guard, "mark_workflow_push_notified", fake_notified)
|
||||
|
||||
async def handler(_request: Any) -> ToolMessage:
|
||||
return ToolMessage(content="pushed", tool_call_id="call-1")
|
||||
|
||||
# Push with the full workflow tree present requires approval.
|
||||
result = await guard.WorkflowPushGuardMiddleware().awrap_tool_call(_Request(), handler)
|
||||
assert isinstance(result, ToolMessage)
|
||||
assert result.status == "error"
|
||||
payload = json.loads(str(result.content))
|
||||
assert payload["workflow_approval_status"] == "approval_required"
|
||||
assert ".github/workflows/ci.yml" in payload["files"]
|
||||
|
||||
# A push whose head tree has deleted ci.yml but still contains other.yml still
|
||||
# requires approval, with a different fingerprint.
|
||||
backend.tree_entries = {".github/workflows/other.yml": "blob-sha-2"}
|
||||
result2 = await guard.WorkflowPushGuardMiddleware().awrap_tool_call(_Request(), handler)
|
||||
assert isinstance(result2, ToolMessage)
|
||||
payload2 = json.loads(str(result2.content))
|
||||
assert payload2["workflow_approval_status"] == "approval_required"
|
||||
assert payload2["files"] == [".github/workflows/other.yml"]
|
||||
assert payload2["fingerprint"] != payload["fingerprint"]
|
||||
|
||||
|
||||
async def test_base_poisoning_does_not_bypass_guard(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# The sandbox can rewrite refs/remotes/origin/*; the guard must use the workflow
|
||||
# tree at the pushed head, not a diff against a remote ref. This backend records
|
||||
# whether any command touches the remote-tracking ref.
|
||||
backend = _Backend(
|
||||
tree_entries={
|
||||
".github/workflows/ci.yml": "blob-sha-1",
|
||||
".github/workflows/evil.yml": "blob-sha-evil",
|
||||
},
|
||||
)
|
||||
guard.SANDBOX_BACKENDS["thread-1"] = backend
|
||||
|
||||
async def fake_approved(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_rejected(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_find_approval(*args: Any, **kwargs: Any) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
async def fake_pending(thread_id: str, **kwargs: Any) -> tuple[dict[str, Any], bool]:
|
||||
return {"fingerprint": kwargs["fingerprint"], "status": "pending", "notified": False}, True
|
||||
|
||||
async def fake_post(*args: Any, **kwargs: Any) -> tuple[str, None]:
|
||||
return "1700000000.000300", None
|
||||
|
||||
async def fake_notified(*args: Any, **kwargs: Any) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(guard, "workflow_push_approved", fake_approved)
|
||||
monkeypatch.setattr(guard, "workflow_push_rejected", fake_rejected)
|
||||
monkeypatch.setattr(guard, "find_workflow_push_approval", fake_find_approval)
|
||||
monkeypatch.setattr(guard, "ensure_workflow_push_pending", fake_pending)
|
||||
monkeypatch.setattr(guard, "post_slack_thread_reply_with_ts", fake_post)
|
||||
monkeypatch.setattr(guard, "mark_workflow_push_notified", fake_notified)
|
||||
|
||||
async def handler(_request: Any) -> ToolMessage:
|
||||
return ToolMessage(content="pushed", tool_call_id="call-1")
|
||||
|
||||
result = await guard.WorkflowPushGuardMiddleware().awrap_tool_call(_Request(), handler)
|
||||
|
||||
assert isinstance(result, ToolMessage)
|
||||
assert result.status == "error"
|
||||
payload = json.loads(str(result.content))
|
||||
assert payload["workflow_approval_status"] == "approval_required"
|
||||
# The evil workflow file present at head must appear in the approval list.
|
||||
assert ".github/workflows/evil.yml" in payload["files"]
|
||||
# No remote-tracking ref should have been consulted.
|
||||
assert not any("refs/remotes/origin" in cmd for cmd in backend.commands)
|
||||
|
||||
|
||||
async def test_exact_rejection_checked_before_stale_approval(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
backend = _Backend()
|
||||
guard.SANDBOX_BACKENDS["thread-1"] = backend
|
||||
|
||||
async def fake_approved(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_rejected(thread_id: str, fingerprint: str) -> bool:
|
||||
return True
|
||||
|
||||
# A prior approval for the same repo/branch/files exists; the stale path would
|
||||
# normally match. But because the exact fingerprint is rejected, we must return
|
||||
# "rejected", not "stale_approval".
|
||||
async def fake_find_approval(*args: Any, **kwargs: Any) -> dict[str, Any] | None:
|
||||
return {"fingerprint": "old-fp", "status": "approved", "decided_at": "2024-01-01T00:00:00"}
|
||||
|
||||
async def fake_pending(*args: Any, **kwargs: Any) -> tuple[dict[str, Any], bool]:
|
||||
raise AssertionError("should not create a new pending record")
|
||||
|
||||
monkeypatch.setattr(guard, "workflow_push_approved", fake_approved)
|
||||
monkeypatch.setattr(guard, "workflow_push_rejected", fake_rejected)
|
||||
monkeypatch.setattr(guard, "find_workflow_push_approval", fake_find_approval)
|
||||
monkeypatch.setattr(guard, "ensure_workflow_push_pending", fake_pending)
|
||||
|
||||
async def handler(_request: Any) -> ToolMessage:
|
||||
return ToolMessage(content="pushed", tool_call_id="call-1")
|
||||
|
||||
result = await guard.WorkflowPushGuardMiddleware().awrap_tool_call(_Request(), handler)
|
||||
|
||||
assert isinstance(result, ToolMessage)
|
||||
assert result.status == "error"
|
||||
payload = json.loads(str(result.content))
|
||||
assert payload["workflow_approval_status"] == "rejected"
|
||||
|
||||
|
||||
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 == []
|
||||
|
||||
|
||||
def test_parse_git_push_guards_wrapped_and_optioned_forms() -> None:
|
||||
# Path-qualified and wrapped git invocations must still be recognized as pushes
|
||||
# (so the guard engages), not passed through as "not a git command".
|
||||
for command in (
|
||||
"/usr/bin/git push origin feature",
|
||||
"command git push origin feature",
|
||||
"env FOO=bar git push origin feature",
|
||||
):
|
||||
parsed = guard._parse_git_push(command)
|
||||
assert isinstance(parsed, guard.ParsedGitPush), command
|
||||
assert parsed.remote_ref == "feature"
|
||||
|
||||
# A push carrying unsupported global options, an unknown wrapper, or a value-taking
|
||||
# wrapper option cannot be normalized, so it is blocked (fail closed) rather than run
|
||||
# unguarded. None of these may return None.
|
||||
for command in (
|
||||
"git -c protocol.version=2 push origin feature",
|
||||
"git --git-dir=.git push origin feature",
|
||||
"git --no-pager push origin feature",
|
||||
"nice -n 10 git push origin feature",
|
||||
"sudo -u ci git push origin feature",
|
||||
"timeout 5 git push origin feature",
|
||||
"ionice -c 2 git push origin feature",
|
||||
):
|
||||
result = guard._parse_git_push(command)
|
||||
assert result is not None, command
|
||||
assert isinstance(result, guard._BlockedGitPush), command
|
||||
|
||||
# A non-push git command is still ignored.
|
||||
assert guard._parse_git_push("/usr/bin/git status") is None
|
||||
|
||||
|
||||
async def test_base_fetched_from_push_url_not_origin_remote(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# A changed workflow file at head (vs the trusted base fetched from the push URL) must
|
||||
# require approval, and the base must be fetched from the push URL, never the bare
|
||||
# `origin` remote name (which the sandbox can repoint via `remote set-url --push`).
|
||||
backend = _Backend(
|
||||
tree_entries={".github/workflows/ci.yml": "head-sha"},
|
||||
base_tree_entries={".github/workflows/ci.yml": "base-sha"},
|
||||
)
|
||||
guard.SANDBOX_BACKENDS["thread-1"] = backend
|
||||
|
||||
async def fake_approved(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_rejected(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_find_approval(*args: Any, **kwargs: Any) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
async def fake_pending(thread_id: str, **kwargs: Any) -> tuple[dict[str, Any], bool]:
|
||||
return {"fingerprint": kwargs["fingerprint"], "status": "pending", "notified": False}, True
|
||||
|
||||
async def fake_post(*args: Any, **kwargs: Any) -> tuple[str, None]:
|
||||
return "1700000000.000300", None
|
||||
|
||||
async def fake_notified(*args: Any, **kwargs: Any) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(guard, "workflow_push_approved", fake_approved)
|
||||
monkeypatch.setattr(guard, "workflow_push_rejected", fake_rejected)
|
||||
monkeypatch.setattr(guard, "find_workflow_push_approval", fake_find_approval)
|
||||
monkeypatch.setattr(guard, "ensure_workflow_push_pending", fake_pending)
|
||||
monkeypatch.setattr(guard, "post_slack_thread_reply_with_ts", fake_post)
|
||||
monkeypatch.setattr(guard, "mark_workflow_push_notified", fake_notified)
|
||||
|
||||
async def handler(_request: Any) -> ToolMessage:
|
||||
return ToolMessage(content="pushed", tool_call_id="call-1")
|
||||
|
||||
result = await guard.WorkflowPushGuardMiddleware().awrap_tool_call(_Request(), handler)
|
||||
|
||||
assert isinstance(result, ToolMessage)
|
||||
assert result.status == "error"
|
||||
payload = json.loads(str(result.content))
|
||||
assert payload["workflow_approval_status"] == "approval_required"
|
||||
assert any("remote get-url --push" in cmd for cmd in backend.commands)
|
||||
assert any(f" fetch {backend.push_url}" in cmd for cmd in backend.commands)
|
||||
assert not any(" fetch origin " in cmd for cmd in backend.commands)
|
||||
|
||||
|
||||
async def test_multiple_push_urls_fail_closed(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# `git push` sends to ALL configured push URLs, but `get-url --push` shows only the
|
||||
# first. If the head workflow tree matches the base fetched from the first (benign)
|
||||
# URL, the guard must still require approval rather than skip, because a second push
|
||||
# URL could land the change on the real repo. Head == base here, but multiple push
|
||||
# URLs force fail-closed (approval required).
|
||||
backend = _Backend(
|
||||
tree_entries={".github/workflows/ci.yml": "same-sha"},
|
||||
base_tree_entries={".github/workflows/ci.yml": "same-sha"},
|
||||
push_urls=[
|
||||
"https://github.com/attacker/local.git",
|
||||
"https://github.com/langchain-ai/open-swe.git",
|
||||
],
|
||||
)
|
||||
guard.SANDBOX_BACKENDS["thread-1"] = backend
|
||||
|
||||
async def fake_approved(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_rejected(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_find_approval(*args: Any, **kwargs: Any) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
async def fake_pending(thread_id: str, **kwargs: Any) -> tuple[dict[str, Any], bool]:
|
||||
return {"fingerprint": kwargs["fingerprint"], "status": "pending", "notified": False}, True
|
||||
|
||||
async def fake_post(*args: Any, **kwargs: Any) -> tuple[str, None]:
|
||||
return "1700000000.000300", None
|
||||
|
||||
async def fake_notified(*args: Any, **kwargs: Any) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(guard, "workflow_push_approved", fake_approved)
|
||||
monkeypatch.setattr(guard, "workflow_push_rejected", fake_rejected)
|
||||
monkeypatch.setattr(guard, "find_workflow_push_approval", fake_find_approval)
|
||||
monkeypatch.setattr(guard, "ensure_workflow_push_pending", fake_pending)
|
||||
monkeypatch.setattr(guard, "post_slack_thread_reply_with_ts", fake_post)
|
||||
monkeypatch.setattr(guard, "mark_workflow_push_notified", fake_notified)
|
||||
|
||||
async def handler(_request: Any) -> ToolMessage:
|
||||
raise AssertionError("push must not run without approval when push URLs are ambiguous")
|
||||
|
||||
result = await guard.WorkflowPushGuardMiddleware().awrap_tool_call(_Request(), handler)
|
||||
|
||||
assert isinstance(result, ToolMessage)
|
||||
assert result.status == "error"
|
||||
payload = json.loads(str(result.content))
|
||||
assert payload["workflow_approval_status"] == "approval_required"
|
||||
|
||||
|
||||
async def test_url_rewrite_fails_closed(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# An insteadOf/pushInsteadOf rewrite means the inspected push URL may not be the real
|
||||
# destination, so the guard must require approval (never auto-skip).
|
||||
backend = _Backend(
|
||||
tree_entries={".github/workflows/ci.yml": "same-sha"},
|
||||
base_tree_entries={".github/workflows/ci.yml": "same-sha"},
|
||||
url_rewrites="url.https://evil.example/.insteadof https://github.com/",
|
||||
)
|
||||
guard.SANDBOX_BACKENDS["thread-1"] = backend
|
||||
|
||||
async def fake_approved(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_rejected(thread_id: str, fingerprint: str) -> bool:
|
||||
return False
|
||||
|
||||
async def fake_find_approval(*args: Any, **kwargs: Any) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
async def fake_pending(thread_id: str, **kwargs: Any) -> tuple[dict[str, Any], bool]:
|
||||
return {"fingerprint": kwargs["fingerprint"], "status": "pending", "notified": False}, True
|
||||
|
||||
async def fake_post(*args: Any, **kwargs: Any) -> tuple[str, None]:
|
||||
return "1700000000.000300", None
|
||||
|
||||
async def fake_notified(*args: Any, **kwargs: Any) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(guard, "workflow_push_approved", fake_approved)
|
||||
monkeypatch.setattr(guard, "workflow_push_rejected", fake_rejected)
|
||||
monkeypatch.setattr(guard, "find_workflow_push_approval", fake_find_approval)
|
||||
monkeypatch.setattr(guard, "ensure_workflow_push_pending", fake_pending)
|
||||
monkeypatch.setattr(guard, "post_slack_thread_reply_with_ts", fake_post)
|
||||
monkeypatch.setattr(guard, "mark_workflow_push_notified", fake_notified)
|
||||
|
||||
async def handler(_request: Any) -> ToolMessage:
|
||||
raise AssertionError("push must not run without approval when a URL rewrite exists")
|
||||
|
||||
result = await guard.WorkflowPushGuardMiddleware().awrap_tool_call(_Request(), handler)
|
||||
|
||||
assert isinstance(result, ToolMessage)
|
||||
assert result.status == "error"
|
||||
payload = json.loads(str(result.content))
|
||||
assert payload["workflow_approval_status"] == "approval_required"
|
||||
|
||||
|
||||
async def test_unreadable_head_tree_blocks(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# If `ls-tree` on the head cannot be read, the guard must fail closed (block), not
|
||||
# skip the guard and run the original push.
|
||||
backend = _Backend(ls_tree_head_ok=False)
|
||||
guard.SANDBOX_BACKENDS["thread-1"] = backend
|
||||
|
||||
async def fail_approval(*args: Any, **kwargs: Any) -> bool:
|
||||
raise AssertionError("approval should not be checked for a blocked push")
|
||||
|
||||
monkeypatch.setattr(guard, "workflow_push_approved", fail_approval)
|
||||
|
||||
called = False
|
||||
|
||||
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.status == "error"
|
||||
payload = json.loads(str(result.content))
|
||||
assert payload["error_type"] == "WorkflowPushBlocked"
|
||||
|
|
|
|||
232
ui/bun.lock
232
ui/bun.lock
|
|
@ -13,7 +13,7 @@
|
|||
"@monaco-editor/react": "^4.7.0",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@pierre/diffs": "^1.2.1",
|
||||
"@pierre/trees": "1.0.0-beta.4",
|
||||
"@pierre/trees": "1.0.0-beta.5",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tanstack/react-devtools": "^0.10.0",
|
||||
"@tanstack/react-query": "^5.100.10",
|
||||
|
|
@ -26,8 +26,8 @@
|
|||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.16.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"nitro": "latest",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"nitro": "3.0.260603-beta",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-icons": "^5.6.0",
|
||||
|
|
@ -40,19 +40,19 @@
|
|||
"vite-tsconfig-paths": "^5.1.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/devtools-vite": "^0.6.0",
|
||||
"@tanstack/devtools-vite": "^0.8.1",
|
||||
"@tanstack/eslint-config": "^0.4.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/node": "^22.19.15",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"jsdom": "^27.4.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.5",
|
||||
"prettier-plugin-tailwindcss": "^0.8.0",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.1.2",
|
||||
"vite-plugin-pwa": "^1.3.0",
|
||||
"vitest": "^4.1.0",
|
||||
"web-vitals": "^5.1.0",
|
||||
|
|
@ -224,10 +224,6 @@
|
|||
|
||||
"@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
|
||||
|
||||
"@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw=="],
|
||||
|
||||
"@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ=="],
|
||||
|
|
@ -294,11 +290,11 @@
|
|||
|
||||
"@ecies/ciphers": ["@ecies/ciphers@0.2.6", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||
"@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
|
||||
|
||||
|
|
@ -448,7 +444,47 @@
|
|||
|
||||
"@oozcitak/util": ["@oozcitak/util@10.0.0", "", {}, "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.134.0", "", {}, "sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ=="],
|
||||
"@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.120.0", "", { "os": "android", "cpu": "arm" }, "sha512-WU3qtINx802wOl8RxAF1v0VvmC2O4D9M8Sv486nLeQ7iPHVmncYZrtBhB4SYyX+XZxj2PNnCcN+PW21jHgiOxg=="],
|
||||
|
||||
"@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.120.0", "", { "os": "android", "cpu": "arm64" }, "sha512-SEf80EHdhlbjZEgzeWm0ZA/br4GKMenDW3QB/gtyeTV1gStvvZeFi40ioHDZvds2m4Z9J1bUAUL8yn1/+A6iGg=="],
|
||||
|
||||
"@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.120.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xVrrbCai8R8CUIBu3CjryutQnEYhZqs1maIqDvtUCFZb8vY33H7uh9mHpL3a0JBIKoBUKjPH8+rzyAeXnS2d6A=="],
|
||||
|
||||
"@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.120.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-xyHBbnJ6mydnQUH7MAcafOkkrNzQC6T+LXgDH/3InEq2BWl/g424IMRiJVSpVqGjB+p2bd0h0WRR8iIwzjU7rw=="],
|
||||
|
||||
"@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.120.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMnVRllquXUYTeNfFKmxTTEdZ/ix1nLl0ducDzMSREoWYGVIHnOOxoKMWlCOvRr9Wk/HZqo2rh1jeumbPGPV9A=="],
|
||||
|
||||
"@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.120.0", "", { "os": "linux", "cpu": "arm" }, "sha512-tkvn2CQ7QdcsMnpfiX3fd3wA3EFsWKYlcQzq9cFw/xc89Al7W6Y4O0FgLVkVQpo0Tnq/qtE1XfkJOnRRA9S/NA=="],
|
||||
|
||||
"@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.120.0", "", { "os": "linux", "cpu": "arm" }, "sha512-WN5y135Ic42gQDk9grbwY9++fDhqf8knN6fnP+0WALlAUh4odY/BDK1nfTJRSfpJD9P3r1BwU0m3pW2DU89whQ=="],
|
||||
|
||||
"@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.120.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1GgQBCcXvFMw99EPdMy+4NZ3aYyXsxjf9kbUUg8HuAy3ZBXzOry5KfFEzT9nqmgZI1cuetvApkiJBZLAPo8uaw=="],
|
||||
|
||||
"@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.120.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gmMQ70gsPdDBgpcErvJEoWNBr7bJooSLlvOBVBSGfOzlP5NvJ3bFvnUeZZ9d+dPrqSngtonf7nyzWUTUj/U+lw=="],
|
||||
|
||||
"@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.120.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-T/kZuU0ajop0xhzVMwH5r3srC9Nqup5HaIo+3uFjIN5uPxa0LvSxC1ZqP4aQGJVW5G0z8/nCkjIfSMS91P/wzw=="],
|
||||
|
||||
"@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.120.0", "", { "os": "linux", "cpu": "none" }, "sha512-vn21KXLAXzaI3N5CZWlBr1iWeXLl9QFIMor7S1hUjUGTeUuWCoE6JZB040/ZNDwf+JXPX8Ao9KbmJq9FMC2iGw=="],
|
||||
|
||||
"@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.120.0", "", { "os": "linux", "cpu": "none" }, "sha512-SUbUxlar007LTGmSLGIC5x/WJvwhdX+PwNzFJ9f/nOzZOrCFbOT4ikt7pJIRg1tXVsEfzk5mWpGO1NFiSs4PIw=="],
|
||||
|
||||
"@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.120.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hYiPJTxyfJY2+lMBFk3p2bo0R9GN+TtpPFlRqVchL1qvLG+pznstramHNvJlw9AjaoRUHwp9IKR7UZQnRPGjgQ=="],
|
||||
|
||||
"@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.120.0", "", { "os": "linux", "cpu": "x64" }, "sha512-q+5jSVZkprJCIy3dzJpApat0InJaoxQLsJuD6DkX8hrUS61z2lHQ1Fe9L2+TYbKHXCLWbL0zXe7ovkIdopBGMQ=="],
|
||||
|
||||
"@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.120.0", "", { "os": "linux", "cpu": "x64" }, "sha512-D9QDDZNnH24e7X4ftSa6ar/2hCavETfW3uk0zgcMIrZNy459O5deTbWrjGzZiVrSWigGtlQwzs2McBP0QsfV1w=="],
|
||||
|
||||
"@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.120.0", "", { "os": "none", "cpu": "arm64" }, "sha512-TBU8ZwOUWAOUWVfmI16CYWbvh4uQb9zHnGBHsw5Cp2JUVG044OIY1CSHODLifqzQIMTXvDvLzcL89GGdUIqNrA=="],
|
||||
|
||||
"@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.120.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-WG/FOZgDJCpJnuF3ToG/K28rcOmSY7FmFmfBKYb2fmLyhDzPpUldFGV7/Fz4ru0Iz/v4KPmf8xVgO8N3lO4KHA=="],
|
||||
|
||||
"@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.120.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1T0HKGcsz/BKo77t7+89L8Qvu4f9DoleKWHp3C5sJEcbCjDOLx3m9m722bWZTY+hANlUEs+yjlK+lBFsA+vrVQ=="],
|
||||
|
||||
"@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.120.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-L7vfLzbOXsjBXV0rv/6Y3Jd9BRjPeCivINZAqrSyAOZN3moCopDN+Psq9ZrGNZtJzP8946MtlRFZ0Als0wBCOw=="],
|
||||
|
||||
"@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.120.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ys+upfqNtSu58huAhJMBKl3XCkGzyVFBlMlGPzHeFKgpFF/OdgNs1MMf8oaJIbgMH8ZxgGF7qfue39eJohmKIg=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="],
|
||||
|
||||
"@package-json/types": ["@package-json/types@0.0.12", "", {}, "sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw=="],
|
||||
|
||||
|
|
@ -458,39 +494,41 @@
|
|||
|
||||
"@pierre/theme": ["@pierre/theme@1.0.3", "", {}, "sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA=="],
|
||||
|
||||
"@pierre/trees": ["@pierre/trees@1.0.0-beta.4", "", { "dependencies": { "preact": "11.0.0-beta.0", "preact-render-to-string": "6.6.5" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-OfT1yk9ne8Te5+GB5zUY8yqE6B8BqjBHQJleH4lu8ltwNpoocZl4vXt1AzlEExpxI/pp+AFX5QG+lR3JjtTEag=="],
|
||||
"@pierre/theming": ["@pierre/theming@0.0.2", "", { "peerDependencies": { "@pierre/theme": "^1.1.0", "@shikijs/themes": "^3.0.0 || ^4.0.0", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0", "shiki": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@pierre/theme", "@shikijs/themes", "react", "react-dom", "shiki"] }, "sha512-QM1M4stXfnzfaE8I8YbjXSApV8c+2dBsXJj8eYg9WTpBR/cTmCZIcfGnN4p13iRrYu2Br/R/OJfEL7uR8Qjctw=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gCYzGOSkYY6Z034suzd20euvds7lPzMEEla62DJGE/ZAlR4OMBnNbvnBSsIGUCAr52gaWMsloGxP4tVGtN5aCA=="],
|
||||
"@pierre/trees": ["@pierre/trees@1.0.0-beta.5", "", { "dependencies": { "@pierre/theming": "0.0.2", "preact": "11.0.0-beta.0", "preact-render-to-string": "6.6.5" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-IzxkB9qv6GLbeEXObhlAD205LfYHiLeRwJdnaIdX0f5keTZF4X9EfiuEQ3QiyxOxouVVmUX3rX7m6a8zNMo/wA=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JQBD77MNgu+4Z6RAyg69acugdrhhVoWesr3l47zohYZ2YV2fwkWMArkN/2p4l6Ei+Sno7W5q+UsKdVWq5Ens0w=="],
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.4", "", { "os": "android", "cpu": "arm64" }, "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-p/8cXUTK4Sob604e+xxPhVSbDFf29E6J0l/xESM9rdCfn3aDai3nEs6TnMHUsdD5aNlFz0+gDbiGlozLKGa2YA=="],
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KbtOSlVv6fElujiZWMcC3aQYhEwLVVf073RcwlSmpGQvIsKZFUqc0ef4sjUuurRwfbiI6JJXji9DQn+86hawmQ=="],
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9fZ9i0o0/MQaw7om6Z6TsT7tfCk0jtbEFtC+aPqZL5RNsGWNcHvn6EHgL3dAprjq+AZzPTAQjg2JtpJaMt+6pg=="],
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-+tog7T66i+yFyIuuAnjL6xmW182W/qTBOUt6BtQ6lBIM1Eikh/fSMz4HGgvuCp5uU0zuIVWng7kDYthjCMOHcg=="],
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.4", "", { "os": "linux", "cpu": "arm" }, "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4b7yruLIIj/oZ3GpcLOvxcLCLDMraohn3IhQfN2hBP4w9UekG0DTIajWguJosRGfySf/+h/NwRUiMKoCpxCrqQ=="],
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-QRDOVZd0bhQ5jLsUsCC3dUxDWdTSVY9WMznowZgCGOrZfLLgctWpelhUASEiBwsXfat/JwYnVd1EaxMhqyT+UQ=="],
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-ypxT+Hq76NFG7woFbNbySnGEajFuYuIXeKz/jfCU+lXUoxfi3zLE6OG/ZQNeK3RpZSYJlAe2bokpsQ046CaieQ=="],
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-IdovCmfROFmpTLahdecTDFL74aLERVYN68F/mLZjfVh6LfoplPfI6deyHNMTcVujbokDV5k05XrFO22zfv+qjg=="],
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pcA8xlFp2tyk9T2R6Fi/rPe3bQ1MA+sSMDNUU5Ogu80GHOatkE4P8YCreGAvZErm5Ho2YRXnyvNrWiRncfVysQ=="],
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.0", "", { "os": "none", "cpu": "arm64" }, "sha512-4+fexHayrLCWpriPh4c6dNvL4an34DEZCG7zOM/FD5QNF6h8DT+bDXzyB/kfC8lDJbaFb7jKShtnjDQFXVQEjg=="],
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-SbL++MNmOw6QamrwIGDMSSfM4ceTzFr+RjbOExJSLLBinScU4WI5OdA413h1qwPw2yH7lVF1+H4svQ+6mSXKTQ=="],
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.4", "", { "os": "none", "cpu": "arm64" }, "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-+xTE6XC7wBgk0VKRXGG+QAnyW5S9b8vfsFpiMjf0waQTmSQSU8onsH/beyZ8X4aXVveJnotiy7VDjLOaW8bTrg=="],
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.4", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Ogji1TQNqH3ACLnYr+1Ns1nyrJ0CO2P585u9Hsh02pXvtFiFpgtgT2b3P4PnCOU86VVCvqtAeCN4OftMT8KU4w=="],
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@rollup/plugin-babel": ["@rollup/plugin-babel@6.1.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.18.6", "@rollup/pluginutils": "^5.0.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "@types/babel__core": "^7.1.9", "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["@types/babel__core", "rollup"] }, "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA=="],
|
||||
|
||||
|
|
@ -622,15 +660,15 @@
|
|||
|
||||
"@tanstack/devtools": ["@tanstack/devtools@0.12.2", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/keyboard": "^1.3.3", "@solid-primitives/resize-observer": "^2.1.3", "@tanstack/devtools-client": "0.0.6", "@tanstack/devtools-event-bus": "0.4.1", "@tanstack/devtools-ui": "0.5.2", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" }, "bin": { "intent": "./bin/intent.js" } }, "sha512-Xdl8pLzoDUvXaclQ0poY36WAPx0jEHk8vqUFd8FYFUm1BMshtB7RnTgD1HE9jCAXODxqw9I0gXBiUZLK3o3+Bw=="],
|
||||
|
||||
"@tanstack/devtools-client": ["@tanstack/devtools-client@0.0.6", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1" } }, "sha512-f85ZJXJnDIFOoykG/BFIixuAevJovCvJF391LPs6YjBAPhGYC50NWlx1y4iF/UmK5/cCMx+/JqI5SBOz7FanQQ=="],
|
||||
"@tanstack/devtools-client": ["@tanstack/devtools-client@0.0.8", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.5.0" } }, "sha512-cG3iZkGWCwN330bLBKa8+9r4Of2AXNoz2zUqcsy/4XsD3105ghVBx78cGyvJj9fSclNomPxoqAnDGXXhg1WLvA=="],
|
||||
|
||||
"@tanstack/devtools-event-bus": ["@tanstack/devtools-event-bus@0.4.1", "", { "dependencies": { "ws": "^8.18.3" } }, "sha512-cNnJ89Q021Zf883rlbBTfsaxTfi2r73/qejGtyTa7ksErF3hyDyAq1aTbo5crK9dAL7zSHh9viKY1BtMls1QOA=="],
|
||||
"@tanstack/devtools-event-bus": ["@tanstack/devtools-event-bus@0.4.2", "", { "dependencies": { "ws": "^8.18.3" } }, "sha512-2LHzhwBFlKHCcklsQrGe8TeyjHd4XAF8nuCO6wHmva5fePUkJUULbu6CsCNAlGlCi0KkEsMXZSvRdR4HgMq4yA=="],
|
||||
|
||||
"@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.3", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw=="],
|
||||
"@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.5.0", "", { "bin": { "intent": "./bin/intent.js" } }, "sha512-H+OH3zC6Vhu/K0NaVfQKknEKawc/+2PT+D3SB3Ox0V8SiMlTo0abbmH2rH0721R2aNYbjdMXA1oENOd8E2UVoA=="],
|
||||
|
||||
"@tanstack/devtools-ui": ["@tanstack/devtools-ui@0.5.2", "", { "dependencies": { "clsx": "^2.1.1", "dayjs": "^1.11.19", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-GtaMk8kaGZ9ZdR8Pu5RAfcse/ZrxzH/xsAIFtHMapLs2VMqSPFfb1NvIDO1MAAfUcub8Ix8XKQEP0uYSPzoFKw=="],
|
||||
|
||||
"@tanstack/devtools-vite": ["@tanstack/devtools-vite@0.6.1", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/generator": "^7.28.3", "@babel/parser": "^7.28.4", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@tanstack/devtools-client": "0.0.6", "@tanstack/devtools-event-bus": "0.4.1", "chalk": "^5.6.2", "launch-editor": "^2.11.1", "picomatch": "^4.0.3" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "bin": { "intent": "./bin/intent.js" } }, "sha512-4NMdFwVDoJZlh4gnKl/LJYSoEKuZr2GiFgbCITfaKYWXk6XbXilclfLwAAdoX5gF2p0mEPPv5JCuAlbJgO9aWQ=="],
|
||||
"@tanstack/devtools-vite": ["@tanstack/devtools-vite@0.8.1", "", { "dependencies": { "@tanstack/devtools-client": "0.0.8", "@tanstack/devtools-event-bus": "0.4.2", "chalk": "^5.6.2", "launch-editor": "^2.11.1", "magic-string": "^0.30.0", "oxc-parser": "^0.120.0", "picomatch": "^4.0.3" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "bin": { "intent": "./bin/intent.js" } }, "sha512-oQxOo0fI0bwhHtw/psFlIR0OS/bsKrirBxwnw2vuhCM4bjt3k4EZZsW/lvZ1+Vpouhts7LSyvngnxvGXbQ1sUQ=="],
|
||||
|
||||
"@tanstack/eslint-config": ["@tanstack/eslint-config@0.4.0", "", { "dependencies": { "@eslint/js": "^10.0.1", "@stylistic/eslint-plugin": "^5.8.0", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-n": "^17.24.0", "globals": "^17.3.0", "typescript-eslint": "^8.55.0", "vue-eslint-parser": "^10.4.0" }, "peerDependencies": { "eslint": "^9.0.0 || ^10.0.0" } }, "sha512-V+Cd81W/f65dqKJKpytbwTGx9R+IwxKAHsG/uJ3nSLYEh36hlAr54lRpstUhggQB8nf/cP733cIw8DuD2dzQUg=="],
|
||||
|
||||
|
|
@ -792,7 +830,7 @@
|
|||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.19.20", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw=="],
|
||||
"@types/node": ["@types/node@24.13.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||
|
||||
|
|
@ -874,7 +912,7 @@
|
|||
|
||||
"@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="],
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@4.1.8", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.8", "@vitest/utils": "4.1.8", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ=="],
|
||||
|
||||
|
|
@ -1164,7 +1202,7 @@
|
|||
|
||||
"dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
||||
|
||||
"dompurify": ["dompurify@3.4.8", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ=="],
|
||||
"dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="],
|
||||
|
||||
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
||||
|
||||
|
|
@ -1640,7 +1678,7 @@
|
|||
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
"marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="],
|
||||
"marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
|
|
@ -1758,7 +1796,7 @@
|
|||
|
||||
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||
|
||||
"monaco-editor": ["monaco-editor@0.52.2", "", {}, "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ=="],
|
||||
"monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
|
|
@ -1820,6 +1858,8 @@
|
|||
|
||||
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
|
||||
|
||||
"oxc-parser": ["oxc-parser@0.120.0", "", { "dependencies": { "@oxc-project/types": "^0.120.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.120.0", "@oxc-parser/binding-android-arm64": "0.120.0", "@oxc-parser/binding-darwin-arm64": "0.120.0", "@oxc-parser/binding-darwin-x64": "0.120.0", "@oxc-parser/binding-freebsd-x64": "0.120.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.120.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.120.0", "@oxc-parser/binding-linux-arm64-gnu": "0.120.0", "@oxc-parser/binding-linux-arm64-musl": "0.120.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.120.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.120.0", "@oxc-parser/binding-linux-riscv64-musl": "0.120.0", "@oxc-parser/binding-linux-s390x-gnu": "0.120.0", "@oxc-parser/binding-linux-x64-gnu": "0.120.0", "@oxc-parser/binding-linux-x64-musl": "0.120.0", "@oxc-parser/binding-openharmony-arm64": "0.120.0", "@oxc-parser/binding-wasm32-wasi": "0.120.0", "@oxc-parser/binding-win32-arm64-msvc": "0.120.0", "@oxc-parser/binding-win32-ia32-msvc": "0.120.0", "@oxc-parser/binding-win32-x64-msvc": "0.120.0" } }, "sha512-WyPWZlcIm+Fkte63FGfgFB8mAAk33aH9h5N9lphXVOHSXEBFFsmYdOBedVKly363aWABjZdaj/m9lBfEY4wt+w=="],
|
||||
|
||||
"p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
|
@ -1876,7 +1916,7 @@
|
|||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
|
||||
"postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@7.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Wjvt4scRFouioIInHf51IFNP4ltJ2EngJM+cZPGiqbKetBfmP3vpdPV8ID2S6JS6/jdo74N8+aEYH9lQr2C6sA=="],
|
||||
|
||||
|
|
@ -1890,7 +1930,7 @@
|
|||
|
||||
"prettier": ["prettier@3.8.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q=="],
|
||||
|
||||
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.4", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-UKii4RjY05SNt/WQi6/NcOn/LsT0/ILLXsxygjbRg5/YZelsSu5jTqorYHPDGq4nZy5q5hpCu+XdGZ1xaJEQgw=="],
|
||||
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.8.0", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-V8ITGH87yuBDF6JpEZTOVlUz/saAwqb8f3HRgUj8Lh+tGCcrmorhsLpYqzygwFwK0PE2Ib6Mv3M7T/uE2tZV1g=="],
|
||||
|
||||
"pretty-bytes": ["pretty-bytes@6.1.1", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="],
|
||||
|
||||
|
|
@ -1922,8 +1962,6 @@
|
|||
|
||||
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||
|
||||
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
|
||||
|
|
@ -1980,7 +2018,7 @@
|
|||
|
||||
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
||||
|
||||
"rolldown": ["rolldown@1.1.0", "", { "dependencies": { "@oxc-project/types": "=0.134.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.0", "@rolldown/binding-darwin-arm64": "1.1.0", "@rolldown/binding-darwin-x64": "1.1.0", "@rolldown/binding-freebsd-x64": "1.1.0", "@rolldown/binding-linux-arm-gnueabihf": "1.1.0", "@rolldown/binding-linux-arm64-gnu": "1.1.0", "@rolldown/binding-linux-arm64-musl": "1.1.0", "@rolldown/binding-linux-ppc64-gnu": "1.1.0", "@rolldown/binding-linux-s390x-gnu": "1.1.0", "@rolldown/binding-linux-x64-gnu": "1.1.0", "@rolldown/binding-linux-x64-musl": "1.1.0", "@rolldown/binding-openharmony-arm64": "1.1.0", "@rolldown/binding-wasm32-wasi": "1.1.0", "@rolldown/binding-win32-arm64-msvc": "1.1.0", "@rolldown/binding-win32-x64-msvc": "1.1.0" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-zpMvlJhs5PkXRTtKc0CaLBVI9AR/VDiJFpM+kx//hgToEca7FgMlGjaRIisXBcb19T76LswgmKECSQ96hjWr5A=="],
|
||||
"rolldown": ["rolldown@1.1.4", "", { "dependencies": { "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.4", "@rolldown/binding-darwin-arm64": "1.1.4", "@rolldown/binding-darwin-x64": "1.1.4", "@rolldown/binding-freebsd-x64": "1.1.4", "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", "@rolldown/binding-linux-arm64-gnu": "1.1.4", "@rolldown/binding-linux-arm64-musl": "1.1.4", "@rolldown/binding-linux-ppc64-gnu": "1.1.4", "@rolldown/binding-linux-s390x-gnu": "1.1.4", "@rolldown/binding-linux-x64-gnu": "1.1.4", "@rolldown/binding-linux-x64-musl": "1.1.4", "@rolldown/binding-openharmony-arm64": "1.1.4", "@rolldown/binding-wasm32-wasi": "1.1.4", "@rolldown/binding-win32-arm64-msvc": "1.1.4", "@rolldown/binding-win32-x64-msvc": "1.1.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA=="],
|
||||
|
||||
"rollup": ["rollup@4.61.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.61.1", "@rollup/rollup-android-arm64": "4.61.1", "@rollup/rollup-darwin-arm64": "4.61.1", "@rollup/rollup-darwin-x64": "4.61.1", "@rollup/rollup-freebsd-arm64": "4.61.1", "@rollup/rollup-freebsd-x64": "4.61.1", "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", "@rollup/rollup-linux-arm-musleabihf": "4.61.1", "@rollup/rollup-linux-arm64-gnu": "4.61.1", "@rollup/rollup-linux-arm64-musl": "4.61.1", "@rollup/rollup-linux-loong64-gnu": "4.61.1", "@rollup/rollup-linux-loong64-musl": "4.61.1", "@rollup/rollup-linux-ppc64-gnu": "4.61.1", "@rollup/rollup-linux-ppc64-musl": "4.61.1", "@rollup/rollup-linux-riscv64-gnu": "4.61.1", "@rollup/rollup-linux-riscv64-musl": "4.61.1", "@rollup/rollup-linux-s390x-gnu": "4.61.1", "@rollup/rollup-linux-x64-gnu": "4.61.1", "@rollup/rollup-linux-x64-musl": "4.61.1", "@rollup/rollup-openbsd-x64": "4.61.1", "@rollup/rollup-openharmony-arm64": "4.61.1", "@rollup/rollup-win32-arm64-msvc": "4.61.1", "@rollup/rollup-win32-ia32-msvc": "4.61.1", "@rollup/rollup-win32-x64-gnu": "4.61.1", "@rollup/rollup-win32-x64-msvc": "4.61.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA=="],
|
||||
|
||||
|
|
@ -2008,7 +2046,7 @@
|
|||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
"semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
|
|
@ -2182,7 +2220,7 @@
|
|||
|
||||
"typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.61.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.61.0", "@typescript-eslint/parser": "8.61.0", "@typescript-eslint/typescript-estree": "8.61.0", "@typescript-eslint/utils": "8.61.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw=="],
|
||||
|
||||
|
|
@ -2190,7 +2228,7 @@
|
|||
|
||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="],
|
||||
|
||||
|
|
@ -2250,7 +2288,7 @@
|
|||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"vite": ["vite@7.3.5", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww=="],
|
||||
"vite": ["vite@8.1.2", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.16", "rolldown": "~1.1.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ=="],
|
||||
|
||||
"vite-plugin-pwa": ["vite-plugin-pwa@1.3.0", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.4.1", "workbox-window": "^7.4.1" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-c5kMgN+ITrOtHXp8PAtk2uOIEea6XjP/unCGxOWWBzQ6qa65qj/awHg0wf+QF9E/2u9vh86LqxPwzEPNbM2r5A=="],
|
||||
|
||||
|
|
@ -2350,8 +2388,18 @@
|
|||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||
|
|
@ -2362,6 +2410,8 @@
|
|||
|
||||
"@pierre/diffs/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
|
||||
|
||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
|
||||
|
|
@ -2380,6 +2430,10 @@
|
|||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@tanstack/devtools/@tanstack/devtools-client": ["@tanstack/devtools-client@0.0.6", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1" } }, "sha512-f85ZJXJnDIFOoykG/BFIixuAevJovCvJF391LPs6YjBAPhGYC50NWlx1y4iF/UmK5/cCMx+/JqI5SBOz7FanQQ=="],
|
||||
|
||||
"@tanstack/devtools/@tanstack/devtools-event-bus": ["@tanstack/devtools-event-bus@0.4.1", "", { "dependencies": { "ws": "^8.18.3" } }, "sha512-cNnJ89Q021Zf883rlbBTfsaxTfi2r73/qejGtyTa7ksErF3hyDyAq1aTbo5crK9dAL7zSHh9viKY1BtMls1QOA=="],
|
||||
|
||||
"@tanstack/router-utils/diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
|
||||
|
||||
"@tanstack/start-plugin-core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
|
@ -2388,10 +2442,14 @@
|
|||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"@unrs/resolver-binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||
|
||||
"@unrs/resolver-binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||
|
||||
"babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
|
||||
|
|
@ -2414,14 +2472,8 @@
|
|||
|
||||
"eslint/espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
||||
|
||||
"eslint-compat-utils/semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="],
|
||||
|
||||
"eslint-plugin-import-x/semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="],
|
||||
|
||||
"eslint-plugin-n/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
|
||||
|
||||
"eslint-plugin-n/semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="],
|
||||
|
||||
"execa/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
|
@ -2438,10 +2490,14 @@
|
|||
|
||||
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"mermaid/dompurify": ["dompurify@3.4.8", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ=="],
|
||||
|
||||
"mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"nitro/rolldown": ["rolldown@1.1.0", "", { "dependencies": { "@oxc-project/types": "=0.134.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.0", "@rolldown/binding-darwin-arm64": "1.1.0", "@rolldown/binding-darwin-x64": "1.1.0", "@rolldown/binding-freebsd-x64": "1.1.0", "@rolldown/binding-linux-arm-gnueabihf": "1.1.0", "@rolldown/binding-linux-arm64-gnu": "1.1.0", "@rolldown/binding-linux-arm64-musl": "1.1.0", "@rolldown/binding-linux-ppc64-gnu": "1.1.0", "@rolldown/binding-linux-s390x-gnu": "1.1.0", "@rolldown/binding-linux-x64-gnu": "1.1.0", "@rolldown/binding-linux-x64-musl": "1.1.0", "@rolldown/binding-openharmony-arm64": "1.1.0", "@rolldown/binding-wasm32-wasi": "1.1.0", "@rolldown/binding-win32-arm64-msvc": "1.1.0", "@rolldown/binding-win32-x64-msvc": "1.1.0" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-zpMvlJhs5PkXRTtKc0CaLBVI9AR/VDiJFpM+kx//hgToEca7FgMlGjaRIisXBcb19T76LswgmKECSQ96hjWr5A=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
|
@ -2452,12 +2508,14 @@
|
|||
|
||||
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
"rolldown/@oxc-project/types": ["@oxc-project/types@0.138.0", "", {}, "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA=="],
|
||||
|
||||
"shadcn/diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
|
||||
|
||||
"shadcn/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
|
||||
|
||||
"shadcn/postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
|
||||
|
||||
"shadcn/stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="],
|
||||
|
||||
"shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
|
@ -2466,18 +2524,20 @@
|
|||
|
||||
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"streamdown/marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="],
|
||||
|
||||
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
||||
|
||||
"vitest/vite": ["vite@7.3.5", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww=="],
|
||||
|
||||
"vue-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"vue-eslint-parser/espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
||||
|
||||
"vue-eslint-parser/semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="],
|
||||
|
||||
"workbox-build/pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="],
|
||||
|
||||
"@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
|
@ -2506,8 +2566,14 @@
|
|||
|
||||
"@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
"@tanstack/devtools/@tanstack/devtools-client/@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.3", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw=="],
|
||||
|
||||
"@unrs/resolver-binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
|
||||
|
|
@ -2520,6 +2586,38 @@
|
|||
|
||||
"hast-util-raw/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"nitro/rolldown/@oxc-project/types": ["@oxc-project/types@0.134.0", "", {}, "sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gCYzGOSkYY6Z034suzd20euvds7lPzMEEla62DJGE/ZAlR4OMBnNbvnBSsIGUCAr52gaWMsloGxP4tVGtN5aCA=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JQBD77MNgu+4Z6RAyg69acugdrhhVoWesr3l47zohYZ2YV2fwkWMArkN/2p4l6Ei+Sno7W5q+UsKdVWq5Ens0w=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-p/8cXUTK4Sob604e+xxPhVSbDFf29E6J0l/xESM9rdCfn3aDai3nEs6TnMHUsdD5aNlFz0+gDbiGlozLKGa2YA=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KbtOSlVv6fElujiZWMcC3aQYhEwLVVf073RcwlSmpGQvIsKZFUqc0ef4sjUuurRwfbiI6JJXji9DQn+86hawmQ=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9fZ9i0o0/MQaw7om6Z6TsT7tfCk0jtbEFtC+aPqZL5RNsGWNcHvn6EHgL3dAprjq+AZzPTAQjg2JtpJaMt+6pg=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-+tog7T66i+yFyIuuAnjL6xmW182W/qTBOUt6BtQ6lBIM1Eikh/fSMz4HGgvuCp5uU0zuIVWng7kDYthjCMOHcg=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4b7yruLIIj/oZ3GpcLOvxcLCLDMraohn3IhQfN2hBP4w9UekG0DTIajWguJosRGfySf/+h/NwRUiMKoCpxCrqQ=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-QRDOVZd0bhQ5jLsUsCC3dUxDWdTSVY9WMznowZgCGOrZfLLgctWpelhUASEiBwsXfat/JwYnVd1EaxMhqyT+UQ=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-ypxT+Hq76NFG7woFbNbySnGEajFuYuIXeKz/jfCU+lXUoxfi3zLE6OG/ZQNeK3RpZSYJlAe2bokpsQ046CaieQ=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-IdovCmfROFmpTLahdecTDFL74aLERVYN68F/mLZjfVh6LfoplPfI6deyHNMTcVujbokDV5k05XrFO22zfv+qjg=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pcA8xlFp2tyk9T2R6Fi/rPe3bQ1MA+sSMDNUU5Ogu80GHOatkE4P8YCreGAvZErm5Ho2YRXnyvNrWiRncfVysQ=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.0", "", { "os": "none", "cpu": "arm64" }, "sha512-4+fexHayrLCWpriPh4c6dNvL4an34DEZCG7zOM/FD5QNF6h8DT+bDXzyB/kfC8lDJbaFb7jKShtnjDQFXVQEjg=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-SbL++MNmOw6QamrwIGDMSSfM4ceTzFr+RjbOExJSLLBinScU4WI5OdA413h1qwPw2yH7lVF1+H4svQ+6mSXKTQ=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-+xTE6XC7wBgk0VKRXGG+QAnyW5S9b8vfsFpiMjf0waQTmSQSU8onsH/beyZ8X4aXVveJnotiy7VDjLOaW8bTrg=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Ogji1TQNqH3ACLnYr+1Ns1nyrJ0CO2P585u9Hsh02pXvtFiFpgtgT2b3P4PnCOU86VVCvqtAeCN4OftMT8KU4w=="],
|
||||
|
||||
"shadcn/stringify-object/is-obj": ["is-obj@3.0.0", "", {}, "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ=="],
|
||||
|
||||
"shadcn/stringify-object/is-regexp": ["is-regexp@3.1.0", "", {}, "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="],
|
||||
|
|
@ -2528,6 +2626,14 @@
|
|||
|
||||
"source-map/whatwg-url/webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
|
||||
|
||||
"vitest/vite/postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
|
||||
|
||||
"filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"@monaco-editor/react": "^4.7.0",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@pierre/diffs": "^1.2.1",
|
||||
"@pierre/trees": "1.0.0-beta.4",
|
||||
"@pierre/trees": "1.0.0-beta.5",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tanstack/react-devtools": "^0.10.0",
|
||||
"@tanstack/react-query": "^5.100.10",
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.16.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"nitro": "latest",
|
||||
"nitro": "3.0.260603-beta",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-icons": "^5.6.0",
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
"@tanstack/eslint-config": "^0.4.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
|
|
|
|||
|
|
@ -1,18 +1,42 @@
|
|||
import { StartClient } from "@tanstack/react-start/client"
|
||||
import { StrictMode } from "react"
|
||||
import { StrictMode, useEffect, useState } 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.
|
||||
if (import.meta.env.PROD) {
|
||||
registerSW()
|
||||
import { PwaUpdateToast } from "@/components/PwaUpdateToast"
|
||||
|
||||
// Prompt-mode PWA: vite-plugin-pwa still generates a SW that precaches the
|
||||
// production bundle, but it does not force skipWaiting/clientsClaim. Instead,
|
||||
// the new SW waits until the user accepts the update, so open dashboard tabs
|
||||
// are not reloaded mid-session. This protects in-flight agent runs and any
|
||||
// unsaved text in the compose box. Skip in dev — the SW precaches production-only
|
||||
// build artifacts that 404 against the dev server.
|
||||
function PwaUpdateProvider() {
|
||||
const [needRefresh, setNeedRefresh] = useState(false)
|
||||
const [updateSW, setUpdateSW] = useState<(() => Promise<void>) | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (!import.meta.env.PROD) return
|
||||
const updateSWRef = registerSW({
|
||||
onNeedRefresh() {
|
||||
setNeedRefresh(true)
|
||||
},
|
||||
})
|
||||
setUpdateSW(() => updateSWRef)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<PwaUpdateToast
|
||||
needRefresh={needRefresh}
|
||||
updateServiceWorker={updateSW}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<StartClient />
|
||||
<PwaUpdateProvider />
|
||||
</StrictMode>
|
||||
)
|
||||
|
|
|
|||
47
ui/src/components/PwaUpdateToast.tsx
Normal file
47
ui/src/components/PwaUpdateToast.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { useEffect, useState } from "react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
interface PwaUpdateToastProps {
|
||||
needRefresh: boolean
|
||||
updateServiceWorker: (() => Promise<void>) | undefined
|
||||
}
|
||||
|
||||
export function PwaUpdateToast({
|
||||
needRefresh,
|
||||
updateServiceWorker,
|
||||
}: PwaUpdateToastProps) {
|
||||
const [dismissed, setDismissed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (needRefresh) {
|
||||
setDismissed(false)
|
||||
}
|
||||
}, [needRefresh])
|
||||
|
||||
if (!needRefresh || dismissed) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed right-4 bottom-4 z-50 flex max-w-sm items-center gap-3 rounded-lg border border-border bg-card p-3 shadow-lg">
|
||||
<div className="flex-1 text-xs">
|
||||
<p className="font-medium">A new version is available.</p>
|
||||
<p className="text-muted-foreground">Reload to update the dashboard.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => setDismissed(true)}
|
||||
aria-label="Dismiss update notice"
|
||||
>
|
||||
Later
|
||||
</Button>
|
||||
<Button size="xs" onClick={() => void updateServiceWorker?.()}>
|
||||
Reload
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useMemo, useState } from "react"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { Link } from "@tanstack/react-router"
|
||||
import { useStreamContext as useAgentThreadStream } from "@langchain/react"
|
||||
import { Map as MapIcon } from "lucide-react"
|
||||
|
|
@ -23,6 +23,12 @@ import { useSubmitAgentMessage } from "@/lib/agents/provider/useSubmitAgentMessa
|
|||
import { useModelOptions } from "@/lib/agents/provider/useModelOptions"
|
||||
import { useIsMobile } from "@/lib/useIsMobile"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
useApproveWorkflowPush,
|
||||
useRejectWorkflowPush,
|
||||
useWorkflowApprovals,
|
||||
} from "@/lib/agents/queries"
|
||||
import { WorkflowApprovalCard } from "@/components/agents/WorkflowApprovalCard"
|
||||
|
||||
interface AgentThreadViewProps {
|
||||
thread: AgentThread
|
||||
|
|
@ -126,6 +132,31 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
|
|||
// Show a loading state during that one-time fetch instead of the empty state.
|
||||
const isHydrating = stream.isThreadLoading && !hasMessages
|
||||
|
||||
const approvalsQuery = useWorkflowApprovals(thread.id)
|
||||
const approveMutation = useApproveWorkflowPush(thread.id)
|
||||
const rejectMutation = useRejectWorkflowPush(thread.id)
|
||||
|
||||
const approvals = approvalsQuery.data
|
||||
const isApprovalOwner = thread.isOwner ?? false
|
||||
|
||||
const hasWorkflowApprovalError = useMemo(
|
||||
() =>
|
||||
baseMessages.some((message) =>
|
||||
message.chunks.some(
|
||||
(chunk) =>
|
||||
chunk.kind === "tool-execution" &&
|
||||
chunk.output?.includes("WorkflowPushApprovalRequired")
|
||||
)
|
||||
),
|
||||
[baseMessages]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasWorkflowApprovalError) {
|
||||
void approvalsQuery.refetch()
|
||||
}
|
||||
}, [hasWorkflowApprovalError])
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-1">
|
||||
<div
|
||||
|
|
@ -167,6 +198,24 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
|
|||
)}
|
||||
{hasConversation ? (
|
||||
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
{approvals && approvals.length > 0 && (
|
||||
<div className="shrink-0 px-4 pt-4">
|
||||
{approvals.map((approval) => (
|
||||
<WorkflowApprovalCard
|
||||
key={approval.fingerprint}
|
||||
approval={approval}
|
||||
isOwner={isApprovalOwner}
|
||||
isPending={
|
||||
approveMutation.isPending || rejectMutation.isPending
|
||||
}
|
||||
onApprove={() =>
|
||||
approveMutation.mutate(approval.fingerprint)
|
||||
}
|
||||
onReject={() => rejectMutation.mutate(approval.fingerprint)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Messages
|
||||
messages={baseMessages}
|
||||
queuedMessages={queuedMessages}
|
||||
|
|
|
|||
138
ui/src/components/agents/WorkflowApprovalCard.tsx
Normal file
138
ui/src/components/agents/WorkflowApprovalCard.tsx
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { Check, FileCode2, ShieldAlert, X } from "lucide-react"
|
||||
|
||||
import type { WorkflowApproval } from "@/lib/agents/api"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
|
||||
type ApprovalStatus = "pending" | "approved" | "rejected" | string
|
||||
|
||||
interface WorkflowApprovalCardProps {
|
||||
approval: WorkflowApproval
|
||||
isOwner: boolean
|
||||
isPending: boolean
|
||||
onApprove: (fingerprint: string) => void
|
||||
onReject: (fingerprint: string) => void
|
||||
}
|
||||
|
||||
function statusBadgeClass(status: ApprovalStatus) {
|
||||
switch (status) {
|
||||
case "approved":
|
||||
return "bg-green-500/10 text-green-600 dark:text-green-400"
|
||||
case "rejected":
|
||||
return "bg-red-500/10 text-red-600 dark:text-red-400"
|
||||
default:
|
||||
return "bg-amber-500/10 text-amber-600 dark:text-amber-400"
|
||||
}
|
||||
}
|
||||
|
||||
function statusLabel(status: ApprovalStatus) {
|
||||
switch (status) {
|
||||
case "approved":
|
||||
return "Approved"
|
||||
case "rejected":
|
||||
return "Rejected"
|
||||
default:
|
||||
return "Pending approval"
|
||||
}
|
||||
}
|
||||
|
||||
export function WorkflowApprovalCard({
|
||||
approval,
|
||||
isOwner,
|
||||
isPending,
|
||||
onApprove,
|
||||
onReject,
|
||||
}: WorkflowApprovalCardProps) {
|
||||
const actionable = isOwner && approval.status === "pending" && !isPending
|
||||
const decided = approval.status !== "pending"
|
||||
|
||||
return (
|
||||
<Card className="mx-auto mb-4 w-full max-w-3xl border-amber-500/20 bg-amber-500/5 dark:bg-amber-500/10">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldAlert className="size-4 text-amber-600 dark:text-amber-400" />
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Workflow push approval required
|
||||
</CardTitle>
|
||||
</div>
|
||||
<span
|
||||
className={`rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wide uppercase ${statusBadgeClass(
|
||||
approval.status
|
||||
)}`}
|
||||
>
|
||||
{statusLabel(approval.status)}
|
||||
</span>
|
||||
</div>
|
||||
<CardDescription className="text-xs">
|
||||
Open SWE is trying to push GitHub workflow file changes in{" "}
|
||||
<strong>{approval.repo}</strong> on <strong>{approval.branch}</strong>
|
||||
. Approve only if this exact workflow diff is expected.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-[11px] font-medium text-[var(--ui-text-muted)]">
|
||||
<FileCode2 className="size-3.5" />
|
||||
Changed workflow files
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{approval.files.map((file) => (
|
||||
<li
|
||||
key={file}
|
||||
className="font-mono text-[11px] break-all text-[var(--ui-text)]"
|
||||
>
|
||||
{file}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="pt-1 text-[11px] text-[var(--ui-text-muted)]">
|
||||
Fingerprint:{" "}
|
||||
<code className="rounded bg-[var(--ui-panel)] px-1 py-0.5 text-[var(--ui-text)]">
|
||||
{approval.fingerprint}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
{actionable ? (
|
||||
<CardFooter className="gap-2 border-t border-[var(--ui-border)] pt-3">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onApprove(approval.fingerprint)}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Check className="mr-1 size-3.5" />
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => onReject(approval.fingerprint)}
|
||||
disabled={isPending}
|
||||
>
|
||||
<X className="mr-1 size-3.5" />
|
||||
Reject
|
||||
</Button>
|
||||
</CardFooter>
|
||||
) : decided ? (
|
||||
<CardFooter className="border-t border-[var(--ui-border)] pt-3 text-[11px] text-[var(--ui-text-muted)]">
|
||||
This workflow push has been {approval.status}. If the workflow files
|
||||
change, a new fingerprint will be required.
|
||||
</CardFooter>
|
||||
) : null}
|
||||
{!isOwner && approval.status === "pending" && (
|
||||
<CardFooter className="border-t border-[var(--ui-border)] pt-3 text-[11px] text-[var(--ui-text-muted)]">
|
||||
Only the thread owner can approve or reject this workflow push.
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
|
@ -65,6 +65,24 @@ export interface ThreadRecoveryPatch {
|
|||
filename: string
|
||||
}
|
||||
|
||||
export interface WorkflowApproval {
|
||||
fingerprint: string
|
||||
status: string
|
||||
repo: string
|
||||
branch: string
|
||||
files: Array<string>
|
||||
requested_at: string
|
||||
}
|
||||
|
||||
export interface WorkflowApprovalsPayload {
|
||||
approvals: Array<WorkflowApproval>
|
||||
}
|
||||
|
||||
export interface WorkflowApprovalDecision {
|
||||
status: string
|
||||
fingerprint: string
|
||||
}
|
||||
|
||||
export interface ThreadsPageParams {
|
||||
limit?: number
|
||||
offset?: number
|
||||
|
|
@ -265,6 +283,24 @@ export const agentsApi = {
|
|||
),
|
||||
streamUrl: (threadId: string) =>
|
||||
`${API_BASE}/dashboard/api/threads/${encodeURIComponent(threadId)}/stream`,
|
||||
listWorkflowApprovals: (threadId: string) =>
|
||||
agentsRequest<WorkflowApprovalsPayload>(
|
||||
`/workflow-approval/${encodeURIComponent(threadId)}`
|
||||
),
|
||||
approveWorkflowPush: (threadId: string, fingerprint: string) =>
|
||||
agentsRequest<WorkflowApprovalDecision>(
|
||||
`/workflow-approval/${encodeURIComponent(threadId)}/${encodeURIComponent(
|
||||
fingerprint
|
||||
)}/approve`,
|
||||
{ method: "POST" }
|
||||
),
|
||||
rejectWorkflowPush: (threadId: string, fingerprint: string) =>
|
||||
agentsRequest<WorkflowApprovalDecision>(
|
||||
`/workflow-approval/${encodeURIComponent(threadId)}/${encodeURIComponent(
|
||||
fingerprint
|
||||
)}/reject`,
|
||||
{ method: "POST" }
|
||||
),
|
||||
}
|
||||
|
||||
export type ThreadGroup = "today" | "last7" | "last30" | "older"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {
|
|||
ScheduleUpdateRequest,
|
||||
SidebarThreads,
|
||||
ThreadsPageParams,
|
||||
WorkflowApproval,
|
||||
} from "./api"
|
||||
import type { AgentThread, Chunk, ImageChunk, Message } from "./types"
|
||||
|
||||
|
|
@ -17,6 +18,8 @@ export const agentThreadKeys = {
|
|||
["agent-threads", "lists", "sidebar", params] as const,
|
||||
detail: (threadId: string) => ["agent-threads", threadId] as const,
|
||||
prDiff: (threadId: string) => ["agent-threads", threadId, "pr-diff"] as const,
|
||||
workflowApprovals: (threadId: string) =>
|
||||
["agent-threads", threadId, "workflow-approvals"] as const,
|
||||
page: (params: ThreadsPageParams) =>
|
||||
["agent-threads", "lists", "page", params] as const,
|
||||
}
|
||||
|
|
@ -125,6 +128,61 @@ export function useAgentThreadPrDiff(threadId: string, enabled: boolean) {
|
|||
})
|
||||
}
|
||||
|
||||
export function useWorkflowApprovals(threadId: string) {
|
||||
return useQuery({
|
||||
queryKey: agentThreadKeys.workflowApprovals(threadId),
|
||||
queryFn: async () => {
|
||||
const { approvals } = await agentsApi.listWorkflowApprovals(threadId)
|
||||
return approvals
|
||||
},
|
||||
refetchInterval: (query) => {
|
||||
const data = query.state.data
|
||||
return data?.some((a) => a.status === "pending") ? 3000 : false
|
||||
},
|
||||
retry: false,
|
||||
})
|
||||
}
|
||||
|
||||
export function useApproveWorkflowPush(threadId: string) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (fingerprint: string) =>
|
||||
agentsApi.approveWorkflowPush(threadId, fingerprint),
|
||||
onSuccess: (_, fingerprint) => {
|
||||
queryClient.setQueryData<Array<WorkflowApproval> | undefined>(
|
||||
agentThreadKeys.workflowApprovals(threadId),
|
||||
(prev) =>
|
||||
prev?.map((record) =>
|
||||
record.fingerprint === fingerprint
|
||||
? { ...record, status: "approved" }
|
||||
: record
|
||||
)
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useRejectWorkflowPush(threadId: string) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (fingerprint: string) =>
|
||||
agentsApi.rejectWorkflowPush(threadId, fingerprint),
|
||||
onSuccess: (_, fingerprint) => {
|
||||
queryClient.setQueryData<Array<WorkflowApproval> | undefined>(
|
||||
agentThreadKeys.workflowApprovals(threadId),
|
||||
(prev) =>
|
||||
prev?.map((record) =>
|
||||
record.fingerprint === fingerprint
|
||||
? { ...record, status: "rejected" }
|
||||
: record
|
||||
)
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useAgentSchedules() {
|
||||
return useQuery({
|
||||
queryKey: agentScheduleKeys.all,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useCallback, useEffect, useState } from "react"
|
||||
|
||||
import {
|
||||
DEFAULT_SIDEBAR_FILTERS,
|
||||
type SidebarFilters,
|
||||
type SidebarGroupMode,
|
||||
DEFAULT_SIDEBAR_FILTERS
|
||||
|
||||
|
||||
} from "./sidebarFilter"
|
||||
import type {SidebarFilters, SidebarGroupMode} from "./sidebarFilter";
|
||||
|
||||
const STORAGE_KEY = "open-swe.agents.sidebar-prefs"
|
||||
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@ function CloudAgentsPage() {
|
|||
</div>
|
||||
<CaretRightIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
</Link>
|
||||
{session.data?.is_admin && (
|
||||
{session.data.is_admin && (
|
||||
<Link
|
||||
to="/agents/snapshots"
|
||||
className="flex items-center justify-between gap-6 px-4 py-3 hover:bg-muted/40"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import http from "node:http"
|
||||
import { defineConfig, type Plugin } from "vite"
|
||||
import { defineConfig } from "vite"
|
||||
import { devtools } from "@tanstack/devtools-vite"
|
||||
import { tanstackStart } from "@tanstack/react-start/plugin/vite"
|
||||
import viteReact from "@vitejs/plugin-react"
|
||||
|
|
@ -7,6 +7,7 @@ import viteTsConfigPaths from "vite-tsconfig-paths"
|
|||
import tailwindcss from "@tailwindcss/vite"
|
||||
import { nitro } from "nitro/vite"
|
||||
import { VitePWA } from "vite-plugin-pwa"
|
||||
import type {Plugin} from "vite";
|
||||
|
||||
// Dev-only: when E2E_HARNESS is set (the `dev:mock` local harness) serve the app
|
||||
// and the harness from one origin by proxying the API routes + the Yjs collab
|
||||
|
|
@ -60,7 +61,7 @@ function mockHarnessProxy(): Plugin | null {
|
|||
socket.write(
|
||||
`HTTP/1.1 ${proxyRes.statusCode} ${proxyRes.statusMessage}\r\n${lines}\r\n\r\n`
|
||||
)
|
||||
if (proxyHead?.length) socket.write(proxyHead)
|
||||
if (proxyHead.length) socket.write(proxyHead)
|
||||
if (head?.length) proxySocket.write(head)
|
||||
proxySocket.on("error", () => socket.destroy())
|
||||
socket.on("error", () => proxySocket.destroy())
|
||||
|
|
|
|||
24
uv.lock
generated
24
uv.lock
generated
|
|
@ -741,7 +741,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "exa-py"
|
||||
version = "2.15.0"
|
||||
version = "2.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpcore" },
|
||||
|
|
@ -752,14 +752,14 @@ dependencies = [
|
|||
{ name = "requests" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/4e/baca6add49c57eb1edc62bd2b2a1574b66bdb559a51f6e8c295e7d419e35/exa_py-2.15.0.tar.gz", hash = "sha256:8b3c3dbfe78e57b116e1ef50dec35c921519fb6cee94ed6c986d707b89de44fb", size = 66274, upload-time = "2026-06-24T23:29:07.932Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/68/a3/dabad8d3dbdb50d2ad49f426cc3e1fb47a0d0fd6ba10fbb27219c9bef684/exa_py-2.16.0.tar.gz", hash = "sha256:dce0db698720b1b7b39b58a1a17ad65fcc5969ee47f810cff5bcc836aeb0f777", size = 63530, upload-time = "2026-07-02T01:04:38.797Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1c/335681b69de8f2659d7555892b35b64fd586739a95623e03e9e92d57f3be/exa_py-2.15.0-py3-none-any.whl", hash = "sha256:12b8f7df70253df4474c9587e8e518934f311199f0e56be267e07268a49a487e", size = 85628, upload-time = "2026-06-24T23:29:06.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/c8/69d100ed12f8493ab8b9d03bf20cd0080a3a60dec4abadb1782cc4f91969/exa_py-2.16.0-py3-none-any.whl", hash = "sha256:cf726f801d6e4be25f38275a6318b536445a92077450a11cb3d7e493ea77ecc2", size = 86866, upload-time = "2026-07-02T01:04:37.451Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.138.2"
|
||||
version = "0.139.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
|
|
@ -768,9 +768,9 @@ dependencies = [
|
|||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0c/a9/9f8f7e00195c29836e9bf58bbbaf579e29878b8a67851efff93d9b6d4eb7/fastapi-0.138.2.tar.gz", hash = "sha256:6432359d067a432134620e7c5e4c6e5063e7f37815bbbbf20acef14b0d2e3fc8", size = 420423, upload-time = "2026-06-29T12:44:12.556Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/af/a5f50ccfa659ec1802cb4ca842c23f06d906a8cc9aef6016a2caeea3d4ed/fastapi-0.139.0.tar.gz", hash = "sha256:99ab7b2d92223c76d6cf10757ab3f89d45b38267fc20b2a136cf02f6beac3145", size = 423016, upload-time = "2026-07-01T16:35:33.436Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/b3/38be2c074bdd0c986340db1d72d7b2321b805b1c5a68069aa00b5d31fd02/fastapi-0.138.2-py3-none-any.whl", hash = "sha256:db90c1ffb5517fba5d4a9f80e866daa008747e646310c9ce155c8c535f9d1615", size = 129271, upload-time = "2026-06-29T12:44:13.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/7c/8e3c6ad324ea5cb36604fc3f968554887891c316d9dfde57761611d907ad/fastapi-0.139.0-py3-none-any.whl", hash = "sha256:cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", size = 130339, upload-time = "2026-07-01T16:35:32.19Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1750,7 +1750,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "langsmith"
|
||||
version = "0.9.4"
|
||||
version = "0.9.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
|
@ -1768,9 +1768,9 @@ dependencies = [
|
|||
{ name = "xxhash" },
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/a7/a78b4105d1c01ee8d404d122d9acc047b8b9f628f3733cb837a321b2f6c6/langsmith-0.9.4.tar.gz", hash = "sha256:36c15e5a692e7812ee927e43d777c6166c839eb02b1d482ba9a805b438b1d91c", size = 4576594, upload-time = "2026-06-30T10:20:43.373Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/85/dc/ca2ab3b74f3a5a2089aee1483e86b6aa7ae15fba96f73866a16e31356319/langsmith-0.9.6.tar.gz", hash = "sha256:1df590a40352b2f40a36229d90e2ef6af276a0bc9f1e44a87b829f879eed2130", size = 4714803, upload-time = "2026-07-02T11:14:30.636Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/15/79367a3d159141fe4fc138f41db4ef21eb4759cd64cf8f635f41d55ded67/langsmith-0.9.4-py3-none-any.whl", hash = "sha256:f10f07438c7c6f9f907e8c810d8f338268fe863a74c37ded3d8a90830bb25451", size = 603804, upload-time = "2026-06-30T10:20:41.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/8f/6e8f1f3e12be9848993c297c879f63f225361eb594f8343ff42c2b26c6fd/langsmith-0.9.6-py3-none-any.whl", hash = "sha256:c5e5f2425dcb8fe363b9e1fa87a9cb9cf5631389bf7e168aa4f325f580ca284c", size = 660131, upload-time = "2026-07-02T11:14:28.552Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
|
@ -2222,8 +2222,8 @@ dev = [
|
|||
requires-dist = [
|
||||
{ name = "cryptography", specifier = ">=48.0.1" },
|
||||
{ name = "deepagents", specifier = "==0.6.12" },
|
||||
{ name = "exa-py", specifier = ">=2.15.0" },
|
||||
{ name = "fastapi", specifier = ">=0.138.2" },
|
||||
{ name = "exa-py", specifier = ">=2.16.0" },
|
||||
{ name = "fastapi", specifier = ">=0.139.0" },
|
||||
{ name = "fireworks-ai", specifier = ">=1.2.0a85" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "langchain", specifier = ">=1.3.9" },
|
||||
|
|
@ -2239,7 +2239,7 @@ requires-dist = [
|
|||
{ name = "langgraph", specifier = ">=1.1.10" },
|
||||
{ name = "langgraph-cli", extras = ["inmem"], specifier = ">=0.4.30" },
|
||||
{ name = "langgraph-sdk", specifier = ">=0.4.2" },
|
||||
{ name = "langsmith", specifier = "==0.9.4" },
|
||||
{ name = "langsmith", specifier = "==0.9.6" },
|
||||
{ name = "markdownify", specifier = ">=1.2.3" },
|
||||
{ name = "pygments", marker = "extra == 'dev'", specifier = ">=2.20.0" },
|
||||
{ name = "pyjwt", specifier = ">=2.13.0" },
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue