[codex] Structure client cloud cryptography errors - #3395

Closed
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors
Closed

[codex] Structure client cloud cryptography errors#3395
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary\n\n- replace message-bearing DPoP errors with Schema errors carrying structured operation context\n- preserve exact lower-level causes and add typed key-format/mismatch causes for validation failures\n- remove valueless error constructor wrappers from web/mobile DPoP and mobile relay token storage\n- annotate best-effort token-store logging with the failing operation\n\n## Validation\n\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})

RUN /Users/julius/.codex/worktrees/client-cloud-crypto-errors

Test Files 3 passed (3)
Tests 10 passed (10)
Start at 08:11:56
Duration 376ms (transform 78ms, setup 0ms, import 632ms, tests 86ms, environment 0ms)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
�[1m�[94mpass:�[39m�[0m All 1848 files are correctly formatted �[2m(775ms, 16 threads)�[0m
! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1250:8]
1249 | () => ({
1250 | ,-> p({ node: _node, children, ...props }) {
1251 | | return <p {...props}>{renderSkillInlineMarkdownChildren(children, skills)}

;
1252 | -> }, 1253 | li({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1253:9]
1252 | },
1253 | ,-> li({ node, children, ...props }) {
1254 | | const listItemStart = node?.position?.start.offset;
1255 | | const markerOffset =
1256 | | typeof listItemStart === "number" ? findTaskListMarkerOffset(text, listItemStart) : null;
1257 | | return (
1258 | | <li {...props} data-task-marker-offset={markerOffset ?? undefined}>
1259 | | {renderSkillInlineMarkdownChildren(children, skills)}
1260 | |
1261 | | );
1262 | -> }, 1263 | input({ node: _node, type, checked, disabled: _disabled, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1263:12]
1262 | },
1263 | ,-> input({ node: _node, type, checked, disabled: _disabled, ...props }) {
1264 | | if (type !== "checkbox" || !onTaskListChange) {
1265 | | return (
1266 | | <input
1267 | | {...props}
1268 | | type={type}
1269 | | checked={checked}
1270 | | disabled={_disabled}
1271 | | readOnly={type === "checkbox"}
1272 | | />
1273 | | );
1274 | | }
1275 | | return (
1276 | | <input
1277 | | {...props}
1278 | | type="checkbox"
1279 | | name="markdown-task"
1280 | | aria-label="Toggle task"
1281 | | checked={checked}
1282 | | onChange={(event) => {
1283 | | const markerOffset = Number(
1284 | | event.currentTarget.closest("li")?.dataset.taskMarkerOffset,
1285 | | );
1286 | | if (!Number.isSafeInteger(markerOffset)) return;
1287 | | onTaskListChange({ markerOffset, checked: event.currentTarget.checked });
1288 | | }}
1289 | | />
1290 | | );
1291 | -> }, 1292 | a({ node, href, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1292:8]
1291 | },
1292 | ,-> a({ node, href, children, ...props }) {
1293 | | const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : "";
1294 | | const fileLinkMeta = normalizedHref ? markdownFileLinkMetaByHref.get(normalizedHref) : null;
1295 | | if (!fileLinkMeta) {
1296 | | const faviconHost = resolveExternalLinkHost(href);
1297 | | const isSameDocumentLink = href?.startsWith("#") ?? false;
1298 | | const onClick = props.onClick;
1299 | | const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime();
1300 | | const link = (
1301 | | <a
1302 | | {...props}
1303 | | href={href}
1304 | | target={isSameDocumentLink ? undefined : "_blank"}
1305 | | rel={isSameDocumentLink ? undefined : "noopener noreferrer"}
1306 | | onClick={(event) => {
1307 | | onClick?.(event);
1308 | | if (isSameDocumentLink && href) {
1309 | | handleMarkdownFragmentClick(event, href);
1310 | | }
1311 | | }}
1312 | | onContextMenu={(event) => {
1313 | | if (!canOpenInPreview || !href) return;
1314 | | event.preventDefault();
1315 | | event.stopPropagation();
1316 | | const api = readLocalApi();
1317 | | if (!api) return;
1318 | | void api.contextMenu
1319 | | .show(
1320 | | [
1321 | | { id: "open-in-browser", label: "Open in integrated browser" },
1322 | | { id: "open-external", label: "Open in system browser" },
1323 | | ] as const,
1324 | | { x: event.clientX, y: event.clientY },
1325 | | )
1326 | | .then((clicked) => {
1327 | | if (clicked === "open-in-browser") {
1328 | | void openExternalLinkInPreview(href);
1329 | | return;
1330 | | }
1331 | | if (clicked === "open-external") return api.shell.openExternal(href);
1332 | | })
1333 | | .catch(() => undefined);
1334 | | }}
1335 | | >
1336 | | {faviconHost ? (
1337 | |
1338 | | {children}
1339 | |
1340 | | ) : (
1341 | | children
1342 | | )}
1343 | |
1344 | | );
1345 | | if (!faviconHost || !href) {
1346 | | return link;
1347 | | }
1348 | | return (
1349 | |
1350 | |
1351 | | <TooltipPopup
1352 | | side="top"
1353 | | className="max-w-[min(36rem,calc(100vw-2rem))] whitespace-normal leading-tight wrap-anywhere"
1354 | | >
1355 | | {href}
1356 | |
1357 | |
1358 | | );
1359 | | }
1360 | |
1361 | | const parentSuffix = fileLinkParentSuffixByPath.get(fileLinkMeta.filePath);
1362 | | const labelParts = [fileLinkMeta.basename];
1363 | | if (typeof parentSuffix === "string" && parentSuffix.length > 0) {
1364 | | labelParts.push(parentSuffix);
1365 | | }
1366 | | if (fileLinkMeta.line) {
1367 | | labelParts.push(
1368 | | L${fileLinkMeta.line}${fileLinkMeta.column ? :C${fileLinkMeta.column} : ""},
1369 | | );
1370 | | }
1371 | |
1372 | | return (
1373 | | <MarkdownFileLink
1374 | | href={fileLinkMeta.targetPath}
1375 | | targetPath={fileLinkMeta.targetPath}
1376 | | iconPath={fileLinkMeta.filePath}
1377 | | displayPath={fileLinkMeta.displayPath}
1378 | | workspaceRelativePath={fileLinkMeta.workspaceRelativePath}
1379 | | line={fileLinkMeta.line}
1380 | | label={labelParts.join(" · ")}
1381 | | copyMarkdown={[${fileLinkMeta.basename}](${normalizedHref})}
1382 | | theme={resolvedTheme}
1383 | | threadRef={threadRef}
1384 | | onOpen={openInPreferredEditor}
1385 | | onOpenInBrowser={
1386 | | threadRef &&
1387 | | isPreviewSupportedInRuntime() &&
1388 | | isBrowserPreviewFile(fileLinkMeta.filePath)
1389 | | ? () => openMarkdownFileInPreview(fileLinkMeta.filePath)
1390 | | : undefined
1391 | | }
1392 | | className={props.className}
1393 | | />
1394 | | );
1395 | -> }, 1396 | table({ node: _node, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1396:12]
1395 | },
1396 | ,-> table({ node: _node, ...props }) {
1397 | | return <MarkdownTable {...props} />;
1398 | -> }, 1399 | details({ node: _node, children, open: detailsOpen }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1399:14]
1398 | },
1399 | ,-> details({ node: _node, children, open: detailsOpen }) {
1400 | | return {children};
1401 | -> }, 1402 | pre({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1402:10]
1401 | },
1402 | ,-> pre({ node, children, ...props }) {
1403 | | const codeBlock = extractCodeBlock(children);
1404 | | if (!codeBlock) {
1405 | | return <pre {...props}>{children};
1406 | | }
1407 | |
1408 | | const language = extractFenceLanguage(codeBlock.className);
1409 | | const fenceTitle = extractFenceTitle(extractPreCodeMeta(node));
1410 | | return (
1411 | | <MarkdownCodeBlock
1412 | | code={codeBlock.code}
1413 | | language={language}
1414 | | fenceTitle={fenceTitle}
1415 | | theme={resolvedTheme}
1416 | | >
1417 | | <CodeHighlightErrorBoundary fallback={<pre {...props}>{children}}>
1418 | | <Suspense fallback={<pre {...props}>{children}}>
1419 | | <SuspenseShikiCodeBlock
1420 | | className={codeBlock.className}
1421 | | code={codeBlock.code}
1422 | | themeName={diffThemeName}
1423 | | isStreaming={isStreaming}
1424 | | />
1425 | |
1426 | |
1427 | |
1428 | | );
1429 | -> }, 1430 | }), ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/review/ReviewSheet.tsx:249:24]
248 | },
249 | ,-> headerTitle: () => (
250 | | <View style={{ alignItems: "center" }}>
251 | | <NativeText
252 | | numberOfLines={1}
253 | | style={{
254 | | fontFamily: "DMSans_700Bold",
255 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
256 | | fontWeight: "900",
257 | | color: headerForeground,
258 | | letterSpacing: -0.4,
259 | | }}
260 | | >
261 | | Files Changed
262 | |
263 | | <View
264 | | style={{
265 | | flexDirection: "row",
266 | | alignItems: "center",
267 | | justifyContent: "center",
268 | | gap: 6,
269 | | flexWrap: "wrap",
270 | | }}
271 | | >
272 | | {headerDiffSummary.additions && headerDiffSummary.deletions ? (
273 | | <>
274 | | <NativeText
275 | | style={{
276 | | fontFamily: "DMSans_700Bold",
277 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
278 | | fontWeight: "700",
279 | | color: "#16a34a",
280 | | }}
281 | | >
282 | | {headerDiffSummary.additions}
283 | |
284 | | <NativeText
285 | | style={{
286 | | fontFamily: "DMSans_700Bold",
287 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
288 | | fontWeight: "700",
289 | | color: "#e11d48",
290 | | }}
291 | | >
292 | | {headerDiffSummary.deletions}
293 | |
294 | | {pendingReviewCommentCount > 0 ? (
295 | | <NativeText
296 | | style={{
297 | | fontFamily: "DMSans_700Bold",
298 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
299 | | fontWeight: "700",
300 | | color: "#b45309",
301 | | }}
302 | | >
303 | | {pendingReviewCommentCount} pending
304 | |
305 | | ) : null}
306 | | </>
307 | | ) : (
308 | | <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
309 | | <NativeText
310 | | numberOfLines={1}
311 | | style={{
312 | | fontFamily: "DMSans_700Bold",
313 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
314 | | fontWeight: "700",
315 | | color: headerMuted,
316 | | }}
317 | | >
318 | | {selectedSection?.title ?? "Review changes"}
319 | |
320 | | {pendingReviewCommentCount > 0 ? (
321 | | <NativeText
322 | | style={{
323 | | fontFamily: "DMSans_700Bold",
324 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
325 | | fontWeight: "700",
326 | | color: "#b45309",
327 | | }}
328 | | >
329 | | {pendingReviewCommentCount} pending
330 | |
331 | | ) : null}
332 | |
333 | | )}
334 | |
335 | |
336 | -> ), 337 | }} ----
help: Move this component definition out of the parent component ReviewSheet and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/threads/ThreadRouteScreen.tsx:383:24]
382 | headerBackTitle: "",
383 | ,-> headerTitle: () => (
384 | | <Pressable
385 | | style={{ alignItems: "center", maxWidth: 200 }}
386 | | onLongPress={() => {
387 | | // TODO: trigger rename modal
388 | | }}
389 | | >
390 | | <RNText
391 | | numberOfLines={1}
392 | | style={{
393 | | fontFamily: "DMSans_700Bold",
394 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
395 | | fontWeight: "900",
396 | | color: foregroundColor,
397 | | letterSpacing: -0.4,
398 | | }}
399 | | >
400 | | {selectedThread.title}
401 | |
402 | | <RNText
403 | | numberOfLines={1}
404 | | style={{
405 | | fontFamily: "DMSans_700Bold",
406 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
407 | | fontWeight: "700",
408 | | color: secondaryFg,
409 | | letterSpacing: 0.3,
410 | | }}
411 | | >
412 | | {headerSubtitle}
413 | |
414 | |
415 | -> ), 416 | }} ----
help: Move this component definition out of the parent component ThreadRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:612:15]
611 | valuePrefix: "project",
612 | ,-> icon: (project) => (
613 | | <ProjectFavicon
614 | | environmentId={project.environmentId}
615 | | cwd={project.workspaceRoot}
616 | | className={ITEM_ICON_CLASS}
617 | | />
618 | -> ), 619 | runProject: openProjectFromSearch, ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:630:15]
629 | shortcutCommand: "chat.new",
630 | ,-> icon: (project) => (
631 | | <ProjectFavicon
632 | | environmentId={project.environmentId}
633 | | cwd={project.workspaceRoot}
634 | | className={ITEM_ICON_CLASS}
635 | | />
636 | -> ), 637 | runProject: async (project) => { ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx:911:24]
910 | title: "",
911 | ,-> headerTitle: () => (
912 | | <View
913 | | style={{
914 | | alignItems: "center",
915 | | gap: 1,
916 | | maxWidth: 240,
917 | | }}
918 | | >
919 | | <RNText
920 | | numberOfLines={1}
921 | | style={{
922 | | color: terminalTheme.foreground,
923 | | fontFamily: "DMSans_700Bold",
924 | | fontSize: MOBILE_TYPOGRAPHY.footnote.fontSize,
925 | | lineHeight: 16,
926 | | }}
927 | | >
928 | | {headerTitle.topLine}
929 | |
930 | | <RNText
931 | | ellipsizeMode="middle"
932 | | numberOfLines={1}
933 | | style={{
934 | | color: terminalTheme.mutedForeground,
935 | | fontFamily: "Menlo",
936 | | fontSize: MOBILE_TYPOGRAPHY.caption.fontSize,
937 | | lineHeight: 14,
938 | | }}
939 | | >
940 | | {headerTitle.bottomLine}
941 | |
942 | |
943 | -> ), 944 | }} ----
help: Move this component definition out of the parent component ThreadTerminalRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx:492:24]
491 | headerShadowVisible: false,
492 | headerTitle: () => ,
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
493 | headerSearchBarOptions: {
---- help: Move this component definition out of the parent component ThreadFilesTreeScreenand pass data as props. If you want to allow component creation in props, setallowAsProps` option to true.

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:293:15]
292 | const stored = yield* Effect.fromResult(
293 | Schema.decodeUnknownResult(StoredShellSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
294 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:313:13]
312 | const legacyStored = yield* Effect.fromResult(
313 | Schema.decodeUnknownResult(LegacyStoredShellSnapshot)(legacyParsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
314 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:328:13]
327 | const encoded = yield* Effect.fromResult(
328 | Schema.encodeUnknownResult(StoredShellSnapshot)(stored),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
329 | ).pipe(Effect.mapError((cause) => shellPersistenceError("save-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:355:13]
354 | const stored = yield* Effect.fromResult(
355 | Schema.decodeUnknownResult(StoredThreadSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
356 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-thread", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:365:13]
364 | const encoded = yield* Effect.fromResult(
365 | Schema.encodeUnknownResult(StoredThreadSnapshot)({
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
366 | schemaVersion: THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION,
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:31:5]
30 | return yield* Effect.fromResult(
31 | Schema.decodeUnknownResult(ConnectionCatalogDocument)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
32 | ).pipe(Effect.mapError((cause) => catalogError("decode", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:39:5]
38 | const encoded = yield* Effect.fromResult(
39 | Schema.encodeUnknownResult(ConnectionCatalogDocument)(catalog),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
40 | ).pipe(Effect.mapError((cause) => catalogError("encode", cause)));
`----

Found 0 errors and 20 warnings in 1748 files (978ms, 16 threads)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
~/packages/contracts$ tsgo --noEmit ⊘ cache disabled
~/apps/marketing$ astro check ⊘ cache disabled

~/oxlint-plugin-t3code$ tsgo --noEmit ⊘ cache disabled

~/packages/effect-codex-app-server$ tsgo --noEmit ⊘ cache disabled
08:12:00 [WARN] [vite] warning: optimizeDeps.esbuildOptions option was specified by "astro:dev-toolbar" plugin. This option is deprecated, please use optimizeDeps.rolldownOptions instead.
08:12:00 [WARN] [vite] You or a plugin you are using have set optimizeDeps.esbuildOptions but this option is now deprecated. Vite now uses Rolldown to optimize the dependencies. Please use optimizeDeps.rolldownOptions instead.
08:12:00 [types] Generated 39ms
08:12:00 [check] Getting diagnostics for Astro files in /Users/julius/.codex/worktrees/client-cloud-crypto-errors/apps/marketing...
Result (10 files):

  • 0 errors
  • 0 warnings
  • 0 hints

~/packages/effect-acp$ tsgo --noEmit ⊘ cache disabled

~/packages/shared$ tsgo --noEmit ⊘ cache disabled

~/packages/client-runtime$ tsgo --noEmit ⊘ cache disabled

~/scripts$ tsgo --noEmit ⊘ cache disabled

~/packages/ssh$ tsgo --noEmit ⊘ cache disabled

~/packages/tailscale$ tsgo --noEmit ⊘ cache disabled

~/apps/web$ tsgo --noEmit ⊘ cache disabled

~/apps/mobile$ tsc --noEmit ⊘ cache disabled

~/infra/relay$ tsgo --noEmit ⊘ cache disabled

~/apps/desktop$ tsgo --noEmit ⊘ cache disabled

~/apps/server$ tsgo --noEmit ⊘ cache disabled


vp run: 0/15 cache hit (0%). (Run vp run --last-details for full details)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
$ node scripts/mobile-native-static-check.ts ⊘ cache disabled
Found 6 Swift and 2 Kotlin native source files.
$ swiftlint lint --config .swiftlint.yml --strict
$ ktlint modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt
$ detekt --config detekt.yml --input modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt,modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt --build-upon-default-config
Skipping generated native project folders: android/, ios/.


Note

Medium Risk
Touches auth-related DPoP and secure token cache paths; error shape and message text change for any caller matching on strings, though signing/storage behavior is unchanged.

Overview
Replaces free-form message strings on mobile/web DPoP and mobile managed relay token failures with Effect Schema tagged errors that carry a discriminated operation literal and an optional cause (Defect), plus derived message getters.

On mobile DPoP, validation failures now use dedicated types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) wrapped as validate-key / derive-public-key where appropriate; invalid proof URLs fail as normalize-proof-urlwithout a cause. Web DPoP follows the same pattern (including isBrowserDpopError for decode chaining).

Managed relay token store errors are structured the same way; load/save/clear logging annotates the full error object and operation instead of a generic operation string. Tests assert operation (and causes where relevant) rather than fixed message text.

Reviewed by Cursor Bugbot for commit df4f4d7. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Structure cloud cryptography errors with operation-tagged Schema.TaggedErrorClass types

  • Replaces Data.TaggedError with Schema.TaggedErrorClass for CloudDpopError, BrowserDpopError, and ManagedRelayTokenStoreError, adding an operation literal discriminant and optional typed cause to each.
  • Each error class now exposes a message getter that formats the operation name, and new specific error types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) are introduced for granular failure cases.
  • Exports isBrowserDpopError and isManagedRelayTokenStoreError type guards via Schema.is.
  • Log annotations in managedRelayTokenStore now include the structured error object and its operation field instead of a freeform string.
  • Behavioral Change: callers that previously matched on error message strings must now inspect the structured operation field.

Macroscope summarized df4f4d7.

Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved

This PR refactors error classes to use structured typed operations instead of free-form string messages. The changes are purely structural with no runtime behavior modifications - errors are thrown at the same points with equivalent semantics.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
MemberAuthor

Superseded by the smaller, more structural #3277, #3307, and #3309. Those PRs retain storage/request identifiers and distinct DPoP failure types instead of combining all client cryptography changes into one branch.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[codex] Structure client cloud cryptography errors - #3395

