Skip to content
quarry Artifact [0acabdf653]
Login

Artifact [0acabdf653]

Artifact 0acabdf65375d68aedd83c1835db03c12848f3a7810504d1ff3f4139776dffa8:


package api

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"reflect"
	"strings"
	"time"

	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
)

// An agent never owns a repository (RFC 0021, amended 2026-10-21): it
// serves the person who answers for it, and may do everything they can
// except the deed's own acts. Those it may ASK for. An agent that asks
// to delete, un-serve or hand on a repository its owner holds gets a
// request back instead of a refusal; the owner approves or denies it,
// and approving runs exactly the request, as the owner's own act.

// deedOps are the deed acts: a request for one goes to the repository's
// deed holder, and approval checks the deed is still theirs.
var deedOps = map[string]bool{"repo.delete": true, "repo.remove": true, "repo.claim": true}

// notAskable is what an agent may never ask its person for (RFC 0027,
// What may be asked): deciding requests and changing who answers for
// an agent. The person's own credentials, sessions and account are
// askableFor's other half.
var notAskable = map[string]bool{
	"signoff.decide": true, "signoff.list": true, "signoff.get": true,
	"account.agent": true, "account.answerable": true,
	planOp: true,
}

// askableFor reports whether an agent may ask person for op with this
// input: anything the person may do (checked when it runs, as them),
// except the exclusions. Every me.* op is the person's own account,
// credentials, sessions or devices; an account op aimed at the person
// themselves is too.
func askableFor(op, person string, in any) bool {
	if notAskable[op] || strings.HasPrefix(op, "me.") {
		return false
	}
	switch op {
	case "account.delete", "account.disable", "account.reset", "account.revoke", "account.email.set":
		return nameOf(in) != person
	}
	return true
}

// nameOf is an input's Name field, the account an account op names.
func nameOf(in any) string {
	v := reflect.ValueOf(in)
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
	}
	if v.Kind() != reflect.Struct {
		return ""
	}
	if f := v.FieldByName("Name"); f.IsValid() && f.Kind() == reflect.String {
		return f.String()
	}
	return ""
}

// SignOffRequest is one request as the agent and the owner see it.
type SignOffRequest struct {
	ID        string `json:"id"`
	Agent     string `json:"agent"`
	Owner     string `json:"owner"`
	Op        string `json:"op"`
	Repo      string `json:"repo"`
	Code      string `json:"-"`
	Input     string `json:"-"`
	Summary   string `json:"summary"`
	State     string `json:"state"`
	Requested string `json:"requested"`
	Decided   string `json:"decided,omitempty"`
	Err       string `json:"err,omitempty"`
	// Reason is the agent's own line on why it asks (RFC 0027).
	Reason string `json:"reason,omitempty"`
	// Result is what the approved call answered, as JSON, without the
	// fields the op marks secret; Secret is those fields, for the
	// asking agent once. Neither is part of the request as listed:
	// signoff.get hands them out.
	Result string `json:"-"`
	Secret string `json:"-"`
	// Pickup is the public code of the pickup key the asker bound to
	// this request (RFC 0030): the hex SHA-256 of 32 random bytes only
	// the asker's program holds. Collected is when the secret was taken.
	// A bound request's secret leaves only through the pickup route.
	Pickup    string `json:"pickup,omitempty"`
	Collected string `json:"collected,omitempty"`
}

