Přeskočit na obsah

Tato stránka byla automaticky přeložena z angličtiny, jediného jazyka, ve kterém se tento web píše. Číst originál.

Provedení

rovina provádění: spuštění schopnosti jako práce, kterou lze sledovat — model, životní cyklus, typovaný protokol událostí, živý kanál a jeho záruky, co nabízí které rozhraní, limity a co se stane u každého z nich, a co záměrně nedělá

Vykresleno z docs/EXECUTIONS.md — stejný Markdown, jaký ukazuje GitHub.

Jak spustitelný soubor v Rustu pod apps/majordomus-cli/ spouští schopnost jako práci, kterou lze sledovat: model, životní cyklus, typovaný protokol událostí, živý kanál, co nabízí každé rozhraní a co záměrně nedělá. Chování tak, jak je implementováno a testováno; kde se tento dokument a spustitelný soubor liší, dokument se mýlí a mění se ve stejném commitu. Rozhodnutí je ADR 22; pravidla jsou project.execution-events-are-typed a project.executions-carry-no-secret; registr, na kterém běží, je CAPABILITIES.md.

flowchart TD
  declaration["ONE CANONICAL DECLARATION<br/><small>capability! { … handler: verify }.cancellable()</small>"]
  registry["CAPABILITY REGISTRY<br/><small>the same registry MCP, HTTP, OpenAPI, the CLI and the Cockpit read</small>"]
  engine["EXECUTION ENGINE<br/><small>validate · create · queue · run on a worker · catch · complete</small>"]
  store["EXECUTION STORE<br/><small>the snapshot, the retained events, the subscribers, all bounded</small>"]
  declaration --> registry --> engine --> store
  subgraph projections["DERIVED PROJECTIONS"]
    channel["the live channel<br/><small>WebSocket</small>"]
    get["executions.get"]
    events["executions.events"]
    pages["the Cockpit's Executions pages"]
    run["majordomus run"]
    tools["MCP tools"]
  end
  store --> projections

Neexistuje registr akcí, definice akce ani druhá implementace čehokoli. Provedení je volání schopnosti, kterému proces dal identitu — a od I1501 jedna rovina provádění na checkout, a ne jedna na proces: majordomus run hlásí serveru, který checkout obsluhuje, takže historie, kterou povrch ukazuje, je historie, kterou člověk vytvořil.

Model

Prováděníjedno volání jedné schopnosti, s id, životním cyklem, vstupem tak, jak byl uložen, výstupem nebo chybou, kroky, průběhem, diagnostikou, údajem, kdo o něj požádal, a repozitářem, proti kterému běželo
Událost provedeníjeden typovaný fakt o provedení, v obálce s verzí protokolu a souvislým pořadovým číslem
Politika prováděníco spuštění schopnosti znamená: její účinek a zda se dvě smějí překrývat, klasifikováno podle druhu; a zda její zrušení něčeho dosáhne, což může říct jen její handler
Průběhhandle, který nese každý kontext; mimo provedení tichý, takže handler se píše jednou pro každého volajícího

Životní cyklus

stateDiagram-v2
  queued --> running
  queued --> cancelled
  running --> succeeded
  running --> failed
  running --> cancelling
  cancelling --> cancelled
  cancelling --> succeeded: it finished before it noticed
  cancelling --> failed

ExecutionState::may_move_to je celá smlouva a úložiště odmítne cokoli jiného, takže klient, který přečte konečný stav, ho už nikdy neuvidí se pohnout. Neexistuje starting: engine v procesu převezme provedení a vstoupí do svého handleru ve stejném okamžiku a stav, kterého nelze dosáhnout, je stav, který by musel zvládat každý klient a nikdy by ho neviděl.

Co tu není

Provádění nepřežije server, který jej drží. Není žádná trvalá fronta, žádný retry, žádný plán a žádné provádění, které přežije restart. Každý limit je typován na execution::store::Limits — kolik provádění se pamatuje, kolik událostí každé, jak dlouhý smí být řádek logu, kolik jich běží najednou, jak daleko smí odběratel zaostat — a každý má definované chování na hranici místo nenapsaného. Nic nad úložištěm nejmenuje Mutex, takže trvalé úložiště je záměna, ne přepis.