Closed
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors
Closed

[codex] Structure client cloud cryptography errors#3395
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary\n\n- replace message-bearing DPoP errors with Schema errors carrying structured operation context\n- preserve exact lower-level causes and add typed key-format/mismatch causes for validation failures\n- remove valueless error constructor wrappers from web/mobile DPoP and mobile relay token storage\n- annotate best-effort token-store logging with the failing operation\n\n## Validation\n\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})

RUN /Users/julius/.codex/worktrees/client-cloud-crypto-errors

Test Files 3 passed (3)
Tests 10 passed (10)
Start at 08:11:56
Duration 376ms (transform 78ms, setup 0ms, import 632ms, tests 86ms, environment 0ms)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
�[1m�[94mpass:�[39m�[0m All 1848 files are correctly formatted �[2m(775ms, 16 threads)�[0m
! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1250:8]
1249 | () => ({
1250 | ,-> p({ node: _node, children, ...props }) {
1251 | | return <p {...props}>{renderSkillInlineMarkdownChildren(children, skills)}

;
1252 | -> }, 1253 | li({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1253:9]
1252 | },
1253 | ,-> li({ node, children, ...props }) {
1254 | | const listItemStart = node?.position?.start.offset;
1255 | | const markerOffset =
1256 | | typeof listItemStart === "number" ? findTaskListMarkerOffset(text, listItemStart) : null;
1257 | | return (
1258 | | <li {...props} data-task-marker-offset={markerOffset ?? undefined}>
1259 | | {renderSkillInlineMarkdownChildren(children, skills)}
1260 | |
1261 | | );
1262 | -> }, 1263 | input({ node: _node, type, checked, disabled: _disabled, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1263:12]
1262 | },
1263 | ,-> input({ node: _node, type, checked, disabled: _disabled, ...props }) {
1264 | | if (type !== "checkbox" || !onTaskListChange) {
1265 | | return (
1266 | | <input
1267 | | {...props}
1268 | | type={type}
1269 | | checked={checked}
1270 | | disabled={_disabled}
1271 | | readOnly={type === "checkbox"}
1272 | | />
1273 | | );
1274 | | }
1275 | | return (
1276 | | <input
1277 | | {...props}
1278 | | type="checkbox"
1279 | | name="markdown-task"
1280 | | aria-label="Toggle task"
1281 | | checked={checked}
1282 | | onChange={(event) => {
1283 | | const markerOffset = Number(
1284 | | event.currentTarget.closest("li")?.dataset.taskMarkerOffset,
1285 | | );
1286 | | if (!Number.isSafeInteger(markerOffset)) return;
1287 | | onTaskListChange({ markerOffset, checked: event.currentTarget.checked });
1288 | | }}
1289 | | />
1290 | | );
1291 | -> }, 1292 | a({ node, href, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1292:8]
1291 | },
1292 | ,-> a({ node, href, children, ...props }) {
1293 | | const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : "";
1294 | | const fileLinkMeta = normalizedHref ? markdownFileLinkMetaByHref.get(normalizedHref) : null;
1295 | | if (!fileLinkMeta) {
1296 | | const faviconHost = resolveExternalLinkHost(href);
1297 | | const isSameDocumentLink = href?.startsWith("#") ?? false;
1298 | | const onClick = props.onClick;
1299 | | const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime();
1300 | | const link = (
1301 | | <a
1302 | | {...props}
1303 | | href={href}
1304 | | target={isSameDocumentLink ? undefined : "_blank"}
1305 | | rel={isSameDocumentLink ? undefined : "noopener noreferrer"}
1306 | | onClick={(event) => {
1307 | | onClick?.(event);
1308 | | if (isSameDocumentLink && href) {
1309 | | handleMarkdownFragmentClick(event, href);
1310 | | }
1311 | | }}
1312 | | onContextMenu={(event) => {
1313 | | if (!canOpenInPreview || !href) return;
1314 | | event.preventDefault();
1315 | | event.stopPropagation();
1316 | | const api = readLocalApi();
1317 | | if (!api) return;
1318 | | void api.contextMenu
1319 | | .show(
1320 | | [
1321 | | { id: "open-in-browser", label: "Open in integrated browser" },
1322 | | { id: "open-external", label: "Open in system browser" },
1323 | | ] as const,
1324 | | { x: event.clientX, y: event.clientY },
1325 | | )
1326 | | .then((clicked) => {
1327 | | if (clicked === "open-in-browser") {
1328 | | void openExternalLinkInPreview(href);
1329 | | return;
1330 | | }
1331 | | if (clicked === "open-external") return api.shell.openExternal(href);
1332 | | })
1333 | | .catch(() => undefined);
1334 | | }}
1335 | | >
1336 | | {faviconHost ? (
1337 | |
1338 | | {children}
1339 | |
1340 | | ) : (
1341 | | children
1342 | | )}
1343 | |
1344 | | );
1345 | | if (!faviconHost || !href) {
1346 | | return link;
1347 | | }
1348 | | return (
1349 | |
1350 | |
1351 | | <TooltipPopup
1352 | | side="top"
1353 | | className="max-w-[min(36rem,calc(100vw-2rem))] whitespace-normal leading-tight wrap-anywhere"
1354 | | >
1355 | | {href}
1356 | |
1357 | |
1358 | | );
1359 | | }
1360 | |
1361 | | const parentSuffix = fileLinkParentSuffixByPath.get(fileLinkMeta.filePath);
1362 | | const labelParts = [fileLinkMeta.basename];
1363 | | if (typeof parentSuffix === "string" && parentSuffix.length > 0) {
1364 | | labelParts.push(parentSuffix);
1365 | | }
1366 | | if (fileLinkMeta.line) {
1367 | | labelParts.push(
1368 | | L${fileLinkMeta.line}${fileLinkMeta.column ? :C${fileLinkMeta.column} : ""},
1369 | | );
1370 | | }
1371 | |
1372 | | return (
1373 | | <MarkdownFileLink
1374 | | href={fileLinkMeta.targetPath}
1375 | | targetPath={fileLinkMeta.targetPath}
1376 | | iconPath={fileLinkMeta.filePath}
1377 | | displayPath={fileLinkMeta.displayPath}
1378 | | workspaceRelativePath={fileLinkMeta.workspaceRelativePath}
1379 | | line={fileLinkMeta.line}
1380 | | label={labelParts.join(" · ")}
1381 | | copyMarkdown={[${fileLinkMeta.basename}](${normalizedHref})}
1382 | | theme={resolvedTheme}
1383 | | threadRef={threadRef}
1384 | | onOpen={openInPreferredEditor}
1385 | | onOpenInBrowser={
1386 | | threadRef &&
1387 | | isPreviewSupportedInRuntime() &&
1388 | | isBrowserPreviewFile(fileLinkMeta.filePath)
1389 | | ? () => openMarkdownFileInPreview(fileLinkMeta.filePath)
1390 | | : undefined
1391 | | }
1392 | | className={props.className}
1393 | | />
1394 | | );
1395 | -> }, 1396 | table({ node: _node, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1396:12]
1395 | },
1396 | ,-> table({ node: _node, ...props }) {
1397 | | return <MarkdownTable {...props} />;
1398 | -> }, 1399 | details({ node: _node, children, open: detailsOpen }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1399:14]
1398 | },
1399 | ,-> details({ node: _node, children, open: detailsOpen }) {
1400 | | return {children};
1401 | -> }, 1402 | pre({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1402:10]
1401 | },
1402 | ,-> pre({ node, children, ...props }) {
1403 | | const codeBlock = extractCodeBlock(children);
1404 | | if (!codeBlock) {
1405 | | return <pre {...props}>{children};
1406 | | }
1407 | |
1408 | | const language = extractFenceLanguage(codeBlock.className);
1409 | | const fenceTitle = extractFenceTitle(extractPreCodeMeta(node));
1410 | | return (
1411 | | <MarkdownCodeBlock
1412 | | code={codeBlock.code}
1413 | | language={language}
1414 | | fenceTitle={fenceTitle}
1415 | | theme={resolvedTheme}
1416 | | >
1417 | | <CodeHighlightErrorBoundary fallback={<pre {...props}>{children}}>
1418 | | <Suspense fallback={<pre {...props}>{children}}>
1419 | | <SuspenseShikiCodeBlock
1420 | | className={codeBlock.className}
1421 | | code={codeBlock.code}
1422 | | themeName={diffThemeName}
1423 | | isStreaming={isStreaming}
1424 | | />
1425 | |
1426 | |
1427 | |
1428 | | );
1429 | -> }, 1430 | }), ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/review/ReviewSheet.tsx:249:24]
248 | },
249 | ,-> headerTitle: () => (
250 | | <View style={{ alignItems: "center" }}>
251 | | <NativeText
252 | | numberOfLines={1}
253 | | style={{
254 | | fontFamily: "DMSans_700Bold",
255 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
256 | | fontWeight: "900",
257 | | color: headerForeground,
258 | | letterSpacing: -0.4,
259 | | }}
260 | | >
261 | | Files Changed
262 | |
263 | | <View
264 | | style={{
265 | | flexDirection: "row",
266 | | alignItems: "center",
267 | | justifyContent: "center",
268 | | gap: 6,
269 | | flexWrap: "wrap",
270 | | }}
271 | | >
272 | | {headerDiffSummary.additions && headerDiffSummary.deletions ? (
273 | | <>
274 | | <NativeText
275 | | style={{
276 | | fontFamily: "DMSans_700Bold",
277 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
278 | | fontWeight: "700",
279 | | color: "#16a34a",
280 | | }}
281 | | >
282 | | {headerDiffSummary.additions}
283 | |
284 | | <NativeText
285 | | style={{
286 | | fontFamily: "DMSans_700Bold",
287 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
288 | | fontWeight: "700",
289 | | color: "#e11d48",
290 | | }}
291 | | >
292 | | {headerDiffSummary.deletions}
293 | |
294 | | {pendingReviewCommentCount > 0 ? (
295 | | <NativeText
296 | | style={{
297 | | fontFamily: "DMSans_700Bold",
298 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
299 | | fontWeight: "700",
300 | | color: "#b45309",
301 | | }}
302 | | >
303 | | {pendingReviewCommentCount} pending
304 | |
305 | | ) : null}
306 | | </>
307 | | ) : (
308 | | <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
309 | | <NativeText
310 | | numberOfLines={1}
311 | | style={{
312 | | fontFamily: "DMSans_700Bold",
313 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
314 | | fontWeight: "700",
315 | | color: headerMuted,
316 | | }}
317 | | >
318 | | {selectedSection?.title ?? "Review changes"}
319 | |
320 | | {pendingReviewCommentCount > 0 ? (
321 | | <NativeText
322 | | style={{
323 | | fontFamily: "DMSans_700Bold",
324 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
325 | | fontWeight: "700",
326 | | color: "#b45309",
327 | | }}
328 | | >
329 | | {pendingReviewCommentCount} pending
330 | |
331 | | ) : null}
332 | |
333 | | )}
334 | |
335 | |
336 | -> ), 337 | }} ----
help: Move this component definition out of the parent component ReviewSheet and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/threads/ThreadRouteScreen.tsx:383:24]
382 | headerBackTitle: "",
383 | ,-> headerTitle: () => (
384 | | <Pressable
385 | | style={{ alignItems: "center", maxWidth: 200 }}
386 | | onLongPress={() => {
387 | | // TODO: trigger rename modal
388 | | }}
389 | | >
390 | | <RNText
391 | | numberOfLines={1}
392 | | style={{
393 | | fontFamily: "DMSans_700Bold",
394 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
395 | | fontWeight: "900",
396 | | color: foregroundColor,
397 | | letterSpacing: -0.4,
398 | | }}
399 | | >
400 | | {selectedThread.title}
401 | |
402 | | <RNText
403 | | numberOfLines={1}
404 | | style={{
405 | | fontFamily: "DMSans_700Bold",
406 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
407 | | fontWeight: "700",
408 | | color: secondaryFg,
409 | | letterSpacing: 0.3,
410 | | }}
411 | | >
412 | | {headerSubtitle}
413 | |
414 | |
415 | -> ), 416 | }} ----
help: Move this component definition out of the parent component ThreadRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:612:15]
611 | valuePrefix: "project",
612 | ,-> icon: (project) => (
613 | | <ProjectFavicon
614 | | environmentId={project.environmentId}
615 | | cwd={project.workspaceRoot}
616 | | className={ITEM_ICON_CLASS}
617 | | />
618 | -> ), 619 | runProject: openProjectFromSearch, ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:630:15]
629 | shortcutCommand: "chat.new",
630 | ,-> icon: (project) => (
631 | | <ProjectFavicon
632 | | environmentId={project.environmentId}
633 | | cwd={project.workspaceRoot}
634 | | className={ITEM_ICON_CLASS}
635 | | />
636 | -> ), 637 | runProject: async (project) => { ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx:911:24]
910 | title: "",
911 | ,-> headerTitle: () => (
912 | | <View
913 | | style={{
914 | | alignItems: "center",
915 | | gap: 1,
916 | | maxWidth: 240,
917 | | }}
918 | | >
919 | | <RNText
920 | | numberOfLines={1}
921 | | style={{
922 | | color: terminalTheme.foreground,
923 | | fontFamily: "DMSans_700Bold",
924 | | fontSize: MOBILE_TYPOGRAPHY.footnote.fontSize,
925 | | lineHeight: 16,
926 | | }}
927 | | >
928 | | {headerTitle.topLine}
929 | |
930 | | <RNText
931 | | ellipsizeMode="middle"
932 | | numberOfLines={1}
933 | | style={{
934 | | color: terminalTheme.mutedForeground,
935 | | fontFamily: "Menlo",
936 | | fontSize: MOBILE_TYPOGRAPHY.caption.fontSize,
937 | | lineHeight: 14,
938 | | }}
939 | | >
940 | | {headerTitle.bottomLine}
941 | |
942 | |
943 | -> ), 944 | }} ----
help: Move this component definition out of the parent component ThreadTerminalRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx:492:24]
491 | headerShadowVisible: false,
492 | headerTitle: () => ,
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
493 | headerSearchBarOptions: {
---- help: Move this component definition out of the parent component ThreadFilesTreeScreenand pass data as props. If you want to allow component creation in props, setallowAsProps` option to true.

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:293:15]
292 | const stored = yield* Effect.fromResult(
293 | Schema.decodeUnknownResult(StoredShellSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
294 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:313:13]
312 | const legacyStored = yield* Effect.fromResult(
313 | Schema.decodeUnknownResult(LegacyStoredShellSnapshot)(legacyParsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
314 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:328:13]
327 | const encoded = yield* Effect.fromResult(
328 | Schema.encodeUnknownResult(StoredShellSnapshot)(stored),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
329 | ).pipe(Effect.mapError((cause) => shellPersistenceError("save-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:355:13]
354 | const stored = yield* Effect.fromResult(
355 | Schema.decodeUnknownResult(StoredThreadSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
356 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-thread", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:365:13]
364 | const encoded = yield* Effect.fromResult(
365 | Schema.encodeUnknownResult(StoredThreadSnapshot)({
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
366 | schemaVersion: THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION,
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:31:5]
30 | return yield* Effect.fromResult(
31 | Schema.decodeUnknownResult(ConnectionCatalogDocument)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
32 | ).pipe(Effect.mapError((cause) => catalogError("decode", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:39:5]
38 | const encoded = yield* Effect.fromResult(
39 | Schema.encodeUnknownResult(ConnectionCatalogDocument)(catalog),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
40 | ).pipe(Effect.mapError((cause) => catalogError("encode", cause)));
`----

Found 0 errors and 20 warnings in 1748 files (978ms, 16 threads)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
~/packages/contracts$ tsgo --noEmit ⊘ cache disabled
~/apps/marketing$ astro check ⊘ cache disabled

~/oxlint-plugin-t3code$ tsgo --noEmit ⊘ cache disabled

~/packages/effect-codex-app-server$ tsgo --noEmit ⊘ cache disabled
08:12:00 [WARN] [vite] warning: optimizeDeps.esbuildOptions option was specified by "astro:dev-toolbar" plugin. This option is deprecated, please use optimizeDeps.rolldownOptions instead.
08:12:00 [WARN] [vite] You or a plugin you are using have set optimizeDeps.esbuildOptions but this option is now deprecated. Vite now uses Rolldown to optimize the dependencies. Please use optimizeDeps.rolldownOptions instead.
08:12:00 [types] Generated 39ms
08:12:00 [check] Getting diagnostics for Astro files in /Users/julius/.codex/worktrees/client-cloud-crypto-errors/apps/marketing...
Result (10 files):

  • 0 errors
  • 0 warnings
  • 0 hints

~/packages/effect-acp$ tsgo --noEmit ⊘ cache disabled

~/packages/shared$ tsgo --noEmit ⊘ cache disabled

~/packages/client-runtime$ tsgo --noEmit ⊘ cache disabled

~/scripts$ tsgo --noEmit ⊘ cache disabled

~/packages/ssh$ tsgo --noEmit ⊘ cache disabled

~/packages/tailscale$ tsgo --noEmit ⊘ cache disabled

~/apps/web$ tsgo --noEmit ⊘ cache disabled

~/apps/mobile$ tsc --noEmit ⊘ cache disabled

~/infra/relay$ tsgo --noEmit ⊘ cache disabled

~/apps/desktop$ tsgo --noEmit ⊘ cache disabled

~/apps/server$ tsgo --noEmit ⊘ cache disabled


vp run: 0/15 cache hit (0%). (Run vp run --last-details for full details)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
$ node scripts/mobile-native-static-check.ts ⊘ cache disabled
Found 6 Swift and 2 Kotlin native source files.
$ swiftlint lint --config .swiftlint.yml --strict
$ ktlint modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt
$ detekt --config detekt.yml --input modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt,modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt --build-upon-default-config
Skipping generated native project folders: android/, ios/.


Note

Medium Risk
Touches auth-related DPoP and secure token cache paths; error shape and message text change for any caller matching on strings, though signing/storage behavior is unchanged.

Overview
Replaces free-form message strings on mobile/web DPoP and mobile managed relay token failures with Effect Schema tagged errors that carry a discriminated operation literal and an optional cause (Defect), plus derived message getters.

On mobile DPoP, validation failures now use dedicated types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) wrapped as validate-key / derive-public-key where appropriate; invalid proof URLs fail as normalize-proof-urlwithout a cause. Web DPoP follows the same pattern (including isBrowserDpopError for decode chaining).

Managed relay token store errors are structured the same way; load/save/clear logging annotates the full error object and operation instead of a generic operation string. Tests assert operation (and causes where relevant) rather than fixed message text.

Reviewed by Cursor Bugbot for commit df4f4d7. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Structure cloud cryptography errors with operation-tagged Schema.TaggedErrorClass types

  • Replaces Data.TaggedError with Schema.TaggedErrorClass for CloudDpopError, BrowserDpopError, and ManagedRelayTokenStoreError, adding an operation literal discriminant and optional typed cause to each.
  • Each error class now exposes a message getter that formats the operation name, and new specific error types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) are introduced for granular failure cases.
  • Exports isBrowserDpopError and isManagedRelayTokenStoreError type guards via Schema.is.
  • Log annotations in managedRelayTokenStore now include the structured error object and its operation field instead of a freeform string.
  • Behavioral Change: callers that previously matched on error message strings must now inspect the structured operation field.

Macroscope summarized df4f4d7.

Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved

This PR refactors error classes to use structured typed operations instead of free-form string messages. The changes are purely structural with no runtime behavior modifications - errors are thrown at the same points with equivalent semantics.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
MemberAuthor

Superseded by the smaller, more structural #3277, #3307, and #3309. Those PRs retain storage/request identifiers and distinct DPoP failure types instead of combining all client cryptography changes into one branch.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[codex] Structure client cloud cryptography errors - #3395

Closed
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors
Closed

[codex] Structure client cloud cryptography errors#3395
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary\n\n- replace message-bearing DPoP errors with Schema errors carrying structured operation context\n- preserve exact lower-level causes and add typed key-format/mismatch causes for validation failures\n- remove valueless error constructor wrappers from web/mobile DPoP and mobile relay token storage\n- annotate best-effort token-store logging with the failing operation\n\n## Validation\n\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})

RUN /Users/julius/.codex/worktrees/client-cloud-crypto-errors

Test Files 3 passed (3)
Tests 10 passed (10)
Start at 08:11:56
Duration 376ms (transform 78ms, setup 0ms, import 632ms, tests 86ms, environment 0ms)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
�[1m�[94mpass:�[39m�[0m All 1848 files are correctly formatted �[2m(775ms, 16 threads)�[0m
! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1250:8]
1249 | () => ({
1250 | ,-> p({ node: _node, children, ...props }) {
1251 | | return <p {...props}>{renderSkillInlineMarkdownChildren(children, skills)}

;
1252 | -> }, 1253 | li({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1253:9]
1252 | },
1253 | ,-> li({ node, children, ...props }) {
1254 | | const listItemStart = node?.position?.start.offset;
1255 | | const markerOffset =
1256 | | typeof listItemStart === "number" ? findTaskListMarkerOffset(text, listItemStart) : null;
1257 | | return (
1258 | | <li {...props} data-task-marker-offset={markerOffset ?? undefined}>
1259 | | {renderSkillInlineMarkdownChildren(children, skills)}
1260 | |
1261 | | );
1262 | -> }, 1263 | input({ node: _node, type, checked, disabled: _disabled, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1263:12]
1262 | },
1263 | ,-> input({ node: _node, type, checked, disabled: _disabled, ...props }) {
1264 | | if (type !== "checkbox" || !onTaskListChange) {
1265 | | return (
1266 | | <input
1267 | | {...props}
1268 | | type={type}
1269 | | checked={checked}
1270 | | disabled={_disabled}
1271 | | readOnly={type === "checkbox"}
1272 | | />
1273 | | );
1274 | | }
1275 | | return (
1276 | | <input
1277 | | {...props}
1278 | | type="checkbox"
1279 | | name="markdown-task"
1280 | | aria-label="Toggle task"
1281 | | checked={checked}
1282 | | onChange={(event) => {
1283 | | const markerOffset = Number(
1284 | | event.currentTarget.closest("li")?.dataset.taskMarkerOffset,
1285 | | );
1286 | | if (!Number.isSafeInteger(markerOffset)) return;
1287 | | onTaskListChange({ markerOffset, checked: event.currentTarget.checked });
1288 | | }}
1289 | | />
1290 | | );
1291 | -> }, 1292 | a({ node, href, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1292:8]
1291 | },
1292 | ,-> a({ node, href, children, ...props }) {
1293 | | const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : "";
1294 | | const fileLinkMeta = normalizedHref ? markdownFileLinkMetaByHref.get(normalizedHref) : null;
1295 | | if (!fileLinkMeta) {
1296 | | const faviconHost = resolveExternalLinkHost(href);
1297 | | const isSameDocumentLink = href?.startsWith("#") ?? false;
1298 | | const onClick = props.onClick;
1299 | | const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime();
1300 | | const link = (
1301 | | <a
1302 | | {...props}
1303 | | href={href}
1304 | | target={isSameDocumentLink ? undefined : "_blank"}
1305 | | rel={isSameDocumentLink ? undefined : "noopener noreferrer"}
1306 | | onClick={(event) => {
1307 | | onClick?.(event);
1308 | | if (isSameDocumentLink && href) {
1309 | | handleMarkdownFragmentClick(event, href);
1310 | | }
1311 | | }}
1312 | | onContextMenu={(event) => {
1313 | | if (!canOpenInPreview || !href) return;
1314 | | event.preventDefault();
1315 | | event.stopPropagation();
1316 | | const api = readLocalApi();
1317 | | if (!api) return;
1318 | | void api.contextMenu
1319 | | .show(
1320 | | [
1321 | | { id: "open-in-browser", label: "Open in integrated browser" },
1322 | | { id: "open-external", label: "Open in system browser" },
1323 | | ] as const,
1324 | | { x: event.clientX, y: event.clientY },
1325 | | )
1326 | | .then((clicked) => {
1327 | | if (clicked === "open-in-browser") {
1328 | | void openExternalLinkInPreview(href);
1329 | | return;
1330 | | }
1331 | | if (clicked === "open-external") return api.shell.openExternal(href);
1332 | | })
1333 | | .catch(() => undefined);
1334 | | }}
1335 | | >
1336 | | {faviconHost ? (
1337 | |
1338 | | {children}
1339 | |
1340 | | ) : (
1341 | | children
1342 | | )}
1343 | |
1344 | | );
1345 | | if (!faviconHost || !href) {
1346 | | return link;
1347 | | }
1348 | | return (
1349 | |
1350 | |
1351 | | <TooltipPopup
1352 | | side="top"
1353 | | className="max-w-[min(36rem,calc(100vw-2rem))] whitespace-normal leading-tight wrap-anywhere"
1354 | | >
1355 | | {href}
1356 | |
1357 | |
1358 | | );
1359 | | }
1360 | |
1361 | | const parentSuffix = fileLinkParentSuffixByPath.get(fileLinkMeta.filePath);
1362 | | const labelParts = [fileLinkMeta.basename];
1363 | | if (typeof parentSuffix === "string" && parentSuffix.length > 0) {
1364 | | labelParts.push(parentSuffix);
1365 | | }
1366 | | if (fileLinkMeta.line) {
1367 | | labelParts.push(
1368 | | L${fileLinkMeta.line}${fileLinkMeta.column ? :C${fileLinkMeta.column} : ""},
1369 | | );
1370 | | }
1371 | |
1372 | | return (
1373 | | <MarkdownFileLink
1374 | | href={fileLinkMeta.targetPath}
1375 | | targetPath={fileLinkMeta.targetPath}
1376 | | iconPath={fileLinkMeta.filePath}
1377 | | displayPath={fileLinkMeta.displayPath}
1378 | | workspaceRelativePath={fileLinkMeta.workspaceRelativePath}
1379 | | line={fileLinkMeta.line}
1380 | | label={labelParts.join(" · ")}
1381 | | copyMarkdown={[${fileLinkMeta.basename}](${normalizedHref})}
1382 | | theme={resolvedTheme}
1383 | | threadRef={threadRef}
1384 | | onOpen={openInPreferredEditor}
1385 | | onOpenInBrowser={
1386 | | threadRef &&
1387 | | isPreviewSupportedInRuntime() &&
1388 | | isBrowserPreviewFile(fileLinkMeta.filePath)
1389 | | ? () => openMarkdownFileInPreview(fileLinkMeta.filePath)
1390 | | : undefined
1391 | | }
1392 | | className={props.className}
1393 | | />
1394 | | );
1395 | -> }, 1396 | table({ node: _node, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1396:12]
1395 | },
1396 | ,-> table({ node: _node, ...props }) {
1397 | | return <MarkdownTable {...props} />;
1398 | -> }, 1399 | details({ node: _node, children, open: detailsOpen }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1399:14]
1398 | },
1399 | ,-> details({ node: _node, children, open: detailsOpen }) {
1400 | | return {children};
1401 | -> }, 1402 | pre({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1402:10]
1401 | },
1402 | ,-> pre({ node, children, ...props }) {
1403 | | const codeBlock = extractCodeBlock(children);
1404 | | if (!codeBlock) {
1405 | | return <pre {...props}>{children};
1406 | | }
1407 | |
1408 | | const language = extractFenceLanguage(codeBlock.className);
1409 | | const fenceTitle = extractFenceTitle(extractPreCodeMeta(node));
1410 | | return (
1411 | | <MarkdownCodeBlock
1412 | | code={codeBlock.code}
1413 | | language={language}
1414 | | fenceTitle={fenceTitle}
1415 | | theme={resolvedTheme}
1416 | | >
1417 | | <CodeHighlightErrorBoundary fallback={<pre {...props}>{children}}>
1418 | | <Suspense fallback={<pre {...props}>{children}}>
1419 | | <SuspenseShikiCodeBlock
1420 | | className={codeBlock.className}
1421 | | code={codeBlock.code}
1422 | | themeName={diffThemeName}
1423 | | isStreaming={isStreaming}
1424 | | />
1425 | |
1426 | |
1427 | |
1428 | | );
1429 | -> }, 1430 | }), ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/review/ReviewSheet.tsx:249:24]
248 | },
249 | ,-> headerTitle: () => (
250 | | <View style={{ alignItems: "center" }}>
251 | | <NativeText
252 | | numberOfLines={1}
253 | | style={{
254 | | fontFamily: "DMSans_700Bold",
255 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
256 | | fontWeight: "900",
257 | | color: headerForeground,
258 | | letterSpacing: -0.4,
259 | | }}
260 | | >
261 | | Files Changed
262 | |
263 | | <View
264 | | style={{
265 | | flexDirection: "row",
266 | | alignItems: "center",
267 | | justifyContent: "center",
268 | | gap: 6,
269 | | flexWrap: "wrap",
270 | | }}
271 | | >
272 | | {headerDiffSummary.additions && headerDiffSummary.deletions ? (
273 | | <>
274 | | <NativeText
275 | | style={{
276 | | fontFamily: "DMSans_700Bold",
277 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
278 | | fontWeight: "700",
279 | | color: "#16a34a",
280 | | }}
281 | | >
282 | | {headerDiffSummary.additions}
283 | |
284 | | <NativeText
285 | | style={{
286 | | fontFamily: "DMSans_700Bold",
287 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
288 | | fontWeight: "700",
289 | | color: "#e11d48",
290 | | }}
291 | | >
292 | | {headerDiffSummary.deletions}
293 | |
294 | | {pendingReviewCommentCount > 0 ? (
295 | | <NativeText
296 | | style={{
297 | | fontFamily: "DMSans_700Bold",
298 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
299 | | fontWeight: "700",
300 | | color: "#b45309",
301 | | }}
302 | | >
303 | | {pendingReviewCommentCount} pending
304 | |
305 | | ) : null}
306 | | </>
307 | | ) : (
308 | | <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
309 | | <NativeText
310 | | numberOfLines={1}
311 | | style={{
312 | | fontFamily: "DMSans_700Bold",
313 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
314 | | fontWeight: "700",
315 | | color: headerMuted,
316 | | }}
317 | | >
318 | | {selectedSection?.title ?? "Review changes"}
319 | |
320 | | {pendingReviewCommentCount > 0 ? (
321 | | <NativeText
322 | | style={{
323 | | fontFamily: "DMSans_700Bold",
324 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
325 | | fontWeight: "700",
326 | | color: "#b45309",
327 | | }}
328 | | >
329 | | {pendingReviewCommentCount} pending
330 | |
331 | | ) : null}
332 | |
333 | | )}
334 | |
335 | |
336 | -> ), 337 | }} ----
help: Move this component definition out of the parent component ReviewSheet and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/threads/ThreadRouteScreen.tsx:383:24]
382 | headerBackTitle: "",
383 | ,-> headerTitle: () => (
384 | | <Pressable
385 | | style={{ alignItems: "center", maxWidth: 200 }}
386 | | onLongPress={() => {
387 | | // TODO: trigger rename modal
388 | | }}
389 | | >
390 | | <RNText
391 | | numberOfLines={1}
392 | | style={{
393 | | fontFamily: "DMSans_700Bold",
394 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
395 | | fontWeight: "900",
396 | | color: foregroundColor,
397 | | letterSpacing: -0.4,
398 | | }}
399 | | >
400 | | {selectedThread.title}
401 | |
402 | | <RNText
403 | | numberOfLines={1}
404 | | style={{
405 | | fontFamily: "DMSans_700Bold",
406 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
407 | | fontWeight: "700",
408 | | color: secondaryFg,
409 | | letterSpacing: 0.3,
410 | | }}
411 | | >
412 | | {headerSubtitle}
413 | |
414 | |
415 | -> ), 416 | }} ----
help: Move this component definition out of the parent component ThreadRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:612:15]
611 | valuePrefix: "project",
612 | ,-> icon: (project) => (
613 | | <ProjectFavicon
614 | | environmentId={project.environmentId}
615 | | cwd={project.workspaceRoot}
616 | | className={ITEM_ICON_CLASS}
617 | | />
618 | -> ), 619 | runProject: openProjectFromSearch, ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:630:15]
629 | shortcutCommand: "chat.new",
630 | ,-> icon: (project) => (
631 | | <ProjectFavicon
632 | | environmentId={project.environmentId}
633 | | cwd={project.workspaceRoot}
634 | | className={ITEM_ICON_CLASS}
635 | | />
636 | -> ), 637 | runProject: async (project) => { ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx:911:24]
910 | title: "",
911 | ,-> headerTitle: () => (
912 | | <View
913 | | style={{
914 | | alignItems: "center",
915 | | gap: 1,
916 | | maxWidth: 240,
917 | | }}
918 | | >
919 | | <RNText
920 | | numberOfLines={1}
921 | | style={{
922 | | color: terminalTheme.foreground,
923 | | fontFamily: "DMSans_700Bold",
924 | | fontSize: MOBILE_TYPOGRAPHY.footnote.fontSize,
925 | | lineHeight: 16,
926 | | }}
927 | | >
928 | | {headerTitle.topLine}
929 | |
930 | | <RNText
931 | | ellipsizeMode="middle"
932 | | numberOfLines={1}
933 | | style={{
934 | | color: terminalTheme.mutedForeground,
935 | | fontFamily: "Menlo",
936 | | fontSize: MOBILE_TYPOGRAPHY.caption.fontSize,
937 | | lineHeight: 14,
938 | | }}
939 | | >
940 | | {headerTitle.bottomLine}
941 | |
942 | |
943 | -> ), 944 | }} ----
help: Move this component definition out of the parent component ThreadTerminalRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx:492:24]
491 | headerShadowVisible: false,
492 | headerTitle: () => ,
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
493 | headerSearchBarOptions: {
---- help: Move this component definition out of the parent component ThreadFilesTreeScreenand pass data as props. If you want to allow component creation in props, setallowAsProps` option to true.

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:293:15]
292 | const stored = yield* Effect.fromResult(
293 | Schema.decodeUnknownResult(StoredShellSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
294 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:313:13]
312 | const legacyStored = yield* Effect.fromResult(
313 | Schema.decodeUnknownResult(LegacyStoredShellSnapshot)(legacyParsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
314 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:328:13]
327 | const encoded = yield* Effect.fromResult(
328 | Schema.encodeUnknownResult(StoredShellSnapshot)(stored),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
329 | ).pipe(Effect.mapError((cause) => shellPersistenceError("save-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:355:13]
354 | const stored = yield* Effect.fromResult(
355 | Schema.decodeUnknownResult(StoredThreadSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
356 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-thread", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:365:13]
364 | const encoded = yield* Effect.fromResult(
365 | Schema.encodeUnknownResult(StoredThreadSnapshot)({
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
366 | schemaVersion: THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION,
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:31:5]
30 | return yield* Effect.fromResult(
31 | Schema.decodeUnknownResult(ConnectionCatalogDocument)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
32 | ).pipe(Effect.mapError((cause) => catalogError("decode", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:39:5]
38 | const encoded = yield* Effect.fromResult(
39 | Schema.encodeUnknownResult(ConnectionCatalogDocument)(catalog),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
40 | ).pipe(Effect.mapError((cause) => catalogError("encode", cause)));
`----

Found 0 errors and 20 warnings in 1748 files (978ms, 16 threads)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
~/packages/contracts$ tsgo --noEmit ⊘ cache disabled
~/apps/marketing$ astro check ⊘ cache disabled

~/oxlint-plugin-t3code$ tsgo --noEmit ⊘ cache disabled

~/packages/effect-codex-app-server$ tsgo --noEmit ⊘ cache disabled
08:12:00 [WARN] [vite] warning: optimizeDeps.esbuildOptions option was specified by "astro:dev-toolbar" plugin. This option is deprecated, please use optimizeDeps.rolldownOptions instead.
08:12:00 [WARN] [vite] You or a plugin you are using have set optimizeDeps.esbuildOptions but this option is now deprecated. Vite now uses Rolldown to optimize the dependencies. Please use optimizeDeps.rolldownOptions instead.
08:12:00 [types] Generated 39ms
08:12:00 [check] Getting diagnostics for Astro files in /Users/julius/.codex/worktrees/client-cloud-crypto-errors/apps/marketing...
Result (10 files):

  • 0 errors
  • 0 warnings
  • 0 hints

~/packages/effect-acp$ tsgo --noEmit ⊘ cache disabled

~/packages/shared$ tsgo --noEmit ⊘ cache disabled

~/packages/client-runtime$ tsgo --noEmit ⊘ cache disabled

~/scripts$ tsgo --noEmit ⊘ cache disabled

~/packages/ssh$ tsgo --noEmit ⊘ cache disabled

~/packages/tailscale$ tsgo --noEmit ⊘ cache disabled

~/apps/web$ tsgo --noEmit ⊘ cache disabled

~/apps/mobile$ tsc --noEmit ⊘ cache disabled

~/infra/relay$ tsgo --noEmit ⊘ cache disabled

~/apps/desktop$ tsgo --noEmit ⊘ cache disabled

~/apps/server$ tsgo --noEmit ⊘ cache disabled


vp run: 0/15 cache hit (0%). (Run vp run --last-details for full details)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
$ node scripts/mobile-native-static-check.ts ⊘ cache disabled
Found 6 Swift and 2 Kotlin native source files.
$ swiftlint lint --config .swiftlint.yml --strict
$ ktlint modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt
$ detekt --config detekt.yml --input modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt,modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt --build-upon-default-config
Skipping generated native project folders: android/, ios/.


Note

Medium Risk
Touches auth-related DPoP and secure token cache paths; error shape and message text change for any caller matching on strings, though signing/storage behavior is unchanged.

Overview
Replaces free-form message strings on mobile/web DPoP and mobile managed relay token failures with Effect Schema tagged errors that carry a discriminated operation literal and an optional cause (Defect), plus derived message getters.

On mobile DPoP, validation failures now use dedicated types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) wrapped as validate-key / derive-public-key where appropriate; invalid proof URLs fail as normalize-proof-urlwithout a cause. Web DPoP follows the same pattern (including isBrowserDpopError for decode chaining).

Managed relay token store errors are structured the same way; load/save/clear logging annotates the full error object and operation instead of a generic operation string. Tests assert operation (and causes where relevant) rather than fixed message text.

Reviewed by Cursor Bugbot for commit df4f4d7. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Structure cloud cryptography errors with operation-tagged Schema.TaggedErrorClass types

  • Replaces Data.TaggedError with Schema.TaggedErrorClass for CloudDpopError, BrowserDpopError, and ManagedRelayTokenStoreError, adding an operation literal discriminant and optional typed cause to each.
  • Each error class now exposes a message getter that formats the operation name, and new specific error types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) are introduced for granular failure cases.
  • Exports isBrowserDpopError and isManagedRelayTokenStoreError type guards via Schema.is.
  • Log annotations in managedRelayTokenStore now include the structured error object and its operation field instead of a freeform string.
  • Behavioral Change: callers that previously matched on error message strings must now inspect the structured operation field.

Macroscope summarized df4f4d7.

Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved

This PR refactors error classes to use structured typed operations instead of free-form string messages. The changes are purely structural with no runtime behavior modifications - errors are thrown at the same points with equivalent semantics.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
MemberAuthor

Superseded by the smaller, more structural #3277, #3307, and #3309. Those PRs retain storage/request identifiers and distinct DPoP failure types instead of combining all client cryptography changes into one branch.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[codex] Structure client cloud cryptography errors - #3395