// SignOffStore keeps the requests. RequestSignOff returns the open one
// when the same request is already waiting; DecideSignOff closes a
// pending request exactly once, and errors on any other.
type SignOffStore interface {
	RequestSignOff(SignOffRequest) (SignOffRequest, error)
	SignOffsFor(owner string, limit int) ([]SignOffRequest, error)
	SignOffByID(id string) (SignOffRequest, bool, error)
	DecideSignOff(id, owner string, approve bool) (SignOffRequest, error)
	FailSignOff(id, why string) error
	FinishSignOff(id, result, secret string) error
	TakeSignOffSecret(id, agent string, now time.Time) (string, error)
	// SignOffByPickup is the request bound to a pickup code, as stored;
	// TakePickupSecret collects by code in the same single take
	// TakeSignOffSecret uses, and says why when it cannot (RFC 0030).
	SignOffByPickup(code string) (SignOffRequest, bool, error)
	// ParkSignOff records a call that already ran as an approved request
	// in one transaction, sealing its secret with seal(id) (RFC 0030).
	ParkSignOff(s SignOffRequest, result, refs string, seal func(id string) (string, error)) (SignOffRequest, error)
	TakePickupSecret(code string, now time.Time) (PickupTake, error)
	// SetSignOffRefs records the credentials an approved request minted;
	// DueSignOffLapses lists the requests a sweep has work on, changing
	// nothing; ClaimSignOffLapse takes one (no collect can succeed on it
	// after) and answers what to revoke, ok false when it was collected
	// first; ClearSignOffLapse finishes it once that is revoked.
	SetSignOffRefs(id, refs string) error
	DueSignOffLapses(before time.Time) ([]SignOffLapse, error)
	ClaimSignOffLapse(id string, cutoff time.Time) (refs string, ok bool, err error)
	ClearSignOffLapse(id string) error
}

// PickupTake is what one attempt to collect by pickup code found: the
// request when any holds the code, its secret set only when Taken, and
// whether an uncollected secret is past its limit.
type PickupTake struct {
	Found   bool
	Taken   bool
	Request SignOffRequest
	Lapsed  bool
}

// ErrPickupBound is a request asked with a pickup code another request
// already holds.
var ErrPickupBound = errors.New("pickup code already bound")

// ErrPickupMismatch is the same request asked again, while it waits,
// with a different pickup code than it holds, or with one where it holds
// none.
var ErrPickupMismatch = errors.New("pickup code differs from the waiting request's")

// pickupAskError is how a refused pickup binding reads to the asker. One
// sentence for a code in use by anybody's request, so a caller learns of
// nobody else's.
func pickupAskError(err error) error {
	switch {
	case errors.Is(err, ErrPickupBound):
		return fmt.Errorf("that pickup code cannot be used; make a new key and ask again: %w", ops.ErrInvalid)
	case errors.Is(err, ErrPickupMismatch):
		return fmt.Errorf("this exact request is already waiting, with its own pickup code or none, and that stands; a request's code never changes, so deny it or let it expire before asking again with another: %w", ops.ErrInvalid)
	}
	return err
}

// SignOffLapse is one uncollected secret the sweep cleared.
type SignOffLapse struct {
	ID    string
	Agent string
	Repo  string
	Refs  string
}

// signOffOwner is the owner an agent would ask about repo: the deed
// holder, when they are the person answering for the caller (its
// operator; for a forge agent, its answerable admin). "" when the
// caller is not an agent, or the repository is not its owner's.
func (d Deps) signOffOwner(ctx context.Context, repo string) string {
	if d.SignOffs == nil || d.Owners == nil {
		return ""
	}
	who := ops.User(ctx)
	acct, ok := d.accountNamed(who)
	if !ok || acct.Operator == "" || acct.Disabled != "" {
		return ""
	}
	answers := acct.Operator
	if answers == forgeOperator {
		answers = acct.Answerable
	}
	owner, err := d.Owners.OwnerOf(repo)
	if err != nil || owner == "" || owner != answers {
		return ""
	}
	return owner
}

