feat(ms-ai-architect): add plugin to open marketplace (v1.5.0 baseline)
Initial addition of ms-ai-architect plugin to the open-source marketplace. Private content excluded: orchestrator/ (Linear tooling), docs/utredning/ (client investigation), generated test reports and PDF export script. skill-gen tooling moved from orchestrator/ to scripts/skill-gen/. Security scan: WARNING (risk 20/100) — no secrets, no injection found. False positive fixed: added gitleaks:allow to Python variable reference in output-validation-grounding-verification.md line 109. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a8d79e4484
commit
6a7632146e
490 changed files with 213249 additions and 2 deletions
93
plugins/ms-ai-architect/hooks/scripts/pre-edit-secrets.mjs
Normal file
93
plugins/ms-ai-architect/hooks/scripts/pre-edit-secrets.mjs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#!/usr/bin/env node
|
||||
// pre-edit-secrets.mjs
|
||||
// Scans file edits for potential secrets before allowing save (cross-platform, no jq dependency)
|
||||
//
|
||||
// Exit codes:
|
||||
// 0 = Allow edit
|
||||
// 2 = Block edit (secrets detected)
|
||||
|
||||
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const input = readFileSync(0, 'utf-8');
|
||||
|
||||
const { tool_input } = JSON.parse(input);
|
||||
const content = tool_input?.content ?? tool_input?.new_string ?? '';
|
||||
const filePath = tool_input?.file_path ?? tool_input?.path ?? '';
|
||||
|
||||
if (!content) process.exit(0);
|
||||
|
||||
// Skip test files, mocks, example/template files
|
||||
if (/\.(test|spec|mock)\.(ts|js|tsx|jsx)$|__tests__|__mocks__/.test(filePath)) {
|
||||
process.exit(0);
|
||||
}
|
||||
if (/\.(example|template|sample)(\..*)?$|\.env\.example|\.env\.template|\.env\.sample/.test(filePath)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// === SECRET PATTERNS ===
|
||||
const SECRETS = [
|
||||
// AWS Keys
|
||||
[/AKIA[0-9A-Z]{16}/, 'AWS Access Key detected in edit.', 'Use environment variables instead.'],
|
||||
|
||||
// Generic API Keys
|
||||
[/(api[_-]?key|apikey)\s*[:=]\s*["'][a-zA-Z0-9]{20,}["']/, 'Potential API key detected.', 'Use environment variables (process.env.API_KEY) instead.'],
|
||||
|
||||
// Private keys
|
||||
[/-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/, 'Private key detected in edit.', 'Never commit private keys. Use environment variables or secret managers.'],
|
||||
|
||||
// JWT secrets
|
||||
[/(jwt[_-]?secret|JWT_SECRET)\s*[:=]\s*["'][^"']{10,}["']/, 'JWT secret detected.', 'Use environment variables instead.'],
|
||||
|
||||
// Database connection strings with passwords
|
||||
[/(postgres|mysql|mongodb):\/\/[^:]+:[^@]+@/, 'Database connection string with credentials detected.', 'Use environment variables for database URLs.'],
|
||||
|
||||
// Azure Storage connection strings
|
||||
[/DefaultEndpointsProtocol=https;AccountName=[^;]+;AccountKey=[A-Za-z0-9+/=]{40,}/, 'Azure Storage connection string with AccountKey detected.', 'Use DefaultAzureCredential or environment variables instead.'],
|
||||
|
||||
// Azure AD / Entra client secrets
|
||||
[/(client[_-]?secret|ClientSecret)\s*[:=]\s*["'][A-Za-z0-9~._-]{34,}["']/, 'Azure AD client secret detected.', 'Use managed identity or environment variables instead.'],
|
||||
|
||||
// Azure Cognitive Services / AI Services keys
|
||||
[/(Ocp-Apim-Subscription-Key|cognitive[_-]?key|ai[_-]?key)\s*[:=]\s*["'][0-9a-f]{32}["']/, 'Azure AI Services key detected.', 'Use DefaultAzureCredential or environment variables instead.'],
|
||||
|
||||
// Slack/Discord webhooks
|
||||
[/https:\/\/hooks\.(slack|discord)\.com\/services\/[A-Za-z0-9/]+/, 'Webhook URL detected.', 'Webhook URLs are secrets. Use environment variables.'],
|
||||
|
||||
// GitHub tokens
|
||||
[/(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}/, 'GitHub token detected.', null],
|
||||
];
|
||||
|
||||
for (const [pattern, reason, advice] of SECRETS) {
|
||||
if (pattern.test(content)) {
|
||||
process.stderr.write(`BLOCKED: ${reason}\n`);
|
||||
if (filePath) process.stderr.write(`File: ${filePath}\n`);
|
||||
if (advice) process.stderr.write(`${advice}\n`);
|
||||
logBlock('secrets', filePath || 'unknown');
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Generic password assignment (warning only, don't block)
|
||||
if (/password\s*[:=]\s*["'][^"']{8,}["']/.test(content)) {
|
||||
if (!/example|placeholder|xxx|your.?password/.test(content)) {
|
||||
process.stderr.write(`WARNING: Possible hardcoded password detected.\n`);
|
||||
if (filePath) process.stderr.write(`File: ${filePath}\n`);
|
||||
process.stderr.write(`Consider using environment variables.\n`);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
|
||||
// --- Audit logging ---
|
||||
function logBlock(hook, target) {
|
||||
try {
|
||||
const home = process.env.HOME || process.env.USERPROFILE || '';
|
||||
const dir = join(home, '.claude', 'audit');
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const ts = new Date().toISOString();
|
||||
appendFileSync(join(dir, 'blocked.log'), `[${ts}] [${hook}] ${target}\n`);
|
||||
} catch {
|
||||
// Audit logging is best-effort
|
||||
}
|
||||
}
|
||||
179
plugins/ms-ai-architect/hooks/scripts/session-start-context.mjs
Normal file
179
plugins/ms-ai-architect/hooks/scripts/session-start-context.mjs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
#!/usr/bin/env node
|
||||
// session-start-context.mjs
|
||||
// Shows active utredning sessions and KB staleness on session start.
|
||||
// Output: plain text to stdout (advisory, never blocking).
|
||||
|
||||
import { readdirSync, statSync, existsSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
|
||||
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || join(process.cwd());
|
||||
const cwd = process.cwd();
|
||||
|
||||
const lines = [];
|
||||
|
||||
// --- 1. Check for active utredning sessions (.work/ directories) ---
|
||||
const workDir = join(cwd, '.work');
|
||||
let activeUtredninger = 0;
|
||||
|
||||
if (existsSync(workDir)) {
|
||||
try {
|
||||
const entries = readdirSync(workDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
activeUtredninger++;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore read errors
|
||||
}
|
||||
}
|
||||
|
||||
// Also check docs/**/utredning.md
|
||||
const docsDir = join(cwd, 'docs');
|
||||
let utredningFiles = 0;
|
||||
|
||||
if (existsSync(docsDir)) {
|
||||
try {
|
||||
utredningFiles = countFiles(docsDir, 'utredning.md');
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. Check KB staleness (stat mtime, no content reading) ---
|
||||
const staleLevels = { critical: 0, high: 0, medium: 0 };
|
||||
const now = Date.now();
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
const skillsDir = join(pluginRoot, 'skills');
|
||||
if (existsSync(skillsDir)) {
|
||||
try {
|
||||
const skillDirs = readdirSync(skillsDir, { withFileTypes: true });
|
||||
for (const skill of skillDirs) {
|
||||
if (!skill.isDirectory()) continue;
|
||||
const refsDir = join(skillsDir, skill.name, 'references');
|
||||
if (!existsSync(refsDir)) continue;
|
||||
countStaleFiles(refsDir, staleLevels, now);
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. Check EU AI Act deadlines ---
|
||||
const AI_ACT_DEADLINES = [
|
||||
{ date: new Date('2025-02-02'), label: 'Forbudte AI-praksiser (Art. 5)' },
|
||||
{ date: new Date('2025-08-02'), label: 'Governance + sanksjoner (Art. 99)' },
|
||||
{ date: new Date('2026-08-02'), label: 'GPAI-krav + høyrisiko i Annex III' },
|
||||
{ date: new Date('2027-08-02'), label: 'Alle høyrisiko-krav (full compliance)' },
|
||||
];
|
||||
|
||||
let nearestDeadline = null;
|
||||
for (const dl of AI_ACT_DEADLINES) {
|
||||
const daysLeft = Math.ceil((dl.date.getTime() - now) / DAY_MS);
|
||||
if (daysLeft > 0 && daysLeft <= 180) {
|
||||
if (!nearestDeadline || daysLeft < nearestDeadline.daysLeft) {
|
||||
nearestDeadline = { ...dl, daysLeft };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. Check onboarding status ---
|
||||
const orgDir = join(pluginRoot, 'org');
|
||||
const ORG_FILES = [
|
||||
'organization-profile.md',
|
||||
'technology-stack.md',
|
||||
'security-compliance.md',
|
||||
'architecture-decisions.md',
|
||||
'business-references.md',
|
||||
];
|
||||
let orgComplete = 0;
|
||||
const orgExists = existsSync(orgDir);
|
||||
if (orgExists) {
|
||||
for (const f of ORG_FILES) {
|
||||
if (existsSync(join(orgDir, f))) orgComplete++;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. Build output ---
|
||||
const parts = [];
|
||||
|
||||
if (activeUtredninger > 0) {
|
||||
parts.push(`${activeUtredninger} aktiv(e) utredning(er) i .work/`);
|
||||
}
|
||||
if (utredningFiles > 0) {
|
||||
parts.push(`${utredningFiles} utredningsdokument(er) i docs/`);
|
||||
}
|
||||
|
||||
if (!orgExists || orgComplete === 0) {
|
||||
parts.push('Ingen virksomhetstilpasning. Kjør /architect:onboard (~5 min)');
|
||||
} else if (orgComplete < ORG_FILES.length) {
|
||||
parts.push(`Onboarding ${orgComplete}/${ORG_FILES.length}. Kjør /architect:onboard for å fullføre`);
|
||||
}
|
||||
|
||||
const staleEntries = [];
|
||||
if (staleLevels.critical > 0) staleEntries.push(`${staleLevels.critical} critical`);
|
||||
if (staleLevels.high > 0) staleEntries.push(`${staleLevels.high} high`);
|
||||
if (staleLevels.medium > 0) staleEntries.push(`${staleLevels.medium} medium`);
|
||||
|
||||
if (staleEntries.length > 0) {
|
||||
parts.push(`KB stale: ${staleEntries.join(', ')}`);
|
||||
}
|
||||
|
||||
if (nearestDeadline) {
|
||||
parts.push(`EU AI Act: ${nearestDeadline.daysLeft} dager til ${nearestDeadline.label}. Kjør /architect:classify`);
|
||||
}
|
||||
|
||||
if (parts.length > 0) {
|
||||
lines.push(`Architect: ${parts.join('. ')}. /architect:help`);
|
||||
} else {
|
||||
lines.push('Architect: Ingen aktive sesjoner. KB oppdatert. /architect:help');
|
||||
}
|
||||
|
||||
if (lines.length > 0) {
|
||||
process.stdout.write(lines.join('\n') + '\n');
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
function countFiles(dir, filename) {
|
||||
let count = 0;
|
||||
try {
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
count += countFiles(fullPath, filename);
|
||||
} else if (entry.name === filename) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore permission errors
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function countStaleFiles(dir, levels, now) {
|
||||
try {
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
countStaleFiles(fullPath, levels, now);
|
||||
} else if (entry.name.endsWith('.md')) {
|
||||
try {
|
||||
const mtime = statSync(fullPath).mtimeMs;
|
||||
const ageDays = (now - mtime) / DAY_MS;
|
||||
if (ageDays > 180) levels.critical++;
|
||||
else if (ageDays > 90) levels.high++;
|
||||
else if (ageDays > 60) levels.medium++;
|
||||
} catch {
|
||||
// Skip unreadable files
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env node
|
||||
// stop-assessment-reminder.mjs
|
||||
// Reminds about uncommitted assessments and suggests next steps on session end.
|
||||
// Output: JSON { systemMessage } to stdout. Always exits 0 (advisory, never blocking).
|
||||
|
||||
import { readdirSync, statSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const cwd = process.cwd();
|
||||
const workDir = join(cwd, '.work');
|
||||
const TWELVE_HOURS_MS = 12 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
|
||||
// No .work/ directory — nothing to remind about
|
||||
if (!existsSync(workDir)) {
|
||||
console.log('{}');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Find recent state files in .work/
|
||||
const recentSessions = [];
|
||||
|
||||
try {
|
||||
const entries = readdirSync(workDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const sessionDir = join(workDir, entry.name);
|
||||
try {
|
||||
const files = readdirSync(sessionDir);
|
||||
for (const file of files) {
|
||||
const filePath = join(sessionDir, file);
|
||||
try {
|
||||
const stat = statSync(filePath);
|
||||
if (now - stat.mtimeMs < TWELVE_HOURS_MS) {
|
||||
recentSessions.push(entry.name);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.log('{}');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (recentSessions.length === 0) {
|
||||
console.log('{}');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Build reminder
|
||||
const suggestions = [
|
||||
'/architect:adr — generer ADR fra vurderinger',
|
||||
'/architect:export — eksporter til PDF',
|
||||
'/architect:summary — lag beslutningsnotat',
|
||||
];
|
||||
|
||||
// Add AI Act suggestion if deadline is within 180 days
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const gpaiDeadline = new Date('2026-08-02');
|
||||
const daysToGpai = Math.ceil((gpaiDeadline.getTime() - now) / DAY_MS);
|
||||
if (daysToGpai > 0 && daysToGpai <= 180) {
|
||||
suggestions.push(`/architect:classify — EU AI Act-klassifisering (${daysToGpai}d til GPAI-frist)`);
|
||||
}
|
||||
|
||||
const sessionList = recentSessions.join(', ');
|
||||
const message = `Architect: ${recentSessions.length} aktiv(e) vurdering(er) i .work/ (${sessionList}). Foreslåtte neste steg: ${suggestions.join(' | ')}`;
|
||||
|
||||
console.log(JSON.stringify({ systemMessage: message }));
|
||||
process.exit(0);
|
||||
Loading…
Add table
Add a link
Reference in a new issue