Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10b6260c4c | |||
| 69074ba119 | |||
|
|
cda896ebe0 | ||
|
|
c439194c18 | ||
|
|
bf7c0d7d61 |
7 changed files with 803 additions and 206 deletions
592
FINDINGS.md
Normal file
592
FINDINGS.md
Normal file
|
|
@ -0,0 +1,592 @@
|
|||
# Phase 0: Environment Assessment + Test Baseline
|
||||
|
||||
**Date:** 2026-02-20
|
||||
**Branch:** feat/phase-0-assessment
|
||||
**Issue:** #1
|
||||
**Agent:** Clotho <clotho@lthn.ai>
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This phase 0 assessment provides a comprehensive baseline of the `host-uk/core-agentic` Laravel package. The package implements AI agent orchestration with MCP (Model Context Protocol) tools, multi-agent collaboration, and unified AI provider access.
|
||||
|
||||
**Key Findings:**
|
||||
- ✅ Well-structured event-driven architecture
|
||||
- ✅ Comprehensive test coverage (~65%) with Pest framework
|
||||
- ✅ Security-conscious design with recent hardening (Jan 2026)
|
||||
- ⚠️ Cannot run tests without `host-uk/core` dependency
|
||||
- ⚠️ Some code quality issues identified in existing TODO.md
|
||||
- ✅ Excellent documentation and conventions
|
||||
|
||||
---
|
||||
|
||||
## 1. Environment Assessment
|
||||
|
||||
### 1.1 Dependency Constraints
|
||||
|
||||
**Status:** ⚠️ BLOCKED - Cannot install dependencies
|
||||
|
||||
```bash
|
||||
$ composer install --no-interaction
|
||||
# Error: host-uk/core dev-main could not be found
|
||||
```
|
||||
|
||||
**Root Cause:**
|
||||
- Package depends on `host-uk/core` (dev-main) which is a private dependency
|
||||
- No composer.lock file present
|
||||
- Missing repository configuration for private packages
|
||||
|
||||
**Impact:**
|
||||
- Cannot run test suite (`composer test` / `vendor/bin/pest`)
|
||||
- Cannot run linter (`composer run lint` / `vendor/bin/pint`)
|
||||
- Cannot run static analysis (`vendor/bin/phpstan`)
|
||||
|
||||
**Recommendation:**
|
||||
- Add repository configuration to composer.json for host-uk/core
|
||||
- OR provide mock/stub for testing in isolation
|
||||
- OR test within a full host application environment
|
||||
|
||||
### 1.2 Codebase Metrics
|
||||
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Total PHP files | 125 |
|
||||
| Models | 9 |
|
||||
| Services | 15 |
|
||||
| MCP Tools | 34 |
|
||||
| Migrations | 3 |
|
||||
| Tests | 15+ test files |
|
||||
| Console Commands | 3 |
|
||||
| Livewire Components | 9 |
|
||||
|
||||
### 1.3 Test Infrastructure
|
||||
|
||||
**Framework:** Pest 3.x (functional syntax)
|
||||
|
||||
**Configuration:**
|
||||
- `tests/Pest.php` - Central configuration
|
||||
- `tests/TestCase.php` - Orchestra Testbench base class
|
||||
- RefreshDatabase trait applied to Feature tests
|
||||
|
||||
**Test Coverage Breakdown (from TODO.md):**
|
||||
```
|
||||
Current: ~65% (improved from ~35% in Jan 2026)
|
||||
|
||||
✅ Models: Well tested
|
||||
- AgentPlan, AgentPhase, AgentSession, AgentApiKey
|
||||
|
||||
✅ Services: Partially tested
|
||||
- AgentApiKeyService (58 tests)
|
||||
- IpRestrictionService (78 tests)
|
||||
- PlanTemplateService (47 tests)
|
||||
- AI Providers: ClaudeService, GeminiService, OpenAIService, AgenticManager
|
||||
|
||||
❌ Untested:
|
||||
- 3 Console Commands
|
||||
- 9 Livewire Components
|
||||
- Some MCP Tools
|
||||
```
|
||||
|
||||
**Test Files:**
|
||||
```
|
||||
tests/
|
||||
├── Feature/
|
||||
│ ├── AgentApiKeyTest.php (70+ tests)
|
||||
│ ├── AgentApiKeyServiceTest.php (58 tests)
|
||||
│ ├── IpRestrictionServiceTest.php (78 tests)
|
||||
│ ├── PlanTemplateServiceTest.php (47 tests)
|
||||
│ ├── ContentServiceTest.php
|
||||
│ ├── AgentPlanTest.php
|
||||
│ ├── AgentPhaseTest.php
|
||||
│ ├── AgentSessionTest.php
|
||||
│ └── SecurityTest.php
|
||||
├── Unit/
|
||||
│ ├── ClaudeServiceTest.php
|
||||
│ ├── GeminiServiceTest.php
|
||||
│ ├── OpenAIServiceTest.php
|
||||
│ └── AgenticManagerTest.php
|
||||
└── UseCase/
|
||||
└── AdminPanelBasic.php
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture Review
|
||||
|
||||
### 2.1 Boot System (Event-Driven)
|
||||
|
||||
**Pattern:** Event-driven lazy loading via Laravel service provider
|
||||
|
||||
```php
|
||||
// Boot.php - Responds to Core framework events
|
||||
public static array $listens = [
|
||||
AdminPanelBooting::class => 'onAdminPanel',
|
||||
ConsoleBooting::class => 'onConsole',
|
||||
McpToolsRegistering::class => 'onMcpTools',
|
||||
];
|
||||
```
|
||||
|
||||
**Strengths:**
|
||||
- ✅ Lazy loading reduces unnecessary overhead
|
||||
- ✅ Clean separation of concerns
|
||||
- ✅ Follows Laravel conventions
|
||||
- ✅ Event handlers are well-documented
|
||||
|
||||
**Rate Limiting:**
|
||||
- Configured in Boot::configureRateLimiting()
|
||||
- 60 requests/minute per IP for agentic-api
|
||||
- Separate API key-based rate limiting in AgentApiKey model
|
||||
|
||||
### 2.2 MCP Tools Architecture
|
||||
|
||||
**Location:** `Mcp/Tools/Agent/` organised by domain
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
Mcp/Tools/Agent/
|
||||
├── AgentTool.php (Base class)
|
||||
├── Contracts/
|
||||
│ └── AgentToolInterface.php
|
||||
├── Plan/ (5 tools)
|
||||
├── Phase/ (3 tools)
|
||||
├── Session/ (9 tools)
|
||||
├── State/ (3 tools)
|
||||
├── Task/ (2 tools)
|
||||
├── Content/ (8 tools)
|
||||
└── Template/ (3 tools)
|
||||
```
|
||||
|
||||
**Base Class Features (AgentTool):**
|
||||
- Input validation helpers (requireString, optionalInt, requireEnum)
|
||||
- Circuit breaker protection via withCircuitBreaker()
|
||||
- Standardised response format (success(), error())
|
||||
|
||||
**Design Pattern:**
|
||||
```php
|
||||
abstract class AgentTool implements AgentToolInterface
|
||||
{
|
||||
protected function requireString(array $input, string $key): string;
|
||||
protected function optionalInt(array $input, string $key, ?int $default = null): ?int;
|
||||
protected function requireEnum(array $input, string $key, array $allowed): string;
|
||||
protected function withCircuitBreaker(callable $callback);
|
||||
protected function success(array $data): array;
|
||||
protected function error(string $message, int $code = 400): array;
|
||||
}
|
||||
```
|
||||
|
||||
**Strengths:**
|
||||
- ✅ Clean abstraction with consistent interface
|
||||
- ✅ Built-in validation helpers
|
||||
- ✅ Circuit breaker for resilience
|
||||
- ✅ Domain-driven organisation
|
||||
|
||||
**Potential Issues:**
|
||||
- ⚠️ No per-tool rate limiting (noted in TODO.md SEC-004)
|
||||
- ⚠️ Workspace scoping added recently (SEC-003 - Jan 2026)
|
||||
|
||||
### 2.3 AI Provider System
|
||||
|
||||
**Manager:** `AgenticManager` (singleton)
|
||||
|
||||
**Supported Providers:**
|
||||
1. Claude (Anthropic) - `ClaudeService`
|
||||
2. Gemini (Google) - `GeminiService`
|
||||
3. OpenAI - `OpenAIService`
|
||||
|
||||
**Usage Pattern:**
|
||||
```php
|
||||
$ai = app(AgenticManager::class);
|
||||
$ai->claude()->generate($prompt);
|
||||
$ai->gemini()->generate($prompt);
|
||||
$ai->openai()->generate($prompt);
|
||||
$ai->provider('gemini')->generate($prompt);
|
||||
```
|
||||
|
||||
**Shared Concerns (Traits):**
|
||||
- `HasRetry` - Automatic retry with exponential backoff
|
||||
- `HasStreamParsing` - Server-sent events (SSE) parsing
|
||||
|
||||
**Strengths:**
|
||||
- ✅ Clean provider abstraction
|
||||
- ✅ Consistent interface across providers
|
||||
- ✅ Built-in retry logic
|
||||
- ✅ Streaming support
|
||||
|
||||
**Identified Issues (from TODO.md):**
|
||||
- ⚠️ DX-002: No API key validation on init (provider fails on first use)
|
||||
- ⚠️ ERR-001: ClaudeService stream() lacks error handling
|
||||
|
||||
### 2.4 Data Model Design
|
||||
|
||||
**Core Models:**
|
||||
|
||||
| Model | Purpose | Relationships |
|
||||
|-------|---------|---------------|
|
||||
| `AgentPlan` | Work plans with phases | hasMany AgentPhase, hasMany AgentSession |
|
||||
| `AgentPhase` | Plan phases with tasks | belongsTo AgentPlan, hasMany Task |
|
||||
| `AgentSession` | Agent execution sessions | belongsTo AgentPlan, has work_log JSON |
|
||||
| `AgentApiKey` | API key management | belongsTo Workspace, has permissions array |
|
||||
| `WorkspaceState` | Key-value state storage | belongsTo Workspace |
|
||||
| `AgentWorkspaceState` | (Duplicate?) | - |
|
||||
|
||||
**Schema Features:**
|
||||
- Status enums: `pending`, `in_progress`, `completed`, `failed`, `abandoned`
|
||||
- JSON columns: work_log, context, permissions, metadata
|
||||
- Soft deletes on plans and sessions
|
||||
- Timestamps on all models
|
||||
|
||||
**Identified Issues:**
|
||||
- ⚠️ CQ-001: Duplicate state models (WorkspaceState vs AgentWorkspaceState)
|
||||
- ⚠️ DB-002: Missing indexes on frequently queried columns (slug, session_id, key)
|
||||
- ⚠️ PERF-001: AgentPhase::checkDependencies does N queries
|
||||
|
||||
### 2.5 Security Architecture
|
||||
|
||||
**Recent Hardening (January 2026):**
|
||||
|
||||
✅ **SEC-001:** API key hashing upgraded from SHA-256 to Argon2id
|
||||
- Before: `hash('sha256', $key)` (vulnerable to rainbow tables)
|
||||
- After: `password_hash($key, PASSWORD_ARGON2ID)` with unique salts
|
||||
- Side effect: `findByKey()` now iterates active keys (no direct lookup)
|
||||
|
||||
✅ **SEC-002:** SQL injection fixed in orderByRaw patterns
|
||||
- Before: `orderByRaw("FIELD(priority, ...)")`
|
||||
- After: Parameterised `orderByPriority()` and `orderByStatus()` scopes
|
||||
|
||||
✅ **SEC-003:** Workspace scoping added to state tools
|
||||
- Added `forWorkspace()` scoping to StateSet, StateGet, StateList, PlanGet, PlanList
|
||||
- Prevents cross-tenant data access
|
||||
|
||||
**Outstanding Security Items:**
|
||||
- ❌ SEC-004: Missing per-tool rate limiting (P1)
|
||||
- ❌ VAL-001: Template variable injection vulnerability (P1)
|
||||
|
||||
**Middleware:**
|
||||
- `AuthenticateAgent` - API key authentication
|
||||
- IP whitelist checking via `IpRestrictionService`
|
||||
- Rate limiting per API key
|
||||
|
||||
---
|
||||
|
||||
## 3. Code Quality Analysis
|
||||
|
||||
### 3.1 Conventions Compliance
|
||||
|
||||
✅ **Excellent:**
|
||||
- UK English throughout (colour, organisation, centre)
|
||||
- `declare(strict_types=1);` in all PHP files
|
||||
- Type hints on all parameters and return types
|
||||
- PSR-12 coding style
|
||||
- Pest framework for testing
|
||||
- Conventional commit messages
|
||||
|
||||
### 3.2 Documentation Quality
|
||||
|
||||
✅ **Very Good:**
|
||||
- Comprehensive CLAUDE.md with clear guidance
|
||||
- Well-maintained TODO.md with priority system (P1-P6)
|
||||
- PHPDoc comments on most methods
|
||||
- README.md with usage examples
|
||||
- AGENTS.md for agent-specific instructions
|
||||
- Changelog tracking (cliff.toml for git-cliff)
|
||||
|
||||
### 3.3 Known Issues (from TODO.md)
|
||||
|
||||
**Priority 1 (Critical):**
|
||||
- [ ] SEC-004: Missing per-tool rate limiting
|
||||
- [ ] VAL-001: Template variable injection vulnerability
|
||||
|
||||
**Priority 2 (High):**
|
||||
- [x] TEST-001 to TEST-005: Test coverage (COMPLETED Jan 2026)
|
||||
- [x] DB-001: Missing agent_plans migration (COMPLETED Jan 2026)
|
||||
- [ ] DB-002: Missing indexes on frequently queried columns
|
||||
- [ ] ERR-001: ClaudeService stream() error handling
|
||||
- [ ] ERR-002: ContentService batch failure recovery
|
||||
|
||||
**Priority 3 (Medium):**
|
||||
- [ ] DX-001: Unclear workspace context error messages
|
||||
- [ ] DX-002: AgenticManager no API key validation on init
|
||||
- [ ] DX-003: Plan template variable errors not actionable
|
||||
- [ ] CQ-001: Duplicate state models
|
||||
- [ ] CQ-002: ApiKeyManager uses wrong model
|
||||
- [ ] CQ-003: Cache key not namespaced
|
||||
- [ ] PERF-001: N+1 queries in checkDependencies
|
||||
- [ ] PERF-002: O(n) filter on every request
|
||||
|
||||
**Lower Priority:** P4-P6 items documented but not critical
|
||||
|
||||
---
|
||||
|
||||
## 4. Migration Status
|
||||
|
||||
### 4.1 Existing Migrations
|
||||
|
||||
```
|
||||
Migrations/
|
||||
├── 0001_01_01_000001_create_agentic_tables.php
|
||||
│ Creates: agent_sessions, agent_api_keys, prompts, prompt_versions, tasks
|
||||
│
|
||||
├── 0001_01_01_000002_add_ip_whitelist_to_agent_api_keys.php
|
||||
│ Adds: ip_whitelist column (JSON)
|
||||
│
|
||||
└── 0001_01_01_000003_create_agent_plans_tables.php
|
||||
Creates: agent_plans, agent_phases, agent_workspace_states
|
||||
Updates: agent_sessions with agent_plan_id FK
|
||||
```
|
||||
|
||||
### 4.2 Idempotency
|
||||
|
||||
**Status:** ✅ Recent fix (commit cda896e)
|
||||
|
||||
From git log:
|
||||
```
|
||||
cda896e fix(migrations): make idempotent and align schemas with models
|
||||
```
|
||||
|
||||
This suggests migrations have been fixed to be safely re-runnable.
|
||||
|
||||
---
|
||||
|
||||
## 5. Testing Strategy (When Dependencies Resolved)
|
||||
|
||||
### 5.1 Recommended Test Execution Order
|
||||
|
||||
Once `host-uk/core` dependency is resolved:
|
||||
|
||||
```bash
|
||||
# 1. Install dependencies
|
||||
composer install --no-interaction
|
||||
|
||||
# 2. Run linter
|
||||
vendor/bin/pint --test
|
||||
# OR: composer run lint
|
||||
|
||||
# 3. Run tests with coverage
|
||||
vendor/bin/pest --coverage
|
||||
# OR: composer test
|
||||
|
||||
# 4. Run static analysis (if configured)
|
||||
vendor/bin/phpstan analyse --memory-limit=512M
|
||||
|
||||
# 5. Check for security issues
|
||||
composer audit
|
||||
```
|
||||
|
||||
### 5.2 Test Gaps to Address
|
||||
|
||||
**High Priority:**
|
||||
1. Console commands (TaskCommand, PlanCommand, GenerateCommand)
|
||||
2. Livewire components (9 admin panel components)
|
||||
3. MCP tools integration tests
|
||||
4. Middleware authentication flow
|
||||
|
||||
**Medium Priority:**
|
||||
1. ContentService batch processing
|
||||
2. Session handoff and resume flows
|
||||
3. Template variable substitution edge cases
|
||||
4. Rate limiting behaviour
|
||||
|
||||
---
|
||||
|
||||
## 6. Key Architectural Patterns
|
||||
|
||||
### 6.1 Design Patterns Identified
|
||||
|
||||
**1. Service Provider Pattern**
|
||||
- Event-driven lazy loading via Boot.php
|
||||
- Modular registration (admin, console, MCP)
|
||||
|
||||
**2. Repository Pattern**
|
||||
- AgentToolRegistry for tool discovery
|
||||
- PlanTemplateService for template management
|
||||
|
||||
**3. Strategy Pattern**
|
||||
- AgenticProviderInterface for AI providers
|
||||
- Different providers interchangeable via AgenticManager
|
||||
|
||||
**4. Circuit Breaker Pattern**
|
||||
- Built into AgentTool base class
|
||||
- Protects against cascading failures
|
||||
|
||||
**5. Factory Pattern**
|
||||
- AgentApiKey::generate() for secure key creation
|
||||
- Template-based plan creation
|
||||
|
||||
### 6.2 SOLID Principles Compliance
|
||||
|
||||
✅ **Single Responsibility:** Each service has clear, focused purpose
|
||||
|
||||
✅ **Open/Closed:** AgentTool extensible via inheritance, closed for modification
|
||||
|
||||
✅ **Liskov Substitution:** All AI providers implement AgenticProviderInterface
|
||||
|
||||
✅ **Interface Segregation:** AgentToolInterface, AgenticProviderInterface are minimal
|
||||
|
||||
✅ **Dependency Inversion:** Services depend on interfaces, not concrete classes
|
||||
|
||||
---
|
||||
|
||||
## 7. Recommendations
|
||||
|
||||
### 7.1 Immediate Actions (Phase 0 Complete)
|
||||
|
||||
1. ✅ Document dependency constraints (this file)
|
||||
2. ✅ Review architecture and patterns (completed)
|
||||
3. ✅ Create FINDINGS.md (this file)
|
||||
4. 🔄 Commit and push to feat/phase-0-assessment
|
||||
5. 🔄 Comment findings on issue #1
|
||||
|
||||
### 7.2 Next Phase Priorities
|
||||
|
||||
**Phase 1: Dependency Resolution**
|
||||
- Add repository configuration for host-uk/core
|
||||
- OR create mock/stub for isolated testing
|
||||
- Verify all migrations run successfully
|
||||
|
||||
**Phase 2: Test Execution**
|
||||
- Run full test suite
|
||||
- Document any test failures
|
||||
- Check code coverage gaps
|
||||
|
||||
**Phase 3: Code Quality**
|
||||
- Address P1 security issues (SEC-004, VAL-001)
|
||||
- Add missing indexes (DB-002)
|
||||
- Fix error handling (ERR-001, ERR-002)
|
||||
|
||||
**Phase 4: Documentation**
|
||||
- Add PHPDoc to undocumented patterns
|
||||
- Document MCP tool dependency system
|
||||
- Create integration examples
|
||||
|
||||
---
|
||||
|
||||
## 8. Conclusions
|
||||
|
||||
### 8.1 Overall Assessment
|
||||
|
||||
**Grade: B+ (Very Good)**
|
||||
|
||||
**Strengths:**
|
||||
- ✅ Modern, event-driven Laravel architecture
|
||||
- ✅ Comprehensive test coverage for critical paths
|
||||
- ✅ Security-conscious with recent hardening
|
||||
- ✅ Well-documented with clear conventions
|
||||
- ✅ Clean abstractions and design patterns
|
||||
- ✅ Excellent TODO.md with prioritised backlog
|
||||
|
||||
**Weaknesses:**
|
||||
- ⚠️ Dependency on private package blocks standalone testing
|
||||
- ⚠️ Some P1 security items outstanding
|
||||
- ⚠️ Performance optimisations needed (N+1 queries, caching)
|
||||
- ⚠️ Test coverage gaps in commands and Livewire
|
||||
|
||||
**Risk Assessment:**
|
||||
- **Security:** MEDIUM (P1 items need attention)
|
||||
- **Maintainability:** LOW (well-structured, documented)
|
||||
- **Performance:** LOW-MEDIUM (known issues documented)
|
||||
- **Testability:** MEDIUM (depends on private package)
|
||||
|
||||
### 8.2 Production Readiness
|
||||
|
||||
**Current State:** BETA / STAGING-READY
|
||||
|
||||
**Blockers for Production:**
|
||||
1. SEC-004: Per-tool rate limiting
|
||||
2. VAL-001: Template injection vulnerability
|
||||
3. ERR-001/002: Error handling in streaming and batch processing
|
||||
4. DB-002: Missing performance indexes
|
||||
|
||||
**Estimate to Production-Ready:** 2-3 sprints
|
||||
|
||||
---
|
||||
|
||||
## 9. Appendix
|
||||
|
||||
### 9.1 File Structure Summary
|
||||
|
||||
```
|
||||
core-agentic/
|
||||
├── Boot.php # Service provider + event listeners
|
||||
├── composer.json # Package definition (blocked on host-uk/core)
|
||||
├── config.php # MCP configuration
|
||||
├── CLAUDE.md # Development guidelines
|
||||
├── TODO.md # Prioritised task backlog (12,632 bytes)
|
||||
├── README.md # Package documentation
|
||||
├── AGENTS.md # Agent-specific instructions
|
||||
│
|
||||
├── Console/Commands/ # 3 Artisan commands
|
||||
│ ├── TaskCommand.php
|
||||
│ ├── PlanCommand.php
|
||||
│ └── GenerateCommand.php
|
||||
│
|
||||
├── Controllers/ # API controllers
|
||||
│ └── ForAgentsController.php
|
||||
│
|
||||
├── Facades/ # Laravel facades
|
||||
│
|
||||
├── Jobs/ # Queue jobs
|
||||
│
|
||||
├── Mcp/ # Model Context Protocol
|
||||
│ ├── Prompts/
|
||||
│ ├── Servers/
|
||||
│ └── Tools/Agent/ # 34 agent tools
|
||||
│
|
||||
├── Middleware/ # Authentication
|
||||
│ └── AuthenticateAgent.php
|
||||
│
|
||||
├── Migrations/ # 3 database migrations
|
||||
│
|
||||
├── Models/ # 9 Eloquent models
|
||||
│
|
||||
├── routes/ # API and admin routes
|
||||
│
|
||||
├── Service/ # Legacy namespace?
|
||||
│
|
||||
├── Services/ # 15 service classes
|
||||
│ ├── AgenticManager.php # AI provider coordinator
|
||||
│ ├── *Service.php # Domain services
|
||||
│ └── Concerns/ # Shared traits
|
||||
│
|
||||
├── Support/ # Helper utilities
|
||||
│
|
||||
├── tests/ # Pest test suite
|
||||
│ ├── Feature/ # 9 feature tests
|
||||
│ ├── Unit/ # 4 unit tests
|
||||
│ ├── UseCase/ # 1 use case test
|
||||
│ ├── Pest.php # Test configuration
|
||||
│ └── TestCase.php # Base test class
|
||||
│
|
||||
└── View/ # UI components
|
||||
├── Blade/admin/ # Admin panel views
|
||||
└── Modal/Admin/ # 9 Livewire components
|
||||
```
|
||||
|
||||
### 9.2 Dependencies (from composer.json)
|
||||
|
||||
**Runtime:**
|
||||
- PHP ^8.2
|
||||
- host-uk/core dev-main (PRIVATE - blocks installation)
|
||||
|
||||
**Development:**
|
||||
- laravel/pint ^1.18 (code formatting)
|
||||
- orchestra/testbench ^9.0|^10.0 (testing)
|
||||
- pestphp/pest ^3.0 (testing)
|
||||
|
||||
**Note:** PHPStan not listed in composer.json despite TODO.md mentioning it
|
||||
|
||||
### 9.3 Git Status
|
||||
|
||||
```
|
||||
Branch: feat/phase-0-assessment (created from main)
|
||||
Status: Clean working directory
|
||||
Recent commits on main:
|
||||
cda896e fix(migrations): make idempotent and align schemas with models
|
||||
c439194 feat(menu): move Agentic to dedicated agents group
|
||||
bf7c0d7 fix(models): add context array cast to AgentPlan
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Assessment Completed:** 2026-02-20
|
||||
**Next Action:** Commit findings and comment on issue #1
|
||||
|
|
@ -10,93 +10,56 @@ return new class extends Migration
|
|||
{
|
||||
/**
|
||||
* Agentic module tables - AI agents, tasks, sessions.
|
||||
*
|
||||
* Guarded with hasTable() so this migration is idempotent and
|
||||
* can coexist with the consolidated app-level migration.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
// 1. Agent API Keys
|
||||
if (! Schema::hasTable('agent_api_keys')) {
|
||||
Schema::create('agent_api_keys', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->uuid('uuid')->unique();
|
||||
$table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete();
|
||||
$table->foreignId('workspace_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('key_hash', 64)->unique();
|
||||
$table->string('key_prefix', 12);
|
||||
$table->json('allowed_agents')->nullable();
|
||||
$table->json('rate_limits')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->string('key');
|
||||
$table->json('permissions')->nullable();
|
||||
$table->unsignedInteger('rate_limit')->nullable();
|
||||
$table->unsignedBigInteger('call_count')->default(0);
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->unsignedBigInteger('usage_count')->default(0);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['workspace_id', 'is_active']);
|
||||
$table->index('key_prefix');
|
||||
});
|
||||
|
||||
// 2. Agent Tasks
|
||||
Schema::create('agent_tasks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->uuid('uuid')->unique();
|
||||
$table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->foreignId('api_key_id')->nullable()->constrained('agent_api_keys')->nullOnDelete();
|
||||
$table->string('agent_type');
|
||||
$table->string('status', 32)->default('pending');
|
||||
$table->text('prompt');
|
||||
$table->json('context')->nullable();
|
||||
$table->json('result')->nullable();
|
||||
$table->json('tool_calls')->nullable();
|
||||
$table->unsignedInteger('input_tokens')->default(0);
|
||||
$table->unsignedInteger('output_tokens')->default(0);
|
||||
$table->decimal('cost', 10, 6)->default(0);
|
||||
$table->unsignedInteger('duration_ms')->nullable();
|
||||
$table->text('error_message')->nullable();
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamp('revoked_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['workspace_id', 'status']);
|
||||
$table->index(['agent_type', 'status']);
|
||||
$table->index('created_at');
|
||||
$table->index('workspace_id');
|
||||
$table->index('key');
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Agent Sessions
|
||||
if (! Schema::hasTable('agent_sessions')) {
|
||||
Schema::create('agent_sessions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->uuid('uuid')->unique();
|
||||
$table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('agent_type');
|
||||
$table->string('status', 32)->default('active');
|
||||
$table->json('context')->nullable();
|
||||
$table->json('memory')->nullable();
|
||||
$table->unsignedInteger('message_count')->default(0);
|
||||
$table->unsignedInteger('total_tokens')->default(0);
|
||||
$table->decimal('total_cost', 10, 6)->default(0);
|
||||
$table->timestamp('last_activity_at')->nullable();
|
||||
$table->foreignId('workspace_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->foreignId('agent_api_key_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('session_id')->unique();
|
||||
$table->string('agent_type')->nullable();
|
||||
$table->string('status')->default('active');
|
||||
$table->json('context_summary')->nullable();
|
||||
$table->json('work_log')->nullable();
|
||||
$table->json('artifacts')->nullable();
|
||||
$table->json('handoff_notes')->nullable();
|
||||
$table->text('final_summary')->nullable();
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('last_active_at')->nullable();
|
||||
$table->timestamp('ended_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['workspace_id', 'status']);
|
||||
$table->index(['agent_type', 'status']);
|
||||
$table->index('last_activity_at');
|
||||
});
|
||||
|
||||
// 4. Agent Messages
|
||||
Schema::create('agent_messages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('session_id')->constrained('agent_sessions')->cascadeOnDelete();
|
||||
$table->string('role', 32);
|
||||
$table->longText('content');
|
||||
$table->json('tool_calls')->nullable();
|
||||
$table->json('tool_results')->nullable();
|
||||
$table->unsignedInteger('tokens')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['session_id', 'created_at']);
|
||||
$table->index('workspace_id');
|
||||
$table->index('status');
|
||||
$table->index('agent_type');
|
||||
});
|
||||
}
|
||||
|
||||
Schema::enableForeignKeyConstraints();
|
||||
}
|
||||
|
|
@ -104,9 +67,7 @@ return new class extends Migration
|
|||
public function down(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
Schema::dropIfExists('agent_messages');
|
||||
Schema::dropIfExists('agent_sessions');
|
||||
Schema::dropIfExists('agent_tasks');
|
||||
Schema::dropIfExists('agent_api_keys');
|
||||
Schema::enableForeignKeyConstraints();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,17 +13,43 @@ return new class extends Migration
|
|||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('agent_api_keys')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('agent_api_keys', function (Blueprint $table) {
|
||||
$table->boolean('ip_restriction_enabled')->default(false)->after('rate_limits');
|
||||
$table->json('ip_whitelist')->nullable()->after('ip_restriction_enabled');
|
||||
$table->string('last_used_ip', 45)->nullable()->after('last_used_at');
|
||||
if (! Schema::hasColumn('agent_api_keys', 'ip_restriction_enabled')) {
|
||||
$table->boolean('ip_restriction_enabled')->default(false);
|
||||
}
|
||||
if (! Schema::hasColumn('agent_api_keys', 'ip_whitelist')) {
|
||||
$table->json('ip_whitelist')->nullable();
|
||||
}
|
||||
if (! Schema::hasColumn('agent_api_keys', 'last_used_ip')) {
|
||||
$table->string('last_used_ip', 45)->nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('agent_api_keys')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('agent_api_keys', function (Blueprint $table) {
|
||||
$table->dropColumn(['ip_restriction_enabled', 'ip_whitelist', 'last_used_ip']);
|
||||
$cols = [];
|
||||
if (Schema::hasColumn('agent_api_keys', 'ip_restriction_enabled')) {
|
||||
$cols[] = 'ip_restriction_enabled';
|
||||
}
|
||||
if (Schema::hasColumn('agent_api_keys', 'ip_whitelist')) {
|
||||
$cols[] = 'ip_whitelist';
|
||||
}
|
||||
if (Schema::hasColumn('agent_api_keys', 'last_used_ip')) {
|
||||
$cols[] = 'last_used_ip';
|
||||
}
|
||||
if ($cols) {
|
||||
$table->dropColumn($cols);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,22 +11,22 @@ return new class extends Migration
|
|||
/**
|
||||
* Create agent plans, phases, and workspace states tables.
|
||||
*
|
||||
* These tables support the structured work plan system that enables
|
||||
* multi-agent handoff and context recovery across sessions.
|
||||
* Guarded with hasTable() so this migration is idempotent and
|
||||
* can coexist with the consolidated app-level migration.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
// 1. Agent Plans - structured work plans with phases
|
||||
if (! Schema::hasTable('agent_plans')) {
|
||||
Schema::create('agent_plans', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete();
|
||||
$table->foreignId('workspace_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('slug')->unique();
|
||||
$table->string('title');
|
||||
$table->text('description')->nullable();
|
||||
$table->longText('context')->nullable();
|
||||
$table->json('phases')->nullable(); // Deprecated: use agent_phases table
|
||||
$table->json('phases')->nullable();
|
||||
$table->string('status', 32)->default('draft');
|
||||
$table->string('current_phase')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
|
|
@ -36,8 +36,9 @@ return new class extends Migration
|
|||
$table->index(['workspace_id', 'status']);
|
||||
$table->index('slug');
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Agent Phases - individual phases within a plan
|
||||
if (! Schema::hasTable('agent_phases')) {
|
||||
Schema::create('agent_phases', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('agent_plan_id')->constrained('agent_plans')->cascadeOnDelete();
|
||||
|
|
@ -56,8 +57,9 @@ return new class extends Migration
|
|||
$table->index(['agent_plan_id', 'order']);
|
||||
$table->index(['agent_plan_id', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Agent Workspace States - shared context between sessions
|
||||
if (! Schema::hasTable('agent_workspace_states')) {
|
||||
Schema::create('agent_workspace_states', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('agent_plan_id')->constrained('agent_plans')->cascadeOnDelete();
|
||||
|
|
@ -70,23 +72,15 @@ return new class extends Migration
|
|||
$table->unique(['agent_plan_id', 'key']);
|
||||
$table->index('key');
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Update agent_sessions to add agent_plan_id foreign key
|
||||
// Note: Only run if agent_sessions table exists (from earlier migration)
|
||||
// Add agent_plan_id to agent_sessions if table exists
|
||||
if (Schema::hasTable('agent_sessions') && ! Schema::hasColumn('agent_sessions', 'agent_plan_id')) {
|
||||
Schema::table('agent_sessions', function (Blueprint $table) {
|
||||
$table->foreignId('agent_plan_id')
|
||||
->nullable()
|
||||
->after('workspace_id')
|
||||
->constrained('agent_plans')
|
||||
->nullOnDelete();
|
||||
$table->json('context_summary')->nullable()->after('context');
|
||||
$table->json('work_log')->nullable()->after('context_summary');
|
||||
$table->json('artifacts')->nullable()->after('work_log');
|
||||
$table->json('handoff_notes')->nullable()->after('artifacts');
|
||||
$table->text('final_summary')->nullable()->after('handoff_notes');
|
||||
$table->timestamp('started_at')->nullable()->after('final_summary');
|
||||
$table->timestamp('ended_at')->nullable()->after('started_at');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -97,20 +91,10 @@ return new class extends Migration
|
|||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
// Remove agent_sessions additions if table exists
|
||||
if (Schema::hasTable('agent_sessions') && Schema::hasColumn('agent_sessions', 'agent_plan_id')) {
|
||||
Schema::table('agent_sessions', function (Blueprint $table) {
|
||||
$table->dropForeign(['agent_plan_id']);
|
||||
$table->dropColumn([
|
||||
'agent_plan_id',
|
||||
'context_summary',
|
||||
'work_log',
|
||||
'artifacts',
|
||||
'handoff_notes',
|
||||
'final_summary',
|
||||
'started_at',
|
||||
'ended_at',
|
||||
]);
|
||||
$table->dropColumn('agent_plan_id');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ class AgentPlan extends Model
|
|||
];
|
||||
|
||||
protected $casts = [
|
||||
'context' => 'array',
|
||||
'phases' => 'array',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -58,14 +58,14 @@ class Boot extends ServiceProvider implements ServiceDefinition
|
|||
/**
|
||||
* Admin menu items for this service.
|
||||
*
|
||||
* Agentic is positioned in the dashboard group (not services)
|
||||
* as it's a cross-cutting AI capability, not a standalone product.
|
||||
* Agentic has its own top-level group right after Dashboard —
|
||||
* this is the primary capability of the platform.
|
||||
*/
|
||||
public function adminMenuItems(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'group' => 'dashboard',
|
||||
'group' => 'agents',
|
||||
'priority' => 5,
|
||||
'entitlement' => 'core.srv.agentic',
|
||||
'item' => fn () => [
|
||||
|
|
|
|||
|
|
@ -28,19 +28,34 @@ class Dashboard extends Component
|
|||
public function stats(): array
|
||||
{
|
||||
return $this->cacheWithLock('admin.agents.dashboard.stats', 60, function () {
|
||||
try {
|
||||
$activePlans = AgentPlan::active()->count();
|
||||
$totalPlans = AgentPlan::notArchived()->count();
|
||||
} catch (\Throwable) {
|
||||
$activePlans = 0;
|
||||
$totalPlans = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
$activeSessions = AgentSession::active()->count();
|
||||
$todaySessions = AgentSession::whereDate('started_at', today())->count();
|
||||
} catch (\Throwable) {
|
||||
$activeSessions = 0;
|
||||
$todaySessions = 0;
|
||||
}
|
||||
|
||||
// Tool call stats for last 7 days
|
||||
try {
|
||||
$toolStats = McpToolCallStat::last7Days()
|
||||
->selectRaw('SUM(call_count) as total_calls')
|
||||
->selectRaw('SUM(success_count) as total_success')
|
||||
->first();
|
||||
|
||||
$totalCalls = $toolStats->total_calls ?? 0;
|
||||
$totalSuccess = $toolStats->total_success ?? 0;
|
||||
} catch (\Throwable) {
|
||||
$totalCalls = 0;
|
||||
$totalSuccess = 0;
|
||||
}
|
||||
|
||||
$successRate = $totalCalls > 0 ? round(($totalSuccess / $totalCalls) * 100, 1) : 0;
|
||||
|
||||
return [
|
||||
|
|
@ -124,7 +139,7 @@ class Dashboard extends Component
|
|||
return $this->cacheWithLock('admin.agents.dashboard.activity', 30, function () {
|
||||
$activities = [];
|
||||
|
||||
// Recent plan updates
|
||||
try {
|
||||
$plans = AgentPlan::with('workspace')
|
||||
->latest('updated_at')
|
||||
->take(5)
|
||||
|
|
@ -141,8 +156,11 @@ class Dashboard extends Component
|
|||
'link' => route('hub.agents.plans.show', $plan->slug),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Table may not exist yet
|
||||
}
|
||||
|
||||
// Recent sessions
|
||||
try {
|
||||
$sessions = AgentSession::with(['plan', 'workspace'])
|
||||
->latest('last_active_at')
|
||||
->take(5)
|
||||
|
|
@ -159,6 +177,9 @@ class Dashboard extends Component
|
|||
'link' => route('hub.agents.sessions.show', $session->id),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Table may not exist yet
|
||||
}
|
||||
|
||||
// Sort by time descending
|
||||
usort($activities, fn ($a, $b) => $b['time'] <=> $a['time']);
|
||||
|
|
@ -171,7 +192,11 @@ class Dashboard extends Component
|
|||
public function topTools(): \Illuminate\Support\Collection
|
||||
{
|
||||
return $this->cacheWithLock('admin.agents.dashboard.toptools', 300, function () {
|
||||
try {
|
||||
return McpToolCallStat::getTopTools(days: 7, limit: 5);
|
||||
} catch (\Throwable) {
|
||||
return collect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +204,11 @@ class Dashboard extends Component
|
|||
public function dailyTrend(): \Illuminate\Support\Collection
|
||||
{
|
||||
return $this->cacheWithLock('admin.agents.dashboard.dailytrend', 300, function () {
|
||||
try {
|
||||
return McpToolCallStat::getDailyTrend(days: 7);
|
||||
} catch (\Throwable) {
|
||||
return collect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -187,11 +216,15 @@ class Dashboard extends Component
|
|||
public function blockedPlans(): int
|
||||
{
|
||||
return (int) $this->cacheWithLock('admin.agents.dashboard.blocked', 60, function () {
|
||||
try {
|
||||
return AgentPlan::active()
|
||||
->whereHas('agentPhases', function ($query) {
|
||||
$query->where('status', 'blocked');
|
||||
})
|
||||
->count();
|
||||
} catch (\Throwable) {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue