[codex] Migrate desktop shell and SSH Effect services - #3194
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
🚀 Expo continuous deployment is ready!
|
ApprovabilityVerdict: Approved Mechanical migration of Effect services to newer patterns - updates import styles, converts error classes to Schema.TaggedErrorClass, and moves interface definitions inline. No significant runtime behavior changes; error messages remain functionally equivalent. No code changes detected at You can customize Macroscope's approvability policy. Learn more. |
17505cc to
36276e8
Compare
Dismissing prior approval to re-evaluate 36276e8
36276e8 to
323bc31
Compare
Dismissing prior approval to re-evaluate 323bc31
323bc31 to
5291b43
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Send error hides window loss
- The catch handler in Effect.try now detects window-unavailable errors and returns DesktopSshPromptWindowUnavailableError instead of wrapping them in DesktopSshPromptSendError, so users see the correct window-unavailable message.
Or push these changes by commenting:
@cursor push f5a583965d
Preview (f5a583965d)
diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.ts b/apps/desktop/src/shell/DesktopShellEnvironment.ts
--- a/apps/desktop/src/shell/DesktopShellEnvironment.ts
+++ b/apps/desktop/src/shell/DesktopShellEnvironment.ts
@@ -3,7 +3,8 @@
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
-import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
+import * as ChildProcess from "effect/unstable/process/ChildProcess";
+import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
@@ -19,13 +20,11 @@
readonly loadProfile: boolean;
}
-export interface DesktopShellEnvironmentShape {
- readonly installIntoProcess: Effect.Effect<void, never>;
-}
-
export class DesktopShellEnvironment extends Context.Service<
DesktopShellEnvironment,
- DesktopShellEnvironmentShape
+ {
+ readonly installIntoProcess: Effect.Effect<void>;
+ }
>()("@t3tools/desktop/shell/DesktopShellEnvironment") {}
const LOGIN_SHELL_ENV_NAMES = [
@@ -336,20 +335,20 @@
return Effect.void;
};
-export const layer = Layer.effect(
- DesktopShellEnvironment,
- Effect.gen(function* () {
- const environment = yield* DesktopEnvironment.DesktopEnvironment;
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
- return DesktopShellEnvironment.of({
- installIntoProcess: installShellEnvironment({
- env: process.env,
- platform: environment.platform,
- userShell: Option.none(),
- }).pipe(
- Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
- Effect.withSpan("desktop.shellEnvironment.installIntoProcess"),
- ),
- });
- }),
-);
+export const make = Effect.gen(function* () {
+ const environment = yield* DesktopEnvironment.DesktopEnvironment;
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
+ const installIntoProcess: DesktopShellEnvironment["Service"]["installIntoProcess"] =
+ installShellEnvironment({
+ env: process.env,
+ platform: environment.platform,
+ userShell: Option.none(),
+ }).pipe(
+ Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
+ Effect.withSpan("desktop.shellEnvironment.installIntoProcess"),
+ );
+
+ return DesktopShellEnvironment.of({ installIntoProcess });
+});
+
+export const layer = Layer.effect(DesktopShellEnvironment, make);
diff --git a/apps/desktop/src/ssh/DesktopSshEnvironment.ts b/apps/desktop/src/ssh/DesktopSshEnvironment.ts
--- a/apps/desktop/src/ssh/DesktopSshEnvironment.ts
+++ b/apps/desktop/src/ssh/DesktopSshEnvironment.ts
@@ -4,11 +4,7 @@
DesktopSshEnvironmentTarget,
} from "@t3tools/contracts";
import * as NetService from "@t3tools/shared/Net";
-import {
- SshPasswordPrompt,
- type SshPasswordPromptShape,
- type SshPasswordRequest,
-} from "@t3tools/ssh/auth";
+import { SshPasswordPrompt, type SshPasswordRequest } from "@t3tools/ssh/auth";
import { discoverSshHosts } from "@t3tools/ssh/config";
import {
SshCommandError,
@@ -25,8 +21,8 @@
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
-import { HttpClient } from "effect/unstable/http";
-import { ChildProcessSpawner } from "effect/unstable/process";
+import * as HttpClient from "effect/unstable/http/HttpClient";
+import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
import * as DesktopSshPasswordPrompts from "./DesktopSshPasswordPrompts.ts";
@@ -52,22 +48,20 @@
| DesktopSshEnvironmentDiscoverError
| DesktopSshEnvironmentOperationError;
-export interface DesktopSshEnvironmentShape {
- readonly discoverHosts: (input?: {
- readonly homeDir?: string;
- }) => Effect.Effect<readonly DesktopDiscoveredSshHost[], DesktopSshEnvironmentDiscoverError>;
- readonly ensureEnvironment: (
- target: DesktopSshEnvironmentTarget,
- options?: { readonly issuePairingToken?: boolean },
- ) => Effect.Effect<DesktopSshEnvironmentBootstrap, DesktopSshEnvironmentOperationError>;
- readonly disconnectEnvironment: (
- target: DesktopSshEnvironmentTarget,
- ) => Effect.Effect<void, DesktopSshEnvironmentOperationError>;
-}
-
export class DesktopSshEnvironment extends Context.Service<
DesktopSshEnvironment,
- DesktopSshEnvironmentShape
+ {
+ readonly discoverHosts: (input?: {
+ readonly homeDir?: string;
+ }) => Effect.Effect<readonly DesktopDiscoveredSshHost[], DesktopSshEnvironmentDiscoverError>;
+ readonly ensureEnvironment: (
+ target: DesktopSshEnvironmentTarget,
+ options?: { readonly issuePairingToken?: boolean },
+ ) => Effect.Effect<DesktopSshEnvironmentBootstrap, DesktopSshEnvironmentOperationError>;
+ readonly disconnectEnvironment: (
+ target: DesktopSshEnvironmentTarget,
+ ) => Effect.Effect<void, DesktopSshEnvironmentOperationError>;
+ }
>()("@t3tools/desktop/ssh/DesktopSshEnvironment") {}
export interface DesktopSshEnvironmentLayerOptions {
@@ -89,8 +83,8 @@
}
const makePasswordPrompt = (
- prompts: DesktopSshPasswordPrompts.DesktopSshPasswordPromptsShape,
-): SshPasswordPromptShape => ({
+ prompts: DesktopSshPasswordPrompts.DesktopSshPasswordPrompts["Service"],
+): SshPasswordPrompt["Service"] => ({
isAvailable: true,
request: (request: SshPasswordRequest) =>
prompts.request(request).pipe(
@@ -104,7 +98,7 @@
),
});
-const make = Effect.gen(function* () {
+export const make = Effect.gen(function* () {
const manager = yield* SshEnvironmentManager;
const prompts = yield* DesktopSshPasswordPrompts.DesktopSshPasswordPrompts;
const runtimeContext = yield* Effect.context<DesktopSshEnvironmentRuntimeServices>();
diff --git a/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts b/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts
--- a/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts
+++ b/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts
@@ -3,7 +3,6 @@
import type { SshPasswordRequest } from "@t3tools/ssh/auth";
import * as Context from "effect/Context";
import * as Crypto from "effect/Crypto";
-import * as Data from "effect/Data";
import * as DateTime from "effect/DateTime";
import * as Deferred from "effect/Deferred";
import * as Duration from "effect/Duration";
@@ -11,84 +10,99 @@
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
+import * as Schema from "effect/Schema";
import * as IpcChannels from "../ipc/channels.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
const DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS = 3 * 60 * 1000;
-const WINDOW_UNAVAILABLE_MESSAGE = "T3 Code window is not available for SSH authentication.";
type DesktopSshPasswordPromptResolutionInput =
typeof DesktopSshPasswordPromptResolutionInputSchema.Type;
-export class DesktopSshPromptUnavailableError extends Data.TaggedError(
+const windowUnavailableMessage = (destination: string): string =>
+ `T3 Code window is not available for SSH authentication to ${destination}.`;
+
+export class DesktopSshPromptUnavailableError extends Schema.TaggedErrorClass<DesktopSshPromptUnavailableError>()(
"DesktopSshPromptUnavailableError",
-)<{
- readonly reason: string;
-}> {
- override get message() {
- return this.reason;
+ {
+ destination: Schema.String,
+ cause: Schema.Defect(),
+ },
+) {
+ override get message(): string {
+ return `Secure randomness is unavailable for SSH authentication to ${this.destination}.`;
}
}
-export class DesktopSshPromptWindowUnavailableError extends Data.TaggedError(
+export class DesktopSshPromptWindowUnavailableError extends Schema.TaggedErrorClass<DesktopSshPromptWindowUnavailableError>()(
"DesktopSshPromptWindowUnavailableError",
-)<{
- readonly destination: string;
-}> {
- override get message() {
- return WINDOW_UNAVAILABLE_MESSAGE;
+ {
+ destination: Schema.String,
+ },
+) {
+ override get message(): string {
+ return windowUnavailableMessage(this.destination);
}
}
-export class DesktopSshPromptSendError extends Data.TaggedError("DesktopSshPromptSendError")<{
- readonly requestId: string;
- readonly destination: string;
- readonly cause: unknown;
-}> {
- override get message() {
- return WINDOW_UNAVAILABLE_MESSAGE;
+export class DesktopSshPromptSendError extends Schema.TaggedErrorClass<DesktopSshPromptSendError>()(
+ "DesktopSshPromptSendError",
+ {
+ requestId: Schema.String,
+ destination: Schema.String,
+ cause: Schema.Defect(),
+ },
+) {
+ override get message(): string {
+ return `Failed to send SSH password prompt ${this.requestId} for ${this.destination}.`;
}
}
-export class DesktopSshPromptTimedOutError extends Data.TaggedError(
+export class DesktopSshPromptTimedOutError extends Schema.TaggedErrorClass<DesktopSshPromptTimedOutError>()(
"DesktopSshPromptTimedOutError",
-)<{
- readonly requestId: string;
- readonly destination: string;
-}> {
- override get message() {
+ {
+ requestId: Schema.String,
+ destination: Schema.String,
+ },
+) {
+ override get message(): string {
return `SSH authentication timed out for ${this.destination}.`;
}
}
-export class DesktopSshPromptCancelledError extends Data.TaggedError(
+export class DesktopSshPromptCancelledError extends Schema.TaggedErrorClass<DesktopSshPromptCancelledError>()(
"DesktopSshPromptCancelledError",
-)<{
- readonly requestId: string;
- readonly destination: string;
- readonly reason: string;
-}> {
- override get message() {
+ {
+ requestId: Schema.String,
+ destination: Schema.String,
+ reason: Schema.String,
+ },
+) {
+ override get message(): string {
return this.reason;
}
}
-export class DesktopSshPromptInvalidRequestIdError extends Data.TaggedError(
+export class DesktopSshPromptInvalidRequestIdError extends Schema.TaggedErrorClass<DesktopSshPromptInvalidRequestIdError>()(
"DesktopSshPromptInvalidRequestIdError",
-)<{
- readonly requestId: string;
-}> {
- override get message() {
- return "Invalid SSH password prompt id.";
+ {
+ requestId: Schema.String,
+ },
+) {
+ override get message(): string {
+ return `Invalid SSH password prompt id: ${this.requestId}.`;
}
}
-export class DesktopSshPromptExpiredError extends Data.TaggedError("DesktopSshPromptExpiredError")<{
- readonly requestId: string;
-}> {
- override get message() {
- return "SSH password prompt expired. Try connecting again.";
+export class DesktopSshPromptExpiredError extends Schema.TaggedErrorClass<DesktopSshPromptExpiredError>()(
+ "DesktopSshPromptExpiredError",
+ {
+ requestId: Schema.String,
+ },
+) {
+ override get message(): string {
+ return `SSH password prompt ${this.requestId} expired. Try connecting again.`;
}
}
@@ -107,28 +121,31 @@
| DesktopSshPasswordPromptRequestError
| DesktopSshPasswordPromptResolveError;
+export const DesktopSshPasswordPromptCancellation = Schema.Union([
+ DesktopSshPromptCancelledError,
+ DesktopSshPromptTimedOutError,
+]);
+export type DesktopSshPasswordPromptCancellation = typeof DesktopSshPasswordPromptCancellation.Type;
+
+const isDesktopSshPasswordPromptCancellationValue = Schema.is(DesktopSshPasswordPromptCancellation);
+
export function isDesktopSshPasswordPromptCancellation(
error: unknown,
-): error is DesktopSshPromptCancelledError | DesktopSshPromptTimedOutError {
- return (
- error instanceof DesktopSshPromptCancelledError ||
- error instanceof DesktopSshPromptTimedOutError
- );
+): error is DesktopSshPasswordPromptCancellation {
+ return isDesktopSshPasswordPromptCancellationValue(error);
}
-export interface DesktopSshPasswordPromptsShape {
- readonly request: (
- request: SshPasswordRequest,
- ) => Effect.Effect<string, DesktopSshPasswordPromptRequestError>;
- readonly resolve: (
- input: DesktopSshPasswordPromptResolutionInput,
- ) => Effect.Effect<void, DesktopSshPasswordPromptResolveError>;
- readonly cancelPending: (reason: string) => Effect.Effect<void>;
-}
-
export class DesktopSshPasswordPrompts extends Context.Service<
DesktopSshPasswordPrompts,
- DesktopSshPasswordPromptsShape
+ {
+ readonly request: (
+ request: SshPasswordRequest,
+ ) => Effect.Effect<string, DesktopSshPasswordPromptRequestError>;
+ readonly resolve: (
+ input: DesktopSshPasswordPromptResolutionInput,
+ ) => Effect.Effect<void, DesktopSshPasswordPromptResolveError>;
+ readonly cancelPending: (reason: string) => Effect.Effect<void>;
+ }
>()("@t3tools/desktop/ssh/DesktopSshPasswordPrompts") {}
interface PendingSshPasswordPrompt {
@@ -137,7 +154,7 @@
readonly deferred: Deferred.Deferred<string, DesktopSshPasswordPromptRequestError>;
}
-interface LayerOptions {
+export interface DesktopSshPasswordPromptsOptions {
readonly passwordPromptTimeoutMs?: number;
}
@@ -161,14 +178,16 @@
error: DesktopSshPasswordPromptRequestError,
) => Deferred.fail(pending.deferred, error).pipe(Effect.asVoid);
-const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: LayerOptions = {}) {
+export const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (
+ options: DesktopSshPasswordPromptsOptions = {},
+) {
const electronWindow = yield* ElectronWindow.ElectronWindow;
const crypto = yield* Crypto.Crypto;
const pendingRef = yield* Ref.make(new Map<string, PendingSshPasswordPrompt>());
const passwordPromptTimeoutMs =
options.passwordPromptTimeoutMs ?? DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS;
- const cancelPending = (reason: string): Effect.Effect<void> =>
+ const cancelPending: DesktopSshPasswordPrompts["Service"]["cancelPending"] = (reason) =>
Ref.getAndSet(pendingRef, new Map()).pipe(
Effect.flatMap((pending) =>
Effect.forEach(
@@ -192,9 +211,9 @@
cancelPending("SSH password prompt service stopped.").pipe(Effect.ignore),
);
- const resolve = Effect.fn("desktop.sshPasswordPrompts.resolve")(function* (
- input: DesktopSshPasswordPromptResolutionInput,
- ): Effect.fn.Return<void, DesktopSshPasswordPromptResolveError> {
+ const resolve: DesktopSshPasswordPrompts["Service"]["resolve"] = Effect.fn(
+ "desktop.sshPasswordPrompts.resolve",
+ )(function* (input) {
const requestId = input.requestId.trim();
if (requestId.length === 0) {
return yield* new DesktopSshPromptInvalidRequestIdError({ requestId: input.requestId });
@@ -221,9 +240,9 @@
yield* Deferred.succeed(entry.deferred, input.password).pipe(Effect.asVoid);
});
- const request = Effect.fn("desktop.sshPasswordPrompts.request")(function* (
- input: SshPasswordRequest,
- ): Effect.fn.Return<string, DesktopSshPasswordPromptRequestError> {
+ const request: DesktopSshPasswordPrompts["Service"]["request"] = Effect.fn(
+ "desktop.sshPasswordPrompts.request",
+ )(function* (input) {
const window = yield* electronWindow.main;
if (Option.isNone(window) || window.value.isDestroyed()) {
return yield* new DesktopSshPromptWindowUnavailableError({
@@ -233,7 +252,11 @@
const requestId = yield* crypto.randomUUIDv4.pipe(
Effect.mapError(
- () => new DesktopSshPromptUnavailableError({ reason: "Secure randomness is unavailable." }),
+ (cause) =>
+ new DesktopSshPromptUnavailableError({
+ destination: input.destination,
+ cause,
+ }),
),
);
const now = yield* DateTime.now;
@@ -302,27 +325,36 @@
return yield* Effect.try({
try: () => {
if (window.value.isDestroyed()) {
- throw new Error(WINDOW_UNAVAILABLE_MESSAGE);
+ throw new Error(windowUnavailableMessage(input.destination));
}
window.value.once("closed", cancelOnWindowClosed);
window.value.webContents.send(IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL, promptRequest);
if (window.value.isDestroyed()) {
- throw new Error(WINDOW_UNAVAILABLE_MESSAGE);
+ throw new Error(windowUnavailableMessage(input.destination));
}
if (window.value.isMinimized()) {
window.value.restore();
}
if (window.value.isDestroyed()) {
- throw new Error(WINDOW_UNAVAILABLE_MESSAGE);
+ throw new Error(windowUnavailableMessage(input.destination));
}
window.value.focus();
},
- catch: (cause) =>
- new DesktopSshPromptSendError({
+ catch: (cause) => {
+ if (
+ cause instanceof Error &&
+ cause.message === windowUnavailableMessage(input.destination)
+ ) {
+ return new DesktopSshPromptWindowUnavailableError({
+ destination: input.destination,
+ });
+ }
+ return new DesktopSshPromptSendError({
requestId,
destination: input.destination,
cause,
- }),
+ });
+ },
}).pipe(Effect.andThen(waitForPassword), Effect.ensuring(cleanup));
});
@@ -333,5 +365,5 @@
});
});
-export const layer = (options: LayerOptions = {}) =>
+export const layer = (options: DesktopSshPasswordPromptsOptions = {}) =>
Layer.effect(DesktopSshPasswordPrompts, make(options));You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 5291b43. Configure here.
5291b43 to
ff9ec3c
Compare
Dismissing prior approval to re-evaluate ff9ec3c
ff9ec3c to
552074d
Compare
Dismissing prior approval to re-evaluate 552074d
552074d to
16db048
Compare
Dismissing prior approval to re-evaluate 16db048
16db048 to
5e42b39
Compare
d8ddfb2 to
b0c4047
Compare
c3c28f8 to
83f8f84
Compare
Dismissing prior approval to re-evaluate 83f8f84
fbcbcb0 to
9911fa8
Compare
Dismissing prior approval to re-evaluate 9911fa8
9911fa8 to
9697d14
Compare
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
9697d14 to
a6dec0e
Compare
Co-authored-by: codex <codex@users.noreply.github.com>
24a3874 to
1a291d8
Compare


Summary
DesktopShellEnvironment,DesktopSshEnvironment, andDesktopSshPasswordPromptsservice modulesContext.Servicecontract and useService["Service"]for implementation and dependency shapesmakeand canonicallayer;Layer.effectremains appropriate here because the constructors have real dependenciesSchema.TaggedErrorClasswith contextual attributes and messages derived from those attributesSchema.Unionfor cancellation predicates while keeping Effect error channels as class unionsError audit
No error in this domain uses a
reason,kind, oroperationswitch to hide materially different failures, so no class split is required.DesktopSshPromptCancelledError.reasonremains internal diagnostic context for one cancellation failure mode. Secure-randomness failure now retains its destination and underlying cause.Scope and impact
The three service modules were already co-located at their domain roots, so no compatibility shims or path migrations are needed. Runtime service behavior and layer options remain intact, with one race correction: window loss during prompt dispatch remains a window-unavailable error instead of being reclassified as a generic send failure. SSH prompt errors now carry richer structured context and more specific messages.
Existing desktop shell/SSH suites cover these paths; no test files or refactor-only test declarations are added. No orchestration modules, MCP modules, or integration harnesses are changed.
Validation
vp check— passed with 0 errors; 20 pre-existing warnings remainvp run typecheck— all 15 workspace tasks passedgit diff --check origin/main— passedmakevalues, 3/3 exportedlayervalues, 0 standalone service shapes, namespace-qualified service modules, and named non-service package APIsNote
Medium Risk
Touches SSH authentication error mapping and cancellation semantics; user-visible messages are mostly preserved but internal classification and cancellation breadth changed.
Overview
Refactors desktop shell and SSH Effect services to match the co-located service pattern: contracts are inlined on
Context.Service, exportedmakefactories replace inlineLayer.effectbodies, and imports move to namespace modules (ChildProcessSpawner,SshAuth,SshTunnel, etc.).SSH password prompts are reworked around
Schema.TaggedErrorClasswith richer structured causes (request-id failure, presentation, window closed, service stopped).cancelPendingis removed from the public service API; shutdown now fails pending prompts via a finalizer withDesktopSshPromptServiceStoppedError.toSshPasswordPromptErrorcentralizes mapping toSshPasswordPromptError, including treating presentation failures like window unavailable for the user-facing message while keeping distinct diagnostic messages on the underlying errors. Cancellation detection broadens via aSchema.Union(cancelled, timed out, window closed, service stopped). Prompt dispatch reclassifies destroyed-window races as window-unavailable instead of generic send failures.Adds a test that presentation diagnostics stay distinct from the wrapped SSH auth message.
Reviewed by Cursor Bugbot for commit 1a291d8. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Migrate desktop shell and SSH Effect services to namespaced imports with structured error types
* as SshAuth,* as SshTunnel) and extractsmakefactories from inlineLayer.effectcalls.Schema.TaggedErrorClass, adding structured types for request ID failures, presentation errors, window-closed, and service-stopped conditions.cancelPendingfrom theDesktopSshPasswordPromptsservice shape; cancellation is now handled internally via a finalizer that emitsDesktopSshPromptServiceStoppedError.toSshPasswordPromptErrorinDesktopSshEnvironmentto map all prompt error variants toSshPasswordPromptErrorwith standardized messages; both window-unavailable and presentation errors map to the same user-facing message.DesktopSshPasswordPromptCancellationnow includes window-closed and service-stopped alongside cancelled and timed-out, broadening the cancellation predicate.Macroscope summarized 1a291d8.