Skip to content

Commit fd383af

Browse files
committed
sync(bfmono): fix(gambit): add codex trust preflight for workbench chat (+19 more) (bfmono@2bfffaaae)
This PR is an automated gambitmono sync of bfmono Gambit packages. - Source: `packages/gambit/` - Core: `packages/gambit/packages/gambit-core/` - bfmono rev: 2bfffaaae Changes: - 2bfffaaae fix(gambit): add codex trust preflight for workbench chat - 9c16505d6 chore(gambit): cut 0.8.5-rc.11 - b4d5cdaef fix(simulator-ui): prevent feedback reason text from being clobbered - 84952a652 fix(gambit-verify): align verify turn labels and stabilize initial run filtering - c56b7f52f feat(gambit): improve verify report controls and harden concurrent calibrate persistence - beb9435c0 feat(gambit-simulator-ui): extend listbox trigger and popover options - 25f9fdcfc fix(gambit-simulator-ui): align verify outlier chip semantics and display - a010b0ee1 feat(gambit-simulator-ui): add verify outliers to workbench chat chips - 383f2500a refactor(simulator-ui): replace nested ternaries in main routing - 13c4c8c22 fix(gambit): preserve shared references in safe session serialization - ae392aa24 feat(gambit-simulator-ui): add grader error chips to workbench chat - 1de6b335c fix(gambit): clamp deck-level maxTurns bounds in test run selection - 01d7abbb9 fix(gambit): default verify tab bootstrap flag to enabled - f3d186c7b fix(gambit): include extension schemas in exports and default serve to restored workspace - a83b7cbe7 fix(gambit): move unbounded build timeout to deck opt-in - acb2de627 fix(gambit): avoid strict json_schema 400s in openrouter responses - 8aba573b6 fix(gambit-simulator-ui): treat errored calibrate runs as failed - ca2028cf8 fix(gambit): prevent circular trace crashes in workspace test run API - 7e41517e5 fix(gambit): make build assistant run timeout unbounded - 24341143d feat(gambit): add deterministic verify fixture seeding Do not edit this repo directly; make changes in bfmono and re-run the sync.
1 parent 49df3f0 commit fd383af

4 files changed

Lines changed: 615 additions & 0 deletions

File tree