Closed
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors
Closed

[codex] Structure client cloud cryptography errors#3395
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary\n\n- replace message-bearing DPoP errors with Schema errors carrying structured operation context\n- preserve exact lower-level causes and add typed key-format/mismatch causes for validation failures\n- remove valueless error constructor wrappers from web/mobile DPoP and mobile relay token storage\n- annotate best-effort token-store logging with the failing operation\n\n## Validation\n\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})

RUN /Users/julius/.codex/worktrees/client-cloud-crypto-errors

Test Files 3 passed (3)
Tests 10 passed (10)
Start at 08:11:56
Duration 376ms (transform 78ms, setup 0ms, import 632ms, tests 86ms, environment 0ms)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
�[1m�[94mpass:�[39m�[0m All 1848 files are correctly formatted �[2m(775ms, 16 threads)�[0m
! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1250:8]
1249 | () => ({
1250 | ,-> p({ node: _node, children, ...props }) {
1251 | | return <p {...props}>{renderSkillInlineMarkdownChildren(children, skills)}

;
1252 | -> }, 1253 | li({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1253:9]
1252 | },
1253 | ,-> li({ node, children, ...props }) {
1254 | | const listItemStart = node?.position?.start.offset;
1255 | | const markerOffset =
1256 | | typeof listItemStart === "number" ? findTaskListMarkerOffset(text, listItemStart) : null;
1257 | | return (
1258 | | <li {...props} data-task-marker-offset={markerOffset ?? undefined}>
1259 | | {renderSkillInlineMarkdownChildren(children, skills)}
1260 | |
1261 | | );
1262 | -> }, 1263 | input({ node: _node, type, checked, disabled: _disabled, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1263:12]
1262 | },
1263 | ,-> input({ node: _node, type, checked, disabled: _disabled, ...props }) {
1264 | | if (type !== "checkbox" || !onTaskListChange) {
1265 | | return (
1266 | | <input
1267 | | {...props}
1268 | | type={type}
1269 | | checked={checked}
1270 | | disabled={_disabled}
1271 | | readOnly={type === "checkbox"}
1272 | | />
1273 | | );
1274 | | }
1275 | | return (
1276 | | <input
1277 | | {...props}
1278 | | type="checkbox"
1279 | | name="markdown-task"
1280 | | aria-label="Toggle task"
1281 | | checked={checked}
1282 | | onChange={(event) => {
1283 | | const markerOffset = Number(
1284 | | event.currentTarget.closest("li")?.dataset.taskMarkerOffset,
1285 | | );
1286 | | if (!Number.isSafeInteger(markerOffset)) return;
1287 | | onTaskListChange({ markerOffset, checked: event.currentTarget.checked });
1288 | | }}
1289 | | />
1290 | | );
1291 | -> }, 1292 | a({ node, href, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1292:8]
1291 | },
1292 | ,-> a({ node, href, children, ...props }) {
1293 | | const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : "";
1294 | | const fileLinkMeta = normalizedHref ? markdownFileLinkMetaByHref.get(normalizedHref) : null;
1295 | | if (!fileLinkMeta) {
1296 | | const faviconHost = resolveExternalLinkHost(href);
1297 | | const isSameDocumentLink = href?.startsWith("#") ?? false;
1298 | | const onClick = props.onClick;
1299 | | const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime();
1300 | | const link = (
1301 | | <a
1302 | | {...props}
1303 | | href={href}
1304 | | target={isSameDocumentLink ? undefined : "_blank"}
1305 | | rel={isSameDocumentLink ? undefined : "noopener noreferrer"}
1306 | | onClick={(event) => {
1307 | | onClick?.(event);
1308 | | if (isSameDocumentLink && href) {
1309 | | handleMarkdownFragmentClick(event, href);
1310 | | }
1311 | | }}
1312 | | onContextMenu={(event) => {
1313 | | if (!canOpenInPreview || !href) return;
1314 | | event.preventDefault();
1315 | | event.stopPropagation();
1316 | | const api = readLocalApi();
1317 | | if (!api) return;
1318 | | void api.contextMenu
1319 | | .show(
1320 | | [
1321 | | { id: "open-in-browser", label: "Open in integrated browser" },
1322 | | { id: "open-external", label: "Open in system browser" },
1323 | | ] as const,
1324 | | { x: event.clientX, y: event.clientY },
1325 | | )
1326 | | .then((clicked) => {
1327 | | if (clicked === "open-in-browser") {
1328 | | void openExternalLinkInPreview(href);
1329 | | return;
1330 | | }
1331 | | if (clicked === "open-external") return api.shell.openExternal(href);
1332 | | })
1333 | | .catch(() => undefined);
1334 | | }}
1335 | | >
1336 | | {faviconHost ? (
1337 | |
1338 | | {children}
1339 | |
1340 | | ) : (
1341 | | children
1342 | | )}
1343 | |
1344 | | );
1345 | | if (!faviconHost || !href) {
1346 | | return link;
1347 | | }
1348 | | return (
1349 | |
1350 | |
1351 | | <TooltipPopup
1352 | | side="top"
1353 | | className="max-w-[min(36rem,calc(100vw-2rem))] whitespace-normal leading-tight wrap-anywhere"
1354 | | >
1355 | | {href}
1356 | |
1357 | |
1358 | | );
1359 | | }
1360 | |
1361 | | const parentSuffix = fileLinkParentSuffixByPath.get(fileLinkMeta.filePath);
1362 | | const labelParts = [fileLinkMeta.basename];
1363 | | if (typeof parentSuffix === "string" && parentSuffix.length > 0) {
1364 | | labelParts.push(parentSuffix);
1365 | | }
1366 | | if (fileLinkMeta.line) {
1367 | | labelParts.push(
1368 | | L${fileLinkMeta.line}${fileLinkMeta.column ? :C${fileLinkMeta.column} : ""},
1369 | | );
1370 | | }
1371 | |
1372 | | return (
1373 | | <MarkdownFileLink
1374 | | href={fileLinkMeta.targetPath}
1375 | | targetPath={fileLinkMeta.targetPath}
1376 | | iconPath={fileLinkMeta.filePath}
1377 | | displayPath={fileLinkMeta.displayPath}
1378 | | workspaceRelativePath={fileLinkMeta.workspaceRelativePath}
1379 | | line={fileLinkMeta.line}
1380 | | label={labelParts.join(" · ")}
1381 | | copyMarkdown={[${fileLinkMeta.basename}](${normalizedHref})}
1382 | | theme={resolvedTheme}
1383 | | threadRef={threadRef}
1384 | | onOpen={openInPreferredEditor}
1385 | | onOpenInBrowser={
1386 | | threadRef &&
1387 | | isPreviewSupportedInRuntime() &&
1388 | | isBrowserPreviewFile(fileLinkMeta.filePath)
1389 | | ? () => openMarkdownFileInPreview(fileLinkMeta.filePath)
1390 | | : undefined
1391 | | }
1392 | | className={props.className}
1393 | | />
1394 | | );
1395 | -> }, 1396 | table({ node: _node, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1396:12]
1395 | },
1396 | ,-> table({ node: _node, ...props }) {
1397 | | return <MarkdownTable {...props} />;
1398 | -> }, 1399 | details({ node: _node, children, open: detailsOpen }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1399:14]
1398 | },
1399 | ,-> details({ node: _node, children, open: detailsOpen }) {
1400 | | return {children};
1401 | -> }, 1402 | pre({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1402:10]
1401 | },
1402 | ,-> pre({ node, children, ...props }) {
1403 | | const codeBlock = extractCodeBlock(children);
1404 | | if (!codeBlock) {
1405 | | return <pre {...props}>{children};
1406 | | }
1407 | |
1408 | | const language = extractFenceLanguage(codeBlock.className);
1409 | | const fenceTitle = extractFenceTitle(extractPreCodeMeta(node));
1410 | | return (
1411 | | <MarkdownCodeBlock
1412 | | code={codeBlock.code}
1413 | | language={language}
1414 | | fenceTitle={fenceTitle}
1415 | | theme={resolvedTheme}
1416 | | >
1417 | | <CodeHighlightErrorBoundary fallback={<pre {...props}>{children}}>
1418 | | <Suspense fallback={<pre {...props}>{children}}>
1419 | | <SuspenseShikiCodeBlock
1420 | | className={codeBlock.className}
1421 | | code={codeBlock.code}
1422 | | themeName={diffThemeName}
1423 | | isStreaming={isStreaming}
1424 | | />
1425 | |
1426 | |
1427 | |
1428 | | );
1429 | -> }, 1430 | }), ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/review/ReviewSheet.tsx:249:24]
248 | },
249 | ,-> headerTitle: () => (
250 | | <View style={{ alignItems: "center" }}>
251 | | <NativeText
252 | | numberOfLines={1}
253 | | style={{
254 | | fontFamily: "DMSans_700Bold",
255 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
256 | | fontWeight: "900",
257 | | color: headerForeground,
258 | | letterSpacing: -0.4,
259 | | }}
260 | | >
261 | | Files Changed
262 | |
263 | | <View
264 | | style={{
265 | | flexDirection: "row",
266 | | alignItems: "center",
267 | | justifyContent: "center",
268 | | gap: 6,
269 | | flexWrap: "wrap",
270 | | }}
271 | | >
272 | | {headerDiffSummary.additions && headerDiffSummary.deletions ? (
273 | | <>
274 | | <NativeText
275 | | style={{
276 | | fontFamily: "DMSans_700Bold",
277 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
278 | | fontWeight: "700",
279 | | color: "#16a34a",
280 | | }}
281 | | >
282 | | {headerDiffSummary.additions}
283 | |
284 | | <NativeText
285 | | style={{
286 | | fontFamily: "DMSans_700Bold",
287 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
288 | | fontWeight: "700",
289 | | color: "#e11d48",
290 | | }}
291 | | >
292 | | {headerDiffSummary.deletions}
293 | |
294 | | {pendingReviewCommentCount > 0 ? (
295 | | <NativeText
296 | | style={{
297 | | fontFamily: "DMSans_700Bold",
298 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
299 | | fontWeight: "700",
300 | | color: "#b45309",
301 | | }}
302 | | >
303 | | {pendingReviewCommentCount} pending
304 | |
305 | | ) : null}
306 | | </>
307 | | ) : (
308 | | <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
309 | | <NativeText
310 | | numberOfLines={1}
311 | | style={{
312 | | fontFamily: "DMSans_700Bold",
313 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
314 | | fontWeight: "700",
315 | | color: headerMuted,
316 | | }}
317 | | >
318 | | {selectedSection?.title ?? "Review changes"}
319 | |
320 | | {pendingReviewCommentCount > 0 ? (
321 | | <NativeText
322 | | style={{
323 | | fontFamily: "DMSans_700Bold",
324 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
325 | | fontWeight: "700",
326 | | color: "#b45309",
327 | | }}
328 | | >
329 | | {pendingReviewCommentCount} pending
330 | |
331 | | ) : null}
332 | |
333 | | )}
334 | |
335 | |
336 | -> ), 337 | }} ----
help: Move this component definition out of the parent component ReviewSheet and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/threads/ThreadRouteScreen.tsx:383:24]
382 | headerBackTitle: "",
383 | ,-> headerTitle: () => (
384 | | <Pressable
385 | | style={{ alignItems: "center", maxWidth: 200 }}
386 | | onLongPress={() => {
387 | | // TODO: trigger rename modal
388 | | }}
389 | | >
390 | | <RNText
391 | | numberOfLines={1}
392 | | style={{
393 | | fontFamily: "DMSans_700Bold",
394 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
395 | | fontWeight: "900",
396 | | color: foregroundColor,
397 | | letterSpacing: -0.4,
398 | | }}
399 | | >
400 | | {selectedThread.title}
401 | |
402 | | <RNText
403 | | numberOfLines={1}
404 | | style={{
405 | | fontFamily: "DMSans_700Bold",
406 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
407 | | fontWeight: "700",
408 | | color: secondaryFg,
409 | | letterSpacing: 0.3,
410 | | }}
411 | | >
412 | | {headerSubtitle}
413 | |
414 | |
415 | -> ), 416 | }} ----
help: Move this component definition out of the parent component ThreadRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:612:15]
611 | valuePrefix: "project",
612 | ,-> icon: (project) => (
613 | | <ProjectFavicon
614 | | environmentId={project.environmentId}
615 | | cwd={project.workspaceRoot}
616 | | className={ITEM_ICON_CLASS}
617 | | />
618 | -> ), 619 | runProject: openProjectFromSearch, ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:630:15]
629 | shortcutCommand: "chat.new",
630 | ,-> icon: (project) => (
631 | | <ProjectFavicon
632 | | environmentId={project.environmentId}
633 | | cwd={project.workspaceRoot}
634 | | className={ITEM_ICON_CLASS}
635 | | />
636 | -> ), 637 | runProject: async (project) => { ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx:911:24]
910 | title: "",
911 | ,-> headerTitle: () => (
912 | | <View
913 | | style={{
914 | | alignItems: "center",
915 | | gap: 1,
916 | | maxWidth: 240,
917 | | }}
918 | | >
919 | | <RNText
920 | | numberOfLines={1}
921 | | style={{
922 | | color: terminalTheme.foreground,
923 | | fontFamily: "DMSans_700Bold",
924 | | fontSize: MOBILE_TYPOGRAPHY.footnote.fontSize,
925 | | lineHeight: 16,
926 | | }}
927 | | >
928 | | {headerTitle.topLine}
929 | |
930 | | <RNText
931 | | ellipsizeMode="middle"
932 | | numberOfLines={1}
933 | | style={{
934 | | color: terminalTheme.mutedForeground,
935 | | fontFamily: "Menlo",
936 | | fontSize: MOBILE_TYPOGRAPHY.caption.fontSize,
937 | | lineHeight: 14,
938 | | }}
939 | | >
940 | | {headerTitle.bottomLine}
941 | |
942 | |
943 | -> ), 944 | }} ----
help: Move this component definition out of the parent component ThreadTerminalRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx:492:24]
491 | headerShadowVisible: false,
492 | headerTitle: () => ,
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
493 | headerSearchBarOptions: {
---- help: Move this component definition out of the parent component ThreadFilesTreeScreenand pass data as props. If you want to allow component creation in props, setallowAsProps` option to true.

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:293:15]
292 | const stored = yield* Effect.fromResult(
293 | Schema.decodeUnknownResult(StoredShellSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
294 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:313:13]
312 | const legacyStored = yield* Effect.fromResult(
313 | Schema.decodeUnknownResult(LegacyStoredShellSnapshot)(legacyParsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
314 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:328:13]
327 | const encoded = yield* Effect.fromResult(
328 | Schema.encodeUnknownResult(StoredShellSnapshot)(stored),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
329 | ).pipe(Effect.mapError((cause) => shellPersistenceError("save-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:355:13]
354 | const stored = yield* Effect.fromResult(
355 | Schema.decodeUnknownResult(StoredThreadSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
356 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-thread", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:365:13]
364 | const encoded = yield* Effect.fromResult(
365 | Schema.encodeUnknownResult(StoredThreadSnapshot)({
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
366 | schemaVersion: THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION,
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:31:5]
30 | return yield* Effect.fromResult(
31 | Schema.decodeUnknownResult(ConnectionCatalogDocument)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
32 | ).pipe(Effect.mapError((cause) => catalogError("decode", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:39:5]
38 | const encoded = yield* Effect.fromResult(
39 | Schema.encodeUnknownResult(ConnectionCatalogDocument)(catalog),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
40 | ).pipe(Effect.mapError((cause) => catalogError("encode", cause)));
`----

Found 0 errors and 20 warnings in 1748 files (978ms, 16 threads)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
~/packages/contracts$ tsgo --noEmit ⊘ cache disabled
~/apps/marketing$ astro check ⊘ cache disabled

~/oxlint-plugin-t3code$ tsgo --noEmit ⊘ cache disabled

~/packages/effect-codex-app-server$ tsgo --noEmit ⊘ cache disabled
08:12:00 [WARN] [vite] warning: optimizeDeps.esbuildOptions option was specified by "astro:dev-toolbar" plugin. This option is deprecated, please use optimizeDeps.rolldownOptions instead.
08:12:00 [WARN] [vite] You or a plugin you are using have set optimizeDeps.esbuildOptions but this option is now deprecated. Vite now uses Rolldown to optimize the dependencies. Please use optimizeDeps.rolldownOptions instead.
08:12:00 [types] Generated 39ms
08:12:00 [check] Getting diagnostics for Astro files in /Users/julius/.codex/worktrees/client-cloud-crypto-errors/apps/marketing...
Result (10 files):

  • 0 errors
  • 0 warnings
  • 0 hints

~/packages/effect-acp$ tsgo --noEmit ⊘ cache disabled

~/packages/shared$ tsgo --noEmit ⊘ cache disabled

~/packages/client-runtime$ tsgo --noEmit ⊘ cache disabled

~/scripts$ tsgo --noEmit ⊘ cache disabled

~/packages/ssh$ tsgo --noEmit ⊘ cache disabled

~/packages/tailscale$ tsgo --noEmit ⊘ cache disabled

~/apps/web$ tsgo --noEmit ⊘ cache disabled

~/apps/mobile$ tsc --noEmit ⊘ cache disabled

~/infra/relay$ tsgo --noEmit ⊘ cache disabled

~/apps/desktop$ tsgo --noEmit ⊘ cache disabled

~/apps/server$ tsgo --noEmit ⊘ cache disabled


vp run: 0/15 cache hit (0%). (Run vp run --last-details for full details)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
$ node scripts/mobile-native-static-check.ts ⊘ cache disabled
Found 6 Swift and 2 Kotlin native source files.
$ swiftlint lint --config .swiftlint.yml --strict
$ ktlint modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt
$ detekt --config detekt.yml --input modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt,modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt --build-upon-default-config
Skipping generated native project folders: android/, ios/.


Note

Medium Risk
Touches auth-related DPoP and secure token cache paths; error shape and message text change for any caller matching on strings, though signing/storage behavior is unchanged.

Overview
Replaces free-form message strings on mobile/web DPoP and mobile managed relay token failures with Effect Schema tagged errors that carry a discriminated operation literal and an optional cause (Defect), plus derived message getters.

On mobile DPoP, validation failures now use dedicated types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) wrapped as validate-key / derive-public-key where appropriate; invalid proof URLs fail as normalize-proof-urlwithout a cause. Web DPoP follows the same pattern (including isBrowserDpopError for decode chaining).

Managed relay token store errors are structured the same way; load/save/clear logging annotates the full error object and operation instead of a generic operation string. Tests assert operation (and causes where relevant) rather than fixed message text.

Reviewed by Cursor Bugbot for commit df4f4d7. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Structure cloud cryptography errors with operation-tagged Schema.TaggedErrorClass types

  • Replaces Data.TaggedError with Schema.TaggedErrorClass for CloudDpopError, BrowserDpopError, and ManagedRelayTokenStoreError, adding an operation literal discriminant and optional typed cause to each.
  • Each error class now exposes a message getter that formats the operation name, and new specific error types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) are introduced for granular failure cases.
  • Exports isBrowserDpopError and isManagedRelayTokenStoreError type guards via Schema.is.
  • Log annotations in managedRelayTokenStore now include the structured error object and its operation field instead of a freeform string.
  • Behavioral Change: callers that previously matched on error message strings must now inspect the structured operation field.

Macroscope summarized df4f4d7.

Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved

This PR refactors error classes to use structured typed operations instead of free-form string messages. The changes are purely structural with no runtime behavior modifications - errors are thrown at the same points with equivalent semantics.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
MemberAuthor

Superseded by the smaller, more structural #3277, #3307, and #3309. Those PRs retain storage/request identifiers and distinct DPoP failure types instead of combining all client cryptography changes into one branch.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[codex] Structure client cloud cryptography errors - #3395