Spuštění jednoho

Jakoukoli spustitelnou schopnost registru lze spustit jako provádění. Neexistuje druhý seznam toho, co smí běžet: odpovídá už capabilities.list a vlastní politika execution deskriptoru říká, co její spuštění znamená.

majordomus run health.report                       # run it here, follow it to the end, and
                                                    # report it to the server serving this checkout
majordomus run objects.verify --input '{"limit":50}' --format json
majordomus executions list                          # what the server serving this repository ran
                                                    # or was told about by a run
majordomus executions show   <id>
majordomus executions events <id> --after 42
majordomus executions cancel <id>
majordomus executions protocol                      # the live channel's contract
POST /api/v1/executions/start        { "capability": "objects.verify", "input": { … } }
GET  /api/v1/executions              ?state=running&capability=objects.verify&limit=50
GET  /api/v1/executions/get          ?id=x-…
GET  /api/v1/executions/events       ?id=x-…&after=42&limit=500
POST /api/v1/executions/cancel       { "id": "x-…" }
GET  /api/v1/executions/protocol
GET  /api/v1/executions/demonstrate  ?steps=3&delay_ms=200
GET  /api/v1/objects/verify          ?kind=rule&limit=50
POST /api/v1/executions/report       { "id": "x-…", "pid": 4242, "events": [ … ], … }  what `run` sends

Přes MCP jsou tytéž schopnosti nástroji majordomus_execution_start, majordomus_executions, majordomus_execution, majordomus_execution_events, majordomus_execution_cancel a majordomus_execution_protocol, přičemž majordomus://executions a majordomus://executions/protocol jsou zdroje. Agent tak objeví, spustí, sleduje a přečte provedení, aniž by parsoval cokoli, na co se měl dívat člověk. executions.report nemá žádný nástroj MCP ani vlastní příkaz: je to to, co běh posílá serveru, nikoli něco, o co žádá agent nebo člověk.

start odpoví okamžitě s provedením a odkazy, jak ho sledovat; nic nečeká na handler. Stav v této první odpovědi je queued, což je to, čím je: tento server odpovídá 200 pravdou místo 202 slibem.

Jedna rovina na checkout: majordomus run a server

majordomus run se provádí ve vlastním procesu — cena a způsoby selhání běhu jsou stejné se serverem i bez něj — a nejdřív klade jednu otázku: obsluhuje už nějaký server tento checkout (jeho pronájem pojmenovává URL a server tam odpovídá za tento kořen)?

  • Server odpovídá. Běh se mu hlásí přes executions.report: svým popisem, pid procesu a svými událostmi po poslední posloupnosti, kterou server převzal, nejpozději každé dvě sekundy a brzy poté, co se cokoli stane. Server ho drží v témže úložišti jako vlastní, takže executions list, /cockpit/executions, vlastní stránka provedení, executions.events a živý kanál ho ukazují jako cokoli, co server spustil, s actor: {"kind": "cli", "pid": …}. Každá událost se aplikuje jednou a v pořadí a odpověď říká, jak daleko server došel (through), takže ztracenou, useknutou nebo dvakrát odeslanou zprávu napraví ta další. Zastavení vyžádané od serveru (executions.cancel, tlačítko Cockpitu) se vrátí v příští odpovědi a běh zruší své vlastní provedení; zrušení zůstává kooperativní. Výstup nebo vstup přesahující mez rámce živého kanálu putuje jako {"withheld": "too_large", …} a zůstává v procesu běhu, který ho vypíše.
  • Žádný server neodpovídá. Běh proběhne jako dříve a řekne to jednou na stderr: žádný server tento repozitář neposkytuje, takže toto provádění je viditelné jen tomuto procesu. Nic se pro něj nespouští: běh nikdy nespustí server.
  • Server uprostřed běhu přestane odpovídat. První neúspěšné hlášení, nebo tři za sebou po jednom úspěšném, způsobí, že běh na stderr oznámí, že je od té chvíle viditelný jen tomuto procesu, a pokračuje. Server, který 30 sekund neuslyší nic, provedení ukončí chybou unavailable a zprávou pojmenovávající proces, místo aby ho ukazoval jako věčně běžící; zabitý běh tedy nikdy není přízrakem v seznamu.

