Skip to content
Merged
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ quota preservation and Code-owned upstream fallback repair are not planned.
## Native workflows

- **Code workbench:** one main view under a top bar of tabs (Generator, Accounts,
Sessions) and one **More** menu (Models, Setup, Options, Shortcuts); the panel
reads its inputs again on its own. The **generator** is five rows of plain
Sessions) and one **More** menu (Models, Setup, Options, Shortcuts, Tutorial); the panel
reads its inputs again on its own. A person's first open offers a short tutorial
that points at the real controls one at a time, once per person and replayable
from More. The **generator** is five rows of plain
words (lane, model, thinking, advisor, fallbacks) changed in place by pointer, keys
or wheel, each model shown by its short alias per tier, with **Defaults** and
**Revert** in its head and a readout line that says what a pointed word does,
Expand Down
46 changes: 43 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ All Code actions are `atyrode.code.<name>`:
| Configuration | `readConfiguration`, `initializeConfiguration`, `select`, `changeAccounts` |
| Catalog authoring | `stageCatalog`, `reviewCatalog`, `promoteCatalog`, `discardCatalog` |
| A person's checks | `readChecks`, `approveChecks`, `adoptCheck` |
| A person's tutorial | `readTutorial`, `seeTutorial` |
| Pure OMP input policy | `composeProbe`, `draftInventory`, `deriveCatalog`, `composeSession` |
| External classifier | `readServiceConfiguration`, `reviewServices`, `configureServices`, `suggest` |

Expand Down Expand Up @@ -94,6 +95,15 @@ provider the pool does not include, and `code_check_current` when the workspace
holds a list of that pool whose benchmark completed no earlier. It saves `selection`
(else the saved one) narrowed to the check's list, and leaves a staged list as it is.

Whether a person has seen the panel's tutorial is their own record too, kept the same
way at `tutorial/<person>`. `readTutorial({})` answers `{ keeps, seenAt }`: whether the
caller keeps the record at all, and when they first finished, skipped or answered the
tutorial's offer (null before). `seeTutorial({})` records it once and answers the same:
a record already there stands as it is, so two tabs or devices marking it agree. Neither
refuses a caller that keeps none: an agent, a service or a confined sign-in reads and
marks `keeps: false`, and the panel then keeps it in the browser instead. Both ask no
container capability and are container-graded, as the checks' doors are.

`CatalogDocumentSchema` is `{ schemaVersion: 1, models: [...] }`. Each model has
an explicit key, provider/id/API identity, tier, quota bucket, costs,
performance, context, thinking levels and image support. Capabilities are 1–4. Tier 0
Expand Down Expand Up @@ -287,7 +297,7 @@ empty prompt. Headless callers still pass a prompt.
## Workbench presentation

The main view is one panel. A **top bar** carries the views as tabs (Generator,
Accounts, Sessions) and one **More** menu (Models, Setup, Options, Shortcuts). The
Accounts, Sessions) and one **More** menu (Models, Setup, Options, Shortcuts, Tutorial). The
panel reads its inputs again on its own, so it has no Refresh button. The **generator**
holds five rows of plain words, a readout line, the cost and speed meters and the
launch row; **routing** stands beside it and
Expand Down Expand Up @@ -325,8 +335,9 @@ one, More reads **More · staged** and its item **Models · staged**, since noth
until it is reviewed there. When session options are set, More and its **Options** item
carry their summary ("· restricted", "· skills off", "· 2 skills"). **Shortcuts** opens a
native modal dialog that lists every key under the view it acts in (Generator, Views,
Accounts, Sessions, Anywhere), with `m`, `u` and `o` under Generator, where they act;
`Esc` or its Close button closes it and it owns its keys while open.
Accounts, Sessions, Tutorial, Anywhere), with `m`, `u` and `o` under Generator, where they act;
`Esc` or its Close button closes it and it owns its keys while open. **Tutorial** walks the
tutorial again (below).

The workbench reads its inputs again on its own: the workspace profile, OMP's setup,
defaults, bundled model list, skills and accounts, and the machine list, once a minute
Expand All @@ -346,6 +357,35 @@ cadence (below). Both cadences are one schedule: reads that come due together sh
pass that reads each observation once, and **Refresh now** (`r`) or opening or closing a
sheet reads everything in one pass and restarts both.

### The tutorial