// askOwner turns an agent's deed act on its owner's repository into a
// request for their sign-off. asked is false for every other caller,
// who goes on to the act's ordinary gate.
func (d Deps) askOwner(ctx context.Context, op, repo string, in any, summary string) (req *SignOffRequest, asked bool, err error) {
	owner := d.signOffOwner(ctx, repo)
	if owner == "" {
		return nil, false, nil
	}
	r, err := d.repo(repo)
	if err != nil {
		return nil, true, err
	}
	raw, err := json.Marshal(in)
	if err != nil {
		return nil, true, err
	}
	agent := ops.User(ctx)
	s, err := d.SignOffs.RequestSignOff(SignOffRequest{Agent: agent, Owner: owner, Op: op, Repo: repo,
		Code: r.Code, Input: string(raw), Summary: summary})
	if err != nil {
		return nil, true, err
	}
	d.attend(owner, AttentionEvent{Kind: "signoff", Repo: repo, From: agent, Summary: summary, Link: "/me/requests/" + s.ID})
	// And to the owner wherever they are (dmestas: "I probably need an
	// email or sms for sign offs"): the request waits on them, and the
	// attention stream only reaches an open tab.
	d.fireSignOff(owner, repo, agent, summary, s.ID)
	return &s, true, nil
}

type signOffListIn struct{}

type signOffListOut struct {
	Requests []SignOffRequest `json:"requests"`
	// Plans is, by request id, the steps of the requests that are plans.
	Plans map[string][]PlanStepView `json:"plans,omitempty"`
}

type signOffDecideIn struct {
	ID      string `json:"id" validate:"required,max=64"`
	Approve bool   `json:"approve"`
}

type signOffDecideOut struct {
	Request SignOffRequest `json:"request"`
}

type signOffGetIn struct {
	ID string `json:"id" validate:"required,max=64"`
}

type signOffGetOut struct {
	Request SignOffRequest `json:"request"`
	// Result is what the approved call answered, without its secret
	// fields; Secret is those fields, given to the asking agent on its
	// first read after approval and never again.
	Result json.RawMessage `json:"result,omitempty"`
	Secret json.RawMessage `json:"secret,omitempty"`
	// Steps is what a plan will do, or did, step by step (RFC 0028).
	Steps []PlanStepView `json:"steps,omitempty"`
	// Note says what the secret's absence means when it lapsed.
	Note string `json:"note,omitempty"`
	// Setup is what an agent-setup request will do, or did, line by line
	// (RFC 0030); nil for any other request.
	Setup *SetupView `json:"setup,omitempty"`
}

// lapsedBy is whether a request's uncollected secret is past its limit,
// whether or not the sweep has cleared it yet.
func lapsedBy(s SignOffRequest, now time.Time) bool {
	at, err := time.Parse(time.RFC3339, s.Decided)
	return err == nil && at.Before(oltp.SignOffSecretCutoff(now).Truncate(time.Second))
}

