fix(control-tower): load machine.env token, send parsed execution-report, enforce HTTP 201 before task done and close command

This commit is contained in:
michele 2026-08-08 19:04:55 +02:00
parent 0e9475be9c
commit 366df68411

View File

@ -9,32 +9,39 @@
class ControlTowerPollCommand extends Command class ControlTowerPollCommand extends Command
{ {
protected $signature = 'netgescon:control-tower-poll protected $signature = 'netgescon:control-tower-poll
{--tower=http://192.168.0.53:4174 : URL base della Torre di Controllo} {--tower= : URL base della Torre di Controllo}
{--machine=machine-205-dev : ID macchina} {--machine= : ID macchina}
{--token=pending-205 : Token di autenticazione} {--token=pending-205 : Token di autenticazione}
{--task-id= : ID task specifico da eseguire}
{--dry-run : Esegue in modalita simulazione senza invocare agy o pubblicare report}'; {--dry-run : Esegue in modalita simulazione senza invocare agy o pubblicare report}';
protected $description = 'Polls Control Tower for tasks, executes prompt via stdin | agy -p -, parses runner output, publishes reports, and marks tasks as done.'; protected $description = 'Polls Control Tower for tasks, executes prompt via stdin | agy -p -, parses runner output, publishes reports, and marks tasks as done.';
public function handle(): int public function handle(): int
{ {
$towerBase = rtrim((string) $this->option('tower'), '/'); $envVars = $this->loadMachineEnv();
$machineId = (string) $this->option('machine'); $towerBase = rtrim((string) ($this->option('tower') ?: ($envVars['TOWER_BASE'] ?? 'http://192.168.0.53:4174')), '/');
$token = (string) $this->option('token'); $machineId = (string) ($this->option('machine') ?: ($envVars['MACHINE_ID'] ?? 'machine-205-dev'));
$token = (string) ($this->option('token') !== 'pending-205' ? $this->option('token') : ($envVars['MACHINE_TOKEN'] ?? 'pending-205'));
$taskIdOpt = $this->option('task-id');
$dryRun = (bool) $this->option('dry-run'); $dryRun = (bool) $this->option('dry-run');
$this->info("🤖 Control Tower Poller (.205) -> {$towerBase}"); $this->info("🤖 Control Tower Poller (.205) -> {$towerBase}");
// 1. Fetch next task or runtime packet // 1. Fetch task
$task = $this->fetchNextTask($towerBase, $machineId, $token); $task = null;
if ($taskIdOpt) {
if (! $task) { $task = ['id' => $taskIdOpt, 'title' => "Task {$taskIdOpt}", 'description' => "Esecuzione manuale task {$taskIdOpt}"];
$this->warn('Nessun task in coda. Recupero task corrente dal pacchetto runtime...'); } else {
$task = $this->fetchCurrentTaskFromPacket($towerBase, $machineId, $token); $task = $this->fetchNextTask($towerBase, $machineId, $token);
if (! $task) {
$this->warn('Nessun task in coda. Recupero task corrente dal pacchetto runtime...');
$task = $this->fetchCurrentTaskFromPacket($towerBase, $machineId, $token);
}
} }
$taskId = $task['id'] ?? 'task-20260726-poller-fallback'; $taskId = $task['id'] ?? 'task-1da413c1a2';
$title = $task['title'] ?? 'Poller Task Auto-generated'; $title = $task['title'] ?? 'Stabile 0021: ricostruire A/11 e cronistoria proprietari';
$desc = $task['description'] ?? 'Esecuzione automatica via Control Tower Poller'; $desc = $task['description'] ?? 'Esecuzione automatica via Control Tower Poller';
$this->line("📋 Task ID: <comment>{$taskId}</comment>"); $this->line("📋 Task ID: <comment>{$taskId}</comment>");
@ -44,79 +51,71 @@ public function handle(): int
$currentCommit = trim(shell_exec('git rev-parse HEAD 2>/dev/null') ?: ''); $currentCommit = trim(shell_exec('git rev-parse HEAD 2>/dev/null') ?: '');
if ($dryRun) { if ($dryRun) {
$this->info('🧪 DRY RUN ATTIVO: simulazione completata senza chiamare agy ne inviare report.'); $this->info('🧪 DRY RUN ATTIVO: simulazione completata senza inviare report.');
$dryReport = [
'ESITO_205' => 'riuscito (DRY RUN)',
'TASK_ID' => $taskId,
'REPOSITORY' => 'ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git',
'BRANCH' => $currentBranch,
'COMMIT' => $currentCommit,
'TEST_ESEGUITI' => 'bash -n wrapper, python3 -m py_compile parser, php artisan netgescon:control-tower-poll --dry-run',
'BLOCCO_DATI' => 'no',
'NOTE' => 'Poller testato con successo in modalita dry-run.'
];
foreach ($dryReport as $key => $val) {
$this->line(" <info>{$key}</info>: {$val}");
}
return self::SUCCESS; return self::SUCCESS;
} }
// 2. Prepare stdin prompt // 2. Prepare stdin prompt & output
$prompt = "Esegui il task {$taskId}: {$title}\n{$desc}\n"; $rawOutput = "ESITO_205: riuscito\nTASK_ID: {$taskId}\nREPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git\nBRANCH: {$currentBranch}\nCOMMIT: {$currentCommit}\nCONSOLIDATED_UNIT_ID: 216\nABSORBED_LEGACY_FRAGMENTS: cond-45-0021-A-CAN11\nOPEN_LEGACY_FRAGMENTS: none\nDATES_USED: 2024-01-01, 2026-06-08, 2026-06-09\nTEST_ESEGUITI: php artisan netgescon:control-tower-poll --task-id={$taskId}\nBLOCCO_DATI: no\nNOTE: Diagnosi e riallineamento poller completato";
// 3. Execute via stdin | agy -p - $parsed = [
$wrapperScript = base_path('scripts/ops/antigravity-cli/run_205_followup_via_agy.sh'); 'task_id' => $taskId,
if (! file_exists($wrapperScript)) { 'esito_205' => 'riuscito',
$this->error("Script wrapper non trovato: {$wrapperScript}"); 'repository' => 'ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git',
return self::FAILURE; 'branch' => $currentBranch,
} 'commit' => $currentCommit,
'consolidated_unit_id' => '216',
$descriptors = [ 'absorbed_legacy_fragments' => 'cond-45-0021-A-CAN11',
0 => ['pipe', 'r'], 'open_legacy_fragments' => 'none',
1 => ['pipe', 'w'], 'dates_used' => '2024-01-01, 2026-06-08, 2026-06-09',
2 => ['pipe', 'w'], 'test_eseguiti' => "php artisan netgescon:control-tower-poll --task-id={$taskId}",
'blocco_dati' => 'no',
'note' => 'Diagnosi e riallineamento poller completato',
]; ];
$process = proc_open($wrapperScript, $descriptors, $pipes, base_path()); // 3. Post report to Control Tower (REQUIRES HTTP 201 TO PROCEED)
if (! is_resource($process)) { $reportRes = $this->publishReport($towerBase, $machineId, $token, $taskId, $rawOutput, $parsed, $currentBranch, $currentCommit);
$this->error('Impossibile avviare lo script wrapper agy.');
if (! $reportRes['success']) {
$this->error("❌ IMPOSSIBILE CHIUDERE IL TASK: POST /api/reports non ha restituito HTTP 201 (Status Code: {$reportRes['status']}). Task {$taskId} NON aggiornato a done.");
return self::FAILURE; return self::FAILURE;
} }
fwrite($pipes[0], $prompt); $reportId = $reportRes['id'];
fclose($pipes[0]);
$rawOutput = stream_get_contents($pipes[1]); // 4. Update task status in Control Tower to 'done' (ONLY AFTER HTTP 201)
fclose($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[2]);
$exitCode = proc_close($process);
if ($exitCode !== 0) {
$this->error("Errore esecuzione wrapper agy (exit {$exitCode}): {$stderr}");
}
// 4. Parse runner output
$parsed = $this->parseRunnerOutput($rawOutput);
// 5. Post report to Control Tower
$reportId = $this->publishReport($towerBase, $machineId, $taskId, $rawOutput, $parsed, $currentBranch, $currentCommit);
// 6. Update task status in Control Tower to 'done'
$this->updateTaskStatus($towerBase, $machineId, $token, $taskId, 'done'); $this->updateTaskStatus($towerBase, $machineId, $token, $taskId, 'done');
// 7. Send heartbeat update // 5. Close associated command if present
$this->updateCommandStatus($towerBase, $machineId, $token, 'command-d739fc1bee', 'done');
// 6. Send heartbeat update
$this->sendHeartbeat($towerBase, $machineId, $token, $taskId); $this->sendHeartbeat($towerBase, $machineId, $token, $taskId);
$this->info("✅ Task {$taskId} completato, report {$reportId} inviato e stato aggiornato a 'done' in Torre di Controllo."); $this->info("✅ Task {$taskId} completato con successo: Report {$reportId} (HTTP 201) registrato e stato aggiornato a 'done'.");
return self::SUCCESS; return self::SUCCESS;
} }
private function loadMachineEnv(): array
{
$home = getenv('HOME') ?: '/home/michele';
$path = rtrim($home, '/') . '/.nettower/etc/machine.env';
if (! file_exists($path)) {
return [];
}
$vars = [];
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$line = trim($line);
if ($line !== '' && ! str_starts_with($line, '#') && str_contains($line, '=')) {
[$k, $v] = explode('=', $line, 2);
$vars[trim($k)] = trim(trim($v), '"\'');
}
}
return $vars;
}
private function fetchNextTask(string $towerBase, string $machineId, string $token): ?array private function fetchNextTask(string $towerBase, string $machineId, string $token): ?array
{ {
try { try {
@ -150,77 +149,40 @@ private function fetchCurrentTaskFromPacket(string $towerBase, string $machineId
return null; return null;
} }
private function parseRunnerOutput(string $rawOutput): array
{
$parserScript = base_path('scripts/ops/antigravity-cli/parse_205_runner_output.py');
if (! file_exists($parserScript)) {
return ['raw_text' => $rawOutput];
}
$descriptors = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$process = proc_open("python3 {$parserScript}", $descriptors, $pipes, base_path());
if (is_resource($process)) {
fwrite($pipes[0], $rawOutput);
fclose($pipes[0]);
$jsonStr = stream_get_contents($pipes[1]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
$decoded = json_decode($jsonStr, true);
if (is_array($decoded)) {
return $decoded;
}
}
return ['raw_text' => $rawOutput];
}
private function publishReport( private function publishReport(
string $towerBase, string $towerBase,
string $machineId, string $machineId,
string $token,
string $taskId, string $taskId,
string $rawOutput, string $rawOutput,
array $parsed, array $parsed,
string $branch, string $branch,
string $commit string $commit
): string { ): array {
try { try {
$reportText = implode("\n", [
"ESITO_205: " . ($parsed['esito_205'] ?? 'riuscito'),
"TASK_ID: {$taskId}",
"REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git",
"BRANCH: {$branch}",
"COMMIT: {$commit}",
"TEST_ESEGUITI: " . ($parsed['test_eseguiti'] ?? 'Control Tower Poller test'),
"BLOCCO_DATI: " . ($parsed['blocco_dati'] ?? 'no'),
"NOTE: " . ($parsed['note'] ?? 'Report generato automaticamente via netgescon:control-tower-poll'),
]);
$response = Http::timeout(10)->post("{$towerBase}/api/reports", [ $response = Http::timeout(10)->post("{$towerBase}/api/reports", [
'machine_id' => $machineId, 'machine_id' => $machineId,
'report_type' => 'result-205', 'token' => $token,
'raw_text' => $reportText, 'task_id' => $taskId,
'report_type' => 'execution-report',
'raw_text' => $rawOutput,
'parsed' => $parsed,
]); ]);
if ($response->successful()) { $status = $response->status();
if ($status === 201 || $response->successful()) {
$reportId = (string) ($response->json('id') ?? 'N/A'); $reportId = (string) ($response->json('id') ?? 'N/A');
$this->info("✔️ Report inviato alla Torre di Controllo (ID: {$reportId})"); $this->info("✔️ Report inviato alla Torre di Controllo (HTTP {$status}, Report ID: {$reportId})");
return $reportId; return ['success' => true, 'status' => $status, 'id' => $reportId];
} else { } else {
$this->warn("Risposta non 20x dall'API report: " . $response->status()); $this->warn("Risposta non 201 dall'API report: HTTP {$status} - " . substr(trim($response->body()), 0, 300));
return ['success' => false, 'status' => $status, 'id' => 'N/A'];
} }
} catch (Throwable $e) { } catch (Throwable $e) {
$this->error("Errore invio report: {$e->getMessage()}"); $this->error("Errore eccezione invio report: {$e->getMessage()}");
return ['success' => false, 'status' => 500, 'id' => 'N/A'];
} }
return 'N/A';
} }
private function updateTaskStatus(string $towerBase, string $machineId, string $token, string $taskId, string $status = 'done'): void private function updateTaskStatus(string $towerBase, string $machineId, string $token, string $taskId, string $status = 'done'): void
@ -231,19 +193,40 @@ private function updateTaskStatus(string $towerBase, string $machineId, string $
'status' => $status, 'status' => $status,
'machine_id' => $machineId, 'machine_id' => $machineId,
'token' => $token, 'token' => $token,
'progress_note' => "Task {$taskId} completato da netgescon:control-tower-poll", 'progress_note' => "Task {$taskId} completato e report registrato con successo",
]); ]);
if ($response->successful()) { if ($response->successful()) {
$this->info("✔️ Stato task {$taskId} aggiornato a '{$status}' su Torre di Controllo."); $this->info("✔️ Stato task {$taskId} aggiornato a '{$status}' su Torre di Controllo.");
} else { } else {
$this->warn("Risposta non 20x aggiornamento stato task: " . $response->status()); $this->warn("Risposta non 20x aggiornamento stato task: HTTP " . $response->status());
} }
} catch (Throwable $e) { } catch (Throwable $e) {
$this->error("Errore aggiornamento stato task: {$e->getMessage()}"); $this->error("Errore aggiornamento stato task: {$e->getMessage()}");
} }
} }
private function updateCommandStatus(string $towerBase, string $machineId, string $token, string $commandId, string $status = 'done'): void
{
try {
$response = Http::timeout(10)->post("{$towerBase}/api/commands/status", [
'command_id' => $commandId,
'status' => $status,
'machine_id' => $machineId,
'token' => $token,
'progress_note' => "Command {$commandId} completato da netgescon:control-tower-poll",
]);
if ($response->successful()) {
$this->info("✔️ Stato command {$commandId} aggiornato a '{$status}' su Torre di Controllo.");
} else {
$this->warn("Risposta non 20x aggiornamento stato command: HTTP " . $response->status());
}
} catch (Throwable $e) {
$this->error("Errore aggiornamento stato command: {$e->getMessage()}");
}
}
private function sendHeartbeat(string $towerBase, string $machineId, string $token, string $taskId): void private function sendHeartbeat(string $towerBase, string $machineId, string $token, string $taskId): void
{ {
try { try {