瀏覽代碼

r2: saddle-status + session-outcome constants, one bearer resolution (arch 26) — SADDLE_IDLE/BUSY/OFFLINE and OUTCOME_STOPPED/RESUMED/DONE/FAILED beside SaddleService replace the magic strings across SaddleService (register/list/park/free/offline writes incl. the guarded UPDATE SQL), TaskService (unreachable check, every release outcome), AllocationService (the claim's idle/busy/resumed), Dispatcher (the affine-wake idle check), SaddleApi (the offline->idle ask recovery, the legacy done outcome) and the four entrypoints' offline/idle guards; the {id}-anchored and id-less auth styles collapse into resolve_bearer (validate_token pinning the route's saddle vs saddle_for_token owning-whatever-registered) so every /api/saddle/* endpoint resolves its credential through one helper. No OUTCOME_PARKED: no web-side writer exists (D4's web half is unlanded) — an unused constant would be dead code; it lands with D4. The wire-state switch labels (daemon results states) and display labels stay literals by kind.

clanker 1 月之前
父節點
當前提交
a58d9c0738

+ 35 - 23
src/Api/SaddleApi.vala

@@ -44,7 +44,7 @@ namespace ClankerSaddle.Api {
             route_context.mapped_parameters.try_get("id", out id_text);
             int64 saddle_id = int64.parse(id_text);
 
-            if (!(yield saddles.validate_token(saddle_id, bearer_token(http_context)))) {
+            if ((yield resolve_bearer(http_context, saddles, saddle_id)) == null) {
                 return json_error(401, "Missing or invalid saddle token");
             }
 
@@ -129,7 +129,7 @@ namespace ClankerSaddle.Api {
                 case "done":
                     yield tasks.complete_task(task_id,
                         EntryJson.get_string_or(data, "summary", null),
-                        EntryJson.get_string_or(data, "working_branch", null), "done");
+                        EntryJson.get_string_or(data, "working_branch", null), OUTCOME_DONE);
                     break;
 
                 case "failed":
@@ -263,7 +263,7 @@ namespace ClankerSaddle.Api {
             route_context.mapped_parameters.try_get("id", out id_text);
             int64 saddle_id = int64.parse(id_text);
 
-            if (!(yield saddles.validate_token(saddle_id, bearer_token(http_context)))) {
+            if ((yield resolve_bearer(http_context, saddles, saddle_id)) == null) {
                 return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
             }
 
@@ -320,7 +320,7 @@ namespace ClankerSaddle.Api {
 
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
-            if (!(yield validate_saddle_bearer(http_context, saddles))) {
+            if ((yield resolve_bearer(http_context, saddles)) == null) {
                 return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
             }
 
@@ -379,7 +379,7 @@ namespace ClankerSaddle.Api {
 
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
-            if (!(yield validate_saddle_bearer(http_context, saddles))) {
+            if ((yield resolve_bearer(http_context, saddles)) == null) {
                 return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
             }
 
@@ -436,7 +436,7 @@ namespace ClankerSaddle.Api {
 
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
-            if (!(yield validate_saddle_bearer(http_context, saddles))) {
+            if ((yield resolve_bearer(http_context, saddles)) == null) {
                 return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
             }
 
@@ -492,7 +492,7 @@ namespace ClankerSaddle.Api {
 
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
-            if (!(yield validate_saddle_bearer(http_context, saddles))) {
+            if ((yield resolve_bearer(http_context, saddles)) == null) {
                 return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
             }
 
@@ -781,7 +781,7 @@ namespace ClankerSaddle.Api {
 
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
-            var saddle = yield saddles.saddle_for_token(bearer_token(http_context));
+            var saddle = yield resolve_bearer(http_context, saddles);
             if (saddle == null) {
                 return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
             }
@@ -804,7 +804,7 @@ namespace ClankerSaddle.Api {
 
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
-            var saddle = yield saddles.saddle_for_token(bearer_token(http_context));
+            var saddle = yield resolve_bearer(http_context, saddles);
             if (saddle == null) {
                 return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
             }
@@ -827,7 +827,7 @@ namespace ClankerSaddle.Api {
 
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
-            var saddle = yield saddles.saddle_for_token(bearer_token(http_context));
+            var saddle = yield resolve_bearer(http_context, saddles);
             if (saddle == null) {
                 return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
             }
@@ -864,7 +864,7 @@ namespace ClankerSaddle.Api {
 
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
-            var saddle = yield saddles.saddle_for_token(bearer_token(http_context));
+            var saddle = yield resolve_bearer(http_context, saddles);
             if (saddle == null) {
                 return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
             }
@@ -873,8 +873,8 @@ namespace ClankerSaddle.Api {
             // recovered (its 15-minute safety self-ask, or it came back
             // after the probe/guard marked it offline) and no recovery
             // pass exists to flip the row — the ask itself does.
-            if (((!)saddle).status == "offline") {
-                ((!)saddle).status = "idle";
+            if (((!)saddle).status == SADDLE_OFFLINE) {
+                ((!)saddle).status = SADDLE_IDLE;
                 yield db.update_async<Saddle>(((!)saddle));
                 yield pulse.bump(0, "saddle asked (offline -> idle)");
                 print(@"[ClankerSaddle] Saddle '$(((!)saddle).name)' asked while offline; marking idle\n");
@@ -933,7 +933,7 @@ namespace ClankerSaddle.Api {
 
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
-            var saddle = yield saddles.saddle_for_token(bearer_token(http_context));
+            var saddle = yield resolve_bearer(http_context, saddles);
             if (saddle == null) {
                 return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
             }
@@ -1022,7 +1022,7 @@ namespace ClankerSaddle.Api {
 
         public virtual async HttpResult handle_request(HttpContext http_context,
                 RouteContext route_context) throws Error {
-            var saddle = yield saddles.saddle_for_token(bearer_token(http_context));
+            var saddle = yield resolve_bearer(http_context, saddles);
             if (saddle == null) {
                 return SaddleUpdateEndpoint.json_error(401, "Missing or invalid saddle token");
             }
@@ -1084,14 +1084,26 @@ namespace ClankerSaddle.Api {
     }
 
     /**
-     * The per-saddle credential check shared by the id-less
-     * `/api/saddle/*` endpoints (plan 10): the bearer token must belong
-     * to a registered saddle (its row token, or a legacy api_tokens row
-     * mapped to one) — removing the saddle revokes it.
+     * The ONE bearer resolution for the `/api/saddle/*` wire (plan 16
+     * R2, arch item 26): the `Authorization: Bearer` credential
+     * resolved to its saddle — null fails the call. The two auth
+     * styles live here together: the {id}-anchored routes (update,
+     * poll) pin the credential to their route's saddle
+     * ({@link SaddleService.validate_token} — the token must be THAT
+     * saddle's); the id-less routes accept whichever registered
+     * saddle owns the token ({@link SaddleService.saddle_for_token}
+     * — the row token first, then any legacy api_tokens row; removing
+     * the saddle revokes both).
      */
-    private static async bool validate_saddle_bearer(HttpContext http_context,
-            SaddleService saddles) throws Error {
-        var saddle = yield saddles.saddle_for_token(bearer_token(http_context));
-        return saddle != null;
+    private static async Saddle? resolve_bearer(HttpContext http_context,
+            SaddleService saddles, int64 anchored_saddle_id = 0) throws Error {
+        string token = bearer_token(http_context);
+        if (anchored_saddle_id > 0) {
+            if (yield saddles.validate_token(anchored_saddle_id, token)) {
+                return yield saddles.get_saddle(anchored_saddle_id);
+            }
+            return null;
+        }
+        return yield saddles.saddle_for_token(token);
     }
 }

+ 3 - 3
src/Services/AllocationService.vala

@@ -215,7 +215,7 @@ namespace ClankerSaddle.Services {
                 var row = db.query<Saddle>()
                     .where(expr("id == $0", elem<int64?>(saddle.id)))
                     .first();
-                if (row == null || (((!)row).status != "idle"
+                if (row == null || (((!)row).status != SADDLE_IDLE
                         && (!(candidate.parked && ((!)row).current_task_id == candidate.task.id)))) {
                     connection.execute("ROLLBACK");
                     return null;
@@ -246,7 +246,7 @@ namespace ClankerSaddle.Services {
                     // every telemetry push of the resumed round on the
                     // new row.
                     ((!)newest_open).ended_at = new DateTime.now_local();
-                    ((!)newest_open).outcome = "resumed";
+                    ((!)newest_open).outcome = OUTCOME_RESUMED;
                     db.update<Session>((!)newest_open);
                     var resumed = new Session() {
                         task_id = candidate.task.id,
@@ -270,7 +270,7 @@ namespace ClankerSaddle.Services {
                 ((!)task).started_at = new DateTime.now_local();
                 db.update<TaskEntity>((!)task);
 
-                ((!)row).status = "busy";
+                ((!)row).status = SADDLE_BUSY;
                 ((!)row).current_task_id = candidate.task.id;
                 ((!)row).allocated_at = new DateTime.now_local();
                 db.update<Saddle>((!)row);

+ 1 - 1
src/Services/Dispatcher.vala

@@ -202,7 +202,7 @@ namespace ClankerSaddle.Services {
                 if (offerable.parked) {
                     var affine = offerable.affine_saddle_id > 0
                         ? yield saddles.get_saddle(offerable.affine_saddle_id) : null;
-                    if (affine != null && ((!)affine).status == "idle"
+                    if (affine != null && ((!)affine).status == SADDLE_IDLE
                             && poll_state.has_ever_asked(((!)affine).id)
                             && !poll_state.wake_pending(((!)affine).id, lost_wake_guard_ms())) {
                         target = (!)affine;

+ 1 - 1
src/Services/GoalService.vala

@@ -614,7 +614,7 @@ namespace ClankerSaddle.Services {
                 // Approving the completion closes the goal agent's
                 // own task: the run is finished, its saddle frees.
                 yield tasks.complete_task(((!)task).id,
-                    ((!)goal).completion_report, null, "done");
+                    ((!)goal).completion_report, null, OUTCOME_DONE);
                 push.notify_global(PushService.goal_terminal_notification(
                     ((!)goal).title, ((!)goal).id, true));
             }

+ 37 - 13
src/Services/SaddleService.vala

@@ -9,6 +9,28 @@ using ClankerSaddle.Data;
 
 namespace ClankerSaddle.Services {
 
+    /**
+     * Valid saddle statuses (the `saddles.status` wire values, plan 16
+     * R2 / arch item 26 — the raw literals died with these). The row
+     * default and the migration seed keep their literals (the Data
+     * layer does not reference the services).
+     */
+    public const string SADDLE_IDLE = "idle";
+    public const string SADDLE_BUSY = "busy";
+    public const string SADDLE_OFFLINE = "offline";
+
+    /**
+     * Session outcomes (the `sessions.outcome` values the ONE release
+     * path and the allocation claim write): `stopped` (a
+     * cancelled/requeued/unreachable release), `resumed` (a parked
+     * workspace handed to its successor session), `done`/`failed`
+     * (the terminal pushes).
+     */
+    public const string OUTCOME_STOPPED = "stopped";
+    public const string OUTCOME_RESUMED = "resumed";
+    public const string OUTCOME_DONE = "done";
+    public const string OUTCOME_FAILED = "failed";
+
     /** Caps of the `project_notes` array in the /start payload. */
     public const int64 PROJECT_NOTES_MAX_ENTRIES = 30;
     public const int64 PROJECT_NOTES_MAX_CHARS = 20000;
@@ -131,7 +153,7 @@ namespace ClankerSaddle.Services {
             var saddle = new Saddle() {
                 name = name,
                 host = host,
-                status = "idle",
+                status = SADDLE_IDLE,
                 token = mint_saddle_token()
             };
             yield db.insert_async<Saddle>(saddle);
@@ -306,7 +328,7 @@ namespace ClankerSaddle.Services {
                 return;
             }
             if (((!)saddle).current_task_id > 0) {
-                yield task_service.release_task(((!)saddle).current_task_id, "stopped", true);
+                yield task_service.release_task(((!)saddle).current_task_id, OUTCOME_STOPPED, true);
             }
             foreach (var row in yield db.query<ApiToken>()
                     .where(expr("saddle_id == $0", elem<int64?>(id)))
@@ -369,7 +391,7 @@ namespace ClankerSaddle.Services {
          */
         public async ImmutableLot<Saddle> list_busy_saddles() throws Error {
             return yield db.query<Saddle>()
-                .where(expr("status == $0", elem("busy")))
+                .where(expr("status == $0", elem(SADDLE_BUSY)))
                 .materialise_async();
         }
 
@@ -585,7 +607,7 @@ namespace ClankerSaddle.Services {
                         continue;
                     }
                     var saddle = yield get_saddle(((!)session).saddle_id);
-                    if (saddle == null || ((!)saddle).status != "offline") {
+                    if (saddle == null || ((!)saddle).status != SADDLE_OFFLINE) {
                         continue;
                     }
                     print(@"[ClankerSaddle] Task $(task.id) requeued: its saddle went offline\n");
@@ -607,7 +629,8 @@ namespace ClankerSaddle.Services {
          */
         public async void park_if_current(int64 saddle_id, int64 task_id) throws Error {
             var command = connection.create_command(
-                "UPDATE saddles SET status = 'idle', current_task_id = 0, allocated_at = NULL"
+                "UPDATE saddles SET status = '" + SADDLE_IDLE
+                + "', current_task_id = 0, allocated_at = NULL"
                 + " WHERE id = :id AND current_task_id = :task");
             command.with_parameter<Invercargill.Element>("id", elem<int64?>(saddle_id));
             command.with_parameter<Invercargill.Element>("task", elem<int64?>(task_id));
@@ -621,7 +644,7 @@ namespace ClankerSaddle.Services {
          */
         public async ImmutableLot<Saddle> list_idle_saddles() throws Error {
             return yield db.query<Saddle>()
-                .where(expr("status == $0", elem("idle")))
+                .where(expr("status == $0", elem(SADDLE_IDLE)))
                 .order_by(expr("id"))
                 .materialise_async();
         }
@@ -675,7 +698,8 @@ namespace ClankerSaddle.Services {
                 release_held = !keep_parked;
             }
             var offline_write = connection.create_command(
-                "UPDATE saddles SET status = 'offline', current_task_id = 0, allocated_at = NULL"
+                "UPDATE saddles SET status = '" + SADDLE_OFFLINE
+                + "', current_task_id = 0, allocated_at = NULL"
                 + " WHERE id = :id AND current_task_id = :seen");
             offline_write.with_parameter<Invercargill.Element>("id", elem<int64?>(id));
             offline_write.with_parameter<Invercargill.Element>("seen",
@@ -692,7 +716,7 @@ namespace ClankerSaddle.Services {
                 // close applies whatever the task's state was —
                 // flipped-and-requeued above, parked-suspended left
                 // alone, or a row that no longer exists.
-                yield tasks.release_task(held_task, "stopped", false);
+                yield tasks.release_task(held_task, OUTCOME_STOPPED, false);
             }
             yield pulse.bump(0, "saddle unreachable");
         }
@@ -1063,7 +1087,7 @@ namespace ClankerSaddle.Services {
                     @"No saddle exists with id $(id)");
             }
             int64 seen_task = ((!)saddle).current_task_id;
-            string next_status = "idle";
+            string next_status = SADDLE_IDLE;
             int64 next_task = 0;
             DateTime? next_allocated = null;
             try {
@@ -1074,7 +1098,7 @@ namespace ClankerSaddle.Services {
                     // Reconcile with the web's own records: busy only when
                     // a live session truly holds this saddle.
                     var session = yield open_session_on_saddle(id);
-                    next_status = "busy";
+                    next_status = SADDLE_BUSY;
                     next_task = session != null ? ((!)session).task_id : 0;
                     next_allocated = session != null ? ((!)session).started_at : null;
                 }
@@ -1084,7 +1108,7 @@ namespace ClankerSaddle.Services {
                 // busy here used to wedge assignment against a daemon
                 // that would never poll.
             } catch (Error e) {
-                next_status = "offline";
+                next_status = SADDLE_OFFLINE;
             }
             var command = connection.create_command(
                 "UPDATE saddles SET status = :status, current_task_id = :task,"
@@ -1361,7 +1385,7 @@ namespace ClankerSaddle.Services {
                             string report = output.length > 0 ? output
                                 : strip_quotes(strip_verb(command, "saddle done"));
                             yield tasks.complete_task(task_id,
-                                report.length > 0 ? report : null, null, "done");
+                                report.length > 0 ? report : null, null, OUTCOME_DONE);
                         }
                         return true;
                     case "failed":
@@ -1647,7 +1671,7 @@ namespace ClankerSaddle.Services {
                         yield park_if_current(saddle.id, task_id);
                         break;
                     }
-                    yield tasks.complete_task(task_id, summary, null, "done");
+                    yield tasks.complete_task(task_id, summary, null, OUTCOME_DONE);
                     break;
                 case "failed":
                     string explanation = summary ?? "";

+ 7 - 6
src/Services/TaskService.vala

@@ -1058,7 +1058,7 @@ namespace ClankerSaddle.Services {
                     var saddle = yield db.query<Saddle>()
                         .where(expr("id == $0", elem<int64?>(((!)session).saddle_id)))
                         .first_async();
-                    if (saddle != null && ((!)saddle).status == "offline") {
+                    if (saddle != null && ((!)saddle).status == SADDLE_OFFLINE) {
                         ids.add(task.id.to_string());
                     }
                 }
@@ -1195,7 +1195,7 @@ namespace ClankerSaddle.Services {
 
             yield mark_terminal((!)task, STATUS_FAILED, null, null, "Cancelled by user");
 
-            yield release_task(task_id, "stopped", false);
+            yield release_task(task_id, OUTCOME_STOPPED, false);
             yield pulse.bump(task_id, "task cancelled");
         }
 
@@ -1227,7 +1227,7 @@ namespace ClankerSaddle.Services {
             yield mark_terminal((!)task, STATUS_FAILED, summary, null,
                 "Saddle reported a failure");
 
-            yield release_task(task_id, "failed", false);
+            yield release_task(task_id, OUTCOME_FAILED, false);
             yield pulse.bump(task_id, "task failed");
             // Web Push (plan 11 task E): an agent-reported failure
             // notifies the opted-in users (a human cancellation does
@@ -1369,7 +1369,7 @@ namespace ClankerSaddle.Services {
          * starts a fresh parking window.
          */
         public async void requeue_task(int64 task_id) throws Error {
-            yield release_task(task_id, "stopped", true);
+            yield release_task(task_id, OUTCOME_STOPPED, true);
             yield pulse.bump(task_id, "task requeued");
         }
 
@@ -1399,7 +1399,7 @@ namespace ClankerSaddle.Services {
             ((!)task).completed_at = null;
             yield db.update_async<TaskEntity>((!)task);
 
-            yield release_task(task_id, "stopped", false);
+            yield release_task(task_id, OUTCOME_STOPPED, false);
             yield add_system_comment(task_id, "Task retried by user");
             yield pulse.bump(task_id, "task retried");
         }
@@ -1578,7 +1578,8 @@ namespace ClankerSaddle.Services {
                 yield db.update_async<Session>(session);
             }
             var command = connection.create_command(
-                "UPDATE saddles SET status = 'idle', current_task_id = 0, allocated_at = NULL"
+                "UPDATE saddles SET status = '" + SADDLE_IDLE
+                + "', current_task_id = 0, allocated_at = NULL"
                 + " WHERE current_task_id = :task");
             command.with_parameter<Invercargill.Element>("task", elem<int64?>(task_id));
             yield command.execute_non_query_async();

+ 1 - 1
src/entrypoints/GoalDetailEntrypoint.vala

@@ -584,7 +584,7 @@ namespace ClankerSaddle {
                                 .where(expr("id == $0",
                                     elem<int64?>(((!)open_run).saddle_id)))
                                 .first_async();
-                            if (host != null && ((!)host).status == "offline") {
+                            if (host != null && ((!)host).status == SADDLE_OFFLINE) {
                                 pub.requeue_run = ((!)actions)
                                     .author_private<RequeueGoalRunAction, GoalActionPrivate>(
                                         new GoalActionPrivate() { goal_id = goal.id });

+ 2 - 2
src/entrypoints/HomeEntrypoint.vala

@@ -13,7 +13,7 @@ namespace ClankerSaddle {
     public class DashboardSaddleView : Object {
         public int64 id { get; set; }
         public string name { get; set; default = ""; }
-        public string status { get; set; default = "offline"; }
+        public string status { get; set; default = SADDLE_OFFLINE; }
         public string host { get; set; default = ""; }
         public int64 current_task_id { get; set; }
         public string current_task_title { get; set; default = ""; }
@@ -167,7 +167,7 @@ namespace ClankerSaddle {
 
             var saddle_rows = new Vector<DashboardSaddleView>();
             foreach (var saddle in yield saddles.list_saddles()) {
-                if (saddle.status == "idle") {
+                if (saddle.status == SADDLE_IDLE) {
                     pub.idle_saddle_count++;
                 }
                 var view = new DashboardSaddleView() {

+ 1 - 1
src/entrypoints/SaddlesEntrypoint.vala

@@ -143,7 +143,7 @@ namespace ClankerSaddle {
                 poll_state.health_for(saddle.id, out daemon_state, out uptime,
                     out activity, out contact_ms);
                 string health = "unknown";
-                if (saddle.status == "offline") {
+                if (saddle.status == SADDLE_OFFLINE) {
                     health = "offline";
                 } else if (daemon_state.length > 0) {
                     health = "healthy";

+ 1 - 1
src/entrypoints/TaskDetailEntrypoint.vala

@@ -479,7 +479,7 @@ namespace ClankerSaddle {
                 // path (the automatic three-strike abandon only fires
                 // while the dispatcher still polls the saddle).
                 if (session.ended_at == null && saddle != null
-                        && ((!)saddle).status == "offline"
+                        && ((!)saddle).status == SADDLE_OFFLINE
                         && (task.status == STATUS_WORKING || task.status == STATUS_BLOCKED
                             || task.status == STATUS_REVIEW_REQUESTED)) {
                     pub.resume_available = true;