// registerSignOff is the owner's half: the requests waiting for them,
// and the decision; and the agent's: reading what became of one.
func registerSignOff(r *ops.Registry, d Deps) error {
	if err := ops.Register(r, ops.Op[signOffGetIn, signOffGetOut]{
		Name: "signoff.get",
		Doc: "One request for sign-off, as the agent that asked or the person it asked sees it: its state and, once approved, " +
			"what the call answered. Fields the call marks secret (a minted token) go to the asking agent alone, on its first read, and are then forgotten. " +
			"An asking agent need not poll: when the request leaves pending, its own `me.attention` stream carries one `signoff.decided` event, its summary the op and the new state (approved, denied or failed) and its link this call's path. " +
			"No other account, and no repository's event stream, is told, and the event never carries a credential or a pickup code; read the result here.",
		Kind:  ops.Query,
		Auth:  ops.Token,
		Route: "GET /api/v1/signoffs/{id}",
		Run: func(ctx context.Context, in signOffGetIn) (signOffGetOut, error) {
			if d.SignOffs == nil {
				return signOffGetOut{}, fmt.Errorf("this forge keeps no sign-offs: %w", ops.ErrUnavailable)
			}
			who := ops.User(ctx)
			s, found, err := d.SignOffs.SignOffByID(in.ID)
			if err != nil {
				return signOffGetOut{}, err
			}
			if !found || (who != s.Agent && who != s.Owner) {
				return signOffGetOut{}, fmt.Errorf("no sign-off request %q: %w", in.ID, ops.ErrNotFound)
			}
			out := signOffGetOut{Request: s, Steps: planSteps(s), Setup: d.setupViewOf(s, who)}
			if s.Result != "" {
				out.Result = json.RawMessage(s.Result)
			}
			switch {
			case s.Secret == oltp.SignOffSecretLapsed || (s.Secret != "" && lapsedBy(s, time.Now())):
				out.Note = "the credential this request minted lapsed uncollected and was revoked; ask again"
			case s.Pickup != "":
				// A request bound to a pickup code never releases its
				// secret here, to the asker or anyone (RFC 0030,
				// decision 13): its one way out is the pickup route.
			case who == s.Agent && s.Secret != "":
				sec, err := d.SignOffs.TakeSignOffSecret(s.ID, who, time.Now())
				if err != nil {
					return signOffGetOut{}, err
				}
				if sec == "" {
					// Refused at the line the sweep uses: it lapsed
					// between the read and the take.
					if again, ok, _ := d.SignOffs.SignOffByID(s.ID); ok && again.Secret != "" {
						out.Note = "the credential this request minted lapsed uncollected and was revoked; ask again"
					}
				} else {
					if sec, err = d.openSignOffSecret(s.ID, sec); err != nil {
						return signOffGetOut{}, err
					}
					out.Secret = json.RawMessage(sec)
				}
			}
			return out, nil
		},
	}); err != nil {
		return err
	}
	if err := d.registerPlan(r); err != nil {
		return err
	}
	if err := ops.Register(r, ops.Op[signOffListIn, signOffListOut]{
		Name:  "signoff.list",
		Doc:   "The requests your agents have made for your sign-off: anything you may do that they may not (RFC 0027), deed acts on your repositories among them. Waiting ones first, then the most recently decided; a request waits seven days.",
		Kind:  ops.Query,
		Auth:  ops.Token,
		Route: "GET /api/v1/signoffs",
		Run: func(ctx context.Context, in signOffListIn) (signOffListOut, error) {
			if d.SignOffs == nil {
				return signOffListOut{Requests: []SignOffRequest{}}, nil
			}
			reqs, err := d.SignOffs.SignOffsFor(ops.User(ctx), 50)
			if err != nil {
				return signOffListOut{}, err
			}
			if reqs == nil {
				reqs = []SignOffRequest{}
			}
			var plans map[string][]PlanStepView
			for _, s := range reqs {
				if steps := planSteps(s); steps != nil {
					if plans == nil {
						plans = map[string][]PlanStepView{}
					}
					plans[s.ID] = steps
				}
			}
			return signOffListOut{Requests: reqs, Plans: plans}, nil
		},
	}); err != nil {
		return err
	}
	return ops.Register(r, ops.Op[signOffDecideIn, signOffDecideOut]{
		Name: "signoff.decide",
		Doc: "Approve or deny a request waiting for your sign-off. Approving runs exactly what the agent asked, as your own act, " +
			"after checking that the repository, its owner and the agent are what they were when it asked. " +
			"An agent cannot decide, and a token limited to other repositories cannot either. " +
			"Deciding tells the asking agent, once, with a `signoff.decided` event on its own `me.attention` stream.",
		Kind:  ops.Command,
		Auth:  ops.Token,
		Route: "POST /api/v1/signoffs/decide",
		Run: func(ctx context.Context, in signOffDecideIn) (signOffDecideOut, error) {
			if d.SignOffs == nil {
				return signOffDecideOut{}, fmt.Errorf("this forge keeps no sign-offs: %w", ops.ErrUnavailable)
			}
			who := ops.User(ctx)
			if op := d.operatorOf(who); op != "" {
				return signOffDecideOut{}, fmt.Errorf("%s is an agent; a sign-off is its owner's: %w", who, ops.ErrForbidden)
			}
			s, found, err := d.SignOffs.SignOffByID(in.ID)
			if err != nil {
				return signOffDecideOut{}, err
			}
			if !found || s.Owner != who {
				return signOffDecideOut{}, fmt.Errorf("no sign-off request %q: %w", in.ID, ops.ErrNotFound)
			}
			if tok, byToken := ops.TokenOf(ctx); byToken && (tok.Limited || !tok.MayAt(s.Repo, ops.Admin)) {
				return signOffDecideOut{}, fmt.Errorf("this token does not reach %s; decide from a session or a token that does: %w", s.Repo, ops.ErrForbidden)
			}
			if in.Approve {
				// Checked before the request is closed, so a request that
				// can no longer run says why and stays answerable.
				if err := d.signOffStillHolds(s); err != nil {
					return signOffDecideOut{}, err
				}
			}
			s, err = d.SignOffs.DecideSignOff(in.ID, who, in.Approve)
			if err != nil {
				return signOffDecideOut{}, fmt.Errorf("%v: %w", err, ops.ErrInvalid)
			}
			if !in.Approve {
				d.tellAsker(s)
				return signOffDecideOut{Request: s}, nil
			}
			if err := d.runSignedOff(ctx, r, s); err != nil {
				if ferr := d.SignOffs.FailSignOff(s.ID, err.Error()); ferr != nil {
					log.Printf("signoff %s: recording failure: %v", s.ID, ferr)
				}
				s.State, s.Err = "failed", err.Error()
				d.tellAsker(s)
				return signOffDecideOut{Request: s}, err
			}
			if d.Acts != nil && s.Op != planOp {
				if err := d.Acts.RecordForgeAct(s.Agent+" (signed off by "+s.Owner+")", s.Op, s.Repo, s.Owner); err != nil {
					logForgeActFailure(s.Op, s.Repo, err)
				}
			}
			d.tellAsker(s)
			return signOffDecideOut{Request: s}, nil
		},
	})
}

