Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions FOLLOWUPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ Until both exist, terms are set once at charter. A role whose terms are wrong
is retired and re-chartered, which is visible in the chain rather than
self-signed inside it.

**Narrowed, 2026-08-31.** `org submit` now exposes exactly one slice of the
kind: it adds a single supervisor and leaves every other term byte-identical.
That is admissible under this entry's own criteria rather than in spite of
them — supervisors are listed above as a verifiable attenuation ("no shrink"),
and adding one is monotone in the accountability direction, so none of the five
self-signed widenings this entry names are reachable through it. The two
missing pieces are unchanged and still block the general verb; note only that
`Tier` became inert in #275, so the tier half of gap 2 is now moot and the
ceilings half is what remains.

## org: annul is a repudiation, not a revert

`applyStructural` appends the annulled digest to `Annulled` and changes nothing
Expand Down
12 changes: 12 additions & 0 deletions cmd/org/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ system name there is Baton; this binary is its first runtime slice.
no `recharter` verb: changing terms can WIDEN authority and the kernel has no
parent-authority check and no tier ordering to verify attenuation with (see
FOLLOWUPS).
- **`submit`** is the one slice of `KindRecharter` that is safe self-signed: it
adds ONE supervisor and leaves every other term byte-identical. FOLLOWUPS
names supervisors as a verifiable attenuation ("no shrink"), and the
direction is what admits it — naming another role that may displace you
strictly increases accountability and cannot widen your own authority, so
there is no escalation to guard against. It exists because `checkTakeover`
admits a displacement only from a role the charter names, so a lane nobody
can clear becomes clearable only by its own consent: recharter is admitted
from Held or Active under the writer's own incarnation, so a role submits to
oversight while sitting in its seat. Nothing can be made supervisable from
outside, which is why this is not the parent-authority mechanism the kernel
still lacks.
- **`transfer`** moves one work item between two roles in the same tenant.
Nothing about it is atomic — two chains, two locks, no cross-chain
transaction — so it makes the failure states RECOVERABLE rather than
Expand Down
12 changes: 12 additions & 0 deletions cmd/org/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ system name there is Baton; this binary is its first runtime slice.
no `recharter` verb: changing terms can WIDEN authority and the kernel has no
parent-authority check and no tier ordering to verify attenuation with (see
FOLLOWUPS).
- **`submit`** is the one slice of `KindRecharter` that is safe self-signed: it
adds ONE supervisor and leaves every other term byte-identical. FOLLOWUPS
names supervisors as a verifiable attenuation ("no shrink"), and the
direction is what admits it — naming another role that may displace you
strictly increases accountability and cannot widen your own authority, so
there is no escalation to guard against. It exists because `checkTakeover`
admits a displacement only from a role the charter names, so a lane nobody
can clear becomes clearable only by its own consent: recharter is admitted
from Held or Active under the writer's own incarnation, so a role submits to
oversight while sitting in its seat. Nothing can be made supervisable from
outside, which is why this is not the parent-authority mechanism the kernel
still lacks.
- **`transfer`** moves one work item between two roles in the same tenant.
Nothing about it is atomic — two chains, two locks, no cross-chain
transaction — so it makes the failure states RECOVERABLE rather than
Expand Down
55 changes: 55 additions & 0 deletions cmd/org/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"io"
"os"
"path/filepath"
"slices"
"strings"
"time"

