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.
@@ -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
- handleTool(manager, toolName, toolInput),
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(userId: string, input: unknown): unknown {
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 at the last provider hook before the
438
- // tool runs. The model-facing call remains {{KEY}}, while normal
439
- // tools (including browser automation) receive the credential.
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 placeholders must be executed through mcp__vault__vault_run (shell/CLI) or mcp__vault__vault_http_request (HTTP) so secret values cannot enter the model transcript.";
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
- return !isVaultBrokerTool(toolName) && host.valueReferencesVaultKey(userId, input);
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({ isSensitivePath, valueReferencesVaultKey });
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}`, "--list-only=true"];
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
- If vault keys are available, reference secrets in tool inputs with the documented double-brace vault placeholder syntax. The real value is substituted at tool execution time; do not ask the user to paste secrets into chat.
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
- If vault keys are available, reference secrets in tool inputs with the documented double-brace vault placeholder syntax. The real value is substituted at tool execution time; do not ask the user to paste secrets into chat.
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.21";
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 const VAULT_BROKER_REDIRECT_ERROR = "Vault placeholders must be executed through mcp__vault__vault_run (shell/CLI) or mcp__vault__vault_http_request (HTTP) so secret values cannot enter the model transcript.";
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.21";
1
+ export declare const NEGOTIUM_VERSION = "0.1.23";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "negotium",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "description": "Install the Negotium multi-agent runtime and CLI with one package",
6
6
  "license": "Apache-2.0",