Two tightly-coupled hardening changes to the credential IPC module.
(1) Drop the self-built `DesktopOk/DesktopErr/ok/err/handle` helpers
that called `ipcMain.handle` directly, bypassing the trusted-renderer
check in `ipc-support.registerHandle` and the `ZodError ->
IPC_INVALID_PAYLOAD` sanitizer. The module now takes `registerHandle`
and `fail` as deps (wired from `createIpcSupport`) and imports `ok`
from `ipc-core-common`, mirroring the OAuth handler layout.
(2) Tighten `redactSecrets` to issuer-prefixed patterns only (Bearer,
`sk-`, `sk-ant-`, `sk-proj-`, AKIA access key IDs, three-segment JWTs).
The previous blanket `[A-Fa-f0-9]{40,}` and `[A-Za-z0-9+/]{48,}={0,2}`
rules were shredding legitimate diagnostic text — git SHAs, UUIDs,
Fireworks model identifiers like `accounts/fireworks/models/...`, and
any long opaque ID — into `[REDACTED-HEX]` / `[REDACTED-B64]`, which
made provider errors unreadable in the renderer.
The lock was previously claimed when the worker reported `running`, which
is after `agent.prompt` had already started streaming. Two concurrent
runs targeting the same workspace could both pass the main-side checks,
both get enqueued, and both read half-written state before one of them
lost the lock race and was cancelled mid-flight.
Move the `claimWorkspaceLock` call into `dispatchRunToWorker` so main
coordinates exclusivity authoritatively. If the lock is unavailable,
the run is finished as failed and never enqueued. The worker-side
lease-renewal path is preserved since it is a lease, not a lock.
The self-built `ok/err/handle` helpers called `ipcMain.handle` directly,
which meant every OAuth IPC channel skipped the trusted-renderer check
in `ipc-support.registerHandle` and its `ZodError -> IPC_INVALID_PAYLOAD`
sanitizer. Any renderer origin could invoke these channels, and thrown
zod errors would leak the raw validator message.
Take `registerHandle` and `fail` as dependencies so callers wire in the
hardened variants from `createIpcSupport`, drop the local helpers, and
import `ok` from `ipc-core-common`. Zod inputs stay on `safeParse` to
preserve the explicit `INVALID_INPUT` code + message path.
Previously `AGENTCOMPANY_ALLOW_UNSAFE_CONNECTOR_BYPASS !== "false"` meant
packaged builds defaulted to passing `dangerouslyBypassApprovalsAndSandbox`,
`dangerouslySkipPermissions`, `yolo`, and `--sandbox danger-full-access`
to CLI connectors, bypassing ApprovalService entirely.
Require both explicit opt-in (`AGENTCOMPANY_ALLOW_UNSAFE_CONNECTOR_BYPASS=true`)
and a non-packaged build. Packaged builds hard-refuse regardless of the env
var. Main process forks the worker with `AGENTCOMPANY_PACKAGED=<app.isPackaged>`
so the worker has an authoritative packaged signal.
Split the single 15s setInterval in scheduler.ts (6 serial jobs) into 4
independent schedulers so a slow job no longer stalls the others:
- scheduler-heartbeat.ts (15s): wake eligible agents, CEO bootstrap,
schedule automation trigger
- scheduler-lock-recovery.ts (60s): stale task-lock recovery and stale
workspace-lock release
- scheduler-idle-automation.ts (60s): idle-agent automation dispatch
- scheduler-housekeeping.ts (300s): monthly budget reset, expired budget
incident auto-close, stale review sweep, expired wakeup purge
scheduler.ts is now a thin composer exposing startAllSchedulers /
stopAllSchedulers. Bootstrap and service-graph updated accordingly.
Business logic, error isolation, and atomic actions preserved.
Pi-ai's `streamSimple`/`SimpleStreamOptions` has no per-request env
escape hatch; its Azure/AWS/GCP ADC providers read `process.env`
directly. The previous implementation wrote `extraEnv` into
`process.env` globally and relied on a `finally` after
`stream.result()` to clean up, so two concurrent `testCredential`
calls would clobber each other's env, expose the wrong credential
to in-flight streams, and on error paths fail to restore
previously-set values.
Wrap the env-mutating branch in a module-level promise chain so at
most one `extraEnv` test runs at a time, snapshot the prior value
of every key we overwrite, and restore (or delete when unset) in
`finally`. Tests that resolve to `extraEnv: undefined` bypass the
mutex entirely.
showMessageBoxSync froze the main-process event loop while the user
decided whether to open an external link, stalling agent-event workers,
IPC, and the scheduler for the duration of the dialog. setWindowOpenHandler
still returns synchronously with action: "deny"; the confirmation dialog
runs in an async continuation that only reaches shell.openExternal when
the user approves.
Origin-less requests previously skipped the allowlist entirely, meaning
any process that omitted the Origin header (trivial for a non-browser
attacker) could reach the auth layer. Browsers always send Origin, so
rejecting Origin-less browser-style requests closes the bypass without
breaking agent CLIs, which authenticate with a bearer token instead.
New policy:
- Requests with a Bearer Authorization header: Origin optional; if
present it must be in the allowlist.
- All other requests (including CORS preflight): Origin required and
must be in the allowlist.
Aligns tests/unit/database.test.ts with the existing convention in
oauth-login-service/credential-resolver/credential-store tests: set
AGENTCOMPANY_ALLOW_INSECURE_VAULT=true in beforeAll so the in-memory
database test can call saveSecret without a real OS keychain.
Follow-up to aa1afd0 which removed the NODE_ENV=test/VITEST auto-enable.
Every handler was wrapping its body in try/catch and returning fail(code,
error.message), leaking raw DB messages to the renderer and bypassing the
sanitizer in registerHandle. Removed the 14 inner try/catch shells so
errors surface through ipc-support's IPC_HANDLER_FAILED / IPC_INVALID_PAYLOAD
path. Also removed the duplicate emitEvent({ type: 'domain-changed' }) in
addThreadEvent — publishDomainChanged already emits the same event.
safeGetAgent was catching all errors and returning null, causing every
permission check (canControlAgent / canManageOrganization / canRequestHire)
to silently deny when the database threw. Only swallow the 'Agent not found'
case and let real DB errors propagate up to registerHandle's sanitizer.
Handlers for deleteTask/deleteGoal/deleteProject live in
ipc-core-task-graph-handlers.ts:166-168 (backed by
database/tasks-crud.ts and database/entity-crud.ts), and
deleteWorkspace lives at line 169 (backed by database/workspaces.ts).
The four @planned comments were misleading.
setSearchQuery only incremented searchRequestId without flipping
searchLoading, so the UI could not distinguish "searching" from "no
results". Now loading flips true on non-empty query and clears
(results + loading) on empty query. setSearchResults keeps the
existing id-match logic to flip loading false on arrival.
Constraint: Linux arm64 assets and x64 assets share the same draft release transport bucket
Rejected: Broad *.AppImage/*.deb/*.rpm globs | x64 upload step collided with arm64 assets and failed the workflow
Confidence: high
Scope-risk: narrow
Directive: Keep per-architecture upload globs disjoint whenever multiple jobs share one draft release
Tested: Prior run proved packaging succeeded and isolated the failure to the x64 upload selection
Not-tested: Updated workflow dispatch before rerun
- Remove self-defeating heartbeat task filter that excluded heartbeat tasks from candidates
- Add Paperclip-style task reuse: recycle existing done/in_review heartbeat tasks instead of creating new ones
- Fix status bug: new heartbeat tasks created as "todo" instead of "in_progress" so CAS checkout succeeds
- Wire one-time duplicate cleanup sweep into heartbeat scheduler startup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add findReusableHeartbeatTask() to find most recent done/in_review heartbeat task per agent
- Add cleanupDuplicateHeartbeatTasks() to cancel old duplicates using ROW_NUMBER window function
- Add 12 unit tests covering coalescing behavior, cross-agent isolation, status filtering
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Step 1: review available skills before designing org structure
- Step 3: assign skills to agents after each hire is approved
- Priority 3: check/assign agent skills before delegating tasks
- GET /api/companies/:companyId/skills (list company skills)
- GET /api/skills/:skillId (skill detail with markdown)
- POST /api/agents/:agentId/skills (assign skill to agent)
- DELETE /api/agents/:agentId/skills/:skillId (remove skill)
- GET /api/agents/:agentId/skills (list agent's skills)
- Kickoff task now instructs CEO to explore workspace codebase before planning
- Company name and description interpolated into both activeGoal and fallback branches
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skills from user home directories are now auto-imported when listing
company skills. Deduplication handled by canonical key uniqueness.
Scans: ~/.agents/skills/, ~/.claude/skills/, and workspace .agents/skills/.
Cross-platform: uses HOME or USERPROFILE env var.
- ChannelSidebar: replace inline color-mix style with Tailwind classes for task discussion icons
- MessageItem: replace broken priority config (color-mix inline) with Tailwind utility classes
- MessageItem: replace broken department badge hex-alpha pattern with Tailwind classes
- automation-config: restore ACCENT/LINE as hex values (used in template literal alpha suffixes)
- desktop-shell-service: set nativeTheme.themeSource to sync Windows title bar with app theme