Skip to content

feat(admin): the budget split tools, the write guard and the allocation view - #576

Merged
edgehero merged 1 commit into
mainfrom
feat/504-admin-surfaces
Oct 4, 2026
Merged

edgehero merged 1 commit into
mainfrom
feat/504-admin-surfaces

Conversation

@edgehero

@edgehero edgehero commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Refs #504 (part C, the last of three). Closes #504 together with #573 and #574.

The operator surfaces for the budget split: a model can now set priorities without a keypress, only an operator can change the envelope, and the files that set money and triggers are guarded against pi's own write tools.

Tools

  • dispatch_allocations (read): the envelope numbers, the current split and spend per project and repo, the plan's id, writer and validity, and the last 20 outcomes. No plan reason text is ever returned to a model.
  • dispatch_priorities_set: sequential, no confirm, works headless. It takes weights (and optional repo weights and a plan life), fills in the basis from the current plan, and applies through the worker's own rules as writer operator-session. It returns the outcome, the refusal reason, the plan id, whether it was clamped and the split before and after. When a plan leaves _other out, its current weight is kept.
  • dispatch_envelope_set: behind the operator's confirm, refused headless. It sets the new digest as the expected one, then writes the file atomically through the same parser the worker uses, so the fleet re-bases instead of reporting a hand edit. If the file write fails, the expected digest is put back by a compare-and-set, only while it still holds this write's digest, so two concurrent writes cannot leave the fleet mismatched.
  • The existing limit, project and settings writers now refuse a change that would make the live envelope invalid (a floor above a new operator row, a floor for a project being removed, a missing per-job cap), naming the conflict.

The write guard

pi's built-in write and edit tools, and powershell by the guarded file names its command contains as whole path segments (folded the same way), are blocked when the target is the envelope, projects.json, scoped-limits.json, triggers.json, settings.json, the deployment's .env or the deployment pointer. A path that was guarded once stays guarded for the rest of the session, so re-pointing a key cannot unguard a file. The working directory's triggers.json, scoped-limits.json and projects.json are guarded when their key is set (by the environment, the pointer, or the pointer folder's .env), or, with no pointer, when the folder pi started in is a deployment made by init. With no pointer, every guarded file that folder's .env names, and that .env itself, are guarded too; that .env can only add to the guard, never change what the panel reads. An ordinary repository that happens to hold a triggers.json is not affected. A deployment the panel cannot see at all (no pointer, no key set, not the folder pi started in) is not guarded, and SECURITY.md says so. Targets are judged by file identity (a symlink, a hard link, a case variant or a ~ or @ spelling is the file it names), a file that does not exist yet by its parent and its fully case-folded name (so a spelling the file system folds, such as a long s or a capital sharp s, is caught), and a resolution error blocks. Nested tool calls and codemode scripts go through the same guard. bash, names built at run time and the operator's own ! commands stay named residuals in SECURITY.md.

Commands and panel

  • /dispatch priorities shows the plan, the split and the history; /dispatch priorities set shop=3 platform=1 writes one. Both are zero spend.
  • The ALLOCATION view opens on b: the envelope, each project's share and spend, the current plan with its reasons (escaped like every other agent-written text), and the history. r on one of the newest 20 history rows asks y or n in the frame and reverts to it as operator-revert, skipping the interval and the step. A banner says when the envelope was changed outside the panel, until a later re-base or plan supersedes that edit. Hand-fired cron runs count as runs of their trigger, with their cost attributed to it.

Specs

AMENDED: REQ-ADMIN-VIA-PI-EXTENSION and DES-ADMIN-VIA-PI-EXTENSION (the three tools by name in the Statement and Decision lists, the guard, the residuals), REQ-DELEGATED-ALLOCATION, DES-DELEGATED-ALLOCATION-INSIDE-ENVELOPE, INT-ENVELOPE-FILE-CONTRACT, INT-PRIORITIES-PLAN-CONTRACT, INT-DEPLOYMENT-POINTER-CONTRACT, SECURITY.md.

Release note: the admin imports the worker's new ./allocation export, so the worker is published before the admin. With #573 this completes the doctrine change #504 asked for.

Tests

The headless apply without a dialog, the envelope refusal headless and on decline, the cross-checks, the guard on every file and spelling including nested and codemode writes in a real pi session, reasons escaped in the panel, the b view, the revert, the banner, and the allocation writes against a live Valkey. Each rule was checked by reverting it and watching its test fail (mutation logs kept with the change's working notes).

…on view

Part C of issue #504: the operator surfaces for delegated allocation,
including the wider write guard issue #504's open question recommended.

Tools. dispatch_allocations reads the envelope, the applied split, each
project's spend in the envelope window under the worker's own keys (with
the key names) and the last 20 alloc:log outcomes, through an allowlist of
fields: no plan reason text reaches it. dispatch_priorities_set is the
delegated allocation write: sequential, no confirm, works headless, writer
operator-session. It calls the worker's applyPlan through read-model.mjs
(applyPriorities), fills the basis from the state it reconciled to, keeps
_other's current weight when it is left out (any other project left out is
plan-incomplete), and returns the outcome, the reason enum, the plan id,
clamped and the micro-dollars before and after. dispatch_envelope_set is
confirm-gated and refused headless: any subset of window, total, floors,
default weights and delegation fields, judged by parseEnvelope before the
confirm, then alloc:envelope:expected is set to the new digest BEFORE the
file is replaced (tmp and rename, mode and owner kept, a symlink refused),
and put back when the file is not written, so the fleet re-bases instead of
reading the change as a hand edit. The key is set with GET, and a failed
file write puts it back by a compare-and-set, only while it still holds
this write's digest, so a second confirmed write that landed meanwhile
keeps its key (a live-Valkey race test holds it). The writes connect their fail-fast
client before the first command: the lab showed a live Valkey refusing
every write otherwise ("Stream isn't writeable"), which the fakes hid; a
live-Valkey test now holds it.

