update
This commit is contained in:
1 parent
54bcc77ef7
commit
7a3db27ffa
33 files changed
+5906
-3089
No files matched your search
@@ -1,13 +1,7 @@
|
||||
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
||||
|
||||
/**
|
||||
* Lightweight HMAC-based agent token system.
|
||||
* Tokens are short-lived and scoped to a specific agent + company + run.
|
||||
* No external JWT library needed.
|
||||
*/
|
||||
|
||||
const SIGNING_KEY = randomBytes(32);
|
||||
const TOKEN_TTL_MS = 12 * 60 * 60 * 1000; // 12 hours
|
||||
const TOKEN_TTL_MS = 48 * 60 * 60 * 1000; // 48 hours
|
||||
|
||||
interface TokenPayload {
|
||||
agentId: string;
|
||||
|
||||
+422
-144
File diff suppressed because it is too large.
Load diff
+176
-77
@@ -3,19 +3,10 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import log from "electron-log/main.js";
|
||||
|
||||
/**
|
||||
* Browser Automation Manager — Electron-native edition.
|
||||
*
|
||||
* Uses Electron's own BrowserWindow and persistent sessions instead of
|
||||
* external Playwright, so it works in packaged apps with zero extra deps.
|
||||
*
|
||||
* Each social account gets its own Electron session partition so cookies,
|
||||
* localStorage, and login state are isolated and persisted across app restarts.
|
||||
*/
|
||||
|
||||
export interface BrowserAction {
|
||||
id: string;
|
||||
socialAccountId: string;
|
||||
platform: string;
|
||||
actionType: string;
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
@@ -33,56 +24,116 @@ interface QueuedAction {
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
|
||||
interface PlatformRule {
|
||||
loginUrl: string;
|
||||
authCheckUrl: string;
|
||||
allowedHosts: string[];
|
||||
cookieOrigins: string[];
|
||||
unauthenticatedUrlMarkers: string[];
|
||||
}
|
||||
|
||||
const PLATFORM_RULES: Record<string, PlatformRule> = {
|
||||
twitter: {
|
||||
loginUrl: "https://x.com/i/flow/login",
|
||||
authCheckUrl: "https://x.com/home",
|
||||
allowedHosts: ["x.com", "twitter.com"],
|
||||
cookieOrigins: ["https://x.com", "https://twitter.com"],
|
||||
unauthenticatedUrlMarkers: ["/i/flow/login", "/login"],
|
||||
},
|
||||
linkedin: {
|
||||
loginUrl: "https://www.linkedin.com/login",
|
||||
authCheckUrl: "https://www.linkedin.com/feed/",
|
||||
allowedHosts: ["linkedin.com"],
|
||||
cookieOrigins: ["https://www.linkedin.com"],
|
||||
unauthenticatedUrlMarkers: ["/login", "/checkpoint"],
|
||||
},
|
||||
instagram: {
|
||||
loginUrl: "https://www.instagram.com/accounts/login/",
|
||||
authCheckUrl: "https://www.instagram.com/",
|
||||
allowedHosts: ["instagram.com"],
|
||||
cookieOrigins: ["https://www.instagram.com"],
|
||||
unauthenticatedUrlMarkers: ["/accounts/login"],
|
||||
},
|
||||
facebook: {
|
||||
loginUrl: "https://www.facebook.com/login",
|
||||
authCheckUrl: "https://www.facebook.com/",
|
||||
allowedHosts: ["facebook.com"],
|
||||
cookieOrigins: ["https://www.facebook.com"],
|
||||
unauthenticatedUrlMarkers: ["/login"],
|
||||
},
|
||||
reddit: {
|
||||
loginUrl: "https://www.reddit.com/login",
|
||||
authCheckUrl: "https://www.reddit.com/",
|
||||
allowedHosts: ["reddit.com"],
|
||||
cookieOrigins: ["https://www.reddit.com"],
|
||||
unauthenticatedUrlMarkers: ["/login"],
|
||||
},
|
||||
youtube: {
|
||||
loginUrl: "https://accounts.google.com/signin",
|
||||
authCheckUrl: "https://www.youtube.com/",
|
||||
allowedHosts: ["youtube.com", "google.com"],
|
||||
cookieOrigins: ["https://www.youtube.com", "https://accounts.google.com"],
|
||||
unauthenticatedUrlMarkers: ["/signin", "/ServiceLogin"],
|
||||
},
|
||||
tiktok: {
|
||||
loginUrl: "https://www.tiktok.com/login",
|
||||
authCheckUrl: "https://www.tiktok.com/foryou",
|
||||
allowedHosts: ["tiktok.com"],
|
||||
cookieOrigins: ["https://www.tiktok.com"],
|
||||
unauthenticatedUrlMarkers: ["/login"],
|
||||
},
|
||||
bluesky: {
|
||||
loginUrl: "https://bsky.app",
|
||||
authCheckUrl: "https://bsky.app/",
|
||||
allowedHosts: ["bsky.app"],
|
||||
cookieOrigins: ["https://bsky.app"],
|
||||
unauthenticatedUrlMarkers: ["/login"],
|
||||
},
|
||||
other: {
|
||||
loginUrl: "https://google.com",
|
||||
authCheckUrl: "https://google.com",
|
||||
allowedHosts: ["google.com"],
|
||||
cookieOrigins: ["https://google.com"],
|
||||
unauthenticatedUrlMarkers: ["/login"],
|
||||
},
|
||||
};
|
||||
|
||||
export class BrowserManager {
|
||||
private screenshotDir: string;
|
||||
private queues = new Map<string, QueuedAction[]>();
|
||||
private processing = new Set<string>();
|
||||
/** Track open login windows so we don't open duplicates */
|
||||
private loginWindows = new Map<string, BrowserWindow>();
|
||||
private initializedSessions = new Set<string>();
|
||||
|
||||
constructor(profileDir: string) {
|
||||
this.screenshotDir = join(profileDir, "browser-screenshots");
|
||||
if (!existsSync(this.screenshotDir)) mkdirSync(this.screenshotDir, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a persistent Electron session for a social account.
|
||||
* Using `persist:` prefix ensures data survives app restarts.
|
||||
*/
|
||||
private getSession(socialAccountId: string): Electron.Session {
|
||||
return session.fromPartition(`persist:social-${socialAccountId}`);
|
||||
const partition = `persist:social-${socialAccountId}`;
|
||||
const socialSession = session.fromPartition(partition);
|
||||
if (!this.initializedSessions.has(partition)) {
|
||||
socialSession.setPermissionCheckHandler(() => false);
|
||||
socialSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false));
|
||||
this.initializedSessions.add(partition);
|
||||
}
|
||||
return socialSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a real browser window for manual login.
|
||||
* The window uses a persistent session so cookies are saved.
|
||||
* Returns a promise that resolves when the user closes the window.
|
||||
*/
|
||||
async launchLoginBrowser(
|
||||
socialAccountId: string,
|
||||
platform: string,
|
||||
loginUrl?: string,
|
||||
): Promise<BrowserActionResult> {
|
||||
// Don't open duplicate login windows for the same account
|
||||
const existing = this.loginWindows.get(socialAccountId);
|
||||
if (existing && !existing.isDestroyed()) {
|
||||
existing.focus();
|
||||
return { success: true, summary: "Login window already open", screenshotPath: null, error: null };
|
||||
}
|
||||
|
||||
const defaultUrls: Record<string, string> = {
|
||||
twitter: "https://x.com/i/flow/login",
|
||||
linkedin: "https://www.linkedin.com/login",
|
||||
instagram: "https://www.instagram.com/accounts/login/",
|
||||
facebook: "https://www.facebook.com/login",
|
||||
reddit: "https://www.reddit.com/login",
|
||||
youtube: "https://accounts.google.com/signin",
|
||||
tiktok: "https://www.tiktok.com/login",
|
||||
bluesky: "https://bsky.app",
|
||||
other: "https://google.com",
|
||||
};
|
||||
|
||||
const url = loginUrl || defaultUrls[platform] || defaultUrls.other;
|
||||
const rules = this.getPlatformRules(platform);
|
||||
const url = loginUrl || rules.loginUrl;
|
||||
const ses = this.getSession(socialAccountId);
|
||||
|
||||
log.info(`[browser] Opening login window for ${platform} → ${url}`);
|
||||
@@ -93,7 +144,6 @@ export class BrowserManager {
|
||||
title: `Login to ${platform} — AgentCompany`,
|
||||
webPreferences: {
|
||||
session: ses,
|
||||
// Standard web page — no node integration, no preload
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
@@ -102,43 +152,32 @@ export class BrowserManager {
|
||||
|
||||
this.loginWindows.set(socialAccountId, win);
|
||||
|
||||
// Remove menu bar for a cleaner look
|
||||
win.setMenuBarVisibility(false);
|
||||
|
||||
// Load the login page
|
||||
try {
|
||||
await win.loadURL(url);
|
||||
} catch (err) {
|
||||
log.error(`[browser] Failed to load ${url}: ${err}`);
|
||||
// Don't fail — the page might redirect or have an expected error
|
||||
}
|
||||
|
||||
// Wait for the user to close the window
|
||||
return new Promise<BrowserActionResult>((resolve) => {
|
||||
win.on("closed", () => {
|
||||
this.loginWindows.delete(socialAccountId);
|
||||
log.info(`[browser] Login window closed for ${platform} — session saved`);
|
||||
resolve({
|
||||
success: true,
|
||||
summary: `Login session saved for ${platform}`,
|
||||
screenshotPath: null,
|
||||
error: null,
|
||||
void this.verifyLoginSession(socialAccountId, platform).then((verified) => {
|
||||
log.info(`[browser] Login window closed for ${platform} — verified=${verified}`);
|
||||
resolve({
|
||||
success: verified,
|
||||
summary: verified
|
||||
? `Authenticated session detected for ${platform}`
|
||||
: `No authenticated ${platform} session detected`,
|
||||
screenshotPath: null,
|
||||
error: verified ? null : `Authentication for ${platform} was not completed.`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if browser automation is available.
|
||||
* Always true — we use Electron's own Chromium.
|
||||
*/
|
||||
async checkPlaywright(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a browser action for execution.
|
||||
*/
|
||||
async executeAction(action: BrowserAction): Promise<BrowserActionResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const queue = this.queues.get(action.socialAccountId) ?? [];
|
||||
@@ -166,17 +205,14 @@ export class BrowserManager {
|
||||
this.processing.delete(socialAccountId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single browser action using an Electron BrowserWindow.
|
||||
*/
|
||||
private async runAction(action: BrowserAction): Promise<BrowserActionResult> {
|
||||
const ses = this.getSession(action.socialAccountId);
|
||||
const payload = action.payload;
|
||||
const screenshotPath = join(this.screenshotDir, `${action.id}.png`);
|
||||
const platform = action.platform;
|
||||
|
||||
log.info(`[browser] Executing ${action.actionType} for account ${action.socialAccountId}`);
|
||||
log.info(`[browser] Executing ${action.actionType} for ${platform}/${action.socialAccountId}`);
|
||||
|
||||
// Create a visible browser window for the action
|
||||
const win = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 860,
|
||||
@@ -197,6 +233,7 @@ export class BrowserManager {
|
||||
switch (action.actionType) {
|
||||
case "navigate": {
|
||||
const url = String(payload.url || "https://google.com");
|
||||
this.assertAllowedUrl(platform, url, "navigate");
|
||||
await win.loadURL(url);
|
||||
await this.waitForLoad(win);
|
||||
const title = await win.webContents.executeJavaScript("document.title");
|
||||
@@ -205,7 +242,6 @@ export class BrowserManager {
|
||||
}
|
||||
|
||||
case "post": {
|
||||
const platform = String(payload.platform || "twitter");
|
||||
const content = String(payload.content || "");
|
||||
const postUrl = this.getComposeUrl(platform);
|
||||
await win.loadURL(postUrl);
|
||||
@@ -213,7 +249,6 @@ export class BrowserManager {
|
||||
await this.delay(2000);
|
||||
|
||||
if (platform === "twitter") {
|
||||
// Type into tweet composer
|
||||
await win.webContents.executeJavaScript(`
|
||||
(async () => {
|
||||
const editor = document.querySelector('[data-testid="tweetTextarea_0"], [role="textbox"]');
|
||||
@@ -224,7 +259,6 @@ export class BrowserManager {
|
||||
})()
|
||||
`);
|
||||
await this.delay(1000);
|
||||
// Click post button
|
||||
await win.webContents.executeJavaScript(`
|
||||
(() => {
|
||||
const btn = document.querySelector('[data-testid="tweetButton"], [data-testid="tweetButtonInline"]');
|
||||
@@ -264,6 +298,7 @@ export class BrowserManager {
|
||||
|
||||
case "reply": {
|
||||
const targetUrl = String(payload.targetUrl || "");
|
||||
this.assertAllowedUrl(platform, targetUrl, "reply");
|
||||
const replyContent = String(payload.content || "");
|
||||
await win.loadURL(targetUrl);
|
||||
await this.waitForLoad(win);
|
||||
@@ -292,6 +327,7 @@ export class BrowserManager {
|
||||
|
||||
case "like": {
|
||||
const targetUrl = String(payload.targetUrl || "");
|
||||
this.assertAllowedUrl(platform, targetUrl, "like");
|
||||
await win.loadURL(targetUrl);
|
||||
await this.waitForLoad(win);
|
||||
await this.delay(2000);
|
||||
@@ -308,6 +344,7 @@ export class BrowserManager {
|
||||
|
||||
case "follow": {
|
||||
const targetUrl = String(payload.targetUrl || payload.profileUrl || "");
|
||||
this.assertAllowedUrl(platform, targetUrl, "follow");
|
||||
await win.loadURL(targetUrl);
|
||||
await this.waitForLoad(win);
|
||||
await this.delay(2000);
|
||||
@@ -323,14 +360,13 @@ export class BrowserManager {
|
||||
}
|
||||
|
||||
case "browse_feed": {
|
||||
const platform = String(payload.platform || "twitter");
|
||||
const feedUrl = String(payload.url || this.getFeedUrl(platform));
|
||||
this.assertAllowedUrl(platform, feedUrl, "browse_feed");
|
||||
const maxPosts = Number(payload.maxPosts || 10);
|
||||
await win.loadURL(feedUrl);
|
||||
await this.waitForLoad(win);
|
||||
await this.delay(3000);
|
||||
|
||||
// Scroll to load content
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await win.webContents.executeJavaScript("window.scrollBy(0, window.innerHeight)");
|
||||
await this.delay(1500);
|
||||
@@ -348,8 +384,8 @@ export class BrowserManager {
|
||||
|
||||
case "search": {
|
||||
const query = String(payload.query || "");
|
||||
const platform = String(payload.platform || "twitter");
|
||||
const searchUrl = this.getSearchUrl(platform, query);
|
||||
this.assertAllowedUrl(platform, searchUrl, "search");
|
||||
await win.loadURL(searchUrl);
|
||||
await this.waitForLoad(win);
|
||||
await this.delay(3000);
|
||||
@@ -367,6 +403,7 @@ export class BrowserManager {
|
||||
case "screenshot": {
|
||||
const url = String(payload.url || "about:blank");
|
||||
if (url !== "about:blank") {
|
||||
this.assertAllowedUrl(platform, url, "screenshot");
|
||||
await win.loadURL(url);
|
||||
await this.waitForLoad(win);
|
||||
await this.delay(2000);
|
||||
@@ -377,10 +414,9 @@ export class BrowserManager {
|
||||
}
|
||||
|
||||
default:
|
||||
summary = `Unknown action type: ${action.actionType}`;
|
||||
throw new Error(`Unsupported browser action type: ${action.actionType}`);
|
||||
}
|
||||
|
||||
// Capture screenshot
|
||||
try {
|
||||
const image: NativeImage = await win.webContents.capturePage();
|
||||
if (!image.isEmpty()) {
|
||||
@@ -390,7 +426,6 @@ export class BrowserManager {
|
||||
log.warn(`[browser] Screenshot failed: ${err}`);
|
||||
}
|
||||
|
||||
// Close the action window after a brief pause so user can see result
|
||||
await this.delay(1000);
|
||||
if (!win.isDestroyed()) win.close();
|
||||
|
||||
@@ -401,7 +436,6 @@ export class BrowserManager {
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
// Try to capture error screenshot
|
||||
try {
|
||||
const image: NativeImage = await win.webContents.capturePage();
|
||||
if (!image.isEmpty()) writeFileSync(screenshotPath, image.toPNG());
|
||||
@@ -420,8 +454,6 @@ export class BrowserManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -440,6 +472,69 @@ export class BrowserManager {
|
||||
});
|
||||
}
|
||||
|
||||
private getPlatformRules(platform: string): PlatformRule {
|
||||
return PLATFORM_RULES[platform] ?? PLATFORM_RULES.other;
|
||||
}
|
||||
|
||||
private assertAllowedUrl(platform: string, rawUrl: string, actionType: string) {
|
||||
const allowedHosts = this.getPlatformRules(platform).allowedHosts;
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(rawUrl);
|
||||
} catch {
|
||||
throw new Error(`Invalid URL for ${actionType}: ${rawUrl}`);
|
||||
}
|
||||
const host = url.hostname.toLowerCase();
|
||||
const isAllowed = allowedHosts.some((allowedHost) => host === allowedHost || host.endsWith(`.${allowedHost}`));
|
||||
if (!isAllowed) {
|
||||
throw new Error(`Navigation blocked for ${actionType}. ${host} is outside the allowed ${platform} domains.`);
|
||||
}
|
||||
}
|
||||
|
||||
private async verifyLoginSession(socialAccountId: string, platform: string): Promise<boolean> {
|
||||
const ses = this.getSession(socialAccountId);
|
||||
const rules = this.getPlatformRules(platform);
|
||||
const cookieSets = await Promise.all(
|
||||
rules.cookieOrigins.map(async (origin) => {
|
||||
try {
|
||||
return await ses.cookies.get({ url: origin });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
);
|
||||
const hasCookies = cookieSets.some((cookies) => cookies.length > 0);
|
||||
if (!hasCookies) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const verificationWindow = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
session: ses,
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await verificationWindow.loadURL(rules.authCheckUrl);
|
||||
await this.waitForLoad(verificationWindow, 10000);
|
||||
const finalUrl = verificationWindow.webContents.getURL();
|
||||
const finalUrlLower = finalUrl.toLowerCase();
|
||||
const bouncedToLogin = rules.unauthenticatedUrlMarkers.some((marker) => finalUrlLower.includes(marker.toLowerCase()));
|
||||
return !bouncedToLogin;
|
||||
} catch (error) {
|
||||
log.warn(`[browser] Login verification failed for ${platform}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return false;
|
||||
} finally {
|
||||
if (!verificationWindow.isDestroyed()) {
|
||||
verificationWindow.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getComposeUrl(platform: string): string {
|
||||
const urls: Record<string, string> = {
|
||||
twitter: "https://x.com/compose/post",
|
||||
@@ -448,8 +543,10 @@ export class BrowserManager {
|
||||
facebook: "https://www.facebook.com/",
|
||||
reddit: "https://www.reddit.com/submit",
|
||||
bluesky: "https://bsky.app/",
|
||||
youtube: "https://www.youtube.com/",
|
||||
tiktok: "https://www.tiktok.com/",
|
||||
};
|
||||
return urls[platform] || "https://google.com";
|
||||
return urls[platform] || this.getPlatformRules(platform).loginUrl;
|
||||
}
|
||||
|
||||
private getFeedUrl(platform: string): string {
|
||||
@@ -457,10 +554,13 @@ export class BrowserManager {
|
||||
twitter: "https://x.com/home",
|
||||
linkedin: "https://www.linkedin.com/feed/",
|
||||
instagram: "https://www.instagram.com/",
|
||||
facebook: "https://www.facebook.com/",
|
||||
reddit: "https://www.reddit.com/",
|
||||
bluesky: "https://bsky.app/",
|
||||
youtube: "https://www.youtube.com/",
|
||||
tiktok: "https://www.tiktok.com/",
|
||||
};
|
||||
return urls[platform] || "https://google.com";
|
||||
return urls[platform] || this.getPlatformRules(platform).loginUrl;
|
||||
}
|
||||
|
||||
private getSearchUrl(platform: string, query: string): string {
|
||||
@@ -474,7 +574,6 @@ export class BrowserManager {
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
// Close any open login windows
|
||||
for (const [, win] of this.loginWindows) {
|
||||
if (!win.isDestroyed()) win.close();
|
||||
}
|
||||
|
||||
@@ -263,10 +263,6 @@ const CONNECTORS: ConnectorDefinition[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export function listConnectorDefinitions(): ConnectorDefinition[] {
|
||||
return CONNECTORS;
|
||||
}
|
||||
|
||||
export function getConnectorDefinition(id: ConnectorId): ConnectorDefinition {
|
||||
const definition = CONNECTORS.find((entry) => entry.id === id);
|
||||
if (!definition) {
|
||||
|
||||
+674
-268
File diff suppressed because it is too large.
Load diff
+1861
-764
File diff suppressed because it is too large.
Load diff
@@ -2,37 +2,19 @@ import { runningProcesses } from "@agentcompany/adapter-utils/server-utils";
|
||||
import { getConnectorDefinition } from "@main/connectors";
|
||||
import type { ConnectorId, RunStatus } from "@shared/types";
|
||||
|
||||
/**
|
||||
* Environment variables allowed to propagate from the host process to connector
|
||||
* child processes. Any env var NOT in this list is stripped. Secret bindings
|
||||
* resolved from the vault are always injected regardless of this allowlist.
|
||||
*/
|
||||
const ENV_ALLOWLIST: ReadonlySet<string> = new Set([
|
||||
// PATH is required for command resolution
|
||||
"PATH",
|
||||
// Basic locale / terminal
|
||||
"HOME", "USER", "SHELL", "LANG", "LC_ALL", "LC_CTYPE", "TERM", "TMPDIR",
|
||||
// macOS-specific
|
||||
"DEVELOPER_DIR", "SDKROOT",
|
||||
// Node
|
||||
"NODE_ENV", "NODE_OPTIONS",
|
||||
// Git
|
||||
"GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL",
|
||||
"GIT_SSH_COMMAND", "GIT_ASKPASS", "GIT_TERMINAL_PROMPT",
|
||||
// XDG
|
||||
"XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_RUNTIME_DIR",
|
||||
// Platform / display (Linux)
|
||||
"DISPLAY", "WAYLAND_DISPLAY", "DBUS_SESSION_BUS_ADDRESS",
|
||||
// Windows
|
||||
"APPDATA", "LOCALAPPDATA", "USERPROFILE", "SYSTEMROOT", "COMSPEC", "PROGRAMFILES",
|
||||
// AgentCompany injected
|
||||
"AGENT_COMPANY_API_URL", "AGENT_COMPANY_API_KEY", "AGENT_COMPANY_RUN_ID",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Build a sanitized environment for a connector child process.
|
||||
* Only allowlisted host vars + explicitly resolved secret bindings are included.
|
||||
*/
|
||||
function buildSanitizedEnv(
|
||||
hostEnv: Record<string, string>,
|
||||
secretBindings: Record<string, string>,
|
||||
@@ -43,25 +25,18 @@ function buildSanitizedEnv(
|
||||
result[key] = hostEnv[key];
|
||||
}
|
||||
}
|
||||
// Secret bindings always override — they are resolved from the vault
|
||||
Object.assign(result, secretBindings);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a log redaction function that masks any secret values present in the env.
|
||||
* Only values that look like secrets (length >= 8, not PATH-like) are redacted.
|
||||
*/
|
||||
function buildRedactor(env: Record<string, string>): (text: string) => string {
|
||||
const sensitivePatterns: string[] = [];
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
// Skip short values, PATH, HOME, and other system vars unlikely to be secrets
|
||||
if (value.length < 8) continue;
|
||||
if (["PATH", "HOME", "USER", "SHELL", "LANG", "TMPDIR", "TERM"].includes(key)) continue;
|
||||
sensitivePatterns.push(value);
|
||||
}
|
||||
if (sensitivePatterns.length === 0) return (text) => text;
|
||||
// Sort by length descending so longer patterns are replaced first
|
||||
sensitivePatterns.sort((a, b) => b.length - a.length);
|
||||
return (text: string) => {
|
||||
let result = text;
|
||||
@@ -85,14 +60,13 @@ interface RunJob {
|
||||
connectorId: ConnectorId;
|
||||
workspaceId: string | null;
|
||||
workspacePath: string | null;
|
||||
workspaceMode: "exclusive" | "shared" | "none";
|
||||
command: string;
|
||||
model: string | null;
|
||||
thinkingEffort: string | null;
|
||||
env: Record<string, string>;
|
||||
sessionParams: Record<string, unknown> | null;
|
||||
/** Rich system prompt with API instructions for autonomous operation */
|
||||
systemPrompt: string | null;
|
||||
/** Heartbeat trigger that caused this run */
|
||||
trigger: string | null;
|
||||
}
|
||||
|
||||
@@ -134,6 +108,9 @@ function post(message: ChildMessage) {
|
||||
}
|
||||
|
||||
function workspaceKey(job: RunJob) {
|
||||
if (job.workspaceMode !== "exclusive") {
|
||||
return null;
|
||||
}
|
||||
return job.workspaceId ?? job.workspacePath ?? `task:${job.taskId}`;
|
||||
}
|
||||
|
||||
@@ -152,7 +129,8 @@ async function maybeStartQueuedRuns() {
|
||||
const readyJobs = [...queue];
|
||||
for (const job of readyJobs) {
|
||||
if (activeByAgent.has(job.agentId)) continue;
|
||||
if (activeByWorkspace.has(workspaceKey(job))) continue;
|
||||
const lockKey = workspaceKey(job);
|
||||
if (lockKey && activeByWorkspace.has(lockKey)) continue;
|
||||
const index = queue.findIndex((entry) => entry.runId === job.runId);
|
||||
if (index >= 0) queue.splice(index, 1);
|
||||
void startRun(job);
|
||||
@@ -163,7 +141,9 @@ async function startRun(job: RunJob) {
|
||||
const definition = getConnectorDefinition(job.connectorId);
|
||||
const lockKey = workspaceKey(job);
|
||||
activeByAgent.set(job.agentId, job.runId);
|
||||
activeByWorkspace.set(lockKey, job.runId);
|
||||
if (lockKey) {
|
||||
activeByWorkspace.set(lockKey, job.runId);
|
||||
}
|
||||
post({
|
||||
type: "run-status",
|
||||
runId: job.runId,
|
||||
@@ -171,7 +151,6 @@ async function startRun(job: RunJob) {
|
||||
message: `Running with ${definition.label}.`,
|
||||
});
|
||||
|
||||
// Build sanitized env: only allowlisted host vars + resolved secret bindings
|
||||
const sanitizedEnv = buildSanitizedEnv(
|
||||
Object.fromEntries(
|
||||
Object.entries(process.env).filter((e): e is [string, string] => typeof e[1] === "string"),
|
||||
@@ -179,7 +158,6 @@ async function startRun(job: RunJob) {
|
||||
job.env,
|
||||
);
|
||||
|
||||
// Build log redactor from secret bindings to prevent accidental secret leakage
|
||||
const redact = buildRedactor(job.env);
|
||||
|
||||
try {
|
||||
@@ -205,13 +183,9 @@ async function startRun(job: RunJob) {
|
||||
cwd: job.workspacePath ?? process.cwd(),
|
||||
model: job.model,
|
||||
env: sanitizedEnv,
|
||||
// Claude: thinking effort
|
||||
effort: job.connectorId === "claude_local" ? (job.thinkingEffort ?? "") : "",
|
||||
// Codex: reasoning effort
|
||||
modelReasoningEffort: job.connectorId === "codex_local" ? (job.thinkingEffort ?? "") : "",
|
||||
// Claude: run without permission prompts — required for headless operation
|
||||
dangerouslySkipPermissions: true,
|
||||
// Codex: bypass sandbox and approval prompts, skip git trust check
|
||||
dangerouslyBypassApprovalsAndSandbox: true,
|
||||
extraArgs: job.connectorId === "codex_local" ? ["--skip-git-repo-check"] : [],
|
||||
},
|
||||
@@ -276,7 +250,9 @@ async function startRun(job: RunJob) {
|
||||
} finally {
|
||||
cancelledRuns.delete(job.runId);
|
||||
activeByAgent.delete(job.agentId);
|
||||
activeByWorkspace.delete(lockKey);
|
||||
if (lockKey) {
|
||||
activeByWorkspace.delete(lockKey);
|
||||
}
|
||||
void maybeStartQueuedRuns();
|
||||
}
|
||||
}
|
||||
|
||||
+573
-317
File diff suppressed because it is too large.
Load diff
+1
-175
@@ -1,10 +1,5 @@
|
||||
import type { CompanyTemplate, DepartmentId } from "@shared/types";
|
||||
import type { CompanyTemplate } from "@shared/types";
|
||||
|
||||
/**
|
||||
* Pre-built company templates for one-click company setup.
|
||||
* Each template defines a complete organizational structure with
|
||||
* agents, goals, and role definitions.
|
||||
*/
|
||||
export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
{
|
||||
id: "software-startup",
|
||||
@@ -313,10 +308,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
],
|
||||
},
|
||||
|
||||
// ── Fortune 500 Enterprise ────────────────────────────────────────────
|
||||
// A truly realistic, fully-staffed company with 37 agents across 12
|
||||
// departments. Modeled after a mature tech company or Fortune 500 org
|
||||
// with every function needed to operate at scale.
|
||||
{
|
||||
id: "fortune500-enterprise",
|
||||
name: "Fortune 500 Enterprise",
|
||||
@@ -337,11 +328,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
{ id: "research", name: "Research & Innovation", description: "Applied research, competitive intelligence, prototyping, and publication of thought-leadership papers." },
|
||||
],
|
||||
agents: [
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// EXECUTIVE SUITE — indices 0-4
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
// index 0
|
||||
name: "Victoria Sterling",
|
||||
role: "CEO",
|
||||
title: "Chief Executive Officer",
|
||||
@@ -352,7 +339,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 250,
|
||||
},
|
||||
{
|
||||
// index 1
|
||||
name: "James Whitfield",
|
||||
role: "COO",
|
||||
title: "Chief Operating Officer",
|
||||
@@ -363,7 +349,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 220,
|
||||
},
|
||||
{
|
||||
// index 2
|
||||
name: "Marcus Chen",
|
||||
role: "CTO",
|
||||
title: "Chief Technology Officer",
|
||||
@@ -374,7 +359,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 250,
|
||||
},
|
||||
{
|
||||
// index 3
|
||||
name: "Priya Sharma",
|
||||
role: "CFO",
|
||||
title: "Chief Financial Officer",
|
||||
@@ -385,7 +369,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 200,
|
||||
},
|
||||
{
|
||||
// index 4
|
||||
name: "Nadira Osei-Mensah",
|
||||
role: "CHRO",
|
||||
title: "Chief Human Resources Officer",
|
||||
@@ -396,11 +379,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 180,
|
||||
},
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// ENGINEERING — indices 5-12
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
// index 5
|
||||
name: "Elena Rodriguez",
|
||||
role: "VP Engineering",
|
||||
title: "Vice President of Engineering",
|
||||
@@ -411,7 +390,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 220,
|
||||
},
|
||||
{
|
||||
// index 6
|
||||
name: "Tomasz Kowalski",
|
||||
role: "Principal Engineer",
|
||||
title: "Principal Software Engineer",
|
||||
@@ -422,7 +400,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 280,
|
||||
},
|
||||
{
|
||||
// index 7
|
||||
name: "Kai Nakamura",
|
||||
role: "Senior Backend Engineer",
|
||||
title: "Senior Backend Engineer",
|
||||
@@ -433,7 +410,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 250,
|
||||
},
|
||||
{
|
||||
// index 8
|
||||
name: "Sofia Andersson",
|
||||
role: "Senior Frontend Engineer",
|
||||
title: "Senior Frontend Engineer",
|
||||
@@ -444,7 +420,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 250,
|
||||
},
|
||||
{
|
||||
// index 9
|
||||
name: "Omar Hassan",
|
||||
role: "Platform / DevOps Engineer",
|
||||
title: "Senior Platform Engineer",
|
||||
@@ -455,7 +430,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 230,
|
||||
},
|
||||
{
|
||||
// index 10
|
||||
name: "Yuki Tanaka",
|
||||
role: "QA Lead",
|
||||
title: "Quality Assurance Lead",
|
||||
@@ -466,7 +440,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 200,
|
||||
},
|
||||
{
|
||||
// index 11
|
||||
name: "Amina Diallo",
|
||||
role: "Security Engineer",
|
||||
title: "Application Security Engineer",
|
||||
@@ -477,7 +450,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 230,
|
||||
},
|
||||
{
|
||||
// index 12
|
||||
name: "Ravi Krishnamurthy",
|
||||
role: "Data Engineer",
|
||||
title: "Senior Data Engineer",
|
||||
@@ -488,11 +460,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 230,
|
||||
},
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// PRODUCT & DESIGN — indices 13-16
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
// index 13
|
||||
name: "Amara Okafor",
|
||||
role: "VP Product",
|
||||
title: "Vice President of Product",
|
||||
@@ -503,7 +471,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 200,
|
||||
},
|
||||
{
|
||||
// index 14
|
||||
name: "Daniel Okonkwo",
|
||||
role: "Senior Product Manager",
|
||||
title: "Senior Product Manager",
|
||||
@@ -514,7 +481,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 180,
|
||||
},
|
||||
{
|
||||
// index 15
|
||||
name: "Ingrid Johansson",
|
||||
role: "UX Director",
|
||||
title: "Director of User Experience",
|
||||
@@ -525,7 +491,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 180,
|
||||
},
|
||||
{
|
||||
// index 16
|
||||
name: "Leo Moretti",
|
||||
role: "UI Designer",
|
||||
title: "Senior UI Designer",
|
||||
@@ -536,11 +501,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 160,
|
||||
},
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// MARKETING — indices 17-20
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
// index 17
|
||||
name: "Jasmine Delacroix",
|
||||
role: "CMO",
|
||||
title: "Chief Marketing Officer",
|
||||
@@ -551,7 +512,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 200,
|
||||
},
|
||||
{
|
||||
// index 18
|
||||
name: "Noah Park",
|
||||
role: "Content Director",
|
||||
title: "Director of Content",
|
||||
@@ -562,7 +522,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 180,
|
||||
},
|
||||
{
|
||||
// index 19
|
||||
name: "Zara Ahmed",
|
||||
role: "Growth Marketing Manager",
|
||||
title: "Growth Marketing Manager",
|
||||
@@ -573,7 +532,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 170,
|
||||
},
|
||||
{
|
||||
// index 20
|
||||
name: "Clara Dubois",
|
||||
role: "Brand & PR Manager",
|
||||
title: "Brand & Public Relations Manager",
|
||||
@@ -584,11 +542,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 150,
|
||||
},
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// SALES — indices 21-23
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
// index 21
|
||||
name: "Daniel Brooks",
|
||||
role: "VP Sales",
|
||||
title: "Vice President of Sales",
|
||||
@@ -599,7 +553,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 200,
|
||||
},
|
||||
{
|
||||
// index 22
|
||||
name: "Grace Kimura",
|
||||
role: "Enterprise Account Executive",
|
||||
title: "Enterprise Account Executive",
|
||||
@@ -610,7 +563,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 180,
|
||||
},
|
||||
{
|
||||
// index 23
|
||||
name: "Marcus Adebayo",
|
||||
role: "SDR Manager",
|
||||
title: "Sales Development Representative Manager",
|
||||
@@ -621,11 +573,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 150,
|
||||
},
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// HR / PEOPLE — indices 24-26
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
// index 24
|
||||
name: "Olivia Nguyen",
|
||||
role: "VP People",
|
||||
title: "Vice President of People",
|
||||
@@ -636,7 +584,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 170,
|
||||
},
|
||||
{
|
||||
// index 25
|
||||
name: "Farid Al-Rashidi",
|
||||
role: "Recruiting Lead",
|
||||
title: "Head of Talent Acquisition",
|
||||
@@ -647,7 +594,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 140,
|
||||
},
|
||||
{
|
||||
// index 26
|
||||
name: "Hannah Johansson",
|
||||
role: "People Operations Manager",
|
||||
title: "People Operations Manager",
|
||||
@@ -658,11 +604,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 120,
|
||||
},
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// FINANCE & LEGAL — indices 27-29
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
// index 27
|
||||
name: "Richard Okonkwo",
|
||||
role: "VP Finance",
|
||||
title: "Vice President of Finance",
|
||||
@@ -673,7 +615,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 180,
|
||||
},
|
||||
{
|
||||
// index 28
|
||||
name: "Mei-Ling Wu",
|
||||
role: "Financial Controller",
|
||||
title: "Financial Controller",
|
||||
@@ -684,7 +625,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 140,
|
||||
},
|
||||
{
|
||||
// index 29
|
||||
name: "Alejandro Vega",
|
||||
role: "General Counsel",
|
||||
title: "General Counsel",
|
||||
@@ -695,11 +635,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 200,
|
||||
},
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// CUSTOMER SUCCESS — indices 30-32
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
// index 30
|
||||
name: "Aisha Williams",
|
||||
role: "VP Customer Success",
|
||||
title: "Vice President of Customer Success",
|
||||
@@ -710,7 +646,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 180,
|
||||
},
|
||||
{
|
||||
// index 31
|
||||
name: "Sergei Volkov",
|
||||
role: "Senior Support Engineer",
|
||||
title: "Senior Technical Support Engineer",
|
||||
@@ -721,7 +656,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 160,
|
||||
},
|
||||
{
|
||||
// index 32
|
||||
name: "Camila Reyes",
|
||||
role: "Customer Success Manager",
|
||||
title: "Customer Success Manager",
|
||||
@@ -732,11 +666,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 140,
|
||||
},
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// OPERATIONS — indices 33-34
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
// index 33
|
||||
name: "Kenji Watanabe",
|
||||
role: "Director of Operations",
|
||||
title: "Director of Operations",
|
||||
@@ -747,7 +677,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 160,
|
||||
},
|
||||
{
|
||||
// index 34
|
||||
name: "Laura Petrov",
|
||||
role: "Program Manager",
|
||||
title: "Senior Program Manager",
|
||||
@@ -758,11 +687,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 150,
|
||||
},
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// RESEARCH — indices 35-36
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
// index 35
|
||||
name: "Dr. Lena Hoffmann",
|
||||
role: "Director of Research",
|
||||
title: "Director of Research & Innovation",
|
||||
@@ -773,7 +698,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 200,
|
||||
},
|
||||
{
|
||||
// index 36
|
||||
name: "Dr. Aarav Mehta",
|
||||
role: "Research Scientist",
|
||||
title: "Research Scientist",
|
||||
@@ -828,7 +752,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
],
|
||||
},
|
||||
|
||||
// ── E-Commerce Company ──────────────────────────────────────────────
|
||||
{
|
||||
id: "ecommerce-company",
|
||||
name: "E-Commerce Business",
|
||||
@@ -922,7 +845,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
],
|
||||
},
|
||||
|
||||
// ── Consulting Firm ──────────────────────────────────────────────────
|
||||
{
|
||||
id: "consulting-firm",
|
||||
name: "Consulting Firm",
|
||||
@@ -992,9 +914,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
],
|
||||
},
|
||||
|
||||
// ── AI Research Company ───────────────────────────────────────────────
|
||||
// Modeled after organizations like OpenAI, Anthropic, and DeepMind.
|
||||
// Research-first with strong engineering to ship models into production.
|
||||
{
|
||||
id: "ai-research-company",
|
||||
name: "AI Research Company",
|
||||
@@ -1013,9 +932,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
{ id: "legal", name: "Legal & Policy", description: "IP strategy, regulatory affairs, AI governance, data licensing, and compliance." },
|
||||
],
|
||||
agents: [
|
||||
// ── Executive (0-1) ──
|
||||
{
|
||||
// index 0
|
||||
name: "Dr. Julian Crane",
|
||||
role: "CEO",
|
||||
title: "Chief Executive Officer",
|
||||
@@ -1026,7 +943,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 250,
|
||||
},
|
||||
{
|
||||
// index 1
|
||||
name: "Dr. Mei-Xing Zhou",
|
||||
role: "Chief Scientist",
|
||||
title: "Chief Scientist",
|
||||
@@ -1036,10 +952,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "claude_local",
|
||||
budgetMonthlyUsd: 280,
|
||||
},
|
||||
|
||||
// ── Research (2-6) ──
|
||||
{
|
||||
// index 2
|
||||
name: "Dr. Sasha Petrov",
|
||||
role: "Research Lead — Pre-training",
|
||||
title: "Senior Research Scientist — Pre-training",
|
||||
@@ -1050,7 +963,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 260,
|
||||
},
|
||||
{
|
||||
// index 3
|
||||
name: "Dr. Nneka Adeyemi",
|
||||
role: "Research Lead — Post-training",
|
||||
title: "Senior Research Scientist — RLHF & Fine-tuning",
|
||||
@@ -1061,7 +973,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 260,
|
||||
},
|
||||
{
|
||||
// index 4
|
||||
name: "Dr. Hiroshi Takeda",
|
||||
role: "Research Scientist — Reasoning",
|
||||
title: "Research Scientist — Reasoning & Agents",
|
||||
@@ -1072,7 +983,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 240,
|
||||
},
|
||||
{
|
||||
// index 5
|
||||
name: "Dr. Amara Osei",
|
||||
role: "Research Scientist — Safety",
|
||||
title: "Research Scientist — AI Safety",
|
||||
@@ -1083,7 +993,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 240,
|
||||
},
|
||||
{
|
||||
// index 6
|
||||
name: "Lila Fernandez",
|
||||
role: "Research Engineer",
|
||||
title: "Senior Research Engineer",
|
||||
@@ -1093,10 +1002,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "codex_local",
|
||||
budgetMonthlyUsd: 250,
|
||||
},
|
||||
|
||||
// ── Engineering (7-10) ──
|
||||
{
|
||||
// index 7
|
||||
name: "Rajiv Sundaram",
|
||||
role: "VP Engineering",
|
||||
title: "Vice President of Engineering",
|
||||
@@ -1107,7 +1013,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 220,
|
||||
},
|
||||
{
|
||||
// index 8
|
||||
name: "Christine Bergstrom",
|
||||
role: "ML Infrastructure Engineer",
|
||||
title: "Senior ML Infrastructure Engineer",
|
||||
@@ -1118,7 +1023,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 250,
|
||||
},
|
||||
{
|
||||
// index 9
|
||||
name: "Andre Williams",
|
||||
role: "Backend Engineer",
|
||||
title: "Senior Backend Engineer",
|
||||
@@ -1129,7 +1033,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 230,
|
||||
},
|
||||
{
|
||||
// index 10
|
||||
name: "Yuna Kim",
|
||||
role: "Frontend Engineer",
|
||||
title: "Senior Frontend Engineer",
|
||||
@@ -1139,10 +1042,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "codex_local",
|
||||
budgetMonthlyUsd: 220,
|
||||
},
|
||||
|
||||
// ── Product & Design (11-13) ──
|
||||
{
|
||||
// index 11
|
||||
name: "Tara Blackwell",
|
||||
role: "VP Product",
|
||||
title: "Vice President of Product",
|
||||
@@ -1153,7 +1053,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 200,
|
||||
},
|
||||
{
|
||||
// index 12
|
||||
name: "Omar Khalil",
|
||||
role: "Product Manager — Developer Platform",
|
||||
title: "Product Manager — Developer Platform",
|
||||
@@ -1164,7 +1063,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 160,
|
||||
},
|
||||
{
|
||||
// index 13
|
||||
name: "Vera Lindqvist",
|
||||
role: "Product Designer",
|
||||
title: "Senior Product Designer",
|
||||
@@ -1174,10 +1072,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "claude_local",
|
||||
budgetMonthlyUsd: 160,
|
||||
},
|
||||
|
||||
// ── Marketing & DevRel (14-15) ──
|
||||
{
|
||||
// index 14
|
||||
name: "Marcus Rhodes",
|
||||
role: "Head of Marketing",
|
||||
title: "Head of Marketing & Communications",
|
||||
@@ -1188,7 +1083,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 170,
|
||||
},
|
||||
{
|
||||
// index 15
|
||||
name: "Priya Khatri",
|
||||
role: "Developer Advocate",
|
||||
title: "Senior Developer Advocate",
|
||||
@@ -1198,10 +1092,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "codex_local",
|
||||
budgetMonthlyUsd: 170,
|
||||
},
|
||||
|
||||
// ── People & Finance & Legal (16-17) ──
|
||||
{
|
||||
// index 16
|
||||
name: "Danielle Moreau",
|
||||
role: "Head of People",
|
||||
title: "Head of People",
|
||||
@@ -1212,7 +1103,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 150,
|
||||
},
|
||||
{
|
||||
// index 17
|
||||
name: "Gabriel Souza",
|
||||
role: "Head of Legal & Policy",
|
||||
title: "Head of Legal & Policy",
|
||||
@@ -1251,9 +1141,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
],
|
||||
},
|
||||
|
||||
// ── Digital Agency ────────────────────────────────────────────────────
|
||||
// A full-service digital agency that designs, builds, and markets
|
||||
// digital products for clients.
|
||||
{
|
||||
id: "digital-agency",
|
||||
name: "Full-Service Digital Agency",
|
||||
@@ -1268,9 +1155,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
{ id: "operations", name: "Operations", description: "Resource planning, process management, and quality assurance." },
|
||||
],
|
||||
agents: [
|
||||
// ── Leadership (0-1) ──
|
||||
{
|
||||
// index 0
|
||||
name: "Isabelle Fontaine",
|
||||
role: "Managing Director",
|
||||
title: "Managing Director",
|
||||
@@ -1281,7 +1166,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 200,
|
||||
},
|
||||
{
|
||||
// index 1
|
||||
name: "Victor Nascimento",
|
||||
role: "Account Director",
|
||||
title: "Account Director",
|
||||
@@ -1291,10 +1175,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "claude_local",
|
||||
budgetMonthlyUsd: 170,
|
||||
},
|
||||
|
||||
// ── Creative & Design (2-4) ──
|
||||
{
|
||||
// index 2
|
||||
name: "Saoirse O'Brien",
|
||||
role: "Creative Director",
|
||||
title: "Creative Director",
|
||||
@@ -1305,7 +1186,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 200,
|
||||
},
|
||||
{
|
||||
// index 3
|
||||
name: "Mateo Cruz",
|
||||
role: "UX/UI Designer",
|
||||
title: "Senior UX/UI Designer",
|
||||
@@ -1316,7 +1196,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 170,
|
||||
},
|
||||
{
|
||||
// index 4
|
||||
name: "Yuki Aoyama",
|
||||
role: "Motion & Visual Designer",
|
||||
title: "Motion & Visual Designer",
|
||||
@@ -1326,10 +1205,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "claude_local",
|
||||
budgetMonthlyUsd: 140,
|
||||
},
|
||||
|
||||
// ── Development (5-7) ──
|
||||
{
|
||||
// index 5
|
||||
name: "Nadia Kowalczyk",
|
||||
role: "Technical Director",
|
||||
title: "Technical Director",
|
||||
@@ -1340,7 +1216,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 220,
|
||||
},
|
||||
{
|
||||
// index 6
|
||||
name: "Kwame Asante",
|
||||
role: "Full-Stack Developer",
|
||||
title: "Senior Full-Stack Developer",
|
||||
@@ -1351,7 +1226,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 230,
|
||||
},
|
||||
{
|
||||
// index 7
|
||||
name: "Elina Koskinen",
|
||||
role: "Frontend Developer",
|
||||
title: "Frontend Developer",
|
||||
@@ -1361,10 +1235,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "codex_local",
|
||||
budgetMonthlyUsd: 180,
|
||||
},
|
||||
|
||||
// ── Digital Marketing (8-10) ──
|
||||
{
|
||||
// index 8
|
||||
name: "Tariq Benali",
|
||||
role: "Head of Digital",
|
||||
title: "Head of Digital Marketing",
|
||||
@@ -1375,7 +1246,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 180,
|
||||
},
|
||||
{
|
||||
// index 9
|
||||
name: "Jessica Torres",
|
||||
role: "SEO & Content Specialist",
|
||||
title: "SEO & Content Specialist",
|
||||
@@ -1386,7 +1256,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 150,
|
||||
},
|
||||
{
|
||||
// index 10
|
||||
name: "Ryan Gallagher",
|
||||
role: "Paid Media Specialist",
|
||||
title: "Paid Media Specialist",
|
||||
@@ -1396,10 +1265,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "claude_local",
|
||||
budgetMonthlyUsd: 150,
|
||||
},
|
||||
|
||||
// ── Strategy & Content (11-12) ──
|
||||
{
|
||||
// index 11
|
||||
name: "Ananya Chakraborty",
|
||||
role: "Strategy Lead",
|
||||
title: "Digital Strategy Lead",
|
||||
@@ -1410,7 +1276,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 180,
|
||||
},
|
||||
{
|
||||
// index 12
|
||||
name: "Liam O'Sullivan",
|
||||
role: "Copywriter",
|
||||
title: "Senior Copywriter",
|
||||
@@ -1420,10 +1285,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "claude_local",
|
||||
budgetMonthlyUsd: 160,
|
||||
},
|
||||
|
||||
// ── Operations (13) ──
|
||||
{
|
||||
// index 13
|
||||
name: "Sandra Muller",
|
||||
role: "Project Manager",
|
||||
title: "Senior Project Manager",
|
||||
@@ -1454,9 +1316,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
],
|
||||
},
|
||||
|
||||
// ── B2B SaaS Company ──────────────────────────────────────────────────
|
||||
// A product-led B2B SaaS company with the complete set of functions
|
||||
// needed to build, sell, and support a subscription software product.
|
||||
{
|
||||
id: "saas-company",
|
||||
name: "B2B SaaS Company",
|
||||
@@ -1474,9 +1333,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
{ id: "hr", name: "People", description: "Recruiting, onboarding, culture, and people operations." },
|
||||
],
|
||||
agents: [
|
||||
// ── Executive (0-1) ──
|
||||
{
|
||||
// index 0
|
||||
name: "Nathan Cross",
|
||||
role: "CEO",
|
||||
title: "Chief Executive Officer",
|
||||
@@ -1487,7 +1344,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 230,
|
||||
},
|
||||
{
|
||||
// index 1
|
||||
name: "Samira Rostami",
|
||||
role: "CTO",
|
||||
title: "Chief Technology Officer",
|
||||
@@ -1497,10 +1353,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "claude_local",
|
||||
budgetMonthlyUsd: 240,
|
||||
},
|
||||
|
||||
// ── Engineering (2-6) ──
|
||||
{
|
||||
// index 2
|
||||
name: "Derek Johansson",
|
||||
role: "VP Engineering",
|
||||
title: "Vice President of Engineering",
|
||||
@@ -1511,7 +1364,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 210,
|
||||
},
|
||||
{
|
||||
// index 3
|
||||
name: "Priya Venkatesh",
|
||||
role: "Senior Backend Engineer",
|
||||
title: "Senior Backend Engineer",
|
||||
@@ -1522,7 +1374,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 250,
|
||||
},
|
||||
{
|
||||
// index 4
|
||||
name: "Luis Mendoza",
|
||||
role: "Senior Frontend Engineer",
|
||||
title: "Senior Frontend Engineer",
|
||||
@@ -1533,7 +1384,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 240,
|
||||
},
|
||||
{
|
||||
// index 5
|
||||
name: "Anika Bergman",
|
||||
role: "Platform Engineer",
|
||||
title: "Senior Platform Engineer",
|
||||
@@ -1544,7 +1394,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 230,
|
||||
},
|
||||
{
|
||||
// index 6
|
||||
name: "Tomoko Sato",
|
||||
role: "QA Engineer",
|
||||
title: "Quality Assurance Engineer",
|
||||
@@ -1554,10 +1403,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "codex_local",
|
||||
budgetMonthlyUsd: 180,
|
||||
},
|
||||
|
||||
// ── Product & Design (7-9) ──
|
||||
{
|
||||
// index 7
|
||||
name: "Rachel Abrams",
|
||||
role: "VP Product",
|
||||
title: "Vice President of Product",
|
||||
@@ -1568,7 +1414,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 200,
|
||||
},
|
||||
{
|
||||
// index 8
|
||||
name: "Jake Morrison",
|
||||
role: "Product Manager",
|
||||
title: "Product Manager",
|
||||
@@ -1579,7 +1424,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 170,
|
||||
},
|
||||
{
|
||||
// index 9
|
||||
name: "Chloe Laurent",
|
||||
role: "Product Designer",
|
||||
title: "Senior Product Designer",
|
||||
@@ -1589,10 +1433,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "claude_local",
|
||||
budgetMonthlyUsd: 170,
|
||||
},
|
||||
|
||||
// ── Marketing (10-12) ──
|
||||
{
|
||||
// index 10
|
||||
name: "Ava Richardson",
|
||||
role: "VP Marketing",
|
||||
title: "Vice President of Marketing",
|
||||
@@ -1603,7 +1444,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 190,
|
||||
},
|
||||
{
|
||||
// index 11
|
||||
name: "Benjamin Hart",
|
||||
role: "Content Marketing Manager",
|
||||
title: "Content Marketing Manager",
|
||||
@@ -1614,7 +1454,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 160,
|
||||
},
|
||||
{
|
||||
// index 12
|
||||
name: "Daria Volkov",
|
||||
role: "Growth & Demand Gen Manager",
|
||||
title: "Growth & Demand Generation Manager",
|
||||
@@ -1624,10 +1463,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "claude_local",
|
||||
budgetMonthlyUsd: 160,
|
||||
},
|
||||
|
||||
// ── Sales (13-15) ──
|
||||
{
|
||||
// index 13
|
||||
name: "Carter Ellison",
|
||||
role: "VP Sales",
|
||||
title: "Vice President of Sales",
|
||||
@@ -1638,7 +1474,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 200,
|
||||
},
|
||||
{
|
||||
// index 14
|
||||
name: "Nina Johansson",
|
||||
role: "Enterprise Account Executive",
|
||||
title: "Enterprise Account Executive",
|
||||
@@ -1649,7 +1484,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 180,
|
||||
},
|
||||
{
|
||||
// index 15
|
||||
name: "Travis Chen",
|
||||
role: "SDR Lead",
|
||||
title: "Sales Development Representative Lead",
|
||||
@@ -1659,10 +1493,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "claude_local",
|
||||
budgetMonthlyUsd: 130,
|
||||
},
|
||||
|
||||
// ── Customer Success & Support (16-18) ──
|
||||
{
|
||||
// index 16
|
||||
name: "Miriam Okafor",
|
||||
role: "VP Customer Success",
|
||||
title: "Vice President of Customer Success",
|
||||
@@ -1673,7 +1504,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 180,
|
||||
},
|
||||
{
|
||||
// index 17
|
||||
name: "Alex Petridis",
|
||||
role: "Customer Success Manager",
|
||||
title: "Customer Success Manager",
|
||||
@@ -1684,7 +1514,6 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
budgetMonthlyUsd: 140,
|
||||
},
|
||||
{
|
||||
// index 18
|
||||
name: "Rina Tanaka",
|
||||
role: "Support Engineer",
|
||||
title: "Technical Support Engineer",
|
||||
@@ -1694,10 +1523,7 @@ export const COMPANY_TEMPLATES: CompanyTemplate[] = [
|
||||
connectorPreference: "codex_local",
|
||||
budgetMonthlyUsd: 140,
|
||||
},
|
||||
|
||||
// ── Finance & People (19) ──
|
||||
{
|
||||
// index 19
|
||||
name: "Henrik Larsen",
|
||||
role: "Head of Finance",
|
||||
title: "Head of Finance",
|
||||
|
||||
Reference in new issue
Block a user