Closed
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors
Closed

[codex] Structure client cloud cryptography errors#3395
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary\n\n- replace message-bearing DPoP errors with Schema errors carrying structured operation context\n- preserve exact lower-level causes and add typed key-format/mismatch causes for validation failures\n- remove valueless error constructor wrappers from web/mobile DPoP and mobile relay token storage\n- annotate best-effort token-store logging with the failing operation\n\n## Validation\n\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})

RUN /Users/julius/.codex/worktrees/client-cloud-crypto-errors

Test Files 3 passed (3)
Tests 10 passed (10)
Start at 08:11:56
Duration 376ms (transform 78ms, setup 0ms, import 632ms, tests 86ms, environment 0ms)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
�[1m�[94mpass:�[39m�[0m All 1848 files are correctly formatted �[2m(775ms, 16 threads)�[0m
! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1250:8]
1249 | () => ({
1250 | ,-> p({ node: _node, children, ...props }) {
1251 | | return <p {...props}>{renderSkillInlineMarkdownChildren(children, skills)}

;
1252 | -> }, 1253 | li({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1253:9]
1252 | },
1253 | ,-> li({ node, children, ...props }) {
1254 | | const listItemStart = node?.position?.start.offset;
1255 | | const markerOffset =
1256 | | typeof listItemStart === "number" ? findTaskListMarkerOffset(text, listItemStart) : null;
1257 | | return (
1258 | | <li {...props} data-task-marker-offset={markerOffset ?? undefined}>
1259 | | {renderSkillInlineMarkdownChildren(children, skills)}
1260 | |
1261 | | );
1262 | -> }, 1263 | input({ node: _node, type, checked, disabled: _disabled, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1263:12]
1262 | },
1263 | ,-> input({ node: _node, type, checked, disabled: _disabled, ...props }) {
1264 | | if (type !== "checkbox" || !onTaskListChange) {
1265 | | return (
1266 | | <input
1267 | | {...props}
1268 | | type={type}
1269 | | checked={checked}
1270 | | disabled={_disabled}
1271 | | readOnly={type === "checkbox"}
1272 | | />
1273 | | );
1274 | | }
1275 | | return (
1276 | | <input
1277 | | {...props}
1278 | | type="checkbox"
1279 | | name="markdown-task"
1280 | | aria-label="Toggle task"
1281 | | checked={checked}
1282 | | onChange={(event) => {
1283 | | const markerOffset = Number(
1284 | | event.currentTarget.closest("li")?.dataset.taskMarkerOffset,
1285 | | );
1286 | | if (!Number.isSafeInteger(markerOffset)) return;
1287 | | onTaskListChange({ markerOffset, checked: event.currentTarget.checked });
1288 | | }}
1289 | | />
1290 | | );
1291 | -> }, 1292 | a({ node, href, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1292:8]
1291 | },
1292 | ,-> a({ node, href, children, ...props }) {
1293 | | const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : "";
1294 | | const fileLinkMeta = normalizedHref ? markdownFileLinkMetaByHref.get(normalizedHref) : null;
1295 | | if (!fileLinkMeta) {
1296 | | const faviconHost = resolveExternalLinkHost(href);
1297 | | const isSameDocumentLink = href?.startsWith("#") ?? false;
1298 | | const onClick = props.onClick;
1299 | | const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime();
1300 | | const link = (
1301 | | <a
1302 | | {...props}
1303 | | href={href}
1304 | | target={isSameDocumentLink ? undefined : "_blank"}
1305 | | rel={isSameDocumentLink ? undefined : "noopener noreferrer"}
1306 | | onClick={(event) => {
1307 | | onClick?.(event);
1308 | | if (isSameDocumentLink && href) {
1309 | | handleMarkdownFragmentClick(event, href);
1310 | | }
1311 | | }}
1312 | | onContextMenu={(event) => {
1313 | | if (!canOpenInPreview || !href) return;
1314 | | event.preventDefault();
1315 | | event.stopPropagation();
1316 | | const api = readLocalApi();
1317 | | if (!api) return;
1318 | | void api.contextMenu
1319 | | .show(
1320 | | [
1321 | | { id: "open-in-browser", label: "Open in integrated browser" },
1322 | | { id: "open-external", label: "Open in system browser" },
1323 | | ] as const,
1324 | | { x: event.clientX, y: event.clientY },
1325 | | )
1326 | | .then((clicked) => {
1327 | | if (clicked === "open-in-browser") {
1328 | | void openExternalLinkInPreview(href);
1329 | | return;
1330 | | }
1331 | | if (clicked === "open-external") return api.shell.openExternal(href);
1332 | | })
1333 | | .catch(() => undefined);
1334 | | }}
1335 | | >
1336 | | {faviconHost ? (
1337 | |
1338 | | {children}
1339 | |
1340 | | ) : (
1341 | | children
1342 | | )}
1343 | |
1344 | | );
1345 | | if (!faviconHost || !href) {
1346 | | return link;
1347 | | }
1348 | | return (
1349 | |
1350 | |
1351 | | <TooltipPopup
1352 | | side="top"
1353 | | className="max-w-[min(36rem,calc(100vw-2rem))] whitespace-normal leading-tight wrap-anywhere"
1354 | | >
1355 | | {href}
1356 | |
1357 | |
1358 | | );
1359 | | }
1360 | |
1361 | | const parentSuffix = fileLinkParentSuffixByPath.get(fileLinkMeta.filePath);
1362 | | const labelParts = [fileLinkMeta.basename];
1363 | | if (typeof parentSuffix === "string" && parentSuffix.length > 0) {
1364 | | labelParts.push(parentSuffix);
1365 | | }
1366 | | if (fileLinkMeta.line) {
1367 | | labelParts.push(
1368 | | L${fileLinkMeta.line}${fileLinkMeta.column ? :C${fileLinkMeta.column} : ""},
1369 | | );
1370 | | }
1371 | |
1372 | | return (
1373 | | <MarkdownFileLink
1374 | | href={fileLinkMeta.targetPath}
1375 | | targetPath={fileLinkMeta.targetPath}
1376 | | iconPath={fileLinkMeta.filePath}
1377 | | displayPath={fileLinkMeta.displayPath}
1378 | | workspaceRelativePath={fileLinkMeta.workspaceRelativePath}
1379 | | line={fileLinkMeta.line}
1380 | | label={labelParts.join(" · ")}
1381 | | copyMarkdown={[${fileLinkMeta.basename}](${normalizedHref})}
1382 | | theme={resolvedTheme}
1383 | | threadRef={threadRef}
1384 | | onOpen={openInPreferredEditor}
1385 | | onOpenInBrowser={
1386 | | threadRef &&
1387 | | isPreviewSupportedInRuntime() &&
1388 | | isBrowserPreviewFile(fileLinkMeta.filePath)
1389 | | ? () => openMarkdownFileInPreview(fileLinkMeta.filePath)
1390 | | : undefined
1391 | | }
1392 | | className={props.className}
1393 | | />
1394 | | );
1395 | -> }, 1396 | table({ node: _node, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1396:12]
1395 | },
1396 | ,-> table({ node: _node, ...props }) {
1397 | | return <MarkdownTable {...props} />;
1398 | -> }, 1399 | details({ node: _node, children, open: detailsOpen }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1399:14]
1398 | },
1399 | ,-> details({ node: _node, children, open: detailsOpen }) {
1400 | | return {children};
1401 | -> }, 1402 | pre({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1402:10]
1401 | },
1402 | ,-> pre({ node, children, ...props }) {
1403 | | const codeBlock = extractCodeBlock(children);
1404 | | if (!codeBlock) {
1405 | | return <pre {...props}>{children};
1406 | | }
1407 | |
1408 | | const language = extractFenceLanguage(codeBlock.className);
1409 | | const fenceTitle = extractFenceTitle(extractPreCodeMeta(node));
1410 | | return (
1411 | | <MarkdownCodeBlock
1412 | | code={codeBlock.code}
1413 | | language={language}
1414 | | fenceTitle={fenceTitle}
1415 | | theme={resolvedTheme}
1416 | | >
1417 | | <CodeHighlightErrorBoundary fallback={<pre {...props}>{children}}>
1418 | | <Suspense fallback={<pre {...props}>{children}}>
1419 | | <SuspenseShikiCodeBlock
1420 | | className={codeBlock.className}
1421 | | code={codeBlock.code}
1422 | | themeName={diffThemeName}
1423 | | isStreaming={isStreaming}
1424 | | />
1425 | |
1426 | |
1427 | |
1428 | | );
1429 | -> }, 1430 | }), ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/review/ReviewSheet.tsx:249:24]
248 | },
249 | ,-> headerTitle: () => (
250 | | <View style={{ alignItems: "center" }}>
251 | | <NativeText
252 | | numberOfLines={1}
253 | | style={{
254 | | fontFamily: "DMSans_700Bold",
255 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
256 | | fontWeight: "900",
257 | | color: headerForeground,
258 | | letterSpacing: -0.4,
259 | | }}
260 | | >
261 | | Files Changed
262 | |
263 | | <View
264 | | style={{
265 | | flexDirection: "row",
266 | | alignItems: "center",
267 | | justifyContent: "center",
268 | | gap: 6,
269 | | flexWrap: "wrap",
270 | | }}
271 | | >
272 | | {headerDiffSummary.additions && headerDiffSummary.deletions ? (
273 | | <>
274 | | <NativeText
275 | | style={{
276 | | fontFamily: "DMSans_700Bold",
277 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
278 | | fontWeight: "700",
279 | | color: "#16a34a",
280 | | }}
281 | | >
282 | | {headerDiffSummary.additions}
283 | |
284 | | <NativeText
285 | | style={{
286 | | fontFamily: "DMSans_700Bold",
287 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
288 | | fontWeight: "700",
289 | | color: "#e11d48",
290 | | }}
291 | | >
292 | | {headerDiffSummary.deletions}
293 | |
294 | | {pendingReviewCommentCount > 0 ? (
295 | | <NativeText
296 | | style={{
297 | | fontFamily: "DMSans_700Bold",
298 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
299 | | fontWeight: "700",
300 | | color: "#b45309",
301 | | }}
302 | | >
303 | | {pendingReviewCommentCount} pending
304 | |
305 | | ) : null}
306 | | </>
307 | | ) : (
308 | | <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
309 | | <NativeText
310 | | numberOfLines={1}
311 | | style={{
312 | | fontFamily: "DMSans_700Bold",
313 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
314 | | fontWeight: "700",
315 | | color: headerMuted,
316 | | }}
317 | | >
318 | | {selectedSection?.title ?? "Review changes"}
319 | |
320 | | {pendingReviewCommentCount > 0 ? (
321 | | <NativeText
322 | | style={{
323 | | fontFamily: "DMSans_700Bold",
324 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
325 | | fontWeight: "700",
326 | | color: "#b45309",
327 | | }}
328 | | >
329 | | {pendingReviewCommentCount} pending
330 | |
331 | | ) : null}
332 | |
333 | | )}
334 | |
335 | |
336 | -> ), 337 | }} ----
help: Move this component definition out of the parent component ReviewSheet and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/threads/ThreadRouteScreen.tsx:383:24]
382 | headerBackTitle: "",
383 | ,-> headerTitle: () => (
384 | | <Pressable
385 | | style={{ alignItems: "center", maxWidth: 200 }}
386 | | onLongPress={() => {
387 | | // TODO: trigger rename modal
388 | | }}
389 | | >
390 | | <RNText
391 | | numberOfLines={1}
392 | | style={{
393 | | fontFamily: "DMSans_700Bold",
394 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
395 | | fontWeight: "900",
396 | | color: foregroundColor,
397 | | letterSpacing: -0.4,
398 | | }}
399 | | >
400 | | {selectedThread.title}
401 | |
402 | | <RNText
403 | | numberOfLines={1}
404 | | style={{
405 | | fontFamily: "DMSans_700Bold",
406 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
407 | | fontWeight: "700",
408 | | color: secondaryFg,
409 | | letterSpacing: 0.3,
410 | | }}
411 | | >
412 | | {headerSubtitle}
413 | |
414 | |
415 | -> ), 416 | }} ----
help: Move this component definition out of the parent component ThreadRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:612:15]
611 | valuePrefix: "project",
612 | ,-> icon: (project) => (
613 | | <ProjectFavicon
614 | | environmentId={project.environmentId}
615 | | cwd={project.workspaceRoot}
616 | | className={ITEM_ICON_CLASS}
617 | | />
618 | -> ), 619 | runProject: openProjectFromSearch, ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:630:15]
629 | shortcutCommand: "chat.new",
630 | ,-> icon: (project) => (
631 | | <ProjectFavicon
632 | | environmentId={project.environmentId}
633 | | cwd={project.workspaceRoot}
634 | | className={ITEM_ICON_CLASS}
635 | | />
636 | -> ), 637 | runProject: async (project) => { ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx:911:24]
910 | title: "",
911 | ,-> headerTitle: () => (
912 | | <View
913 | | style={{
914 | | alignItems: "center",
915 | | gap: 1,
916 | | maxWidth: 240,
917 | | }}
918 | | >
919 | | <RNText
920 | | numberOfLines={1}
921 | | style={{
922 | | color: terminalTheme.foreground,
923 | | fontFamily: "DMSans_700Bold",
924 | | fontSize: MOBILE_TYPOGRAPHY.footnote.fontSize,
925 | | lineHeight: 16,
926 | | }}
927 | | >
928 | | {headerTitle.topLine}
929 | |
930 | | <RNText
931 | | ellipsizeMode="middle"
932 | | numberOfLines={1}
933 | | style={{
934 | | color: terminalTheme.mutedForeground,
935 | | fontFamily: "Menlo",
936 | | fontSize: MOBILE_TYPOGRAPHY.caption.fontSize,
937 | | lineHeight: 14,
938 | | }}
939 | | >
940 | | {headerTitle.bottomLine}
941 | |
942 | |
943 | -> ), 944 | }} ----
help: Move this component definition out of the parent component ThreadTerminalRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx:492:24]
491 | headerShadowVisible: false,
492 | headerTitle: () => ,
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
493 | headerSearchBarOptions: {
---- help: Move this component definition out of the parent component ThreadFilesTreeScreenand pass data as props. If you want to allow component creation in props, setallowAsProps` option to true.

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:293:15]
292 | const stored = yield* Effect.fromResult(
293 | Schema.decodeUnknownResult(StoredShellSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
294 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:313:13]
312 | const legacyStored = yield* Effect.fromResult(
313 | Schema.decodeUnknownResult(LegacyStoredShellSnapshot)(legacyParsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
314 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:328:13]
327 | const encoded = yield* Effect.fromResult(
328 | Schema.encodeUnknownResult(StoredShellSnapshot)(stored),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
329 | ).pipe(Effect.mapError((cause) => shellPersistenceError("save-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:355:13]
354 | const stored = yield* Effect.fromResult(
355 | Schema.decodeUnknownResult(StoredThreadSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
356 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-thread", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:365:13]
364 | const encoded = yield* Effect.fromResult(
365 | Schema.encodeUnknownResult(StoredThreadSnapshot)({
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
366 | schemaVersion: THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION,
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:31:5]
30 | return yield* Effect.fromResult(
31 | Schema.decodeUnknownResult(ConnectionCatalogDocument)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
32 | ).pipe(Effect.mapError((cause) => catalogError("decode", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:39:5]
38 | const encoded = yield* Effect.fromResult(
39 | Schema.encodeUnknownResult(ConnectionCatalogDocument)(catalog),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
40 | ).pipe(Effect.mapError((cause) => catalogError("encode", cause)));
`----

Found 0 errors and 20 warnings in 1748 files (978ms, 16 threads)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
~/packages/contracts$ tsgo --noEmit ⊘ cache disabled
~/apps/marketing$ astro check ⊘ cache disabled

~/oxlint-plugin-t3code$ tsgo --noEmit ⊘ cache disabled

~/packages/effect-codex-app-server$ tsgo --noEmit ⊘ cache disabled
08:12:00 [WARN] [vite] warning: optimizeDeps.esbuildOptions option was specified by "astro:dev-toolbar" plugin. This option is deprecated, please use optimizeDeps.rolldownOptions instead.
08:12:00 [WARN] [vite] You or a plugin you are using have set optimizeDeps.esbuildOptions but this option is now deprecated. Vite now uses Rolldown to optimize the dependencies. Please use optimizeDeps.rolldownOptions instead.
08:12:00 [types] Generated 39ms
08:12:00 [check] Getting diagnostics for Astro files in /Users/julius/.codex/worktrees/client-cloud-crypto-errors/apps/marketing...
Result (10 files):

  • 0 errors
  • 0 warnings
  • 0 hints

~/packages/effect-acp$ tsgo --noEmit ⊘ cache disabled

~/packages/shared$ tsgo --noEmit ⊘ cache disabled

~/packages/client-runtime$ tsgo --noEmit ⊘ cache disabled

~/scripts$ tsgo --noEmit ⊘ cache disabled

~/packages/ssh$ tsgo --noEmit ⊘ cache disabled

~/packages/tailscale$ tsgo --noEmit ⊘ cache disabled

~/apps/web$ tsgo --noEmit ⊘ cache disabled

~/apps/mobile$ tsc --noEmit ⊘ cache disabled

~/infra/relay$ tsgo --noEmit ⊘ cache disabled

~/apps/desktop$ tsgo --noEmit ⊘ cache disabled

~/apps/server$ tsgo --noEmit ⊘ cache disabled


vp run: 0/15 cache hit (0%). (Run vp run --last-details for full details)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
$ node scripts/mobile-native-static-check.ts ⊘ cache disabled
Found 6 Swift and 2 Kotlin native source files.
$ swiftlint lint --config .swiftlint.yml --strict
$ ktlint modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt
$ detekt --config detekt.yml --input modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt,modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt --build-upon-default-config
Skipping generated native project folders: android/, ios/.


Note

Medium Risk
Touches auth-related DPoP and secure token cache paths; error shape and message text change for any caller matching on strings, though signing/storage behavior is unchanged.

Overview
Replaces free-form message strings on mobile/web DPoP and mobile managed relay token failures with Effect Schema tagged errors that carry a discriminated operation literal and an optional cause (Defect), plus derived message getters.

On mobile DPoP, validation failures now use dedicated types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) wrapped as validate-key / derive-public-key where appropriate; invalid proof URLs fail as normalize-proof-urlwithout a cause. Web DPoP follows the same pattern (including isBrowserDpopError for decode chaining).

Managed relay token store errors are structured the same way; load/save/clear logging annotates the full error object and operation instead of a generic operation string. Tests assert operation (and causes where relevant) rather than fixed message text.

Reviewed by Cursor Bugbot for commit df4f4d7. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Structure cloud cryptography errors with operation-tagged Schema.TaggedErrorClass types

  • Replaces Data.TaggedError with Schema.TaggedErrorClass for CloudDpopError, BrowserDpopError, and ManagedRelayTokenStoreError, adding an operation literal discriminant and optional typed cause to each.
  • Each error class now exposes a message getter that formats the operation name, and new specific error types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) are introduced for granular failure cases.
  • Exports isBrowserDpopError and isManagedRelayTokenStoreError type guards via Schema.is.
  • Log annotations in managedRelayTokenStore now include the structured error object and its operation field instead of a freeform string.
  • Behavioral Change: callers that previously matched on error message strings must now inspect the structured operation field.

Macroscope summarized df4f4d7.

Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved

This PR refactors error classes to use structured typed operations instead of free-form string messages. The changes are purely structural with no runtime behavior modifications - errors are thrown at the same points with equivalent semantics.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
MemberAuthor

Superseded by the smaller, more structural #3277, #3307, and #3309. Those PRs retain storage/request identifiers and distinct DPoP failure types instead of combining all client cryptography changes into one branch.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[codex] Structure client cloud cryptography errors - #3395

