mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 12:33:16 -05:00
A connection now has a write mode, chosen on the add-on page: off (the default), propose (each change waits for the person to confirm) or auto (a chat of the person makes it at once). The host enforces it for every write tool of an add-on that holds a credential, before the add-on is called, and holds the call to the repository list of the connection. Host: - Binding.WriteMode, stored in both stores and in the connector API (write_mode). Absent means off, so existing bindings stay read only. - A credentialed add-on may declare write tools. They name the resource with the credential slot argument, never resourceId. A tool can set approval "always", a ceiling that keeps a proposal even in auto. - The proposal broker fetches the credential for the preview and again for the apply, so it is never stored. A mode lowered between the proposal and the confirmation refuses the apply. A proposal that awaits a person cannot be confirmed by a chat turn. - New capability name tools.write for the roles a connector kind declares. Add-on, tools (kind write): comment-issue, comment-pr, create-issue, edit-issue (title, close or reopen), add-labels, review-comment-pr. They preview plain facts, refuse a stale target, cap and screen the text for secrets, and keep a receipt journal plus a hidden marker in the text, so a change whose answer was lost is not sent twice. Assisted-by: Claude:claude-sonnet-5-5 go gofmt ginkgo node
131 lines
3.1 KiB
Go
131 lines
3.1 KiB
Go
// SPDX-License-Identifier: MIT
|
|
package tools
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/mudler/LocalAI/addons/internal/atomicfile"
|
|
"github.com/mudler/LocalAI/pkg/addons/contract"
|
|
)
|
|
|
|
// maxReceipts bounds the receipts the journal keeps. The host asks for a
|
|
// receipt only for a recent, unsettled operation.
|
|
const maxReceipts = 512
|
|
|
|
// journalFile holds, per operation, the receipt of a finished change and the
|
|
// time an attempt was started. It never holds a token or any text sent to
|
|
// GitHub.
|
|
type journalFile struct {
|
|
Receipts map[string]contract.Receipt `json:"receipts"`
|
|
Started map[string]time.Time `json:"started"`
|
|
Order []string `json:"order"`
|
|
}
|
|
|
|
// journal is one JSON file in the data directory, written through a temporary
|
|
// file and renamed. Without a directory it lives in memory only.
|
|
type journal struct {
|
|
mu sync.Mutex
|
|
path string
|
|
data journalFile
|
|
}
|
|
|
|
func openJournal(dir string) *journal {
|
|
j := &journal{data: journalFile{Receipts: map[string]contract.Receipt{}, Started: map[string]time.Time{}}}
|
|
if dir == "" {
|
|
return j
|
|
}
|
|
j.path = filepath.Join(dir, "journal.json")
|
|
if b, err := os.ReadFile(j.path); err == nil {
|
|
var f journalFile
|
|
if json.Unmarshal(b, &f) == nil {
|
|
if f.Receipts != nil {
|
|
j.data.Receipts = f.Receipts
|
|
}
|
|
if f.Started != nil {
|
|
j.data.Started = f.Started
|
|
}
|
|
j.data.Order = f.Order
|
|
}
|
|
}
|
|
return j
|
|
}
|
|
|
|
func (j *journal) receipt(op string) (contract.Receipt, bool) {
|
|
j.mu.Lock()
|
|
defer j.mu.Unlock()
|
|
r, ok := j.data.Receipts[op]
|
|
return r, ok
|
|
}
|
|
|
|
func (j *journal) started(op string) (time.Time, bool) {
|
|
j.mu.Lock()
|
|
defer j.mu.Unlock()
|
|
t, ok := j.data.Started[op]
|
|
return t, ok
|
|
}
|
|
|
|
// save writes next and, when the write worked, makes it the state. A failed
|
|
// write leaves the memory as it was.
|
|
func (j *journal) save(next journalFile) error {
|
|
if j.path != "" {
|
|
b, err := json.Marshal(next)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := atomicfile.Write(j.path, b, 0o600); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
j.data = next
|
|
return nil
|
|
}
|
|
|
|
func (j *journal) copy() journalFile {
|
|
n := journalFile{Receipts: map[string]contract.Receipt{}, Started: map[string]time.Time{}, Order: append([]string(nil), j.data.Order...)}
|
|
for k, v := range j.data.Receipts {
|
|
n.Receipts[k] = v
|
|
}
|
|
for k, v := range j.data.Started {
|
|
n.Started[k] = v
|
|
}
|
|
return n
|
|
}
|
|
|
|
// begin notes, before the request leaves, that an attempt started. If the
|
|
// note cannot be stored the request is not sent.
|
|
func (j *journal) begin(op string, at time.Time) error {
|
|
j.mu.Lock()
|
|
defer j.mu.Unlock()
|
|
n := j.copy()
|
|
n.Started[op] = at
|
|
return j.save(n)
|
|
}
|
|
|
|
func (j *journal) forget(op string) {
|
|
j.mu.Lock()
|
|
defer j.mu.Unlock()
|
|
n := j.copy()
|
|
delete(n.Started, op)
|
|
_ = j.save(n)
|
|
}
|
|
|
|
func (j *journal) commit(r contract.Receipt) error {
|
|
j.mu.Lock()
|
|
defer j.mu.Unlock()
|
|
n := j.copy()
|
|
delete(n.Started, r.OperationID)
|
|
if _, seen := n.Receipts[r.OperationID]; !seen {
|
|
n.Order = append(n.Order, r.OperationID)
|
|
}
|
|
n.Receipts[r.OperationID] = r
|
|
for len(n.Order) > maxReceipts {
|
|
delete(n.Receipts, n.Order[0])
|
|
n.Order = n.Order[1:]
|
|
}
|
|
return j.save(n)
|
|
}
|