Co se tím nemění: práce běží tam, kde je běh, nikdy v serveru, a historie je stále pamětí serveru — končí s tímto serverem.

Protokol událostí

Jedna obálka, uzavřená množina payloadů, verze. type a data jsou jediní dva členové, kteří se mění, takže klient přepíná podle jednoho řetězce.

{
  "schema_version": "1",
  "event_id": "x-20260908T010203Z-0a1b2c3d#17",
  "execution_id": "x-20260908T010203Z-0a1b2c3d",
  "sequence": 17,
  "timestamp": "2026-09-08T01:02:07Z",
  "type": "execution.progress",
  "data": { "current": 17, "total": 42, "message": "137 file(s) read" }
}
typkdy
execution.createdexistuje; nese schopnost a vstup tak, jak byly uloženy
execution.queuedčeká na pracovníka a kolik jich je před ním
execution.startedhandler byl vstoupen
execution.progressjak daleko, s celkem, když ho handler zná
execution.step.started, execution.step.completedbyla vstoupena pojmenovaná fáze a jak dopadla
execution.logřádek výstupu, z handleru nebo z procesu, který spustil
execution.diagnosticstrukturovaný nález, odlišný od výsledku
execution.cancelling, execution.cancelledbyl požádán, aby se zastavil; zastavil se
execution.completedskončilo, s tím, co handler vrátil
execution.failedskončilo s typovanou chybou a korelačním id

Výstup putuje na execution.completed místo na vlastní události, takže žádný klient nemůže přečíst „succeeded“ a nenajít výsledek. sequence je souvislé a v rámci provedení začíná od 1: klient, který viděl n, ví, že mu něco uniklo, když další, co čte, není n + 1, a o mezeru si řekne kurzorem, místo aby znovu načítal celý svět.

Schéma se odvozuje z enumu v Rustu pomocí schemars, přesně jako vstupní a výstupní schémata každé schopnosti, a servíruje se — s celým kontraktem — přes executions.protocol. Nic se nezapisuje dvakrát.

Klient musí ignorovat type, které nezná. Díky tomu lze přidat variantu bez změny verze; schema_version se pohne jen při změně, kterou klient verze 1 nemohl bezpečně ignorovat.

Živý kanál

GET /events                                  every execution of this process, live
GET /events?execution=<id>                   one execution: its retained history, then live
GET /events?execution=<id>&after=<sequence>   the same, from a cursor — what a reconnect sends
GET /events?execution=<a>&execution=<b>       several executions: each one's history, then live

Nativní WebSocket (RFC 6455), povýšený na stejném socketu, na kterém se servíruje zbytek povrchu: jeden proces, jeden port, jedna startovní cesta, žádný druhý daemon a žádný Socket.IO. Odběr je cíl požadavku, takže není co posílat a není co potvrzovat.

Server zapisuje jeden JSON dokument na každý textový rámec: buď událost provádění, nebo jednu ze čtyř řídicích zpráv — stream.ready (první rámec: co bylo odebíráno, kolik bylo přehráno, zda už byla předchozí historie zahozena, interval heartbeatu), stream.heartbeat (kanál je tichý a živý), stream.lagged (tento klient zaostal a události byly zahozeny místo bufferování; znovu načtěte snapshot a historii od poslední viděné sekvence) a stream.closing.

Pořadí a mezery. Spojení se nejprve přihlásí k odběru a teprve poté přehrává, a v živém streamu přeskakuje vše, co přehrávání už zapsalo. Přehrávání se čte po stránkách po 1000, dokud není dohnáno, takže kurzor zaostávající o víc než jednu stránku stále dostane každou uchovanou událost po něm. Mezi historií a streamem tedy není mezera a nic se nezapisuje dvakrát. Klient Cockpitu (share/cockpit/socket.js) kontroluje obojí na své straně: událost na posledním viděném pořadovém čísle nebo pod ním se zahodí a na kanálu sledujícím jedno provedení událost za dalším číslem způsobí opětovné připojení s after=<last seen>, takže server přehraje, co bylo zmeškáno; táž mezera podruhé se přečte z historie přes HTTP.

