cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / cowork / cowork.go
cowork.go 3.6K
1// Package cowork is live mode (docs/agents/live.md): the agent working in2// a running 012 session, as one of the participants of the session's3// room (package room). A session listens on a socket only its user can4// reach (Listen); 012 mcp --attach connects to it (Attach) and carries5// the agent's MCP messages to a server running in the session, whose6// tools stand on the room's workbook (Agent, an mcp.Live). What the7// agent reads is the workbook on the screen; what it changes is a8// proposal (sheet.Propose) on the room's Board, marked on the person's9// screen until they accept or reject it, or, when they let agents edit10// directly, a step of the agent's own. The Board also carries the11// agent's questions to the person (Ask) and what the person allows it.12//13// Everything on the Board is the room's, so it is read and changed in14// a turn (room.Seat.Do), as the workbook is.15package cowork1617import (18 "errors"19 "fmt"20 "slices"2122 "src.rest/cat/012/internal/sheet"23)2425// ScopeKind is how much of the workbook the agent may change.26type ScopeKind int2728const (29 // Workbook lets the agent change anything.30 Workbook ScopeKind = iota31 // OneSheet lets it change one sheet's cells, lines and charts.32 OneSheet33 // OneRange lets it change the cells of one range.34 OneRange35 // ReadOnly lets it change nothing.36 ReadOnly37)3839// Scope is what the person invited the agent to change. The agent reads40// the whole workbook whatever the scope, as formulas in it read cells41// outside it; the scope bounds its changes and where it points.42type Scope struct {43 Kind ScopeKind44 Sheet *sheet.Sheet // OneSheet, OneRange45 Range sheet.Rect // OneRange46}4748// String is the scope in words: "the whole workbook", "sheet Q3",49// "Q3!A1:C9", "read only".50func (s Scope) String() string {51 switch s.Kind {52 case OneSheet:53 return "sheet " + s.Sheet.Name()54 case OneRange:55 return sheet.Qualified(s.Sheet.Name(), s.Range)56 case ReadOnly:57 return "read only"58 }59 return "the whole workbook"60}6162// ErrReadOnly refuses any change of the agent invited read only.63var ErrReadOnly = errors.New("you were invited read only: you can read the workbook, point at cells and ask the person, but not change anything; ask them to invite you again with a scope that lets you")6465// Allows reports whether the agent may point at r of sheet s.66func (s Scope) Allows(sh *sheet.Sheet, r sheet.Rect) bool {67 switch s.Kind {68 case OneSheet:69 return sh == s.Sheet70 case OneRange:71 return sh == s.Sheet && s.Range.Contains(r.From) && s.Range.Contains(r.To)72 }73 return true74}7576// Check refuses a proposal that changes anything outside the scope,77// saying what.78func (s Scope) Check(p *sheet.Proposal) error {79 switch s.Kind {80 case ReadOnly:81 return ErrReadOnly82 case Workbook:83 return nil84 }85 name := s.Sheet.Name()86 if p.Book || s.Kind == OneRange && len(p.Other) > 0 || slices.ContainsFunc(p.Sheets, func(n string) bool { return n != name }) {87 return s.outside(p.Other[0])88 }89 for _, c := range p.Cells {90 if c.Sheet != name || s.Kind == OneRange && !s.Range.Contains(c.At) {91 return s.outside("changes " + sheet.Qualified(c.Sheet, sheet.Rect{From: c.At, To: c.At}))92 }93 }94 return nil95}9697func (s Scope) outside(what string) error {98 return fmt.Errorf("the change %s, outside what the person let you change (%s): keep to it, or ask them to invite you with a wider scope", what, s)99}100101// Grants are what the person lets agents do beyond changing cells, for102// the session: run notebook cells with nu, and enter formulas that ask103// a hosted model over the network (JEV) directly, as untrusted macros104// ask before either. Without a grant, the agent's request asks the105// person first.106type Grants struct {107 Notebooks bool108 JEV bool109}