// signOffStillHolds checks, at approval, what the request was made
// about: the same repository (by project code, so a name deleted and
// made again is not it), still the approver's, and an agent that still
// exists, is open, and still answers to them.
func (d Deps) signOffStillHolds(s SignOffRequest) error {
	stale := func(why string) error {
		return fmt.Errorf("this request can no longer run: %s: %w", why, ops.ErrInvalid)
	}
	if s.Repo != "" {
		r, err := d.repo(s.Repo)
		if err != nil || r.Code != s.Code {
			return stale(s.Repo + " is not the repository it was asked about")
		}
	}
	if deedOps[s.Op] {
		if owner, err := d.Owners.OwnerOf(s.Repo); err != nil || owner != s.Owner {
			return stale(s.Repo + " is no longer yours")
		}
	}
	if isClientAsker(s.Agent) {
		// A claimed start has no account behind it: its asker is the
		// client, vouched for by the person who claimed its code.
		if s.Agent != clientAsker(s.Owner) {
			return stale("its asker is not the client you claimed")
		}
		return nil
	}
	acct, ok := d.accountNamed(s.Agent)
	if !ok || acct.Disabled != "" {
		return stale(s.Agent + " is gone or shut")
	}
	answers := acct.Operator
	if answers == forgeOperator {
		answers = acct.Answerable
	}
	if answers != s.Owner {
		return stale(s.Agent + " no longer answers to you")
	}
	return nil
}