Expand All @@ -52,6 +53,7 @@ func main() { os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) }
// read verbs never take the lock.
var verbs = map[string]func(*env, []string) error{
"charter": cmdCharter,
"submit": cmdSubmit,
"annul": cmdAnnul,
"attach": cmdAttach,
"assign": cmdAssign,
Expand Down Expand Up @@ -116,6 +118,7 @@ func usage(w io.Writer) {
fmt.Fprintln(w, `usage: org <verb> [flags]

lifecycle charter · attach · release · retire · takeover · revoke · delegate
submit (accept a supervisor; the only self-signed terms change)
correction annul (repudiate the tip; corrects forward, does not revert)
work assign · transfer · unassign · claim · yield · complete · abandon
composite begin (attach+assign+claim) · done (claim?+complete+release)
Expand Down Expand Up @@ -541,6 +544,58 @@ func cmdCharter(e *env, args []string) error {
return appendAndReport(e, h, s, home.Draft{Kind: org.KindCharter, Terms: terms()})
}

// cmdSubmit adds one supervisor to a live role's terms and changes nothing
// else. It is the deliberately narrow slice of KindRecharter that is safe to
// self-sign.
//
// A general recharter verb was written and withdrawn from #272, and FOLLOWUPS
// records why: checkRecharter verifies only that min_reader is monotone, and
// checkWriter accepts the holder's own incarnation, so exposing the whole kind
// would let a role widen its own scope, lift its ceilings, add effect classes,
// or drop the supervisors that may take it over — all self-signed. That
// remains true and this verb does not change it.
//
// What makes THIS safe is the direction of travel. FOLLOWUPS names supervisors
// as one of the three attenuations a kernel can actually verify ("no shrink"),
// and adding one is monotone in the accountability direction: it strictly
// increases the set of roles that may displace you and cannot widen your own
// authority. There is no escalation available by naming more parties who can
// remove you. So the guard is total rather than partial — every other term
// must be byte-identical to the chain's current terms, and a supervisor may
// only be added, never removed.
//
// Who writes it matters as much as what it says: recharter is admitted only
// from Held or Active and carries the writer's own incarnation, so a role
// submits to oversight while sitting in its own seat. Consent, not imposition
// — no role can be made supervisable from outside, which is the property that
// keeps this from becoming the parent-authority mechanism the kernel still
// lacks.
func cmdSubmit(e *env, args []string) error {
s := newScope("submit")
party := s.fs.String("party", "", "role to add as a supervisor, e.g. supervisor:mh")
h, err := s.open(args, true)
if err != nil {
return err
}
if *party == "" {
return fmt.Errorf("-party is required: the role being added as a supervisor")
}
_, state, err := h.Load(s.tenant, s.role)
if err != nil {
return err
}
if slices.Contains(state.Terms.Supervisors, *party) {
return reportNoOp(e, s, fmt.Sprintf("%s already supervises %s; nothing to do", *party, s.role))
}
// The new terms are derived from the snapshot read above, so the append is
// fenced to that snapshot's tip: two concurrent submits would otherwise
// each carry "old supervisors + mine", and the second to land would
// silently drop the first's party — the one edit this verb must never make.
next := state.Terms
next.Supervisors = append(slices.Clone(state.Terms.Supervisors), *party)
return appendAndReport(e, h, s, home.Draft{Kind: org.KindRecharter, Terms: &next, ExpectTip: state.Tip})
}

// cmdTransfer moves one work item between two roles in the same tenant.
//
// Nothing here is atomic: two chains, two locks, no cross-chain transaction.
Expand Down
124 changes: 124 additions & 0 deletions cmd/org/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -936,3 +936,127 @@ func TestTransferNoOpSpeaksJSON(t *testing.T) {
t.Fatalf("no-op envelope = %#v", envelope)
}
}

// TestSubmitAddsSupervisorAndChangesNothingElse pins the property that makes
// this verb safe to self-sign. FOLLOWUPS withdrew a general recharter because
// a role could widen its own scope, lift its ceilings or drop the supervisors
// that may take it over. Submit is admitted only because it moves one term in
// the one direction the kernel can verify — supervisors, no shrink — so the
// test asserts the whole rest of Terms survives byte-identical rather than
// merely asserting the supervisor landed.
func TestSubmitAddsSupervisorAndChangesNothingElse(t *testing.T) {
state := t.TempDir()
role := []string{"-tenant", "acme", "-role", "steward:api"}
must := func(verb ...string) string {
t.Helper()
code, out, errOut := exec(t, state, append(verb, role...)...)
if code != 0 {
t.Fatalf("%v: exit %d: %s", verb, code, errOut)
}
return out
}
must("charter", "-scope", "github:acme/api", "-scope", "jira:API",
"-supervisor", "human:op", "-effect-class", "merge",
"-cycle-ceiling", "3", "-spend-ceiling", "500", "-retire-when", "api retires")
must("attach")

must("submit", "-party", "supervisor:acme")

var boot struct {
Boot struct {
Terms org.Terms `json:"terms"`
} `json:"boot"`
}
if err := json.Unmarshal([]byte(must("boot", "-json")), &boot); err != nil {
t.Fatalf("boot: %v", err)
}
got := boot.Boot.Terms
if want := []string{"human:op", "supervisor:acme"}; !slices.Equal(got.Supervisors, want) {
t.Fatalf("supervisors = %v, want %v", got.Supervisors, want)
}
// Everything else is untouched. A general recharter would have dropped all
// of this to its flag defaults.
if want := []string{"github:acme/api", "jira:API"}; !slices.Equal(got.Scope, want) {
t.Fatalf("scope = %v, want %v — submit must not narrow scope", got.Scope, want)
}
if want := []string{"merge"}; !slices.Equal(got.EffectClasses, want) {
t.Fatalf("effect classes = %v, want %v", got.EffectClasses, want)
}
if got.CycleCeiling != 3 || got.SpendCeiling != 500 || got.Retire != "api retires" {
t.Fatalf("ceilings/retire drifted: cycles=%d spend=%d retire=%q",
got.CycleCeiling, got.SpendCeiling, got.Retire)
}
}

// TestSubmitEnablesTakeover is the reason the verb exists: a supervisor may
// displace a holder only when the charter names it, so submit is what turns a
// lane nobody can clear into one its supervisor can.
func TestSubmitEnablesTakeover(t *testing.T) {
state := t.TempDir()
role := []string{"-tenant", "acme", "-role", "steward:api"}
run := func(verb ...string) (int, string, string) {
t.Helper()
return exec(t, state, append(verb, role...)...)
}
if code, _, err := run("charter", "-scope", "github:acme/api", "-supervisor", "human:op"); code != 0 {
t.Fatalf("charter: %s", err)
}
if code, _, err := run("attach"); code != 0 {
t.Fatalf("attach: %s", err)
}
// Before submit, the supervisor is a stranger to this charter.
code, _, errOut := run("takeover", "-party", "supervisor:acme")
if code != 1 || !strings.Contains(errOut, org.ReasonNotSupervisor) {
t.Fatalf("takeover before submit: exit %d, want 1 with %s: %s",
code, org.ReasonNotSupervisor, errOut)
}
if code, _, err := run("submit", "-party", "supervisor:acme"); code != 0 {
t.Fatalf("submit: %s", err)
}
if code, _, err := run("takeover", "-party", "supervisor:acme"); code != 0 {
t.Fatalf("takeover after submit: exit %d: %s", code, err)
}
}

// TestSubmitIsIdempotentAndNeedsAParty covers the two ways a caller gets it
// wrong. Re-submitting an existing supervisor writes nothing rather than
// growing the chain with a no-op recharter.
func TestSubmitIsIdempotentAndNeedsAParty(t *testing.T) {
state := t.TempDir()
role := []string{"-tenant", "acme", "-role", "steward:api"}
run := func(verb ...string) (int, string, string) {
t.Helper()
return exec(t, state, append(verb, role...)...)
}
if code, _, err := run("charter", "-scope", "github:acme/api", "-supervisor", "human:op"); code != 0 {
t.Fatalf("charter: %s", err)
}
if code, _, err := run("attach"); code != 0 {
t.Fatalf("attach: %s", err)
}
if code, _, errOut := run("submit"); code == 0 {
t.Fatalf("submit without -party succeeded: %s", errOut)
}
if code, _, err := run("submit", "-party", "human:op"); code != 0 {
t.Fatalf("idempotent submit: %s", err)
}
// A machine retry gets a decodable no-op, not an empty stdout.
code, out, err := run("submit", "-json", "-party", "human:op")
if code != 0 {
t.Fatalf("idempotent submit -json: %s", err)
}
var noop struct {
Steps []receipt `json:"steps"`
Note string `json:"note"`
}
if err := json.Unmarshal([]byte(out), &noop); err != nil || len(noop.Steps) != 0 || noop.Note == "" {
t.Fatalf("idempotent submit -json must print a no-op receipt, got %q (%v)", out, err)
}
code, out, err = run("log")
if code != 0 {
t.Fatalf("log: %s", err)
}
if strings.Contains(out, org.KindRecharter) {
t.Fatalf("re-submitting an existing supervisor wrote a record:\n%s", out)
}
}
Loading