netstorage: verify-before-commit orchestration — agentapi verify fields, uid-1000 re-exec probe, detached add job, orphan rows

Controller half of the verify pipeline (SPIKE-nas-verify b57f6c1): AddNetStorage
gains verify/job fields + typed NetAddRefusedError; NetVerifyStatus polls the
agent slot; --netprobe hidden re-exec mode (SysProcAttr.Credential uid/gid 1000,
no shell) proves in-guest writability; the add handler starts a detached
single-flight job (agent_add → verifying → probing → registering LAST) with full
rollback on any failure incl. verify-lost-after-restart (Scenario F); §3.2
Hungarian error map server-side; live-but-unregistered shares surface as remove-
only 'Árva megosztás' rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-11 09:59:33 +02:00
parent 3db9126121
commit bb8737a81f
10 changed files with 962 additions and 46 deletions
+56 -5
View File
@@ -652,7 +652,9 @@ type AddNetStorageRequest struct {
Password string `json:"password,omitempty"` // SMB secret — pass-through, never persisted
}
// NetStorageAddResult mirrors the agent's add response (the in-guest path the media app's data dir points at).
// NetStorageAddResult mirrors the agent's add response (the in-guest path the media app's data dir
// points at). Since agent v0.81.0 (verify-before-commit) a successful add means "units installed,
// verify STARTED" — Verify/JobID carry the detached verify job the caller polls via NetVerifyStatus.
type NetStorageAddResult struct {
Name string `json:"name"`
Protocol string `json:"protocol"`
@@ -660,18 +662,67 @@ type NetStorageAddResult struct {
GuestPath string `json:"guest_path"`
HostUID int `json:"host_uid"`
HostGID int `json:"host_gid"`
Verify string `json:"verify"` // "started" on a verify-before-commit agent (v0.81.0+)
JobID string `json:"job_id"`
Code string `json:"code"` // set on a categorized SYNC refusal (e.g. "unreachable", "busy")
}
// AddNetStorage mounts a NAS share host-side (the agent automounts it; it propagates into this guest via
// the shared bind). Returns the in-guest path the media app's data dir is pointed at.
// NetAddRefusedError is the agent's CATEGORIZED sync refusal of a netstorage add (the 2 s TCP
// pre-probe "unreachable", or "busy" single-flight). Code is the verify-category vocabulary the UI
// maps to Hungarian; nothing was installed agent-side.
type NetAddRefusedError struct {
Code string
Msg string
}
func (e *NetAddRefusedError) Error() string {
return "agentapi: netstorage add refused (" + e.Code + "): " + e.Msg
}
// NetVerifyStatus mirrors the agent's GET /netstorage/verify-status: the single verify slot.
// Phase "none" is a REAL signal — after an agent restart the in-memory job is gone; the caller
// treats none-after-install as verify-lost and rolls the add back (Scenario F).
type NetVerifyStatus struct {
Phase string `json:"phase"` // none | running | done | failed
Name string `json:"name"`
Where string `json:"where"`
Protocol string `json:"protocol"`
Code string `json:"code"` // failure category (the agent's classifier vocabulary)
Detail string `json:"detail"` // operator hint + raw journal fragment
JobID string `json:"job_id"`
}
// AddNetStorage installs a NAS share host-side and starts the agent's detached verify job (agent
// v0.81.0 verify-before-commit). A categorized sync refusal returns *NetAddRefusedError carrying
// the category code (the result body also carries it); other failures are plain errors.
func (c *Client) AddNetStorage(ctx context.Context, req AddNetStorageRequest) (NetStorageAddResult, error) {
var out NetStorageAddResult
body, err := c.post(ctx, "/netstorage/add", req)
env, status, err := c.postWithStatus(ctx, "/netstorage/add", req)
if err != nil {
return out, err
}
if len(env.Data) > 0 {
_ = json.Unmarshal(env.Data, &out) // best-effort; the refusal body carries {code}
}
if rerr := refusalError("/netstorage/add", status, env); rerr != nil {
if out.Code != "" {
return out, &NetAddRefusedError{Code: out.Code, Msg: truncateErr(env.Error, 300)}
}
return out, rerr
}
return out, nil
}
// NetVerifyStatus polls the agent's verify slot (short GET — fits the client's global 15 s timeout;
// the LONG wait lives in the caller's poll loop, never in one HTTP call).
func (c *Client) NetVerifyStatus(ctx context.Context) (NetVerifyStatus, error) {
var out NetVerifyStatus
body, err := c.get(ctx, "/netstorage/verify-status")
if err != nil {
return out, err
}
if err := json.Unmarshal(body, &out); err != nil {
return out, fmt.Errorf("agentapi: decode /netstorage/add: %w", err)
return out, fmt.Errorf("agentapi: decode /netstorage/verify-status: %w", err)
}
return out, nil
}