negotium 0.3.13 → 0.4.0

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.
Files changed (39) hide show
  1. package/README.md +2 -6
  2. package/dist/agent-helpers.js +328 -185
  3. package/dist/agent-helpers.js.map +9 -8
  4. package/dist/{chunk-0ynjwr50.js → chunk-zq2tcq4k.js} +12 -29
  5. package/dist/{chunk-0ynjwr50.js.map → chunk-zq2tcq4k.js.map} +4 -4
  6. package/dist/hosted-agent.js +207 -64
  7. package/dist/hosted-agent.js.map +8 -7
  8. package/dist/main.js +753 -886
  9. package/dist/main.js.map +13 -13
  10. package/dist/mcp-factories.js +300 -468
  11. package/dist/mcp-factories.js.map +9 -10
  12. package/dist/registry.js +3 -3
  13. package/dist/registry.js.map +2 -2
  14. package/dist/rollout.js +1 -1
  15. package/dist/runtime/src/agents/codex-provider.ts +33 -8
  16. package/dist/runtime/src/agents/codex-vault-hook-bridge.ts +192 -0
  17. package/dist/runtime/src/agents/codex-vault-hook.mjs +48 -0
  18. package/dist/runtime/src/agents/execution-host.ts +1 -14
  19. package/dist/runtime/src/agents/public-helpers.ts +0 -9
  20. package/dist/runtime/src/agents/vault-tool-policy.ts +9 -51
  21. package/dist/runtime/src/mcp/factories/index.ts +1 -8
  22. package/dist/runtime/src/mcp/factories/vault.ts +6 -104
  23. package/dist/runtime/src/mcp/vault-server.ts +4 -20
  24. package/dist/runtime/src/prompts/sessions/_shared-tools.md +1 -1
  25. package/dist/runtime/src/version.ts +1 -1
  26. package/dist/types/packages/core/src/agents/codex-vault-hook-bridge.d.ts +27 -0
  27. package/dist/types/packages/core/src/agents/execution-host.d.ts +0 -2
  28. package/dist/types/packages/core/src/agents/public-helpers.d.ts +0 -1
  29. package/dist/types/packages/core/src/agents/vault-tool-policy.d.ts +1 -14
  30. package/dist/types/packages/core/src/mcp/factories/index.d.ts +1 -3
  31. package/dist/types/packages/core/src/mcp/factories/vault.d.ts +4 -13
  32. package/dist/types/packages/core/src/version.d.ts +1 -1
  33. package/package.json +1 -1
  34. package/dist/runtime/src/mcp/factories/vault-host.ts +0 -8
  35. package/dist/runtime/src/mcp/vault-http.ts +0 -235
  36. package/dist/runtime/src/mcp/vault-run.ts +0 -154
  37. package/dist/types/packages/core/src/mcp/factories/vault-host.d.ts +0 -7
  38. package/dist/types/packages/core/src/mcp/vault-http.d.ts +0 -23
  39. package/dist/types/packages/core/src/mcp/vault-run.d.ts +0 -19
@@ -1,44 +1,21 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { z } from "zod";
3
- import { executeVaultHttpRequest, type VaultHttpRequest } from "#mcp/vault-http";
4
- import { executeVaultRun, type VaultRunRequest } from "#mcp/vault-run";
5
- import { mcpError, mcpOk } from "../mcp-helpers";
6
- import type { VaultCredentialHost } from "./vault-host";
2
+ import type { VaultEntry } from "#storage/vault";
3
+ import { mcpOk } from "../mcp-helpers";
7
4
 
8
5
  export interface VaultMcpContext {
9
6
  userId?: string;
10
- listOnly?: boolean;
11
- httpOnly?: boolean;
12
- cwd?: string;
13
7
  }
14
8
 
