Skip to content

Commit d34ab57

Browse files
committed
fix(admin): console and panel sentences follow the venue a run used (#712)
Part 1 of #712. Every sentence the console and the /dispatch panel print about a run's venue is now derived from the worker's own tables, so none is wrong for local, podman, openshell or openshell-bind. - BACKENDS_TABLE gains serves: { liveTranscript, steering, recovery, sandboxShell }, each yes, no or image. A test holds it to the real sources: each bundle's binds and transcript mode, SANDBOX_LAUNCHERS, and every venue's preflight with and without recover. - The record gains liveSkipped (LIVE_SKIPPED_REASONS). It was only logged before. - Admin sentence tables (STEER_SKIPPED_TEXT, LIVE_SKIPPED_TEXT, SERVES_TEXT, CUSTODY_TEXT) are bolted to the worker enums. - The console prints the sandbox command only for a venue with a sandbox shell. Otherwise it gives the panel's own reason. - The secrets disclosure follows the custody of the trigger's venue: on openshell, placeholders in the agent, not values. - The live and progress "absent" sentences give the record's liveSkipped, else the venue or image cause. - The panel offers its steer keys only when the run's inbox is open, and both run views show steerSkipped as a sentence. - The setup wizard gates an openshell-only deployment on its gateway's engine, then on the openshell CLI and gateway, and offers or refuses the venue by name. - PI_SECRET_PROFILES env profiles are listed beside the overlay's. Adding a profile refuses, before the confirm, a name the env already declares and a resolver whose real path is outside the roots as written (the worker's own rule). A path this host cannot resolve gets a note. - run.recover with an explicit venue that cannot recover is refused at load and at write. Admin also judges the implicit venue against PI_BACKENDS[0]. - The comments claiming profile management lives "nowhere else" are corrected. Specs: DES-CONTAINER-BACKEND-REGISTRY, DES-OPERATOR-CONSOLE, DES-ADMIN-VIA-PI-EXTENSION, DES-FIRST-RUN-SETUP-WIZARD, DES-OPENSHELL-VENUE, INT-RUN-HISTORY-FILE-CONTRACT, INT-TRIGGERS-FILE-CONTRACT, REQ-OPERATOR-CONSOLE-SCREENS, REQ-DEPLOYMENT-BOOTSTRAP and REQ-RUN-RECOVERY are amended. Signed-off-by: Rob Boerman <robboerman@live.nl>
1 parent 18968da commit d34ab57

43 files changed

Lines changed: 1400 additions & 108 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎admin/src/console/actions.mjs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,8 @@ const configActions = [
679679
fileAction({
680680
name: "secret-profile-add",
681681
kind: "secret-profile-add",
682-
build: (body, ctx) => ctx.a.plans.planSecretProfileAdd({ name: str(body, "name", { required: true, max: 64 }), path: str(body, "path", { required: true, max: 4096 }), profiles: profilesOf(ctx), settingsFile: ctx.a.paths.settingsFile }),
682+
// Issue #712: the env-declared names and the resolver roots judged before the confirm, as `/dispatch secrets add` does.
683+
build: (body, ctx) => ctx.a.plans.planSecretProfileAdd({ name: str(body, "name", { required: true, max: 64 }), path: str(body, "path", { required: true, max: 4096 }), profiles: profilesOf(ctx), envProfiles: ctx.a.rm.envSecretProfiles?.(ctx.a.env?.PI_SECRET_PROFILES)?.profiles ?? {}, env: ctx.a.env ?? null, settingsFile: ctx.a.paths.settingsFile, ...(ctx.fs ? { fs: ctx.fs } : {}) }),
683684
target: (ctx) => ctx.a.paths.settingsFile,
684685
write: writeSettingsOf,
685686
}),
@@ -695,7 +696,7 @@ const configActions = [
695696
fileAction({
696697
name: "secret-bundle-add",
697698
kind: "secret-bundle-add",
698-
build: (body, ctx) => ctx.a.plans.planSecretBundleAdd({ name: str(body, "name", { required: true, max: 64 }), path: str(body, "path", { required: true, max: 4096 }), format: str(body, "format", { required: true, max: 16 }), bundles: bundleProfilesOf(ctx), envBundles: ctx.a.rm.envBundleProfiles(ctx.a.env?.PI_SECRET_BUNDLES).profiles, settingsFile: ctx.a.paths.settingsFile }),
699+
build: (body, ctx) => ctx.a.plans.planSecretBundleAdd({ name: str(body, "name", { required: true, max: 64 }), path: str(body, "path", { required: true, max: 4096 }), format: str(body, "format", { required: true, max: 16 }), bundles: bundleProfilesOf(ctx), envBundles: ctx.a.rm.envBundleProfiles(ctx.a.env?.PI_SECRET_BUNDLES).profiles, env: ctx.a.env ?? null, settingsFile: ctx.a.paths.settingsFile, ...(ctx.fs ? { fs: ctx.fs } : {}) }),
699700
target: (ctx) => ctx.a.paths.settingsFile,
700701
write: writeSettingsOf,
701702
}),

‎admin/src/console/panels.mjs‎

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ import {
5555
import { fmtCost } from "../panel.mjs";
5656
import { renderWhatIf } from "../render.mjs";
5757
import { recordedProgress } from "@edgehero/pi-dispatch/run-history";
58-
import { BUNDLE_SKIPPED_TEXT, bundleCountsText, bundleDeclaredText, bundleEndpointsText, bundleReceivedText, checkpointsText, forgeLinkOf, isProgressSnapshot, overlayBundleProfiles, progressStaleFor, recordBundle, recordRecovery, recoveredText, secretProfileNames, triggerBundleOf } from "../read-model.mjs";
58+
import { UNATTRIBUTED_BACKEND } from "@edgehero/pi-dispatch/backends";
59+
import { BUNDLE_SKIPPED_TEXT, LIVE_SKIPPED_TEXT, STEER_SKIPPED_TEXT, VENUE_MAY_NOT_STREAM, liveAbsentCause, progressAbsentText, recordSkips, sandboxShellRefusal, bundleCountsText, bundleDeclaredText, bundleEndpointsText, bundleReceivedText, checkpointsText, forgeLinkOf, isProgressSnapshot, overlayBundleProfiles, progressStaleFor, recordBundle, recordRecovery, recoveredText, secretProfileNames, triggerBundleOf } from "../read-model.mjs";
5960
import { RUN_FILTER_FIELDS } from "../run-filter.mjs";
6061
import { sessionKeyOfRecord } from "@edgehero/pi-dispatch/session-key";
6162
import { parkedVerdictOf } from "@edgehero/pi-dispatch/chat-mode";
@@ -518,8 +519,12 @@ function liveProgressBody(g, view) {
518519
])}<h3>Running now</h3>${running}<h3>Phase</h3>${phaseBody(g, operator)}<h3>Recent steps</h3>${table("Recent steps", ["Tool", "Status", { num: "Took" }, "At"], recent.map((r) => html`<tr${r.status === "ok" ? null : html` class="warn"`}><td class="mono">${r.tool}</td><td>${r.status}</td><td class="num">${stepDuration(r.durationMs)}</td><td>${utcText(r.at)}</td></tr>`), "No step has finished yet.")}`;
519520
}
520521

521-
/** Why a run has no progress snapshot: the key cannot tell these apart, so the panel names all three. */
522-
export const PROGRESS_ABSENT = "No live progress: not published (PI_RUN_PROGRESS=0), expired, or no step has been recorded yet.";
522+
/**
523+
* Why a run on a venue that streams has no progress snapshot: the key cannot tell these apart, so the panel names all
524+
* three. Issue #712: the panel says `progressAbsentText(record)`, `TRANSCRIPT_ABSENT`'s rule (progress is read off the
525+
* live transcript), so a run on a venue that does not stream it is not blamed on the switch alone.
526+
*/
527+
export const PROGRESS_ABSENT = progressAbsentText({ backend: "local" });
523528

524529
/**
525530
* The run page's "Progress" panel (issue #644). A run with a record shows the record's `progress` (and, while the key
@@ -566,13 +571,18 @@ export function runProgress(view) {
566571
const recordStart = typeof record.startedAt === "string" ? Date.parse(record.startedAt) : Number.NaN;
567572
const same = live && live.ended && Number.isFinite(recordEnd) && Number.isFinite(recordStart) && live.startedAt >= recordStart && live.startedAt <= recordEnd;
568573
const names = same && view.role === "operator" && live.phases.length > 0 ? html`<h3>Phases</h3>${phaseBody(live, true)}` : null;
569-
if (body === null && names === null) return titled(title, html`<p class="note">This run's record holds no progress (an older worker, or PI_RUN_PROGRESS=0).</p>`);
574+
// Issue #712: a record that names its live skip says it; one on a venue that does not always stream adds that cause.
575+
if (body === null && names === null) {
576+
const cause = liveAbsentCause(record);
577+
const why = cause?.skipped ? `it had no live transcript to read it from: ${cause.skipped}` : cause?.venue ? `an older worker, PI_RUN_PROGRESS=0, ${VENUE_MAY_NOT_STREAM}` : "an older worker, or PI_RUN_PROGRESS=0";
578+
return titled(title, html`<p class="note">This run's record holds no progress (${why}).</p>`);
579+
}
570580
return titled(title, html`${body ?? html`<p class="note">This run's record holds no progress.</p>`}${names}`);
571581
}
572582
if (live) return titled(title, liveProgressBody(live, view));
573583
if (g?.unreachable) return titled(title, html`<p class="warn">The run's progress could not be read (is Valkey reachable?).</p>`);
574584
if (g?.invalid) return titled(title, html`<p class="warn">The run's progress key is not one this console reads (a newer or older worker?).</p>`);
575-
return titled(title, html`<p class="note">${PROGRESS_ABSENT}</p>`);
585+
return titled(title, html`<p class="note">${progressAbsentText(record)}</p>`);
576586
}
577587

578588
// ---- run inbox (issue #646) ------------------------------------------------------------------------------------------
@@ -749,7 +759,26 @@ function chatDrawsSteer(view) {
749759
*/
750760
function runActions(r, view) {
751761
if (view.role !== "operator" || typeof r.jobId !== "string" || !RUN_ID_RE.test(r.jobId)) return null;
752-
return html`<h3>Actions</h3><p>Open its retained workspace on the host: <code class="mono">${`pi-dispatch sandbox ${r.jobId}`}</code></p>${canAct(view) ? actForm("rerun", { hidden: [["jobId", r.jobId]], label: "Preview a re-run" }) : null}`;
762+
// Issue #712: the command only for a venue with a sandbox shell (`serves.sandboxShell`, held to the worker's
763+
// SANDBOX_LAUNCHERS), and otherwise the reason the panel's sandbox pane gives, in its words (`sandboxShellRefusal`). A
764+
// record with no `backend` is a `local` run from before attribution (`UNATTRIBUTED_BACKEND`), as the sandbox reads it.
765+
const venue = typeof r.backend === "string" && r.backend !== "" ? r.backend : UNATTRIBUTED_BACKEND;
766+
const refused = sandboxShellRefusal(venue);
767+
const reopen = refused === null ? html`<p>Open its retained workspace on the host: <code class="mono">${`pi-dispatch sandbox ${r.jobId}`}</code></p>` : html`<p class="note">Its workspace is ${refused}.</p>`;
768+
return html`<h3>Actions</h3>${reopen}${canAct(view) ? actForm("rerun", { hidden: [["jobId", r.jobId]], label: "Preview a re-run" }) : null}`;
769+
}
770+
771+
/**
772+
* The run summary's skip rows (issue #712): "Steering" (why a steer-armed run took no message, the record's
773+
* `steerSkipped`) and "Live transcript" (why it had none, `liveSkipped`), each the closed enum and its sentence; no row
774+
* for a record that carries neither, so every other run renders byte-identical.
775+
*/
776+
function skipRows(r) {
777+
const { liveSkipped, steerSkipped } = recordSkips(r);
778+
return [
779+
["Steering", steerSkipped !== null ? `skipped (${steerSkipped}): ${STEER_SKIPPED_TEXT[steerSkipped]}` : null],
780+
["Live transcript", liveSkipped !== null ? `none (${liveSkipped}): ${LIVE_SKIPPED_TEXT[liveSkipped]}` : null],
781+
];
753782
}
754783

755784
/**
@@ -803,6 +832,7 @@ export const runSummary = withRun("Run", (r, view, source) => {
803832
...recoveryRows(r),
804833
["Host", cell(r.host)],
805834
["Venue", cell(r.backend)],
835+
...skipRows(r),
806836
["Project", cell(r.project)],
807837
...bundleRows(r),
808838
["Provider and model", [r.provider, r.model].filter(Boolean).join(" / ") || "-"],
@@ -1373,8 +1403,15 @@ function profilesEditor(view) {
13731403

13741404
export const configSecretProfiles = fromSnapshot("Secret profiles", (s, view) => {
13751405
const names = secretProfileNames(s.settings);
1376-
if (names.length === 0) return html`<p class="note">No secret profile is declared.</p>${profilesEditor(view)}`;
1377-
return html`<p class="note">Names only: a profile's resolver and its references stay in the settings file.</p><ul class="plain">${names.map((n) => html`<li class="mono">${n}</li>`)}</ul>${profilesEditor(view)}`;
1406+
// Issue #712: the PI_SECRET_PROFILES names too (the snapshot's `secretProfilesEnv`), since the worker merges both; a
1407+
// name in both is the worker's ambiguity, which refuses every secrets job, and is marked so.
1408+
const env = s.secretProfilesEnv ?? null;
1409+
const fromEnv = Array.isArray(env?.names) ? env.names : [];
1410+
const envNote = env?.malformed ? html`<p class="warn">PI_SECRET_PROFILES does not parse, so the worker refuses to boot until it is fixed.</p>` : null;
1411+
// "None" only where the console can see both sources; otherwise it names the one it read.
1412+
if (names.length === 0 && fromEnv.length === 0) return html`<p class="note">No secret profile is declared${env?.seen === true ? "" : " in the settings overlay"}.</p>${envNote}${profilesEditor(view)}`;
1413+
const overlayItem = (n) => (fromEnv.includes(n) ? html`<li class="mono warn">${n} (AMBIGUOUS: also in PI_SECRET_PROFILES, so the worker refuses every secrets job)</li>` : html`<li class="mono">${n}</li>`);
1414+
return html`<p class="note">Names only: a profile's resolver and its references stay in the settings file${fromEnv.length > 0 ? " or the .env" : ""}.</p><ul class="plain">${fromEnv.map((n) => html`<li class="mono">${n} (PI_SECRET_PROFILES)</li>`)}${names.map(overlayItem)}</ul>${envNote}${profilesEditor(view)}`;
13781415
});
13791416

13801417
/** The bundle resolvers editor (issue #704): a name, its resolver's absolute path and the format it prints. */

‎admin/src/console/transcript.mjs‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* Nothing here reaches `/api/state`, a hub panel or a list.
1616
*/
1717
import { LIVE_OUTCOMES, LIVE_TOOL_RE } from "@edgehero/pi-dispatch/live-transcript";
18-
import { TRANSCRIPT_ID_RE } from "../read-model.mjs";
18+
import { TRANSCRIPT_ID_RE, transcriptAbsentText } from "../read-model.mjs";
1919
import { html } from "./html.mjs";
2020

2121
/** The kinds that carry text worth folding: a tool call's arguments and a tool's result go in a `<details>`. */
@@ -101,8 +101,13 @@ export function transcriptEntryJson(entry) {
101101
};
102102
}
103103

104-
/** Why a run has no stream: the stream cannot tell these apart, so the page names all three. */
105-
export const TRANSCRIPT_ABSENT = "No live transcript: not published (PI_LIVE_TRANSCRIPTS=0), expired, or not started yet.";
104+
/**
105+
* Why a run on a venue that streams it has no stream: the stream cannot tell these apart, so the page names all three.
106+
* Issue #712: the page says `transcriptAbsentText(record)`, which is this sentence for such a venue, the record's named
107+
* skip when it has one (`liveSkipped`), and these three plus the venue's cause for a run on a venue that does not always
108+
* stream or whose venue the page cannot name yet (a running job has no record).
109+
*/
110+
export const TRANSCRIPT_ABSENT = transcriptAbsentText({ backend: "local" });
106111

107112
/**
108113
* The run page's "Live transcript" section (operator only), from the page data's `transcript` (the fleet reader's
@@ -118,7 +123,7 @@ export function runTranscriptSection(view) {
118123
if (typeof id !== "string" || id === "") return title;
119124
const meta = view?.data?.transcript ?? null;
120125
if (meta?.unreachable) return html`${title}<p class="warn">The live transcript could not be read (is Valkey reachable?). Reload to try again.</p>`;
121-
if (meta?.exists !== true) return html`${title}<p class="note">${TRANSCRIPT_ABSENT} Reload to check again.</p>`;
126+
if (meta?.exists !== true) return html`${title}<p class="note">${transcriptAbsentText(view?.data?.run?.record ?? null)} Reload to check again.</p>`;
122127
const note = html`<p class="note">${meta.ended ? `The run has ended (${meta.outcome ?? "unknown"}); its stream is kept 24 hours.` : "The run is publishing its transcript."}</p>`;
123128
return html`${title}${note}<p class="t-status" data-transcript-status role="status" aria-live="polite">not live</p><div class="transcript" data-transcript data-job="${id}" tabindex="0"></div>`;
124129
}

0 commit comments

Comments
 (0)