On a person's first open of the panel, wherever they open it, a question asks "Welcome
to Code" with **Take the tour** and **Skip** (`Esc` and the backdrop skip), once the
generator shows its profile in the main view with no sheet open, nothing running, no
charge waiting and no other question open. It is offered to people only: an agent or a
service is never asked. Either answer marks the tutorial seen for that person
(`seeTutorial`, or the browser for a sign-in that keeps no record), so it is never offered
again; **Tutorial** in More walks it whenever they like. The walk starts on the generator
(a sheet closes and the view goes back to it) and keeps to the steps the panel shows
there, so its count holds when a step opens another view: the profile's rows, the
readout, Defaults and Revert, routing, usage, the machine, the launch, the Accounts and
Sessions tabs, and More. Each step draws a thin ring in the accent around its control and
a card beside it (under it where the panel shows room, else over it), both laid over the
panel so nothing in it moves, and both kept to the part of the panel the window shows: the
step's name, where the walk stands ("3 of 10"), one sentence, and **Next** (**Done** at
the last), **Back** and **Skip**; `→` and `←` step, `Esc` skips, and a held key steps once.
Each step's card is its own, so moving to it moves focus to its Next and it is read out,
and its control is scrolled into view. Routing and usage are pointed at where the layout
puts them: beside the generator, else their tab or Show control. A step may be tried:
choosing a word in the rows, or opening Accounts, goes on by itself once done since the
step began, and Next always works too; Back onto a step another view hides brings the
generator back. The walk launches nothing and spends nothing; a word tried changes the
rows as any choice does, which Revert puts back. A step whose control is not shown (another
view or a sheet has the panel), or any step while a popover of the panel is open (More's
menu, the machine list), hides its card until that changes. The walk's end, finished or
skipped, puts focus on More, where it is replayed, so a second `↵` of a double press opens
a menu rather than launching.

### The generator

