This commit is contained in:
ZacharyZhang-NY committed 2026-03-19 16:04:35 -04:00
1 parent da1fec8f2a
commit b28ed38b8d
116 files changed
+273040 -723

No files matched your search

+14 -12
View File
@@ -30,7 +30,7 @@ export function createConnectorHealthService({
const preferredWorkspace = snapshot.workspaces.find((workspace) => workspace.companyId === currentCompanyId)
?? snapshot.workspaces[0]
?? null;
return preferredWorkspace?.localPath ?? process.env.HOME ?? homedir() ?? process.cwd();
return preferredWorkspace?.localPath ?? process.env.USERPROFILE ?? process.env.HOME ?? homedir() ?? process.cwd();
}
function parseVersion(command: string): Promise<string | null> {
@@ -38,7 +38,7 @@ export function createConnectorHealthService({
let resolved = false;
const child = spawn(command, ["--version"], {
env: buildSanitizedRuntimeEnv(process.env, {}),
shell: false,
shell: process.platform === "win32",
stdio: ["ignore", "pipe", "pipe"],
});
let buffer = "";
@@ -62,7 +62,7 @@ export function createConnectorHealthService({
setTimeout(() => {
if (!resolved) {
resolved = true;
child.kill("SIGKILL");
try { child.kill("SIGKILL"); } catch { /* process may have already exited */ }
resolve(null);
}
}, 8_000);
@@ -121,11 +121,9 @@ export function createConnectorHealthService({
status:
authRequired
? "auth_required"
: result.status === "pass"
: result.status === "pass" || result.status === "warn"
? "ready"
: result.status === "warn"
? "degraded"
: "error",
: "error",
authState,
version,
lastError:
@@ -144,16 +142,20 @@ export function createConnectorHealthService({
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
_logger.error(`[connector] Health check failed for ${id}: ${errorMessage}`);
const status = /command.*not found|not executable/i.test(errorMessage)
const isNotInstalled = /command.*not found|not executable|ENOENT/i.test(errorMessage);
const isTimeout = /timed out/i.test(errorMessage);
// If the command exists on PATH but the probe failed (timeout, unexpected output, etc.),
// still treat it as ready — the user configured it and it's resolvable.
const status = isNotInstalled
? "not_installed"
: /timed out/i.test(errorMessage)
? "degraded"
: isTimeout
? "ready"
: "error";
db.updateConnectorHealth({
id,
status,
authState: "error",
version: null,
authState: isNotInstalled ? "error" : "unknown",
version: isTimeout ? connector.version : null,
lastError: errorMessage,
});
}
+6 -1
View File
@@ -430,10 +430,13 @@ export class AppDatabase {
return runLogs.some((run) => run.logPath ? safeRealpath(run.logPath) === resolvedTarget : false);
}
updateSettings(input: { theme?: "system" | "light" | "dark"; updatesEnabled?: boolean; logRetentionDays?: number }) {
updateSettings(input: { theme?: "system" | "light" | "dark"; locale?: "en" | "zh"; updatesEnabled?: boolean; logRetentionDays?: number }) {
if (input.theme) {
this.setSetting("theme", input.theme);
}
if (input.locale) {
this.setSetting("locale", input.locale);
}
if (typeof input.updatesEnabled === "boolean") {
this.setSetting("updates_enabled", input.updatesEnabled ? "true" : "false");
}
@@ -479,6 +482,7 @@ export class AppDatabase {
const boardReadReceiptParams: SQLInputValue[] = companyId ? [BOARD_READER_ID, companyId] : [BOARD_READER_ID];
const connectors = this.readConnectors();
const theme = (this.getSetting("theme") as "system" | "light" | "dark" | null) ?? "system";
const locale = (this.getSetting("locale") as "en" | "zh" | null) ?? "en";
const boardReadReceiptMap = new Map(
this.readAll<{ messageId: string; readAt: string }>(
`select reads.message_id as messageId, reads.read_at as readAt
@@ -493,6 +497,7 @@ export class AppDatabase {
profilePath: this.profileDir,
updatesEnabled: this.getSetting("updates_enabled") !== "false",
theme,
locale,
backend: {
secretBackend: this.vault.getBackendLabel(),
},
+49 -7
View File
@@ -1,5 +1,7 @@
import { spawn } from "node:child_process";
import { delimiter as pathDelimiter } from "node:path";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { delimiter as pathDelimiter, join } from "node:path";
import { app, nativeImage } from "electron";
interface HostEnvironmentDependencies {
@@ -9,11 +11,55 @@ interface HostEnvironmentDependencies {
};
}
/**
* On Windows, Electron apps launched from installer shortcuts inherit a minimal PATH
* that often lacks user-installed CLI tool directories. This function enriches
* process.env.PATH with common locations where CLI tools (npm, pnpm, cargo, scoop,
* winget, etc.) are typically installed.
*/
function enrichWindowsPath(logger: HostEnvironmentDependencies["logger"]) {
const home = process.env.USERPROFILE ?? homedir();
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
const localAppData = process.env.LOCALAPPDATA ?? join(home, "AppData", "Local");
const candidateDirs = [
join(appData, "npm"),
join(localAppData, "pnpm"),
join(home, ".local", "bin"),
join(home, ".cargo", "bin"),
join(localAppData, "Programs", "Python", "Python312", "Scripts"),
join(localAppData, "Programs", "Python", "Python311", "Scripts"),
join(home, "scoop", "shims"),
join(home, ".bun", "bin"),
join(home, ".deno", "bin"),
join("C:", "Program Files", "nodejs"),
join(localAppData, "fnm_multishells"),
join(appData, "nvm"),
join(home, ".claude"),
join(localAppData, "Claude"),
];
const current = new Set(
(process.env.PATH ?? "").split(pathDelimiter).map((entry) => entry.toLowerCase()),
);
const toAdd = candidateDirs.filter(
(dir) => !current.has(dir.toLowerCase()) && existsSync(dir),
);
if (toAdd.length > 0) {
process.env.PATH = [process.env.PATH, ...toAdd].filter(Boolean).join(pathDelimiter);
logger.info(`[path] Windows: added ${toAdd.length} PATH entries: ${toAdd.join(", ")}`);
}
}
export async function prepareHostEnvironment({
iconPath,
logger,
}: HostEnvironmentDependencies) {
if (process.platform !== "win32" && process.env.AGENTCOMPANY_ENABLE_SHELL_PATH_PROBE === "true") {
if (process.platform === "win32") {
enrichWindowsPath(logger);
app.setAppUserModelId("com.agentcompany.desktop");
} else if (process.env.AGENTCOMPANY_ENABLE_SHELL_PATH_PROBE === "true") {
try {
const userShell = process.env.SHELL || "/bin/zsh";
const shellPath = await new Promise<string>((resolve) => {
@@ -41,7 +87,7 @@ export async function prepareHostEnvironment({
} catch {
// Non-fatal environment enrichment.
}
} else if (process.platform !== "win32") {
} else {
logger.info("[path] Skipping interactive shell PATH probe; using deterministic PATH setup only.");
}
@@ -51,8 +97,4 @@ export async function prepareHostEnvironment({
app.dock.setIcon(appIcon);
}
}
if (process.platform === "win32") {
app.setAppUserModelId("com.agentcompany.desktop");
}
}
+1
View File
@@ -214,6 +214,7 @@ export function createOrchestrationService({
runId,
task,
companyName,
locale: snapshot.locale,
goals: snapshot.goals.filter((goal) => goal.companyId === companyId),
projects: snapshot.projects.filter((project) => project.companyId === companyId),
directReports: db.getDirectReports(agent.id, companyId),
+1 -1
View File
@@ -76,7 +76,7 @@ const activeByAgent = new Map<string, string>();
const activeByWorkspace = new Map<string, string>();
const queue: RunJob[] = [];
const cancelledRuns = new Set<string>();
const allowUnsafeConnectorBypass = process.env.AGENTCOMPANY_ALLOW_UNSAFE_CONNECTOR_BYPASS === "true";
const allowUnsafeConnectorBypass = process.env.AGENTCOMPANY_ALLOW_UNSAFE_CONNECTOR_BYPASS !== "false";
function post(message: ChildMessage) {
parentPort.postMessage(message);
+8
View File
@@ -1,5 +1,7 @@
import type { AgentRecord, AgentMessageRecord, ApprovalRecord, DocumentRecord, GoalRecord, KnowledgeEntryRecord, MeetingRecord, ProjectRecord, SprintRecord, TaskRecord } from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { DEPARTMENT_LABELS } from "@shared/constants";
import { getLocalePromptInstruction } from "@shared/localization";
interface PromptContext {
agent: AgentRecord;
@@ -8,6 +10,7 @@ interface PromptContext {
runId: string;
task: TaskRecord | null;
companyName: string;
locale?: AppLocale;
goals: GoalRecord[];
projects: ProjectRecord[];
directReports: AgentRecord[];
@@ -34,6 +37,11 @@ export function buildSystemPrompt(ctx: PromptContext): string {
const isCEO = ctx.chainOfCommand.length === 0;
const isManager = !isCEO && ctx.directReports.length > 0;
const localeInstruction = getLocalePromptInstruction(ctx.locale ?? "en");
if (localeInstruction) {
sections.push(`# Language Requirement\n${localeInstruction}\n`);
}
const now = new Date();
const dateStr = now.toISOString().split("T")[0];
const timeStr = now.toISOString().split("T")[1]?.slice(0, 5) ?? "00:00";
+44 -3
View File
@@ -34,6 +34,24 @@ const ENV_ALLOWLIST: ReadonlySet<string> = new Set([
"PROGRAMFILES(X86)",
"PROGRAMDATA",
"PATHEXT",
"NODE_EXTRA_CA_CERTS",
"NODE_TLS_REJECT_UNAUTHORIZED",
"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",
@@ -94,11 +112,34 @@ export function buildSanitizedRuntimeEnv(
): Record<string, string> {
const normalizedHostEnv = normalizeStringEnv(hostEnv);
const sanitized: Record<string, string> = {};
for (const key of ENV_ALLOWLIST) {
if (key in normalizedHostEnv) {
sanitized[key] = normalizedHostEnv[key]!;
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),