Closed
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors
Closed

[codex] Structure client cloud cryptography errors#3395
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary\n\n- replace message-bearing DPoP errors with Schema errors carrying structured operation context\n- preserve exact lower-level causes and add typed key-format/mismatch causes for validation failures\n- remove valueless error constructor wrappers from web/mobile DPoP and mobile relay token storage\n- annotate best-effort token-store logging with the failing operation\n\n## Validation\n\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})

RUN /Users/julius/.codex/worktrees/client-cloud-crypto-errors

Test Files 3 passed (3)
Tests 10 passed (10)
Start at 08:11:56
Duration 376ms (transform 78ms, setup 0ms, import 632ms, tests 86ms, environment 0ms)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
�[1m�[94mpass:�[39m�[0m All 1848 files are correctly formatted �[2m(775ms, 16 threads)�[0m
! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1250:8]
1249 | () => ({
1250 | ,-> p({ node: _node, children, ...props }) {
1251 | | return <p {...props}>{renderSkillInlineMarkdownChildren(children, skills)}

;
1252 | -> }, 1253 | li({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1253:9]
1252 | },
1253 | ,-> li({ node, children, ...props }) {
1254 | | const listItemStart = node?.position?.start.offset;
1255 | | const markerOffset =
1256 | | typeof listItemStart === "number" ? findTaskListMarkerOffset(text, listItemStart) : null;
1257 | | return (
1258 | | <li {...props} data-task-marker-offset={markerOffset ?? undefined}>
1259 | | {renderSkillInlineMarkdownChildren(children, skills)}
1260 | |
1261 | | );
1262 | -> }, 1263 | input({ node: _node, type, checked, disabled: _disabled, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1263:12]
1262 | },
1263 | ,-> input({ node: _node, type, checked, disabled: _disabled, ...props }) {
1264 | | if (type !== "checkbox" || !onTaskListChange) {
1265 | | return (
1266 | | <input
1267 | | {...props}
1268 | | type={type}
1269 | | checked={checked}
1270 | | disabled={_disabled}
1271 | | readOnly={type === "checkbox"}
1272 | | />
1273 | | );
1274 | | }
1275 | | return (
1276 | | <input
1277 | | {...props}
1278 | | type="checkbox"
1279 | | name="markdown-task"
1280 | | aria-label="Toggle task"
1281 | | checked={checked}
1282 | | onChange={(event) => {
1283 | | const markerOffset = Number(
1284 | | event.currentTarget.closest("li")?.dataset.taskMarkerOffset,
1285 | | );
1286 | | if (!Number.isSafeInteger(markerOffset)) return;
1287 | | onTaskListChange({ markerOffset, checked: event.currentTarget.checked });
1288 | | }}
1289 | | />
1290 | | );
1291 | -> }, 1292 | a({ node, href, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1292:8]
1291 | },
1292 | ,-> a({ node, href, children, ...props }) {
1293 | | const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : "";
1294 | | const fileLinkMeta = normalizedHref ? markdownFileLinkMetaByHref.get(normalizedHref) : null;
1295 | | if (!fileLinkMeta) {
1296 | | const faviconHost = resolveExternalLinkHost(href);
1297 | | const isSameDocumentLink = href?.startsWith("#") ?? false;
1298 | | const onClick = props.onClick;
1299 | | const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime();
1300 | | const link = (
1301 | | <a
1302 | | {...props}
1303 | | href={href}
1304 | | target={isSameDocumentLink ? undefined : "_blank"}
1305 | | rel={isSameDocumentLink ? undefined : "noopener noreferrer"}
1306 | | onClick={(event) => {
1307 | | onClick?.(event);
1308 | | if (isSameDocumentLink && href) {
1309 | | handleMarkdownFragmentClick(event, href);
1310 | | }
1311 | | }}
1312 | | onContextMenu={(event) => {
1313 | | if (!canOpenInPreview || !href) return;
1314 | | event.preventDefault();
1315 | | event.stopPropagation();
1316 | | const api = readLocalApi();
1317 | | if (!api) return;
1318 | | void api.contextMenu
1319 | | .show(
1320 | | [
1321 | | { id: "open-in-browser", label: "Open in integrated browser" },
1322 | | { id: "open-external", label: "Open in system browser" },
1323 | | ] as const,
1324 | | { x: event.clientX, y: event.clientY },
1325 | | )
1326 | | .then((clicked) => {
1327 | | if (clicked === "open-in-browser") {
1328 | | void openExternalLinkInPreview(href);
1329 | | return;
1330 | | }
1331 | | if (clicked === "open-external") return api.shell.openExternal(href);
1332 | | })
1333 | | .catch(() => undefined);
1334 | | }}
1335 | | >
1336 | | {faviconHost ? (
1337 | |
1338 | | {children}
1339 | |
1340 | | ) : (
1341 | | children
1342 | | )}
1343 | |
1344 | | );
1345 | | if (!faviconHost || !href) {
1346 | | return link;
1347 | | }
1348 | | return (
1349 | |
1350 | |
1351 | | <TooltipPopup
1352 | | side="top"
1353 | | className="max-w-[min(36rem,calc(100vw-2rem))] whitespace-normal leading-tight wrap-anywhere"
1354 | | >
1355 | | {href}
1356 | |
1357 | |
1358 | | );
1359 | | }
1360 | |
1361 | | const parentSuffix = fileLinkParentSuffixByPath.get(fileLinkMeta.filePath);
1362 | | const labelParts = [fileLinkMeta.basename];
1363 | | if (typeof parentSuffix === "string" && parentSuffix.length > 0) {
1364 | | labelParts.push(parentSuffix);
1365 | | }
1366 | | if (fileLinkMeta.line) {
1367 | | labelParts.push(
1368 | | L${fileLinkMeta.line}${fileLinkMeta.column ? :C${fileLinkMeta.column} : ""},
1369 | | );
1370 | | }
1371 | |
1372 | | return (
1373 | | <MarkdownFileLink
1374 | | href={fileLinkMeta.targetPath}
1375 | | targetPath={fileLinkMeta.targetPath}
1376 | | iconPath={fileLinkMeta.filePath}
1377 | | displayPath={fileLinkMeta.displayPath}
1378 | | workspaceRelativePath={fileLinkMeta.workspaceRelativePath}
1379 | | line={fileLinkMeta.line}
1380 | | label={labelParts.join(" · ")}
1381 | | copyMarkdown={[${fileLinkMeta.basename}](${normalizedHref})}
1382 | | theme={resolvedTheme}
1383 | | threadRef={threadRef}
1384 | | onOpen={openInPreferredEditor}
1385 | | onOpenInBrowser={
1386 | | threadRef &&
1387 | | isPreviewSupportedInRuntime() &&
1388 | | isBrowserPreviewFile(fileLinkMeta.filePath)
1389 | | ? () => openMarkdownFileInPreview(fileLinkMeta.filePath)
1390 | | : undefined
1391 | | }
1392 | | className={props.className}
1393 | | />
1394 | | );
1395 | -> }, 1396 | table({ node: _node, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1396:12]
1395 | },
1396 | ,-> table({ node: _node, ...props }) {
1397 | | return <MarkdownTable {...props} />;
1398 | -> }, 1399 | details({ node: _node, children, open: detailsOpen }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1399:14]
1398 | },
1399 | ,-> details({ node: _node, children, open: detailsOpen }) {
1400 | | return {children};
1401 | -> }, 1402 | pre({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1402:10]
1401 | },
1402 | ,-> pre({ node, children, ...props }) {
1403 | | const codeBlock = extractCodeBlock(children);
1404 | | if (!codeBlock) {
1405 | | return <pre {...props}>{children};
1406 | | }
1407 | |
1408 | | const language = extractFenceLanguage(codeBlock.className);
1409 | | const fenceTitle = extractFenceTitle(extractPreCodeMeta(node));
1410 | | return (
1411 | | <MarkdownCodeBlock
1412 | | code={codeBlock.code}
1413 | | language={language}
1414 | | fenceTitle={fenceTitle}
1415 | | theme={resolvedTheme}
1416 | | >
1417 | | <CodeHighlightErrorBoundary fallback={<pre {...props}>{children}}>
1418 | | <Suspense fallback={<pre {...props}>{children}}>
1419 | | <SuspenseShikiCodeBlock
1420 | | className={codeBlock.className}
1421 | | code={codeBlock.code}
1422 | | themeName={diffThemeName}
1423 | | isStreaming={isStreaming}
1424 | | />
1425 | |
1426 | |
1427 | |
1428 | | );
1429 | -> }, 1430 | }), ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/review/ReviewSheet.tsx:249:24]
248 | },
249 | ,-> headerTitle: () => (
250 | | <View style={{ alignItems: "center" }}>
251 | | <NativeText
252 | | numberOfLines={1}
253 | | style={{
254 | | fontFamily: "DMSans_700Bold",
255 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
256 | | fontWeight: "900",
257 | | color: headerForeground,
258 | | letterSpacing: -0.4,
259 | | }}
260 | | >
261 | | Files Changed
262 | |
263 | | <View
264 | | style={{
265 | | flexDirection: "row",
266 | | alignItems: "center",
267 | | justifyContent: "center",
268 | | gap: 6,
269 | | flexWrap: "wrap",
270 | | }}
271 | | >
272 | | {headerDiffSummary.additions && headerDiffSummary.deletions ? (
273 | | <>
274 | | <NativeText
275 | | style={{
276 | | fontFamily: "DMSans_700Bold",
277 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
278 | | fontWeight: "700",
279 | | color: "#16a34a",
280 | | }}
281 | | >
282 | | {headerDiffSummary.additions}
283 | |
284 | | <NativeText
285 | | style={{
286 | | fontFamily: "DMSans_700Bold",
287 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
288 | | fontWeight: "700",
289 | | color: "#e11d48",
290 | | }}
291 | | >
292 | | {headerDiffSummary.deletions}
293 | |
294 | | {pendingReviewCommentCount > 0 ? (
295 | | <NativeText
296 | | style={{
297 | | fontFamily: "DMSans_700Bold",
298 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
299 | | fontWeight: "700",
300 | | color: "#b45309",
301 | | }}
302 | | >
303 | | {pendingReviewCommentCount} pending
304 | |
305 | | ) : null}
306 | | </>
307 | | ) : (
308 | | <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
309 | | <NativeText
310 | | numberOfLines={1}
311 | | style={{
312 | | fontFamily: "DMSans_700Bold",
313 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
314 | | fontWeight: "700",
315 | | color: headerMuted,
316 | | }}
317 | | >
318 | | {selectedSection?.title ?? "Review changes"}
319 | |
320 | | {pendingReviewCommentCount > 0 ? (
321 | | <NativeText
322 | | style={{
323 | | fontFamily: "DMSans_700Bold",
324 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
325 | | fontWeight: "700",
326 | | color: "#b45309",
327 | | }}
328 | | >
329 | | {pendingReviewCommentCount} pending
330 | |
331 | | ) : null}
332 | |
333 | | )}
334 | |
335 | |
336 | -> ), 337 | }} ----
help: Move this component definition out of the parent component ReviewSheet and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/threads/ThreadRouteScreen.tsx:383:24]
382 | headerBackTitle: "",
383 | ,-> headerTitle: () => (
384 | | <Pressable
385 | | style={{ alignItems: "center", maxWidth: 200 }}
386 | | onLongPress={() => {
387 | | // TODO: trigger rename modal
388 | | }}
389 | | >
390 | | <RNText
391 | | numberOfLines={1}
392 | | style={{
393 | | fontFamily: "DMSans_700Bold",
394 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
395 | | fontWeight: "900",
396 | | color: foregroundColor,
397 | | letterSpacing: -0.4,
398 | | }}
399 | | >
400 | | {selectedThread.title}
401 | |
402 | | <RNText
403 | | numberOfLines={1}
404 | | style={{
405 | | fontFamily: "DMSans_700Bold",
406 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
407 | | fontWeight: "700",
408 | | color: secondaryFg,
409 | | letterSpacing: 0.3,
410 | | }}
411 | | >
412 | | {headerSubtitle}
413 | |
414 | |
415 | -> ), 416 | }} ----
help: Move this component definition out of the parent component ThreadRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:612:15]
611 | valuePrefix: "project",
612 | ,-> icon: (project) => (
613 | | <ProjectFavicon
614 | | environmentId={project.environmentId}
615 | | cwd={project.workspaceRoot}
616 | | className={ITEM_ICON_CLASS}
617 | | />
618 | -> ), 619 | runProject: openProjectFromSearch, ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:630:15]
629 | shortcutCommand: "chat.new",
630 | ,-> icon: (project) => (
631 | | <ProjectFavicon
632 | | environmentId={project.environmentId}
633 | | cwd={project.workspaceRoot}
634 | | className={ITEM_ICON_CLASS}
635 | | />
636 | -> ), 637 | runProject: async (project) => { ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx:911:24]
910 | title: "",
911 | ,-> headerTitle: () => (
912 | | <View
913 | | style={{
914 | | alignItems: "center",
915 | | gap: 1,
916 | | maxWidth: 240,
917 | | }}
918 | | >
919 | | <RNText
920 | | numberOfLines={1}
921 | | style={{
922 | | color: terminalTheme.foreground,
923 | | fontFamily: "DMSans_700Bold",
924 | | fontSize: MOBILE_TYPOGRAPHY.footnote.fontSize,
925 | | lineHeight: 16,
926 | | }}
927 | | >
928 | | {headerTitle.topLine}
929 | |
930 | | <RNText
931 | | ellipsizeMode="middle"
932 | | numberOfLines={1}
933 | | style={{
934 | | color: terminalTheme.mutedForeground,
935 | | fontFamily: "Menlo",
936 | | fontSize: MOBILE_TYPOGRAPHY.caption.fontSize,
937 | | lineHeight: 14,
938 | | }}
939 | | >
940 | | {headerTitle.bottomLine}
941 | |
942 | |
943 | -> ), 944 | }} ----
help: Move this component definition out of the parent component ThreadTerminalRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx:492:24]
491 | headerShadowVisible: false,
492 | headerTitle: () => ,
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
493 | headerSearchBarOptions: {
---- help: Move this component definition out of the parent component ThreadFilesTreeScreenand pass data as props. If you want to allow component creation in props, setallowAsProps` option to true.

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:293:15]
292 | const stored = yield* Effect.fromResult(
293 | Schema.decodeUnknownResult(StoredShellSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
294 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:313:13]
312 | const legacyStored = yield* Effect.fromResult(
313 | Schema.decodeUnknownResult(LegacyStoredShellSnapshot)(legacyParsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
314 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:328:13]
327 | const encoded = yield* Effect.fromResult(
328 | Schema.encodeUnknownResult(StoredShellSnapshot)(stored),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
329 | ).pipe(Effect.mapError((cause) => shellPersistenceError("save-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:355:13]
354 | const stored = yield* Effect.fromResult(
355 | Schema.decodeUnknownResult(StoredThreadSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
356 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-thread", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:365:13]
364 | const encoded = yield* Effect.fromResult(
365 | Schema.encodeUnknownResult(StoredThreadSnapshot)({
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
366 | schemaVersion: THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION,
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:31:5]
30 | return yield* Effect.fromResult(
31 | Schema.decodeUnknownResult(ConnectionCatalogDocument)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
32 | ).pipe(Effect.mapError((cause) => catalogError("decode", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:39:5]
38 | const encoded = yield* Effect.fromResult(
39 | Schema.encodeUnknownResult(ConnectionCatalogDocument)(catalog),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
40 | ).pipe(Effect.mapError((cause) => catalogError("encode", cause)));
`----

Found 0 errors and 20 warnings in 1748 files (978ms, 16 threads)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
~/packages/contracts$ tsgo --noEmit ⊘ cache disabled
~/apps/marketing$ astro check ⊘ cache disabled

~/oxlint-plugin-t3code$ tsgo --noEmit ⊘ cache disabled

~/packages/effect-codex-app-server$ tsgo --noEmit ⊘ cache disabled
08:12:00 [WARN] [vite] warning: optimizeDeps.esbuildOptions option was specified by "astro:dev-toolbar" plugin. This option is deprecated, please use optimizeDeps.rolldownOptions instead.
08:12:00 [WARN] [vite] You or a plugin you are using have set optimizeDeps.esbuildOptions but this option is now deprecated. Vite now uses Rolldown to optimize the dependencies. Please use optimizeDeps.rolldownOptions instead.
08:12:00 [types] Generated 39ms
08:12:00 [check] Getting diagnostics for Astro files in /Users/julius/.codex/worktrees/client-cloud-crypto-errors/apps/marketing...
Result (10 files):

  • 0 errors
  • 0 warnings
  • 0 hints

~/packages/effect-acp$ tsgo --noEmit ⊘ cache disabled

~/packages/shared$ tsgo --noEmit ⊘ cache disabled

~/packages/client-runtime$ tsgo --noEmit ⊘ cache disabled

~/scripts$ tsgo --noEmit ⊘ cache disabled

~/packages/ssh$ tsgo --noEmit ⊘ cache disabled

~/packages/tailscale$ tsgo --noEmit ⊘ cache disabled

~/apps/web$ tsgo --noEmit ⊘ cache disabled

~/apps/mobile$ tsc --noEmit ⊘ cache disabled

~/infra/relay$ tsgo --noEmit ⊘ cache disabled

~/apps/desktop$ tsgo --noEmit ⊘ cache disabled

~/apps/server$ tsgo --noEmit ⊘ cache disabled


vp run: 0/15 cache hit (0%). (Run vp run --last-details for full details)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
$ node scripts/mobile-native-static-check.ts ⊘ cache disabled
Found 6 Swift and 2 Kotlin native source files.
$ swiftlint lint --config .swiftlint.yml --strict
$ ktlint modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt
$ detekt --config detekt.yml --input modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt,modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt --build-upon-default-config
Skipping generated native project folders: android/, ios/.


Note

Medium Risk
Touches auth-related DPoP and secure token cache paths; error shape and message text change for any caller matching on strings, though signing/storage behavior is unchanged.

Overview
Replaces free-form message strings on mobile/web DPoP and mobile managed relay token failures with Effect Schema tagged errors that carry a discriminated operation literal and an optional cause (Defect), plus derived message getters.

On mobile DPoP, validation failures now use dedicated types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) wrapped as validate-key / derive-public-key where appropriate; invalid proof URLs fail as normalize-proof-urlwithout a cause. Web DPoP follows the same pattern (including isBrowserDpopError for decode chaining).

Managed relay token store errors are structured the same way; load/save/clear logging annotates the full error object and operation instead of a generic operation string. Tests assert operation (and causes where relevant) rather than fixed message text.

Reviewed by Cursor Bugbot for commit df4f4d7. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Structure cloud cryptography errors with operation-tagged Schema.TaggedErrorClass types

  • Replaces Data.TaggedError with Schema.TaggedErrorClass for CloudDpopError, BrowserDpopError, and ManagedRelayTokenStoreError, adding an operation literal discriminant and optional typed cause to each.
  • Each error class now exposes a message getter that formats the operation name, and new specific error types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) are introduced for granular failure cases.
  • Exports isBrowserDpopError and isManagedRelayTokenStoreError type guards via Schema.is.
  • Log annotations in managedRelayTokenStore now include the structured error object and its operation field instead of a freeform string.
  • Behavioral Change: callers that previously matched on error message strings must now inspect the structured operation field.

Macroscope summarized df4f4d7.

Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved

This PR refactors error classes to use structured typed operations instead of free-form string messages. The changes are purely structural with no runtime behavior modifications - errors are thrown at the same points with equivalent semantics.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
MemberAuthor

Superseded by the smaller, more structural #3277, #3307, and #3309. Those PRs retain storage/request identifiers and distinct DPoP failure types instead of combining all client cryptography changes into one branch.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[codex] Structure client cloud cryptography errors - #3395

