Extract local IPC request handler seam

This commit is contained in:
OpenPets Dev 2026-06-19 21:32:28 +00:00
parent ef4d4bcd92
commit fa21d29fcd
6 changed files with 408 additions and 183 deletions

View file

@ -236,6 +236,7 @@ const loggerSource = readFileSync(join(appDir, "src", "logger.ts"), "utf8");
const mainSource = readFileSync(join(appDir, "src", "main.ts"), "utf8");
const localIpcSourceForLogging = readFileSync(join(appDir, "src", "local-ipc.ts"), "utf8");
const localIpcSocketSource = readFileSync(join(appDir, "src", "local-ipc-socket.ts"), "utf8");
const localIpcRequestHandlersSource = readFileSync(join(appDir, "src", "local-ipc-request-handlers.ts"), "utf8");
const localIpcPathsSource = readFileSync(join(appDir, "src", "local-ipc-paths.ts"), "utf8");
const leaseManagerSource = readFileSync(join(appDir, "src", "lease-manager.ts"), "utf8");
const defaultPetControllerSource = readFileSync(join(appDir, "src", "default-familiar-controller.ts"), "utf8");
@ -447,11 +448,15 @@ assert.match(agentPetControllerSource, /dismissedAgentPets\.has\(petId\)/, "dism
assert.match(agentPetControllerSource, /function clearAgentPetLeaseState/, "agent familiar lease cleanup must clear dismissal, timers, and hidden transient state.");
assert.match(localIpcSource, /handleLastExplicitLease/, "agent familiar dismissal must clear when the explicit lease group ends.");
assert.match(localIpcSource, /from "\.\/local-ipc-socket(?:\.js)?"/, "local-ipc must compose the extracted socket helper seam.");
assert.match(localIpcSource, /from "\.\/local-ipc-request-handlers(?:\.js)?"/, "local-ipc must compose the extracted request-handler seam.");
assert.match(localIpcSocketSource, /export function handleLocalIpcSocket/, "local-ipc socket seam must export the client socket handler.");
assert.match(localIpcSocketSource, /export function getLocalIpcListeningEndpoint/, "local-ipc socket seam must export advertised endpoint resolution.");
assert.match(localIpcSocketSource, /export function isAllowedRemoteAddress/, "local-ipc socket seam must export remote-address filtering.");
assert.match(localIpcRequestHandlersSource, /export async function handleLocalIpcRequest/, "local-ipc request-handler seam must export the async dispatcher.");
assert.match(localIpcRequestHandlersSource, /export function buildHelloResponse/, "local-ipc request-handler seam must export the hello response builder.");
assert.match(localIpcRequestHandlersSource, /export function buildFamiliarsListResponse/, "local-ipc request-handler seam must export familiar list response shaping.");
assert.match(localIpcSource, /clearAgentPetLeaseState\(petId\)/, "last explicit lease cleanup must reset dismissed agent familiar state.");
assert.match(localIpcSource, /reason: applied\.reason/, "IPC responses must report dismissed explicit familiar events as not shown.");
assert.match(localIpcRequestHandlersSource, /reason: applied\.reason/, "IPC responses must report dismissed explicit familiar events as not shown.");
assert.match(updateCheckerSource, /alvinunreal\/familiaros/, "GitHub release notice must check the public FamiliarOS repository.");
assert.match(updateCheckerSource, /api\.github\.com\/repos\/\$\{githubRepository\}\/releases\/latest/, "update checker must use GitHub latest release API.");
assert.match(updateCheckerSource, /shell\.openExternal\(url\)/, "update action must open the GitHub release page externally.");

View file

@ -27,13 +27,14 @@ main.ts
├── plugin-service.ts (plugin state/runtime init, JS host wiring)
├── tray.ts (tray creation)
├── local-ipc.ts (IPC server start)
│ ├── local-ipc-request-handlers.ts (protocol request dispatch, familiar actions, lease routes, and memory CRUD shaping)
│ └── local-ipc-socket.ts (socket accept loop, advertised endpoint resolution, and remote address filtering)
└── windows.ts (UI handlers)
```
**IPC Request Flow**:
```
local-ipc.ts → parseIpcRequest() → handleRequest()
local-ipc.ts → parseIpcRequest() → handleLocalIpcRequest()
├── hello/status/familiars.list/familiars.install
└── lease.acquire/heartbeat/release
└── lease-manager.ts
@ -280,7 +281,8 @@ plugin-service-local-support.ts → plugin-local-loader.ts validates selected fo
- `reaction-animation-mapping.ts`: Reaction-to-animation state mapping, user-configurable overrides, sprite state definitions
**IPC**:
- `local-ipc.ts`: net.Server lifecycle, request routing, discovery file management, and logging
- `local-ipc.ts`: net.Server lifecycle, discovery file management, lease wiring, and request-entry logging
- `local-ipc-request-handlers.ts`: Extracted local IPC protocol request dispatch, status shaping, familiar actions, and memory CRUD handlers
- `local-ipc-socket.ts`: Extracted socket accept loop, advertised endpoint resolution, and loopback/private address filtering
- `local-ipc-protocol.ts`: Protocol constants, request/response types, validation functions
- `local-ipc-paths.ts`: Platform-specific socket paths and discovery file locations

View file

@ -0,0 +1,320 @@
import { applyAgentPetReaction, applyAgentPetSay } from "./agent-familiar-controller.js";
import type { FamiliarOSActivityRecord } from "./app-state.js";
import { applyExternalPetReaction, applyExternalPetSay, getDefaultPetPaused, isDefaultPetVisible } from "./default-familiar-controller.js";
import { forgetFamiliarOSMemory, listFamiliarOSMemories, searchFamiliarOSMemories, storeFamiliarOSMemory } from "./familiaros-memory.js";
import { installPet } from "./familiar-installation.js";
import { createStaleLeaseStatus, type LeaseManager, type LeaseSnapshot } from "./lease-manager.js";
import {
IpcProtocolError,
isRecord,
validateInstallPetId,
validateMemoryId,
validateMemoryImportance,
validateMemoryLimit,
validateMemoryQuery,
validateMemoryTags,
validateMemoryText,
validateOptionalMemoryKind,
validateOptionalLeaseId,
validateReaction,
validateRequestedPetId,
validateSayMessage,
type FamiliarOSIpcRequest,
} from "./local-ipc-protocol.js";
interface InstalledFamiliarLike {
readonly id: string;
readonly displayName: string;
readonly builtIn: boolean;
readonly broken?: boolean;
}
interface AppStateSnapshotLike {
readonly familiars: {
readonly installed: readonly InstalledFamiliarLike[];
};
readonly preferences: {
readonly defaultPetId: string;
readonly openDefaultPetOnLaunch: boolean;
readonly speechBubblesEnabled: boolean;
};
}
interface DefaultPetLike extends InstalledFamiliarLike {}
export interface LocalIpcRequestDependencies {
readonly appVersion: string;
readonly getAppStateSnapshot: () => AppStateSnapshotLike;
readonly getCurrentDefaultPet: () => { readonly id: string; readonly displayName: string };
readonly getCurrentDefaultPetWithFallback: () => DefaultPetLike;
readonly getLeaseTarget: (value: unknown) => LeaseSnapshot | null;
readonly leaseManager: Pick<LeaseManager, "get" | "acquire" | "heartbeat" | "release">;
readonly safeRecordFamiliarOSActivity: (activity: FamiliarOSActivityRecord) => void;
readonly validateRequiredLeaseId: (value: unknown) => string;
readonly onDebug: (message: string, fields: Record<string, unknown>) => void;
}
export function buildHelloResponse(appVersion: string) {
return {
ok: true,
protocol: "familiaros-ipc",
protocolVersion: 1,
appVersion,
} as const;
}
export function buildFamiliarsListResponse(state: AppStateSnapshotLike) {
return {
ok: true,
familiars: state.familiars.installed.map((familiar) => ({
id: familiar.id,
displayName: familiar.displayName,
builtIn: familiar.builtIn,
broken: familiar.broken === true,
})),
defaultPetId: state.preferences.defaultPetId,
} as const;
}
export async function handleLocalIpcRequest(
request: FamiliarOSIpcRequest,
dependencies: LocalIpcRequestDependencies,
): Promise<unknown> {
if (request.method === "hello") {
return buildHelloResponse(dependencies.appVersion);
}
if (request.method === "status") {
return handleStatusRequest(request, dependencies);
}
if (request.method === "familiars.list") {
return buildFamiliarsListResponse(dependencies.getAppStateSnapshot());
}
if (request.method === "familiars.install") {
return handleFamiliarInstallRequest(request);
}
if (request.method === "lease.acquire") {
return handleLeaseAcquireRequest(request, dependencies);
}
if (request.method === "lease.heartbeat") {
return handleLeaseHeartbeatRequest(request, dependencies);
}
if (request.method === "lease.release") {
return handleLeaseReleaseRequest(request, dependencies);
}
if (request.method === "memory.list") {
return handleMemoryListRequest(request, dependencies);
}
if (request.method === "memory.search") {
return handleMemorySearchRequest(request, dependencies);
}
if (request.method === "memory.store") {
return handleMemoryStoreRequest(request, dependencies);
}
if (request.method === "memory.delete") {
return handleMemoryDeleteRequest(request, dependencies);
}
if (request.method === "familiar.react") {
return handleFamiliarReactionRequest(request, dependencies);
}
return handleFamiliarSayRequest(request, dependencies);
}
function handleStatusRequest(
request: FamiliarOSIpcRequest,
dependencies: LocalIpcRequestDependencies,
) {
const params = isRecord(request.params) ? request.params : {};
const leaseId = validateOptionalLeaseId(params.leaseId);
if (leaseId) {
const lease = dependencies.leaseManager.get(leaseId);
if (!lease) return createStaleLeaseStatus(leaseId);
return { ok: true, appRunning: true, ...lease };
}
const state = dependencies.getAppStateSnapshot();
const defaultPet = dependencies.getCurrentDefaultPetWithFallback();
return {
ok: true,
appRunning: true,
protocolVersion: 1,
appVersion: dependencies.appVersion,
defaultPet: {
id: defaultPet.id,
displayName: defaultPet.displayName,
builtIn: defaultPet.builtIn,
broken: defaultPet.broken === true,
},
paused: getDefaultPetPaused(),
defaultPetVisible: isDefaultPetVisible(),
openDefaultPetOnLaunch: state.preferences.openDefaultPetOnLaunch,
speechBubblesEnabled: state.preferences.speechBubblesEnabled,
};
}
async function handleFamiliarInstallRequest(
request: FamiliarOSIpcRequest,
): Promise<{ readonly ok: true; readonly petId: string; readonly displayName: string; readonly installed: true }> {
const params = isRecord(request.params) ? request.params : {};
const petId = validateInstallPetId(params.petId);
const state = await installPet(petId);
const installed = state.familiars.installed.find((familiar) => familiar.id === petId);
if (!installed) throw new IpcProtocolError("install_failed", "Familiar install did not complete.");
return { ok: true, petId: installed.id, displayName: installed.displayName, installed: true };
}
function handleLeaseAcquireRequest(
request: FamiliarOSIpcRequest,
dependencies: LocalIpcRequestDependencies,
) {
const params = isRecord(request.params) ? request.params : {};
const requestedPetId = validateRequestedPetId(params.requestedPetId);
dependencies.onDebug("lease acquire requested", { requestId: request.id, requestedPetId });
return dependencies.leaseManager.acquire(requestedPetId);
}
function handleLeaseHeartbeatRequest(
request: FamiliarOSIpcRequest,
dependencies: LocalIpcRequestDependencies,
) {
const params = isRecord(request.params) ? request.params : {};
const leaseId = dependencies.validateRequiredLeaseId(params.leaseId);
dependencies.onDebug("lease heartbeat requested", { requestId: request.id, leaseId });
try {
return dependencies.leaseManager.heartbeat(leaseId);
} catch {
throw new IpcProtocolError("unknown_lease", "Unknown or expired lease.");
}
}
function handleLeaseReleaseRequest(
request: FamiliarOSIpcRequest,
dependencies: LocalIpcRequestDependencies,
) {
const params = isRecord(request.params) ? request.params : {};
const leaseId = dependencies.validateRequiredLeaseId(params.leaseId);
dependencies.onDebug("lease release requested", { requestId: request.id, leaseId });
return dependencies.leaseManager.release(leaseId);
}
function handleMemoryListRequest(
request: FamiliarOSIpcRequest,
dependencies: LocalIpcRequestDependencies,
) {
const params = isRecord(request.params) ? request.params : {};
const limit = validateMemoryLimit(params.limit, 12, 25);
dependencies.onDebug("memory list requested", { requestId: request.id, limit });
return {
ok: true,
memories: listFamiliarOSMemories(limit),
};
}
function handleMemorySearchRequest(
request: FamiliarOSIpcRequest,
dependencies: LocalIpcRequestDependencies,
) {
const params = isRecord(request.params) ? request.params : {};
const query = validateMemoryQuery(params.query);
const limit = validateMemoryLimit(params.limit, 8, 12);
dependencies.onDebug("memory search requested", { requestId: request.id, limit });
return {
ok: true,
query,
memories: searchFamiliarOSMemories(query, limit),
};
}
function handleMemoryStoreRequest(
request: FamiliarOSIpcRequest,
dependencies: LocalIpcRequestDependencies,
) {
const params = isRecord(request.params) ? request.params : {};
const text = validateMemoryText(params.text);
const kind = validateOptionalMemoryKind(params.kind);
const tags = validateMemoryTags(params.tags);
const importance = validateMemoryImportance(params.importance);
dependencies.onDebug("memory store requested", {
requestId: request.id,
kind,
hasTags: Array.isArray(tags) && tags.length > 0,
importance,
});
return {
ok: true,
memory: storeFamiliarOSMemory({
text,
kind,
tags,
importance,
source: "mcp",
}),
};
}
function handleMemoryDeleteRequest(
request: FamiliarOSIpcRequest,
dependencies: LocalIpcRequestDependencies,
) {
const params = isRecord(request.params) ? request.params : {};
const id = validateMemoryId(params.id);
dependencies.onDebug("memory delete requested", { requestId: request.id, id });
return {
ok: true,
deleted: forgetFamiliarOSMemory(id),
id,
};
}
function handleFamiliarReactionRequest(
request: FamiliarOSIpcRequest,
dependencies: LocalIpcRequestDependencies,
) {
const params = isRecord(request.params) ? request.params : {};
const reaction = validateReaction(params.reaction);
const lease = dependencies.getLeaseTarget(params.leaseId);
const petId = lease?.actualTargetPetId ?? dependencies.getCurrentDefaultPet().id;
dependencies.onDebug("familiar react requested", {
requestId: request.id,
reaction,
leaseId: lease?.leaseId,
targetKind: lease?.targetKind,
actualPetId: lease?.actualTargetPetId,
});
if (lease?.targetKind === "explicit") {
if (getDefaultPetPaused()) return { ok: true, reaction, shown: false, reason: "paused", leaseId: lease.leaseId };
const applied = applyAgentPetReaction(lease.actualTargetPetId, reaction);
dependencies.safeRecordFamiliarOSActivity({ kind: "react", reaction, petId });
return { ok: true, reaction, shown: applied.shown, reason: applied.reason, leaseId: lease.leaseId };
}
const applied = applyExternalPetReaction(reaction);
dependencies.safeRecordFamiliarOSActivity({ kind: "react", reaction, petId });
return { ok: true, reaction, shown: applied.shown, reason: applied.reason };
}
function handleFamiliarSayRequest(
request: FamiliarOSIpcRequest,
dependencies: LocalIpcRequestDependencies,
) {
const params = isRecord(request.params) ? request.params : {};
const message = validateSayMessage(params.message);
const reaction = params.reaction === undefined ? undefined : validateReaction(params.reaction);
const lease = dependencies.getLeaseTarget(params.leaseId);
const petId = lease?.actualTargetPetId ?? dependencies.getCurrentDefaultPet().id;
dependencies.onDebug("familiar say requested", {
requestId: request.id,
reaction,
messageLength: message.length,
leaseId: lease?.leaseId,
targetKind: lease?.targetKind,
actualPetId: lease?.actualTargetPetId,
});
if (lease?.targetKind === "explicit") {
if (getDefaultPetPaused()) return { ok: true, shown: false, reason: "paused", reaction, leaseId: lease.leaseId };
const applied = applyAgentPetSay(lease.actualTargetPetId, message, reaction);
dependencies.safeRecordFamiliarOSActivity({ kind: "say", reaction, petId });
return { ok: true, shown: applied.shown, reason: applied.reason, reaction, leaseId: lease.leaseId };
}
const applied = applyExternalPetSay(message, reaction);
dependencies.safeRecordFamiliarOSActivity({ kind: "say", reaction, petId });
return { ok: true, shown: applied.shown, reason: applied.reason, reaction };
}

View file

@ -1,17 +1,15 @@
import { randomBytes } from "node:crypto";
import net from "node:net";
import { applyAgentPetReaction, applyAgentPetSay, clearAgentPetLeaseState, showAgentPet } from "./agent-familiar-controller.js";
import { clearAgentPetLeaseState, showAgentPet } from "./agent-familiar-controller.js";
import { getAppStateSnapshot, recordFamiliarOSActivity } from "./app-state.js";
import { builtInPet } from "./built-in-familiar.js";
import { applyExternalPetReaction, applyExternalPetSay, getDefaultPetPaused, isDefaultPetVisible } from "./default-familiar-controller.js";
import { createStaleLeaseStatus, LeaseManager } from "./lease-manager.js";
import { LeaseManager } from "./lease-manager.js";
import { debug, error as logError, info } from "./logger.js";
import { formatIpcEndpoint, getLocalIpcListeningEndpoint, handleLocalIpcSocket, listenOnIpcEndpoint } from "./local-ipc-socket.js";
import { cleanupUnixSocket, getDiscoveryFilePath, getIpcEndpointConfig, protectUnixSocket, removeDiscoveryFile, writeDiscoveryFile, type FamiliarOSDiscoveryFile } from "./local-ipc-paths.js";
import { errorResponse, IpcProtocolError, isRecord, okResponse, parseIpcRequest, validateInstallPetId, validateMemoryId, validateMemoryImportance, validateMemoryLimit, validateMemoryQuery, validateMemoryTags, validateMemoryText, validateOptionalLeaseId, validateOptionalMemoryKind, validateReaction, validateRequestedPetId, validateSayMessage, type FamiliarOSIpcRequest } from "./local-ipc-protocol.js";
import { forgetFamiliarOSMemory, listFamiliarOSMemories, searchFamiliarOSMemories, storeFamiliarOSMemory } from "./familiaros-memory.js";
import { installPet } from "./familiar-installation.js";
import { errorResponse, IpcProtocolError, okResponse, parseIpcRequest, validateOptionalLeaseId } from "./local-ipc-protocol.js";
import { handleLocalIpcRequest } from "./local-ipc-request-handlers.js";
let ipcServer: net.Server | null = null;
let ipcDiscovery: FamiliarOSDiscoveryFile | null = null;
@ -98,185 +96,23 @@ async function handleRawRequest(raw: string, token: string) {
const request = parseIpcRequest(raw, token);
requestId = request.id;
debug("ipc", "request received", { requestId, method: request.method });
return okResponse(request.id, await handleRequest(request));
return okResponse(request.id, await handleLocalIpcRequest(request, {
appVersion: ipcDiscovery?.appVersion ?? "0.0.0",
getAppStateSnapshot,
getCurrentDefaultPet,
getCurrentDefaultPetWithFallback,
getLeaseTarget,
leaseManager,
safeRecordFamiliarOSActivity,
validateRequiredLeaseId,
onDebug: (message, fields) => debug("ipc", message, fields),
}));
} catch (error) {
logError("ipc", "request failed", error instanceof Error ? error : { requestId, error });
return errorResponse(requestId, error);
}
}
async function handleRequest(request: FamiliarOSIpcRequest): Promise<unknown> {
if (request.method === "hello") {
return {
ok: true,
protocol: "familiaros-ipc",
protocolVersion: 1,
appVersion: ipcDiscovery?.appVersion ?? "0.0.0",
};
}
if (request.method === "status") {
const params = isRecord(request.params) ? request.params : {};
const leaseId = validateOptionalLeaseId(params.leaseId);
if (leaseId) {
const lease = leaseManager.get(leaseId);
if (!lease) return createStaleLeaseStatus(leaseId);
return { ok: true, appRunning: true, ...lease };
}
const state = getAppStateSnapshot();
const defaultPet = state.familiars.installed.find((familiar) => familiar.id === state.preferences.defaultPetId) ?? builtInPet;
return {
ok: true,
appRunning: true,
protocolVersion: 1,
appVersion: ipcDiscovery?.appVersion ?? "0.0.0",
defaultPet: {
id: defaultPet.id,
displayName: defaultPet.displayName,
builtIn: defaultPet.builtIn,
broken: "broken" in defaultPet && defaultPet.broken === true,
},
paused: getDefaultPetPaused(),
defaultPetVisible: isDefaultPetVisible(),
openDefaultPetOnLaunch: state.preferences.openDefaultPetOnLaunch,
speechBubblesEnabled: state.preferences.speechBubblesEnabled,
};
}
if (request.method === "familiars.list") {
const state = getAppStateSnapshot();
return {
ok: true,
familiars: state.familiars.installed.map((familiar) => ({
id: familiar.id,
displayName: familiar.displayName,
builtIn: familiar.builtIn,
broken: familiar.broken === true,
})),
defaultPetId: state.preferences.defaultPetId,
};
}
if (request.method === "familiars.install") {
const params = isRecord(request.params) ? request.params : {};
const petId = validateInstallPetId(params.petId);
const state = await installPet(petId);
const installed = state.familiars.installed.find((familiar) => familiar.id === petId);
if (!installed) throw new IpcProtocolError("install_failed", "Familiar install did not complete.");
return { ok: true, petId: installed.id, displayName: installed.displayName, installed: true };
}
if (request.method === "lease.acquire") {
const params = isRecord(request.params) ? request.params : {};
const requestedPetId = validateRequestedPetId(params.requestedPetId);
debug("ipc", "lease acquire requested", { requestId: request.id, requestedPetId });
return leaseManager.acquire(requestedPetId);
}
if (request.method === "lease.heartbeat") {
const params = isRecord(request.params) ? request.params : {};
const leaseId = validateRequiredLeaseId(params.leaseId);
debug("ipc", "lease heartbeat requested", { requestId: request.id, leaseId });
try {
return leaseManager.heartbeat(leaseId);
} catch {
throw new IpcProtocolError("unknown_lease", "Unknown or expired lease.");
}
}
if (request.method === "lease.release") {
const params = isRecord(request.params) ? request.params : {};
const leaseId = validateRequiredLeaseId(params.leaseId);
debug("ipc", "lease release requested", { requestId: request.id, leaseId });
return leaseManager.release(leaseId);
}
if (request.method === "memory.list") {
const params = isRecord(request.params) ? request.params : {};
const limit = validateMemoryLimit(params.limit, 12, 25);
debug("ipc", "memory list requested", { requestId: request.id, limit });
return {
ok: true,
memories: listFamiliarOSMemories(limit),
};
}
if (request.method === "memory.search") {
const params = isRecord(request.params) ? request.params : {};
const query = validateMemoryQuery(params.query);
const limit = validateMemoryLimit(params.limit, 8, 12);
debug("ipc", "memory search requested", { requestId: request.id, limit });
return {
ok: true,
query,
memories: searchFamiliarOSMemories(query, limit),
};
}
if (request.method === "memory.store") {
const params = isRecord(request.params) ? request.params : {};
const text = validateMemoryText(params.text);
const kind = validateOptionalMemoryKind(params.kind);
const tags = validateMemoryTags(params.tags);
const importance = validateMemoryImportance(params.importance);
debug("ipc", "memory store requested", { requestId: request.id, kind, hasTags: Array.isArray(tags) && tags.length > 0, importance });
return {
ok: true,
memory: storeFamiliarOSMemory({
text,
kind,
tags,
importance,
source: "mcp",
}),
};
}
if (request.method === "memory.delete") {
const params = isRecord(request.params) ? request.params : {};
const id = validateMemoryId(params.id);
debug("ipc", "memory delete requested", { requestId: request.id, id });
return {
ok: true,
deleted: forgetFamiliarOSMemory(id),
id,
};
}
if (request.method === "familiar.react") {
const params = isRecord(request.params) ? request.params : {};
const reaction = validateReaction(params.reaction);
const lease = getLeaseTarget(params.leaseId);
const petId = lease?.actualTargetPetId ?? getCurrentDefaultPet().id;
debug("ipc", "familiar react requested", { requestId: request.id, reaction, leaseId: lease?.leaseId, targetKind: lease?.targetKind, actualPetId: lease?.actualTargetPetId });
if (lease?.targetKind === "explicit") {
if (getDefaultPetPaused()) return { ok: true, reaction, shown: false, reason: "paused", leaseId: lease.leaseId };
const applied = applyAgentPetReaction(lease.actualTargetPetId, reaction);
safeRecordFamiliarOSActivity({ kind: "react", reaction, petId });
return { ok: true, reaction, shown: applied.shown, reason: applied.reason, leaseId: lease.leaseId };
}
const applied = applyExternalPetReaction(reaction);
safeRecordFamiliarOSActivity({ kind: "react", reaction, petId });
return { ok: true, reaction, shown: applied.shown, reason: applied.reason };
}
const params = isRecord(request.params) ? request.params : {};
const message = validateSayMessage(params.message);
const reaction = params.reaction === undefined ? undefined : validateReaction(params.reaction);
const lease = getLeaseTarget(params.leaseId);
const petId = lease?.actualTargetPetId ?? getCurrentDefaultPet().id;
debug("ipc", "familiar say requested", { requestId: request.id, reaction, messageLength: message.length, leaseId: lease?.leaseId, targetKind: lease?.targetKind, actualPetId: lease?.actualTargetPetId });
if (lease?.targetKind === "explicit") {
if (getDefaultPetPaused()) return { ok: true, shown: false, reason: "paused", reaction, leaseId: lease.leaseId };
const applied = applyAgentPetSay(lease.actualTargetPetId, message, reaction);
safeRecordFamiliarOSActivity({ kind: "say", reaction, petId });
return { ok: true, shown: applied.shown, reason: applied.reason, reaction, leaseId: lease.leaseId };
}
const applied = applyExternalPetSay(message, reaction);
safeRecordFamiliarOSActivity({ kind: "say", reaction, petId });
return { ok: true, shown: applied.shown, reason: applied.reason, reaction };
}
function safeRecordFamiliarOSActivity(activity: Parameters<typeof recordFamiliarOSActivity>[0]): void {
try {
recordFamiliarOSActivity(activity);
@ -323,7 +159,13 @@ function getCurrentDefaultPet(): { readonly id: string; readonly displayName: st
return { id: familiar.id, displayName: familiar.displayName };
}
function getCurrentDefaultPetWithFallback(): { readonly id: string; readonly displayName: string; readonly fallbackReason?: "default_broken_fallback_builtin" } {
function getCurrentDefaultPetWithFallback(): {
readonly id: string;
readonly displayName: string;
readonly builtIn: boolean;
readonly broken?: boolean;
readonly fallbackReason?: "default_broken_fallback_builtin";
} {
const state = getAppStateSnapshot();
const configuredDefault = state.familiars.installed.find((familiar) => familiar.id === state.preferences.defaultPetId);
if (configuredDefault && !configuredDefault.broken) return configuredDefault;

View file

@ -0,0 +1,53 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { buildFamiliarsListResponse, buildHelloResponse } from "../src/local-ipc-request-handlers.js";
const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
const localIpcSource = readFileSync(resolve(desktopRoot, "src/local-ipc.ts"), "utf8");
const localIpcRequestHandlersSource = readFileSync(resolve(desktopRoot, "src/local-ipc-request-handlers.ts"), "utf8");
assert.match(localIpcSource, /from "\.\/local-ipc-request-handlers(?:\.js)?"/, "local-ipc must compose the extracted request-handler seam.");
assert.match(localIpcRequestHandlersSource, /export async function handleLocalIpcRequest/, "local-ipc request-handler seam must export the async dispatcher.");
assert.match(localIpcRequestHandlersSource, /export function buildHelloResponse/, "local-ipc request-handler seam must export the hello response builder.");
assert.match(localIpcRequestHandlersSource, /export function buildFamiliarsListResponse/, "local-ipc request-handler seam must export familiar list response shaping.");
assert.deepEqual(
buildHelloResponse("1.2.3"),
{
ok: true,
protocol: "familiaros-ipc",
protocolVersion: 1,
appVersion: "1.2.3",
},
"local-ipc hello responses must preserve protocol and app version metadata.",
);
assert.deepEqual(
buildFamiliarsListResponse({
familiars: {
installed: [
{ id: "built-in", displayName: "Built In", builtIn: true },
{ id: "fox", displayName: "Fox", builtIn: false, broken: true },
],
},
preferences: {
defaultPetId: "fox",
openDefaultPetOnLaunch: true,
speechBubblesEnabled: true,
},
}),
{
ok: true,
familiars: [
{ id: "built-in", displayName: "Built In", builtIn: true, broken: false },
{ id: "fox", displayName: "Fox", builtIn: false, broken: true },
],
defaultPetId: "fox",
},
"local-ipc familiar list responses must normalize broken flags for each installed familiar.",
);
console.error("Local IPC request-handler seam validation passed.");

View file

@ -7,12 +7,15 @@ import { getLocalIpcListeningEndpoint, isAllowedRemoteAddress } from "../src/loc
const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
const localIpcSource = readFileSync(resolve(desktopRoot, "src/local-ipc.ts"), "utf8");
const localIpcRequestHandlersSource = readFileSync(resolve(desktopRoot, "src/local-ipc-request-handlers.ts"), "utf8");
const localIpcSocketSource = readFileSync(resolve(desktopRoot, "src/local-ipc-socket.ts"), "utf8");
assert.match(localIpcSource, /from "\.\/local-ipc-socket(?:\.js)?"/, "local-ipc must compose the extracted socket helper seam.");
assert.match(localIpcSource, /from "\.\/local-ipc-request-handlers(?:\.js)?"/, "local-ipc must compose the extracted request-handler seam.");
assert.match(localIpcSocketSource, /export function handleLocalIpcSocket/, "local-ipc socket seam must export the client socket handler.");
assert.match(localIpcSocketSource, /export function getLocalIpcListeningEndpoint/, "local-ipc socket seam must export advertised endpoint resolution.");
assert.match(localIpcSocketSource, /export function isAllowedRemoteAddress/, "local-ipc socket seam must export remote-address filtering.");
assert.match(localIpcRequestHandlersSource, /export async function handleLocalIpcRequest/, "local-ipc request-handler seam must export the async dispatcher.");
assert.equal(isAllowedRemoteAddress("127.0.0.1", "127.0.0.1"), true, "loopback clients must always be allowed.");
assert.equal(isAllowedRemoteAddress("::ffff:127.0.0.1", "127.0.0.1"), true, "IPv4-mapped loopback clients must be allowed.");