diff --git a/AGENTS.md b/AGENTS.md index de7351d..a68f1e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,175 +1,99 @@ # MISSION: Corrosion Admin Panel // AGENTS.md -## 1. STANDING ORDERS (IMMEDIATE ACTION) +## 1. STANDING ORDERS (THE PROTOCOL) -* **COMMUNICATION:** Use military terminology. Be direct and precise. -* **DECISION MAKING:** Present trade-offs as **Courses of Action (COAs)** with clear pros/cons. Let the Operator (User) decide. -* **STANDARD:** Treat *every* change as a production deployment (target: corrosionmgmt.com). -* **DOCUMENTATION:** Document **WHY**, not just WHAT, in all commits and `CHANGELOG.md`. +- **COMMAND STRUCTURE:** **The Architect (Opus)** has the conn. All strategic decisions, debugging logic, and COA drafting start here. -## 1.5. RESOURCE DISCIPLINE -* **Context Loading:** Use **The Scout (Haiku)** for all initial file reading, searching, and summarizing. -* **Cost Efficiency:** Default to **The Specialist (Sonnet)** for standard logic and code generation. -* **Heavy Lifting:** Reserve **The Architect/Sniper (Opus)** strictly for complex planning or critical debugging. +- **RESOURCE DISCIPLINE:** + + - **Opus (XO):** Strategic Command. Output is **Plans**, not Code. Limit output to high-value reasoning. + + - **Haiku (Scout):** Context Loading. "Read this 100-line file and summarize it" costs pennies compared to Opus reading it. + + - **Sonnet (Specialist):** Mass Execution. "Write the code, write the tests, fix the lint." --- -## 2. INTELLIGENCE BRIEF: OPERATOR PROFILE & LESSONS LEARNED +## 2. THE ROSTER (STREAMLINED) -*Context: These are the confirmed capabilities and preferences of the Commander based on the Feb 2026 Build Sprint. All agents must operate assuming this level of competency and standard.* +### **COMMAND ELEMENT** -### **A. Systemic Vision & Debugging** +### **THE ARCHITECT (The XO)** -* **The "Onion" Standard:** Do not stop at the first symptom. The Operator traces cascading failures across infrastructure layers (e.g., NATS FK spam → DB exhaustion → Auth timeout). -* **Security Instinct:** When touching auth flows, instinctively upgrade security. Do not just patch bugs; architectural flaws (like tokens-in-URLs) must be remediated with proper patterns (code exchange). +- **Model:** opus -### **B. Operational Velocity** +- **Role:** Mission Commander, Strategy, Root Cause Analysis. -* **Massive Context:** The Operator can hold the entire platform in their head. Agents are authorized to execute broad, multi-file changes (30+ files, full-stack vertical slices) in a single pass. -* **Production Reality:** Debugging is not just reading code; it is correlating timestamps, checking event loops, and analyzing live telemetry. - -### **C. Command Style: "Autonomy with Guardrails"** - -* **V4_WORKFLOW:** (RECON → PLAN → EXECUTE → SITREP). -* **The Balance:** The Operator works best when trusted to move fast ("Full Send") but expects agents to communicate via COAs when real trade-offs exist. +- **Directives:** + + - **Think, Don't Type:** Analyze the situation, define the architecture, and issue the **Course of Action (COA)**. + + - **Delegate:** Do not write implementation code. Issue the blueprint to The Specialist. + + - **The Nuclear Option:** Only engage in direct coding if The Specialist fails critical logic twice. --- -## 3. THE ROSTER (AGENT PERSONA MAPPING) +### **TROOP ELEMENT (THE WORKHORSES)** -### **THE OVERWATCH (Project Manager)** +### **TYPE 1: THE SCOUT (Intelligence)** -* **Role:** Mission coherence, documentation, and state management. -* **Directives:** - * Maintain `README.md` and `CHANGELOG.md` as the single source of truth. - * Ensure no "scope creep" occurs without Commander approval. - * Summarize technical debts incurred by the engineering team. +- **Model:** haiku -### **THE SCOUT (Reconnaissance)** +- **Role:** Reconnaissance, Context Mapping, Log Analysis. -* **Model:** haiku (or claude-3-5-haiku) -* **Role:** High-speed intelligence gathering, context mapping, and file summarization. -* **Directives:** - * **Read-Only:** STRICTLY FORBIDDEN from writing code or modifying files. - * **Map:** Generate directory trees and dependency graphs. - * **Summarize:** Digest large documentation or log files into executive summaries. - * **Hunt:** Locate specific strings, TODOs, or "Broken Windows" across the entire codebase. -* **Trigger Conditions:** - * Initial project startup (booting context). - * "Catch me up" requests after a break. - * Searching for relevant files before a strike. -* **Anti-Patterns (DO NOT DEPLOY FOR):** - * Writing code. - * Logical reasoning or debugging. - * Architecture planning. +- **Directives:** + + - **Read-Only:** STRICTLY FORBIDDEN from writing code or modifying files. + + - **The Pre-Flight:** Run this agent *before* waking up the Architect. + + - **Summarize:** Digest large documentation/logs into executive summaries for the Commander. -### **THE ARCHITECT (Lead Developer)** +### **TYPE 2: THE SPECIALIST (Execution)** -* **Model:** opus -* **Role:** System design, strategy, and risk assessment. -* **Directives:** - * Do NOT write implementation code. - * Produce **COAs**. Always offer at least two paths (e.g., "Fast & Dirty" vs. "Robust & Slow"). - * Enforce the "Resurrection" standard (no single points of failure). +- **Model:** sonnet -### **THE SNIPER (Deep Debugging / Complex Implementation)** +- **Role:** **Consolidated Engineering** (Coding, Testing, Scaffolding, Refactoring). -* **Model:** opus -* **Role:** Surgical strikes on high-complexity problems. -* **Directives:** - * **Escalation Only:** Engage only on Architect/Specialist request or during critical failure. - * **Trace & Destroy:** Trace cascading failures across infrastructure layers (Database → API → Client). - * **Novelty:** Design and implement solutions where no pattern currently exists. - * **Security:** Own the implementation of security-critical code (auth, permissions, encryption). - * **Handoff:** Document architectural decisions clearly for the Specialist to maintain, then return control. -* **Trigger Conditions:** - * Cascading failure analysis (root cause unknown). - * Security-critical implementations (Zero Trust, AuthZ/AuthN). - * Novel architecture requirements (no existing SOP). - * Production debugging with unclear symptoms. -* **Anti-Patterns (DO NOT DEPLOY FOR):** - * CRUD operations. - * API route plumbing. - * Pattern-following implementations. - * UI components or CSS. - * Routine refactoring. - -### **THE SPECIALIST (Sr. Developer)** - -* **Model:** sonnet -* **Role:** Heavy lifting, core logic, complex algorithms. -* **Directives:** - * Focus on "Critical Path" code. - * Assume high autonomy. - * Optimize for performance and security. - -### **THE SAPPER (Jr. Developer)** - -* **Model:** sonnet -* **Role:** Scaffolding, boilerplate, refactoring, and cleanup. -* **Directives:** - * Follow the Architect's specs exactly. Do not improvise. - * Handle verbosity (logging, comments, minor bug fixes). - * "Clear the path" for the Specialist. - -### **THE AUDITOR (QA / Tester)** - -* **Model:** sonnet -* **Role:** Verification, stress testing, and breaking things. -* **Directives:** - * Act hostile to the code. Try to break it. - * Enforce "Resurrection" checks: Kill the process and see if it recovers. - * If tests fail, reject the PR (Pulse Check) immediately. +- **Directives:** + + - **Full Stack Capable:** Merges previous "Specialist" and "Sapper" roles. + + - **High Volume:** Writes the implementation, writes the tests, fixes the lint errors. + + - **Autonomy:** If a test fails, self-correct. Do not wake the Architect unless stuck in a logical loop. --- -## 4. STANDARD OPERATING PROCEDURES (SOP) +## 3. STANDARD OPERATING PROCEDURES (SOP) -### **PHASE 1: RECON (The Scan)** +### **PHASE 1: RECON (The Scout)** -* **Agent:** Scout (Haiku) -* **Order:** 1. "Scan the target directory. Map the dependencies." - 2. "Summarize the current state of [File/Module]." - 3. "Identify potential conflicts for the upcoming strike." - 4. **Handoff:** Pass the *summarized* context to the Architect/Specialist (saving their context tokens). +- **Agent:** Scout (Haiku) -### **PHASE 2: PLAN (The Blueprint)** +- **Command:** "Scan [Target Directory]. Map dependencies. Summarize current state." -* **Agent:** Architect -* **Order:** "Review the User's request. Draft a COA. Update `AGENTS.md` if the mission parameters change." +- **Goal:** Build the context window cheaply. -### **PHASE 3: ENGAGE (The Swarm)** +### **PHASE 2: STRATEGY (The Architect)** -* **Standard Team:** Specialist (Sonnet) + Sapper (Sonnet) -* **Protocol:** - 1. **Specialist (XO):** Execute COA 1. If logic follows an existing pattern, execute immediately. - 2. **Escalation Trigger:** If the problem requires novel reasoning, complex security, or debugging a cascade, **STOP** and issue command: *"Requesting Sniper Support."* - 3. **Sniper:** Intervene, resolve the specific blockage, document the fix, and return command to Specialist. - 4. **Sapper:** Clean up, write tests, and handle documentation in parallel. +- **Agent:** Architect (Opus) + +- **Command:** "Review Scout intel. Create a COA for [Mission]." + +- **Goal:** High-IQ planning. Output is a Markdown plan with clear steps. + +### **PHASE 3: EXECUTION (The Specialist)** + +- **Agent:** Specialist (Sonnet) + +- **Command:** "Execute the Architect's COA. Implement features, write tests, and verify." + +- **Goal:** burn tokens on syntax and boilerplate, saving Opus for pure thought. ### **PHASE 4: SITREP (The Report)** -* **Agent:** Overwatch -* **Order:** "Compile the results. Report status. Await next command." +- **Agent:** Specialist (Sonnet) ---- - -## 5. MISSION LOG - -### 2026-02-15 // NestJS Module Generation (Wipes, Maps, Plugins) - -**Agent:** Specialist (Sonnet 4.5) -**Objective:** Generate complete NestJS modules with controller/service/DTO/module structure for Wipes, Maps, and Plugins. - -**Execution:** -- Generated 3 complete modules totaling 16 files across DTOs, services, controllers, and module definitions -- All files follow established patterns: @InjectRepository, @CurrentTenant(), @RequirePermission(), ApiTags/ApiBearerAuth -- class-validator decorators on all DTO fields, PartialType imported from @nestjs/swagger for proper Swagger integration -- Permission-based guards applied: wipe.view/manage/execute, map.view/manage, plugin.view/manage - -**Deliverables:** -- **Wipes Module** (7 files): Profile/schedule CRUD, wipe history, manual trigger, dry-run simulation -- **Maps Module** (5 files): Library management, rotation system with order control -- **Plugins Module** (6 files): Install/uninstall, config management, reload trigger, uMod search stub - -**Result:** All modules operational and ready for integration into main app.module.ts. Multi-tenant isolation enforced via license_id scoping. +- **Command:** "Report status. Update CHANGELOG. Identify Lessons Learned." diff --git a/backend-nest/src/app.module.ts b/backend-nest/src/app.module.ts index 7d3ef41..ca99b07 100644 --- a/backend-nest/src/app.module.ts +++ b/backend-nest/src/app.module.ts @@ -34,6 +34,7 @@ import { WebstoreModule } from './modules/webstore/webstore.module'; import { AdminModule } from './modules/admin/admin.module'; import { SetupModule } from './modules/setup/setup.module'; import { MigrationModule } from './modules/migration/migration.module'; +import { ChangelogModule } from './modules/changelog/changelog.module'; // Shared Services import { NatsService } from './services/nats.service'; @@ -103,6 +104,7 @@ import { NatsBridgeGateway } from './gateways/nats-bridge.gateway'; AdminModule, SetupModule, MigrationModule, + ChangelogModule, ], providers: [ // Global guards (order matters: auth first, then license, then permissions) diff --git a/backend-nest/src/common/decorators/current-tenant.decorator.ts b/backend-nest/src/common/decorators/current-tenant.decorator.ts index 3e3f7a6..c5348ca 100644 --- a/backend-nest/src/common/decorators/current-tenant.decorator.ts +++ b/backend-nest/src/common/decorators/current-tenant.decorator.ts @@ -1,8 +1,14 @@ -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@nestjs/common'; export const CurrentTenant = createParamDecorator( (_data: unknown, ctx: ExecutionContext): string => { const request = ctx.switchToHttp().getRequest(); - return request.user?.license_id; + const licenseId = request.user?.license_id; + if (!licenseId) { + throw new UnauthorizedException( + 'No license associated with this account. Create or join a server first.', + ); + } + return licenseId; }, ); diff --git a/backend-nest/src/modules/auth/jwt.strategy.ts b/backend-nest/src/modules/auth/jwt.strategy.ts index 6ed968c..04845b0 100644 --- a/backend-nest/src/modules/auth/jwt.strategy.ts +++ b/backend-nest/src/modules/auth/jwt.strategy.ts @@ -52,16 +52,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) { last_login_at: new Date(), }); - // If super admin, return basic payload - if (user.is_super_admin) { - return { - sub: user.id, - email: user.email, - username: user.username, - is_super_admin: true, - }; - } - // Find user's license - either as owner or team member let license: License | null = null; let role: Role | null = null; @@ -76,8 +66,8 @@ export class JwtStrategy extends PassportStrategy(Strategy) { role = await this.roleRepository.findOne({ where: { role_name: 'Owner', is_system_default: true }, }); - } else { - // Check if user is a team member + } else if (!user.is_super_admin) { + // Check if user is a team member (skip for super admins without a license) const teamMember = await this.teamMemberRepository.findOne({ where: { user_id: user.id }, relations: ['license', 'role'], @@ -93,7 +83,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { sub: user.id, email: user.email, username: user.username, - is_super_admin: false, + is_super_admin: user.is_super_admin, license_id: license?.id, permissions: role?.permissions || {}, }; diff --git a/backend-nest/src/modules/changelog/changelog.controller.ts b/backend-nest/src/modules/changelog/changelog.controller.ts new file mode 100644 index 0000000..b02fa9e --- /dev/null +++ b/backend-nest/src/modules/changelog/changelog.controller.ts @@ -0,0 +1,24 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger'; +import { ChangelogService } from './changelog.service'; +import { Public } from '../../common/decorators/public.decorator'; + +@ApiTags('changelog') +@Controller('changelog') +export class ChangelogController { + constructor(private readonly changelogService: ChangelogService) {} + + @Get() + @Public() + @ApiOperation({ summary: 'Get changelog entries' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + async getEntries( + @Query('page') page?: string, + @Query('limit') limit?: string, + ) { + const p = parseInt(page || '1', 10); + const l = parseInt(limit || '20', 10); + return this.changelogService.getEntries(p, l); + } +} diff --git a/backend-nest/src/modules/changelog/changelog.module.ts b/backend-nest/src/modules/changelog/changelog.module.ts new file mode 100644 index 0000000..cbcf0f4 --- /dev/null +++ b/backend-nest/src/modules/changelog/changelog.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ChangelogController } from './changelog.controller'; +import { ChangelogService } from './changelog.service'; + +@Module({ + controllers: [ChangelogController], + providers: [ChangelogService], +}) +export class ChangelogModule {} diff --git a/backend-nest/src/modules/changelog/changelog.service.ts b/backend-nest/src/modules/changelog/changelog.service.ts new file mode 100644 index 0000000..0132520 --- /dev/null +++ b/backend-nest/src/modules/changelog/changelog.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; + +export interface ChangelogEntry { + id: string; + version: string; + title: string; + description: string; + type: 'feature' | 'fix' | 'improvement' | 'breaking'; + created_at: string; +} + +@Injectable() +export class ChangelogService { + async getEntries( + page: number, + limit: number, + ): Promise<{ entries: ChangelogEntry[]; total: number }> { + // Stub — returns empty until changelog entries are seeded or managed + return { entries: [], total: 0 }; + } +} diff --git a/frontend/src/stores/wipe.ts b/frontend/src/stores/wipe.ts index e697fa6..ebf98ec 100644 --- a/frontend/src/stores/wipe.ts +++ b/frontend/src/stores/wipe.ts @@ -3,12 +3,10 @@ import { ref, onMounted } from 'vue' import type { WipeProfile, WipeSchedule, WipeHistory } from '@/types' import { useApi } from '@/composables/useApi' import { useWebSocket, type WebSocketMessage } from '@/composables/useWebSocket' -import { useAuthStore } from '@/stores/auth' import { useToastStore } from '@/stores/toast' export const useWipeStore = defineStore('wipe', () => { const api = useApi() - const authStore = useAuthStore() const websocket = useWebSocket() const toast = useToastStore() @@ -91,7 +89,7 @@ export const useWipeStore = defineStore('wipe', () => { isLoading.value = true error.value = null try { - const data = await api.get('/profiles') + const data = await api.get('/wipes/profiles') profiles.value = data } catch (err) { error.value = err instanceof Error ? err.message : 'Failed to fetch profiles' @@ -106,7 +104,7 @@ export const useWipeStore = defineStore('wipe', () => { isLoading.value = true error.value = null try { - const data = await api.get('/schedules') + const data = await api.get('/wipes/schedules') schedules.value = data } catch (err) { error.value = err instanceof Error ? err.message : 'Failed to fetch schedules' @@ -121,11 +119,6 @@ export const useWipeStore = defineStore('wipe', () => { isLoading.value = true error.value = null try { - const licenseId = authStore.license?.id - if (!licenseId) { - throw new Error('No license ID available') - } - const data = await api.get(`/wipes/history?limit=${limit}`) history.value = data } catch (err) { @@ -144,15 +137,9 @@ export const useWipeStore = defineStore('wipe', () => { isLoading.value = true error.value = null try { - const licenseId = authStore.license?.id - if (!licenseId) { - throw new Error('No license ID available') - } - const result = await api.post<{ wipe_history_id: string }>( `/wipes/trigger`, { - license_id: licenseId, wipe_type: wipeType, profile_id: profileId, trigger_type: 'manual', @@ -197,11 +184,6 @@ export const useWipeStore = defineStore('wipe', () => { isLoading.value = true error.value = null try { - const licenseId = authStore.license?.id - if (!licenseId) { - throw new Error('No license ID available') - } - const result = await api.post<{ would_delete: string[] would_preserve: string[] @@ -209,7 +191,6 @@ export const useWipeStore = defineStore('wipe', () => { }>( `/wipes/dry-run`, { - license_id: licenseId, wipe_type: wipeType, profile_id: profileId, } @@ -229,15 +210,7 @@ export const useWipeStore = defineStore('wipe', () => { isLoading.value = true error.value = null try { - const licenseId = authStore.license?.id - if (!licenseId) { - throw new Error('No license ID available') - } - - const newProfile = await api.post('/profiles', { - ...profile, - license_id: licenseId, - }) + const newProfile = await api.post('/wipes/profiles', profile) profiles.value.push(newProfile) return newProfile @@ -254,7 +227,7 @@ export const useWipeStore = defineStore('wipe', () => { isLoading.value = true error.value = null try { - const updated = await api.put(`/profiles/${id}`, updates) + const updated = await api.put(`/wipes/profiles/${id}`, updates) const index = profiles.value.findIndex(p => p.id === id) if (index !== -1) { profiles.value[index] = updated @@ -272,7 +245,7 @@ export const useWipeStore = defineStore('wipe', () => { isLoading.value = true error.value = null try { - await api.del(`/profiles/${id}`) + await api.del(`/wipes/profiles/${id}`) profiles.value = profiles.value.filter(p => p.id !== id) } catch (err) { error.value = err instanceof Error ? err.message : 'Failed to delete profile' diff --git a/frontend/src/views/admin/ChangelogView.vue b/frontend/src/views/admin/ChangelogView.vue index 75a8012..4cb8731 100644 --- a/frontend/src/views/admin/ChangelogView.vue +++ b/frontend/src/views/admin/ChangelogView.vue @@ -21,11 +21,12 @@ const hasMore = ref(true) async function fetchChangelog() { isLoading.value = true try { - const result = await api.get(`/changelog?page=${page.value}&limit=20`) - if (result.length === 0) { + const result = await api.get<{ entries: ChangelogEntry[] } | ChangelogEntry[]>(`/changelog?page=${page.value}&limit=20`) + const items = Array.isArray(result) ? result : (result.entries ?? []) + if (items.length === 0) { hasMore.value = false } else { - entries.value.push(...result) + entries.value.push(...items) } } finally { isLoading.value = false