simulator-ui/src/WorkbenchDrawer.tsx

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,35 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) {
9595
const [chatHistoryLoading, setChatHistoryLoading] = useState(false);
9696
const [chatHistoryError, setChatHistoryError] = useState<string | null>(null);
9797
const [copiedStatePath, setCopiedStatePath] = useState(false);
98+
const [copiedCodexLoginCommand, setCopiedCodexLoginCommand] = useState(false);
99+
const [codexTrustPending, setCodexTrustPending] = useState(false);
100+
const [codexTrustError, setCodexTrustError] = useState<string | null>(null);
101+
const [codexTrustSuccess, setCodexTrustSuccess] = useState<string | null>(
102+
null,
103+
);
104+
const [codexWorkspaceWriteEnabled, setCodexWorkspaceWriteEnabled] = useState<
105+
boolean | null
106+
>(null);
107+
const [codexWorkspaceLoggedIn, setCodexWorkspaceLoggedIn] = useState<
108+
boolean | null
109+
>(null);
110+
const [codexLoginStatusText, setCodexLoginStatusText] = useState<
111+
string | null
112+
>(null);
113+
const [codexTrustedPath, setCodexTrustedPath] = useState<string | null>(null);
114+
const [codexTrustOverlayDismissed, setCodexTrustOverlayDismissed] = useState(
115+
false,
116+
);
98117
const initializedChipTrackingRef = useRef(false);
99118
const seenRatingChipIdsRef = useRef(new Set<string>());
100119
const seenFlagChipIdsRef = useRef(new Set<string>());
120+
const showCodexTrustOverlay = (codexWorkspaceWriteEnabled === false ||
121+
codexWorkspaceLoggedIn === false) &&
122+
!codexTrustOverlayDismissed || Boolean(codexTrustError);
123+
const workspaceIdForTrust = (sessionId ?? run.id) || undefined;
124+
const codexLoginCommand = codexTrustedPath
125+
? `CODEX_HOME="${codexTrustedPath}/.codex" codex login`
126+
: 'CODEX_HOME="<workspace>/.codex" codex login';
101127
const resolvedStatePath = useMemo(() => {
102128
if (statePath) return statePath;
103129
const meta = sessionDetail?.meta;
@@ -327,8 +353,67 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) {
327353
initializedChipTrackingRef.current = false;
328354
seenRatingChipIdsRef.current.clear();
329355
seenFlagChipIdsRef.current.clear();
356+
setCodexTrustPending(false);
357+
setCodexTrustError(null);
358+
setCodexTrustSuccess(null);
359+
setCodexWorkspaceWriteEnabled(null);
360+
setCodexWorkspaceLoggedIn(null);
361+
setCodexLoginStatusText(null);
362+
setCodexTrustedPath(null);
363+
setCopiedCodexLoginCommand(false);
364+
setCodexTrustOverlayDismissed(false);
330365
}, [sessionId]);
331366

367+
useEffect(() => {
368+
if (!open) return;
369+
if (!workspaceIdForTrust) return;
370+
let canceled = false;
371+
setCodexTrustError(null);
372+
fetch(
373+
`/api/codex/trust-workspace?workspaceId=${
374+
encodeURIComponent(workspaceIdForTrust)
375+
}`,
376+
)
377+
.then(async (response) => {
378+
const payload = await response.json() as {
379+
ok?: boolean;
380+
trusted?: boolean;
381+
writeEnabled?: boolean;
382+
codexLoggedIn?: boolean;
383+
codexLoginStatus?: string;
384+
trustedPath?: string;
385+
error?: string;
386+
};
387+
if (!response.ok || payload.ok === false) {
388+
throw new Error(payload.error || response.statusText);
389+
}
390+
if (canceled) return;
391+
setCodexWorkspaceWriteEnabled(payload.writeEnabled === true);
392+
setCodexWorkspaceLoggedIn(payload.codexLoggedIn === true);
393+
setCodexLoginStatusText(
394+
typeof payload.codexLoginStatus === "string"
395+
? payload.codexLoginStatus
396+
: null,
397+
);
398+
setCodexTrustedPath(
399+
typeof payload.trustedPath === "string" ? payload.trustedPath : null,
400+
);
401+
})
402+
.catch((err) => {
403+
if (canceled) return;
404+
setCodexWorkspaceWriteEnabled(null);
405+
setCodexWorkspaceLoggedIn(null);
406+
setCodexLoginStatusText(null);
407+
setCodexTrustError(err instanceof Error ? err.message : String(err));
408+
})
409+
.finally(() => {
410+
if (canceled) return;
411+
});
412+
return () => {
413+
canceled = true;
414+
};
415+
}, [open, workspaceIdForTrust]);
416+
332417
useEffect(() => {
333418
if (loading) return;
334419
const currentRatingChipIds = new Set(
@@ -482,6 +567,11 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) {
482567
window.setTimeout(() => setCopiedStatePath(false), 1200);
483568
};
484569
}, [resolvedStatePath]);
570+
const handleCopyCodexLoginCommand = useCallback(() => {
571+
navigator.clipboard?.writeText(codexLoginCommand);
572+
setCopiedCodexLoginCommand(true);
573+
window.setTimeout(() => setCopiedCodexLoginCommand(false), 1200);
574+
}, [codexLoginCommand]);
485575
useEffect(() => {
486576
if (!open) return;
487577
if (!onClose) return;
@@ -493,6 +583,90 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) {
493583
window.addEventListener("keydown", handler);
494584
return () => window.removeEventListener("keydown", handler);
495585
}, [onClose, open]);
586+
const trustWorkspaceInCodex = useCallback(async () => {
587+
setCodexTrustPending(true);
588+
setCodexTrustError(null);
589+
setCodexTrustSuccess(null);
590+
try {
591+
const statusResponse = await fetch(
592+
`/api/codex/trust-workspace?workspaceId=${
593+
encodeURIComponent(workspaceIdForTrust ?? "")
594+
}`,
595+
);
596+
const statusPayload = await statusResponse.json() as {
597+
ok?: boolean;
598+
trusted?: boolean;
599+
writeEnabled?: boolean;
600+
codexLoggedIn?: boolean;
601+
codexLoginStatus?: string;
602+
trustedPath?: string;
603+
error?: string;
604+
};
605+
if (!statusResponse.ok || statusPayload.ok === false) {
606+
throw new Error(statusPayload.error || statusResponse.statusText);
607+
}
608+
if (
609+
statusPayload.writeEnabled === true &&
610+
statusPayload.codexLoggedIn === true
611+
) {
612+
setCodexWorkspaceWriteEnabled(true);
613+
setCodexWorkspaceLoggedIn(true);
614+
setCodexTrustSuccess(
615+
"Workspace is already configured for Codex writes.",
616+
);
617+
setCodexTrustOverlayDismissed(true);
618+
return;
619+
}
620+
setCodexWorkspaceWriteEnabled(statusPayload.writeEnabled === true);
621+
setCodexWorkspaceLoggedIn(statusPayload.codexLoggedIn === true);
622+
setCodexLoginStatusText(
623+
typeof statusPayload.codexLoginStatus === "string"
624+
? statusPayload.codexLoginStatus
625+
: null,
626+
);
627+
setCodexTrustedPath(
628+
typeof statusPayload.trustedPath === "string"
629+
? statusPayload.trustedPath
630+
: null,
631+
);
632+
633+
const response = await fetch("/api/codex/trust-workspace", {
634+
method: "POST",
635+
headers: { "content-type": "application/json" },
636+
body: JSON.stringify({ workspaceId: workspaceIdForTrust }),
637+
});
638+
const payload = await response.json() as {
639+
ok?: boolean;
640+
error?: string;
641+
trustedPath?: string;
642+
writeEnabled?: boolean;
643+
codexLoggedIn?: boolean;
644+
codexLoginStatus?: string;
645+
};
646+
if (!response.ok || payload.ok === false) {
647+
throw new Error(payload.error || response.statusText);
648+
}
649+
const trustedPath = typeof payload.trustedPath === "string"
650+
? payload.trustedPath
651+
: "workspace";
652+
setCodexTrustSuccess(`Codex write enabled for: ${trustedPath}`);
653+
setCodexWorkspaceWriteEnabled(payload.writeEnabled === true);
654+
setCodexWorkspaceLoggedIn(payload.codexLoggedIn === true);
655+
setCodexLoginStatusText(
656+
typeof payload.codexLoginStatus === "string"
657+
? payload.codexLoginStatus
658+
: null,
659+
);
660+
setCodexTrustedPath(
661+
typeof payload.trustedPath === "string" ? payload.trustedPath : null,
662+
);
663+
setCodexTrustOverlayDismissed(payload.codexLoggedIn === true);
664+
} catch (err) {
665+
setCodexTrustError(err instanceof Error ? err.message : String(err));
666+
} finally {
667+
setCodexTrustPending(false);
668+
}
669+
}, [workspaceIdForTrust]);
496670
if (!open) return null;
497671
return (
498672
<aside className="workbench-drawer-docked" role="dialog">
@@ -605,6 +779,81 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) {
605779
chatHistoryOpen ? " is-history" : ""
606780
}`}
607781
>
782+
{showCodexTrustOverlay && (
783+
<div className="workbench-chat-readonly-overlay">
784+
<div className="workbench-chat-readonly-card">
785+
<h3 className="workbench-chat-readonly-title">
786+
Codex setup required
787+
</h3>
788+
{codexWorkspaceWriteEnabled === false && (
789+
<p className="workbench-chat-readonly-copy">
790+
Codex write access is disabled for this
791+
workspace. Trust this workspace to enable file
792+
edits.
793+
</p>
794+
)}
795+
<div className="workbench-chat-readonly-actions">
796+
{codexWorkspaceWriteEnabled === false && (
797+
<Button
798+
variant="primary"
799+
onClick={() => trustWorkspaceInCodex()}
800+
disabled={codexTrustPending}
801+
>
802+
{codexTrustPending
803+
? "Trusting..."
804+
: "Trust workspace"}
805+
</Button>
806+
)}
807+
</div>
808+
{codexWorkspaceLoggedIn === false && (
809+
<>
810+
<p className="workbench-chat-readonly-copy">
811+
Codex login is required for this workspace.
812+
</p>
813+
<p className="workbench-chat-readonly-copy">
814+
Run this in this workspace to authenticate
815+
Codex, then restart Gambit.
816+
</p>
817+
<div className="workbench-chat-command-row">
818+
<pre className="workbench-chat-command-code">
819+
<code>{codexLoginCommand}</code>
820+
</pre>
821+
<Button
822+
variant="secondary"
823+
size="small"
824+
onClick={handleCopyCodexLoginCommand}
825+
>
826+
<Icon
827+
name={copiedCodexLoginCommand
828+
? "copied"
829+
: "copy"}
830+
size={14}
831+
/>
832+
{copiedCodexLoginCommand
833+
? "Copied"
834+
: "Copy"}
835+
</Button>
836+
</div>
837+
</>
838+
)}
839+
{codexLoginStatusText &&
840+
!/^not logged in$/i.test(
841+
codexLoginStatusText.trim(),
842+
) && <Callout>{codexLoginStatusText}</Callout>}
843+
{codexTrustError && (
844+
<div className="error">{codexTrustError}</div>
845+
)}
846+
<Button
847+
variant="secondary"
848+
onClick={() =>
849+
setCodexTrustOverlayDismissed(true)}
850+
disabled={codexTrustPending}
851+
>
852+
Dismiss
853+
</Button>
854+
</div>
855+
</div>
856+
)}
608857
<Chat
609858
composerChips={composerChips}
610859
onComposerChipsChange={onComposerChipsChange}
@@ -621,6 +870,7 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) {
621870
defaultOpen: false,
622871
content: (
623872
<div className="workbench-ratings">
873+
{codexTrustSuccess && <Callout>{codexTrustSuccess}</Callout>}
624874
{showCopyStatePath && handleCopyStatePath && (
625875
<>
626876
<Button variant="secondary" onClick={handleCopyStatePath}>

simulator-ui/src/styles.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2269,6 +2269,67 @@ code:not(pre *) {
22692269
.workbench-chat-current.is-history {
22702270
transform: translateX(85%);
22712271
}
2272+
.workbench-chat-readonly-overlay {
2273+
position: absolute;
2274+
inset: 0;
2275+
z-index: 2;
2276+
background: rgba(248, 250, 252, 0.88);
2277+
display: flex;
2278+
align-items: center;
2279+
justify-content: center;
2280+
padding: 16px;
2281+
}
2282+
.workbench-chat-readonly-card {
2283+
background: var(--color-surface);
2284+
border: 1px solid var(--color-border);
2285+
border-radius: calc(16px * var(--corner-radius-scale, 1));
2286+
corner-shape: squircle;
2287+
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
2288+
width: 100%;
2289+
max-width: 640px;
2290+
padding: 16px;
2291+
display: flex;
2292+
flex-direction: column;
2293+
gap: 10px;
2294+
text-align: left;
2295+
}
2296+
.workbench-chat-readonly-title {
2297+
margin: 0;
2298+
font-size: 16px;
2299+
}
2300+
.workbench-chat-readonly-copy {
2301+
margin: 0;
2302+
font-size: 13px;
2303+
color: var(--color-text-muted);
2304+
}
2305+
.workbench-chat-readonly-actions {
2306+
display: flex;
2307+
flex-wrap: wrap;
2308+
align-items: center;
2309+
gap: 8px;
2310+
}
2311+
.workbench-chat-command-row {
2312+
display: flex;
2313+
align-items: flex-start;
2314+
gap: 8px;
2315+
}
2316+
.workbench-chat-command-code {
2317+
margin: 0;
2318+
flex: 1;
2319+
max-width: 100%;
2320+
padding: 10px 12px;
2321+
border: 1px solid var(--color-border);
2322+
border-radius: calc(10px * var(--corner-radius-scale, 1));
2323+
corner-shape: squircle;
2324+
background: var(--color-surface-muted);
2325+
overflow-x: auto;
2326+
}
2327+
.workbench-chat-command-code code {
2328+
font-size: 12px;
2329+
line-height: 1.4;
2330+
white-space: pre-wrap;
2331+
word-break: break-all;
2332+
}
22722333
.gds-accordion .gds-accordion-open-only {
22732334
display: none;
22742335
}

src/providers/codex.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,9 +616,14 @@ function defaultCommandRunner(input: {
616616
onStdoutLine?: (line: string) => void;
617617
}): Promise<CommandOutput> {
618618
const codexBin = Deno.env.get(CODEX_BIN_ENV)?.trim() || "codex";
619+
const env = Deno.env.toObject();
620+
if (!env.CODEX_HOME || env.CODEX_HOME.trim().length === 0) {
621+
env.CODEX_HOME = path.join(input.cwd, ".codex");
622+
}
619623
const child = new Deno.Command(codexBin, {
620624
args: input.args,
621625
cwd: input.cwd,
626+
env,
622627
stdout: "piped",
623628
stderr: "piped",
624629
}).spawn();

0 commit comments

Comments
 (0)