15
- export interface VaultMcpExecutors {
16
- run?(
17
- userId: string,
18
- request: VaultRunRequest,
19
- host: VaultCredentialHost,
20
- ): ReturnType<typeof executeVaultRun>;
21
- http?(
22
- userId: string,
23
- request: VaultHttpRequest,
24
- host: VaultCredentialHost,
25
- ): ReturnType<typeof executeVaultHttpRequest>;
9
+ export interface VaultMcpHost {
10
+ list(userId: string): readonly VaultEntry[];
26
11
  }
27
12
 
28
- export function createVaultMcpServer(
29
- context: VaultMcpContext,
30
- host: VaultCredentialHost,
31
- executors: VaultMcpExecutors = {},
32
- ): McpServer {
13
+ export function createVaultMcpServer(context: VaultMcpContext, host: VaultMcpHost): McpServer {
33
14
  const server = new McpServer({ name: "vault", version: "1.0.0" });
34
- const run = executors.run ?? executeVaultRun;
35
- const http = executors.http ?? executeVaultHttpRequest;
36
15
 
37
16
  server.tool(
38
17
  "vault_list",
39
- context.httpOnly
40
- ? "List the user's Vault keys and descriptions without exposing values. Use vault_http_request for HTTPS APIs that need a credential."
41
- : "List the user's Vault keys and descriptions without exposing values. Use vault_http_request for APIs and vault_run for shell/CLI work that needs a credential.",
18
+ "List the user's Vault keys and descriptions without exposing values. Use {{KEY}} placeholders directly in supported transient tool inputs.",
42
19
  {},
43
20
  () => {
44
21
  if (!context.userId) return mcpOk("(vault unavailable: no user context)");
@@ -51,80 +28,5 @@ export function createVaultMcpServer(
51
28
  },
52
29
  );
53
30
 
54
- if (!context.listOnly && !context.httpOnly) {
55
- server.tool(
56
- "vault_run",
57
- "Run a shell command containing {{KEY}} references inside Otium's credential broker. Expanded command input never reaches the model/provider, and stdout/stderr are redacted before return. Prefer vault_http_request for HTTP APIs.",
58
- {
59
- command: z
60
- .string()
61
- .min(1)
62
- .max(64 * 1024),
63
- timeout_ms: z.number().int().min(1_000).max(600_000).optional(),
64
- max_output_bytes: z
65
- .number()
66
- .int()
67
- .min(1_024)
68
- .max(2 * 1024 * 1024)
69
- .optional(),
70
- },
71
- async ({ command, timeout_ms, max_output_bytes }) => {
72
- if (!context.userId) return mcpError("Vault unavailable: no user context");
73
- const result = await run(
74
- context.userId,
75
- {
76
- command,
77
- timeoutMs: timeout_ms,
78
- maxOutputBytes: max_output_bytes,
79
- cwd: context.cwd,
80
- },
81
- host,
82
- );
83
- return result.error && result.exitCode === null
84
- ? mcpError(result.error)
85
- : mcpOk(JSON.stringify(result, null, 2));
86
- },
87
- );
88
- }
89
-
90
- if (!context.listOnly)
91
- server.tool(
92
- "vault_http_request",
93
- "Make an HTTPS request with {{KEY}} references resolved inside Otium. Put secrets in headers or body, never in the URL. The expanded request is not returned; the response is redacted before the model sees it.",
94
- {
95
- method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET"),
96
- url: z.string().url().describe("Absolute HTTPS URL without Vault placeholders"),
97
- headers: z.record(z.string(), z.string()).optional(),
98
- body: z.string().optional(),
99
- timeout_ms: z.number().int().min(1_000).max(120_000).optional(),
100
- max_response_bytes: z
101
- .number()
102
- .int()
103
- .min(1_024)
104
- .max(1024 * 1024)
105
- .optional(),
106
- },
107
- async ({ method, url, headers, body, timeout_ms, max_response_bytes }) => {
108
- if (!context.userId) return mcpError("Vault unavailable: no user context");
109
- const result = await http(
110
- context.userId,
111
- {
112
- method,
113
- url,
114
- headers,
115
- body,
116
- timeoutMs: timeout_ms,
117
- maxResponseBytes: max_response_bytes,
118
- },
119
- host,
120
- );
121
- return result.error ? mcpError(result.error) : mcpOk(JSON.stringify(result, null, 2));
122
- },
123
- );
124
-
125
31
  return server;
126
32
  }
127
-
128
- export type { VaultHttpRequest, VaultHttpResult } from "#mcp/vault-http";
129
- export type { VaultRunRequest, VaultRunResult } from "#mcp/vault-run";
130
- export type { VaultCredentialHost } from "./vault-host";
@@ -1,28 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import "./stdio-protect";
3
- import { createVaultMcpServer, type VaultCredentialHost } from "#mcp/factories/vault";
3
+ import { createVaultMcpServer, type VaultMcpHost } from "#mcp/factories/vault";
4
4
  import { connectStdio, parseUserIdArg } from "#mcp/mcp-helpers";
5
- import { logger } from "#platform/logger";
6
- import { redactVaultSecrets, vaultList, vaultSubstituteDetailed } from "#storage/vault";
5
+ import { vaultList } from "#storage/vault";
7
6
 
8
7
  const args = process.argv.slice(2);
9
- const host: VaultCredentialHost = {
8
+ const host: VaultMcpHost = {
10
9
  list: vaultList,
11
- substitute: vaultSubstituteDetailed,
12
- redact: redactVaultSecrets,
13
- log(level, details, message) {
14
- logger[level](details, message);
15
- },
16
10
  };
17
11
 
18
- await connectStdio(
19
- createVaultMcpServer(
20
- {
21
- userId: parseUserIdArg(args),
22
- listOnly: args.includes("--list-only=true"),
23
- httpOnly: args.includes("--http-only=true"),
24
- cwd: process.cwd(),
25
- },
26
- host,
27
- ),
28
- );
12
+ await connectStdio(createVaultMcpServer({ userId: parseUserIdArg(args) }, host));
@@ -11,7 +11,7 @@ Use a tool only when it is actually available; otherwise say so instead of prete
11
11
  - Voice: user voice arrives transcribed; fix misheard proper nouns from context.
12
12
  - Skills: when a task looks unfamiliar, slow, or error-prone, `skill_query` first; save or update a reusable solution with `skill_save`.
13
13
  - Memory: when a Memory section is injected, use it for past context; `wiki_query` for deeper recall.
14
- - Vault: 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.
14
+ - Vault: use `{{KEY}}` directly in supported transient tool inputs for every provider; never ask the user to paste secrets into chat.
15
15
  - Background shell: use background-bash only for independent commands expected to outlive the current turn (typically over 2 minutes). Run ordinary builds, tests, and commands needed for the next step inline and wait for them; do not background work merely to avoid waiting. Results are injected automatically, so do not poll unless live output is required.
16
16
  - Scheduled tasks: manage with `cron-manager` tools; scripts must already exist (`cron_list_scripts`). Jobs in one topic share a Cron conversation, so `cron_reset` clears the topic's whole Cron context, not one job.
17
17
  - Heavy work (large files, video encode, big crawls, browser automation): check `get_system_health` first and back off under resource pressure.
@@ -1 +1 @@
1
- export const NEGOTIUM_VERSION = "0.3.13";
1
+ export const NEGOTIUM_VERSION = "0.4.0";
@@ -0,0 +1,27 @@
1
+ export interface CodexPreToolUseInput {
2
+ hook_event_name?: string;
3
+ tool_name: string;
4
+ tool_input: unknown;
5
+ }
6
+ export type CodexPreToolUseOutput = Record<string, unknown>;
7
+ export interface CodexVaultHookOperations {
8
+ referencesSensitiveStorage(value: unknown): boolean;
9
+ substitute(userId: string, value: string): string;
10
+ }
11
+ export declare function evaluateCodexVaultPreToolUse(input: CodexPreToolUseInput, userId: string, operations: CodexVaultHookOperations): CodexPreToolUseOutput;
12
+ export interface CodexVaultHookBridge {
13
+ codexPathOverride: string;
14
+ hooks: {
15
+ PreToolUse: Array<{
16
+ matcher: string;
17
+ hooks: Array<{
18
+ type: string;
19
+ command: string;
20
+ timeout: number;
21
+ statusMessage: string;
22
+ }>;
23
+ }>;
24
+ };
25
+ close(): Promise<void>;
26
+ }
27
+ export declare function createCodexVaultHookBridge(userId: string): Promise<CodexVaultHookBridge>;
@@ -12,7 +12,6 @@ export interface AgentExecutionHost {
12
12
  redactVaultSecrets(userId: string, value: string): string;
13
13
  substituteVaultSecrets(userId: string, value: string): string;
14
14
  referencesRuntimeSecretStorage(value: unknown): boolean;
15
- shouldRedirectVaultTool(userId: string, toolName: string, input: unknown): boolean;
16
15
  claudeCodeExecutablePath(): string | undefined;
17
16
  codexAuthFilePath(): string;
18
17
  transformQueryOptions?(opts: AgentQueryOptions): AgentQueryOptions;
@@ -27,7 +26,6 @@ export declare function hostedMcpServers(opts: AgentQueryOptions): Record<string
27
26
  export declare function redactHostedSecrets(userId: string, value: string): string;
28
27
  export declare function substituteHostedSecrets(userId: string, value: string): string;
29
28
  export declare function referencesHostedSecretStorage(value: unknown): boolean;
30
- export declare function shouldRedirectHostedVaultTool(userId: string, toolName: string, input: unknown): boolean;
31
29
  export declare function transformHostedQueryOptions(opts: AgentQueryOptions): AgentQueryOptions;
32
30
  export declare function hostedClaudeCodeExecutablePath(): string | undefined;
33
31
  export declare function hostedCodexAuthFilePath(): string;
@@ -14,4 +14,3 @@ export { createSelfConfigCore, DEFAULT_SELF_CONFIG_PRODUCT, type SelfConfigAgent
14
14
  export { resolveTaskEventScope, type TaskEventHost, type TaskEventScope, withTaskSnapshots, } from "./task-events";
15
15
  export { buildNumberedDiffSummary, classifyShellToolName, formatToolUse, type NumberedDiffSummary, summarizeDisplayText, summarizeShellCommand, summarizeToolInput, type ToolCallSummaryInput, type ToolCallSummaryValue, } from "./tool-format";
16
16
  export { cleanupTopicRollouts, cleanupTopicRolloutsFromEntries, createTopicLogMaintenance, type PurgeSessionRef, type PurgeTopicLogsOptions, purgeTopicLogs, type RotateTopicLogsOptions, type RotateTopicLogsResult, rotateTopicLogs, type TopicConversationEntry, type TopicLogMaintenance, type TopicLogMaintenanceHost, } from "./topic-cleanup";
17
- export { createVaultToolPolicy, isVaultBrokerTool, referencesRuntimeSecretStorage, shouldRedirectVaultTool, VAULT_BROKER_REDIRECT_ERROR, type VaultToolPolicy, type VaultToolPolicyHost, } from "./vault-tool-policy";
@@ -1,15 +1,2 @@
1
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.";
3
- export interface VaultToolPolicyHost {
4
- isSensitivePath(path: string): boolean;
5
- valueReferencesVaultKey(userId: string, value: unknown): boolean;
6
- }
7
- export interface VaultToolPolicy {
8
- isVaultBrokerTool(toolName: string): boolean;
9
- referencesRuntimeSecretStorage(value: unknown): boolean;
10
- shouldRedirectVaultTool(userId: string, toolName: string, input: unknown): boolean;
11
- }
12
- export declare function createVaultToolPolicy(host: VaultToolPolicyHost): VaultToolPolicy;
13
- export declare const isVaultBrokerTool: (toolName: string) => boolean;
14
- export declare const referencesRuntimeSecretStorage: (value: unknown) => boolean;
15
- export declare const shouldRedirectVaultTool: (userId: string, toolName: string, input: unknown) => boolean;
2
+ export declare function referencesRuntimeSecretStorage(value: unknown): boolean;
@@ -1,7 +1,5 @@
1
1
  export { parseSessionCommContext, type SessionCommContext, type SessionCommContextDefaults, } from "../session-comm/context";
2
2
  export { createSessionTargetCatalog, type SessionTarget, type SessionTargetCatalog, type SessionTargetCatalogHost, type SessionTopicEntry, type SessionTopicRow, type ValidateSessionTargetResult, } from "../session-comm/topic-catalog";
3
- export { executeVaultHttpRequest } from "../vault-http";
4
- export { executeVaultRun } from "../vault-run";
5
3
  export { createWikiMcpServer, type WikiMcpContext, type WikiMcpHost, type WikiSurface, type WikiTopicBrief, } from "../wiki-server";
6
4
  export { type AgentHealthMcpContext, createAgentHealthMcpServer, } from "./agent-health";
7
5
  export { type CompactionLogMcpContext, createCompactionLogMcpServer, } from "./compaction-log";
@@ -11,4 +9,4 @@ export { type McpStdioProtectionTarget, protectMcpStdio, } from "./stdio-protect
11
9
  export { createSystemHealthMcpServer, defaultSystemHealthMcpHost, type SystemHealthMcpHost, type SystemHealthSnapshot, } from "./system-health";
12
10
  export { createTaskMcpServer, defaultTaskMcpHost, type TaskMcpContext, type TaskMcpHost, } from "./task";
13
11
  export { createTokenStatsMcpServer, defaultTokenStatsMcpHost, type TokenStatsMcpContext, type TokenStatsMcpHost, type TokenStatsSnapshot, } from "./token-stats";
14
- export { createVaultMcpServer, type VaultCredentialHost, type VaultHttpRequest, type VaultHttpResult, type VaultMcpContext, type VaultMcpExecutors, type VaultRunRequest, type VaultRunResult, } from "./vault";
12
+ export { createVaultMcpServer, type VaultMcpContext, type VaultMcpHost, } from "./vault";
@@ -1,18 +1,9 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { executeVaultHttpRequest, type VaultHttpRequest } from "../vault-http";
3
- import { executeVaultRun, type VaultRunRequest } from "../vault-run";
4
- import type { VaultCredentialHost } from "./vault-host";
2
+ import type { VaultEntry } from "../../storage/vault";
5
3
  export interface VaultMcpContext {
6
4
  userId?: string;
7
- listOnly?: boolean;
8
- httpOnly?: boolean;
9
- cwd?: string;
10
5
  }
11
- export interface VaultMcpExecutors {
12
- run?(userId: string, request: VaultRunRequest, host: VaultCredentialHost): ReturnType<typeof executeVaultRun>;
13
- http?(userId: string, request: VaultHttpRequest, host: VaultCredentialHost): ReturnType<typeof executeVaultHttpRequest>;
6
+ export interface VaultMcpHost {
7
+ list(userId: string): readonly VaultEntry[];
14
8
  }
15
- export declare function createVaultMcpServer(context: VaultMcpContext, host: VaultCredentialHost, executors?: VaultMcpExecutors): McpServer;
16
- export type { VaultHttpRequest, VaultHttpResult } from "../vault-http";
17
- export type { VaultRunRequest, VaultRunResult } from "../vault-run";
18
- export type { VaultCredentialHost } from "./vault-host";
9
+ export declare function createVaultMcpServer(context: VaultMcpContext, host: VaultMcpHost): McpServer;
@@ -1 +1 @@
1
- export declare const NEGOTIUM_VERSION = "0.3.13";
1
+ export declare const NEGOTIUM_VERSION = "0.4.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "negotium",
3
- "version": "0.3.13",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Install the Negotium multi-agent runtime and CLI with one package",
6
6
  "license": "Apache-2.0",
@@ -1,8 +0,0 @@
1
- import type { VaultEntry, VaultSubstitutionResult } from "#storage/vault";
2
-
3
- export interface VaultCredentialHost {
4
- list(userId: string): readonly VaultEntry[];
5
- substitute(userId: string, text: string): VaultSubstitutionResult;
6
- redact(userId: string, text: string): string;
7
- log?(level: "info" | "warn", details: Record<string, unknown>, message: string): void;
8
- }
@@ -1,235 +0,0 @@
1
- import type { VaultCredentialHost } from "#mcp/factories/vault-host";
2
-
3
- const SAFE_RESPONSE_HEADERS = new Set([
4
- "content-type",
5
- "content-length",
6
- "location",
7
- "retry-after",
8
- "x-ratelimit-limit",
9
- "x-ratelimit-remaining",
10
- "x-ratelimit-reset",
11
- ]);
12
-
13
- const FORBIDDEN_REQUEST_HEADERS = new Set([
14
- "connection",
15
- "content-length",
16
- "host",
17
- "proxy-authorization",
18
- "transfer-encoding",
19
- ]);
20
-
21
- export interface VaultHttpRequest {
22
- method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
23
- url: string;
24
- headers?: Record<string, string>;
25
- body?: string;
26
- timeoutMs?: number;
27
- maxResponseBytes?: number;
28
- }
29
-
30
- export interface VaultHttpResult {
31
- ok: boolean;
32
- status?: number;
33
- statusText?: string;
34
- headers?: Record<string, string>;
35
- body?: string;
36
- truncated?: boolean;
37
- error?: string;
38
- }
39
-
40
- function substituteObject(
41
- userId: string,
42
- values: Record<string, string>,
43
- host: VaultCredentialHost,
44
- ): { values: Record<string, string>; usedKeys: string[] } {
45
- const used = new Set<string>();
46
- const substituted: Record<string, string> = {};
47
- for (const [key, value] of Object.entries(values)) {
48
- if (FORBIDDEN_REQUEST_HEADERS.has(key.toLowerCase())) {
49
- throw new Error(`Header "${key}" is not allowed`);
50
- }
51
- const result = host.substitute(userId, value);
52
- for (const usedKey of result.usedKeys) used.add(usedKey);
53
- substituted[key] = result.text;
54
- }
55
- return { values: substituted, usedKeys: [...used] };
56
- }
57
-
58
- function safeResponseHeaders(
59
- userId: string,
60
- headers: Headers,
61
- host: VaultCredentialHost,
62
- ): Record<string, string> {
63
- const output: Record<string, string> = {};
64
- for (const [key, value] of headers.entries()) {
65
- if (!SAFE_RESPONSE_HEADERS.has(key.toLowerCase())) continue;
66
- output[key] = host.redact(userId, value);
67
- }
68
- return output;
69
- }
70
-
71
- async function readBoundedBody(
72
- response: Response,
73
- maxBytes: number,
74
- ): Promise<{ bytes: Uint8Array; truncated: boolean }> {
75
- if (!response.body) return { bytes: new Uint8Array(), truncated: false };
76
- const reader = response.body.getReader();
77
- const chunks: Uint8Array[] = [];
78
- let keptBytes = 0;
79
- let truncated = false;
80
- try {
81
- while (true) {
82
- const next = await reader.read();
83
- if (next.done) break;
84
- const remaining = maxBytes - keptBytes;
85
- if (remaining <= 0) {
86
- truncated = true;
87
- await reader.cancel();
88
- break;
89
- }
90
- const visible = next.value.subarray(0, remaining);
91
- chunks.push(visible);
92
- keptBytes += visible.byteLength;
93
- if (visible.byteLength < next.value.byteLength) {
94
- truncated = true;
95
- await reader.cancel();
96
- break;
97
- }
98
- }
99
- } finally {
100
- reader.releaseLock();
101
- }
102
-
103
- const bytes = new Uint8Array(keptBytes);
104
- let offset = 0;
105
- for (const chunk of chunks) {
106
- bytes.set(chunk, offset);
107
- offset += chunk.byteLength;
108
- }
109
- return { bytes, truncated };
110
- }
111
-
112
- /**
113
- * Execute an HTTPS request inside the credential boundary. The caller supplies
114
- * only {{KEY}} references; expanded headers/body never leave this function.
115
- */
116
- export async function executeVaultHttpRequest(
117
- userId: string,
118
- request: VaultHttpRequest,
119
- host: VaultCredentialHost,
120
- fetchImpl: typeof fetch = fetch,
121
- ): Promise<VaultHttpResult> {
122
- let parsedUrl: URL;
123
- try {
124
- parsedUrl = new URL(request.url);
125
- } catch {
126
- return { ok: false, error: "url must be an absolute HTTPS URL" };
127
- }
128
- if (parsedUrl.protocol !== "https:") {
129
- return {
130
- ok: false,
131
- error: "Vault credentials may only be sent over HTTPS",
132
- };
133
- }
134
- if (parsedUrl.username || parsedUrl.password || /\{\{[^}]+\}\}/.test(request.url)) {
135
- return {
136
- ok: false,
137
- error: "Keep Vault placeholders out of URLs; put credentials in headers or body",
138
- };
139
- }
140
-
141
- let headers: Record<string, string>;
142
- let headerKeys: string[];
143
- try {
144
- const substituted = substituteObject(userId, request.headers ?? {}, host);
145
- headers = substituted.values;
146
- headerKeys = substituted.usedKeys;
147
- } catch (error) {
148
- return {
149
- ok: false,
150
- error: error instanceof Error ? error.message : String(error),
151
- };
152
- }
153
-
154
- const bodyResult =
155
- request.body === undefined
156
- ? { text: undefined, usedKeys: [] as string[] }
157
- : host.substitute(userId, request.body);
158
- const usedKeys = [...new Set([...headerKeys, ...bodyResult.usedKeys])].sort();
159
- if (usedKeys.length === 0) {
160
- return {
161
- ok: false,
162
- error: "No valid Vault placeholder was found in headers or body",
163
- };
164
- }
165
- if (request.method === "GET" && request.body !== undefined) {
166
- return {
167
- ok: false,
168
- error: "GET requests cannot include a credential-bearing body",
169
- };
170
- }
171
-
172
- const controller = new AbortController();
173
- const timeoutMs = Math.min(Math.max(request.timeoutMs ?? 30_000, 1_000), 120_000);
174
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
175
- const startedAt = Date.now();
176
- try {
177
- const response = await fetchImpl(parsedUrl, {
178
- method: request.method,
179
- headers,
180
- body: request.method === "GET" ? undefined : bodyResult.text,
181
- redirect: "manual",
182
- signal: controller.signal,
183
- });
184
- const maxBytes = Math.min(Math.max(request.maxResponseBytes ?? 256 * 1024, 1_024), 1024 * 1024);
185
- const { bytes, truncated } = await readBoundedBody(response, maxBytes);
186
- const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
187
- const textual =
188
- contentType.startsWith("text/") ||
189
- contentType.includes("json") ||
190
- contentType.includes("xml") ||
191
- contentType.includes("javascript") ||
192
- contentType === "";
193
- const body = textual
194
- ? host.redact(userId, new TextDecoder().decode(bytes))
195
- : `[binary response omitted: ${bytes.byteLength} bytes, ${contentType || "unknown content type"}]`;
196
-
197
- host.log?.(
198
- "info",
199
- {
200
- userId,
201
- vaultKeys: usedKeys,
202
- host: parsedUrl.hostname,
203
- method: request.method,
204
- status: response.status,
205
- durationMs: Date.now() - startedAt,
206
- },
207
- "vault credential used",
208
- );
209
-
210
- return {
211
- ok: response.ok,
212
- status: response.status,
213
- statusText: response.statusText,
214
- headers: safeResponseHeaders(userId, response.headers, host),
215
- body,
216
- truncated,
217
- };
218
- } catch (error) {
219
- const raw = error instanceof Error ? error.message : String(error);
220
- host.log?.(
221
- "warn",
222
- {
223
- userId,
224
- vaultKeys: usedKeys,
225
- host: parsedUrl.hostname,
226
- method: request.method,
227
- durationMs: Date.now() - startedAt,
228
- },
229
- "vault credential request failed",
230
- );
231
- return { ok: false, error: host.redact(userId, raw) };
232
- } finally {
233
- clearTimeout(timeout);
234
- }
235
- }