Closed
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors
Closed

[codex] Structure client cloud cryptography errors#3395
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary\n\n- replace message-bearing DPoP errors with Schema errors carrying structured operation context\n- preserve exact lower-level causes and add typed key-format/mismatch causes for validation failures\n- remove valueless error constructor wrappers from web/mobile DPoP and mobile relay token storage\n- annotate best-effort token-store logging with the failing operation\n\n## Validation\n\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})

RUN /Users/julius/.codex/worktrees/client-cloud-crypto-errors

Test Files 3 passed (3)
Tests 10 passed (10)
Start at 08:11:56
Duration 376ms (transform 78ms, setup 0ms, import 632ms, tests 86ms, environment 0ms)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
�[1m�[94mpass:�[39m�[0m All 1848 files are correctly formatted �[2m(775ms, 16 threads)�[0m
! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1250:8]
1249 | () => ({
1250 | ,-> p({ node: _node, children, ...props }) {
1251 | | return <p {...props}>{renderSkillInlineMarkdownChildren(children, skills)}

;
1252 | -> }, 1253 | li({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1253:9]
1252 | },
1253 | ,-> li({ node, children, ...props }) {
1254 | | const listItemStart = node?.position?.start.offset;
1255 | | const markerOffset =
1256 | | typeof listItemStart === "number" ? findTaskListMarkerOffset(text, listItemStart) : null;
1257 | | return (
1258 | | <li {...props} data-task-marker-offset={markerOffset ?? undefined}>
1259 | | {renderSkillInlineMarkdownChildren(children, skills)}
1260 | |
1261 | | );
1262 | -> }, 1263 | input({ node: _node, type, checked, disabled: _disabled, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1263:12]
1262 | },
1263 | ,-> input({ node: _node, type, checked, disabled: _disabled, ...props }) {
1264 | | if (type !== "checkbox" || !onTaskListChange) {
1265 | | return (
1266 | | <input
1267 | | {...props}
1268 | | type={type}
1269 | | checked={checked}
1270 | | disabled={_disabled}
1271 | | readOnly={type === "checkbox"}
1272 | | />
1273 | | );
1274 | | }
1275 | | return (
1276 | | <input
1277 | | {...props}
1278 | | type="checkbox"
1279 | | name="markdown-task"
1280 | | aria-label="Toggle task"
1281 | | checked={checked}
1282 | | onChange={(event) => {
1283 | | const markerOffset = Number(
1284 | | event.currentTarget.closest("li")?.dataset.taskMarkerOffset,
1285 | | );
1286 | | if (!Number.isSafeInteger(markerOffset)) return;
1287 | | onTaskListChange({ markerOffset, checked: event.currentTarget.checked });
1288 | | }}
1289 | | />
1290 | | );
1291 | -> }, 1292 | a({ node, href, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1292:8]
1291 | },
1292 | ,-> a({ node, href, children, ...props }) {
1293 | | const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : "";
1294 | | const fileLinkMeta = normalizedHref ? markdownFileLinkMetaByHref.get(normalizedHref) : null;
1295 | | if (!fileLinkMeta) {
1296 | | const faviconHost = resolveExternalLinkHost(href);
1297 | | const isSameDocumentLink = href?.startsWith("#") ?? false;
1298 | | const onClick = props.onClick;
1299 | | const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime();
1300 | | const link = (
1301 | | <a
1302 | | {...props}
1303 | | href={href}
1304 | | target={isSameDocumentLink ? undefined : "_blank"}
1305 | | rel={isSameDocumentLink ? undefined : "noopener noreferrer"}
1306 | | onClick={(event) => {
1307 | | onClick?.(event);
1308 | | if (isSameDocumentLink && href) {
1309 | | handleMarkdownFragmentClick(event, href);
1310 | | }
1311 | | }}
1312 | | onContextMenu={(event) => {
1313 | | if (!canOpenInPreview || !href) return;
1314 | | event.preventDefault();
1315 | | event.stopPropagation();
1316 | | const api = readLocalApi();
1317 | | if (!api) return;
1318 | | void api.contextMenu
1319 | | .show(
1320 | | [
1321 | | { id: "open-in-browser", label: "Open in integrated browser" },
1322 | | { id: "open-external", label: "Open in system browser" },
1323 | | ] as const,
1324 | | { x: event.clientX, y: event.clientY },
1325 | | )
1326 | | .then((clicked) => {
1327 | | if (clicked === "open-in-browser") {
1328 | | void openExternalLinkInPreview(href);
1329 | | return;
1330 | | }
1331 | | if (clicked === "open-external") return api.shell.openExternal(href);
1332 | | })
1333 | | .catch(() => undefined);
1334 | | }}
1335 | | >
1336 | | {faviconHost ? (
1337 | |
1338 | | {children}
1339 | |
1340 | | ) : (
1341 | | children
1342 | | )}
1343 | |
1344 | | );
1345 | | if (!faviconHost || !href) {
1346 | | return link;
1347 | | }
1348 | | return (
1349 | |
1350 | |
1351 | | <TooltipPopup
1352 | | side="top"
1353 | | className="max-w-[min(36rem,calc(100vw-2rem))] whitespace-normal leading-tight wrap-anywhere"
1354 | | >
1355 | | {href}
1356 | |
1357 | |
1358 | | );
1359 | | }
1360 | |
1361 | | const parentSuffix = fileLinkParentSuffixByPath.get(fileLinkMeta.filePath);
1362 | | const labelParts = [fileLinkMeta.basename];
1363 | | if (typeof parentSuffix === "string" && parentSuffix.length > 0) {
1364 | | labelParts.push(parentSuffix);
1365 | | }
1366 | | if (fileLinkMeta.line) {
1367 | | labelParts.push(
1368 | | L${fileLinkMeta.line}${fileLinkMeta.column ? :C${fileLinkMeta.column} : ""},
1369 | | );
1370 | | }
1371 | |
1372 | | return (
1373 | | <MarkdownFileLink
1374 | | href={fileLinkMeta.targetPath}
1375 | | targetPath={fileLinkMeta.targetPath}
1376 | | iconPath={fileLinkMeta.filePath}
1377 | | displayPath={fileLinkMeta.displayPath}
1378 | | workspaceRelativePath={fileLinkMeta.workspaceRelativePath}
1379 | | line={fileLinkMeta.line}
1380 | | label={labelParts.join(" · ")}
1381 | | copyMarkdown={[${fileLinkMeta.basename}](${normalizedHref})}
1382 | | theme={resolvedTheme}
1383 | | threadRef={threadRef}
1384 | | onOpen={openInPreferredEditor}
1385 | | onOpenInBrowser={
1386 | | threadRef &&
1387 | | isPreviewSupportedInRuntime() &&
1388 | | isBrowserPreviewFile(fileLinkMeta.filePath)
1389 | | ? () => openMarkdownFileInPreview(fileLinkMeta.filePath)
1390 | | : undefined
1391 | | }
1392 | | className={props.className}
1393 | | />
1394 | | );
1395 | -> }, 1396 | table({ node: _node, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1396:12]
1395 | },
1396 | ,-> table({ node: _node, ...props }) {
1397 | | return <MarkdownTable {...props} />;
1398 | -> }, 1399 | details({ node: _node, children, open: detailsOpen }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1399:14]
1398 | },
1399 | ,-> details({ node: _node, children, open: detailsOpen }) {
1400 | | return {children};
1401 | -> }, 1402 | pre({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1402:10]
1401 | },
1402 | ,-> pre({ node, children, ...props }) {
1403 | | const codeBlock = extractCodeBlock(children);
1404 | | if (!codeBlock) {
1405 | | return <pre {...props}>{children};
1406 | | }
1407 | |
1408 | | const language = extractFenceLanguage(codeBlock.className);
1409 | | const fenceTitle = extractFenceTitle(extractPreCodeMeta(node));
1410 | | return (
1411 | | <MarkdownCodeBlock
1412 | | code={codeBlock.code}
1413 | | language={language}
1414 | | fenceTitle={fenceTitle}
1415 | | theme={resolvedTheme}
1416 | | >
1417 | | <CodeHighlightErrorBoundary fallback={<pre {...props}>{children}}>
1418 | | <Suspense fallback={<pre {...props}>{children}}>
1419 | | <SuspenseShikiCodeBlock
1420 | | className={codeBlock.className}
1421 | | code={codeBlock.code}
1422 | | themeName={diffThemeName}
1423 | | isStreaming={isStreaming}
1424 | | />
1425 | |
1426 | |
1427 | |
1428 | | );
1429 | -> }, 1430 | }), ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/review/ReviewSheet.tsx:249:24]
248 | },
249 | ,-> headerTitle: () => (
250 | | <View style={{ alignItems: "center" }}>
251 | | <NativeText
252 | | numberOfLines={1}
253 | | style={{
254 | | fontFamily: "DMSans_700Bold",
255 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
256 | | fontWeight: "900",
257 | | color: headerForeground,
258 | | letterSpacing: -0.4,
259 | | }}
260 | | >
261 | | Files Changed
262 | |
263 | | <View
264 | | style={{
265 | | flexDirection: "row",
266 | | alignItems: "center",
267 | | justifyContent: "center",
268 | | gap: 6,
269 | | flexWrap: "wrap",
270 | | }}
271 | | >
272 | | {headerDiffSummary.additions && headerDiffSummary.deletions ? (
273 | | <>
274 | | <NativeText
275 | | style={{
276 | | fontFamily: "DMSans_700Bold",
277 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
278 | | fontWeight: "700",
279 | | color: "#16a34a",
280 | | }}
281 | | >
282 | | {headerDiffSummary.additions}
283 | |
284 | | <NativeText
285 | | style={{
286 | | fontFamily: "DMSans_700Bold",
287 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
288 | | fontWeight: "700",
289 | | color: "#e11d48",
290 | | }}
291 | | >
292 | | {headerDiffSummary.deletions}
293 | |
294 | | {pendingReviewCommentCount > 0 ? (
295 | | <NativeText
296 | | style={{
297 | | fontFamily: "DMSans_700Bold",
298 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
299 | | fontWeight: "700",
300 | | color: "#b45309",
301 | | }}
302 | | >
303 | | {pendingReviewCommentCount} pending
304 | |
305 | | ) : null}
306 | | </>
307 | | ) : (
308 | | <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
309 | | <NativeText
310 | | numberOfLines={1}
311 | | style={{
312 | | fontFamily: "DMSans_700Bold",
313 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
314 | | fontWeight: "700",
315 | | color: headerMuted,
316 | | }}
317 | | >
318 | | {selectedSection?.title ?? "Review changes"}
319 | |
320 | | {pendingReviewCommentCount > 0 ? (
321 | | <NativeText
322 | | style={{
323 | | fontFamily: "DMSans_700Bold",
324 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
325 | | fontWeight: "700",
326 | | color: "#b45309",
327 | | }}
328 | | >
329 | | {pendingReviewCommentCount} pending
330 | |
331 | | ) : null}
332 | |
333 | | )}
334 | |
335 | |
336 | -> ), 337 | }} ----
help: Move this component definition out of the parent component ReviewSheet and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/threads/ThreadRouteScreen.tsx:383:24]
382 | headerBackTitle: "",
383 | ,-> headerTitle: () => (
384 | | <Pressable
385 | | style={{ alignItems: "center", maxWidth: 200 }}
386 | | onLongPress={() => {
387 | | // TODO: trigger rename modal
388 | | }}
389 | | >
390 | | <RNText
391 | | numberOfLines={1}
392 | | style={{
393 | | fontFamily: "DMSans_700Bold",
394 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
395 | | fontWeight: "900",
396 | | color: foregroundColor,
397 | | letterSpacing: -0.4,
398 | | }}
399 | | >
400 | | {selectedThread.title}
401 | |
402 | | <RNText
403 | | numberOfLines={1}
404 | | style={{
405 | | fontFamily: "DMSans_700Bold",
406 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
407 | | fontWeight: "700",
408 | | color: secondaryFg,
409 | | letterSpacing: 0.3,
410 | | }}
411 | | >
412 | | {headerSubtitle}
413 | |
414 | |
415 | -> ), 416 | }} ----
help: Move this component definition out of the parent component ThreadRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:612:15]
611 | valuePrefix: "project",
612 | ,-> icon: (project) => (
613 | | <ProjectFavicon
614 | | environmentId={project.environmentId}
615 | | cwd={project.workspaceRoot}
616 | | className={ITEM_ICON_CLASS}
617 | | />
618 | -> ), 619 | runProject: openProjectFromSearch, ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:630:15]
629 | shortcutCommand: "chat.new",
630 | ,-> icon: (project) => (
631 | | <ProjectFavicon
632 | | environmentId={project.environmentId}
633 | | cwd={project.workspaceRoot}
634 | | className={ITEM_ICON_CLASS}
635 | | />
636 | -> ), 637 | runProject: async (project) => { ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx:911:24]
910 | title: "",
911 | ,-> headerTitle: () => (
912 | | <View
913 | | style={{
914 | | alignItems: "center",
915 | | gap: 1,
916 | | maxWidth: 240,
917 | | }}
918 | | >
919 | | <RNText
920 | | numberOfLines={1}
921 | | style={{
922 | | color: terminalTheme.foreground,
923 | | fontFamily: "DMSans_700Bold",
924 | | fontSize: MOBILE_TYPOGRAPHY.footnote.fontSize,
925 | | lineHeight: 16,
926 | | }}
927 | | >
928 | | {headerTitle.topLine}
929 | |
930 | | <RNText
931 | | ellipsizeMode="middle"
932 | | numberOfLines={1}
933 | | style={{
934 | | color: terminalTheme.mutedForeground,
935 | | fontFamily: "Menlo",
936 | | fontSize: MOBILE_TYPOGRAPHY.caption.fontSize,
937 | | lineHeight: 14,
938 | | }}
939 | | >
940 | | {headerTitle.bottomLine}
941 | |
942 | |
943 | -> ), 944 | }} ----
help: Move this component definition out of the parent component ThreadTerminalRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx:492:24]
491 | headerShadowVisible: false,
492 | headerTitle: () => ,
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
493 | headerSearchBarOptions: {
---- help: Move this component definition out of the parent component ThreadFilesTreeScreenand pass data as props. If you want to allow component creation in props, setallowAsProps` option to true.

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:293:15]
292 | const stored = yield* Effect.fromResult(
293 | Schema.decodeUnknownResult(StoredShellSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
294 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:313:13]
312 | const legacyStored = yield* Effect.fromResult(
313 | Schema.decodeUnknownResult(LegacyStoredShellSnapshot)(legacyParsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
314 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:328:13]
327 | const encoded = yield* Effect.fromResult(
328 | Schema.encodeUnknownResult(StoredShellSnapshot)(stored),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
329 | ).pipe(Effect.mapError((cause) => shellPersistenceError("save-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:355:13]
354 | const stored = yield* Effect.fromResult(
355 | Schema.decodeUnknownResult(StoredThreadSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
356 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-thread", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:365:13]
364 | const encoded = yield* Effect.fromResult(
365 | Schema.encodeUnknownResult(StoredThreadSnapshot)({
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
366 | schemaVersion: THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION,
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:31:5]
30 | return yield* Effect.fromResult(
31 | Schema.decodeUnknownResult(ConnectionCatalogDocument)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
32 | ).pipe(Effect.mapError((cause) => catalogError("decode", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:39:5]
38 | const encoded = yield* Effect.fromResult(
39 | Schema.encodeUnknownResult(ConnectionCatalogDocument)(catalog),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
40 | ).pipe(Effect.mapError((cause) => catalogError("encode", cause)));
`----

Found 0 errors and 20 warnings in 1748 files (978ms, 16 threads)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
~/packages/contracts$ tsgo --noEmit ⊘ cache disabled
~/apps/marketing$ astro check ⊘ cache disabled

~/oxlint-plugin-t3code$ tsgo --noEmit ⊘ cache disabled

~/packages/effect-codex-app-server$ tsgo --noEmit ⊘ cache disabled
08:12:00 [WARN] [vite] warning: optimizeDeps.esbuildOptions option was specified by "astro:dev-toolbar" plugin. This option is deprecated, please use optimizeDeps.rolldownOptions instead.
08:12:00 [WARN] [vite] You or a plugin you are using have set optimizeDeps.esbuildOptions but this option is now deprecated. Vite now uses Rolldown to optimize the dependencies. Please use optimizeDeps.rolldownOptions instead.
08:12:00 [types] Generated 39ms
08:12:00 [check] Getting diagnostics for Astro files in /Users/julius/.codex/worktrees/client-cloud-crypto-errors/apps/marketing...
Result (10 files):

  • 0 errors
  • 0 warnings
  • 0 hints

~/packages/effect-acp$ tsgo --noEmit ⊘ cache disabled

~/packages/shared$ tsgo --noEmit ⊘ cache disabled

~/packages/client-runtime$ tsgo --noEmit ⊘ cache disabled

~/scripts$ tsgo --noEmit ⊘ cache disabled

~/packages/ssh$ tsgo --noEmit ⊘ cache disabled

~/packages/tailscale$ tsgo --noEmit ⊘ cache disabled

~/apps/web$ tsgo --noEmit ⊘ cache disabled

~/apps/mobile$ tsc --noEmit ⊘ cache disabled

~/infra/relay$ tsgo --noEmit ⊘ cache disabled

~/apps/desktop$ tsgo --noEmit ⊘ cache disabled

~/apps/server$ tsgo --noEmit ⊘ cache disabled


vp run: 0/15 cache hit (0%). (Run vp run --last-details for full details)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
$ node scripts/mobile-native-static-check.ts ⊘ cache disabled
Found 6 Swift and 2 Kotlin native source files.
$ swiftlint lint --config .swiftlint.yml --strict
$ ktlint modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt
$ detekt --config detekt.yml --input modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt,modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt --build-upon-default-config
Skipping generated native project folders: android/, ios/.


Note

Medium Risk
Touches auth-related DPoP and secure token cache paths; error shape and message text change for any caller matching on strings, though signing/storage behavior is unchanged.

Overview
Replaces free-form message strings on mobile/web DPoP and mobile managed relay token failures with Effect Schema tagged errors that carry a discriminated operation literal and an optional cause (Defect), plus derived message getters.

On mobile DPoP, validation failures now use dedicated types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) wrapped as validate-key / derive-public-key where appropriate; invalid proof URLs fail as normalize-proof-urlwithout a cause. Web DPoP follows the same pattern (including isBrowserDpopError for decode chaining).

Managed relay token store errors are structured the same way; load/save/clear logging annotates the full error object and operation instead of a generic operation string. Tests assert operation (and causes where relevant) rather than fixed message text.

Reviewed by Cursor Bugbot for commit df4f4d7. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Structure cloud cryptography errors with operation-tagged Schema.TaggedErrorClass types

  • Replaces Data.TaggedError with Schema.TaggedErrorClass for CloudDpopError, BrowserDpopError, and ManagedRelayTokenStoreError, adding an operation literal discriminant and optional typed cause to each.
  • Each error class now exposes a message getter that formats the operation name, and new specific error types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) are introduced for granular failure cases.
  • Exports isBrowserDpopError and isManagedRelayTokenStoreError type guards via Schema.is.
  • Log annotations in managedRelayTokenStore now include the structured error object and its operation field instead of a freeform string.
  • Behavioral Change: callers that previously matched on error message strings must now inspect the structured operation field.

Macroscope summarized df4f4d7.

Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved

This PR refactors error classes to use structured typed operations instead of free-form string messages. The changes are purely structural with no runtime behavior modifications - errors are thrown at the same points with equivalent semantics.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
MemberAuthor

Superseded by the smaller, more structural #3277, #3307, and #3309. Those PRs retain storage/request identifiers and distinct DPoP failure types instead of combining all client cryptography changes into one branch.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[codex] Structure client cloud cryptography errors - #3395

Closed
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors
Closed

