refactor: prune dead IPC surface and orphaned modules
Drop code that nothing consumed. Verified by full renderer+shared grep:
every removed channel had zero callers via window.agentCompany.* and
zero references from desktop-api types. No backend internal use either —
the database/API-server layers keep their methods, only the IPC exposure
goes.
Removed IPC handlers (26 total):
Thread CRUD surface (12) — writes happen inside the backend
createThread, listThreads, addThreadEvent,
setThreadWaitState, resolveThreadWaitState,
addParticipant, removeParticipant, listParticipants,
subscribeThread, unsubscribeThread,
listSubscriptions, listSubscribedThreads
(kept reads: getThread, listThreadEvents, listThreadWaitStates)
Half-implemented features (13)
saveSecret, listComments, getAgentMetrics, getApiPort,
listWakeupRequests, listAgentMessages, autoAssignTask,
listRunEvents, recoveryDismiss,
getHeartbeatPolicy, setHeartbeatPolicy,
getRuntimeState, setRuntimeState
Whole vertical slice: openConnectorAuthTerminal
IPC handler + channel + preload + DesktopApi type +
connector-auth-service function + useConnectorCallbacks
handleLoginConnector + OnboardingContext.onLoginConnector +
OnboardingWizard wiring. Pi-mono has no CLI auth flow;
the no-op that "returned false for legacy UI compatibility"
had no legacy UI left.
Orphaned modules (zero importers in repo):
src/main/runtime-env.ts
src/main/database-stale-recovery.ts
Dead schema files (only referenced by their own barrel re-export):
schemas-participants, schemas-subscriptions, schemas-heartbeat,
schemas-run-events, schemas-runtime-state, schemas-wakeup
Renderer leftovers:
.activity-row-enter CSS + its two unreferenced keyframes
5 orphan i18n keys (app.console, app.refresh, app.expand,
activity.timeline, auto.trigger) left over from earlier feature
deletions
Also update two unit tests that mocked openConnectorAuthTerminal in
the RegisterCoreHandlersDependencies pick.
This commit is contained in:
1 parent
1f7230fe87
commit
c71d033b0c
37 files changed
+17
-929
No files matched your search
@@ -5,7 +5,6 @@ import { join } from "node:path";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import log from "electron-log/main.js";
|
||||
import type { ConnectorId } from "@shared/types";
|
||||
|
||||
function quoteShell(value: string) {
|
||||
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
||||
@@ -88,25 +87,6 @@ function spawnTerminalWithScript(script: string, _title: string, _cwd: string):
|
||||
throw new Error("No supported terminal application was found.");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Connector auth terminal — deprecated under pi-mono */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Pi-mono has no CLI auth flow. OAuth runs through `oauth-login-service`,
|
||||
* API keys are entered via the Settings form, and cloud providers (AWS,
|
||||
* Azure, GCP) use environment-driven credentials. The IPC handler still
|
||||
* calls this function for legacy UI compatibility, so we keep the signature
|
||||
* but treat it as a no-op that reports "not available".
|
||||
*/
|
||||
export async function openConnectorAuthTerminal(
|
||||
_connectorId: ConnectorId,
|
||||
_command: string,
|
||||
_cwd?: string,
|
||||
): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Generic terminal command (for install flows, etc.) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { STALE_LOCK_TIMEOUT_MS } from "./database-base";
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stale-state recovery helpers extracted from `database-base.ts` to keep that
|
||||
* file under the 400-line cap. Both helpers are called by `AppDatabase.init`
|
||||
* (interruptStaleRuns) and the orchestration recovery loop (recoverStaleLocks).
|
||||
*/
|
||||
|
||||
interface InterruptDeps {
|
||||
db: DatabaseSync;
|
||||
transaction: <T>(op: () => T) => T;
|
||||
releaseAllWorkspaceLocks: () => void;
|
||||
}
|
||||
|
||||
export function interruptStaleRunsImpl(deps: InterruptDeps): number {
|
||||
const ts = nowIso();
|
||||
let interruptedCount = 0;
|
||||
deps.transaction(() => {
|
||||
const result = deps.db
|
||||
.prepare(
|
||||
"update runs set status = 'interrupted', error_message = 'AgentCompany restarted before the run could report completion.', finished_at = ?, updated_at = ? where status = 'running'",
|
||||
)
|
||||
.run(ts, ts);
|
||||
interruptedCount = Number(result.changes);
|
||||
if (interruptedCount > 0) {
|
||||
deps.db
|
||||
.prepare(
|
||||
"update tasks set status = 'blocked', updated_at = ? where id in (select task_id from runs where status = 'interrupted' and finished_at = ?) and status in ('in_progress', 'todo')",
|
||||
)
|
||||
.run(ts, ts);
|
||||
}
|
||||
deps.db
|
||||
.prepare(
|
||||
"update tasks set active_run_id = null where active_run_id in (select id from runs where status = 'interrupted')",
|
||||
)
|
||||
.run();
|
||||
deps.db.prepare("update agents set status = 'idle', updated_at = ? where status = 'running'").run(ts);
|
||||
deps.releaseAllWorkspaceLocks();
|
||||
});
|
||||
return interruptedCount;
|
||||
}
|
||||
|
||||
interface RecoverDeps {
|
||||
db: DatabaseSync;
|
||||
readAll: <T>(sql: string, ...params: import("node:sqlite").SQLInputValue[]) => T[];
|
||||
}
|
||||
|
||||
export function recoverStaleLocksImpl(
|
||||
deps: RecoverDeps,
|
||||
): Array<{ taskId: string; companyId: string; title: string }> {
|
||||
const cutoff = new Date(Date.now() - STALE_LOCK_TIMEOUT_MS).toISOString();
|
||||
const stale = deps.readAll<{ id: string; companyId: string; title: string }>(
|
||||
"SELECT id, company_id AS companyId, title FROM tasks WHERE execution_locked_at IS NOT NULL AND execution_locked_at < ? AND status = 'in_progress' AND checkout_run_id NOT IN (SELECT id FROM runs WHERE status = 'running')",
|
||||
cutoff,
|
||||
);
|
||||
const ts = nowIso();
|
||||
for (const task of stale) {
|
||||
deps.db
|
||||
.prepare(
|
||||
"UPDATE tasks SET checkout_run_id = NULL, execution_locked_at = NULL, status = 'todo', updated_at = ? WHERE id = ?",
|
||||
)
|
||||
.run(ts, task.id);
|
||||
}
|
||||
return stale.map((t) => ({ taskId: t.id, companyId: t.companyId, title: t.title }));
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import {
|
||||
IPC_CHANNELS,
|
||||
agentMessageInputSchema,
|
||||
autoAssignSchema,
|
||||
markMessageReadSchema,
|
||||
messageQuerySchema,
|
||||
searchMessagesSchema,
|
||||
} from "@shared/contracts";
|
||||
import { fail, ok, type RegisterDomainHandlersDependencies } from "./ipc-domain-common";
|
||||
@@ -13,7 +11,6 @@ export function registerAgentCoordinationHandlers({
|
||||
registerHandle,
|
||||
db,
|
||||
ensureCompanyExists,
|
||||
findInvalidReference,
|
||||
publishDomainChanged,
|
||||
emitEvent,
|
||||
notify,
|
||||
@@ -24,7 +21,6 @@ export function registerAgentCoordinationHandlers({
|
||||
| "registerHandle"
|
||||
| "db"
|
||||
| "ensureCompanyExists"
|
||||
| "findInvalidReference"
|
||||
| "publishDomainChanged"
|
||||
| "emitEvent"
|
||||
| "notify"
|
||||
@@ -56,26 +52,6 @@ export function registerAgentCoordinationHandlers({
|
||||
});
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.listAgentMessages, async (_event, payload) => {
|
||||
const parsed = messageQuerySchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
const invalidReference = findInvalidReference(parsed.companyId, [
|
||||
{ id: parsed.agentId, table: "agents", label: "message reader" },
|
||||
{ id: parsed.channel === "project" ? parsed.channelTargetId : null, table: "projects", label: "project channel" },
|
||||
]);
|
||||
if (invalidReference) {
|
||||
return fail("INVALID_COMPANY_REFERENCE", `The selected ${invalidReference.label} does not belong to the current company.`);
|
||||
}
|
||||
return ok(db.listAgentMessages(parsed.companyId, {
|
||||
agentId: parsed.agentId,
|
||||
channel: parsed.channel,
|
||||
channelTargetId: parsed.channelTargetId,
|
||||
unreadOnly: parsed.unreadOnly,
|
||||
limit: parsed.limit,
|
||||
}));
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.markMessageRead, async (_event, payload) => {
|
||||
const parsed = markMessageReadSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
@@ -115,15 +91,4 @@ export function registerAgentCoordinationHandlers({
|
||||
}));
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.autoAssignTask, async (_event, payload) => {
|
||||
const parsed = autoAssignSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
const agentId = db.autoAssignTask(parsed.taskId, parsed.companyId);
|
||||
if (!agentId) {
|
||||
return fail("NO_AVAILABLE_AGENT", "No available agent could be found for this task.");
|
||||
}
|
||||
publishDomainChanged();
|
||||
return ok(agentId);
|
||||
});
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
connectorInputSchema,
|
||||
deleteEntitySchema,
|
||||
openTerminalCommandSchema,
|
||||
secretInputSchema,
|
||||
} from "@shared/contracts";
|
||||
import { validateConnectorConfigurationModel, validateConnectorModelSelection } from "./connector-model-policy";
|
||||
import { reconcileExecutionModeChange } from "./execution-mode-reconciler";
|
||||
@@ -40,11 +39,10 @@ export function registerCoreAdminHandlers({
|
||||
publishDomainChanged,
|
||||
handleApprovalResolved,
|
||||
checkConnector,
|
||||
openConnectorAuthTerminal,
|
||||
openGenericTerminal,
|
||||
}: Pick<
|
||||
RegisterCoreHandlersDependencies,
|
||||
"registerHandle" | "db" | "ensureCompanyExists" | "publishDomainChanged" | "handleApprovalResolved" | "checkConnector" | "openConnectorAuthTerminal" | "openGenericTerminal"
|
||||
"registerHandle" | "db" | "ensureCompanyExists" | "publishDomainChanged" | "handleApprovalResolved" | "checkConnector" | "openGenericTerminal"
|
||||
>) {
|
||||
registerHandle(IPC_CHANNELS.saveCompany, async (_event, payload) => {
|
||||
const parsed = companyInputSchema.parse(payload);
|
||||
@@ -123,12 +121,6 @@ export function registerCoreAdminHandlers({
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.openConnectorAuthTerminal, async (_event, id) => {
|
||||
const parsed = connectorInputSchema.shape.id.parse(id);
|
||||
await openConnectorAuthTerminal(parsed);
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.checkNodeInstalled, async () => {
|
||||
return ok(await probeShellNode());
|
||||
});
|
||||
@@ -146,23 +138,6 @@ export function registerCoreAdminHandlers({
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.saveSecret, async (_event, payload) => {
|
||||
const parsed = secretInputSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
const id = db.saveSecret(parsed);
|
||||
db.addActivity({
|
||||
companyId: parsed.companyId,
|
||||
actor: "board",
|
||||
action: parsed.id ? "secret.updated" : "secret.created",
|
||||
entityType: "secret",
|
||||
entityId: id,
|
||||
detail: parsed.name,
|
||||
});
|
||||
publishDomainChanged();
|
||||
return ok(id);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.deleteSecret, async (_event, payload) => {
|
||||
const parsed = deleteEntitySchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
|
||||
@@ -26,12 +26,10 @@ export interface RegisterCoreHandlersDependencies {
|
||||
dispatchAutomation: (trigger: string, ctx: { companyId: string; [key: string]: unknown }) => void;
|
||||
queueTaskRun: (taskId: string, trigger: string, actor: string) => DesktopResult<string>;
|
||||
checkConnector: (id: ConnectorId) => Promise<void>;
|
||||
openConnectorAuthTerminal: (id: ConnectorId) => Promise<boolean>;
|
||||
openGenericTerminal: (input: { commandKey: string }) => Promise<boolean>;
|
||||
checkForUpdates: (options?: { manual?: boolean }) => Promise<boolean>;
|
||||
getWorkerProcess: () => { postMessage: (message: unknown) => void } | null;
|
||||
resetAfterRestore: () => void;
|
||||
getApiPort: () => number;
|
||||
listConnectorModels: (connectorId: string) => Promise<Array<{ id: string; label: string }>>;
|
||||
notify: (options: { title: string; body: string; urgency: "critical" | "informational"; navigation?: { section: SectionId; entityId?: string }; batchKey?: string }) => void;
|
||||
logger: {
|
||||
|
||||
@@ -4,15 +4,9 @@ import {
|
||||
approvalInputSchema,
|
||||
cancelRunSchema,
|
||||
commentInputSchema,
|
||||
heartbeatPolicySchema,
|
||||
heartbeatSettingsSchema,
|
||||
listCommentsSchema,
|
||||
listRunEventsSchema,
|
||||
runtimeStateSchema,
|
||||
runLogChunkSchema,
|
||||
runLogSchema,
|
||||
setHeartbeatPolicySchema,
|
||||
setRuntimeStateSchema,
|
||||
startRunSchema,
|
||||
triggerHeartbeatSchema,
|
||||
} from "@shared/contracts";
|
||||
@@ -209,16 +203,6 @@ export function registerCoreOperationHandlers({
|
||||
return ok(comment.id);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.listComments, async (_event, payload) => {
|
||||
const parsed = listCommentsSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) {
|
||||
return fail("TASK_NOT_FOUND", "Task not found in the current company.");
|
||||
}
|
||||
return ok(db.listComments(parsed.taskId));
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.setHeartbeat, async (_event, payload) => {
|
||||
const parsed = heartbeatSettingsSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
@@ -254,57 +238,4 @@ export function registerCoreOperationHandlers({
|
||||
return ok(runId);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.listRunEvents, async (_event, payload) => {
|
||||
const parsed = listRunEventsSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
if (!db.belongsToCompany("runs", parsed.runId, parsed.companyId)) {
|
||||
return fail("RUN_NOT_FOUND", "Run not found in the current company.");
|
||||
}
|
||||
return ok(db.listRunEvents(parsed.runId, parsed.companyId, { offset: parsed.offset, limit: parsed.limit }));
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.getHeartbeatPolicy, async (_event, payload) => {
|
||||
const parsed = heartbeatPolicySchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
if (!db.belongsToCompany("agents", parsed.agentId, parsed.companyId)) {
|
||||
return fail("AGENT_NOT_FOUND", "Agent not found in the current company.");
|
||||
}
|
||||
return ok(db.getHeartbeatPolicy(parsed.agentId));
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.setHeartbeatPolicy, async (_event, payload) => {
|
||||
const parsed = setHeartbeatPolicySchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
if (!db.belongsToCompany("agents", parsed.agentId, parsed.companyId)) {
|
||||
return fail("AGENT_NOT_FOUND", "Agent not found in the current company.");
|
||||
}
|
||||
const { companyId, agentId, ...policy } = parsed;
|
||||
db.setHeartbeatPolicy(agentId, companyId, policy);
|
||||
publishDomainChanged();
|
||||
return ok(null);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.getRuntimeState, async (_event, payload) => {
|
||||
const parsed = runtimeStateSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
if (!db.belongsToCompany("agents", parsed.agentId, parsed.companyId)) {
|
||||
return fail("AGENT_NOT_FOUND", "Agent not found in the current company.");
|
||||
}
|
||||
return ok(db.getRuntimeState(parsed.agentId));
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.setRuntimeState, async (_event, payload) => {
|
||||
const parsed = setRuntimeStateSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
if (!db.belongsToCompany("agents", parsed.agentId, parsed.companyId)) {
|
||||
return fail("AGENT_NOT_FOUND", "Agent not found in the current company.");
|
||||
}
|
||||
db.setRuntimeState(parsed.agentId, parsed.companyId, parsed.state);
|
||||
return ok(null);
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
entityGetterSchema,
|
||||
metricsQuerySchema,
|
||||
settingsSchema,
|
||||
wakeupQuerySchema,
|
||||
} from "@shared/contracts";
|
||||
import { fail, ok, type RegisterCoreHandlersDependencies } from "./ipc-core-common";
|
||||
|
||||
@@ -18,7 +17,6 @@ export function registerCoreReadHandlers({
|
||||
publishDomainChanged,
|
||||
checkForUpdates,
|
||||
resetAfterRestore,
|
||||
getApiPort,
|
||||
listConnectorModels,
|
||||
logger,
|
||||
}: Pick<
|
||||
@@ -29,7 +27,6 @@ export function registerCoreReadHandlers({
|
||||
| "publishDomainChanged"
|
||||
| "checkForUpdates"
|
||||
| "resetAfterRestore"
|
||||
| "getApiPort"
|
||||
| "listConnectorModels"
|
||||
| "logger"
|
||||
>) {
|
||||
@@ -105,8 +102,6 @@ export function registerCoreReadHandlers({
|
||||
return ok(db.getSidebarBadges(companyId));
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.getApiPort, () => ok(getApiPort()));
|
||||
|
||||
registerHandle(IPC_CHANNELS.listConnectorModels, async (_event, connectorId) => {
|
||||
try {
|
||||
const parsed = connectorInputSchema.shape.id.parse(connectorId);
|
||||
@@ -141,21 +136,6 @@ export function registerCoreReadHandlers({
|
||||
return ok(db.getCompanyMetrics(input.companyId, input.days));
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.getAgentMetrics, (_event, payload) => {
|
||||
const input = metricsQuerySchema.parse(payload);
|
||||
const err = ensureCompanyExists(input.companyId);
|
||||
if (err) return err;
|
||||
if (!input.agentId) {
|
||||
return fail("MISSING_AGENT_ID", "agentId is required");
|
||||
}
|
||||
const metrics = db.getCompanyMetrics(input.companyId, input.days);
|
||||
const agentMetric = metrics.agentMetrics.find((metric: { agentId: string }) => metric.agentId === input.agentId);
|
||||
if (!agentMetric) {
|
||||
return fail("AGENT_NOT_FOUND", "Agent metrics not found");
|
||||
}
|
||||
return ok(agentMetric);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.getStandupReport, (_event, payload) => {
|
||||
const companyId = z.string().uuid().parse(payload);
|
||||
const err = ensureCompanyExists(companyId);
|
||||
@@ -163,17 +143,6 @@ export function registerCoreReadHandlers({
|
||||
return ok(db.getStandupReport(companyId));
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.listWakeupRequests, (_event, input) => {
|
||||
const parsed = wakeupQuerySchema.parse(input);
|
||||
const err = ensureCompanyExists(parsed.companyId);
|
||||
if (err) return err;
|
||||
return ok(db.listWakeupRequests(parsed.companyId, {
|
||||
status: parsed.status,
|
||||
agentId: parsed.agentId,
|
||||
limit: parsed.limit,
|
||||
}));
|
||||
});
|
||||
|
||||
// --- Entity getters for granular IPC updates ---
|
||||
|
||||
registerHandle(IPC_CHANNELS.getTask, (_event, payload) => {
|
||||
|
||||
@@ -46,18 +46,6 @@ export function registerRecoveryHandlers({
|
||||
return ok(null);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.recoveryDismiss, async (_event, payload) => {
|
||||
const parsed = recoveryTaskSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) {
|
||||
return fail("TASK_NOT_FOUND", "Task not found in the current company.");
|
||||
}
|
||||
db.dismissInterruptedTask(parsed.taskId);
|
||||
publishDomainChanged();
|
||||
return ok(null);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.recoveryFail, async (_event, payload) => {
|
||||
const parsed = recoveryTaskSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
|
||||
@@ -1,55 +1,13 @@
|
||||
import {
|
||||
IPC_CHANNELS,
|
||||
addThreadEventSchema,
|
||||
addParticipantSchema,
|
||||
createThreadSchema,
|
||||
listParticipantsSchema,
|
||||
listSubscriptionsSchema,
|
||||
listSubscribedThreadsSchema,
|
||||
listThreadEventsSchema,
|
||||
listThreadWaitStatesSchema,
|
||||
listThreadsSchema,
|
||||
removeParticipantSchema,
|
||||
resolveThreadWaitStateSchema,
|
||||
setThreadWaitStateSchema,
|
||||
subscribeThreadSchema,
|
||||
threadGetterSchema,
|
||||
unsubscribeThreadSchema,
|
||||
} from "@shared/contracts";
|
||||
import type { RegisterCoreHandlersDependencies } from "./ipc-core-common";
|
||||
import { fail, ok } from "./ipc-core-common";
|
||||
|
||||
export function registerThreadHandlers({ registerHandle, db, ensureCompanyExists, publishDomainChanged, emitEvent }: RegisterCoreHandlersDependencies) {
|
||||
registerHandle(IPC_CHANNELS.createThread, async (_event, payload) => {
|
||||
const input = createThreadSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(input.companyId);
|
||||
if (companyError) return companyError;
|
||||
const thread = db.createThread({
|
||||
companyId: input.companyId,
|
||||
title: input.title,
|
||||
type: input.type,
|
||||
relatedEntityType: input.relatedEntityType ?? null,
|
||||
relatedEntityId: input.relatedEntityId ?? null,
|
||||
});
|
||||
publishDomainChanged();
|
||||
return ok(thread);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.listThreads, async (_event, payload) => {
|
||||
const input = listThreadsSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(input.companyId);
|
||||
if (companyError) return companyError;
|
||||
const threads = db.listThreads({
|
||||
companyId: input.companyId,
|
||||
type: input.type,
|
||||
relatedEntityType: input.relatedEntityType,
|
||||
relatedEntityId: input.relatedEntityId,
|
||||
limit: input.limit,
|
||||
offset: input.offset,
|
||||
});
|
||||
return ok(threads);
|
||||
});
|
||||
|
||||
export function registerThreadHandlers({ registerHandle, db, ensureCompanyExists }: RegisterCoreHandlersDependencies) {
|
||||
registerHandle(IPC_CHANNELS.getThread, async (_event, payload) => {
|
||||
const input = threadGetterSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(input.companyId);
|
||||
@@ -59,22 +17,6 @@ export function registerThreadHandlers({ registerHandle, db, ensureCompanyExists
|
||||
return ok(thread);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.addThreadEvent, async (_event, payload) => {
|
||||
const input = addThreadEventSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(input.companyId);
|
||||
if (companyError) return companyError;
|
||||
const event = db.addThreadEvent({
|
||||
threadId: input.threadId,
|
||||
actorId: input.actorId,
|
||||
actorType: input.actorType,
|
||||
eventType: input.eventType,
|
||||
body: input.body,
|
||||
metadataJson: input.metadataJson,
|
||||
});
|
||||
publishDomainChanged();
|
||||
return ok(event);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.listThreadEvents, async (_event, payload) => {
|
||||
const input = listThreadEventsSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(input.companyId);
|
||||
@@ -86,50 +28,6 @@ export function registerThreadHandlers({ registerHandle, db, ensureCompanyExists
|
||||
return ok(events);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.setThreadWaitState, async (_event, payload) => {
|
||||
const input = setThreadWaitStateSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(input.companyId);
|
||||
if (companyError) return companyError;
|
||||
const waitState = db.setThreadWaitState({
|
||||
threadId: input.threadId,
|
||||
companyId: input.companyId,
|
||||
waitingFor: input.waitingFor,
|
||||
relatedApprovalId: input.relatedApprovalId ?? null,
|
||||
reason: input.reason,
|
||||
});
|
||||
publishDomainChanged();
|
||||
emitEvent({
|
||||
type: "thread-wait-state-updated",
|
||||
threadId: input.threadId,
|
||||
companyId: input.companyId,
|
||||
waitStateId: waitState.id,
|
||||
action: "created",
|
||||
});
|
||||
return ok(waitState);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.resolveThreadWaitState, async (_event, payload) => {
|
||||
const input = resolveThreadWaitStateSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(input.companyId);
|
||||
if (companyError) return companyError;
|
||||
|
||||
// Query the wait state before resolving to capture its threadId for the event
|
||||
const waitStates = db.listThreadWaitStates(input.companyId);
|
||||
const target = waitStates.find((ws) => ws.id === input.waitStateId);
|
||||
if (!target) return fail("THREAD_WAIT_STATE_NOT_FOUND", "Wait state not found");
|
||||
|
||||
db.resolveThreadWaitState(input.waitStateId, input.companyId);
|
||||
publishDomainChanged();
|
||||
emitEvent({
|
||||
type: "thread-wait-state-updated",
|
||||
threadId: target.threadId,
|
||||
companyId: input.companyId,
|
||||
waitStateId: input.waitStateId,
|
||||
action: "resolved",
|
||||
});
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.listThreadWaitStates, async (_event, payload) => {
|
||||
const input = listThreadWaitStatesSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(input.companyId);
|
||||
@@ -137,72 +35,4 @@ export function registerThreadHandlers({ registerHandle, db, ensureCompanyExists
|
||||
const waitStates = db.listThreadWaitStates(input.companyId, { resolvedOnly: input.resolvedOnly });
|
||||
return ok(waitStates);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.listParticipants, async (_event, payload) => {
|
||||
const input = listParticipantsSchema.parse(payload);
|
||||
const thread = db.getThread(input.threadId, input.companyId);
|
||||
if (!thread) return fail("THREAD_NOT_FOUND", "Thread not found in this company");
|
||||
const participants = db.listParticipants(input.threadId);
|
||||
return ok(participants);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.addParticipant, async (_event, payload) => {
|
||||
const input = addParticipantSchema.parse(payload);
|
||||
const thread = db.getThread(input.threadId, input.companyId);
|
||||
if (!thread) return fail("THREAD_NOT_FOUND", "Thread not found in this company");
|
||||
const participant = db.addParticipant({
|
||||
threadId: input.threadId,
|
||||
participantId: input.participantId,
|
||||
participantType: input.participantType,
|
||||
role: input.role,
|
||||
});
|
||||
publishDomainChanged();
|
||||
return ok(participant);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.removeParticipant, async (_event, payload) => {
|
||||
const input = removeParticipantSchema.parse(payload);
|
||||
const thread = db.getThread(input.threadId, input.companyId);
|
||||
if (!thread) return fail("THREAD_NOT_FOUND", "Thread not found in this company");
|
||||
db.removeParticipant(input.threadId, input.participantId);
|
||||
publishDomainChanged();
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.subscribeThread, async (_event, payload) => {
|
||||
const input = subscribeThreadSchema.parse(payload);
|
||||
const thread = db.getThread(input.threadId, input.companyId);
|
||||
if (!thread) return fail("THREAD_NOT_FOUND", "Thread not found in this company");
|
||||
const subscription = db.subscribe({
|
||||
threadId: input.threadId,
|
||||
subscriberId: input.subscriberId,
|
||||
subscriberType: input.subscriberType,
|
||||
notifyOn: input.notifyOn,
|
||||
});
|
||||
publishDomainChanged();
|
||||
return ok(subscription);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.unsubscribeThread, async (_event, payload) => {
|
||||
const input = unsubscribeThreadSchema.parse(payload);
|
||||
const thread = db.getThread(input.threadId, input.companyId);
|
||||
if (!thread) return fail("THREAD_NOT_FOUND", "Thread not found in this company");
|
||||
db.unsubscribe(input.threadId, input.subscriberId);
|
||||
publishDomainChanged();
|
||||
return ok(true);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.listSubscriptions, async (_event, payload) => {
|
||||
const input = listSubscriptionsSchema.parse(payload);
|
||||
const thread = db.getThread(input.threadId, input.companyId);
|
||||
if (!thread) return fail("THREAD_NOT_FOUND", "Thread not found in this company");
|
||||
const subscriptions = db.listSubscriptions(input.threadId);
|
||||
return ok(subscriptions);
|
||||
});
|
||||
|
||||
registerHandle(IPC_CHANNELS.listSubscribedThreads, async (_event, payload) => {
|
||||
const input = listSubscribedThreadsSchema.parse(payload);
|
||||
const threads = db.listSubscribedThreads(input.subscriberId, input.companyId);
|
||||
return ok(threads);
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import log from "electron-log/main.js";
|
||||
import type { AgentApiServer } from "./api-server";
|
||||
import { initializeAgentTokenSigningKey } from "./agent-auth";
|
||||
import { bootstrapApplication } from "./bootstrap-application";
|
||||
import { openConnectorAuthTerminal as openConnectorAuthTerminalImpl, openGenericTerminal as openGenericTerminalImpl } from "./connector-auth-service";
|
||||
import { openGenericTerminal as openGenericTerminalImpl } from "./connector-auth-service";
|
||||
import { getConnectorModelPolicy } from "./connector-model-policy";
|
||||
import type { ConnectorId } from "@shared/types";
|
||||
import type { AppDatabase } from "./database";
|
||||
@@ -145,10 +145,6 @@ export function createMainRuntimeSupport(state: MainRuntimeState) {
|
||||
queueTaskRun: (taskId, trigger, actor) =>
|
||||
requireOrchestrationService().queueTaskRun(taskId, trigger, actor),
|
||||
checkConnector: async (id) => requireConnectorHealthService().checkConnector(id),
|
||||
openConnectorAuthTerminal: async (id) => {
|
||||
const connector = requireDb().getConnector(id);
|
||||
return openConnectorAuthTerminalImpl(id, connector.command);
|
||||
},
|
||||
openGenericTerminal: (input) => openGenericTerminalImpl(input),
|
||||
checkForUpdates: () => requireUpdaterService().checkForUpdates(),
|
||||
getWorkerProcess: () => requireServiceGraph().getWorkerService()?.getWorker() ?? null,
|
||||
@@ -156,7 +152,6 @@ export function createMainRuntimeSupport(state: MainRuntimeState) {
|
||||
initializeAgentTokenSigningKey(requireDb().profileDir);
|
||||
void requireBrowserActionService().resetBrowserManager();
|
||||
},
|
||||
getApiPort: () => state.apiServer?.port ?? 0,
|
||||
notify: (options) => requireIpcSupport().notify(options),
|
||||
listConnectorModels: async (connectorId) => {
|
||||
const policy = getConnectorModelPolicy(requireDb(), connectorId as ConnectorId);
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
const ENV_ALLOWLIST: ReadonlySet<string> = new Set([
|
||||
"PATH",
|
||||
"HOME",
|
||||
"USER",
|
||||
"SHELL",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
"LC_CTYPE",
|
||||
"TERM",
|
||||
"TMPDIR",
|
||||
"DEVELOPER_DIR",
|
||||
"SDKROOT",
|
||||
"NODE_ENV",
|
||||
"GIT_AUTHOR_NAME",
|
||||
"GIT_AUTHOR_EMAIL",
|
||||
"GIT_COMMITTER_NAME",
|
||||
"GIT_COMMITTER_EMAIL",
|
||||
"GIT_SSH_COMMAND",
|
||||
"GIT_ASKPASS",
|
||||
"GIT_TERMINAL_PROMPT",
|
||||
"XDG_CONFIG_HOME",
|
||||
"XDG_DATA_HOME",
|
||||
"XDG_CACHE_HOME",
|
||||
"XDG_RUNTIME_DIR",
|
||||
"DISPLAY",
|
||||
"WAYLAND_DISPLAY",
|
||||
"DBUS_SESSION_BUS_ADDRESS",
|
||||
"APPDATA",
|
||||
"LOCALAPPDATA",
|
||||
"USERPROFILE",
|
||||
"SYSTEMROOT",
|
||||
"COMSPEC",
|
||||
"PROGRAMFILES",
|
||||
"PROGRAMFILES(X86)",
|
||||
"PROGRAMDATA",
|
||||
"PATHEXT",
|
||||
"NODE_EXTRA_CA_CERTS",
|
||||
"SSL_CERT_FILE",
|
||||
"SSL_CERT_DIR",
|
||||
"REQUESTS_CA_BUNDLE",
|
||||
"CURL_CA_BUNDLE",
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
"NO_PROXY",
|
||||
"no_proxy",
|
||||
"DENO_CERT",
|
||||
"CARGO_HTTP_CAINFO",
|
||||
"CARGO_HTTP_PROXY_CAINFO",
|
||||
"CARGO_HTTP_CHECK_REVOKE",
|
||||
"AGENT_COMPANY_AGENT_ID",
|
||||
"AGENT_COMPANY_COMPANY_ID",
|
||||
"AGENT_COMPANY_API_URL",
|
||||
"AGENT_COMPANY_API_KEY",
|
||||
"AGENT_COMPANY_RUN_ID",
|
||||
"AGENT_COMPANY_TASK_ID",
|
||||
"AGENT_COMPANY_WAKE_REASON",
|
||||
"AGENT_COMPANY_WAKE_COMMENT_ID",
|
||||
"AGENT_COMPANY_APPROVAL_ID",
|
||||
"AGENT_COMPANY_APPROVAL_STATUS",
|
||||
"AGENT_COMPANY_WORKSPACE_CWD",
|
||||
"AGENT_COMPANY_WORKSPACE_SOURCE",
|
||||
"AGENT_COMPANY_WORKSPACE_STRATEGY",
|
||||
"AGENT_COMPANY_WORKSPACE_ID",
|
||||
"AGENT_COMPANY_WORKSPACE_REPO_URL",
|
||||
"AGENT_COMPANY_WORKSPACE_REPO_REF",
|
||||
"AGENT_COMPANY_WORKSPACE_BRANCH",
|
||||
"AGENT_COMPANY_WORKSPACE_WORKTREE_PATH",
|
||||
"AGENT_COMPANY_WORKSPACES_JSON",
|
||||
"AGENT_COMPANY_RUNTIME_SERVICE_INTENTS_JSON",
|
||||
"AGENT_COMPANY_RUNTIME_SERVICES_JSON",
|
||||
"AGENT_COMPANY_RUNTIME_PRIMARY_URL",
|
||||
"AGENT_COMPANY_LINKED_ISSUE_IDS",
|
||||
]);
|
||||
|
||||
const FORBIDDEN_RUNTIME_OVERRIDE_KEYS: ReadonlySet<string> = new Set([
|
||||
"PATH",
|
||||
"HOME",
|
||||
"USER",
|
||||
"SHELL",
|
||||
"NODE_OPTIONS",
|
||||
"ELECTRON_RUN_AS_NODE",
|
||||
"DYLD_INSERT_LIBRARIES",
|
||||
"LD_PRELOAD",
|
||||
"PWD",
|
||||
]);
|
||||
|
||||
function normalizeStringEnv(input: NodeJS.ProcessEnv | Record<string, string>): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(input).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
|
||||
);
|
||||
}
|
||||
|
||||
export function stripForbiddenRuntimeOverrides(env: Record<string, string>): Record<string, string> {
|
||||
const sanitized: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (FORBIDDEN_RUNTIME_OVERRIDE_KEYS.has(key)) {
|
||||
continue;
|
||||
}
|
||||
sanitized[key] = value;
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
export function buildSanitizedRuntimeEnv(
|
||||
hostEnv: NodeJS.ProcessEnv | Record<string, string>,
|
||||
runtimeOverrides: Record<string, string>,
|
||||
): Record<string, string> {
|
||||
const normalizedHostEnv = normalizeStringEnv(hostEnv);
|
||||
const sanitized: Record<string, string> = {};
|
||||
|
||||
if (process.platform === "win32") {
|
||||
// On Windows, env var keys are case-insensitive but plain JS objects are not.
|
||||
// Build a case-insensitive lookup so allowlist entries like "PATH" match "Path".
|
||||
const hostKeyMap = new Map<string, string>();
|
||||
for (const key of Object.keys(normalizedHostEnv)) {
|
||||
hostKeyMap.set(key.toUpperCase(), key);
|
||||
}
|
||||
for (const allowedKey of ENV_ALLOWLIST) {
|
||||
const actualKey = hostKeyMap.get(allowedKey.toUpperCase());
|
||||
if (actualKey && normalizedHostEnv[actualKey]) {
|
||||
sanitized[allowedKey] = normalizedHostEnv[actualKey]!;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const key of ENV_ALLOWLIST) {
|
||||
if (key in normalizedHostEnv) {
|
||||
sanitized[key] = normalizedHostEnv[key]!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On Windows, many CLI tools (Claude Code, etc.) use HOME to find config directories.
|
||||
// Windows doesn't set HOME by default — synthesize it from USERPROFILE if missing.
|
||||
if (process.platform === "win32" && !sanitized.HOME && sanitized.USERPROFILE) {
|
||||
sanitized.HOME = sanitized.USERPROFILE;
|
||||
}
|
||||
|
||||
return {
|
||||
...sanitized,
|
||||
...stripForbiddenRuntimeOverrides(runtimeOverrides),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user