From 76385f2fc36fe802ad16d3e96a2fac25a062f39f Mon Sep 17 00:00:00 2001 From: Viet Tran Date: Mon, 6 Apr 2026 13:18:02 +0700 Subject: [PATCH 1/2] fix(security): harden exec path exemption matching (#721) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add absolute path exemption for dataDir/skills-store/ (fixes skill scripts using absolute paths like /app/data/skills-store/ being denied) - Strip surrounding quotes before prefix matching (LLMs often quote paths) - Reject path traversal ("..") in exempt fields to prevent escape - Switch from "any field exempt → skip" to per-field matching: only exempt if ALL fields that match the deny pattern are individually exempt - Closes pipe/comment bypass vectors where an exempt path in one argument would exempt the entire command including non-exempt paths Includes 27 test cases covering: legitimate access, quoted paths, path traversal, unicode bypass, pipe/comment bypass, mixed args. --- cmd/gateway_setup.go | 2 +- internal/tools/shell.go | 36 +++-- internal/tools/shell_deny_test.go | 252 ++++++++++++++++++++++++++++++ 3 files changed, 278 insertions(+), 12 deletions(-) diff --git a/cmd/gateway_setup.go b/cmd/gateway_setup.go index 49b3e446..dc05c3d7 100644 --- a/cmd/gateway_setup.go +++ b/cmd/gateway_setup.go @@ -214,7 +214,7 @@ func setupToolRegistry( if execTool, ok := toolsReg.Get("exec"); ok { if et, ok := execTool.(*tools.ExecTool); ok { et.DenyPaths(dataDir, ".goclaw/") - et.AllowPathExemptions(".goclaw/skills-store/") + et.AllowPathExemptions(".goclaw/skills-store/", filepath.Join(dataDir, "skills-store")+"/") // Harden: block access to internal workspace files via shell commands. // Prevents `cat ../config.json`, `cat memory.db` etc. from user workspaces. et.DenyPaths( diff --git a/internal/tools/shell.go b/internal/tools/shell.go index 5d7e1829..fd4bde60 100644 --- a/internal/tools/shell.go +++ b/internal/tools/shell.go @@ -159,22 +159,36 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *Result { // Check for dangerous commands (applies to both host and sandbox). for _, pattern := range allPatterns { if pattern.MatchString(normalizedCommand) { - // Check if any exemption applies (e.g. skills-store within .goclaw). - // Uses argument-level prefix matching to prevent bypass via comments - // (e.g. "echo pwned # .goclaw/skills-store/") while still allowing - // commands like "cat .goclaw/skills-store/tool.py". + // Check if exemption applies. Only exempt if EVERY field that + // individually matches the deny pattern is covered by an exemption. + // This prevents pipe/comment bypass: "cat /app/data/skills-store/x | cat /app/data/secret" + // — the second field matches deny but has no exemption → denied. + // Strips surrounding quotes (LLMs often quote paths) and rejects + // path traversal ("..") to prevent exemption escape. exempt := false trimmed := strings.TrimSpace(normalizedCommand) - for _, ex := range t.denyExemptions { - for _, field := range strings.Fields(trimmed) { - if strings.HasPrefix(field, ex) { - exempt = true + fields := strings.Fields(trimmed) + matchingFields := 0 + exemptFields := 0 + for _, field := range fields { + clean := strings.Trim(field, `"'`) + if !pattern.MatchString(clean) { + continue // field doesn't trigger this deny pattern + } + matchingFields++ + if strings.Contains(clean, "..") { + continue // path traversal — never exempt + } + for _, ex := range t.denyExemptions { + if strings.HasPrefix(clean, ex) { + exemptFields++ break } } - if exempt { - break - } + } + // Exempt only if at least one field matched AND all matched fields are exempt. + if matchingFields > 0 && exemptFields == matchingFields { + exempt = true } if exempt { continue diff --git a/internal/tools/shell_deny_test.go b/internal/tools/shell_deny_test.go index eb3e18a3..17b100cd 100644 --- a/internal/tools/shell_deny_test.go +++ b/internal/tools/shell_deny_test.go @@ -170,6 +170,258 @@ func TestExecute_RejectsNULByte(t *testing.T) { } } +func TestPathExemptions(t *testing.T) { + tool := &ExecTool{ + workspace: "/workspace", + restrict: false, + } + tool.DenyPaths("/app/data", ".goclaw/") + tool.AllowPathExemptions(".goclaw/skills-store/", "/app/data/skills-store/") + + cases := []struct { + name string + cmd string + allow bool // true = exempt (should pass deny check), false = denied + }{ + // --- Exempted commands --- + { + "relative_skills_store", + "python3 .goclaw/skills-store/ck-ui/scripts/search.py --query test", + true, + }, + { + "absolute_skills_store", + `python3 /app/data/skills-store/ck-ui-ux-pro-max/1/scripts/search.py "professional" --design-system`, + true, + }, + { + "quoted_double_absolute", + `cat "/app/data/skills-store/my-skill/README.md"`, + true, + }, + { + "quoted_single_absolute", + `cat '/app/data/skills-store/my-skill/README.md'`, + true, + }, + { + "quoted_double_relative", + `python3 ".goclaw/skills-store/tool.py"`, + true, + }, + + // --- Denied commands (not exempt) --- + { + "datadir_config", + "cat /app/data/config.json", + false, + }, + { + "datadir_db", + "cp /app/data/goclaw.db /tmp/", + false, + }, + { + "dotgoclaw_root", + "ls .goclaw/", + false, + }, + { + "dotgoclaw_secrets", + "cat .goclaw/secrets.json", + false, + }, + + // --- Path traversal attacks (must be denied) --- + { + "traversal_absolute", + "cat /app/data/skills-store/../../config.json", + false, + }, + { + "traversal_relative", + "cat .goclaw/skills-store/../secrets.json", + false, + }, + { + "traversal_double_quoted", + `cat "/app/data/skills-store/../config.json"`, + false, + }, + { + "traversal_deep", + "python3 /app/data/skills-store/skill/../../../etc/passwd", + false, + }, + + // --- Comment/pipe bypass attempts (denied by per-field matching) --- + { + "comment_with_exempt_path", + "cat /app/data/config.json # .goclaw/skills-store/legit", + false, // /app/data/config.json matches deny and is NOT exempt + }, + + // --- Unicode/encoding bypass attempts (must be denied) --- + { + "unicode_fullwidth_dots", + "cat /app/data/skills-store/\uff0e\uff0e/config.json", // fullwidth dots .. + false, // NFKC normalizes .→. so ".." check catches it + }, + { + "zero_width_in_traversal", + "cat /app/data/skills-store/..\u200b/config.json", // zero-width space in .. + false, // normalizeCommand strips zero-width chars, ".." check catches it + }, + + // --- Pipe/redirect attempts (must be denied) --- + { + "pipe_after_exempt_path", + "cat /app/data/skills-store/tool.py | grep password /app/data/config.json", + false, // /app/data/config.json matches deny, pipe doesn't exempt it + }, + + // --- Subshell/backtick in path (should be denied if contains datadir) --- + { + "subshell_in_command", + "$(cat /app/data/config.json)", + false, + }, + { + "backtick_in_command", + "`cat /app/data/config.json`", + false, + }, + + // --- Edge: exempt path as substring (should NOT exempt) --- + { + "exempt_prefix_not_in_path", + "cat /app/data/not-skills-store/secret.txt", + false, // /app/data/not-skills-store/ does NOT start with /app/data/skills-store/ + }, + { + "partial_exempt_match", + "cat /app/data/skills-storebad/evil.py", + false, // /app/data/skills-storebad/ does NOT start with /app/data/skills-store/ + }, + + // --- Symlink-named path (defense-in-depth; sandbox handles actual resolution) --- + { + "skills_store_valid_nested", + "python3 /app/data/skills-store/my-skill/v2/scripts/run.py --flag", + true, // legitimate nested skill path + }, + { + "skills_store_just_prefix", + "ls /app/data/skills-store/", + true, // listing skills-store itself is allowed + }, + + // --- Exact deny path (not a prefix of skills-store) --- + { + "exact_datadir", + "ls /app/data", + false, + }, + { + "datadir_trailing_slash", + "ls /app/data/", + false, + }, + } + + allPatterns := make([]*regexp.Regexp, 0) + allPatterns = append(allPatterns, tool.pathDenyPatterns...) + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + normalizedCmd := normalizeCommand(tc.cmd) + denied := false + for _, pattern := range allPatterns { + if !pattern.MatchString(normalizedCmd) { + continue + } + // Replicate per-field exemption logic from Execute() + fields := strings.Fields(strings.TrimSpace(normalizedCmd)) + matchingFields := 0 + exemptFields := 0 + for _, field := range fields { + clean := strings.Trim(field, `"'`) + if !pattern.MatchString(clean) { + continue + } + matchingFields++ + if strings.Contains(clean, "..") { + continue // traversal — never exempt + } + for _, ex := range tool.denyExemptions { + if strings.HasPrefix(clean, ex) { + exemptFields++ + break + } + } + } + exempt := matchingFields > 0 && exemptFields == matchingFields + if !exempt { + denied = true + break + } + } + + if tc.allow && denied { + t.Errorf("expected command to be exempt (allowed), but was denied: %s", tc.cmd) + } + if !tc.allow && !denied { + t.Errorf("expected command to be denied, but was allowed: %s", tc.cmd) + } + }) + } +} + +// TestPathExemptions_MixedArgs verifies that a command with both a denied +// path and an exempt path in different arguments is correctly denied. +// Per-field matching ensures the non-exempt field causes denial. +func TestPathExemptions_MixedArgs(t *testing.T) { + tool := &ExecTool{} + tool.DenyPaths("/app/data") + tool.AllowPathExemptions("/app/data/skills-store/") + + cmd := "cat /app/data/config.json /app/data/skills-store/tool.py" + normalizedCmd := normalizeCommand(cmd) + + denied := false + for _, pattern := range tool.pathDenyPatterns { + if !pattern.MatchString(normalizedCmd) { + continue + } + fields := strings.Fields(strings.TrimSpace(normalizedCmd)) + matchingFields := 0 + exemptFields := 0 + for _, field := range fields { + clean := strings.Trim(field, `"'`) + if !pattern.MatchString(clean) { + continue + } + matchingFields++ + if strings.Contains(clean, "..") { + continue + } + for _, ex := range tool.denyExemptions { + if strings.HasPrefix(clean, ex) { + exemptFields++ + break + } + } + } + if matchingFields == 0 || exemptFields != matchingFields { + denied = true + } + } + + if !denied { + t.Error("mixed-path command should be denied: /app/data/config.json is not exempt") + } +} + func TestLimitedBuffer(t *testing.T) { t.Run("under limit", func(t *testing.T) { lb := &limitedBuffer{max: 100} From 0e2282be8f5d1b3555ac57ebb372dde8f4515158 Mon Sep 17 00:00:00 2001 From: Luan Vu Date: Tue, 7 Apr 2026 12:16:45 +0700 Subject: [PATCH 2/2] fix: use errors.Is() for sentinel comparisons + remove unused @xyflow/react (#727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace direct sentinel error comparisons (==, !=) with errors.Is() across the codebase. Direct comparison breaks when errors are wrapped with fmt.Errorf("...: %w", err), which can cause missed matches for sql.ErrNoRows, io.EOF, context.DeadlineExceeded, and context.Canceled. Affected packages: - store/pg: sql.ErrNoRows in secure_cli, agents_export_team_*, mcp_export_queries - store/sqlitestore: sql.ErrNoRows in schema migration - tools: context.DeadlineExceeded in shell, credentialed_exec - mcp: context.DeadlineExceeded in bridge_tool - providers: context.Canceled in acp_provider, anthropic_stream_test - updater: io.EOF in tar extraction Also removes unused @xyflow/react dependency from web UI — it was replaced by react-force-graph-2d but never cleaned up from package.json. Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com> --- internal/mcp/bridge_tool.go | 3 +- internal/providers/acp_provider.go | 3 +- internal/providers/anthropic_stream_test.go | 3 +- .../store/pg/agents_export_team_queries.go | 5 +- .../store/pg/agents_export_team_standalone.go | 7 +- internal/store/pg/mcp_export_queries.go | 3 +- internal/store/pg/secure_cli.go | 3 +- internal/store/sqlitestore/schema.go | 3 +- internal/tools/credentialed_exec.go | 3 +- internal/tools/shell.go | 2 +- internal/updater/updater.go | 3 +- ui/web/package.json | 1 - ui/web/pnpm-lock.yaml | 90 ------------------- 13 files changed, 24 insertions(+), 105 deletions(-) diff --git a/internal/mcp/bridge_tool.go b/internal/mcp/bridge_tool.go index d1bba9d6..5d3215cc 100644 --- a/internal/mcp/bridge_tool.go +++ b/internal/mcp/bridge_tool.go @@ -2,6 +2,7 @@ package mcp import ( "context" + "errors" "fmt" "strings" "sync/atomic" @@ -112,7 +113,7 @@ func (t *BridgeTool) Execute(ctx context.Context, args map[string]any) *tools.Re result, err := t.client.CallTool(callCtx, req) if err != nil { - if callCtx.Err() == context.DeadlineExceeded { + if errors.Is(callCtx.Err(), context.DeadlineExceeded) { return tools.ErrorResult(fmt.Sprintf("MCP tool %q timeout after %ds", t.registeredName, t.timeoutSec)) } return tools.ErrorResult(fmt.Sprintf("MCP tool %q error: %v", t.registeredName, err)) diff --git a/internal/providers/acp_provider.go b/internal/providers/acp_provider.go index 03d9f0c4..55b10df8 100644 --- a/internal/providers/acp_provider.go +++ b/internal/providers/acp_provider.go @@ -2,6 +2,7 @@ package providers import ( "context" + "errors" "fmt" "log/slog" "regexp" @@ -139,7 +140,7 @@ func (p *ACPProvider) ChatStream(ctx context.Context, req ChatRequest, onChunk f defer cancelFn() go func() { <-cancelCtx.Done() - if ctx.Err() == context.Canceled { + if errors.Is(ctx.Err(), context.Canceled) { _ = proc.Cancel() } }() diff --git a/internal/providers/anthropic_stream_test.go b/internal/providers/anthropic_stream_test.go index 297fb20e..11521277 100644 --- a/internal/providers/anthropic_stream_test.go +++ b/internal/providers/anthropic_stream_test.go @@ -2,6 +2,7 @@ package providers import ( "context" + "errors" "fmt" "net/http" "net/http/httptest" @@ -71,7 +72,7 @@ func TestStreamChat_CancelledContext(t *testing.T) { if err == nil { t.Fatal("expected error from cancelled context, got nil") } - if err != context.Canceled { + if !errors.Is(err, context.Canceled) { // Accept either context.Canceled or context.DeadlineExceeded; the HTTP // layer may wrap the error, so we check ctx.Err() as fallback. if ctx.Err() == nil { diff --git a/internal/store/pg/agents_export_team_queries.go b/internal/store/pg/agents_export_team_queries.go index c7ce5107..99ee2fdc 100644 --- a/internal/store/pg/agents_export_team_queries.go +++ b/internal/store/pg/agents_export_team_queries.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/json" + "errors" "log/slog" "github.com/google/uuid" @@ -95,7 +96,7 @@ func ExportTeamByLead(ctx context.Context, db *sql.DB, agentID uuid.UUID) (*Team " FROM agent_teams WHERE lead_agent_id = $1"+tc, args..., ).Scan(&teamID, &t.Name, &t.Description, &t.Status, &t.Settings) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, uuid.Nil, nil, nil } if err != nil { @@ -421,7 +422,7 @@ func ExportTeamPreviewCounts(ctx context.Context, db *sql.DB, agentID uuid.UUID) err = db.QueryRowContext(ctx, "SELECT id FROM agent_teams WHERE lead_agent_id = $1"+tc, args..., ).Scan(&teamID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { err = nil // Still count agent links even if no team _ = db.QueryRowContext(ctx, diff --git a/internal/store/pg/agents_export_team_standalone.go b/internal/store/pg/agents_export_team_standalone.go index 90678280..6bacd95d 100644 --- a/internal/store/pg/agents_export_team_standalone.go +++ b/internal/store/pg/agents_export_team_standalone.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/json" + "errors" "log/slog" "github.com/google/uuid" @@ -21,7 +22,7 @@ func ExportTeamByID(ctx context.Context, db *sql.DB, teamID uuid.UUID) (*TeamExp " FROM agent_teams WHERE id = $1"+tc, append([]any{teamID}, tcArgs...)..., ).Scan(&t.Name, &t.Description, &t.Status, &t.Settings) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { @@ -162,7 +163,7 @@ func ExportTeamLeadAgentID(ctx context.Context, db *sql.DB, teamID uuid.UUID) (u "SELECT lead_agent_id FROM agent_teams WHERE id = $1"+tc, append([]any{teamID}, tcArgs...)..., ).Scan(&leadID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return uuid.Nil, nil } return leadID, err @@ -179,7 +180,7 @@ func ExportTeamAgentBasicInfo(ctx context.Context, db *sql.DB, agentID uuid.UUID "SELECT agent_key FROM agents WHERE id = $1"+tc, append([]any{agentID}, tcArgs...)..., ).Scan(&agentKey) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return "", nil } return agentKey, err diff --git a/internal/store/pg/mcp_export_queries.go b/internal/store/pg/mcp_export_queries.go index 43f4e147..730c0376 100644 --- a/internal/store/pg/mcp_export_queries.go +++ b/internal/store/pg/mcp_export_queries.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/json" + "errors" "log/slog" "github.com/google/uuid" @@ -176,7 +177,7 @@ func ImportMCPServer(ctx context.Context, db *sql.DB, srv MCPServerExport, creat if err == nil { return existing, false, nil // already exists — return existing ID } - if err != sql.ErrNoRows { + if !errors.Is(err, sql.ErrNoRows) { return uuid.Nil, false, err } diff --git a/internal/store/pg/secure_cli.go b/internal/store/pg/secure_cli.go index 73642c9e..86fd1572 100644 --- a/internal/store/pg/secure_cli.go +++ b/internal/store/pg/secure_cli.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/json" + "errors" "fmt" "log/slog" "time" @@ -339,7 +340,7 @@ func (s *PGSecureCLIStore) scanRowWithGrantAndUserEnv(row *sql.Row) (*store.Secu &userEnv, ) if err != nil { - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, err diff --git a/internal/store/sqlitestore/schema.go b/internal/store/sqlitestore/schema.go index 82252fe6..1caeef28 100644 --- a/internal/store/sqlitestore/schema.go +++ b/internal/store/sqlitestore/schema.go @@ -5,6 +5,7 @@ package sqlitestore import ( "database/sql" _ "embed" + "errors" "fmt" "log/slog" ) @@ -118,7 +119,7 @@ func EnsureSchema(db *sql.DB) error { var current int err := db.QueryRow("SELECT version FROM schema_version LIMIT 1").Scan(¤t) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { // Fresh database — apply full schema. slog.Info("sqlite: applying initial schema", "version", SchemaVersion) tx, txErr := db.Begin() diff --git a/internal/tools/credentialed_exec.go b/internal/tools/credentialed_exec.go index 7d818d48..a38d3833 100644 --- a/internal/tools/credentialed_exec.go +++ b/internal/tools/credentialed_exec.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "log/slog" "maps" @@ -259,7 +260,7 @@ func formatCredentialedResult(binary string, args []string, } if err != nil { - if ctx.Err() == context.DeadlineExceeded { + if errors.Is(ctx.Err(), context.DeadlineExceeded) { return ErrorResult(fmt.Sprintf("[CREDENTIALED EXEC] Command timed out after %s.\nBinary: %s", timeout, binary)) } exitCode := -1 diff --git a/internal/tools/shell.go b/internal/tools/shell.go index fd4bde60..33b2b9db 100644 --- a/internal/tools/shell.go +++ b/internal/tools/shell.go @@ -328,7 +328,7 @@ func (t *ExecTool) executeOnHost(ctx context.Context, command, cwd string) *Resu } if err != nil { - if ctx.Err() == context.DeadlineExceeded { + if errors.Is(ctx.Err(), context.DeadlineExceeded) { return ErrorResult(fmt.Sprintf("command timed out after %s", t.timeout)) } if result == "" { diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 22a668d7..c86f8ca6 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -7,6 +7,7 @@ import ( "archive/zip" "compress/gzip" "encoding/json" + "errors" "fmt" "io" "log/slog" @@ -182,7 +183,7 @@ func applyMacOS(r io.Reader, tmpDir, appPath string) error { tr := tar.NewReader(gz) for { hdr, err := tr.Next() - if err == io.EOF { + if errors.Is(err, io.EOF) { break } if err != nil { diff --git a/ui/web/package.json b/ui/web/package.json index 0a652842..c141983a 100644 --- a/ui/web/package.json +++ b/ui/web/package.json @@ -17,7 +17,6 @@ "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.95.2", "@tanstack/react-table": "^8.21.3", - "@xyflow/react": "^12.10.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "d3-force": "^3.0.0", diff --git a/ui/web/pnpm-lock.yaml b/ui/web/pnpm-lock.yaml index 47753e51..d9625736 100644 --- a/ui/web/pnpm-lock.yaml +++ b/ui/web/pnpm-lock.yaml @@ -29,9 +29,6 @@ importers: '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@xyflow/react': - specifier: ^12.10.2 - version: 12.10.2(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -1153,9 +1150,6 @@ packages: '@types/d3-color@3.1.3': resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - '@types/d3-drag@3.0.7': - resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} - '@types/d3-ease@3.0.2': resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} @@ -1171,9 +1165,6 @@ packages: '@types/d3-scale@4.0.9': resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} - '@types/d3-selection@3.0.11': - resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} - '@types/d3-shape@3.1.8': resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} @@ -1183,12 +1174,6 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - '@types/d3-transition@3.0.9': - resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} - - '@types/d3-zoom@3.0.8': - resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} - '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -1246,15 +1231,6 @@ packages: babel-plugin-react-compiler: optional: true - '@xyflow/react@12.10.2': - resolution: {integrity: sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - - '@xyflow/system@0.0.76': - resolution: {integrity: sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==} - accessor-fn@1.5.3: resolution: {integrity: sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==} engines: {node: '>=12'} @@ -1291,9 +1267,6 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - classcat@5.0.5: - resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -2220,21 +2193,6 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} - zustand@4.5.7: - resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} - engines: {node: '>=12.7.0'} - peerDependencies: - '@types/react': '>=16.8' - immer: '>=9.0.6' - react: '>=16.8' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - zustand@5.0.12: resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==} engines: {node: '>=12.20.0'} @@ -3267,10 +3225,6 @@ snapshots: '@types/d3-color@3.1.3': {} - '@types/d3-drag@3.0.7': - dependencies: - '@types/d3-selection': 3.0.11 - '@types/d3-ease@3.0.2': {} '@types/d3-force@3.0.10': {} @@ -3285,8 +3239,6 @@ snapshots: dependencies: '@types/d3-time': 3.0.4 - '@types/d3-selection@3.0.11': {} - '@types/d3-shape@3.1.8': dependencies: '@types/d3-path': 3.1.1 @@ -3295,15 +3247,6 @@ snapshots: '@types/d3-timer@3.0.2': {} - '@types/d3-transition@3.0.9': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-zoom@3.0.8': - dependencies: - '@types/d3-interpolate': 3.0.4 - '@types/d3-selection': 3.0.11 - '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -3353,29 +3296,6 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.7 vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(jiti@2.6.1) - '@xyflow/react@12.10.2(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@xyflow/system': 0.0.76 - classcat: 5.0.5 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4) - transitivePeerDependencies: - - '@types/react' - - immer - - '@xyflow/system@0.0.76': - dependencies: - '@types/d3-drag': 3.0.7 - '@types/d3-interpolate': 3.0.4 - '@types/d3-selection': 3.0.11 - '@types/d3-transition': 3.0.9 - '@types/d3-zoom': 3.0.8 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-zoom: 3.0.0 - accessor-fn@1.5.3: {} aria-hidden@1.2.6: @@ -3404,8 +3324,6 @@ snapshots: dependencies: clsx: 2.1.1 - classcat@5.0.5: {} - clsx@2.1.1: {} comma-separated-tokens@2.0.3: {} @@ -4583,14 +4501,6 @@ snapshots: zod@4.3.6: {} - zustand@4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4): - dependencies: - use-sync-external-store: 1.6.0(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - immer: 11.1.4 - react: 19.2.4 - zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): optionalDependencies: '@types/react': 19.2.14