[codex] Structure client cloud cryptography errors#3395
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary\n\n- replace message-bearing DPoP errors with Schema errors carrying structured operation context\n- preserve exact lower-level causes and add typed key-format/mismatch causes for validation failures\n- remove valueless error constructor wrappers from web/mobile DPoP and mobile relay token storage\n- annotate best-effort token-store logging with the failing operation\n\n## Validation\n\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})

RUN /Users/julius/.codex/worktrees/client-cloud-crypto-errors

Test Files 3 passed (3)
Tests 10 passed (10)
Start at 08:11:56
Duration 376ms (transform 78ms, setup 0ms, import 632ms, tests 86ms, environment 0ms)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
�[1m�[94mpass:�[39m�[0m All 1848 files are correctly formatted �[2m(775ms, 16 threads)�[0m
! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1250:8]
1249 | () => ({
1250 | ,-> p({ node: _node, children, ...props }) {
1251 | | return <p {...props}>{renderSkillInlineMarkdownChildren(children, skills)}

;
1252 | -> }, 1253 | li({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1253:9]
1252 | },
1253 | ,-> li({ node, children, ...props }) {
1254 | | const listItemStart = node?.position?.start.offset;
1255 | | const markerOffset =
1256 | | typeof listItemStart === "number" ? findTaskListMarkerOffset(text, listItemStart) : null;
1257 | | return (
1258 | | <li {...props} data-task-marker-offset={markerOffset ?? undefined}>
1259 | | {renderSkillInlineMarkdownChildren(children, skills)}
1260 | |
1261 | | );
1262 | -> }, 1263 | input({ node: _node, type, checked, disabled: _disabled, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1263:12]
1262 | },
1263 | ,-> input({ node: _node, type, checked, disabled: _disabled, ...props }) {
1264 | | if (type !== "checkbox" || !onTaskListChange) {
1265 | | return (
1266 | | <input
1267 | | {...props}
1268 | | type={type}
1269 | | checked={checked}
1270 | | disabled={_disabled}
1271 | | readOnly={type === "checkbox"}
1272 | | />
1273 | | );
1274 | | }
1275 | | return (
1276 | | <input
1277 | | {...props}
1278 | | type="checkbox"
1279 | | name="markdown-task"
1280 | | aria-label="Toggle task"
1281 | | checked={checked}
1282 | | onChange={(event) => {
1283 | | const markerOffset = Number(
1284 | | event.currentTarget.closest("li")?.dataset.taskMarkerOffset,
1285 | | );
1286 | | if (!Number.isSafeInteger(markerOffset)) return;
1287 | | onTaskListChange({ markerOffset, checked: event.currentTarget.checked });
1288 | | }}
1289 | | />
1290 | | );
1291 | -> }, 1292 | a({ node, href, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1292:8]
1291 | },
1292 | ,-> a({ node, href, children, ...props }) {
1293 | | const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : "";
1294 | | const fileLinkMeta = normalizedHref ? markdownFileLinkMetaByHref.get(normalizedHref) : null;
1295 | | if (!fileLinkMeta) {
1296 | | const faviconHost = resolveExternalLinkHost(href);
1297 | | const isSameDocumentLink = href?.startsWith("#") ?? false;
1298 | | const onClick = props.onClick;
1299 | | const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime();
1300 | | const link = (
1301 | | <a
1302 | | {...props}
1303 | | href={href}
1304 | | target={isSameDocumentLink ? undefined : "_blank"}
1305 | | rel={isSameDocumentLink ? undefined : "noopener noreferrer"}
1306 | | onClick={(event) => {
1307 | | onClick?.(event);
1308 | | if (isSameDocumentLink && href) {
1309 | | handleMarkdownFragmentClick(event, href);
1310 | | }
1311 | | }}
1312 | | onContextMenu={(event) => {
1313 | | if (!canOpenInPreview || !href) return;
1314 | | event.preventDefault();
1315 | | event.stopPropagation();
1316 | | const api = readLocalApi();
1317 | | if (!api) return;
1318 | | void api.contextMenu
1319 | | .show(
1320 | | [
1321 | | { id: "open-in-browser", label: "Open in integrated browser" },
1322 | | { id: "open-external", label: "Open in system browser" },
1323 | | ] as const,
1324 | | { x: event.clientX, y: event.clientY },
1325 | | )
1326 | | .then((clicked) => {
1327 | | if (clicked === "open-in-browser") {
1328 | | void openExternalLinkInPreview(href);
1329 | | return;
1330 | | }
1331 | | if (clicked === "open-external") return api.shell.openExternal(href);
1332 | | })
1333 | | .catch(() => undefined);
1334 | | }}
1335 | | >
1336 | | {faviconHost ? (
1337 | |
1338 | | {children}
1339 | |
1340 | | ) : (
1341 | | children
1342 | | )}
1343 | |
1344 | | );
1345 | | if (!faviconHost || !href) {
1346 | | return link;
1347 | | }
1348 | | return (
1349 | |
1350 | |
1351 | | <TooltipPopup
1352 | | side="top"
1353 | | className="max-w-[min(36rem,calc(100vw-2rem))] whitespace-normal leading-tight wrap-anywhere"
1354 | | >
1355 | | {href}
1356 | |
1357 | |
1358 | | );
1359 | | }
1360 | |
1361 | | const parentSuffix = fileLinkParentSuffixByPath.get(fileLinkMeta.filePath);
1362 | | const labelParts = [fileLinkMeta.basename];
1363 | | if (typeof parentSuffix === "string" && parentSuffix.length > 0) {
1364 | | labelParts.push(parentSuffix);
1365 | | }
1366 | | if (fileLinkMeta.line) {
1367 | | labelParts.push(
1368 | | L${fileLinkMeta.line}${fileLinkMeta.column ? :C${fileLinkMeta.column} : ""},
1369 | | );
1370 | | }
1371 | |
1372 | | return (
1373 | | <MarkdownFileLink
1374 | | href={fileLinkMeta.targetPath}
1375 | | targetPath={fileLinkMeta.targetPath}
1376 | | iconPath={fileLinkMeta.filePath}
1377 | | displayPath={fileLinkMeta.displayPath}
1378 | | workspaceRelativePath={fileLinkMeta.workspaceRelativePath}
1379 | | line={fileLinkMeta.line}
1380 | | label={labelParts.join(" · ")}
1381 | | copyMarkdown={[${fileLinkMeta.basename}](${normalizedHref})}
1382 | | theme={resolvedTheme}
1383 | | threadRef={threadRef}
1384 | | onOpen={openInPreferredEditor}
1385 | | onOpenInBrowser={
1386 | | threadRef &&
1387 | | isPreviewSupportedInRuntime() &&
1388 | | isBrowserPreviewFile(fileLinkMeta.filePath)
1389 | | ? () => openMarkdownFileInPreview(fileLinkMeta.filePath)
1390 | | : undefined
1391 | | }
1392 | | className={props.className}
1393 | | />
1394 | | );
1395 | -> }, 1396 | table({ node: _node, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1396:12]
1395 | },
1396 | ,-> table({ node: _node, ...props }) {
1397 | | return <MarkdownTable {...props} />;
1398 | -> }, 1399 | details({ node: _node, children, open: detailsOpen }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1399:14]
1398 | },
1399 | ,-> details({ node: _node, children, open: detailsOpen }) {
1400 | | return {children};
1401 | -> }, 1402 | pre({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1402:10]
1401 | },
1402 | ,-> pre({ node, children, ...props }) {
1403 | | const codeBlock = extractCodeBlock(children);
1404 | | if (!codeBlock) {
1405 | | return <pre {...props}>{children};
1406 | | }
1407 | |
1408 | | const language = extractFenceLanguage(codeBlock.className);
1409 | | const fenceTitle = extractFenceTitle(extractPreCodeMeta(node));
1410 | | return (
1411 | | <MarkdownCodeBlock
1412 | | code={codeBlock.code}
1413 | | language={language}
1414 | | fenceTitle={fenceTitle}
1415 | | theme={resolvedTheme}
1416 | | >
1417 | | <CodeHighlightErrorBoundary fallback={<pre {...props}>{children}}>
1418 | | <Suspense fallback={<pre {...props}>{children}}>
1419 | | <SuspenseShikiCodeBlock
1420 | | className={codeBlock.className}
1421 | | code={codeBlock.code}
1422 | | themeName={diffThemeName}
1423 | | isStreaming={isStreaming}
1424 | | />
1425 | |
1426 | |
1427 | |
1428 | | );
1429 | -> }, 1430 | }), ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/review/ReviewSheet.tsx:249:24]
248 | },
249 | ,-> headerTitle: () => (
250 | | <View style={{ alignItems: "center" }}>
251 | | <NativeText
252 | | numberOfLines={1}
253 | | style={{
254 | | fontFamily: "DMSans_700Bold",
255 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
256 | | fontWeight: "900",
257 | | color: headerForeground,
258 | | letterSpacing: -0.4,
259 | | }}
260 | | >
261 | | Files Changed
262 | |
263 | | <View
264 | | style={{
265 | | flexDirection: "row",
266 | | alignItems: "center",
267 | | justifyContent: "center",
268 | | gap: 6,
269 | | flexWrap: "wrap",
270 | | }}
271 | | >
272 | | {headerDiffSummary.additions && headerDiffSummary.deletions ? (
273 | | <>
274 | | <NativeText
275 | | style={{
276 | | fontFamily: "DMSans_700Bold",
277 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
278 | | fontWeight: "700",
279 | | color: "#16a34a",
280 | | }}
281 | | >
282 | | {headerDiffSummary.additions}
283 | |
284 | | <NativeText
285 | | style={{
286 | | fontFamily: "DMSans_700Bold",
287 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
288 | | fontWeight: "700",
289 | | color: "#e11d48",
290 | | }}
291 | | >
292 | | {headerDiffSummary.deletions}
293 | |
294 | | {pendingReviewCommentCount > 0 ? (
295 | | <NativeText
296 | | style={{
297 | | fontFamily: "DMSans_700Bold",
298 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
299 | | fontWeight: "700",
300 | | color: "#b45309",
301 | | }}
302 | | >
303 | | {pendingReviewCommentCount} pending
304 | |
305 | | ) : null}
306 | | </>
307 | | ) : (
308 | | <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
309 | | <NativeText
310 | | numberOfLines={1}
311 | | style={{
312 | | fontFamily: "DMSans_700Bold",
313 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
314 | | fontWeight: "700",
315 | | color: headerMuted,
316 | | }}
317 | | >
318 | | {selectedSection?.title ?? "Review changes"}
319 | |
320 | | {pendingReviewCommentCount > 0 ? (
321 | | <NativeText
322 | | style={{
323 | | fontFamily: "DMSans_700Bold",
324 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
325 | | fontWeight: "700",
326 | | color: "#b45309",
327 | | }}
328 | | >
329 | | {pendingReviewCommentCount} pending
330 | |
331 | | ) : null}
332 | |
333 | | )}
334 | |
335 | |
336 | -> ), 337 | }} ----
help: Move this component definition out of the parent component ReviewSheet and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/threads/ThreadRouteScreen.tsx:383:24]
382 | headerBackTitle: "",
383 | ,-> headerTitle: () => (
384 | | <Pressable
385 | | style={{ alignItems: "center", maxWidth: 200 }}
386 | | onLongPress={() => {
387 | | // TODO: trigger rename modal
388 | | }}
389 | | >
390 | | <RNText
391 | | numberOfLines={1}
392 | | style={{
393 | | fontFamily: "DMSans_700Bold",
394 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
395 | | fontWeight: "900",
396 | | color: foregroundColor,
397 | | letterSpacing: -0.4,
398 | | }}
399 | | >
400 | | {selectedThread.title}
401 | |
402 | | <RNText
403 | | numberOfLines={1}
404 | | style={{
405 | | fontFamily: "DMSans_700Bold",
406 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
407 | | fontWeight: "700",
408 | | color: secondaryFg,
409 | | letterSpacing: 0.3,
410 | | }}
411 | | >
412 | | {headerSubtitle}
413 | |
414 | |
415 | -> ), 416 | }} ----
help: Move this component definition out of the parent component ThreadRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:612:15]
611 | valuePrefix: "project",
612 | ,-> icon: (project) => (
613 | | <ProjectFavicon
614 | | environmentId={project.environmentId}
615 | | cwd={project.workspaceRoot}
616 | | className={ITEM_ICON_CLASS}
617 | | />
618 | -> ), 619 | runProject: openProjectFromSearch, ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:630:15]
629 | shortcutCommand: "chat.new",
630 | ,-> icon: (project) => (
631 | | <ProjectFavicon
632 | | environmentId={project.environmentId}
633 | | cwd={project.workspaceRoot}
634 | | className={ITEM_ICON_CLASS}
635 | | />
636 | -> ), 637 | runProject: async (project) => { ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx:911:24]
910 | title: "",
911 | ,-> headerTitle: () => (
912 | | <View
913 | | style={{
914 | | alignItems: "center",
915 | | gap: 1,
916 | | maxWidth: 240,
917 | | }}
918 | | >
919 | | <RNText
920 | | numberOfLines={1}
921 | | style={{
922 | | color: terminalTheme.foreground,
923 | | fontFamily: "DMSans_700Bold",
924 | | fontSize: MOBILE_TYPOGRAPHY.footnote.fontSize,
925 | | lineHeight: 16,
926 | | }}
927 | | >
928 | | {headerTitle.topLine}
929 | |
930 | | <RNText
931 | | ellipsizeMode="middle"
932 | | numberOfLines={1}
933 | | style={{
934 | | color: terminalTheme.mutedForeground,
935 | | fontFamily: "Menlo",
936 | | fontSize: MOBILE_TYPOGRAPHY.caption.fontSize,
937 | | lineHeight: 14,
938 | | }}
939 | | >
940 | | {headerTitle.bottomLine}
941 | |
942 | |
943 | -> ), 944 | }} ----
help: Move this component definition out of the parent component ThreadTerminalRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx:492:24]
491 | headerShadowVisible: false,
492 | headerTitle: () => ,
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
493 | headerSearchBarOptions: {
---- help: Move this component definition out of the parent component ThreadFilesTreeScreenand pass data as props. If you want to allow component creation in props, setallowAsProps` option to true.

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:293:15]
292 | const stored = yield* Effect.fromResult(
293 | Schema.decodeUnknownResult(StoredShellSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
294 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:313:13]
312 | const legacyStored = yield* Effect.fromResult(
313 | Schema.decodeUnknownResult(LegacyStoredShellSnapshot)(legacyParsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
314 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:328:13]
327 | const encoded = yield* Effect.fromResult(
328 | Schema.encodeUnknownResult(StoredShellSnapshot)(stored),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
329 | ).pipe(Effect.mapError((cause) => shellPersistenceError("save-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:355:13]
354 | const stored = yield* Effect.fromResult(
355 | Schema.decodeUnknownResult(StoredThreadSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
356 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-thread", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:365:13]
364 | const encoded = yield* Effect.fromResult(
365 | Schema.encodeUnknownResult(StoredThreadSnapshot)({
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
366 | schemaVersion: THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION,
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:31:5]
30 | return yield* Effect.fromResult(
31 | Schema.decodeUnknownResult(ConnectionCatalogDocument)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
32 | ).pipe(Effect.mapError((cause) => catalogError("decode", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:39:5]
38 | const encoded = yield* Effect.fromResult(
39 | Schema.encodeUnknownResult(ConnectionCatalogDocument)(catalog),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
40 | ).pipe(Effect.mapError((cause) => catalogError("encode", cause)));
`----

Found 0 errors and 20 warnings in 1748 files (978ms, 16 threads)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
~/packages/contracts$ tsgo --noEmit ⊘ cache disabled
~/apps/marketing$ astro check ⊘ cache disabled

~/oxlint-plugin-t3code$ tsgo --noEmit ⊘ cache disabled

~/packages/effect-codex-app-server$ tsgo --noEmit ⊘ cache disabled
08:12:00 [WARN] [vite] warning: optimizeDeps.esbuildOptions option was specified by "astro:dev-toolbar" plugin. This option is deprecated, please use optimizeDeps.rolldownOptions instead.
08:12:00 [WARN] [vite] You or a plugin you are using have set optimizeDeps.esbuildOptions but this option is now deprecated. Vite now uses Rolldown to optimize the dependencies. Please use optimizeDeps.rolldownOptions instead.
08:12:00 [types] Generated 39ms
08:12:00 [check] Getting diagnostics for Astro files in /Users/julius/.codex/worktrees/client-cloud-crypto-errors/apps/marketing...
Result (10 files):

  • 0 errors
  • 0 warnings
  • 0 hints

~/packages/effect-acp$ tsgo --noEmit ⊘ cache disabled

~/packages/shared$ tsgo --noEmit ⊘ cache disabled

~/packages/client-runtime$ tsgo --noEmit ⊘ cache disabled

~/scripts$ tsgo --noEmit ⊘ cache disabled

~/packages/ssh$ tsgo --noEmit ⊘ cache disabled

~/packages/tailscale$ tsgo --noEmit ⊘ cache disabled

~/apps/web$ tsgo --noEmit ⊘ cache disabled

~/apps/mobile$ tsc --noEmit ⊘ cache disabled

~/infra/relay$ tsgo --noEmit ⊘ cache disabled

~/apps/desktop$ tsgo --noEmit ⊘ cache disabled

~/apps/server$ tsgo --noEmit ⊘ cache disabled


vp run: 0/15 cache hit (0%). (Run vp run --last-details for full details)\n- WARN Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
$ node scripts/mobile-native-static-check.ts ⊘ cache disabled
Found 6 Swift and 2 Kotlin native source files.
$ swiftlint lint --config .swiftlint.yml --strict
$ ktlint modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt
$ detekt --config detekt.yml --input modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt,modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt --build-upon-default-config
Skipping generated native project folders: android/, ios/.


Note

Medium Risk
Touches auth-related DPoP and secure token cache paths; error shape and message text change for any caller matching on strings, though signing/storage behavior is unchanged.

Overview
Replaces free-form message strings on mobile/web DPoP and mobile managed relay token failures with Effect Schema tagged errors that carry a discriminated operation literal and an optional cause (Defect), plus derived message getters.

On mobile DPoP, validation failures now use dedicated types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) wrapped as validate-key / derive-public-key where appropriate; invalid proof URLs fail as normalize-proof-urlwithout a cause. Web DPoP follows the same pattern (including isBrowserDpopError for decode chaining).

Managed relay token store errors are structured the same way; load/save/clear logging annotates the full error object and operation instead of a generic operation string. Tests assert operation (and causes where relevant) rather than fixed message text.

Reviewed by Cursor Bugbot for commit df4f4d7. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Structure cloud cryptography errors with operation-tagged Schema.TaggedErrorClass types

  • Replaces Data.TaggedError with Schema.TaggedErrorClass for CloudDpopError, BrowserDpopError, and ManagedRelayTokenStoreError, adding an operation literal discriminant and optional typed cause to each.
  • Each error class now exposes a message getter that formats the operation name, and new specific error types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) are introduced for granular failure cases.
  • Exports isBrowserDpopError and isManagedRelayTokenStoreError type guards via Schema.is.
  • Log annotations in managedRelayTokenStore now include the structured error object and its operation field instead of a freeform string.
  • Behavioral Change: callers that previously matched on error message strings must now inspect the structured operation field.

Macroscope summarized df4f4d7.

Co-authored-by: codex <codex@users.noreply.github.com>
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved

This PR refactors error classes to use structured typed operations instead of free-form string messages. The changes are purely structural with no runtime behavior modifications - errors are thrown at the same points with equivalent semantics.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
MemberAuthor

Superseded by the smaller, more structural #3277, #3307, and #3309. Those PRs retain storage/request identifiers and distinct DPoP failure types instead of combining all client cryptography changes into one branch.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge