negotium 0.1.21 → 0.1.23
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 +199 -203
- package/dist/agent-helpers.js.map +10 -10
- package/dist/cron.js +249 -223
- package/dist/cron.js.map +12 -12
- package/dist/hosted-agent.js +249 -253
- package/dist/hosted-agent.js.map +11 -11
- package/dist/main.js +409 -398
- package/dist/main.js.map +14 -14
- package/dist/runtime/scripts/browser-webauthn-policy.mjs +37 -0
- package/dist/runtime/scripts/mcp-patchright-http.mjs +8 -3
- package/dist/runtime/src/agents/claude-provider.ts +11 -5
- package/dist/runtime/src/agents/maestro-provider.ts +4 -1
- package/dist/runtime/src/agents/vault-tool-policy.ts +28 -4
- package/dist/runtime/src/platform/mcp-config.ts +2 -1
- package/dist/runtime/src/platform/playwright/manager.ts +18 -0
- package/dist/runtime/src/prompts/sessions/channel-system.md +1 -1
- package/dist/runtime/src/prompts/sessions/topic-system.md +1 -1
- package/dist/runtime/src/version.ts +1 -1
- package/dist/types/packages/core/src/agents/claude-provider.d.ts +1 -1
- package/dist/types/packages/core/src/agents/vault-tool-policy.d.ts +2 -1
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const NO_BROWSER_START_TOOLS = new Set(["browser_status", "browser_close"]);
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Install Patchright's virtual authenticator before a page can invoke WebAuthn.
|
|
5
|
+
* This prevents Chrome's native passkey chooser from invisibly blocking DOM tools.
|
|
6
|
+
*/
|
|
7
|
+
export function createBrowserWebAuthnGuard() {
|
|
8
|
+
const installedContexts = new WeakSet();
|
|
9
|
+
|
|
10
|
+
async function ensureInstalled(manager) {
|
|
11
|
+
const page = await manager.getPage();
|
|
12
|
+
const context = page.context();
|
|
13
|
+
if (installedContexts.has(context)) return false;
|
|
14
|
+
await context.credentials.install();
|
|
15
|
+
installedContexts.add(context);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
async beforeTool(toolName, manager) {
|
|
21
|
+
if (
|
|
22
|
+
NO_BROWSER_START_TOOLS.has(toolName) ||
|
|
23
|
+
toolName === "browser_start" ||
|
|
24
|
+
toolName === "browser_passkey_install"
|
|
25
|
+
)
|
|
26
|
+
return;
|
|
27
|
+
await ensureInstalled(manager);
|
|
28
|
+
},
|
|
29
|
+
async afterTool(toolName, manager) {
|
|
30
|
+
if (toolName === "browser_start") await ensureInstalled(manager);
|
|
31
|
+
if (toolName === "browser_passkey_install") {
|
|
32
|
+
const page = await manager.getPage();
|
|
33
|
+
installedContexts.add(page.context());
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
secureBrowserToolOutput,
|
|
19
19
|
} from "./browser-passkey-policy.mjs";
|
|
20
20
|
import { createBrowserVaultTransforms } from "./browser-vault-transform.mjs";
|
|
21
|
+
import { createBrowserWebAuthnGuard } from "./browser-webauthn-policy.mjs";
|
|
21
22
|
|
|
22
23
|
function parseCli(argv = process.argv.slice(2)) {
|
|
23
24
|
const options = { host: "127.0.0.1" };
|
|
@@ -64,6 +65,7 @@ const vaultTransforms = await createBrowserVaultTransforms(
|
|
|
64
65
|
process.env.NEGOTIUM_BROWSER_VAULT_USER_ID,
|
|
65
66
|
);
|
|
66
67
|
const exposedTools = secureBrowserToolCatalog(tools);
|
|
68
|
+
const webAuthnGuard = createBrowserWebAuthnGuard();
|
|
67
69
|
|
|
68
70
|
function createMcpServer(defaultStartOptions, sharedManager, owner) {
|
|
69
71
|
const server = new Server(
|
|
@@ -79,9 +81,12 @@ function createMcpServer(defaultStartOptions, sharedManager, owner) {
|
|
|
79
81
|
toolName,
|
|
80
82
|
vaultTransforms.substitute(request.params.arguments ?? {}),
|
|
81
83
|
);
|
|
82
|
-
const result = await manager.runAsOwner(owner, () =>
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const result = await manager.runAsOwner(owner, async () => {
|
|
85
|
+
await webAuthnGuard.beforeTool(toolName, manager);
|
|
86
|
+
const toolResult = await handleTool(manager, toolName, toolInput);
|
|
87
|
+
await webAuthnGuard.afterTool(toolName, manager);
|
|
88
|
+
return toolResult;
|
|
89
|
+
});
|
|
85
90
|
return vaultTransforms.redact(secureBrowserToolOutput(toolName, result));
|
|
86
91
|
} catch (error) {
|
|
87
92
|
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
referencesHostedSecretStorage,
|
|
30
30
|
substituteHostedSecrets,
|
|
31
31
|
} from "#agents/execution-host";
|
|
32
|
+
import { shouldSubstituteVaultToolInput } from "#agents/vault-tool-policy";
|
|
32
33
|
import { extractFileEvents } from "#media/file-events";
|
|
33
34
|
import { errMsg } from "#platform/error";
|
|
34
35
|
import { logger } from "#platform/logger";
|
|
@@ -47,7 +48,12 @@ const CLAUDE_DEFAULT_DISALLOWED_TOOLS = [
|
|
|
47
48
|
|
|
48
49
|
const CLAUDE_NATIVE_AGENT_TOOLS = ["Task", "Agent", "TaskOutput", "TaskStop"] as const;
|
|
49
50
|
|
|
50
|
-
export function substituteClaudeToolInput(
|
|
51
|
+
export function substituteClaudeToolInput(
|
|
52
|
+
userId: string,
|
|
53
|
+
toolName: string,
|
|
54
|
+
input: unknown,
|
|
55
|
+
): unknown {
|
|
56
|
+
if (!shouldSubstituteVaultToolInput(toolName)) return input;
|
|
51
57
|
return deepMapStrings(input, (value) => substituteHostedSecrets(userId, value));
|
|
52
58
|
}
|
|
53
59
|
|
|
@@ -434,11 +440,11 @@ export async function* claudeProvider(opts: AgentQueryOptions): AsyncGenerator<U
|
|
|
434
440
|
};
|
|
435
441
|
}
|
|
436
442
|
|
|
437
|
-
// Resolve Vault placeholders
|
|
438
|
-
//
|
|
439
|
-
//
|
|
443
|
+
// Resolve Vault placeholders only for explicitly allowlisted
|
|
444
|
+
// transient execution tools. Messaging and persistence tools
|
|
445
|
+
// must retain {{KEY}} verbatim.
|
|
440
446
|
const userId = opts.userId ?? "";
|
|
441
|
-
const withVault = substituteClaudeToolInput(userId, tool_input);
|
|
447
|
+
const withVault = substituteClaudeToolInput(userId, tool_name, tool_input);
|
|
442
448
|
if (JSON.stringify(withVault) === JSON.stringify(tool_input)) {
|
|
443
449
|
return { continue: true };
|
|
444
450
|
}
|
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
referencesHostedSecretStorage,
|
|
71
71
|
substituteHostedSecrets,
|
|
72
72
|
} from "#agents/execution-host";
|
|
73
|
+
import { shouldSubstituteVaultToolInput } from "#agents/vault-tool-policy";
|
|
73
74
|
import type { AgentQueryOptions, UnifiedEvent } from "#types";
|
|
74
75
|
|
|
75
76
|
/**
|
|
@@ -118,11 +119,13 @@ export function buildMaestroDisallowedTools(
|
|
|
118
119
|
function buildVaultHook(userId: string): HookRegistration {
|
|
119
120
|
return {
|
|
120
121
|
name: "vault-guard",
|
|
121
|
-
pre({ input }) {
|
|
122
|
+
pre({ toolName, input }) {
|
|
122
123
|
if (referencesHostedSecretStorage(input)) {
|
|
123
124
|
return { decision: "block", error: "Runtime secret storage access is not permitted" };
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
if (!shouldSubstituteVaultToolInput(toolName)) return { decision: "allow" };
|
|
128
|
+
|
|
126
129
|
const substituted = deepMapStrings(input, (value) => substituteHostedSecrets(userId, value));
|
|
127
130
|
return JSON.stringify(substituted) === JSON.stringify(input)
|
|
128
131
|
? { decision: "allow" }
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isSensitivePath } from "#security/sensitive-path";
|
|
2
|
-
import { valueReferencesVaultKey } from "#storage/vault";
|
|
3
2
|
|
|
4
3
|
const SENSITIVE_RUNTIME_NAMES = [
|
|
5
4
|
"vault.db",
|
|
@@ -8,8 +7,23 @@ const SENSITIVE_RUNTIME_NAMES = [
|
|
|
8
7
|
"sessions.db",
|
|
9
8
|
] as const;
|
|
10
9
|
|
|
10
|
+
// Direct substitution is deliberately default-deny. These provider-owned
|
|
11
|
+
// execution tools consume credentials transiently; persistence and messaging
|
|
12
|
+
// tools must keep placeholders unresolved.
|
|
13
|
+
const DIRECT_VAULT_EXECUTION_TOOLS = new Set(["Bash", "WebFetch"]);
|
|
14
|
+
|
|
15
|
+
function leafToolName(toolName: string): string {
|
|
16
|
+
const parts = toolName.split("__");
|
|
17
|
+
return parts.at(-1) ?? toolName;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function shouldSubstituteVaultToolInput(toolName: string): boolean {
|
|
21
|
+
const leaf = leafToolName(toolName);
|
|
22
|
+
return leaf.startsWith("browser_") || DIRECT_VAULT_EXECUTION_TOOLS.has(leaf);
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
export const VAULT_BROKER_REDIRECT_ERROR =
|
|
12
|
-
"Vault
|
|
26
|
+
"Vault broker redirection is disabled; use {{KEY}} directly in normal tool inputs.";
|
|
13
27
|
|
|
14
28
|
export interface VaultToolPolicyHost {
|
|
15
29
|
isSensitivePath(path: string): boolean;
|
|
@@ -41,13 +55,23 @@ export function createVaultToolPolicy(host: VaultToolPolicyHost): VaultToolPolic
|
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
function shouldRedirectVaultTool(userId: string, toolName: string, input: unknown): boolean {
|
|
44
|
-
|
|
58
|
+
// Kept as a compatibility surface for embedding hosts that implemented the
|
|
59
|
+
// pre-0.1.20 broker-only policy. Normal tools now receive Vault values from
|
|
60
|
+
// the execution-time substitution hook, so redirecting a {{KEY}} call
|
|
61
|
+
// would only make browser form filling and other interactive tools fail.
|
|
62
|
+
void userId;
|
|
63
|
+
void toolName;
|
|
64
|
+
void input;
|
|
65
|
+
return false;
|
|
45
66
|
}
|
|
46
67
|
|
|
47
68
|
return { isVaultBrokerTool, referencesRuntimeSecretStorage, shouldRedirectVaultTool };
|
|
48
69
|
}
|
|
49
70
|
|
|
50
|
-
const defaultVaultToolPolicy = createVaultToolPolicy({
|
|
71
|
+
const defaultVaultToolPolicy = createVaultToolPolicy({
|
|
72
|
+
isSensitivePath,
|
|
73
|
+
valueReferencesVaultKey: () => false,
|
|
74
|
+
});
|
|
51
75
|
|
|
52
76
|
export const isVaultBrokerTool = defaultVaultToolPolicy.isVaultBrokerTool;
|
|
53
77
|
export const referencesRuntimeSecretStorage = defaultVaultToolPolicy.referencesRuntimeSecretStorage;
|
|
@@ -459,7 +459,8 @@ const MCP_CATALOG: Record<string, RuntimeMcpCatalogEntry> = {
|
|
|
459
459
|
// Normal turns use {{KEY}} directly in tool inputs. Keep the Vault MCP
|
|
460
460
|
// surface focused on key discovery; broker tools remain available from
|
|
461
461
|
// the public factory for compatibility but are no longer the default UX.
|
|
462
|
-
const args = [`--user-id=${userId}
|
|
462
|
+
const args = [`--user-id=${userId}`];
|
|
463
|
+
if (agent !== "codex") args.push("--list-only=true");
|
|
463
464
|
return buildStdioMcpServer(agent, VAULT_SERVER, args);
|
|
464
465
|
},
|
|
465
466
|
},
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
hasBrowserProfileTopic,
|
|
33
33
|
normalizeBrowserProfileName,
|
|
34
34
|
} from "#storage/browser-profiles";
|
|
35
|
+
import { getRuntimeProcessLease } from "#storage/runtime-process-leases";
|
|
35
36
|
|
|
36
37
|
export { PLAYWRIGHT_PORTS_DIR };
|
|
37
38
|
|
|
@@ -514,6 +515,17 @@ export function selectOrphanBrowserPids(
|
|
|
514
515
|
return out;
|
|
515
516
|
}
|
|
516
517
|
|
|
518
|
+
/**
|
|
519
|
+
* The browser process table is shared across every Negotium process, while the
|
|
520
|
+
* in-memory `instances` map is not. Only the current node-daemon lease owner may
|
|
521
|
+
* compare those two views and reap untracked browsers. Otherwise an old daemon
|
|
522
|
+
* that survived shutdown can mistake the replacement daemon's browser for an
|
|
523
|
+
* orphan and kill it.
|
|
524
|
+
*/
|
|
525
|
+
export function isBrowserJanitorOwner(leaseOwnerPid: number | null, selfPid: number): boolean {
|
|
526
|
+
return leaseOwnerPid === selfPid;
|
|
527
|
+
}
|
|
528
|
+
|
|
517
529
|
/**
|
|
518
530
|
* Periodic sweep that reaps orphaned browser processes the tracked-instance map
|
|
519
531
|
* has lost sight of. The 30-min idle eviction only touches instances still in
|
|
@@ -522,6 +534,12 @@ export function selectOrphanBrowserPids(
|
|
|
522
534
|
* orphans under memory pressure are exactly this failure mode.
|
|
523
535
|
*/
|
|
524
536
|
function reapOrphanBrowsers(): void {
|
|
537
|
+
// Use an infinite stale window here intentionally: even if the active daemon
|
|
538
|
+
// briefly misses a heartbeat, the recorded owner remains the only process
|
|
539
|
+
// allowed to run this destructive cross-process sweep.
|
|
540
|
+
const daemonLease = getRuntimeProcessLease("node-daemon", Date.now(), Number.POSITIVE_INFINITY);
|
|
541
|
+
if (!isBrowserJanitorOwner(daemonLease?.pid ?? null, process.pid)) return;
|
|
542
|
+
|
|
525
543
|
const profileRoot = resolve(BROWSER_PROFILES_DIR);
|
|
526
544
|
let pids: string;
|
|
527
545
|
try {
|
|
@@ -34,7 +34,7 @@ If the task looks unfamiliar, slow, or error-prone and `skill_query` is availabl
|
|
|
34
34
|
When the user asks for HTML/CSS, dashboards, charts, tables, or interactive visual output, use the visual tool described below rather than pasting a large HTML blob into chat.
|
|
35
35
|
|
|
36
36
|
## Vault
|
|
37
|
-
|
|
37
|
+
Use `{{KEY}}` directly in browser tools and Claude/Maestro tool inputs. For Codex native shell or HTTP, use the Vault broker tools; never ask the user to paste secrets into chat.
|
|
38
38
|
|
|
39
39
|
## Background Bash
|
|
40
40
|
For long-running shell work, prefer the background-bash MCP tools when they are available. Their results are injected back into the session; avoid polling unless you need live output.
|
|
@@ -33,7 +33,7 @@ When the user asks for HTML/CSS, dashboards, charts, tables, or interactive visu
|
|
|
33
33
|
If this topic has an injected Memory section, use it for past context. For deeper recall, call `wiki_query` when available.
|
|
34
34
|
|
|
35
35
|
## Vault
|
|
36
|
-
|
|
36
|
+
Use `{{KEY}}` directly in browser tools and Claude/Maestro tool inputs. For Codex native shell or HTTP, use the Vault broker tools; never ask the user to paste secrets into chat.
|
|
37
37
|
|
|
38
38
|
## Background Bash
|
|
39
39
|
For long-running shell work, prefer the background-bash MCP tools when they are available. Their results are injected back into the session; avoid polling unless you need live output.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const NEGOTIUM_VERSION = "0.1.
|
|
1
|
+
export const NEGOTIUM_VERSION = "0.1.23";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import type { AgentQueryOptions, UnifiedEvent } from "../types";
|
|
3
|
-
export declare function substituteClaudeToolInput(userId: string, input: unknown): unknown;
|
|
3
|
+
export declare function substituteClaudeToolInput(userId: string, toolName: string, input: unknown): unknown;
|
|
4
4
|
export declare function buildClaudeDisallowedTools(extra?: readonly string[] | undefined): string[];
|
|
5
5
|
type SpawnClaudeCodeProcess = NonNullable<Options["spawnClaudeCodeProcess"]>;
|
|
6
6
|
type SpawnClaudeCodeOptions = Parameters<SpawnClaudeCodeProcess>[0];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export declare
|
|
1
|
+
export declare function shouldSubstituteVaultToolInput(toolName: string): boolean;
|
|
2
|
+
export declare const VAULT_BROKER_REDIRECT_ERROR = "Vault broker redirection is disabled; use {{KEY}} directly in normal tool inputs.";
|
|
2
3
|
export interface VaultToolPolicyHost {
|
|
3
4
|
isSensitivePath(path: string): boolean;
|
|
4
5
|
valueReferencesVaultKey(userId: string, value: unknown): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const NEGOTIUM_VERSION = "0.1.
|
|
1
|
+
export declare const NEGOTIUM_VERSION = "0.1.23";
|