Několik provádění, každé se svým kurzorem. sequence je husté pro každé provádění, takže spojení, které jmenuje několik provádění, drží kurzor pro každé a události každého deduplikuje proti jeho vlastnímu. Jeden after spouští každý kurzor: každé jmenované provádění se přehrává stránku po stránce, dokud není dohnáno, od událostí po něm, a provádění jmenované dvakrát se sleduje jednou. stream.ready počítá celé přehrání v replayed a truncated je pravda, když historie kteréhokoli sledovaného provádění už byla zahozena. Klient, který se znovu připojuje k několika prováděním, nepošle žádný kurzor a pro každé provádění zahodí, co už má; share/cockpit/socket.js Cockpitu dělá přesně to. Spojení s jedním prováděním posílá sekvenci, kterou naposledy viděl, jako vždy.

Heartbeat a odpojení. Server pinguje tiché spojení každých dvacet sekund a nikdy nečte: tiny_http při upgradu předá jedinou hodnotu Read + Write, kterou nelze rozdělit, takže by čtecí vlákno drželo zámek, který potřebuje zapisovač. Klient, který zmizí, se pozná, když selže další zápis, což heartbeat zaručuje; co klient pošle, se ignoruje. Uzavření se tedy zjistí do jednoho heartbeatu, ne okamžitě. Ping je řídicí rámec, který prohlížeč stránce nikdy neukáže, takže stejný interval zapisuje i textový rámec stream.heartbeat; klient Cockpitu zahodí kanál, který dva a půl heartbeatu neřekl nic, a znovu se připojí od svého kurzoru, místo aby napůl otevřené spojení ukazoval jako živé.

Hodnota příliš velká pro rámec. Jeden rámec má nejvýše 256 KiB. Událost, jejíž rámec by byl větší — output dokončeného provedení, input vytvořeného — se zapíše s touto hodnotou nahrazenou odkazem, {"withheld": "too_large", "bytes": n, "limit": 262144, "self": "/api/v1/executions/get?id=…"}, a kanál zůstane otevřený; snímek, který jmenuje, nese celou hodnotu.

Zpětný tlak. Odběratel, který zaostane za svou frontou, se nikdy nečeká: události se zahazují jen pro tohoto klienta, je mu to řečeno stream.lagged a provedení nezpomalí nikdo, kdo je sleduje. Odběratel, jehož čtenář zmizel, se zapomene při další události.

Odmítnutí. Prosté GET /events odpoví 426 a řekne, kde jsou tytéž události čitelné přes HTTP. Požadavek z jiného originu se odmítne 403 před handshake — stránka jinde může otevřít WebSocket na loopback server a same-origin policy to nezastaví. Stejně tak upgrade z tohoto stroje adresovaný na jméno hostitele, které není ani adresa, ani localhost: to je stránka, jejíž doména byla donucena překládat se sem (DNS rebinding), odmítnutá před upgradem, jako ji odmítá každá jiná routa. Parametr execution, který není id provedení, je 400, odmítnutý podle tvaru dřív, než se cokoli hledá.

V Cockpitu

/cockpit/executions vypisuje, co tento server spustil a co mu majordomus run nahlásil, s počty vedle; /cockpit/executions/<id> je jedno provedení na stabilní URL, kterou člověk může znovu načíst, sdílet lokálně nebo jich otevřít několik v kartách.

Obě stránky jsou kompletní HTML dříve, než běží jakýkoli skript — stav, kroky, průběh, diagnostika, log, výstup a vstup jsou to, co vykreslil server — a skripty přidávají, že přestávají být fotografií. socket.js je jediné místo v prohlížeči, které otevírá socket: znovu se připojuje s exponenciálním backoffem, stropem a jitterem, znovu se přihlásí od sekvence, při níž byla stránka vykreslena, a ukazuje spojení slovem a tvarem, ne jen barvou. Každou URL, kterou používá, vykreslil do značek server; v prohlížeči není zapsána žádná cesta.

Na stránce samotné schopnosti stojí Spustit jako provádění vedle přímého runneru. Oba používají stejný generovaný formulář a tlačítko žádá o potvrzení tehdy — a jen tehdy — když efekt deskriptoru říká, že se něco mění. Událost, kterou stránka nezná, zobrazí, nezahodí.

