negotium 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-helpers.js +3117 -2915
- package/dist/agent-helpers.js.map +38 -37
- package/dist/background-bash.js.map +1 -1
- package/dist/browser-runtime.js +137 -21
- package/dist/browser-runtime.js.map +10 -9
- package/dist/{chunk-kc5kmr95.js → chunk-1s9ryz8g.js} +1195 -21
- package/dist/chunk-1s9ryz8g.js.map +35 -0
- package/dist/hosted-agent.js +60 -48
- package/dist/hosted-agent.js.map +10 -10
- package/dist/main.js +3038 -3357
- package/dist/main.js.map +35 -35
- package/dist/mcp-factories.js +6227 -6021
- package/dist/mcp-factories.js.map +40 -39
- package/dist/prompts.js.map +1 -1
- package/dist/query-runtime.js +22 -2
- package/dist/query-runtime.js.map +4 -4
- package/dist/registry.js +169 -213
- package/dist/registry.js.map +8 -9
- package/dist/rollout.js +1 -1
- package/dist/runtime/src/agents/codex-app-server.ts +136 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +1 -1
- package/dist/runtime/src/agents/codex-provider.ts +21 -11
- package/dist/runtime/src/agents/codex-registry.ts +9 -17
- package/dist/runtime/src/agents/contracts.ts +2 -0
- package/dist/runtime/src/agents/execution-host.ts +6 -0
- package/dist/runtime/src/agents/fork.ts +24 -18
- package/dist/runtime/src/agents/idle-archiver.ts +7 -2
- package/dist/runtime/src/agents/index.ts +2 -1
- package/dist/runtime/src/agents/maestro-registry.ts +26 -7
- package/dist/runtime/src/agents/memory-archive-policy.ts +20 -0
- package/dist/runtime/src/agents/rollout/codex.ts +6 -5
- package/dist/runtime/src/agents/rollout/shared.ts +3 -5
- package/dist/runtime/src/mcp/factories/session-comm.ts +12 -0
- package/dist/runtime/src/mcp/session-comm/default-host.ts +16 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +25 -0
- package/dist/runtime/src/platform/config.ts +4 -1
- package/dist/runtime/src/platform/playwright/manager.ts +57 -1
- package/dist/runtime/src/platform/playwright/profile-management.ts +33 -0
- package/dist/runtime/src/platform/playwright/public-runtime.ts +5 -0
- package/dist/runtime/src/platform/playwright/vault-broker.ts +9 -10
- package/dist/runtime/src/storage/browser-profiles.ts +35 -10
- package/dist/runtime/src/storage/storage-host.ts +31 -5
- package/dist/runtime/src/storage/vault.ts +30 -2
- package/dist/runtime/src/topics/derive.ts +38 -2
- package/dist/runtime/src/topics/lifecycle.ts +12 -2
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js +22 -2
- package/dist/runtime-helpers.js.map +4 -4
- package/dist/storage.js +22 -2
- package/dist/storage.js.map +4 -4
- package/dist/types/packages/core/src/agents/codex-app-server.d.ts +13 -0
- package/dist/types/packages/core/src/agents/codex-native-multi-agent.d.ts +1 -0
- package/dist/types/packages/core/src/agents/contracts.d.ts +2 -0
- package/dist/types/packages/core/src/agents/execution-host.d.ts +2 -0
- package/dist/types/packages/core/src/agents/fork.d.ts +8 -7
- package/dist/types/packages/core/src/agents/memory-archive-policy.d.ts +10 -0
- package/dist/types/packages/core/src/agents/rollout/codex.d.ts +4 -2
- package/dist/types/packages/core/src/mcp/factories/session-comm.d.ts +1 -0
- package/dist/types/packages/core/src/platform/playwright/manager.d.ts +15 -0
- package/dist/types/packages/core/src/platform/playwright/profile-management.d.ts +8 -0
- package/dist/types/packages/core/src/platform/playwright/public-runtime.d.ts +2 -1
- package/dist/types/packages/core/src/storage/browser-profiles.d.ts +3 -1
- package/dist/types/packages/core/src/storage/storage-host.d.ts +1 -0
- package/dist/types/packages/core/src/storage/vault.d.ts +8 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/vault.js +19 -3
- package/dist/vault.js.map +4 -4
- package/package.json +2 -2
- package/dist/chunk-kc5kmr95.js.map +0 -19
- package/dist/runtime/src/migration/single-user.ts +0 -805
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, unlinkSync } from "node:fs";
|
|
2
|
+
import type { ForkRegistryResult } from "#agents/contracts";
|
|
2
3
|
import { getRegistryOperations } from "#agents/registry";
|
|
3
4
|
import { logger } from "#platform/logger";
|
|
4
5
|
import type { AgentKind, EffortLevel } from "#types";
|
|
@@ -7,9 +8,10 @@ import type { AgentKind, EffortLevel } from "#types";
|
|
|
7
8
|
* Agent-aware fork primitives.
|
|
8
9
|
*
|
|
9
10
|
* Strategy lives in each agent's registry (`*-registry.ts:forkSession`).
|
|
10
|
-
* Claude uses the SDK's native `forkSession
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Claude uses the SDK's native `forkSession`, Codex uses App Server's native
|
|
12
|
+
* `thread/fork`, and Maestro uses the SDK's `forkSessionAt`. Providers without
|
|
13
|
+
* a native branch primitive synthesize from the provider-agnostic conversation
|
|
14
|
+
* log. All surfaces converge on ForkHandle.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
export interface ForkAgentSessionOptions {
|
|
@@ -35,13 +37,15 @@ export interface ForkHandle {
|
|
|
35
37
|
forkId: string;
|
|
36
38
|
/** Absolute path of the rollout file to remove on cleanup. */
|
|
37
39
|
rolloutPath: string;
|
|
40
|
+
/** Additional provider-owned files removed with the primary rollout. */
|
|
41
|
+
cleanupPaths?: string[];
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
export interface AgentForkHost {
|
|
41
45
|
forkSession(
|
|
42
46
|
agent: AgentKind,
|
|
43
47
|
options: Omit<ForkAgentSessionOptions, "agent">,
|
|
44
|
-
): Promise<
|
|
48
|
+
): Promise<ForkRegistryResult>;
|
|
45
49
|
exists(path: string): boolean;
|
|
46
50
|
unlink(path: string): void;
|
|
47
51
|
warn(details: Record<string, unknown>, message: string): void;
|
|
@@ -56,22 +60,24 @@ export function createAgentForkHelpers(host: AgentForkHost): AgentForkHelpers {
|
|
|
56
60
|
return {
|
|
57
61
|
async forkAgentSession(options) {
|
|
58
62
|
const { agent, ...forkOptions } = options;
|
|
59
|
-
const { forkId, rolloutPath } = await host.forkSession(agent, forkOptions);
|
|
60
|
-
return { agent, forkId, rolloutPath };
|
|
63
|
+
const { forkId, rolloutPath, cleanupPaths } = await host.forkSession(agent, forkOptions);
|
|
64
|
+
return { agent, forkId, rolloutPath, ...(cleanupPaths?.length ? { cleanupPaths } : {}) };
|
|
61
65
|
},
|
|
62
66
|
cleanupAgentFork(handle) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
for (const path of [handle.rolloutPath, ...(handle.cleanupPaths ?? [])]) {
|
|
68
|
+
try {
|
|
69
|
+
if (host.exists(path)) host.unlink(path);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
host.warn(
|
|
72
|
+
{
|
|
73
|
+
error,
|
|
74
|
+
agent: handle.agent,
|
|
75
|
+
forkId: handle.forkId,
|
|
76
|
+
path,
|
|
77
|
+
},
|
|
78
|
+
"cleanupAgentFork: failed to remove rollout",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
75
81
|
}
|
|
76
82
|
},
|
|
77
83
|
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { runArchiverTurn } from "#agents/archiver";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
countMemoryArchiveExchanges,
|
|
4
|
+
meetsMemoryArchiveExchangeThreshold,
|
|
5
|
+
} from "#agents/memory-archive-policy";
|
|
3
6
|
import { logger } from "#platform/logger";
|
|
4
7
|
import { getRoomQuery } from "#query/active-rooms";
|
|
5
8
|
import { getMessagesForTopicAfterRowid } from "#storage/api-messages";
|
|
@@ -141,7 +144,9 @@ export function archiveActiveTopicForMemory(
|
|
|
141
144
|
return null;
|
|
142
145
|
}
|
|
143
146
|
const exchangeCount = countMemoryArchiveExchanges(pending);
|
|
144
|
-
skipMemoryTurn =
|
|
147
|
+
skipMemoryTurn =
|
|
148
|
+
options.minExchanges !== undefined &&
|
|
149
|
+
!meetsMemoryArchiveExchangeThreshold(exchangeCount, options.minExchanges, topic);
|
|
145
150
|
const archived = (options.archiveMessages ?? archiveTopicMessages)(topicId, topic.title, {
|
|
146
151
|
afterRowid,
|
|
147
152
|
reason: options.reason,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { hostedCodexHomePath } from "#agents/execution-host";
|
|
4
5
|
import { getRegistryOperations } from "#agents/registry";
|
|
5
6
|
import { encodeClaudeCwd } from "#agents/rollout/claude";
|
|
6
7
|
import { resolveTaskEventScope, withTaskSnapshots } from "#agents/task-events";
|
|
@@ -108,7 +109,7 @@ export async function resolveSessionFileMissing(
|
|
|
108
109
|
return !existsSync(path);
|
|
109
110
|
}
|
|
110
111
|
case "codex": {
|
|
111
|
-
const sessionsDir = join(
|
|
112
|
+
const sessionsDir = join(hostedCodexHomePath(), "sessions");
|
|
112
113
|
const glob = new Bun.Glob(`**/rollout-*-${sessionId}.jsonl`);
|
|
113
114
|
for await (const _rel of glob.scan({ cwd: sessionsDir, onlyFiles: true })) {
|
|
114
115
|
return false; // found at least one match → session exists
|
|
@@ -5,7 +5,6 @@ import { join, resolve } from "node:path";
|
|
|
5
5
|
import type { AgentRegistry, AgentRegistryOperations } from "#agents/contracts";
|
|
6
6
|
import { assertUuidLike, ensureCwdExists, extractChatPairs } from "#agents/rollout/shared";
|
|
7
7
|
import { logger } from "#platform/logger";
|
|
8
|
-
import { readConversation } from "#storage/conversations";
|
|
9
8
|
import { MAESTRO_EFFORT_VALUES } from "#types";
|
|
10
9
|
|
|
11
10
|
const ALIAS_MAP: Record<string, string> = {
|
|
@@ -80,7 +79,7 @@ function writeRollout(options: Parameters<AgentRegistryOperations["writeRollout"
|
|
|
80
79
|
version: 1,
|
|
81
80
|
cwd: options.cwd,
|
|
82
81
|
createdAt: existingCreatedAt(path) ?? new Date().toISOString(),
|
|
83
|
-
sdkVersion: "0.
|
|
82
|
+
sdkVersion: "0.2.0",
|
|
84
83
|
},
|
|
85
84
|
},
|
|
86
85
|
...messages,
|
|
@@ -96,13 +95,33 @@ export const maestroRegistryOperations: AgentRegistryOperations = {
|
|
|
96
95
|
return writeRollout(options);
|
|
97
96
|
},
|
|
98
97
|
async forkSession(options) {
|
|
99
|
-
|
|
98
|
+
// Fork the full raw history while preserving the compacted working view.
|
|
99
|
+
// A compacted parent that cannot copy its active projection is not a usable
|
|
100
|
+
// cache-preserving fork, so let the caller fall back to bounded synthesis.
|
|
101
|
+
const { deleteMaestroSession, forkSessionAt, loadRawMaestroSession } = await import(
|
|
102
|
+
"maestro-agent-sdk"
|
|
103
|
+
);
|
|
104
|
+
const parentMessages = loadRawMaestroSession(options.parentSessionId);
|
|
105
|
+
if (!parentMessages) {
|
|
106
|
+
throw new Error(`Maestro parent session not found: ${options.parentSessionId}`);
|
|
107
|
+
}
|
|
108
|
+
const parentHasActiveProjection = existsSync(maestroActiveSessionPath(options.parentSessionId));
|
|
109
|
+
const fork = forkSessionAt({
|
|
110
|
+
parentSessionId: options.parentSessionId,
|
|
111
|
+
messageIndex: parentMessages.length,
|
|
100
112
|
cwd: options.cwd,
|
|
101
|
-
|
|
102
|
-
...(options.model ? { model: options.model } : {}),
|
|
103
|
-
...(options.effort ? { effort: options.effort } : {}),
|
|
113
|
+
userId: String(options.userId),
|
|
104
114
|
});
|
|
105
|
-
|
|
115
|
+
if (parentHasActiveProjection && !fork.activeProjectionForked) {
|
|
116
|
+
deleteMaestroSession(fork.sessionId);
|
|
117
|
+
throw new Error(`Maestro active projection could not be forked: ${options.parentSessionId}`);
|
|
118
|
+
}
|
|
119
|
+
const activePath = maestroActiveSessionPath(fork.sessionId);
|
|
120
|
+
return {
|
|
121
|
+
forkId: fork.sessionId,
|
|
122
|
+
rolloutPath: fork.rolloutPath,
|
|
123
|
+
...(existsSync(activePath) ? { cleanupPaths: [activePath] } : {}),
|
|
124
|
+
};
|
|
106
125
|
},
|
|
107
126
|
async cleanupRollouts(options) {
|
|
108
127
|
// Keep the SDK off the daemon startup path, but use its canonical cleanup
|
|
@@ -12,6 +12,26 @@ export interface MemoryArchiveMessage {
|
|
|
12
12
|
*/
|
|
13
13
|
export const MIN_MEMORY_ARCHIVE_EXCHANGES = 6;
|
|
14
14
|
|
|
15
|
+
export interface MemoryArchiveTopicContext {
|
|
16
|
+
isSubagent?: boolean;
|
|
17
|
+
memoryTopicId?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Explicit-memory subagents are usually short, focused turns. Persist one
|
|
22
|
+
* completed exchange back to their selected memory topic, while retaining the
|
|
23
|
+
* normal threshold for regular topics and incomplete/failed turns.
|
|
24
|
+
*/
|
|
25
|
+
export function meetsMemoryArchiveExchangeThreshold(
|
|
26
|
+
exchangeCount: number,
|
|
27
|
+
minExchanges: number,
|
|
28
|
+
topic: MemoryArchiveTopicContext,
|
|
29
|
+
): boolean {
|
|
30
|
+
if (exchangeCount <= 0) return false;
|
|
31
|
+
if (topic.isSubagent && topic.memoryTopicId) return true;
|
|
32
|
+
return exchangeCount >= minExchanges;
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
/** Count completed conversational exchanges while ignoring system/tool/card noise. */
|
|
16
36
|
export function countMemoryArchiveExchanges(rows: readonly MemoryArchiveMessage[]): number {
|
|
17
37
|
let waitingForAssistant = false;
|
|
@@ -10,14 +10,15 @@
|
|
|
10
10
|
* synthesized `response_item`/`event_msg` pairs per turn.
|
|
11
11
|
*
|
|
12
12
|
* Tool history is folded into assistant text as `[Tool: ...]` annotations
|
|
13
|
-
* (see shared.ts:extractChatPairs)
|
|
14
|
-
*
|
|
13
|
+
* (see shared.ts:extractChatPairs). Native same-agent forks use App Server's
|
|
14
|
+
* `thread/fork`; this encoder remains the fallback and cross-agent bridge,
|
|
15
|
+
* where synthetic tool_use IDs are too fragile to reconstruct structurally.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import { randomBytes } from "node:crypto";
|
|
18
19
|
import { existsSync, readFileSync, realpathSync, statSync, unlinkSync } from "node:fs";
|
|
19
|
-
import { homedir } from "node:os";
|
|
20
20
|
import { basename, dirname, join, resolve } from "node:path";
|
|
21
|
+
import { hostedCodexHomePath } from "#agents/execution-host";
|
|
21
22
|
import {
|
|
22
23
|
assertUuidLike,
|
|
23
24
|
type ChatPair,
|
|
@@ -40,7 +41,7 @@ interface CodexShell {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
function codexSessionsDir(): string {
|
|
43
|
-
return join(
|
|
44
|
+
return join(hostedCodexHomePath(), "sessions");
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
let _shellCache: CodexShell | null = null;
|
|
@@ -420,7 +421,7 @@ export function migrateCodexRolloutNativeMultiAgentMetadata(threadId: string): b
|
|
|
420
421
|
}
|
|
421
422
|
}
|
|
422
423
|
|
|
423
|
-
function latestCodexRolloutPath(threadId: string): string | undefined {
|
|
424
|
+
export function latestCodexRolloutPath(threadId: string): string | undefined {
|
|
424
425
|
const sessionsDir = codexSessionsDir();
|
|
425
426
|
const candidates: string[] = [];
|
|
426
427
|
const buckets = candidateDateBuckets(threadId);
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { mkdirSync } from "node:fs";
|
|
11
|
-
import {
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { resolve } from "node:path";
|
|
13
12
|
import {
|
|
14
13
|
DM_WORKSPACE_DIR,
|
|
14
|
+
PROJECT_ROOT,
|
|
15
15
|
SESSION_WORKSPACE_DIR,
|
|
16
16
|
TOPIC_WORKSPACE_DIR,
|
|
17
17
|
WORKSPACE_DIR,
|
|
@@ -20,8 +20,6 @@ import { logger } from "#platform/logger";
|
|
|
20
20
|
import { renderUserPromptBatch } from "#runtime/user-turn-envelope";
|
|
21
21
|
import type { ConversationEntry } from "#storage/conversations";
|
|
22
22
|
|
|
23
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
-
|
|
25
23
|
export interface RolloutHostOptions {
|
|
26
24
|
/** Absolute workspace roots under which synthetic provider sessions may be written. */
|
|
27
25
|
workspaceRoots?: readonly string[];
|
|
@@ -69,7 +67,7 @@ export function configureRolloutHost(options: RolloutHostOptions): () => void {
|
|
|
69
67
|
* non-conversational entries from the resulting rollout file, and update
|
|
70
68
|
* `src/agents/fixtures/{codex-shell,claude-attachments}.jsonl`.
|
|
71
69
|
*/
|
|
72
|
-
export let FIXTURES_DIR =
|
|
70
|
+
export let FIXTURES_DIR = resolve(PROJECT_ROOT, "src", "agents", "fixtures");
|
|
73
71
|
|
|
74
72
|
/** Deep-clone to avoid mutating cached fixture objects. */
|
|
75
73
|
export function clone<T>(obj: T): T {
|
|
@@ -22,6 +22,10 @@ export interface SessionCommMcpHost {
|
|
|
22
22
|
context: SessionCommContext,
|
|
23
23
|
profile: string,
|
|
24
24
|
): MaybePromise<SessionCommMcpResult>;
|
|
25
|
+
deleteBrowserProfile?(
|
|
26
|
+
context: SessionCommContext,
|
|
27
|
+
profile: string,
|
|
28
|
+
): MaybePromise<SessionCommMcpResult>;
|
|
25
29
|
peekSession(context: SessionCommContext): MaybePromise<SessionCommMcpResult>;
|
|
26
30
|
setDescription(
|
|
27
31
|
context: SessionCommContext,
|
|
@@ -90,6 +94,14 @@ export function createSessionCommMcpServer(
|
|
|
90
94
|
async ({ profile }) => host.setBrowserProfile!(context, profile),
|
|
91
95
|
);
|
|
92
96
|
}
|
|
97
|
+
if (host.deleteBrowserProfile) {
|
|
98
|
+
server.tool(
|
|
99
|
+
"delete_browser_profile",
|
|
100
|
+
"Delete an unused named browser profile and its on-disk data. The default or an assigned profile cannot be deleted.",
|
|
101
|
+
{ profile: z.string() },
|
|
102
|
+
async ({ profile }) => host.deleteBrowserProfile!(context, profile),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
93
105
|
server.tool(
|
|
94
106
|
"peek_session",
|
|
95
107
|
"Inspect running and idle sessions, including pending ask_session calls.",
|
|
@@ -11,6 +11,7 @@ import { ACTIVE_QUERY_STALE_MS, MAX_TELL_DEPTH, USERS_LOG_DIR } from "#platform/
|
|
|
11
11
|
import { appendJsonlEntry, readJsonFile } from "#platform/jsonl";
|
|
12
12
|
import { OPTIONAL_FORUM_MCP_SERVERS, REQUIRED_FORUM_MCP_SERVERS } from "#platform/mcp-config";
|
|
13
13
|
import { closeBrowserOwnerTabs } from "#platform/playwright/manager";
|
|
14
|
+
import { deleteManagedBrowserProfile } from "#platform/playwright/profile-management";
|
|
14
15
|
import { sessionInboxPath } from "#query/session-inbox-path";
|
|
15
16
|
import { sanitizeId } from "#security/sanitize";
|
|
16
17
|
import { getApiTopicConfig, setApiTopicConfig } from "#storage/api-topic-config";
|
|
@@ -199,6 +200,21 @@ export function createDefaultSessionCommMcpHost(): SessionCommMcpHost {
|
|
|
199
200
|
}
|
|
200
201
|
},
|
|
201
202
|
|
|
203
|
+
async deleteBrowserProfile(context, profile) {
|
|
204
|
+
const topic = currentTopic(context);
|
|
205
|
+
if (!topic) return error("Error: No current API topic.");
|
|
206
|
+
if (!isTopicBrowserProfileOwner(topic.id, context.userId)) {
|
|
207
|
+
return error("Error: Only the topic owner can delete its browser profiles.");
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const ownerId = getBrowserProfileOwner(topic.id, context.userId);
|
|
211
|
+
const result = await deleteManagedBrowserProfile(ownerId, profile);
|
|
212
|
+
return ok(JSON.stringify(result, null, 2));
|
|
213
|
+
} catch (err) {
|
|
214
|
+
return error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
|
|
202
218
|
peekSession(context) {
|
|
203
219
|
const targets = targetCatalog(context).listTargets();
|
|
204
220
|
const running: string[] = [];
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
REQUIRED_FORUM_MCP_SERVERS,
|
|
13
13
|
} from "#platform/mcp-config";
|
|
14
14
|
import { closeBrowserOwnerTabs } from "#platform/playwright/manager";
|
|
15
|
+
import { deleteManagedBrowserProfile } from "#platform/playwright/profile-management";
|
|
15
16
|
import { sanitizeId } from "#security/sanitize";
|
|
16
17
|
import { getTopic } from "#storage/api-topics";
|
|
17
18
|
import {
|
|
@@ -318,6 +319,30 @@ server.tool(
|
|
|
318
319
|
},
|
|
319
320
|
);
|
|
320
321
|
|
|
322
|
+
server.tool(
|
|
323
|
+
"delete_browser_profile",
|
|
324
|
+
"Delete an unused named browser profile and its on-disk data. The default or an assigned profile cannot be deleted.",
|
|
325
|
+
{
|
|
326
|
+
profile: z
|
|
327
|
+
.string()
|
|
328
|
+
.describe("Unused named profile to delete; the default profile cannot be deleted"),
|
|
329
|
+
},
|
|
330
|
+
async ({ profile }) => {
|
|
331
|
+
const topicId = currentApiTopicId();
|
|
332
|
+
if (!topicId || !userId) return mcpError("Error: No current API topic.");
|
|
333
|
+
if (!isTopicBrowserProfileOwner(topicId, userId)) {
|
|
334
|
+
return mcpError("Error: Only the topic owner can delete its browser profiles.");
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const ownerId = getBrowserProfileOwner(topicId, userId);
|
|
338
|
+
const result = await deleteManagedBrowserProfile(ownerId, profile);
|
|
339
|
+
return mcpOk(JSON.stringify(result, null, 2));
|
|
340
|
+
} catch (err) {
|
|
341
|
+
return mcpError(`Error: ${errMsg(err)}`);
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
|
|
321
346
|
// --- always-available session inspection / self-config ---
|
|
322
347
|
|
|
323
348
|
server.tool(
|
|
@@ -453,7 +453,10 @@ export const MAX_TELL_DEPTH =
|
|
|
453
453
|
/** Codex CLI auth file. 호출 시점에 env를 읽는다 — 테스트가 런타임에
|
|
454
454
|
* NEGOTIUM_CODEX_AUTH_FILE을 바꾸므로 모듈 로드 상수로 만들면 안 된다. */
|
|
455
455
|
export function codexAuthFilePath(): string {
|
|
456
|
-
return
|
|
456
|
+
return (
|
|
457
|
+
process.env.NEGOTIUM_CODEX_AUTH_FILE ||
|
|
458
|
+
join(process.env.CODEX_HOME || join(homedir(), ".codex"), "auth.json")
|
|
459
|
+
);
|
|
457
460
|
}
|
|
458
461
|
|
|
459
462
|
// System defaults moved to per-agent registries
|
|
@@ -84,10 +84,23 @@ export interface PlaywrightManagerHost {
|
|
|
84
84
|
) => NodeJS.ProcessEnv;
|
|
85
85
|
/** Reap an orphan that owns this exact host-managed data directory. */
|
|
86
86
|
readonly cleanupBrowserProcessesForDataDir: (userDataDir: string) => void;
|
|
87
|
+
/** Remove an exact host-managed profile directory, rejecting paths outside the host's root. */
|
|
88
|
+
readonly removeProfileDataDir?: (userDataDir: string) => boolean;
|
|
87
89
|
/** Reap host-managed browsers that are not represented by the supplied live directories. */
|
|
88
90
|
readonly reapOrphanBrowsers: (liveUserDataDirs: Iterable<string>) => void;
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
function removeDefaultProfileDataDir(userDataDir: string): boolean {
|
|
94
|
+
const root = resolve(BROWSER_PROFILES_DIR);
|
|
95
|
+
const target = resolve(userDataDir);
|
|
96
|
+
if (dirname(target) !== root) {
|
|
97
|
+
throw new Error(`Refusing to delete browser profile outside managed root: ${target}`);
|
|
98
|
+
}
|
|
99
|
+
const existed = existsSync(target);
|
|
100
|
+
rmSync(target, { recursive: true, force: true });
|
|
101
|
+
return existed;
|
|
102
|
+
}
|
|
103
|
+
|
|
91
104
|
export {
|
|
92
105
|
cleanupZombiePlaywright,
|
|
93
106
|
isBrowserJanitorOwner,
|
|
@@ -210,6 +223,7 @@ const defaultManagerHost: Readonly<PlaywrightManagerHost> = Object.freeze({
|
|
|
210
223
|
},
|
|
211
224
|
createChildEnvironment: defaultChildEnvironment,
|
|
212
225
|
cleanupBrowserProcessesForDataDir: killBrowserProcsForUserDataDir,
|
|
226
|
+
removeProfileDataDir: removeDefaultProfileDataDir,
|
|
213
227
|
reapOrphanBrowsers,
|
|
214
228
|
});
|
|
215
229
|
|
|
@@ -239,10 +253,12 @@ export function configurePlaywrightManagerHost(
|
|
|
239
253
|
next.resolveInstanceDataDir !== defaultManagerHost.resolveInstanceDataDir &&
|
|
240
254
|
(next.cleanupBrowserProcessesForDataDir ===
|
|
241
255
|
defaultManagerHost.cleanupBrowserProcessesForDataDir ||
|
|
256
|
+
!next.removeProfileDataDir ||
|
|
257
|
+
next.removeProfileDataDir === defaultManagerHost.removeProfileDataDir ||
|
|
242
258
|
next.reapOrphanBrowsers === defaultManagerHost.reapOrphanBrowsers)
|
|
243
259
|
) {
|
|
244
260
|
throw new Error(
|
|
245
|
-
"custom Playwright profile paths require host crash cleanup and orphan sweep hooks",
|
|
261
|
+
"custom Playwright profile paths require host crash cleanup, profile removal, and orphan sweep hooks",
|
|
246
262
|
);
|
|
247
263
|
}
|
|
248
264
|
managerHost = Object.freeze(next);
|
|
@@ -293,6 +309,7 @@ function deletePortFile(instanceKey: string) {
|
|
|
293
309
|
try {
|
|
294
310
|
unlinkSync(join(managerHost.portsDir, portFileName(instanceKey)));
|
|
295
311
|
} catch (e) {
|
|
312
|
+
if ((e as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
296
313
|
logger.warn({ err: e, instanceKey }, "Failed to delete playwright port file");
|
|
297
314
|
}
|
|
298
315
|
}
|
|
@@ -433,6 +450,45 @@ export function stopPlaywrightProfile(ownerId: string, rawProfile: string): Prom
|
|
|
433
450
|
);
|
|
434
451
|
}
|
|
435
452
|
|
|
453
|
+
export interface RemovePlaywrightProfileDataResult {
|
|
454
|
+
dataDir: string;
|
|
455
|
+
processStopped: boolean;
|
|
456
|
+
dataRemoved: boolean;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export interface RemovePlaywrightProfileDataOptions {
|
|
460
|
+
/** Revalidate external ownership immediately before the synchronous removal. */
|
|
461
|
+
beforeRemove?: () => void;
|
|
462
|
+
/** Commit external metadata before releasing the profile lifecycle barrier. */
|
|
463
|
+
afterRemove?: () => void;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Stop all exact-profile processes and remove its data through the configured host boundary. */
|
|
467
|
+
export function removePlaywrightProfileData(
|
|
468
|
+
ownerId: string,
|
|
469
|
+
rawProfile: string,
|
|
470
|
+
options: RemovePlaywrightProfileDataOptions = {},
|
|
471
|
+
): Promise<RemovePlaywrightProfileDataResult> {
|
|
472
|
+
return withPlaywrightProfileMaintenance(
|
|
473
|
+
ownerId,
|
|
474
|
+
rawProfile,
|
|
475
|
+
async (binding, { stopInstance }) => {
|
|
476
|
+
const dataDir = resolveUserDataDir(binding.instanceKey);
|
|
477
|
+
const processStopped = await stopInstance(binding.instanceKey);
|
|
478
|
+
managerHost.cleanupBrowserProcessesForDataDir(dataDir);
|
|
479
|
+
deletePortFile(binding.instanceKey);
|
|
480
|
+
options.beforeRemove?.();
|
|
481
|
+
const removeProfileDataDir = managerHost.removeProfileDataDir;
|
|
482
|
+
if (!removeProfileDataDir) {
|
|
483
|
+
throw new Error("Browser profile deletion is not configured for this host.");
|
|
484
|
+
}
|
|
485
|
+
const dataRemoved = removeProfileDataDir(dataDir);
|
|
486
|
+
options.afterRemove?.();
|
|
487
|
+
return { dataDir, processStopped, dataRemoved };
|
|
488
|
+
},
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
436
492
|
// A shared profile may have several concurrent turns. Reference counts keep an
|
|
437
493
|
// idle sweep from evicting the process until every borrower has finished.
|
|
438
494
|
const pinnedInstances = new Map<string, number>();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { removePlaywrightProfileData } from "#platform/playwright/manager";
|
|
2
|
+
import { assertBrowserProfileDeletable, deleteBrowserProfile } from "#storage/browser-profiles";
|
|
3
|
+
|
|
4
|
+
export interface DeleteManagedBrowserProfileResult {
|
|
5
|
+
profile: string;
|
|
6
|
+
metadataRemoved: boolean;
|
|
7
|
+
dataRemoved: boolean;
|
|
8
|
+
processStopped: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Delete an unused named profile while serialized against browser startup. */
|
|
12
|
+
export async function deleteManagedBrowserProfile(
|
|
13
|
+
ownerId: string,
|
|
14
|
+
rawProfile: string,
|
|
15
|
+
): Promise<DeleteManagedBrowserProfileResult> {
|
|
16
|
+
const profile = assertBrowserProfileDeletable(ownerId, rawProfile);
|
|
17
|
+
let metadataRemoved = false;
|
|
18
|
+
const removed = await removePlaywrightProfileData(ownerId, profile, {
|
|
19
|
+
beforeRemove: () => {
|
|
20
|
+
assertBrowserProfileDeletable(ownerId, profile);
|
|
21
|
+
},
|
|
22
|
+
afterRemove: () => {
|
|
23
|
+
metadataRemoved = deleteBrowserProfile(ownerId, profile);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
profile,
|
|
29
|
+
metadataRemoved,
|
|
30
|
+
dataRemoved: removed.dataRemoved,
|
|
31
|
+
processStopped: removed.processStopped,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -20,6 +20,7 @@ export {
|
|
|
20
20
|
type PlaywrightProfileBinding,
|
|
21
21
|
pinPlaywrightInstance,
|
|
22
22
|
reapPlaywrightOrphans,
|
|
23
|
+
removePlaywrightProfileData,
|
|
23
24
|
resetPlaywrightManagerHost,
|
|
24
25
|
resolvePlaywrightCapabilityOwner,
|
|
25
26
|
resolvePlaywrightProfileBinding,
|
|
@@ -39,6 +40,10 @@ export {
|
|
|
39
40
|
waitForChildProcessExit,
|
|
40
41
|
waitForChildProcessSpawnError,
|
|
41
42
|
} from "./manager-utils";
|
|
43
|
+
export {
|
|
44
|
+
type DeleteManagedBrowserProfileResult,
|
|
45
|
+
deleteManagedBrowserProfile,
|
|
46
|
+
} from "./profile-management";
|
|
42
47
|
export {
|
|
43
48
|
probeMcpTransport,
|
|
44
49
|
probePlaywrightMcpTransports,
|
|
@@ -3,7 +3,7 @@ import { chmodSync } from "node:fs";
|
|
|
3
3
|
import { createServer, type Server, type Socket } from "node:net";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { redactVaultSecrets, vaultGetValue, vaultSubstituteDetailed } from "#storage/vault-public";
|
|
7
7
|
|
|
8
8
|
const MAX_REQUEST_BYTES = 16 * 1024 * 1024;
|
|
9
9
|
const DEFAULT_OUTPUT_LIMIT = 100_000;
|
|
@@ -158,16 +158,15 @@ export async function createBrowserVaultBroker(userId: string): Promise<BrowserV
|
|
|
158
158
|
if (leases.size >= MAX_ACTIVE_LEASES) {
|
|
159
159
|
return { id, ok: false, error: "too many active leases" };
|
|
160
160
|
}
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
if (secret === undefined) return match;
|
|
161
|
+
const substituted = deepMapStrings(request.value, (text) => {
|
|
162
|
+
const result = vaultSubstituteDetailed(userId, text);
|
|
163
|
+
for (const key of result.usedKeys) {
|
|
164
|
+
const secret = vaultGetValue(userId, key);
|
|
165
|
+
if (secret === undefined) continue;
|
|
167
166
|
for (const form of encodedSecretForms(secret)) retainedForms.set(form, key);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
);
|
|
167
|
+
}
|
|
168
|
+
return result.text;
|
|
169
|
+
});
|
|
171
170
|
const prepared = prepareInput(request.tool, substituted);
|
|
172
171
|
const lease = randomBytes(24).toString("hex");
|
|
173
172
|
leases.set(lease, { boundary: prepared.boundary, createdAt: Date.now() });
|
|
@@ -6,6 +6,7 @@ export interface BrowserProfileRecord {
|
|
|
6
6
|
name: string;
|
|
7
7
|
createdAt: string | null;
|
|
8
8
|
topics: Array<{ id: string; title: string }>;
|
|
9
|
+
deletable: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function normalizeBrowserProfileName(name: string): string {
|
|
@@ -103,14 +104,26 @@ export function listBrowserProfiles(ownerId: string): BrowserProfileRecord[] {
|
|
|
103
104
|
name: DEFAULT_BROWSER_PROFILE,
|
|
104
105
|
createdAt: null,
|
|
105
106
|
topics: [],
|
|
107
|
+
deletable: false,
|
|
106
108
|
});
|
|
107
109
|
for (const row of records) {
|
|
108
|
-
byName.set(row.name, {
|
|
110
|
+
byName.set(row.name, {
|
|
111
|
+
name: row.name,
|
|
112
|
+
createdAt: row.created_at,
|
|
113
|
+
topics: [],
|
|
114
|
+
deletable: true,
|
|
115
|
+
});
|
|
109
116
|
}
|
|
110
117
|
for (const topic of topics) {
|
|
111
118
|
const name = topic.browser_profile || DEFAULT_BROWSER_PROFILE;
|
|
112
|
-
const record = byName.get(name) ?? {
|
|
119
|
+
const record = byName.get(name) ?? {
|
|
120
|
+
name,
|
|
121
|
+
createdAt: null,
|
|
122
|
+
topics: [],
|
|
123
|
+
deletable: name !== DEFAULT_BROWSER_PROFILE,
|
|
124
|
+
};
|
|
113
125
|
record.topics.push({ id: topic.id, title: topic.title });
|
|
126
|
+
record.deletable = false;
|
|
114
127
|
byName.set(name, record);
|
|
115
128
|
}
|
|
116
129
|
return [...byName.values()].sort((a, b) => {
|
|
@@ -120,16 +133,28 @@ export function listBrowserProfiles(ownerId: string): BrowserProfileRecord[] {
|
|
|
120
133
|
});
|
|
121
134
|
}
|
|
122
135
|
|
|
123
|
-
export function
|
|
136
|
+
export function assertBrowserProfileDeletable(ownerId: string, rawName: string): string {
|
|
124
137
|
void ownerId;
|
|
125
138
|
const name = normalizeBrowserProfileName(rawName);
|
|
126
139
|
if (name === DEFAULT_BROWSER_PROFILE) throw new Error("The default profile cannot be deleted.");
|
|
127
|
-
const
|
|
128
|
-
.query<{
|
|
129
|
-
`SELECT
|
|
130
|
-
WHERE t.browser_profile =
|
|
140
|
+
const topics = db
|
|
141
|
+
.query<{ id: string; title: string }, string>(
|
|
142
|
+
`SELECT t.id, t.title FROM api_topics t
|
|
143
|
+
WHERE t.browser_profile = ?
|
|
144
|
+
ORDER BY t.title`,
|
|
131
145
|
)
|
|
132
|
-
.
|
|
133
|
-
if (
|
|
134
|
-
|
|
146
|
+
.all(name);
|
|
147
|
+
if (topics.length > 0) {
|
|
148
|
+
const assigned = topics.map((topic) => `${topic.title} (${topic.id})`).join(", ");
|
|
149
|
+
throw new Error(`Profile "${name}" is assigned to ${topics.length} topic(s): ${assigned}.`);
|
|
150
|
+
}
|
|
151
|
+
return name;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function deleteBrowserProfile(ownerId: string, rawName: string): boolean {
|
|
155
|
+
const name = assertBrowserProfileDeletable(ownerId, rawName);
|
|
156
|
+
const result = db
|
|
157
|
+
.query("DELETE FROM browser_profiles WHERE owner_id = ? AND name = ?")
|
|
158
|
+
.run("local", name);
|
|
159
|
+
return Number(result.changes ?? 0) > 0;
|
|
135
160
|
}
|