aboutsummaryrefslogtreecommitdiff
path: root/api/debug
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-23 14:39:38 +0200
committerAgando <roduccomm@gmail.com>2026-10-23 14:39:38 +0200
commitcfe5b7a842890c232358ae191652c5e5e81591e1 (patch)
treeba27d718d1fd1c8df48b9e91eea071949aa1946f /api/debug
parentf6a48a90e374e79b5ca8c0f73d2eb9b3d95868ec (diff)
feat(newos4): maintenance agent channel — triage engine, two-way replies, cron
- scripts/newos4-triage.py: deterministic collector (records vs occurrences, real vs noise vs self-referential, new reports since last run, checker WARN/FAIL). Untrusted strings are sanitised and prefixed DATA| before they can reach an agent prompt; state + output written under flock with mode 644. - scripts/newos4-reply.py: appends one reply to data/maintenance/replies.json under flock. Hermes writes here instead of POSTing with an admin cookie, so an expired session can never look like a delivered reply. - api/debug/agent-replies.php: GET/POST (admin-gated by nginx) half of the thread. - api/debug/report.php: accepts thread + kind=maintenance so in-app feedback is distinguishable from operator bug reports. - terminal: `feedback <message>` and `feedback-inbox` commands; help.html rows added (help coverage 357/357, modules-index regenerated). Live proof: `feedback "…"` filed RPT-20261023-123825-781fba86 with thread thr-glmfwae2 and kind=maintenance; agent-replies GET returned the posted reply, POST returned 201; triage first run reported 9 new reports, second run 0. Cron: newos4-maintenance (7e3b326daa4c) daily 07:15, newos4-skills-weekly (cb6bc2a16216) Mondays 06:30.
Diffstat (limited to 'api/debug')
-rw-r--r--api/debug/agent-replies.php57
-rw-r--r--api/debug/report.php9
2 files changed, 66 insertions, 0 deletions
diff --git a/api/debug/agent-replies.php b/api/debug/agent-replies.php
new file mode 100644
index 0000000..070c0e6
--- /dev/null
+++ b/api/debug/agent-replies.php
@@ -0,0 +1,57 @@
+<?php
+// api/debug/agent-replies.php — outbound half of the maintenance thread.
+// Hermes (or the operator) writes replies here; the in-page agent reads them with
+// `feedback-inbox`. nginx gates this path behind the WebX11 admin session, so it is
+// owner-only: never expose it in the public debug/course location block.
+require_once __DIR__ . '/config.php';
+
+define('REPLIES_DIR', __DIR__ . '/../../data/maintenance');
+define('REPLIES_FILE', REPLIES_DIR . '/replies.json');
+
+if (!is_dir(REPLIES_DIR)) {
+ @mkdir(REPLIES_DIR, 0775, true);
+ @chgrp(REPLIES_DIR, 'www-data');
+}
+
+$method = $_SERVER['REQUEST_METHOD'];
+
+if ($method === 'GET') {
+ $since = $_GET['since'] ?? null;
+ $all = load_json(REPLIES_FILE);
+ if (!is_array($all)) $all = [];
+ if ($since) {
+ $all = array_values(array_filter($all, fn($r) => ($r['created_at'] ?? '') > $since));
+ }
+ json_response(['success' => true, 'replies' => array_slice($all, -100)]);
+}
+
+if ($method !== 'POST') {
+ error_response('Method not allowed', 405);
+}
+
+$raw = file_get_contents('php://input');
+if (!$raw) error_response('Empty request body');
+if (strlen($raw) > 16384) error_response('Payload too large (max 16KB)', 413);
+
+$p = json_decode($raw, true);
+if (!is_array($p)) error_response('Invalid JSON');
+
+$body = trim((string)($p['body'] ?? ''));
+if ($body === '') error_response('body is required');
+if (mb_strlen($body) > 2000) $body = mb_substr($body, 0, 2000);
+
+$list = load_json(REPLIES_FILE);
+if (!is_array($list)) $list = [];
+
+$author = (($p['author'] ?? 'hermes') === 'newos4') ? 'newos4' : 'hermes';
+$entry = [
+ 'id' => 'REP-' . gmdate('Ymd-His') . '-' . substr(bin2hex(random_bytes(3)), 0, 6),
+ 'created_at' => gmdate('c'),
+ 'author' => $author,
+ 'body' => $body,
+];
+
+$list[] = $entry;
+save_json(REPLIES_FILE, array_slice($list, -200));
+
+json_response(['success' => true, 'id' => $entry['id']], 201);
diff --git a/api/debug/report.php b/api/debug/report.php
index cefcae6..5d4fc95 100644
--- a/api/debug/report.php
+++ b/api/debug/report.php
@@ -29,6 +29,13 @@ $body = (string)($payload['body'] ?? '');
if (mb_strlen($body) > 4000) $body = mb_substr($body, 0, 4000);
$diag = (isset($payload['diagnostics']) && is_array($payload['diagnostics'])) ? $payload['diagnostics'] : [];
+// Maintenance thread: lets the in-app agent and the Hermes maintenance job hold a conversation.
+// `thread` is a client-generated id (thr-xxxxxxxx); `kind` distinguishes agent feedback from
+// operator bug reports so triage can weight them differently.
+$thread = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)($payload['thread'] ?? ''));
+if (mb_strlen($thread) > 40) $thread = substr($thread, 0, 40);
+$kind = (($payload['kind'] ?? '') === 'maintenance') ? 'maintenance' : 'user';
+
// Storage guard: data/ is not writable by php-fpm, so never pretend we wrote a report.
if (!is_dir(REPORTS_DIR)) @mkdir(REPORTS_DIR, 0775, true);
if (!is_dir(REPORTS_DIR) || !is_writable(REPORTS_DIR)) {
@@ -85,6 +92,8 @@ $record = [
'title' => $title,
'body' => $body,
'diagnostics' => $diag,
+ 'thread' => $thread,
+ 'kind' => $kind,
'meta' => [
'ip' => $ip,
'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',