Řádek logu se na stránku dostává přes textContent, nikdy ne innerHTML, a server z něj už odstranil řídicí znaky a terminálové escape sekvence. Log se posouvá uvnitř vlastního boxu a je omezen v prohlížeči i na serveru, takže dlouhá cesta v řádku se zalomí a nerozšíří stránku.

Přidání schopnosti, kterou lze sledovat

Nic navíc. Napište blok capability!, jak jej popisuje CAPABILITIES.md, a lze jej spustit jako provádění v okamžiku, kdy existuje. Dvě volitelné věci jej činí lepším:

  1. Report. ctx.progress nese step, step_done, progress, log, stream a diagnostic. Všechna lze volat zdarma mimo provedení, takže handler reportuje bezpodmínečně a píše se jednou pro každého volajícího.
  2. Deklarujte ji jako zrušitelnou — .cancellable() za blokem capability! — když handler kontroluje ctx.progress.cancelled() a zastaví se. Cockpit pak nabídne tlačítko Zrušit, které něco dělá, executions.cancel to řekne ve své odpovědi a handler, který svůj příznak ignoruje, nikdy nevytvoří ovládací prvek, který lže.
fn verify(ctx: &Context, input: VerifyInput) -> Result<Report, CapabilityError> {
    let p = &ctx.progress;
    p.step("read", "Reading the layer");
    for (n, file) in files.iter().enumerate() {
        if p.cancelled() {
            return Err(p.cancellation());
        }
        p.progress(n as u64 + 1, Some(total), format!("read {}", file.path));
    }
    p.step_done("read", true, None);
    Ok(report)
}

Cachovaná schopnost spuštěná jako provedení se spustí. Cache činí jednu odpověď k nerozeznání od druhé, což je správné pro odpověď a špatné pro záznam práce: provedení odpovězené z cache by nehlásilo žádné kroky a tvrdilo by úspěch v něčem, co se nestalo. Context::execute_observed je cesta, kterou engine jde, a přeskakuje cache v obou směrech.

Bezpečnost

Běží jen registrované schopnosti. Rovina bere id schopnosti a vstup; neexistuje žádný endpoint, který by bral příkaz, a vstup, který smí klient poslat, jsou dvě pole, jejichž schéma je v dokumentu OpenAPI. Schopnost, která by jednou spouštěla proces, by byla blokem capability! s vlastním seznamem povolených, přezkoumaným jako každý jiný.

Izolace repozitáře. Proces obsluhuje repozitář, který přečetl při startu, a každé provedení je jím označeno. Nic nemůže spustit schopnost proti jinému checkoutu a nic, co klient pošle, nejmenuje adresář.

Tajemství. Hodnota, kterou vstupní schéma označí jako citlivou — format: "password", writeOnly nebo x-majordomus-sensitive — se nahradí jednou, v enginu, před uložením vstupu, takže ji nemůže nést žádný snímek, událost, historie, stránka, výpis ani generovaný dokument. O tom nikdy nerozhoduje název pole: password_policy může být dokument a redaktor shodující se podle názvu ho zničí, zatímco další pole napsané jinak mine.

Selhání. Handler, který zpanikaří, se zachytí: provedení selže s internal, korelačním id a návrhem a samotná panika jde do logu tohoto procesu, kde ji operátor přečte. Žádný backtrace ani cesta uvnitř tohoto crate se ke klientovi nedostane.

Origin. Živý kanál a každá trasa měnící stav odmítnou požadavek z jiného originu. Content-security policy Cockpitu povoluje spojení jen s tímto originem.

Limity a co se stane u každého

limitvýchozína mezi
zapamatovaná provedení200nejstarší dokončené se zapomene; běžící nikdy
událostí uchovaných na jedno provádění2000nejstarší se zahazují a snapshot i každá stránka historie uvádějí truncated
řádek logu2000 znakůzkráceno, se značkou
provedení běžící současně4další čekají queued a execution.queued říká, kolik jich je před nimi
fronta odběratele512události se pro tohoto klienta zahazují a klient se dozví stream.lagged
živé kanály64další spojení je odmítnuto 503 s důvodem
jeden snímek živého kanálu256 KiBvýstup nebo vstup nad ním se zapisuje jako odkaz na snapshot
sériová schopnost (příkaz)po jednomdruhé provádění téhož čeká na první
události v jednom reportu z běhu500 a tři čtvrtiny meze těla 1 MiBodmítnuto nad 500; běh pošle zbytek v dalším reportu
ticho nahlášeného provedení30 s (heartbeat běhu je 2 s)server ho selže s unavailable; samotný běh se nedotkne