Rows run top to bottom: **lead**, **model**, **thinking**, **advisor** and
Expand Down
29 changes: 3 additions & 26 deletions plugins/code/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { CatalogDocumentSchema, DomainError, SelectionSchema, type CatalogDocume
import { clampSelection } from "../domain/routing.ts";
import { ChecksSchema, MAX_CHECKS, VerificationProvenanceSchema, digest, id, revision, type ActionInput, type Checks,
type Configuration, type ModelCheck, type VerificationProvenance } from "./contract.ts";
import { CodeRefusal, digestOf, type CodeContext } from "./context.ts";
import { CodeRefusal, type CodeContext } from "./context.ts";
import { bound, keeperOf, personOf } from "./person.ts";
import { commitConfiguration, expectRevision, poolIdentity, readConfiguration, requireConfiguration } from "./state.ts";

/*
Expand All @@ -15,7 +16,7 @@ import { commitConfiguration, expectRevision, poolIdentity, readConfiguration, r
also kept for the person who checked it, under the exact pool it was verified with and the budget
it was derived under, and any workspace whose saved choices select that pool adopts it instead of
spending another check. The person's standing approval of what checks spend lives beside them.
Both are keyed by the caller's own principal: nobody reads or writes another person's, and an
Both are the person's own records (person.ts): nobody reads or writes another person's, and an
agent, a service or a token confined to one container keeps none. Workspaces keep their own
copies, so a `runSession` caller still reads a workspace.
*/
Expand All @@ -33,33 +34,9 @@ const StoredApprovalSchema = z.strictObject({
/** Plugin storage holds at most this many bytes per value (plugin-kit `MAX_STORAGE_VALUE_BYTES`). */
const MAX_VALUE_BYTES = 64 << 10;

/** Why a caller keeps no checks: not a person, or a person whose sign-in reaches less than the whole workspace tree. */
type Keeper = { person: string } | { refused: "person_required" | "checks_confined" };
/**
* The caller's records are filed under a digest of their principal, cut to 40 hex characters so a
* check's key (`checks/<person>/<budget>/<pool>`) stays within the 128 characters plugin storage
* keys may have; every record also names its principal, which every read compares. Only a person
* whose sign-in reaches the workspace root keeps checks, asked where an unconfined credential is
* anchored: a token confined to one container, or by an authority ceiling, keeps none, since a
* check and the approval to spend on checks are the person's own, never one container's.
*/
async function keeperOf(ctx: CodeContext): Promise<Keeper> {
if (ctx.auth.principal.kind !== "human") return { refused: "person_required" };
if (ctx.auth.containerScope !== null || !(await ctx.auth.allows("containers:read"))) return { refused: "checks_confined" };
return { person: digestOf({ principalId: ctx.auth.principal.id }).slice(0, 40) };
}
async function personOf(ctx: CodeContext): Promise<string | null> {
const keeper = await keeperOf(ctx);
return "person" in keeper ? keeper.person : null;
}
const checkKey = (person: string, budget: Budget, pool: string) => `checks/${person}/${budget}/${pool}`;
const approvalKey = (person: string) => `approvals/${person}`;

/** A stored record that names another principal, or another key than its own, is corruption, never someone's data. */
function bound<T extends { principalId: string }>(ctx: CodeContext, record: T): T {
if (record.principalId !== ctx.auth.principal.id) throw new CodeRefusal("invalid_configuration");
return record;
}
async function readApproval(ctx: CodeContext, person: string) {
const raw = await ctx.storage.get(approvalKey(person));
return raw === null ? null : { raw, approval: bound(ctx, StoredApprovalSchema.parse(JSON.parse(raw))) };
Expand Down
10 changes: 10 additions & 0 deletions plugins/code/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ export const ChecksSchema = z.strictObject({
checks: z.array(ModelCheckSchema).max(MAX_CHECKS),
});
export type Checks = z.infer<typeof ChecksSchema>;
/**
* WHETHER A PERSON HAS SEEN THE PANEL'S TUTORIAL. `keeps` says whether the caller keeps the record
* at all (false for an agent, a service or a confined sign-in, whose browser keeps it instead);
* `seenAt` is when they first answered its offer or ended a walk, null before.
*/
export const TutorialSchema = z.strictObject({ keeps: z.boolean(), seenAt: epochMilliseconds.nullable() });
export type Tutorial = z.infer<typeof TutorialSchema>;
/** The prompt bound is OMP's, in bytes, because the prompt reaches the machine as one entry
* of the job input map the hub bounds (`PROMPT_MAX_BYTES`, 44 KiB). Code takes the schema
* itself rather than the number, so the two can never disagree. */
Expand Down Expand Up @@ -254,6 +261,9 @@ export const rootActionSchemas = {
readChecks: { input: z.strictObject({}), result: ChecksSchema },
// `requests: 0` withdraws the approval, so every later check asks again.
approveChecks: { input: z.strictObject({ expectedRevision: revision, requests: ChecksSchema.shape.approvedRequests }), result: ChecksSchema },
// The caller's own tutorial record: read it, or record that they have seen it (idempotent).
readTutorial: { input: z.strictObject({}), result: TutorialSchema },
seeTutorial: { input: z.strictObject({}), result: TutorialSchema },
// Puts the caller's check of the workspace's present pool, derived under `budget`, in use, with `selection` (else the saved
// one) narrowed to it. `poolIdentityDigest` is the pool the caller found a check for; Code recomputes the pool and refuses any other.
adoptCheck: { input: RevisionWorkspaceSchema.extend({ accounts: AccountsObservationSchema, poolIdentityDigest: digest,
Expand Down
36 changes: 23 additions & 13 deletions plugins/code/generator/decision-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@ import type { RefusalView, StatusAction, Vocabulary } from "./statement-model.ts
import type { VerificationMode } from "./verification.ts";

/*
* WHAT A PRESS WAITS ON THE PERSON FOR, as one question: a model check's charge to approve, a
* reviewed session that differs from what the panel showed, a check that changed the profile, or a
* refused Launch and its fixes. Pure, so which question is asked, which choices it offers, which is
* the way out and what answering does are tested apart from the dialog that asks it
* (decision-dialog.tsx). Only a press the person made asks: a check the panel runs on its own never does.
* WHAT THE PANEL WAITS ON THE PERSON FOR, as one question: a model check's charge to approve, a
* reviewed session that differs from what the panel showed, a check that changed the profile, a
* refused Launch and its fixes, or, on a person's first open, whether to take the tutorial. Pure, so
* which question is asked, which choices it offers, which is the way out and what answering does are
* tested apart from the dialog that asks it (decision-dialog.tsx). Only a press the person made asks
* about a check: a check the panel runs on its own never does.
*/

/** What a choice does: approve or cancel the waiting charge, take the launch's own press, leave the question, or run a fix. */
/** What a choice does: approve or cancel the waiting charge, take the launch's own press, start the tutorial, leave the question, or run a fix. */
export type DecisionDo =
| { readonly kind: "approve" }
| { readonly kind: "cancel" }
| { readonly kind: "launch" }
| { readonly kind: "tour" }
| { readonly kind: "dismiss" }
| { readonly kind: "fix"; readonly action: Extract<StatusAction, { kind: "fix" }> };
export type DecisionChoice = { readonly label: string; readonly does: DecisionDo };
export type Decision = {
readonly kind: "charge" | "differs" | "narrowed" | "refused";
readonly kind: "charge" | "differs" | "narrowed" | "refused" | "tutorial";
readonly title: string;
/** What is asked and why, a plain sentence each. */
readonly text: readonly string[];
Expand All @@ -28,6 +30,8 @@ export type Decision = {
/** The choices in reading order. The first is the next action; the last is the way out, which Esc and the backdrop take, and which never spends or launches. */
readonly choices: readonly DecisionChoice[];
};
/** The questions a press waits on, which `decisionFor` asks and `answer` answers; the tutorial's offer is the workbench's own. */
export type PressDecision = Decision & { readonly kind: Exclude<Decision["kind"], "tutorial"> };
/** A check's charge as it waits: the requests in all and per provider. */
export type ChargeFacts = { readonly requests: number; readonly providers: readonly { readonly provider: string; readonly requests: number }[] };

Expand All @@ -53,7 +57,7 @@ export type DecisionFacts = {
* struck lane, never from a check the panel runs on its own), a review that differs, a check that
* changed the profile, then a refused press; null when nothing waits on the person.
*/
export function decisionFor(facts: DecisionFacts, vocab: Vocabulary): Decision | null {
export function decisionFor(facts: DecisionFacts, vocab: Vocabulary): PressDecision | null {
if (facts.charge && facts.mode !== null && facts.mode !== "background") return chargeDecision(facts.charge, facts.mode, facts.keepsChecks, vocab);
if (facts.differs && facts.ready) return differsDecision(facts.differs);
if (facts.narrowed && facts.ready) return narrowedDecision(facts.narrowed);
Expand All @@ -75,7 +79,7 @@ export type DecisionEffect =
* only with the launch's press, and Not now drops it, so the next press reviews again. A narrowed
* stop and a refused press end their question, then run what was chosen.
*/
export function answer(decision: Decision, choice: DecisionChoice): DecisionEffect[] {
export function answer(decision: PressDecision, choice: DecisionChoice): DecisionEffect[] {
const does = choice.does;
const then: DecisionEffect[] = does.kind === "launch" ? [{ kind: "launch" }] : does.kind === "fix" ? [{ kind: "fix", action: does.action }] : [];
switch (decision.kind) {
Expand All @@ -93,7 +97,7 @@ const plural = (count: number, word: string) => `${count} ${word}${count === 1 ?
* once lets later checks run without asking (only for a person who keeps checks). A charge of nothing
* asks nothing: it says so, offers the accounts, and its way out cancels the check.
*/
function chargeDecision(charge: ChargeFacts, mode: Exclude<VerificationMode, "background">, keepsChecks: boolean, vocab: Vocabulary): Decision {
function chargeDecision(charge: ChargeFacts, mode: Exclude<VerificationMode, "background">, keepsChecks: boolean, vocab: Vocabulary): PressDecision {
if (charge.requests === 0) {
return { kind: "charge", title: "Nothing to check", text: ["None of the models your included accounts offer can be checked."], items: [],
choices: [{ label: "Show accounts", does: { kind: "fix", action: { kind: "fix", key: "accounts", label: "Show accounts", fix: { kind: "open", place: "accounts" } } } },
Expand All @@ -118,19 +122,25 @@ function chargeDecision(charge: ChargeFacts, mode: Exclude<VerificationMode, "ba
}

/** A Launch press whose session review differs from what the panel showed: what differs, and both ways on. */
function differsDecision(differs: readonly string[]): Decision {
function differsDecision(differs: readonly string[]): PressDecision {
return { kind: "differs", title: "What will run differs", text: ["The session Code is about to start is not quite what the panel shows."], items: differs,
choices: [{ label: "Launch it", does: { kind: "launch" } }, { label: "Not now", does: { kind: "dismiss" } }] };
}

/** A Launch press whose model check narrowed the profile to what answered: the settings it moved, and both ways on. */
function narrowedDecision(settings: readonly string[]): Decision {
function narrowedDecision(settings: readonly string[]): PressDecision {
return { kind: "narrowed", title: "The model check changed your profile", text: ["Some models didn't answer, so the check changed these settings."], items: settings,
choices: [{ label: "Launch", does: { kind: "launch" } }, { label: "Not now", does: { kind: "dismiss" } }] };
}

/** A refused Launch press: what stops it, why, the fixes the launch line offers for that refusal, and Close. */
function refusedDecision(view: RefusalView): Decision {
function refusedDecision(view: RefusalView): PressDecision {
return { kind: "refused", title: view.title, text: view.why ? [view.why] : [], items: [],
choices: [...view.fixes.map(action => ({ label: action.label, does: { kind: "fix", action } }) as const), { label: "Close", does: { kind: "dismiss" } }] };
}

/** A person's first open: what Code is, and whether to take the short tutorial now. Skip is the way out; More replays it. */
export function tutorialOffer(): Decision {
return { kind: "tutorial", title: "Welcome to Code", text: ["Code starts OMP coding sessions with the models you choose. A short tour shows where everything is."],
items: [], choices: [{ label: "Take the tour", does: { kind: "tour" } }, { label: "Skip", does: { kind: "dismiss" } }] };
}
Loading
Loading