feat(team): add app-managed native bootstrap

This commit is contained in:
777genius 2026-05-06 17:34:01 +03:00
parent d60abd54fe
commit 1febc3448b
12 changed files with 1236 additions and 112 deletions

View file

@ -0,0 +1,149 @@
// electron.vite.config.ts
import { defineConfig } from "electron-vite";
import { sentryVitePlugin } from "@sentry/vite-plugin";
import react from "@vitejs/plugin-react";
import { readFileSync } from "fs";
import { resolve } from "path";
var __electron_vite_injected_dirname = "/Users/belief/dev/projects/claude/claude_team";
var pkg = JSON.parse(readFileSync(resolve(__electron_vite_injected_dirname, "package.json"), "utf-8"));
var prodDeps = Object.keys(pkg.dependencies || {});
var runtimeExternalDeps = /* @__PURE__ */ new Set([
"node-pty",
"agent-teams-controller",
"fastify",
"@fastify/cors",
"@fastify/static"
]);
var bundledDeps = prodDeps.filter((d) => !runtimeExternalDeps.has(d));
function nativeModuleStub() {
const STUB_ID = "\0native-stub";
const NODE_MODULE_RE = /\.node(?:\?.*)?$/;
return {
name: "native-module-stub",
enforce: "pre",
resolveId(source) {
if (NODE_MODULE_RE.test(source)) return `${STUB_ID}:${source}`;
return null;
},
load(id) {
if (id.startsWith(STUB_ID) || NODE_MODULE_RE.test(id)) return "export default {}";
return null;
}
};
}
var sentryPlugins = process.env.SENTRY_AUTH_TOKEN ? [
sentryVitePlugin({
org: process.env.SENTRY_ORG ?? "quant-jump-pro",
project: process.env.SENTRY_PROJECT ?? "electron",
authToken: process.env.SENTRY_AUTH_TOKEN,
release: { name: `agent-teams-ai@${pkg.version}` },
sourcemaps: {
filesToDeleteAfterUpload: ["./out/renderer/**/*.map", "./dist-electron/**/*.map"]
}
})
] : [];
var electron_vite_config_default = defineConfig({
main: {
plugins: [
nativeModuleStub(),
...sentryPlugins
],
define: {
__APP_VERSION__: JSON.stringify(pkg.version),
// Inject DSN at compile time — process.env.SENTRY_DSN is NOT available
// at runtime in packaged Electron apps (only during CI build).
"process.env.SENTRY_DSN": JSON.stringify(process.env.SENTRY_DSN ?? "")
},
resolve: {
alias: {
"@features": resolve(__electron_vite_injected_dirname, "src/features"),
"@main": resolve(__electron_vite_injected_dirname, "src/main"),
"@shared": resolve(__electron_vite_injected_dirname, "src/shared"),
"@preload": resolve(__electron_vite_injected_dirname, "src/preload")
}
},
build: {
externalizeDeps: {
exclude: bundledDeps
},
commonjsOptions: {
strictRequires: [/node_modules\/.*ssh2\//]
},
sourcemap: "hidden",
outDir: "dist-electron/main",
rollupOptions: {
input: {
index: resolve(__electron_vite_injected_dirname, "src/main/index.ts"),
"team-fs-worker": resolve(__electron_vite_injected_dirname, "src/main/workers/team-fs-worker.ts"),
"task-change-worker": resolve(__electron_vite_injected_dirname, "src/main/workers/task-change-worker.ts"),
"team-data-worker": resolve(__electron_vite_injected_dirname, "src/main/workers/team-data-worker.ts")
},
output: {
// CJS format so bundled deps can use __dirname/require.
// Use .cjs extension since package.json has "type": "module".
format: "cjs",
entryFileNames: "[name].cjs",
// Set UV_THREADPOOL_SIZE before any module code runs.
// Must be in the banner because ESM→CJS hoists imports above top-level code.
// On Windows, fs.watch({recursive:true}) occupies a UV pool thread per watcher;
// with 3+ watchers + concurrent fs/DNS/spawn, the default 4 threads deadlock.
banner: `if(!process.env.UV_THREADPOOL_SIZE){process.env.UV_THREADPOOL_SIZE='24'}`
}
}
}
},
preload: {
resolve: {
alias: {
"@features": resolve(__electron_vite_injected_dirname, "src/features"),
"@preload": resolve(__electron_vite_injected_dirname, "src/preload"),
"@shared": resolve(__electron_vite_injected_dirname, "src/shared"),
"@main": resolve(__electron_vite_injected_dirname, "src/main")
}
},
build: {
outDir: "dist-electron/preload",
rollupOptions: {
input: {
index: resolve(__electron_vite_injected_dirname, "src/preload/index.ts")
},
output: {
format: "cjs",
entryFileNames: "[name].js"
}
}
}
},
renderer: {
optimizeDeps: {
include: ["@codemirror/language-data"],
exclude: ["@claude-teams/agent-graph"]
},
define: {
__APP_VERSION__: JSON.stringify(pkg.version),
// Pass SENTRY_DSN to renderer as VITE_SENTRY_DSN (Vite replaces at compile time)
"import.meta.env.VITE_SENTRY_DSN": JSON.stringify(process.env.SENTRY_DSN ?? "")
},
resolve: {
alias: {
"@features": resolve(__electron_vite_injected_dirname, "src/features"),
"@renderer": resolve(__electron_vite_injected_dirname, "src/renderer"),
"@shared": resolve(__electron_vite_injected_dirname, "src/shared"),
"@main": resolve(__electron_vite_injected_dirname, "src/main"),
"@claude-teams/agent-graph": resolve(__electron_vite_injected_dirname, "packages/agent-graph/src/index.ts")
}
},
plugins: [react(), ...sentryPlugins],
build: {
sourcemap: "hidden",
rollupOptions: {
input: {
index: resolve(__electron_vite_injected_dirname, "src/renderer/index.html")
}
}
}
}
});
export {
electron_vite_config_default as default
};

View file

@ -255,6 +255,14 @@ import {
import { TeamSentMessagesStore } from './TeamSentMessagesStore';
import { TeamTaskReader } from './TeamTaskReader';
import { TeamTranscriptProjectResolver } from './TeamTranscriptProjectResolver';
import {
buildNativeAppManagedBootstrapSpecs,
type NativeAppManagedBootstrapSpec,
} from './bootstrap/NativeAppManagedBootstrapContextBuilder';
import {
parseBootstrapRuntimeProofDetail,
validateBootstrapRuntimeProofEnvelope,
} from './bootstrap/BootstrapProofValidation';
import type {
OpenCodeCommittedBootstrapSessionRecord,
@ -307,6 +315,10 @@ interface PersistedRuntimeMemberLike {
cwd?: string;
bootstrapExpectedAfter?: string;
bootstrapProofToken?: string;
bootstrapRunId?: string;
bootstrapProofMode?: string;
bootstrapContextHash?: string;
bootstrapBriefingHash?: string;
bootstrapRuntimeEventsPath?: string;
runtimePid?: number;
runtimeSessionId?: string;
@ -341,7 +353,6 @@ interface LaunchStateWriteResult {
type BootstrapTranscriptSuccessSource = 'member_briefing' | 'assistant_text';
const BOOTSTRAP_RUNTIME_PROOF_SOURCE = 'member_briefing_tool_success';
const BOOTSTRAP_RUNTIME_PROOF_TAIL_BYTES = 256 * 1024;
function sanitizeRuntimeEventFilePrefix(value: string): string {
@ -350,31 +361,6 @@ function sanitizeRuntimeEventFilePrefix(value: string): string {
.toLowerCase();
}
function parseRuntimeBootstrapProofDetail(detail: unknown): Record<string, unknown> {
if (typeof detail !== 'string' || detail.trim().length === 0) {
return {};
}
try {
const parsed = JSON.parse(detail) as unknown;
return parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : {};
} catch {
return {};
}
}
function getRuntimeBootstrapProofString(
event: Record<string, unknown>,
detail: Record<string, unknown>,
field: 'source' | 'bootstrapProofToken'
): string | undefined {
const direct = event[field];
if (typeof direct === 'string' && direct.trim().length > 0) {
return direct.trim();
}
const nested = detail[field];
return typeof nested === 'string' && nested.trim().length > 0 ? nested.trim() : undefined;
}
type BootstrapTranscriptOutcome =
| {
kind: 'success';
@ -3927,6 +3913,7 @@ interface RuntimeBootstrapMemberSpec {
description?: string;
useSplitPane?: boolean;
planModeRequired?: boolean;
nativeAppManagedBootstrap?: NativeAppManagedBootstrapSpec;
}
interface RuntimeBootstrapSpec {
@ -3961,7 +3948,8 @@ interface RuntimeBootstrapSpec {
function buildDeterministicCreateBootstrapSpec(
runId: string,
request: TeamCreateRequest,
effectiveMembers: TeamCreateRequest['members']
effectiveMembers: TeamCreateRequest['members'],
nativeAppManagedBootstrapByMember: ReadonlyMap<string, NativeAppManagedBootstrapSpec> = new Map()
): RuntimeBootstrapSpec {
return {
version: 1,
@ -4001,6 +3989,9 @@ function buildDeterministicCreateBootstrapSpec(
...(member.effort ? { effort: member.effort } : {}),
...(member.isolation === 'worktree' ? { isolation: 'worktree' as const } : {}),
...(member.role?.trim() ? { description: member.role.trim() } : {}),
...(nativeAppManagedBootstrapByMember.get(member.name)
? { nativeAppManagedBootstrap: nativeAppManagedBootstrapByMember.get(member.name)! }
: {}),
})),
launch: {
continueOnPartialFailure: true,
@ -4014,7 +4005,8 @@ function buildDeterministicCreateBootstrapSpec(
function buildDeterministicLaunchBootstrapSpec(
runId: string,
request: TeamLaunchRequest,
effectiveMembers: TeamCreateRequest['members']
effectiveMembers: TeamCreateRequest['members'],
nativeAppManagedBootstrapByMember: ReadonlyMap<string, NativeAppManagedBootstrapSpec> = new Map()
): RuntimeBootstrapSpec {
return {
version: 1,
@ -4051,6 +4043,9 @@ function buildDeterministicLaunchBootstrapSpec(
...(member.role?.trim() ? { role: member.role.trim() } : {}),
...(member.workflow?.trim() ? { workflow: member.workflow.trim() } : {}),
...(member.role?.trim() ? { description: member.role.trim() } : {}),
...(nativeAppManagedBootstrapByMember.get(member.name)
? { nativeAppManagedBootstrap: nativeAppManagedBootstrapByMember.get(member.name)! }
: {}),
})),
launch: {
continueOnPartialFailure: true,
@ -12041,6 +12036,14 @@ export class TeamProvisioningService {
);
const liveRuntimeByMember = await this.getLiveTeamAgentRuntimeMetadata(teamName);
const spawnStatusSnapshot = await this.getMemberSpawnStatuses(teamName).catch(() => null);
const activeRuntimeRunId =
run?.runId?.trim() || currentRuntimeAdapterRun?.runId?.trim() || runId?.trim() || '';
const spawnStatusRunId = spawnStatusSnapshot?.runId?.trim() ?? '';
const canUseLiveSpawnStatusRuntimeTruth =
spawnStatusSnapshot?.source === 'live' &&
activeRuntimeRunId.length > 0 &&
spawnStatusRunId === activeRuntimeRunId;
const runtimePids = new Set<number>();
const leadPid = run?.child?.pid;
if (typeof leadPid === 'number' && Number.isFinite(leadPid) && leadPid > 0) {
@ -12077,6 +12080,23 @@ export class TeamProvisioningService {
}
return fallback;
};
const getSpawnStatusMember = (memberName: string): MemberSpawnStatusEntry | undefined => {
const statuses = spawnStatusSnapshot?.statuses;
if (!statuses) {
return undefined;
}
const direct = statuses[memberName];
if (direct) {
return direct;
}
let fallback: MemberSpawnStatusEntry | undefined;
for (const [candidateName, status] of Object.entries(statuses)) {
if (matchesMemberNameOrBase(candidateName, memberName)) {
fallback = status;
}
}
return fallback;
};
const candidateMembers = new Map<string, TeamMember>();
for (const member of configuredMembers) {
@ -12137,6 +12157,7 @@ export class TeamProvisioningService {
const persistedRuntimeMember = getPersistedRuntimeMember(memberName);
const liveRuntimeMember = getLiveRuntimeMember(memberName);
const spawnStatusMember = getSpawnStatusMember(memberName);
const launchMember = launchSnapshot?.members[memberName];
const backendType =
liveRuntimeMember?.backendType ??
@ -12172,7 +12193,38 @@ export class TeamProvisioningService {
: backendType !== 'in-process';
const historicalBootstrapConfirmed =
launchMember?.bootstrapConfirmed === true ||
launchMember?.launchState === 'confirmed_alive';
launchMember?.launchState === 'confirmed_alive' ||
spawnStatusMember?.bootstrapConfirmed === true ||
spawnStatusMember?.launchState === 'confirmed_alive';
const hasOpenCodeRuntimeHandle =
isOpenCodeMember &&
(typeof liveRuntimeMember?.pid === 'number' ||
typeof liveRuntimeMember?.metricsPid === 'number' ||
typeof liveRuntimeMember?.runtimeSessionId === 'string');
const confirmedOpenCodeRuntimeAlive =
isOpenCodeMember &&
canUseLiveSpawnStatusRuntimeTruth &&
historicalBootstrapConfirmed &&
hasOpenCodeRuntimeHandle &&
spawnStatusMember?.hardFailure !== true &&
spawnStatusMember?.launchState !== 'failed_to_start' &&
spawnStatusMember?.launchState !== 'runtime_pending_permission';
const effectiveAlive = liveRuntimeMember?.alive === true || confirmedOpenCodeRuntimeAlive;
const effectiveLivenessKind =
confirmedOpenCodeRuntimeAlive &&
liveRuntimeMember?.livenessKind === 'runtime_process_candidate'
? 'confirmed_bootstrap'
: liveRuntimeMember?.livenessKind;
const effectiveRuntimeDiagnostic =
confirmedOpenCodeRuntimeAlive &&
liveRuntimeMember?.livenessKind === 'runtime_process_candidate'
? 'OpenCode bootstrap confirmed; runtime host/session evidence present.'
: liveRuntimeMember?.runtimeDiagnostic;
const effectiveRuntimeDiagnosticSeverity =
confirmedOpenCodeRuntimeAlive &&
liveRuntimeMember?.livenessKind === 'runtime_process_candidate'
? 'info'
: liveRuntimeMember?.runtimeDiagnosticSeverity;
let rssBytes = rssPid ? rssBytesByPid.get(rssPid) : undefined;
if (rssBytes == null && isSharedOpenCodeHost && typeof rssPid === 'number' && rssPid > 0) {
try {
@ -12188,7 +12240,7 @@ export class TeamProvisioningService {
snapshotMembers[memberName] = {
memberName,
alive: liveRuntimeMember?.alive === true,
alive: effectiveAlive,
restartable,
...(backendType ? { backendType } : {}),
...(memberProviderId ? { providerId: memberProviderId } : {}),
@ -12201,9 +12253,7 @@ export class TeamProvisioningService {
...(runtimeModel ? { runtimeModel } : {}),
...(runtimeCwd ? { cwd: runtimeCwd } : {}),
...(typeof rssBytes === 'number' && rssBytes >= 0 ? { rssBytes } : {}),
...(liveRuntimeMember?.livenessKind
? { livenessKind: liveRuntimeMember.livenessKind }
: {}),
...(effectiveLivenessKind ? { livenessKind: effectiveLivenessKind } : {}),
...(liveRuntimeMember?.pidSource ? { pidSource: liveRuntimeMember.pidSource } : {}),
...(liveRuntimeMember?.processCommand
? { processCommand: liveRuntimeMember.processCommand }
@ -12221,11 +12271,9 @@ export class TeamProvisioningService {
? { runtimeLastSeenAt: liveRuntimeMember.runtimeLastSeenAt }
: {}),
...(historicalBootstrapConfirmed ? { historicalBootstrapConfirmed: true } : {}),
...(liveRuntimeMember?.runtimeDiagnostic
? { runtimeDiagnostic: liveRuntimeMember.runtimeDiagnostic }
: {}),
...(liveRuntimeMember?.runtimeDiagnosticSeverity
? { runtimeDiagnosticSeverity: liveRuntimeMember.runtimeDiagnosticSeverity }
...(effectiveRuntimeDiagnostic ? { runtimeDiagnostic: effectiveRuntimeDiagnostic } : {}),
...(effectiveRuntimeDiagnosticSeverity
? { runtimeDiagnosticSeverity: effectiveRuntimeDiagnosticSeverity }
: {}),
...(liveRuntimeMember?.diagnostics ? { diagnostics: liveRuntimeMember.diagnostics } : {}),
updatedAt,
@ -16315,16 +16363,6 @@ export class TeamProvisioningService {
emitProvisioningCheckpoint(run, 'Clearing persisted launch state');
await this.clearPersistedLaunchState(request.teamName);
emitProvisioningCheckpoint(
run,
'Building deterministic create bootstrap spec',
`expectedMembers=${effectiveMemberSpecs.length}`
);
const bootstrapSpec = buildDeterministicCreateBootstrapSpec(
runId,
request,
effectiveMemberSpecs
);
const initialUserPrompt = request.prompt?.trim() ?? '';
const promptSize = getPromptSizeSummary(initialUserPrompt);
let child: ReturnType<typeof spawn>;
@ -16335,6 +16373,52 @@ export class TeamProvisioningService {
let bootstrapSpecPath: string;
let bootstrapUserPromptPath: string | null = null;
try {
// Pre-save our meta files before native app-managed briefing generation.
// member_briefing intentionally reads canonical team metadata/inboxes, so
// createTeam must materialize those files before building the bootstrap spec.
emitProvisioningCheckpoint(run, 'Persisting team metadata before spawn');
const teamDir = path.join(getTeamsBasePath(), request.teamName);
const tasksDir = path.join(getTasksBasePath(), request.teamName);
await fs.promises.mkdir(teamDir, { recursive: true });
await fs.promises.mkdir(tasksDir, { recursive: true });
await this.teamMetaStore.writeMeta(request.teamName, {
displayName: request.displayName,
description: request.description,
color: request.color,
cwd: request.cwd,
prompt: request.prompt,
providerId: request.providerId,
providerBackendId: request.providerBackendId,
model: request.model,
effort: request.effort,
fastMode: request.fastMode,
skipPermissions: request.skipPermissions,
worktree: request.worktree,
extraCliArgs: request.extraCliArgs,
limitContext: request.limitContext,
launchIdentity,
createdAt: Date.now(),
});
const membersToWrite = this.buildMembersMetaWritePayload(allEffectiveMemberSpecs);
await this.membersMetaStore.writeMembers(request.teamName, membersToWrite, {
providerBackendId: request.providerBackendId,
});
emitProvisioningCheckpoint(
run,
'Building deterministic create bootstrap spec',
`expectedMembers=${effectiveMemberSpecs.length}`
);
const nativeAppManagedBootstrapByMember = await buildNativeAppManagedBootstrapSpecs({
teamName: request.teamName,
cwd: request.cwd,
members: effectiveMemberSpecs,
});
const bootstrapSpec = buildDeterministicCreateBootstrapSpec(
runId,
request,
effectiveMemberSpecs,
nativeAppManagedBootstrapByMember
);
emitProvisioningCheckpoint(run, 'Writing deterministic bootstrap spec file');
bootstrapSpecPath = await writeDeterministicBootstrapSpecFile(bootstrapSpec);
run.bootstrapSpecPath = bootstrapSpecPath;
@ -16366,6 +16450,11 @@ export class TeamProvisioningService {
directory: provisioningEnv.anthropicApiKeyHelper.directory,
}).catch(() => undefined);
}
await this.teamMetaStore.deleteMeta(request.teamName).catch(() => {});
const teamDir = path.join(getTeamsBasePath(), request.teamName);
const tasksDir = path.join(getTasksBasePath(), request.teamName);
await fs.promises.rm(teamDir, { recursive: true, force: true }).catch(() => {});
await fs.promises.rm(tasksDir, { recursive: true, force: true }).catch(() => {});
await removeDeterministicBootstrapSpecFile(run.bootstrapSpecPath).catch(() => {});
run.bootstrapSpecPath = null;
await removeDeterministicBootstrapUserPromptFile(run.bootstrapUserPromptPath).catch(
@ -16434,35 +16523,6 @@ export class TeamProvisioningService {
launchIdentity,
});
try {
// Pre-save our meta files before spawn — CLI doesn't touch these.
// If provisioning fails before TeamCreate, user can retry without re-entering config.
emitProvisioningCheckpoint(run, 'Persisting team metadata before spawn');
const teamDir = path.join(getTeamsBasePath(), request.teamName);
const tasksDir = path.join(getTasksBasePath(), request.teamName);
await fs.promises.mkdir(teamDir, { recursive: true });
await fs.promises.mkdir(tasksDir, { recursive: true });
await this.teamMetaStore.writeMeta(request.teamName, {
displayName: request.displayName,
description: request.description,
color: request.color,
cwd: request.cwd,
prompt: request.prompt,
providerId: request.providerId,
providerBackendId: request.providerBackendId,
model: request.model,
effort: request.effort,
fastMode: request.fastMode,
skipPermissions: request.skipPermissions,
worktree: request.worktree,
extraCliArgs: request.extraCliArgs,
limitContext: request.limitContext,
launchIdentity,
createdAt: Date.now(),
});
const membersToWrite = this.buildMembersMetaWritePayload(allEffectiveMemberSpecs);
await this.membersMetaStore.writeMembers(request.teamName, membersToWrite, {
providerBackendId: request.providerBackendId,
});
if (
run.cancelRequested ||
run.processKilled ||
@ -17617,7 +17677,12 @@ export class TeamProvisioningService {
const bootstrapSpec = buildDeterministicLaunchBootstrapSpec(
runId,
request,
effectiveMemberSpecs
effectiveMemberSpecs,
await buildNativeAppManagedBootstrapSpecs({
teamName: request.teamName,
cwd: request.cwd,
members: effectiveMemberSpecs,
})
);
emitProvisioningCheckpoint(run, 'Writing deterministic bootstrap spec file');
bootstrapSpecPath = await writeDeterministicBootstrapSpecFile(bootstrapSpec);
@ -20413,6 +20478,74 @@ export class TeamProvisioningService {
return undefined;
}
private buildLaunchMemberSpawnStatus(
member: PersistedTeamLaunchMemberState | undefined,
runtimeModel?: string
): MemberSpawnStatusEntry | undefined {
if (!member) {
return undefined;
}
return {
status: member.hardFailure
? 'error'
: member.bootstrapConfirmed || member.launchState === 'confirmed_alive'
? 'online'
: member.agentToolAccepted
? 'waiting'
: 'spawning',
launchState: member.launchState,
...(member.hardFailureReason ? { hardFailureReason: member.hardFailureReason } : {}),
...(member.pendingPermissionRequestIds?.length
? { pendingPermissionRequestIds: member.pendingPermissionRequestIds }
: {}),
agentToolAccepted: member.agentToolAccepted,
runtimeAlive: member.runtimeAlive,
bootstrapConfirmed: member.bootstrapConfirmed,
hardFailure: member.hardFailure,
...(runtimeModel ? { runtimeModel } : {}),
...(member.livenessKind ? { livenessKind: member.livenessKind } : {}),
...(member.runtimeDiagnostic ? { runtimeDiagnostic: member.runtimeDiagnostic } : {}),
...(member.runtimeDiagnosticSeverity
? { runtimeDiagnosticSeverity: member.runtimeDiagnosticSeverity }
: {}),
...(member.bootstrapStalled ? { bootstrapStalled: true } : {}),
...(member.firstSpawnAcceptedAt ? { firstSpawnAcceptedAt: member.firstSpawnAcceptedAt } : {}),
...(member.lastHeartbeatAt ? { lastHeartbeatAt: member.lastHeartbeatAt } : {}),
updatedAt: member.lastEvaluatedAt,
};
}
private shouldPreferCurrentLaunchMemberStatus(
trackedStatus: MemberSpawnStatusEntry | undefined,
launchStatus: MemberSpawnStatusEntry | undefined
): boolean {
if (!launchStatus?.bootstrapConfirmed && launchStatus?.launchState !== 'confirmed_alive') {
return false;
}
if (!trackedStatus) {
return true;
}
return (
trackedStatus.hardFailure !== true &&
trackedStatus.launchState !== 'failed_to_start' &&
trackedStatus.launchState !== 'runtime_pending_permission'
);
}
private isLaunchMemberStatusRelevantToRuntimeRun(
member: PersistedTeamLaunchMemberState | undefined,
activeRuntimeRunId: string
): boolean {
if (!member || activeRuntimeRunId.length === 0) {
return false;
}
const memberRuntimeRunId = member.runtimeRunId?.trim() ?? '';
if (member.providerId === 'opencode') {
return memberRuntimeRunId.length > 0 && memberRuntimeRunId === activeRuntimeRunId;
}
return memberRuntimeRunId.length === 0 || memberRuntimeRunId === activeRuntimeRunId;
}
private async getLiveTeamAgentRuntimeMetadata(
teamName: string
): Promise<Map<string, LiveTeamAgentRuntimeMetadata>> {
@ -20620,7 +20753,12 @@ export class TeamProvisioningService {
}
const currentRuntimeAdapterRun = this.runtimeAdapterRunByTeam.get(teamName);
const persistedLaunchSnapshot = await this.launchStateStore.read(teamName).catch(() => null);
const persistedLaunchSnapshot = choosePreferredLaunchSnapshot(
await readBootstrapLaunchSnapshot(teamName).catch(() => null),
await this.launchStateStore.read(teamName).catch(() => null)
);
const activeRuntimeRunId =
run?.runId?.trim() || currentRuntimeAdapterRun?.runId?.trim() || runId?.trim() || '';
for (const persistedMember of Object.values(persistedLaunchSnapshot?.members ?? {})) {
const memberName = persistedMember.name?.trim() ?? '';
if (!memberName || this.isMemberRemovedInMeta(metaMembers, memberName)) {
@ -20739,7 +20877,6 @@ export class TeamProvisioningService {
updatedAt: persistedLaunchSnapshot?.updatedAt ?? nowIso(),
}
: undefined;
const status = this.findTrackedMemberSpawnStatus(run, memberName) ?? adapterStatus;
const shouldUseWindowsHostRows =
process.platform === 'win32' &&
(metadata.providerId === 'opencode' ||
@ -20754,6 +20891,15 @@ export class TeamProvisioningService {
const memberProcessTableAvailable = shouldUseWindowsHostRows
? windowsHostProcessTableAvailable || processTableAvailable
: processTableAvailable;
const trackedStatus = this.findTrackedMemberSpawnStatus(run, memberName);
const launchStatus =
this.isLaunchMemberStatusRelevantToRuntimeRun(launchMember, activeRuntimeRunId) &&
launchMember
? this.buildLaunchMemberSpawnStatus(launchMember, metadata.model)
: undefined;
const status = this.shouldPreferCurrentLaunchMemberStatus(trackedStatus, launchStatus)
? launchStatus
: (trackedStatus ?? adapterStatus ?? launchStatus);
const resolved = resolveTeamMemberRuntimeLiveness({
teamName,
memberName,
@ -23194,24 +23340,21 @@ export class TeamProvisioningService {
boundaryMs: number;
}): boolean {
const { event, detail, teamName, memberName, runtimeMember, boundaryMs } = input;
if (event.type !== 'bootstrap_confirmed') {
return false;
}
if (typeof event.teamName === 'string' && event.teamName.trim() !== teamName) {
return false;
}
const source = getRuntimeBootstrapProofString(event, detail, 'source');
if (source !== BOOTSTRAP_RUNTIME_PROOF_SOURCE) {
return false;
}
const timestamp = typeof event.timestamp === 'string' ? event.timestamp : '';
const eventMs = Date.parse(timestamp);
if (Number.isFinite(boundaryMs) && (!Number.isFinite(eventMs) || eventMs < boundaryMs)) {
return false;
}
const expectedToken = runtimeMember?.bootstrapProofToken?.trim();
const eventToken = getRuntimeBootstrapProofString(event, detail, 'bootstrapProofToken');
if (expectedToken && eventToken !== expectedToken) {
if (
!validateBootstrapRuntimeProofEnvelope({
event,
detail,
expected: {
teamName,
boundaryMs,
proofToken: runtimeMember?.bootstrapProofToken?.trim(),
proofMode: runtimeMember?.bootstrapProofMode?.trim(),
contextHash: runtimeMember?.bootstrapContextHash?.trim(),
briefingHash: runtimeMember?.bootstrapBriefingHash?.trim(),
runId: runtimeMember?.bootstrapRunId?.trim(),
},
})
) {
return false;
}
const eventAgentName = typeof event.agentName === 'string' ? event.agentName.trim() : '';
@ -23245,7 +23388,7 @@ export class TeamProvisioningService {
let latest: string | null = null;
let latestMs = Number.NEGATIVE_INFINITY;
for (const event of events) {
const detail = parseRuntimeBootstrapProofDetail(event.detail);
const detail = parseBootstrapRuntimeProofDetail(event.detail);
if (
!this.isRuntimeBootstrapProofEventValid({
event,

View file

@ -0,0 +1,225 @@
export const LEGACY_MEMBER_BRIEFING_BOOTSTRAP_PROOF_SOURCE = 'member_briefing_tool_success';
export const NATIVE_APP_MANAGED_BOOTSTRAP_PROOF_SOURCE =
'native_app_managed_bootstrap_private_turn';
type BootstrapProofField =
| 'source'
| 'bootstrapProofToken'
| 'contextHash'
| 'briefingHash'
| 'runId';
export type BootstrapProofSource =
| typeof LEGACY_MEMBER_BRIEFING_BOOTSTRAP_PROOF_SOURCE
| typeof NATIVE_APP_MANAGED_BOOTSTRAP_PROOF_SOURCE;
export type BootstrapProofValidationFailureReason =
| 'wrong_event_type'
| 'wrong_team'
| 'stale_timestamp'
| 'unsupported_source'
| 'missing_team'
| 'missing_token'
| 'token_mismatch'
| 'missing_run_id'
| 'run_id_mismatch'
| 'missing_hash'
| 'hash_mismatch'
| 'wrong_proof_mode';
export type BootstrapProofValidationResult =
| { ok: true; source: BootstrapProofSource }
| { ok: false; reason: BootstrapProofValidationFailureReason; diagnostic: string };
export interface BootstrapRuntimeProofEventLike {
type?: unknown;
timestamp?: unknown;
teamName?: unknown;
source?: unknown;
bootstrapProofToken?: unknown;
contextHash?: unknown;
briefingHash?: unknown;
runId?: unknown;
detail?: unknown;
}
export interface BootstrapRuntimeProofExpected {
teamName: string;
boundaryMs: number;
proofToken?: string;
proofMode?: string;
contextHash?: string;
briefingHash?: string;
runId?: string;
}
export function parseBootstrapRuntimeProofDetail(detail: unknown): Record<string, unknown> {
if (typeof detail !== 'string' || detail.trim().length === 0) {
return {};
}
try {
const parsed = JSON.parse(detail) as unknown;
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: {};
} catch {
return {};
}
}
function readProofField(
event: BootstrapRuntimeProofEventLike,
detail: Record<string, unknown>,
field: BootstrapProofField
): string | undefined {
const direct = event[field];
if (typeof direct === 'string' && direct.trim().length > 0) {
return direct.trim();
}
const nested = detail[field];
return typeof nested === 'string' && nested.trim().length > 0 ? nested.trim() : undefined;
}
function getBootstrapProofSource(
event: BootstrapRuntimeProofEventLike,
detail: Record<string, unknown>
): BootstrapProofSource | undefined {
const source = readProofField(event, detail, 'source');
return source === LEGACY_MEMBER_BRIEFING_BOOTSTRAP_PROOF_SOURCE ||
source === NATIVE_APP_MANAGED_BOOTSTRAP_PROOF_SOURCE
? source
: undefined;
}
function reject(
reason: BootstrapProofValidationFailureReason,
diagnostic: string
): BootstrapProofValidationResult {
return { ok: false, reason, diagnostic };
}
function validateExpectedProofToken(input: {
event: BootstrapRuntimeProofEventLike;
detail: Record<string, unknown>;
expected: BootstrapRuntimeProofExpected;
}): BootstrapProofValidationResult | null {
if (!input.expected.proofToken) {
return null;
}
const eventToken = readProofField(input.event, input.detail, 'bootstrapProofToken');
if (!eventToken) {
return reject('missing_token', 'Bootstrap proof token is missing');
}
if (eventToken !== input.expected.proofToken) {
return reject('token_mismatch', 'Bootstrap proof token does not match the current attempt');
}
return null;
}
function validateLegacyMemberBriefingProof(input: {
event: BootstrapRuntimeProofEventLike;
detail: Record<string, unknown>;
expected: BootstrapRuntimeProofExpected;
}): BootstrapProofValidationResult {
const tokenFailure = validateExpectedProofToken(input);
return tokenFailure ?? { ok: true, source: LEGACY_MEMBER_BRIEFING_BOOTSTRAP_PROOF_SOURCE };
}
function validateNativeAppManagedProof(input: {
event: BootstrapRuntimeProofEventLike;
detail: Record<string, unknown>;
expected: BootstrapRuntimeProofExpected;
}): BootstrapProofValidationResult {
const eventTeamName = typeof input.event.teamName === 'string' ? input.event.teamName.trim() : '';
if (!eventTeamName) {
return reject('missing_team', 'Native app-managed bootstrap proof is missing teamName');
}
if (eventTeamName !== input.expected.teamName) {
return reject('wrong_team', 'Native app-managed bootstrap proof teamName does not match');
}
if (input.expected.proofMode !== 'native_app_managed_context') {
return reject('wrong_proof_mode', 'Native app-managed bootstrap proof mode is not expected');
}
const tokenFailure = validateExpectedProofToken(input);
if (tokenFailure) {
return tokenFailure;
}
if (!input.expected.proofToken) {
return reject('missing_token', 'Native app-managed bootstrap expected proof token is missing');
}
const runId = readProofField(input.event, input.detail, 'runId');
if (!input.expected.runId || !runId) {
return reject('missing_run_id', 'Native app-managed bootstrap runId is missing');
}
if (runId !== input.expected.runId) {
return reject('run_id_mismatch', 'Native app-managed bootstrap runId does not match');
}
const contextHash = readProofField(input.event, input.detail, 'contextHash');
const briefingHash = readProofField(input.event, input.detail, 'briefingHash');
if (
!input.expected.contextHash ||
!input.expected.briefingHash ||
!contextHash ||
!briefingHash
) {
return reject('missing_hash', 'Native app-managed bootstrap proof hash metadata is missing');
}
if (contextHash !== input.expected.contextHash || briefingHash !== input.expected.briefingHash) {
return reject('hash_mismatch', 'Native app-managed bootstrap proof hashes do not match');
}
return { ok: true, source: NATIVE_APP_MANAGED_BOOTSTRAP_PROOF_SOURCE };
}
const BOOTSTRAP_PROOF_VALIDATORS: Record<
BootstrapProofSource,
(input: {
event: BootstrapRuntimeProofEventLike;
detail: Record<string, unknown>;
expected: BootstrapRuntimeProofExpected;
}) => BootstrapProofValidationResult
> = {
[LEGACY_MEMBER_BRIEFING_BOOTSTRAP_PROOF_SOURCE]: validateLegacyMemberBriefingProof,
[NATIVE_APP_MANAGED_BOOTSTRAP_PROOF_SOURCE]: validateNativeAppManagedProof,
};
export function validateBootstrapRuntimeProofEnvelopeDetailed(input: {
event: BootstrapRuntimeProofEventLike;
detail?: Record<string, unknown>;
expected: BootstrapRuntimeProofExpected;
}): BootstrapProofValidationResult {
const { event, expected } = input;
const detail = input.detail ?? parseBootstrapRuntimeProofDetail(event.detail);
if (event.type !== 'bootstrap_confirmed') {
return reject('wrong_event_type', 'Runtime event is not bootstrap_confirmed');
}
if (typeof event.teamName === 'string' && event.teamName.trim() !== expected.teamName) {
return reject('wrong_team', 'Bootstrap proof teamName does not match');
}
const timestamp = typeof event.timestamp === 'string' ? event.timestamp : '';
const eventMs = Date.parse(timestamp);
if (
Number.isFinite(expected.boundaryMs) &&
(!Number.isFinite(eventMs) || eventMs < expected.boundaryMs)
) {
return reject('stale_timestamp', 'Bootstrap proof timestamp is older than the current attempt');
}
const source = getBootstrapProofSource(event, detail);
if (!source) {
return reject('unsupported_source', 'Bootstrap proof source is missing or unsupported');
}
return BOOTSTRAP_PROOF_VALIDATORS[source]({ event, detail, expected });
}
export function validateBootstrapRuntimeProofEnvelope(input: {
event: BootstrapRuntimeProofEventLike;
detail?: Record<string, unknown>;
expected: BootstrapRuntimeProofExpected;
}): boolean {
return validateBootstrapRuntimeProofEnvelopeDetailed(input).ok;
}

View file

@ -0,0 +1,186 @@
import * as agentTeamsControllerModule from 'agent-teams-controller';
import { createHash } from 'crypto';
import { getClaudeBasePath } from '@main/utils/pathDecoder';
import type { TeamCreateRequest, TeamProviderId } from '@shared/types';
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
const { createController } = agentTeamsControllerModule;
export interface NativeAppManagedBootstrapSpec {
schemaVersion: 1;
mode: 'startup_context_file';
contextText: string;
contextHash: string;
briefingHash: string;
generatedAt: string;
}
const MAX_NATIVE_BOOTSTRAP_BRIEFING_CHARS = 18_000;
const MAX_NATIVE_BOOTSTRAP_CONTEXT_CHARS = 24_000;
const MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS = 96_000;
export function isNativeAppManagedBootstrapProvider(providerId?: TeamProviderId): boolean {
return providerId == null || providerId === 'anthropic' || providerId === 'codex';
}
export function canonicalizeNativeBootstrapContextText(input: string): string {
return input
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.replace(/[ \t]+\n/g, '\n')
.trim();
}
export function hashNativeBootstrapText(input: string): string {
return createHash('sha256').update(canonicalizeNativeBootstrapContextText(input)).digest('hex');
}
function redactNativeBootstrapContextText(input: string): string {
return input
.replace(/sk-ant-[A-Za-z0-9_-]+/g, '[REDACTED_ANTHROPIC_API_KEY]')
.replace(/sk-[A-Za-z0-9_-]{20,}/g, '[REDACTED_API_KEY]')
.replace(/(ANTHROPIC_API_KEY|OPENAI_API_KEY|CODEX_API_KEY)=\S+/g, '$1=[REDACTED]')
.replace(/Bearer\s+[A-Za-z0-9._-]+/gi, 'Bearer [REDACTED]');
}
function boundText(input: string, maxChars: number): string {
const canonical = canonicalizeNativeBootstrapContextText(input);
if (canonical.length <= maxChars) {
return canonical;
}
return `${canonical.slice(0, maxChars)}\n[truncated native bootstrap context]`;
}
function buildContextText(params: {
teamName: string;
memberName: string;
providerId?: TeamProviderId;
cwd: string;
briefing: string;
}): string {
const briefing = boundText(
redactNativeBootstrapContextText(params.briefing),
MAX_NATIVE_BOOTSTRAP_BRIEFING_CHARS
);
return boundText(
[
'<agent_teams_native_bootstrap_context>',
`Team: ${params.teamName}`,
`Member: ${params.memberName}`,
`Provider: ${params.providerId ?? 'anthropic'}`,
`Project: ${params.cwd}`,
'',
'<member_briefing_context_data>',
briefing,
'</member_briefing_context_data>',
'</agent_teams_native_bootstrap_context>',
].join('\n'),
MAX_NATIVE_BOOTSTRAP_CONTEXT_CHARS
);
}
function buildLocalNativeMemberBriefing(params: {
teamName: string;
cwd: string;
providerId?: TeamProviderId;
member: TeamCreateRequest['members'][number];
unavailableReason: string;
}): string {
const member = params.member;
return [
`You are ${member.name}, a teammate in team ${params.teamName}.`,
`Provider: ${params.providerId ?? 'anthropic'}`,
`Project: ${member.cwd?.trim() || params.cwd}`,
member.role ? `Role: ${member.role}` : '',
member.workflow ? `Workflow: ${member.workflow}` : '',
member.model ? `Model: ${member.model}` : '',
member.effort ? `Effort: ${member.effort}` : '',
'',
'The app loaded this startup context from the current team launch request because canonical member_briefing metadata was not available yet.',
`Diagnostic: ${params.unavailableReason}`,
'',
'Startup rules:',
'- Treat yourself as unavailable until the private bootstrap turn succeeds.',
'- Do not call member_briefing for launch readiness in this flow.',
'- Use Agent Teams messaging/task tools only after launch readiness is confirmed.',
]
.filter((line) => line.length > 0)
.join('\n');
}
export async function buildNativeAppManagedBootstrapSpecs(params: {
teamName: string;
cwd: string;
members: TeamCreateRequest['members'];
}): Promise<Map<string, NativeAppManagedBootstrapSpec>> {
const controller = createController({
teamName: params.teamName,
claudeDir: getClaudeBasePath(),
allowUserMessageSender: false,
});
const result = new Map<string, NativeAppManagedBootstrapSpec>();
let totalContextChars = 0;
for (const member of params.members) {
const providerId = normalizeOptionalTeamProviderId(member.providerId) ?? 'anthropic';
if (!isNativeAppManagedBootstrapProvider(providerId)) {
continue;
}
let briefing: string;
try {
briefing = String(
await controller.tasks.memberBriefing(member.name, {
runtimeProvider: 'native',
includeActiveProcesses: false,
})
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes('Member not found in team metadata or inboxes')) {
throw error;
}
// In createTeam, the orchestrator's canonical config/inboxes may not
// exist until after the lead process runs. Fail-closed would break team
// creation, so use bounded request metadata while keeping readiness tied
// to the private bootstrap proof, never to this context load.
briefing = buildLocalNativeMemberBriefing({
teamName: params.teamName,
cwd: params.cwd,
providerId,
member,
unavailableReason: message,
});
}
const boundedBriefing = boundText(
redactNativeBootstrapContextText(briefing),
MAX_NATIVE_BOOTSTRAP_BRIEFING_CHARS
);
if (!boundedBriefing) {
throw new Error(`Native app-managed member briefing was empty for ${member.name}`);
}
const contextText = buildContextText({
teamName: params.teamName,
memberName: member.name,
providerId,
cwd: member.cwd?.trim() || params.cwd,
briefing: boundedBriefing,
});
totalContextChars += contextText.length;
if (totalContextChars > MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS) {
throw new Error('Native app-managed bootstrap context exceeds aggregate size budget');
}
result.set(member.name, {
schemaVersion: 1,
mode: 'startup_context_file',
contextText,
contextHash: hashNativeBootstrapText(contextText),
briefingHash: hashNativeBootstrapText(boundedBriefing),
generatedAt: new Date().toISOString(),
});
}
return result;
}

View file

@ -72,10 +72,10 @@ export async function resolveDesktopTeammateModeDecision(
};
}
const tmuxAvailable = await isTmuxAvailable();
await isTmuxAvailable();
return {
injectedTeammateMode: tmuxAvailable ? 'tmux' : null,
injectedTeammateMode: null,
forceProcessTeammates: true,
};
}

View file

@ -177,7 +177,8 @@ async function assertExecutable(filePath: string): Promise<void> {
}
async function writeTrustedClaudeConfig(configDir: string, projectPath: string): Promise<void> {
const normalizedProjectPath = path.normalize(projectPath).replace(/\\/g, '/');
const realProjectPath = await fs.realpath(projectPath).catch(() => projectPath);
const normalizedProjectPath = path.normalize(realProjectPath).replace(/\\/g, '/');
const approvedApiKeySuffix = process.env.ANTHROPIC_API_KEY?.trim().slice(-20);
const config: {
projects: Record<string, { hasTrustDialogAccepted: true }>;
@ -203,17 +204,28 @@ async function writeTrustedClaudeConfig(configDir: string, projectPath: string):
}
async function removeTempDirWithRetries(dirPath: string): Promise<void> {
const attempts = process.platform === 'win32' ? 20 : 1;
const attempts = process.platform === 'win32' ? 20 : 5;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
await fs.rm(dirPath, { recursive: true, force: true });
await fs.rm(dirPath, {
recursive: true,
force: true,
maxRetries: 3,
retryDelay: 200,
});
return;
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if ((code !== 'EBUSY' && code !== 'EPERM') || attempt === attempts) {
if (code === 'ENOENT') {
return;
}
if (
(code !== 'EBUSY' && code !== 'EPERM' && code !== 'ENOTEMPTY') ||
attempt === attempts
) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, 100));
await new Promise((resolve) => setTimeout(resolve, 200));
}
}
}

View file

@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import {
parseBootstrapRuntimeProofDetail,
validateBootstrapRuntimeProofEnvelope,
validateBootstrapRuntimeProofEnvelopeDetailed,
} from '../../../../src/main/services/team/bootstrap/BootstrapProofValidation';
describe('BootstrapProofValidation', () => {
const expected = {
teamName: 'native-proof-team',
boundaryMs: Date.parse('2026-05-01T10:00:00.000Z'),
proofToken: 'proof-token',
proofMode: 'native_app_managed_context',
runId: 'run-native-proof',
contextHash: 'a'.repeat(64),
briefingHash: 'b'.repeat(64),
};
it('accepts native app-managed proof only when team, token, run and hashes match', () => {
expect(
validateBootstrapRuntimeProofEnvelope({
event: {
type: 'bootstrap_confirmed',
timestamp: '2026-05-01T10:00:01.000Z',
teamName: expected.teamName,
source: 'native_app_managed_bootstrap_private_turn',
bootstrapProofToken: expected.proofToken,
runId: expected.runId,
contextHash: expected.contextHash,
briefingHash: expected.briefingHash,
},
expected,
})
).toBe(true);
});
it('rejects native app-managed proof without explicit team binding', () => {
const result = validateBootstrapRuntimeProofEnvelopeDetailed({
event: {
type: 'bootstrap_confirmed',
timestamp: '2026-05-01T10:00:01.000Z',
source: 'native_app_managed_bootstrap_private_turn',
bootstrapProofToken: expected.proofToken,
runId: expected.runId,
contextHash: expected.contextHash,
briefingHash: expected.briefingHash,
},
expected,
});
expect(result).toMatchObject({ ok: false, reason: 'missing_team' });
});
it('keeps legacy member_briefing proof compatible with missing teamName', () => {
expect(
validateBootstrapRuntimeProofEnvelope({
event: {
type: 'bootstrap_confirmed',
timestamp: '2026-05-01T10:00:01.000Z',
source: 'member_briefing_tool_success',
bootstrapProofToken: expected.proofToken,
},
detail: parseBootstrapRuntimeProofDetail(''),
expected: {
teamName: expected.teamName,
boundaryMs: expected.boundaryMs,
proofToken: expected.proofToken,
},
})
).toBe(true);
});
});

View file

@ -185,6 +185,7 @@ liveDescribe('Member work sync Claude Stop hook live e2e', () => {
teamName = `member-work-sync-claude-stop-${scenario.markerSuffix}-${startedAt}`;
const projectPath = path.join(tempDir, 'project');
await fs.mkdir(projectPath, { recursive: true });
await writeTrustedClaudeConfig(tempClaudeRoot, projectPath);
await fs.writeFile(
path.join(projectPath, 'README.md'),
'# Member work sync Claude Stop hook live e2e\n\nKeep this project intentionally tiny.\n',
@ -514,13 +515,28 @@ async function removeTempDirAfterLateShellWrites(tempDir: string): Promise<void>
// Claude Code can leave child shells that write ~/.zsh_history just after stopTeam cleanup.
// Bounded repeated passes keep live tests from leaving tiny recreated HOME directories behind.
for (let attempt = 0; attempt < 6; attempt += 1) {
await fs.rm(tempDir, { recursive: true, force: true });
await removeTempDirBestEffort(tempDir);
if (attempt < 5) {
await new Promise((resolve) => setTimeout(resolve, 1_000));
}
}
}
async function removeTempDirBestEffort(tempDir: string): Promise<void> {
try {
await fs.rm(tempDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
} catch (error) {
const code = typeof error === 'object' && error ? (error as { code?: unknown }).code : null;
if (code === 'ENOENT') {
return;
}
// Live Claude processes can briefly recreate files under the temp HOME while
// the test harness is tearing down. The repeated outer cleanup loop handles
// those late writes, so cleanup must not turn an already-finished live e2e
// assertion into a false failure.
}
}
async function cleanupScopedClaudeStopHookLiveTempDirs(): Promise<void> {
const tmpRoot = os.tmpdir();
for (let attempt = 0; attempt < 6; attempt += 1) {
@ -533,7 +549,7 @@ async function cleanupScopedClaudeStopHookLiveTempDirs(): Promise<void> {
await Promise.all(
entries
.filter((entry) => entry.isDirectory() && entry.name.startsWith('member-work-sync-claude-stop-live-'))
.map((entry) => fs.rm(path.join(tmpRoot, entry.name), { recursive: true, force: true }))
.map((entry) => removeTempDirBestEffort(path.join(tmpRoot, entry.name)))
);
if (attempt < 5) {
await new Promise((resolve) => setTimeout(resolve, 1_000));
@ -545,6 +561,33 @@ function hasLiveAnthropicApiKey(): boolean {
return Boolean(process.env.ANTHROPIC_API_KEY?.trim());
}
async function writeTrustedClaudeConfig(configDir: string, projectPath: string): Promise<void> {
const canonicalProjectPath = await fs.realpath(projectPath).catch(() => projectPath);
const normalizedProjectPath = path.normalize(canonicalProjectPath).replace(/\\/g, '/');
const approvedApiKeySuffix = process.env.ANTHROPIC_API_KEY?.trim().slice(-20);
const config: {
projects: Record<string, { hasTrustDialogAccepted: true }>;
customApiKeyResponses?: { approved: string[]; rejected: string[] };
} = {
projects: {
[normalizedProjectPath]: {
hasTrustDialogAccepted: true,
},
},
};
if (approvedApiKeySuffix) {
config.customApiKeyResponses = {
approved: [approvedApiKeySuffix],
rejected: [],
};
}
await fs.writeFile(
path.join(configDir, '.claude.json'),
`${JSON.stringify(config, null, 2)}\n`,
'utf8'
);
}
function resolveConnectedClaudeHome(previousHome: string | undefined): string {
const explicit = process.env.MEMBER_WORK_SYNC_CLAUDE_CONNECTED_HOME?.trim();
if (explicit) {

View file

@ -283,7 +283,8 @@ async function assertExecutable(filePath: string): Promise<void> {
}
async function writeTrustedClaudeConfig(configDir: string, projectPath: string): Promise<void> {
const normalizedProjectPath = path.normalize(projectPath).replace(/\\/g, '/');
const canonicalProjectPath = await fs.realpath(projectPath).catch(() => projectPath);
const normalizedProjectPath = path.normalize(canonicalProjectPath).replace(/\\/g, '/');
const approvedApiKeySuffix = process.env.ANTHROPIC_API_KEY?.trim().slice(-20);
const config: {
projects: Record<string, { hasTrustDialogAccepted: true }>;

View file

@ -0,0 +1,131 @@
import { mkdtemp, rm } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
buildNativeAppManagedBootstrapSpecs,
hashNativeBootstrapText,
} from '../../../../src/main/services/team/bootstrap/NativeAppManagedBootstrapContextBuilder';
import { TeamMembersMetaStore } from '../../../../src/main/services/team/TeamMembersMetaStore';
import { TeamMetaStore } from '../../../../src/main/services/team/TeamMetaStore';
import { setClaudeBasePathOverride } from '../../../../src/main/utils/pathDecoder';
describe('NativeAppManagedBootstrapContextBuilder', () => {
let tempClaudeRoot = '';
beforeEach(async () => {
tempClaudeRoot = await mkdtemp(join(tmpdir(), 'native-bootstrap-builder-'));
setClaudeBasePathOverride(tempClaudeRoot);
});
afterEach(async () => {
setClaudeBasePathOverride(null);
await rm(tempClaudeRoot, { recursive: true, force: true });
});
it('canonical hash normalizes line endings and trailing whitespace', () => {
expect(hashNativeBootstrapText('line 1\r\nline 2 \n')).toBe(
hashNativeBootstrapText('line 1\nline 2')
);
});
it('builds bounded redacted context for native providers and skips non-native providers', async () => {
await new TeamMetaStore().writeMeta('native-ready-team', {
cwd: '/tmp/workspace',
providerId: 'anthropic',
model: 'claude-opus-4-6',
createdAt: Date.now(),
});
await new TeamMembersMetaStore().writeMembers('native-ready-team', [
{
name: 'alice',
providerId: 'anthropic',
role: 'Reviewer ANTHROPIC_API_KEY=sk-ant-secret',
},
{
name: 'bob',
providerId: 'codex',
role: 'Developer Bearer secret-token',
},
{
name: 'zoe',
providerId: 'gemini',
role: 'Gemini member',
},
{
name: 'tom',
providerId: 'opencode',
role: 'OpenCode member',
},
]);
const specs = await buildNativeAppManagedBootstrapSpecs({
teamName: 'native-ready-team',
cwd: '/tmp/workspace',
members: [
{
name: 'alice',
providerId: 'anthropic',
role: 'Reviewer ANTHROPIC_API_KEY=sk-ant-secret',
},
{
name: 'bob',
providerId: 'codex',
role: 'Developer Bearer secret-token',
},
{
name: 'zoe',
providerId: 'gemini',
role: 'Gemini member',
},
{
name: 'tom',
providerId: 'opencode',
role: 'OpenCode member',
},
],
});
expect([...specs.keys()].sort()).toEqual(['alice', 'bob']);
const alice = specs.get('alice');
const bob = specs.get('bob');
expect(alice?.contextText).toContain('<agent_teams_native_bootstrap_context>');
expect(alice?.contextText).not.toContain('sk-ant-secret');
expect(alice?.contextText).toContain('ANTHROPIC_API_KEY=[REDACTED]');
expect(bob?.contextText).not.toContain('Bearer secret-token');
expect(bob?.contextText).toContain('Bearer [REDACTED]');
expect(alice?.contextHash).toBe(hashNativeBootstrapText(alice?.contextText ?? ''));
});
it('fails closed when aggregate native context budget is exceeded', async () => {
const hugeRole = 'x'.repeat(40_000);
await new TeamMetaStore().writeMeta('large-native-team', {
cwd: '/tmp/workspace',
providerId: 'anthropic',
model: 'claude-opus-4-6',
createdAt: Date.now(),
});
await new TeamMembersMetaStore().writeMembers(
'large-native-team',
Array.from({ length: 8 }, (_, index) => ({
name: `member-${index}`,
providerId: 'anthropic' as const,
role: hugeRole,
}))
);
await expect(
buildNativeAppManagedBootstrapSpecs({
teamName: 'large-native-team',
cwd: '/tmp/workspace',
members: Array.from({ length: 8 }, (_, index) => ({
name: `member-${index}`,
providerId: 'anthropic' as const,
role: hugeRole,
})),
})
).rejects.toThrow(/aggregate size budget/);
});
});

View file

@ -12779,6 +12779,167 @@ describe('TeamProvisioningService', () => {
});
});
it('heals terminal bootstrap-state failures when native app-managed proof matches token and hashes', async () => {
allowConsoleLogs();
const teamName = 'zz-unit-bootstrap-state-native-runtime-proof-heals';
const leadSessionId = 'lead-session';
const projectPath = '/Users/test/proj';
const acceptedAt = new Date(Date.now() - 90_000).toISOString();
const proofAt = new Date(Date.now() - 60_000).toISOString();
const failureAt = new Date(Date.now() - 30_000).toISOString();
const proofToken = 'proof-token-jack-native';
const bootstrapRunId = 'run-native-proof';
const contextHash = 'a'.repeat(64);
const briefingHash = 'b'.repeat(64);
const runtimeEventsPath = path.join(tempTeamsBase, teamName, 'runtime', 'jack.runtime.jsonl');
writeLaunchConfig(teamName, projectPath, leadSessionId, ['jack']);
const configPath = path.join(tempTeamsBase, teamName, 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8')) as {
members: Array<Record<string, unknown>>;
};
config.members = config.members.map((member) =>
member.name === 'jack'
? {
...member,
agentId: `jack@${teamName}`,
bootstrapExpectedAfter: acceptedAt,
bootstrapProofToken: proofToken,
bootstrapRunId,
bootstrapProofMode: 'native_app_managed_context',
bootstrapContextHash: contextHash,
bootstrapBriefingHash: briefingHash,
bootstrapRuntimeEventsPath: runtimeEventsPath,
}
: member
);
fs.writeFileSync(configPath, JSON.stringify(config), 'utf8');
writeBootstrapState(
teamName,
[
{
name: 'jack',
status: 'failed',
lastAttemptAt: Date.parse(acceptedAt),
lastObservedAt: Date.parse(failureAt),
failureReason: 'Teammate was registered but did not bootstrap-confirm before timeout.',
},
],
failureAt
);
fs.mkdirSync(path.dirname(runtimeEventsPath), { recursive: true });
fs.writeFileSync(
runtimeEventsPath,
`${JSON.stringify({
version: 1,
type: 'bootstrap_confirmed',
timestamp: proofAt,
pid: 1234,
teamName,
agentName: 'jack',
agentId: `jack@${teamName}`,
runId: bootstrapRunId,
source: 'native_app_managed_bootstrap_private_turn',
bootstrapProofToken: proofToken,
contextHash,
briefingHash,
})}\n`,
'utf8'
);
const svc = new TeamProvisioningService();
const result = await svc.getMemberSpawnStatuses(teamName);
expect(result.teamLaunchState).toBe('clean_success');
expect(result.statuses.jack).toMatchObject({
status: 'online',
launchState: 'confirmed_alive',
bootstrapConfirmed: true,
runtimeAlive: true,
hardFailure: false,
error: undefined,
});
});
it('does not heal terminal bootstrap-state failures from native app-managed proof with mismatched hashes', async () => {
allowConsoleLogs();
const teamName = 'zz-unit-bootstrap-state-native-runtime-proof-hash-mismatch';
const leadSessionId = 'lead-session';
const projectPath = '/Users/test/proj';
const acceptedAt = new Date(Date.now() - 90_000).toISOString();
const proofAt = new Date(Date.now() - 60_000).toISOString();
const failureAt = new Date(Date.now() - 30_000).toISOString();
const proofToken = 'proof-token-jack-native';
const bootstrapRunId = 'run-native-proof';
const runtimeEventsPath = path.join(tempTeamsBase, teamName, 'runtime', 'jack.runtime.jsonl');
writeLaunchConfig(teamName, projectPath, leadSessionId, ['jack']);
const configPath = path.join(tempTeamsBase, teamName, 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8')) as {
members: Array<Record<string, unknown>>;
};
config.members = config.members.map((member) =>
member.name === 'jack'
? {
...member,
agentId: `jack@${teamName}`,
bootstrapExpectedAfter: acceptedAt,
bootstrapProofToken: proofToken,
bootstrapRunId,
bootstrapProofMode: 'native_app_managed_context',
bootstrapContextHash: 'a'.repeat(64),
bootstrapBriefingHash: 'b'.repeat(64),
bootstrapRuntimeEventsPath: runtimeEventsPath,
}
: member
);
fs.writeFileSync(configPath, JSON.stringify(config), 'utf8');
writeBootstrapState(
teamName,
[
{
name: 'jack',
status: 'failed',
lastAttemptAt: Date.parse(acceptedAt),
lastObservedAt: Date.parse(failureAt),
failureReason: 'Teammate was registered but did not bootstrap-confirm before timeout.',
},
],
failureAt
);
fs.mkdirSync(path.dirname(runtimeEventsPath), { recursive: true });
fs.writeFileSync(
runtimeEventsPath,
`${JSON.stringify({
version: 1,
type: 'bootstrap_confirmed',
timestamp: proofAt,
pid: 1234,
teamName,
agentName: 'jack',
agentId: `jack@${teamName}`,
runId: bootstrapRunId,
source: 'native_app_managed_bootstrap_private_turn',
bootstrapProofToken: proofToken,
contextHash: 'c'.repeat(64),
briefingHash: 'b'.repeat(64),
})}\n`,
'utf8'
);
const svc = new TeamProvisioningService();
const result = await svc.getMemberSpawnStatuses(teamName);
expect(result.teamLaunchState).toBe('partial_failure');
expect(result.statuses.jack).toMatchObject({
status: 'error',
launchState: 'failed_to_start',
bootstrapConfirmed: false,
runtimeAlive: false,
hardFailure: true,
});
});
it('does not heal bootstrap-state failures from stale runtime proof before spawn acceptance', async () => {
allowConsoleLogs();
const teamName = 'zz-unit-bootstrap-state-stale-runtime-proof-ignored';

View file

@ -12,7 +12,7 @@ describe('runtimeTeammateMode', () => {
vi.clearAllMocks();
});
it('enables process teammates in auto mode when tmux runtime is ready', async () => {
it('does not inject tmux mode in default desktop launch when tmux runtime is ready', async () => {
mockIsTmuxRuntimeReadyForCurrentPlatform.mockResolvedValue(true);
const { resolveDesktopTeammateModeDecision } =
await import('@main/services/team/runtimeTeammateMode');
@ -20,7 +20,7 @@ describe('runtimeTeammateMode', () => {
const decision = await resolveDesktopTeammateModeDecision(undefined);
expect(decision.forceProcessTeammates).toBe(true);
expect(decision.injectedTeammateMode).toBe('tmux');
expect(decision.injectedTeammateMode).toBeNull();
});
it('uses native process teammates when tmux runtime is not ready', async () => {
@ -97,6 +97,6 @@ describe('runtimeTeammateMode', () => {
expect(firstDecision.forceProcessTeammates).toBe(true);
expect(firstDecision.injectedTeammateMode).toBeNull();
expect(secondDecision.forceProcessTeammates).toBe(true);
expect(secondDecision.injectedTeammateMode).toBe('tmux');
expect(secondDecision.injectedTeammateMode).toBeNull();
});
});