Výchozí hodnoty jsou pro vývojový server, který člověk sleduje, a executions.protocol odpovídá těmi, se kterými tento proces běží.

Když něco selže

zprávavýznamnáprava
no capability 'x'; the executable ones are listed by capabilities.listid není v registrukontrola majordomus capabilities list
not executable: 'x' is a resource: it is read, never executedbyl pojmenován deklarativní objektčtěte pomocí objects.get
invalid input: /steps: "many" is not of type "integer"vstup nesplňuje vlastní schéma schopnostischéma je v dokumentu OpenAPI a na stránce schopnosti v Cockpitu
no execution x-… in this processtady nikdy neběželo, nebo se na to zapomněloprovedení žije v serveru, který ho drží; executions.list říká, co se pamatuje
majordomus run: no server is serving this repository, so this execution is visible to this process only…run nenašel žádný server odpovídající pro tento checkoutspusťte majordomus serve před během, abyste ho tam viděli; běh žádný nespouští
majordomus run: the server at … did not take this run's report (…)server odmítl nebo neodpověděl; starší server nemá executions.reportběh pokračuje; důvod v závorkách je důvodem serveru
unavailable: process N, which ran this execution and reported it here, has not reported for … sběh byl zabit nebo ztratil spojení se serverem na 30 svýsledek má terminál, který ho spustil
426 … /events is a WebSocketbyl otevřen prostým GETotevřete ji klientem WebSocket, nebo čtěte executions.events
403 … a WebSocket from origin '…' is refusedstránka z jiného originu se pokusila sledovat tento serverotevřít Cockpit z vlastní adresy tohoto serveru
stream.laggedtento klient nestíhalznovu načíst snapshot a historii od poslední viděné sekvence
provedení, které nehlásí žádné krokyschopnost žádnou nehlásíto je fakt o schopnosti, ne o rovině; executions.demonstrate ukazuje, jak vypadá taková, která to dělá

Stabilita

status
stavový automat, jeho přechody a odmítnutí neplatnéhoověřeno chováním (execution::model, execution::store)
meze úložiště a chování u každé: vytěsnění, zkrácení, zpoždění, zmizelý odběratelbehaviorálně ověřeno (execution::store)
redakce od schématu, přes reference, pole a mapybehaviorálně ověřeno (execution::redact)
handshake, rámování a SHA-1 proti publikovaným vektorůmbehaviorálně ověřeno (http::ws)
start, sledování, znovunačtení, znovupřipojení s kurzorem, zrušení, selhání — přes skutečný socket se skutečným klientem WebSocketbehaviorálně ověřeno (tests/executions.rs)
existující schopnost pozorovatelná bez druhé implementacebehaviorálně ověřeno (tests/executions.rs)
provedení, na které cache nikdy neodpovědělabehaviorálně ověřeno (tests/executions.rs)
celá cesta zvenčí spustitelného souborubehaviorálně ověřeno (test/cases/100_execution_plane.sh)
běh hlášený serveru: aplikován jednou a v pořadí, obnoven po mezeře, zastavení vráceno, mlčící reportér označen jako selhanýbehaviorálně ověřeno (execution::store)
běh vypsaný serverem, dokud probíhá, stejný výsledek po něm, zastavení vyžádané od serveru, které doputuje k běhu, prostý řádek bez serverubehaviorálně ověřeno dotazováním serveru (test/cases/3024_a_run_is_seen_by_the_server.sh)
trvanlivost, opakování, plánování, provedení přežívající svůj procesneimplementováno a Limits je místo, kde by se nahradilo trvalé úložiště
schopnost podložená procesem, jehož výstup jde jako logové události stdout/stderrneimplementováno; model událostí pro to nese proudy
čtení rámců klienta, a tedy okamžité uzavřeníneimplementováno; důvod je v http::ws

Prohledává názvy a popisy všech stránek, které tento web publikuje.