// runSignedOff runs the recorded request through the op itself, as the
// owner: the agent's token may be long revoked, and under the owner
// every deed gate the op has passes or refuses honestly.
func (d Deps) runSignedOff(ctx context.Context, r *ops.Registry, s SignOffRequest) error {
	if s.Op == planOp {
		return d.runPlan(ctx, r, s)
	}
	desc, ok := r.Lookup(s.Op)
	if !ok {
		return fmt.Errorf("no operation %q: %w", s.Op, ops.ErrInvalid)
	}
	in := desc.NewIn()
	dec := json.NewDecoder(strings.NewReader(s.Input))
	dec.DisallowUnknownFields()
	if err := dec.Decode(in); err != nil {
		return fmt.Errorf("the request's input no longer reads: %v: %w", err, ops.ErrInvalid)
	}
	if s.Repo != "" && repoOf(in) != s.Repo {
		return fmt.Errorf("the request names another repository: %w", ops.ErrInvalid)
	}
	arg := reflect.ValueOf(in).Elem().Interface()
	if !askableFor(s.Op, s.Owner, arg) {
		return fmt.Errorf("%s is not something an agent may ask for: %w", s.Op, ops.ErrInvalid)
	}
	out, err := r.Invoke(withSignOffAsk(ctx, s), s.Op, arg)
	if err != nil {
		if s.Op == setupOp {
			// The store rolled back whole: nothing was made.
			d.recordSetupOutcome(s, setupOutcome{Outcome: setupUndone})
		}
		return err
	}
	result, secret, err := splitSecret(out)
	if err == nil {
		err = d.holdSignOffSecret(ctx, s, result, secret, lapseRefOf(s.Op, s.Repo, arg, out))
	}
	if err != nil {
		// A setup that cannot be held is taken back whole, the agent it
		// made included. Nothing outward has gone out yet, so that is all
		// there is to take back; what became of it is recorded on the
		// request, so its page says only what is true.
		if o, ok := out.(agentSetupOut); ok {
			if rerr := d.retractSetup(out); rerr != nil {
				log.Printf("signoff %s: ERROR: %s could not be held (%v) and could not be taken back: %v; agent %s and token %s remain", s.ID, s.Op, err, rerr, o.Agent, o.Token.ID)
				d.recordSetupOutcome(s, setupOutcome{Outcome: setupLeft, Left: []string{"the agent " + o.Agent, "its token " + o.Token.ID}})
				return fmt.Errorf("%v; and what it made could not be taken back automatically, tell an administrator: %w", err, ops.ErrUnavailable)
			}
			d.recordSetupOutcome(s, setupOutcome{Outcome: setupUndone})
		}
		return err
	}
	if o, ok := out.(agentSetupOut); ok && o.after != nil {
		o.after()
	}
	return nil
}

// recordSetupOutcome writes what became of a setup that did not
// complete into the request's result, while the request is still
// approved; the decision then closes it as failed.
func (d Deps) recordSetupOutcome(s SignOffRequest, oc setupOutcome) {
	raw, _ := json.Marshal(oc)
	if err := d.SignOffs.FinishSignOff(s.ID, string(raw), ""); err != nil {
		log.Printf("signoff %s: recording what became of the setup: %v", s.ID, err)
	}
}

// splitSecret renders an op's answer as two JSON objects: the answer
// without the fields tagged secret:"once", and those fields alone (""
// when there are none). The secret half goes to the asking agent once
// and never to a page, an email or the journal (RFC 0027, Results).
func splitSecret(out any) (result, secret string, err error) {
	raw, err := json.Marshal(out)
	if err != nil {
		return "", "", err
	}
	v := reflect.ValueOf(out)
	for v.Kind() == reflect.Ptr {
		v = v.Elem()
	}
	if v.Kind() != reflect.Struct {
		return string(raw), "", nil
	}
	var fields map[string]json.RawMessage
	if err := json.Unmarshal(raw, &fields); err != nil {
		return string(raw), "", nil
	}
	hidden := map[string]json.RawMessage{}
	t := v.Type()
	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		if f.Tag.Get("secret") != "once" {
			continue
		}
		key, _, _ := strings.Cut(f.Tag.Get("json"), ",")
		if key == "" {
			key = f.Name
		}
		if val, ok := fields[key]; ok {
			hidden[key] = val
			delete(fields, key)
		}
	}
	if len(hidden) == 0 {
		return string(raw), "", nil
	}
	pub, err := json.Marshal(fields)
	if err != nil {
		return "", "", err
	}
	sec, err := json.Marshal(hidden)
	if err != nil {
		return "", "", err
	}
	return string(pub), string(sec), nil
}