From 351ccc5a3c4f5c22e00aa0973156eccf7cc2517d Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 16 Jun 2026 02:08:14 -0400 Subject: [PATCH] docs+chore: add README Security section; fix lint regressions on main - README: add a visible ## Security section (official sources, vuln reporting via SECURITY.md, GateGuard/IOC/AgentShield guardrails, security guide); make stats line a plain paragraph to clear MD028 - eslint: empty catch comment in run-with-flags.js; drop unneeded escape in github-coordination/parsing.js; remove unused execFileSync import in its test (#2236 follow-ups) - markdownlint: wrap bare URLs in rules/vue/*.md (#2250 follow-up) npm run lint green; full suite 2836/2836. --- README.md | 13 +- rules/vue/coding-style.md | 2 +- rules/vue/hooks.md | 2 +- rules/vue/patterns.md | 2 +- rules/vue/security.md | 2 +- rules/vue/testing.md | 2 +- scripts/hooks/run-with-flags.js | 11 +- scripts/lib/github-coordination/parsing.js | 36 +-- tests/scripts/github-coordination.test.js | 281 +++++++++++---------- 9 files changed, 180 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index 5f6692dd..723f360b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ > [!WARNING] > **Official sources only.** Install ECC only from verified channels: the GitHub repository [github.com/affaan-m/ECC](https://github.com/affaan-m/ECC), the npm packages [`ecc-universal`](https://www.npmjs.com/package/ecc-universal) and [`ecc-agentshield`](https://www.npmjs.com/package/ecc-agentshield), the [GitHub App](https://github.com/apps/ecc-tools), the plugin slug `ecc@ecc`, and the project website [ecc.tools](https://ecc.tools). Third-party re-uploads and unofficial mirrors are not maintained or reviewed by the project and may contain malware. -> **211.9K+ stars** | **32.5K+ forks** | **230+ contributors** | **12+ language ecosystems** | **Cross-harness agent workflows** +**211.9K+ stars** | **32.5K+ forks** | **230+ contributors** | **12+ language ecosystems** | **Cross-harness agent workflows** --- @@ -1808,6 +1808,17 @@ These configs work for my workflow. You should: --- +## Security + +ECC takes supply-chain and agent safety seriously. + +- **Official sources only.** Install ECC only from the verified channels listed in the banner at the top of this README — the [GitHub repo](https://github.com/affaan-m/ECC), the `ecc-universal` / `ecc-agentshield` npm packages, the [GitHub App](https://github.com/apps/ecc-tools), the plugin slug `ecc@ecc`, and [ecc.tools](https://ecc.tools). Third-party re-uploads and mirrors are unreviewed and may ship malware. +- **Report a vulnerability.** Use the private process in [SECURITY.md](SECURITY.md) (GitHub private vulnerability reporting). Please do not open public issues for security reports. +- **Built-in guardrails.** GateGuard gates destructive shell commands (including `rm`, force/path `git checkout`, and destructive `find -exec`) before they run; the supply-chain IOC scanner runs in CI; and [AgentShield](#agentshield--security-auditor) audits your own agent, hook, MCP, permission, and secret surfaces (`/security-scan`). +- **Deep dive.** See the [Security Guide](./the-security-guide.md). + +--- + ## Sponsors Featured sponsors are at the top of this README — full list and tiers in [SPONSORS.md](SPONSORS.md). [Become a sponsor](https://github.com/sponsors/affaan-m). diff --git a/rules/vue/coding-style.md b/rules/vue/coding-style.md index f6c0e31b..fbef678a 100644 --- a/rules/vue/coding-style.md +++ b/rules/vue/coding-style.md @@ -51,4 +51,4 @@ const open = defineModel('open', { default: false }) ## Reference - ECC skills: `frontend-patterns`, `vite-patterns`. -- Docs: https://vuejs.org/api/sfc-script-setup.html · https://vuejs.org/guide/essentials/reactivity-fundamentals.html · https://eslint.vuejs.org/ +- Docs: · · diff --git a/rules/vue/hooks.md b/rules/vue/hooks.md index b0c8d916..16c92ca4 100644 --- a/rules/vue/hooks.md +++ b/rules/vue/hooks.md @@ -42,4 +42,4 @@ vue-tsc --noEmit ## Reference - ECC skills: `frontend-patterns`, `vite-patterns`. -- Docs: https://github.com/vuejs/language-tools (vue-tsc) · https://eslint.vuejs.org/ · https://github.com/feature-sliced/steiger +- Docs: (vue-tsc) · · diff --git a/rules/vue/patterns.md b/rules/vue/patterns.md index 35059f0f..e5eb354d 100644 --- a/rules/vue/patterns.md +++ b/rules/vue/patterns.md @@ -53,4 +53,4 @@ queryClient.invalidateQueries({ queryKey: ['auction', id] }) ## Reference - ECC skills: `frontend-patterns`, `vite-patterns`. -- Docs: https://pinia.vuejs.org/ · https://router.vuejs.org/ · https://tanstack.com/query/latest/docs/framework/vue/overview · https://vuejs.org/guide/reusability/composables.html +- Docs: · · · diff --git a/rules/vue/security.md b/rules/vue/security.md index 6a04d333..8f21953f 100644 --- a/rules/vue/security.md +++ b/rules/vue/security.md @@ -43,4 +43,4 @@ paths: ## Reference - ECC skills: `frontend-patterns`, `vite-patterns`. -- Docs: https://vuejs.org/guide/best-practices/security.html · https://github.com/cure53/DOMPurify · https://github.com/braintree/sanitize-url +- Docs: · · diff --git a/rules/vue/testing.md b/rules/vue/testing.md index 7dbf9f07..951712f8 100644 --- a/rules/vue/testing.md +++ b/rules/vue/testing.md @@ -50,4 +50,4 @@ expect(wrapper.emitted('bid')).toBeTruthy() ## Reference - ECC skills: `frontend-patterns`, `vite-patterns`. -- Docs: https://test-utils.vuejs.org/api/ · https://pinia.vuejs.org/cookbook/testing.html · https://vitest.dev/ +- Docs: · · diff --git a/scripts/hooks/run-with-flags.js b/scripts/hooks/run-with-flags.js index 41ff1462..37a10f38 100755 --- a/scripts/hooks/run-with-flags.js +++ b/scripts/hooks/run-with-flags.js @@ -100,9 +100,8 @@ function getPluginRoot() { return path.resolve(__dirname, '..', '..'); } - //Safely extract target context from hook stdin JSON for dry-run preview. - + function extractTargetContext(raw) { const result = { tool: '', filePath: '', command: '' }; if (!raw || typeof raw !== 'string') return result; @@ -118,18 +117,16 @@ function extractTargetContext(raw) { } } } catch { + // best-effort field extraction; ignore malformed input } return result; } // Build the [DryRun] preview line for stderr. - + function buildDryRunPreview(hookId, relScriptPath, profilesCsv, raw) { const ctx = extractTargetContext(raw); - const parts = [ - `[DryRun] Hook "${hookId}" would execute: ${relScriptPath}`, - `(enabled=true, profiles=${profilesCsv || 'default'})`, - ]; + const parts = [`[DryRun] Hook "${hookId}" would execute: ${relScriptPath}`, `(enabled=true, profiles=${profilesCsv || 'default'})`]; if (ctx.tool) { parts.push(`tool=${ctx.tool}`); diff --git a/scripts/lib/github-coordination/parsing.js b/scripts/lib/github-coordination/parsing.js index 08bc3b54..987067c1 100644 --- a/scripts/lib/github-coordination/parsing.js +++ b/scripts/lib/github-coordination/parsing.js @@ -7,17 +7,12 @@ function escapeRegExp(str) { } function normalizeBodyForComparison(body) { - return (body || '').replace(/"lastSyncAt"\s*:\s*[^,\}\n]+/g, '"lastSyncAt": NORMALIZED'); + return (body || '').replace(/"lastSyncAt"\s*:\s*[^,}\n]+/g, '"lastSyncAt": NORMALIZED'); } function extractCoordinationState(body, policy = DEFAULT_POLICY) { const marker = escapeRegExp(policy.sectionMarker || DEFAULT_SECTION_MARKER); - const regex = new RegExp( - `\\s*` + - '```json\\s*([\\s\\S]*?)\\s*```' + - `\\s*`, - 'm' - ); + const regex = new RegExp(`\\s*` + '```json\\s*([\\s\\S]*?)\\s*```' + `\\s*`, 'm'); const match = String(body || '').match(regex); if (!match) { @@ -28,9 +23,7 @@ function extractCoordinationState(body, policy = DEFAULT_POLICY) { const parsed = JSON.parse(match[1]); return parsed && typeof parsed === 'object' ? parsed : null; } catch (error) { - throw new SyntaxError( - `Malformed coordination JSON in body: ${error.message} — raw: ${match[1].slice(0, 120)}` - ); + throw new SyntaxError(`Malformed coordination JSON in body: ${error.message} — raw: ${match[1].slice(0, 120)}`); } } @@ -40,7 +33,9 @@ function extractIssueReferences(text) { for (const match of source.matchAll(/(?:^|[^\d])#(\d+)\b/g)) { refs.add(Number.parseInt(match[1], 10)); } - return Array.from(refs).filter(Number.isFinite).sort((a, b) => a - b); + return Array.from(refs) + .filter(Number.isFinite) + .sort((a, b) => a - b); } function extractTasks(body) { @@ -62,7 +57,7 @@ function extractTasks(body) { if (taskMatch) { tasks.push({ title: taskMatch[2].trim(), - done: taskMatch[1].toLowerCase() === 'x', + done: taskMatch[1].toLowerCase() === 'x' }); } } @@ -98,26 +93,17 @@ function renderCoordinationState(state, policy = DEFAULT_POLICY) { lastAction: state.lastAction || 'sync', lastActionAt: state.lastActionAt || new Date().toISOString(), lastSyncAt: state.lastSyncAt || new Date().toISOString(), - notes: state.notes || null, + notes: state.notes || null }; - return [ - ``, - '```json', - JSON.stringify(payload, null, 2), - '```', - ``, - ].join('\n'); + return [``, '```json', JSON.stringify(payload, null, 2), '```', ``].join('\n'); } function mergeIssueBody(issue, nextState, policy = DEFAULT_POLICY) { const body = String(issue.body || ''); const markerEscaped = escapeRegExp(policy.sectionMarker || DEFAULT_SECTION_MARKER); const rendered = renderCoordinationState(nextState, policy); - const regex = new RegExp( - `\\n?[\\s\\S]*?\\n?`, - 'm' - ); + const regex = new RegExp(`\\n?[\\s\\S]*?\\n?`, 'm'); if (regex.test(body)) { return body.replace(regex, `\n${rendered}\n`).trim() + '\n'; @@ -139,5 +125,5 @@ module.exports = { mergeIssueBody, normalizeBodyForComparison, parseStringList, - renderCoordinationState, + renderCoordinationState }; diff --git a/tests/scripts/github-coordination.test.js b/tests/scripts/github-coordination.test.js index aca4edbf..158fc7c9 100644 --- a/tests/scripts/github-coordination.test.js +++ b/tests/scripts/github-coordination.test.js @@ -6,7 +6,7 @@ const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); -const { execFileSync, spawnSync } = require('child_process'); +const { spawnSync } = require('child_process'); const { createStateStore } = require('../../scripts/lib/state-store'); @@ -23,7 +23,9 @@ function cleanup(dirPath) { function writeGhShim(rootDir, responses) { const shimPath = path.join(rootDir, 'gh-shim.js'); const logPath = path.join(rootDir, 'gh-calls.jsonl'); - fs.writeFileSync(shimPath, ` + fs.writeFileSync( + shimPath, + ` const fs = require('fs'); const responses = ${JSON.stringify(responses)}; const args = process.argv.slice(2); @@ -42,7 +44,8 @@ if (args[0] === 'issue' && (args[1] === 'edit' || args[1] === 'comment')) { } console.error('Unexpected gh args: ' + key); process.exit(3); -`); +` + ); return { shimPath, logPath }; } @@ -51,11 +54,11 @@ function run(args = [], options = {}) { cwd: options.cwd || path.join(__dirname, '..', '..'), env: { ...process.env, - ...(options.env || {}), + ...(options.env || {}) }, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], - timeout: 10000, + timeout: 10000 }); } @@ -90,143 +93,155 @@ async function runTests() { let passed = 0; let failed = 0; - if (await test('claims an epic issue, updates GitHub state, and caches a work item', async () => { - const rootDir = createTempDir('github-coordination-claim-'); - const dbPath = path.join(rootDir, 'state.db'); + if ( + await test('claims an epic issue, updates GitHub state, and caches a work item', async () => { + const rootDir = createTempDir('github-coordination-claim-'); + const dbPath = path.join(rootDir, 'state.db'); - try { - const epicBody = [ - '# Ship GitHub-native coordination', - '', - 'We want deterministic epic state.', - '', - '## Tasks', - '- [ ] Claim the epic', - '- [ ] Validate the epic', - ].join('\n'); - const issueView = { - number: 12, - title: 'Ship GitHub-native coordination', - body: epicBody, - url: 'https://github.com/affaan-m/ECC/issues/12', - state: 'OPEN', - labels: [{ name: 'epic' }], - author: { login: 'maintainer' }, - updatedAt: '2026-06-01T12:00:00Z', - }; - const shim = writeGhShim(rootDir, { - 'issue view 12 --repo affaan-m/ECC --json number,title,body,url,state,labels,author,updatedAt,assignees': issueView, - }); + try { + const epicBody = ['# Ship GitHub-native coordination', '', 'We want deterministic epic state.', '', '## Tasks', '- [ ] Claim the epic', '- [ ] Validate the epic'].join('\n'); + const issueView = { + number: 12, + title: 'Ship GitHub-native coordination', + body: epicBody, + url: 'https://github.com/affaan-m/ECC/issues/12', + state: 'OPEN', + labels: [{ name: 'epic' }], + author: { login: 'maintainer' }, + updatedAt: '2026-06-01T12:00:00Z' + }; + const shim = writeGhShim(rootDir, { + 'issue view 12 --repo affaan-m/ECC --json number,title,body,url,state,labels,author,updatedAt,assignees': issueView + }); - const result = run(['claim', '12', '--repo', 'affaan-m/ECC', '--actor', 'codex', '--db', dbPath, '--json'], { - cwd: rootDir, - env: { - ECC_GH_SHIM: shim.shimPath, - ECC_GH_SHIM_LOG: shim.logPath, - }, - }); - assert.strictEqual(result.status, 0, result.stderr); - const payload = parseJson(result.stdout); - assert.strictEqual(payload.status, 'claimed'); - assert.strictEqual(payload.owner, 'codex'); - assert.strictEqual(payload.project.state, 'in-progress'); + const result = run(['claim', '12', '--repo', 'affaan-m/ECC', '--actor', 'codex', '--db', dbPath, '--json'], { + cwd: rootDir, + env: { + ECC_GH_SHIM: shim.shimPath, + ECC_GH_SHIM_LOG: shim.logPath + } + }); + assert.strictEqual(result.status, 0, result.stderr); + const payload = parseJson(result.stdout); + assert.strictEqual(payload.status, 'claimed'); + assert.strictEqual(payload.owner, 'codex'); + assert.strictEqual(payload.project.state, 'in-progress'); - const logEntries = fs.readFileSync(shim.logPath, 'utf8').trim().split(/\r?\n/).map(line => JSON.parse(line)); - assert.ok(logEntries.some(entry => entry.args[0] === 'issue' && entry.args[1] === 'edit')); - assert.ok(logEntries.some(entry => entry.args[0] === 'issue' && entry.args[1] === 'comment')); + const logEntries = fs + .readFileSync(shim.logPath, 'utf8') + .trim() + .split(/\r?\n/) + .map(line => JSON.parse(line)); + assert.ok(logEntries.some(entry => entry.args[0] === 'issue' && entry.args[1] === 'edit')); + assert.ok(logEntries.some(entry => entry.args[0] === 'issue' && entry.args[1] === 'comment')); - const stored = await readStore(dbPath); - const epicItem = stored.items.find(item => item.source === 'github-epic'); - assert.ok(epicItem, 'expected github epic work item'); - assert.strictEqual(epicItem.status, 'in-progress'); - assert.strictEqual(epicItem.metadata.coordination.status, 'claimed'); - assert.strictEqual(epicItem.metadata.coordination.owner, 'codex'); - } finally { - cleanup(rootDir); - } - })) passed++; else failed++; + const stored = await readStore(dbPath); + const epicItem = stored.items.find(item => item.source === 'github-epic'); + assert.ok(epicItem, 'expected github epic work item'); + assert.strictEqual(epicItem.status, 'in-progress'); + assert.strictEqual(epicItem.metadata.coordination.status, 'claimed'); + assert.strictEqual(epicItem.metadata.coordination.owner, 'codex'); + } finally { + cleanup(rootDir); + } + }) + ) + passed++; + else failed++; - if (await test('unblocks an epic when dependencies are closed', async () => { - const rootDir = createTempDir('github-coordination-unblock-'); - const dbPath = path.join(rootDir, 'state.db'); + if ( + await test('unblocks an epic when dependencies are closed', async () => { + const rootDir = createTempDir('github-coordination-unblock-'); + const dbPath = path.join(rootDir, 'state.db'); - try { - const blockedBody = [ - '# Release readiness', - '', - 'Dependencies: #2', - '', - '', - '```json', - JSON.stringify({ - schemaVersion: 'ecc.github.coordination.v1', - kind: 'epic', - status: 'blocked', - owner: 'codex', - branch: 'feat/release-readiness', - validation: 'pending', - review: 'requested', - project: { state: 'blocked', fields: {} }, - dependencies: [2], - tasks: [{ title: 'Check release checklist', done: false }], - labels: ['epic', 'coordination:blocked'], - lastAction: 'claim', - lastActionAt: '2026-06-01T13:00:00Z', - lastSyncAt: '2026-06-01T13:00:00Z', - notes: null, - }, null, 2), - '```', - '', - ].join('\n'); - const openIssue = { - number: 1, - title: 'Release readiness', - body: blockedBody, - url: 'https://github.com/affaan-m/ECC/issues/1', - state: 'OPEN', - labels: [{ name: 'epic' }, { name: 'coordination:blocked' }], - author: { login: 'codex' }, - updatedAt: '2026-06-01T13:00:00Z', - }; - const closedDependency = { - number: 2, - title: 'Release prerequisite', - body: '# Release prerequisite', - url: 'https://github.com/affaan-m/ECC/issues/2', - state: 'CLOSED', - labels: [{ name: 'blocked-by-release' }], - author: { login: 'maintainer' }, - updatedAt: '2026-06-01T10:00:00Z', - }; - const shim = writeGhShim(rootDir, { - 'issue list --repo affaan-m/ECC --state all --limit 100 --json number,title,body,url,state,labels,author,updatedAt,assignees': [openIssue, closedDependency], - 'issue view 1 --repo affaan-m/ECC --json number,title,body,url,state,labels,author,updatedAt,assignees': openIssue, - }); + try { + const blockedBody = [ + '# Release readiness', + '', + 'Dependencies: #2', + '', + '', + '```json', + JSON.stringify( + { + schemaVersion: 'ecc.github.coordination.v1', + kind: 'epic', + status: 'blocked', + owner: 'codex', + branch: 'feat/release-readiness', + validation: 'pending', + review: 'requested', + project: { state: 'blocked', fields: {} }, + dependencies: [2], + tasks: [{ title: 'Check release checklist', done: false }], + labels: ['epic', 'coordination:blocked'], + lastAction: 'claim', + lastActionAt: '2026-06-01T13:00:00Z', + lastSyncAt: '2026-06-01T13:00:00Z', + notes: null + }, + null, + 2 + ), + '```', + '' + ].join('\n'); + const openIssue = { + number: 1, + title: 'Release readiness', + body: blockedBody, + url: 'https://github.com/affaan-m/ECC/issues/1', + state: 'OPEN', + labels: [{ name: 'epic' }, { name: 'coordination:blocked' }], + author: { login: 'codex' }, + updatedAt: '2026-06-01T13:00:00Z' + }; + const closedDependency = { + number: 2, + title: 'Release prerequisite', + body: '# Release prerequisite', + url: 'https://github.com/affaan-m/ECC/issues/2', + state: 'CLOSED', + labels: [{ name: 'blocked-by-release' }], + author: { login: 'maintainer' }, + updatedAt: '2026-06-01T10:00:00Z' + }; + const shim = writeGhShim(rootDir, { + 'issue list --repo affaan-m/ECC --state all --limit 100 --json number,title,body,url,state,labels,author,updatedAt,assignees': [openIssue, closedDependency], + 'issue view 1 --repo affaan-m/ECC --json number,title,body,url,state,labels,author,updatedAt,assignees': openIssue + }); - const result = run(['unblock', '--repo', 'affaan-m/ECC', '--db', dbPath, '--json'], { - cwd: rootDir, - env: { - ECC_GH_SHIM: shim.shimPath, - ECC_GH_SHIM_LOG: shim.logPath, - }, - }); - assert.strictEqual(result.status, 0, result.stderr); - const payload = parseJson(result.stdout); - assert.strictEqual(payload.count, 1); - assert.strictEqual(payload.items[0].status, 'ready'); + const result = run(['unblock', '--repo', 'affaan-m/ECC', '--db', dbPath, '--json'], { + cwd: rootDir, + env: { + ECC_GH_SHIM: shim.shimPath, + ECC_GH_SHIM_LOG: shim.logPath + } + }); + assert.strictEqual(result.status, 0, result.stderr); + const payload = parseJson(result.stdout); + assert.strictEqual(payload.count, 1); + assert.strictEqual(payload.items[0].status, 'ready'); - const logEntries = fs.readFileSync(shim.logPath, 'utf8').trim().split(/\r?\n/).map(line => JSON.parse(line)); - assert.ok(logEntries.some(entry => entry.args[0] === 'issue' && entry.args[1] === 'edit')); - assert.ok(logEntries.some(entry => entry.args[0] === 'issue' && entry.args[1] === 'comment')); + const logEntries = fs + .readFileSync(shim.logPath, 'utf8') + .trim() + .split(/\r?\n/) + .map(line => JSON.parse(line)); + assert.ok(logEntries.some(entry => entry.args[0] === 'issue' && entry.args[1] === 'edit')); + assert.ok(logEntries.some(entry => entry.args[0] === 'issue' && entry.args[1] === 'comment')); - const stored = await readStore(dbPath); - const epicItem = stored.items.find(item => item.source === 'github-epic'); - assert.ok(epicItem, 'expected github epic work item'); - assert.strictEqual(epicItem.metadata.coordination.status, 'ready'); - } finally { - cleanup(rootDir); - } - })) passed++; else failed++; + const stored = await readStore(dbPath); + const epicItem = stored.items.find(item => item.source === 'github-epic'); + assert.ok(epicItem, 'expected github epic work item'); + assert.strictEqual(epicItem.metadata.coordination.status, 'ready'); + } finally { + cleanup(rootDir); + } + }) + ) + passed++; + else failed++; process.stdout.write(`\nResults: Passed: ${passed}, Failed: ${failed}\n`); process.exit(failed > 0 ? 1 : 0);