GIT13: the forge module keeps no trusted callers; who may call a verb is the seat definition's
The config value callers, the package seats' list of callers with its watch, and the seat git's caller judgement are gone: the grants part grants by each definition's callers and the bus refuses the rest. Builds: design 47 G §68, §127, §313, §321 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -197,7 +197,7 @@ the module's own object store `incoming`, its transfers in the state `transfers`
|
||||
side), `versions` and `remove` (a tagged version, by the builder). Before anything is uploaded the holder asks the
|
||||
builder whether it numbered the version with that digest; a package goes up as npm's publish sends it, a Go module's
|
||||
zip to the Go upload. Every outcome is said on the seat's events (`published`, `refused`, `removed`), a Go package's
|
||||
with its module path. Who may call each verb is the trusted config value `callers` (empty: design 51's list).
|
||||
with its module path. Who may call each verb is the seat definition's `callers`, which the grants part grants by; the holder keeps no list.
|
||||
|
||||
## The package registry's retention (novox/hq ADR 0251 §4, to-be 51)
|
||||
|
||||
|
||||
@@ -488,7 +488,7 @@ func TestToolsAreTheOnesTheReadmeNamesAndSayWhatTheyDo(t *testing.T) {
|
||||
t.Errorf("transfers has options (a lifetime): %s", req)
|
||||
}
|
||||
// The retention asks the builder's versions; the forge's other bundles ask the catalog half and record a re-read.
|
||||
if strings.Join(m.Invokes, ",") != "seat:mesh-catalog.revision,seat:mesh-hand-acts.hand-act,mesh-delivery-builder.builder_versions" {
|
||||
if strings.Join(m.Invokes, ",") != "seat:mesh-catalog.revision,seat:mesh-catalog.registrations,seat:mesh-hand-acts.hand-act,mesh-delivery-builder.builder_versions" {
|
||||
t.Errorf("the manifest invokes %v", m.Invokes)
|
||||
}
|
||||
if len(m.Tools) != 0 {
|
||||
|
||||
@@ -276,11 +276,7 @@ var boundCap = parts.BusCap(packageseats.IncomingObjects)
|
||||
|
||||
// busHolder is the holder over the runtime's bus state, the builder's numbering and the forge.
|
||||
func busHolder(forge func() (*Forge, error)) (*packageseats.Holder, error) {
|
||||
callers, err := packageseats.ConfiguredCallers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &packageseats.Holder{Callers: callers, Objects: parts.BusObjects(packageseats.IncomingObjects),
|
||||
return &packageseats.Holder{Objects: parts.BusObjects(packageseats.IncomingObjects),
|
||||
Transfers: parts.BusTransfers[packageseats.Opening](packageseats.TransfersState),
|
||||
Builder: builds.BuilderVersions{Ask: stdio.Ask}, Store: seatStore{forge}, Say: say, Finding: finding,
|
||||
Cap: boundCap, Record: recordAct, Idle: parts.BusArm(packageseats.IdleState)}, nil
|
||||
|
||||
@@ -6,7 +6,7 @@ package main
|
||||
// the built package-registry bundle, played over its real stdin and stdout as the node's tool runtime plays it, its
|
||||
// `incoming` an object store and its `transfers` and `findings` key-value buckets of a nats-server of this test alone
|
||||
// (so the publish in parts runs through mesh-sdk's parts.BusObjects and BusTransfers, ruling 4 of the lead's 14:0x), the
|
||||
// builder's builder_versions answered on mesh/ask, the config value `callers` watched on mesh/provision.watch, and the forge an
|
||||
// builder's builder_versions answered on mesh/ask, and the forge an
|
||||
// httptest server that takes reads only with the tools' token and writes only with the packages account's.
|
||||
//
|
||||
// A Go package the builder numbered goes up in two chunks, is uploaded whole as the packages account and says
|
||||
@@ -113,7 +113,6 @@ func (p *player) handle(m wire) {
|
||||
Data []byte `json:"data"`
|
||||
Subject string `json:"subject"`
|
||||
Body json.RawMessage `json:"body"`
|
||||
Kind string `json:"kind"`
|
||||
TTL int `json:"ttl_seconds"`
|
||||
}
|
||||
_ = json.Unmarshal(m.Params, &a)
|
||||
@@ -123,12 +122,6 @@ func (p *player) handle(m wire) {
|
||||
_ = json.Unmarshal(a.Body, &q)
|
||||
d, ok := p.numbered[q.Name+"@"+q.Version]
|
||||
p.answer(m.ID, map[string]any{"recorded": ok && d == q.Digest, "why": "not numbered with that digest"}, nil)
|
||||
case m.Method == "mesh/provision.watch" && a.Kind == "config":
|
||||
// The watch of `callers`, as the runtime answers it: the value now as one change (mesh/provision,
|
||||
// named by the watch's id) handed over before the watch is answered, here unset, so design 51's list holds.
|
||||
p.send(map[string]any{"id": "change-" + watchID(m.ID), "method": "mesh/provision", "params": map[string]any{
|
||||
"watch": watchID(m.ID), "change": map[string]any{"kind": "config", "key": a.Key}}})
|
||||
p.answer(m.ID, map[string]any{}, nil)
|
||||
case m.Method == "mesh/seat.publish":
|
||||
p.answer(m.ID, map[string]any{}, p.nc.Publish(a.Subject, a.Body))
|
||||
case m.Method == "mesh/publish":
|
||||
@@ -142,15 +135,6 @@ func (p *player) handle(m wire) {
|
||||
}
|
||||
}
|
||||
|
||||
// watchID is the watch's id as a string, the form the change names it by.
|
||||
func watchID(raw json.RawMessage) string {
|
||||
var id string
|
||||
if json.Unmarshal(raw, &id) != nil {
|
||||
return string(raw)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (p *player) objects(m wire, local, name string, data []byte) {
|
||||
os, err := p.js.ObjectStore(p.ctx, "gitea_"+local)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,21 +2,18 @@
|
||||
// `go-package-registry` (novox/hq design 51 *The four registry seats*, ADR 0605, ADR 0611). A build module publishes
|
||||
// a package through `publish` and `publish-part`: the package's one file lands in the forge module's own object store
|
||||
// `incoming`, is hashed, checked against the builder's numbering and then uploaded into the forge's package registry
|
||||
// with the module's own write side. The receiving side is mesh-sdk's seat-neutral parts.Receiver and the caller list
|
||||
// mesh-sdk's VerbCallers (ADR 0627 §2): this package binds both to the two seats and to the forge.
|
||||
// with the module's own write side. The receiving side is mesh-sdk's seat-neutral parts.Receiver: this package binds it to the two seats and to the
|
||||
// forge. Who may call a verb is the seat definition's `callers`, which the grants part grants by (design 47 §68, §321).
|
||||
package packageseats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/parts"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
// The seats the forge module holds.
|
||||
@@ -67,103 +64,6 @@ const (
|
||||
ReasonRetention = "retention"
|
||||
)
|
||||
|
||||
// Callers is the trusted config value `callers`, read as mesh-sdk's list per verb.
|
||||
type Callers = stdio.VerbCallers
|
||||
|
||||
// DefaultCallers is design 51's list for the package seats: publish and publish-part the two build modules that
|
||||
// publish packages; remove the builder (a tagged version whose branch ended); versions anyone on the mesh, as the
|
||||
// registry's reads are anonymous.
|
||||
func DefaultCallers() Callers {
|
||||
builds := []string{"mesh-build-go", "mesh-build-node"}
|
||||
return Callers{VerbPublish: builds, VerbPublishPart: builds, VerbRemove: {"mesh-delivery-builder"},
|
||||
VerbVersions: {stdio.AnyCaller}}
|
||||
}
|
||||
|
||||
// ConfiguredCallers is the config value `callers` as mesh-config resolved it, its lines of the package seats' verbs; unset,
|
||||
// or with none of them, is DefaultCallers. It is held from a watch of the value and replaced on each change, so a call
|
||||
// never asks the runtime (issue 1966). The same value carries the lines of the other seat the module holds (git, served
|
||||
// by its TypeScript side), so each holder takes its own verbs' lines and neither refuses the other's.
|
||||
func ConfiguredCallers() (Callers, error) { return configured() }
|
||||
|
||||
var (
|
||||
watchMu sync.Mutex
|
||||
heldMu sync.Mutex
|
||||
heldList Callers
|
||||
heldBad error
|
||||
)
|
||||
|
||||
// configured reads the held list; the first call starts the watch, and a change that cannot be read keeps the list last
|
||||
// read, said and never widened.
|
||||
func configured() (Callers, error) {
|
||||
if l, err := read(); l != nil || err != nil {
|
||||
return l, err
|
||||
}
|
||||
watchMu.Lock()
|
||||
defer watchMu.Unlock()
|
||||
if l, err := read(); l != nil || err != nil {
|
||||
return l, err
|
||||
}
|
||||
if err := stdio.WatchConfig("callers", func(c stdio.ProvisionChange) error {
|
||||
heldMu.Lock()
|
||||
defer heldMu.Unlock()
|
||||
if c.Refused != "" {
|
||||
heldBad = fmt.Errorf("the config value callers is not answered: %s", c.Refused)
|
||||
return nil
|
||||
}
|
||||
l, err := OwnCallers(c.Value)
|
||||
if err != nil {
|
||||
heldBad = fmt.Errorf("the config value callers cannot be read: %w", err)
|
||||
return nil
|
||||
}
|
||||
heldList, heldBad = l, nil
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("the config value callers cannot be read: %w", err)
|
||||
}
|
||||
l, err := read()
|
||||
if l == nil && err == nil {
|
||||
err = errors.New("the config value callers cannot be read: the runtime handed over no value")
|
||||
}
|
||||
return l, err
|
||||
}
|
||||
|
||||
func read() (Callers, error) {
|
||||
heldMu.Lock()
|
||||
defer heldMu.Unlock()
|
||||
if heldList != nil {
|
||||
return heldList, nil
|
||||
}
|
||||
return nil, heldBad
|
||||
}
|
||||
|
||||
// OwnCallers reads the package seats' lines of the config value as the runtime answered it: a JSON object of verb to
|
||||
// callers, or that object written as a string. A value that cannot be read is an error, never the defaults.
|
||||
func OwnCallers(raw json.RawMessage) (Callers, error) {
|
||||
text := strings.TrimSpace(string(raw))
|
||||
var quoted string
|
||||
if json.Unmarshal(raw, "ed) == nil {
|
||||
text = strings.TrimSpace(quoted)
|
||||
}
|
||||
if text == "" || text == "null" {
|
||||
return DefaultCallers(), nil
|
||||
}
|
||||
var all map[string]json.RawMessage
|
||||
if err := json.Unmarshal([]byte(text), &all); err != nil {
|
||||
return nil, fmt.Errorf("callers is not a JSON object of verb to callers: %w", err)
|
||||
}
|
||||
own := map[string]json.RawMessage{}
|
||||
for _, verb := range Verbs {
|
||||
if line, ok := all[verb]; ok {
|
||||
own[verb] = line
|
||||
}
|
||||
}
|
||||
if len(own) == 0 {
|
||||
return DefaultCallers(), nil
|
||||
}
|
||||
b, _ := json.Marshal(own)
|
||||
return stdio.ParseVerbCallers(string(b), Verbs, DefaultCallers())
|
||||
}
|
||||
|
||||
// Publish is a publish as the seats' contract names it (mesh-contracts npmpackageregistry and gopackageregistry share
|
||||
// one shape): what the tools bundle reads from either seat's PublishInput.
|
||||
type Publish struct {
|
||||
@@ -227,7 +127,6 @@ type Store interface {
|
||||
|
||||
// Holder answers the verbs of both seats.
|
||||
type Holder struct {
|
||||
Callers Callers
|
||||
Objects parts.Objects
|
||||
Transfers parts.Transfers[Opening]
|
||||
Builder Builder
|
||||
@@ -316,9 +215,6 @@ func (h *Holder) answer(transfer string, r parts.Received) Answer {
|
||||
|
||||
// Publish is `publish` on seat for the proved caller: a package is one file, its digest the file's sha256.
|
||||
func (h *Holder) Publish(seat, caller string, p Publish) (Answer, error) {
|
||||
if err := h.Callers.Allowed(VerbPublish, caller); err != nil {
|
||||
return Answer{}, err
|
||||
}
|
||||
if err := checkPublish(seat, p); err != nil {
|
||||
return Answer{}, err
|
||||
}
|
||||
@@ -349,9 +245,6 @@ func (h *Holder) Publish(seat, caller string, p Publish) (Answer, error) {
|
||||
|
||||
// PublishPart is `publish-part`: one chunk, from the caller that opened the transfer alone.
|
||||
func (h *Holder) PublishPart(seat, caller, transfer, part string, sequence int, last bool, data []byte) (Answer, error) {
|
||||
if err := h.Callers.Allowed(VerbPublishPart, caller); err != nil {
|
||||
return Answer{}, err
|
||||
}
|
||||
got, err := h.receiver().Take(key(seat, transfer), caller, part, sequence, last, data, func(t Transfer) error {
|
||||
if t.Shape.Seat != seat {
|
||||
return fmt.Errorf("transfer %s is %s's, not %s's", transfer, t.Shape.Seat, seat)
|
||||
@@ -366,9 +259,6 @@ func (h *Holder) PublishPart(seat, caller, transfer, part string, sequence int,
|
||||
|
||||
// Versions is `versions`: every version the seat holds of name, newest first.
|
||||
func (h *Holder) Versions(seat, caller, name string) ([]Version, error) {
|
||||
if err := h.Callers.Allowed(VerbVersions, caller); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := Seats[seat]; !ok {
|
||||
return nil, fmt.Errorf("versions: the forge holds no seat %s", seat)
|
||||
}
|
||||
@@ -385,9 +275,6 @@ func (h *Holder) Versions(seat, caller, name string) ([]Version, error) {
|
||||
// Remove is `remove`: the builder removes a tagged version (its branch ended); an untagged version goes only by the
|
||||
// forge's own retention, never by a call. A version already gone is said removed all the same, so a retry ends.
|
||||
func (h *Holder) Remove(seat, caller, name, version, reason string) (Outcome, error) {
|
||||
if err := h.Callers.Allowed(VerbRemove, caller); err != nil {
|
||||
return Outcome{}, err
|
||||
}
|
||||
if _, ok := Seats[seat]; !ok {
|
||||
return Outcome{}, fmt.Errorf("remove: the forge holds no seat %s", seat)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/parts"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
type memObjects map[string][]byte
|
||||
@@ -102,7 +101,7 @@ type bed struct {
|
||||
|
||||
func newBed(numbered bool) *bed {
|
||||
b := &bed{forge: &forge{files: map[string]map[string][]byte{}}, findings: map[string]bool{}}
|
||||
b.h = &Holder{Callers: DefaultCallers(), Objects: memObjects{}, Transfers: memTransfers{}, Builder: builder{ok: numbered},
|
||||
b.h = &Holder{Objects: memObjects{}, Transfers: memTransfers{}, Builder: builder{ok: numbered},
|
||||
Store: b.forge, Now: func() time.Time { return time.Unix(1e9, 0) },
|
||||
Say: func(seat, event string, o Outcome) error { b.said = append(b.said, said{seat, event, o}); return nil },
|
||||
Finding: func(kind, subject, _ string, raised bool) { b.findings[kind+" "+subject] = raised }}
|
||||
@@ -212,14 +211,6 @@ func TestTheForgeHoldsBothPackageSeats(t *testing.T) {
|
||||
do func(b *bed) error
|
||||
want string
|
||||
}{
|
||||
{"a caller not on publish's list", func(b *bed) error {
|
||||
_, err := b.h.Publish(NpmSeat, "ace.claude-code", pub("x", "0.1.0", file))
|
||||
return err
|
||||
}, "not on the holder's caller list"},
|
||||
{"an unproven caller", func(b *bed) error {
|
||||
_, err := b.h.Publish(NpmSeat, stdio.Unproven, pub("x", "0.1.0", file))
|
||||
return err
|
||||
}, "no caller the tool runner proved"},
|
||||
{"a seat the forge does not hold", func(b *bed) error {
|
||||
_, err := b.h.Publish("cargo-package-registry", goBuild, pub("x", "0.1.0", file))
|
||||
return err
|
||||
@@ -244,7 +235,6 @@ func TestTheForgeHoldsBothPackageSeats(t *testing.T) {
|
||||
_, err := b.h.PublishPart(GoSeat, nodeBuild, "t-x", digest(file), 0, true, file)
|
||||
return err
|
||||
}, "not open"},
|
||||
{"remove by a build module", func(b *bed) error { _, err := b.h.Remove(GoSeat, goBuild, "x", "0.1.1-feat.1", "tag-gone"); return err }, "not on the holder's caller list"},
|
||||
{"remove of a version with no tag", func(b *bed) error {
|
||||
_, err := b.h.Remove(GoSeat, "novox.mesh-delivery-builder", "x", "0.1.1", "tag-gone")
|
||||
return err
|
||||
@@ -304,29 +294,4 @@ func TestTheForgeHoldsBothPackageSeats(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the callers value: its own verbs' lines, the other seat's left to it, unset or none of its own the defaults", func(t *testing.T) {
|
||||
manifest := `"{\"publish\":[\"mesh-build-go\"],\"remove\":[\"mesh-delivery-builder\"],\"pull\":[\"mesh-delivery-gate\"]}"`
|
||||
for _, row := range []struct {
|
||||
raw string
|
||||
publish []string
|
||||
fails bool
|
||||
}{
|
||||
{raw: manifest, publish: []string{"mesh-build-go"}},
|
||||
{raw: `{"pull":["mesh-delivery-gate"]}`, publish: DefaultCallers()[VerbPublish]},
|
||||
{raw: `""`, publish: DefaultCallers()[VerbPublish]},
|
||||
{raw: `{"publish":"mesh-build-go"}`, fails: true},
|
||||
{raw: `"not json"`, fails: true},
|
||||
} {
|
||||
got, err := OwnCallers([]byte(row.raw))
|
||||
if row.fails != (err != nil) {
|
||||
t.Fatalf("%s: err %v", row.raw, err)
|
||||
}
|
||||
if !row.fails && strings.Join(got[VerbPublish], ",") != strings.Join(row.publish, ",") {
|
||||
t.Errorf("%s: publish %v, want %v", row.raw, got[VerbPublish], row.publish)
|
||||
}
|
||||
if _, theirs := got["pull"]; theirs {
|
||||
t.Errorf("%s: the seat git's line is not the package seats'", row.raw)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -248,13 +248,6 @@
|
||||
"default": [
|
||||
"jschoubben"
|
||||
]
|
||||
},
|
||||
"callers": {
|
||||
"type": "string",
|
||||
"trusted": true,
|
||||
"runtime": true,
|
||||
"default": "{\"publish\":[\"mesh-build-go\",\"mesh-build-node\"],\"publish-part\":[\"mesh-build-go\",\"mesh-build-node\"],\"remove\":[\"mesh-delivery-builder\"],\"versions\":[\"*\"],\"pull\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-gate\"],\"branch\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-indexer\"],\"open-pulls\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-indexer\",\"mesh-delivery-gate\"],\"tags\":[\"mesh-delivery-forge-bridge\"],\"trees\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-checker\",\"mesh-delivery-indexer\"],\"file\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-checker\",\"mesh-delivery-indexer\"],\"contains\":[\"mesh-delivery-indexer\",\"mesh-delivery-gate\"],\"statuses\":[\"mesh-delivery-gate\"],\"status\":[\"mesh-delivery-forge-bridge\"],\"comment\":[\"mesh-delivery-forge-bridge\"],\"view\":[\"mesh-delivery-forge-bridge\"],\"note\":[\"mesh-delivery-forge-bridge\"],\"merge-pull\":[\"mesh-delivery-forge-bridge\"],\"tag\":[\"mesh-delivery-builder\"]}",
|
||||
"description": "per verb of the seats the module holds, the callers that may call it (JSON object of verb to callers; * every proved caller, operator the operator's session); each holder reads the lines of its own verbs. The package seats' lines are design 51's (empty is that list for them); the seat git's are design 47's: its reads the parts that declare them in invokes, its acts (status, comment, view, note, merge-pull) the forge bridge alone, and a verb of git with no line is called by no one"
|
||||
}
|
||||
},
|
||||
"facts": {
|
||||
|
||||
+4
-32
@@ -2,10 +2,8 @@
|
||||
// delivery part makes of the forge is a verb of the seat, served here as a tool `git.<verb>` against the seat's generated
|
||||
// contract, and no part names this module's own tools.
|
||||
//
|
||||
// **Who may call which verb** is the holder's trusted config value `callers` (ADR 0627 §2), the one value the module's
|
||||
// package seats read too: each holder takes the lines of the verbs it serves, so neither refuses the other's. Unset, no
|
||||
// one may call a verb of `git`: the list is the manifest's, never a list in this code. The caller is the one the tool
|
||||
// runner proved in the call's `_meta` (mesh-sdk `callers.callerOf`), never an argument.
|
||||
// **Who may call which verb** is the seat definition's `callers`, which the grants part grants by: the bus refuses a caller a
|
||||
// definition does not name (design 47 §68, §313, §321), so this holder judges no caller and keeps no list of its own.
|
||||
//
|
||||
// **A failed read answers an error, never an empty answer**: the forge's 404 is the kind `not-found`, its refusal (another
|
||||
// 4xx, or a refusal of this holder) the kind `refused`, a forge that failed or did not answer the kind `failed`; an empty list or an absent file is answered
|
||||
@@ -16,8 +14,6 @@
|
||||
// (design 47 §314): only into a base whose protection refuses a push, and only that head.
|
||||
|
||||
import type { ToolDefinition } from "@novox/mesh-sdk/tools";
|
||||
import { callerOf, refusal, parseVerbCallers, type VerbCallers } from "@novox/mesh-sdk/callers";
|
||||
import { config } from "@novox/mesh-sdk/runtime";
|
||||
import {
|
||||
seat as GIT, verbs as GIT_VERBS,
|
||||
type BranchAnswer, type ContainsAnswer, type CommentAnswer, type ErrorKind, type FileAnswer, type MergePullAnswer, type Merging, type NoteAnswer,
|
||||
@@ -33,9 +29,6 @@ import { appendNote, viewBody, viewComment, type Runner } from "./delivery.js";
|
||||
/** The verbs of the seat, in its definition's order. */
|
||||
export const VERBS: readonly string[] = Object.values(GIT_VERBS);
|
||||
|
||||
/** The config value whose lines per verb say who may call it. */
|
||||
export const CALLERS_KEY = "callers";
|
||||
|
||||
/** How many tags a page of `tags` holds. */
|
||||
export const TAGS_PAGE = 50;
|
||||
|
||||
@@ -50,31 +43,13 @@ export interface HeadStatuses {
|
||||
}
|
||||
|
||||
/** What the holder reaches beside the forge's API: git in the forge's container (the note), the head's one writer of its
|
||||
* statuses, and the callers' list. */
|
||||
* statuses, and whether the first registrations are written. */
|
||||
export interface SeatDeps {
|
||||
run: Runner;
|
||||
container: string;
|
||||
statuses: HeadStatuses;
|
||||
/** Whether the first registrations were written (registrations.ts): until then merge-pull refuses every merge (design 47 §95). */
|
||||
registered: () => boolean;
|
||||
/** The config value `callers` as the runtime answers it; the runtime's own config read by default. */
|
||||
callers?: () => Promise<unknown>;
|
||||
}
|
||||
|
||||
/** The lines of `raw` (the config value, an object or that object as a string) for the seat's verbs; unset is none. */
|
||||
export function gitCallers(raw: unknown): VerbCallers {
|
||||
if (raw === undefined || raw === null || raw === "") return {};
|
||||
let all: unknown = raw;
|
||||
if (typeof raw === "string") {
|
||||
try {
|
||||
all = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
throw new Error(`callers is not a JSON object of verb to callers: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
if (!all || typeof all !== "object" || Array.isArray(all)) throw new Error("callers is not a JSON object of verb to callers");
|
||||
const mine = Object.fromEntries(Object.entries(all as Record<string, unknown>).filter(([verb]) => VERBS.includes(verb)));
|
||||
return parseVerbCallers(JSON.stringify(mine), VERBS, {});
|
||||
}
|
||||
|
||||
/** A failure as the contract says it: the forge's or this holder's refusal, or a forge that failed. */
|
||||
@@ -146,17 +121,14 @@ async function mergingOf(gitea: GiteaClient, owner: string, repo: string, p: Git
|
||||
return { mergeable: p.mergeable, protected: !!rule, required, writes };
|
||||
}
|
||||
|
||||
/** The seat's verbs as tools `git.<verb>`, each judging its caller first and answering in the seat's contract. */
|
||||
/** The seat's verbs as tools `git.<verb>`, each answering in the seat's contract. */
|
||||
export function seatTools(gitea: GiteaClient, deps: SeatDeps): ToolDefinition[] {
|
||||
const listOf = deps.callers ?? (() => config(CALLERS_KEY));
|
||||
const verb = (name: string, description: string, act: (args: Record<string, unknown>) => Promise<object>): ToolDefinition => ({
|
||||
name: `${GIT}.${name}`,
|
||||
description,
|
||||
input: {},
|
||||
run: async (args: Record<string, unknown>) => {
|
||||
try {
|
||||
const why = refusal(gitCallers(await listOf()), name, callerOf(args));
|
||||
if (why) return failure(new Refused(why.message));
|
||||
return await act(args);
|
||||
} catch (err) {
|
||||
return failure(err);
|
||||
|
||||
@@ -431,19 +431,14 @@ test("the two new tools are named for what mesh_search is asked, and gitea_api s
|
||||
assert.ok("refused" in judgeApiCall("PATCH", "/repos/a/b/pulls/7", { base: "x" }));
|
||||
});
|
||||
|
||||
// **The seat git's verbs judge their caller, and merge-pull judges the Approve's head and no check** (design 47 *The seat
|
||||
// git*, ADR 0661 §3): a caller the trusted list does not name is refused before the forge is asked; merge-pull merges the
|
||||
// **merge-pull judges the head it is asked at and no check** (design 47 *The seat git*, §314): merge-pull merges the
|
||||
// head it is named into a base that takes no push, whatever its statuses say, and an id said again after the merge is
|
||||
// answered performed.
|
||||
test("the seat git's verbs judge their caller, and merge-pull merges the Approve's head and judges no check", async () => {
|
||||
const { seatTools, gitCallers } = await import("../dist/seat.js");
|
||||
test("merge-pull merges the head the caller names and judges no check", async () => {
|
||||
const { seatTools } = await import("../dist/seat.js");
|
||||
const { runAs } = await import("@novox/mesh-sdk/callers");
|
||||
const bridge = "ace.mesh-delivery-forge-bridge";
|
||||
let registered = true;
|
||||
const list = JSON.stringify({ publish: ["mesh-build-go"], "merge-pull": ["mesh-delivery-forge-bridge"],
|
||||
pull: ["mesh-delivery-forge-bridge", "mesh-delivery-gate"], status: ["mesh-delivery-forge-bridge"] });
|
||||
assert.deepEqual(Object.keys(gitCallers(list)).sort(), ["merge-pull", "pull", "status"], "only the seat git's lines are its");
|
||||
assert.deepEqual(gitCallers(undefined), {}, "unset, no one may call");
|
||||
// The head's one writer of its statuses (gate.ts), over a forge whose first write is still in flight when the next is asked.
|
||||
const { GateWriter } = await import("../dist/gate.js");
|
||||
const landed: string[] = [];
|
||||
@@ -457,15 +452,13 @@ test("the seat git's verbs judge their caller, and merge-pull merges the Approve
|
||||
};
|
||||
const gates = new GateWriter(slow);
|
||||
const seat = seatTools(Object.assign(Object.create(client), { setCommitStatus: slow.setCommitStatus }),
|
||||
{ run: async () => ({ stdout: "", code: 0 }), container: "gitea", callers: async () => list, statuses: gates, registered: () => registered });
|
||||
{ run: async () => ({ stdout: "", code: 0 }), container: "gitea", statuses: gates, registered: () => registered });
|
||||
const call = (verb: string, args: Record<string, unknown>, caller = bridge) =>
|
||||
runAs({ "mesh/caller": caller }, args, () => seat.find((t: any) => t.name === `git.${verb}`)!.run(args)) as Promise<any>;
|
||||
const merge = (extra: Record<string, unknown> = {}, caller?: string) =>
|
||||
call("merge-pull", { id: "act-1", repository: "novox/mesh-catalog", number: 7, head: REVIEWED, ...extra }, caller);
|
||||
const rows: { name: string; set: () => void; caller?: string; extra?: Record<string, unknown>; want: RegExp | "merged" | "held"; loud?: boolean; kind?: string }[] = [
|
||||
{ name: "no registration written yet", set: () => { registered = false; }, want: /no module is registered yet/, kind: "no-registrations" },
|
||||
{ name: "a caller not on the list", set: () => {}, caller: "ace.mesh-delivery-checker", want: /refused for ace.mesh-delivery-checker/ },
|
||||
{ name: "an unproven caller", set: () => {}, caller: "", want: /no caller the tool runner proved/ },
|
||||
{ name: "a head that moved", set: () => { state.head = MOVED; }, want: /its head is 3c4d5e6f7a8b, not the abc1234def56/, kind: "head-moved" },
|
||||
{ name: "a base no rule guards", set: () => { state.rules = []; }, want: /guarded by no protection rule/, kind: "unprotected" },
|
||||
{ name: "a base whose protection was removed after the last branch-moved", set: () => { state.rules = [protectedMain()]; state.rules = []; }, want: /guarded by no protection rule/, kind: "unprotected" },
|
||||
@@ -555,11 +548,10 @@ test("every read and act of the seat git answers its contract, and an error is a
|
||||
};
|
||||
const run = async (_f: string, args: string[]) => args.includes("show") || args.includes("--ref") && !args.includes("append")
|
||||
? { stdout: noteShown, code: noteShown ? 0 : 1 } : (noteWritten++, { stdout: "", code: 0 });
|
||||
const seat = seatTools(forge as any, { run: run as any, container: "gitea", callers: async () => JSON.stringify({}), statuses: { status: async () => {} }, registered: () => true });
|
||||
const everyone = JSON.stringify(Object.fromEntries(seat.map((t: any) => [t.name.slice(4), ["*"]])));
|
||||
const seat = seatTools(forge as any, { run: run as any, container: "gitea", statuses: { status: async () => {} }, registered: () => true });
|
||||
const call = (verb: string, args: Record<string, unknown>, over: Record<string, unknown> = {}) => {
|
||||
const t = seat.find((x: any) => x.name === `git.${verb}`)!;
|
||||
const wrapped = seatTools(Object.assign(Object.create(forge), over) as any, { run: run as any, container: "gitea", callers: async () => everyone, statuses: { status: async () => {} }, registered: () => true });
|
||||
const wrapped = seatTools(Object.assign(Object.create(forge), over) as any, { run: run as any, container: "gitea", statuses: { status: async () => {} }, registered: () => true });
|
||||
return runAs({ "mesh/caller": "ace.x" }, args, () => wrapped.find((x: any) => x.name === t.name)!.run(args)) as Promise<any>;
|
||||
};
|
||||
const rows: [string, string, Record<string, unknown>, Record<string, unknown>, (a: any) => unknown, unknown][] = [
|
||||
|
||||
Reference in New Issue
Block a user