Cross-check. The project, limit and per-job cap writers (tools and the
operator's dialogs and set/unset, each tested) refuse a change that would make a loading
envelope invalid (a floored project removed, a row below a floor, the cap
removed), before the confirm, in the parser's words; an envelope that
already fails does not block them.

Write guard. A tool_call handler blocks pi's built-in write and edit
whose target is the envelope, projects.json, scoped-limits.json,
triggers.json, the settings overlay, the deployment's .env or the
deployment pointer, and a powershell command naming one. The set is read
per call through the pointer and .env and never shrinks within a session,
so pointing .env at a decoy cannot unguard the real file. With no
pointer, the .env of the folder pi started in may only ADD to the set
(every guarded file it names, under every reading a loader might make of
the value, and itself; it configures nothing), so an init plus up
deployment with pi started in it is guarded. That .env is read through a
bounded read (non-blocking open, a regular file by fstat, at most 1 MiB),
so a FIFO or a /dev/zero link a cloned repository carries cannot hang or
grow the session. The cwd defaults
(./triggers.json, ./scoped-limits.json, ./projects.json) are guarded when
their key is set or when that folder is an init scaffold folder, and a
repository's own file of that name is otherwise never refused. A
deployment the panel cannot see at all is not guarded, a named limit. The tool set is
pinned to pi 0.99.1's allToolNames; pi's path rules (Unicode spaces, @,
~, file://, against ctx.cwd) are copied and pinned against its own
resolveToCwd. Identity is (dev, ino) for an existing target, and the
nearest existing ancestor plus the rest, fully case-folded (APFS folds a
long s, the Kelvin sign and the capital sharp s, measured), for a new
one, with every link followed; a resolution error blocks. A powershell
command matches a guarded name only as a folded path segment, trailing
dots and spaces included; an 8.3 short name is a named residual. A real pi session (faux
provider) shows a model write, a nested ctx.executeTool write and a
codemode write blocked, and an edit of .env and the pointer blocked. bash,
a built powershell name and a bash link in the same parallel batch stay
named residuals.

Commands and panel. /dispatch priorities (no reasons; zero-spend) and
/dispatch priorities set <id>=<weight> ..., in USAGE and
KNOWN_SUBCOMMANDS. The ALLOCATION view on b: the envelope, each entry's
floor, weight, share and spend with its key, the plan's per-project
reasons from alloc:plan only and every history cell (escaped, through the
control-byte gate), the newest 20 outcomes of alloc:log; r arms an in-frame y/n on the captured row and
reverts as operator-revert (no interval, no step, still a CAS); a banner
while an envelope-changed-externally row is newer than the last re-base or
applied plan; the hint rides the
spend divider's meta. PI_ENVELOPE_FILE joins resolvePaths, the panel's
.env keys and POINTER_ENV_ALLOWLIST; the setup wizard never writes it.
The panel's cron run counts and cost attribution also join a hand-fired
run of the trigger (manual:<id>:<millis>, issue #505).

Specs: REQ-ADMIN-VIA-PI-EXTENSION (the Statement names the three tools,
the Acceptance gains part 1's clauses) and DES-ADMIN-VIA-PI-EXTENSION
(the Decision names them, the view, the guard, rejected and residuals)
amended, completing #504's same-change spec acceptance across part A and
this part; REQ-DELEGATED-ALLOCATION and DES-DELEGATED-ALLOCATION-INSIDE-
ENVELOPE name part C; INT-ENVELOPE-FILE-CONTRACT, INT-PRIORITIES-PLAN-
CONTRACT and INT-DEPLOYMENT-POINTER-CONTRACT amended; SECURITY.md's
session residual widened. REQ-SCOPED-LIMITS, DES-FIRST-RUN-SETUP-WIZARD,
INT-HOST-REGISTRY-CONTRACT, INT-RUN-HISTORY-FILE-CONTRACT and
CONST-BUDGET-BEFORE-TOKENS unchanged, checked.

Refs #504

Signed-off-by: Rob Boerman <robboerman@live.nl>
@edgehero
edgehero force-pushed the feat/504-admin-surfaces branch from d4d16b4 to 33542d4 Compare October 4, 2026 14:22
@edgehero
edgehero merged commit 219deaf into main Oct 4, 2026
7 checks passed
@edgehero
edgehero deleted the feat/504-admin-surfaces branch October 4, 2026 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Delegated allocation: an agent splits the dollar budget between projects inside an operator envelope, with no keypress

1 participant