hub v0.128.0: set-aside deletion through the hub after a 7-day wait (decision 74, R-823), key-file clean-up route (R-826), read-only key check (R-827), window cap = half
gates / gates (push) Successful in 29s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-04 07:05:21 +02:00
parent 697c2a7b10
commit d3c50b50f6
15 changed files with 613 additions and 20 deletions
+6
View File
@@ -366,6 +366,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handleOffsiteWindowOpen(w, r, strings.TrimPrefix(path, "/offsite/window-open/"))
case r.Method == http.MethodPost && strings.HasPrefix(path, "/offsite/window-close/"):
h.handleOffsiteWindowClose(w, r, strings.TrimPrefix(path, "/offsite/window-close/"))
case r.Method == http.MethodPost && strings.HasPrefix(path, "/offsite/abandon-request/"):
h.handleOffsiteAbandon(w, r, "request", strings.TrimPrefix(path, "/offsite/abandon-request/"))
case r.Method == http.MethodPost && strings.HasPrefix(path, "/offsite/abandon-cancel/"):
h.handleOffsiteAbandon(w, r, "cancel", strings.TrimPrefix(path, "/offsite/abandon-cancel/"))
case r.Method == http.MethodPost && strings.HasPrefix(path, "/offsite/abandon-status/"):
h.handleOffsiteAbandon(w, r, "status", strings.TrimPrefix(path, "/offsite/abandon-status/"))
case r.Method == http.MethodGet && strings.HasPrefix(path, "/artifacts/"):
customerID := strings.TrimPrefix(path, "/artifacts/")
h.handleArtifactManifest(w, r, customerID)
+47
View File
@@ -18,6 +18,9 @@ type OffsiteKeyService interface {
MoveAside(ctx context.Context, customerID string) (string, error)
OpenWindowFor(ctx context.Context, customerID string, countBefore int) (offsitekeys.WindowGrant, error)
CloseWindowFor(ctx context.Context, customerID string, r offsitekeys.WindowResult) error
RequestAbandon(ctx context.Context, customerID, path string) (offsitekeys.AbandonStatus, error)
CancelAbandon(customerID, by string) (int, error)
AbandonStatusFor(customerID string) (offsitekeys.AbandonStatus, error)
}
// SetOffsiteKeyService wires the key registrar.
@@ -166,3 +169,47 @@ func (h *Handler) handleOffsiteWindowClose(w http.ResponseWriter, r *http.Reques
}
writeJSON(w, http.StatusOK, map[string]any{"closed": true})
}
// handleOffsiteAbandon: the box's half of decision 74 (R-823).
//
// POST /offsite/abandon-request/<id> {"path": "<repo>.orphaned-…"} → recorded; deleted by the hub after the delay
// POST /offsite/abandon-cancel/<id> → every pending request cancelled
// POST /offsite/abandon-status/<id> → {"state": none|pending|cancelled|deleted, …}
func (h *Handler) handleOffsiteAbandon(w http.ResponseWriter, r *http.Request, verb, customerID string) {
if !h.offsiteKeyAuth(w, r, customerID) {
return
}
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
defer cancel()
switch verb {
case "request":
var req struct {
Path string `json:"path"`
}
body, _ := io.ReadAll(io.LimitReader(r.Body, 4<<10))
if err := json.Unmarshal(body, &req); err != nil || req.Path == "" {
http.Error(w, "body must be {\"path\": \"…\"}", http.StatusBadRequest)
return
}
st, err := h.offsiteKeys.RequestAbandon(ctx, customerID, req.Path)
if err != nil {
offsiteKeyErr(w, err)
return
}
writeJSON(w, http.StatusOK, st)
case "cancel":
n, err := h.offsiteKeys.CancelAbandon(customerID, "box")
if err != nil {
offsiteKeyErr(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"cancelled": n})
default:
st, err := h.offsiteKeys.AbandonStatusFor(customerID)
if err != nil {
offsiteKeyErr(w, err)
return
}
writeJSON(w, http.StatusOK, st)
}
}
+10
View File
@@ -69,6 +69,13 @@ func (f *fakeKeySvc) OpenWindowFor(context.Context, string, int) (offsitekeys.Wi
return offsitekeys.WindowGrant{Granted: false, Reason: "not due"}, f.err
}
func (f *fakeKeySvc) CloseWindowFor(context.Context, string, offsitekeys.WindowResult) error { return f.err }
func (f *fakeKeySvc) RequestAbandon(context.Context, string, string) (offsitekeys.AbandonStatus, error) {
return offsitekeys.AbandonStatus{State: "pending"}, f.err
}
func (f *fakeKeySvc) CancelAbandon(string, string) (int, error) { return 1, f.err }
func (f *fakeKeySvc) AbandonStatusFor(string) (offsitekeys.AbandonStatus, error) {
return offsitekeys.AbandonStatus{State: "none"}, f.err
}
func (f *fakeKeySvc) MoveAside(context.Context, string) (string, error) {
return "/home/felhom-repo.orphaned-20261003", f.err
}
@@ -108,6 +115,9 @@ func TestOffsiteKeyEndpoints_AuthAndNoPasswordInAnyResponse(t *testing.T) {
{"/api/v1/offsite/move-aside/c1", ``},
{"/api/v1/offsite/window-open/c1", `{"count_before":3}`},
{"/api/v1/offsite/window-close/c1", `{"window_id":1,"count_after":3,"outcome":"nothing"}`},
{"/api/v1/offsite/abandon-request/c1", `{"path":"/home/felhom-repo.orphaned-20261004"}`},
{"/api/v1/offsite/abandon-cancel/c1", ``},
{"/api/v1/offsite/abandon-status/c1", ``},
} {
rr := post(c.path, "ckey", c.body)
if rr.Code != http.StatusOK {