feat: inject the run-scoped provider tool bridge (#970) (#973)

This commit is contained in:
Brad Groux 2026-07-24 08:17:35 -05:00 committed by GitHub
parent 9c15d348fa
commit 4cc5891f82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1204 additions and 47 deletions

View file

@ -137,6 +137,11 @@ Do not run `npm install`, `yarn`, or `bun install`. If lockfile conflicts arise,
mediated invocation issues exact-action leases using the server-owned launch
manifest digest. Credential-bound sessions are one-shot and raw values may
exist only inside the controlled downstream dispatch callback.
- Providers access credential-bound tools only through the system-owned
`veritas-run` MCP bridge and an opaque in-memory run handle. Codex CLI/SDK,
Codex app-server, Claude Code, and ACP stdio inject this shared contract;
Hermes and OpenClaw fail closed until their certified transports can enforce
it.
- Classify launch credentials through `run-launch-credential-plan/v1`.
Provider boot authentication, task integration definition IDs, and explicit
high-risk environment passthrough are separate classes. Task integration

View file

@ -26,6 +26,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added the system-owned `veritas-run` MCP bridge for credential-bound tools.
Opaque, in-memory authority binds the exact task, attempt, catalog, launch
manifest, and two allowed bridge methods. Codex CLI/SDK, Codex app-server,
Claude Code, and ACP stdio inject the same narrow bridge; Hermes and OpenClaw
fail closed before dispatch when a credential-bound catalog is selected
(#970).
- Added exact-action credential lease consumption to mediated run-tool calls.
The server binds each operation to the active launch manifest, catalog,
server, tool, arguments digest, approval, and operation ID; resolves values
@ -38,8 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
definitions, exact source targets, and immutable definition/scope digests.
Discovery strips source environment and header values, native provider
configuration omits credential-bound servers, covered launch references
report a tool-control-plane boundary, and uncovered, drifted, or invoked
references remain fail closed pending exact-action lease consumption (#968).
report a tool-control-plane boundary, and uncovered or drifted references
remain fail closed (#968).
- Added `vk acp serve --stdio` and `vk acp status --json` to expose a
task-bound, provider-neutral ACP v1 server view over Veritas-managed
conversations. Fresh prompts retain the immutable task envelope, reconnect

View file

@ -762,8 +762,12 @@ Credential-bound tool definitions compile only when enabled broker definitions,
MCP scopes, source targets, and immutable catalog evidence match. They are
omitted from native provider MCP configuration and provider environment
passthrough. Mediated calls consume exact-action leases using the server-owned
launch-manifest digest and one-shot downstream sessions. System-owned provider
bridge injection remains under #970.
launch-manifest digest and one-shot downstream sessions. The system-owned
`veritas-run` MCP bridge exposes only catalog read and mediated call methods
through an opaque, in-memory run handle. Codex CLI/SDK, Codex app-server,
Claude Code, and ACP stdio inject it. Hermes and OpenClaw reject
credential-bound launches because their certified transports cannot yet
enforce the same bridge contract.
Model-provider boot authentication and explicit `env-passthrough`
compatibility remain separate, high-risk paths and are never labeled as
brokered. See [Credential Broker](CREDENTIAL-BROKER.md).

View file

@ -3812,6 +3812,14 @@ MCP action and receive the active launch-manifest digest from the server, not
the request body. Values are resolved only inside a one-shot downstream
dispatch callback; replays, stale evidence, approval mismatch, unavailable
sources, and credential-bearing results fail closed.
Credential-bound provider runs receive a system-owned `veritas-run` stdio MCP
bridge. Its opaque handle is accepted only by `GET
/api/run-tool-bridge/catalog` and `POST /api/run-tool-bridge/call`. Those
dedicated routes derive task, attempt, catalog, and manifest identity from the
handle; callers cannot supply or override them. The handle grants no general
Veritas API authority and is rejected after terminal lifecycle cleanup,
expiration, or server restart.
See [Tool Control Plane v1](architecture/TOOL-CONTROL-PLANE-V1.md).
---

View file

@ -19,9 +19,11 @@ The v6 foundation includes:
The tool control plane compiles value-free credential boundary evidence into a
run catalog and consumes leases only inside mediated tool calls. A handle in a
prompt or provider environment is still not a security boundary:
credential-bound native server injection remains omitted, and automatic
system-owned provider bridge injection is tracked by #970.
prompt or provider environment is not itself a credential boundary:
credential-bound native server injection remains omitted. A system-owned
`veritas-run` MCP bridge receives only an opaque, in-memory authority bound to
the exact task, attempt, catalog, launch manifest, and catalog/call methods.
The provider never receives the task credential value.
## Credential classes

View file

@ -377,6 +377,10 @@ persists an immutable `run-tool-catalog/v1` digest in the launch manifest.
server-owned launch manifest, deliver values only inside one-shot downstream
sessions, and reject replay, drift, approval mismatch, source failure, or
credential-bearing results.
- Credential-bound runs inject the same narrow `veritas-run` MCP bridge into
Codex CLI/SDK, Codex app-server, Claude Code, and ACP stdio. The bridge
carries an opaque run handle, never a task credential. Hermes and OpenClaw
fail closed until their certified transports can enforce this contract.
See [Tool Control Plane v1](architecture/TOOL-CONTROL-PLANE-V1.md).

View file

@ -156,11 +156,25 @@ Approval-required definitions reuse the durable approval for the same
operation and credential-action fingerprint. Replayed operations, caller
manifest overrides, stale run bindings, changed definitions/scopes, mismatched
approvals, unavailable sources, and credential-bearing results fail closed.
System-owned provider bridge injection remains under #970.
The system-owned `veritas-run` stdio MCP bridge exposes only catalog read and
mediated call. Its opaque authority is held in memory and bound to the exact
task, attempt, catalog, launch manifest, expiry, and allowed bridge methods.
The dedicated HTTP surface derives run identity from that authority, so the
provider cannot choose another task, attempt, catalog, or manifest.
Codex CLI/SDK, Codex app-server, Claude Code, and ACP stdio inject this same
bridge contract. Credential-bound native definitions remain omitted. Hermes
and OpenClaw fail closed at manifest compilation because their certified
transports cannot yet enforce system-owned bridge injection. Completion,
failure, interruption, cancellation, expiry, or restart revokes or rejects the
authority.
## Operator Surfaces
- REST: `/api/v1/tool-servers`
- Run bridge: `/api/run-tool-bridge/catalog` and
`/api/run-tool-bridge/call` (opaque run authority only)
- CLI: `vk tool-servers` (alias `vk tools`)
- MCP: `list_tool_servers`, `discover_tool_server`,
`get_run_tool_catalog`, and `call_run_tool`

View file

@ -77,15 +77,18 @@ export default [
},
},
// Node release/maintenance scripts
// Node release/maintenance scripts and dependency-free runtime assets
{
files: ['scripts/**/*.mjs'],
files: ['scripts/**/*.mjs', 'server/runtime/**/*.mjs'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: {
Buffer: 'readonly',
console: 'readonly',
fetch: 'readonly',
process: 'readonly',
URL: 'readonly',
},
},
},

View file

@ -0,0 +1,239 @@
#!/usr/bin/env node
import readline from 'node:readline';
import { readFileSync } from 'node:fs';
const MAX_RECORD_BYTES = 1024 * 1024;
const PROTOCOL_VERSION = '2025-06-18';
const HANDLE_PATTERN = /^vkbridge_[A-Za-z0-9_-]{32,}$/;
const SERVER_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,79}$/;
const TOOL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,239}$/;
const OPERATION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/;
const packageVersion = JSON.parse(
readFileSync(new URL('../package.json', import.meta.url), 'utf8')
).version;
const tools = [
{
name: 'get_run_tool_catalog',
description: 'Read the immutable tool catalog bound to this exact Veritas run',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false,
},
},
{
name: 'call_run_tool',
description:
'Invoke an allowed tool through this run authority with policy, approval, credential, redaction, and causal event enforcement',
inputSchema: {
type: 'object',
properties: {
serverId: { type: 'string' },
tool: { type: 'string' },
arguments: { type: 'object', additionalProperties: true },
operationId: { type: 'string' },
approvalId: { type: 'string' },
},
required: ['serverId', 'tool', 'arguments', 'operationId'],
additionalProperties: false,
},
},
];
const handle = process.env.VK_RUN_TOOL_BRIDGE_HANDLE;
const apiUrl = normalizeApiUrl(process.env.VK_API_URL || 'http://localhost:3001');
if (!HANDLE_PATTERN.test(handle || '')) {
throw new Error('Run tool bridge handle is missing or invalid.');
}
const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
input.on('line', (line) => {
void accept(line);
});
input.on('close', () => {
process.exitCode = 0;
});
async function accept(line) {
if (!line.trim()) return;
if (Buffer.byteLength(line, 'utf8') > MAX_RECORD_BYTES) {
writeError(null, -32600, 'MCP request exceeded the bounded record limit.');
return;
}
let record;
try {
record = JSON.parse(line);
} catch {
writeError(null, -32700, 'MCP request was not valid JSON.');
return;
}
if (!record || typeof record !== 'object' || Array.isArray(record)) {
writeError(null, -32600, 'MCP request must be a JSON object.');
return;
}
if (!Object.hasOwn(record, 'id')) return;
try {
writeResult(record.id, await dispatch(record.method, record.params));
} catch (error) {
writeError(
record.id,
error instanceof InvalidParamsError ? -32602 : -32603,
error instanceof Error ? error.message : 'Run tool bridge request failed.'
);
}
}
async function dispatch(method, params) {
if (method === 'initialize') {
return {
protocolVersion: PROTOCOL_VERSION,
capabilities: { tools: {} },
serverInfo: { name: 'veritas-run-tools', version: packageVersion },
};
}
if (method === 'ping') return {};
if (method === 'tools/list') {
assertEmptyObject(params);
return { tools };
}
if (method === 'tools/call') {
const call = object(params, 'tools/call params');
const name = string(call.name, 'tools/call name');
if (!['get_run_tool_catalog', 'call_run_tool'].includes(name)) {
throw new InvalidParamsError(`Unknown run tool bridge action: ${name}`);
}
const argumentsValue = call.arguments ?? {};
const content =
name === 'get_run_tool_catalog'
? await request('/api/run-tool-bridge/catalog')
: await request('/api/run-tool-bridge/call', {
method: 'POST',
body: JSON.stringify(parseCall(argumentsValue)),
});
return {
content: [{ type: 'text', text: JSON.stringify(content, null, 2) }],
};
}
throw new InvalidParamsError(`Unsupported MCP method: ${String(method)}`);
}
async function request(requestPath, options = {}) {
const response = await fetch(`${apiUrl}${requestPath}`, {
...options,
headers: {
'content-type': 'application/json',
'x-vk-run-tool-bridge': handle,
...options.headers,
},
});
const body = await response.json().catch(() => undefined);
if (!response.ok) {
throw new Error(apiError(body) || `Run tool bridge request failed (${response.status}).`);
}
if (
body &&
typeof body === 'object' &&
!Array.isArray(body) &&
body.success === true &&
Object.hasOwn(body, 'data')
) {
return body.data;
}
return body;
}
function parseCall(value) {
const call = object(value, 'call_run_tool arguments');
const serverId = string(call.serverId, 'serverId');
const tool = string(call.tool, 'tool');
const operationId = string(call.operationId, 'operationId');
const argumentsValue = object(call.arguments, 'arguments');
const approvalId =
call.approvalId === undefined ? undefined : string(call.approvalId, 'approvalId');
if (!SERVER_ID_PATTERN.test(serverId)) throw new InvalidParamsError('serverId is invalid.');
if (!TOOL_PATTERN.test(tool)) throw new InvalidParamsError('tool is invalid.');
if (!OPERATION_PATTERN.test(operationId)) throw new InvalidParamsError('operationId is invalid.');
if (approvalId && !OPERATION_PATTERN.test(approvalId)) {
throw new InvalidParamsError('approvalId is invalid.');
}
const allowed = new Set(['serverId', 'tool', 'arguments', 'operationId', 'approvalId']);
if (Object.keys(call).some((key) => !allowed.has(key))) {
throw new InvalidParamsError('call_run_tool contains unsupported fields.');
}
return {
serverId,
tool,
arguments: argumentsValue,
operationId,
...(approvalId ? { approvalId } : {}),
};
}
function assertEmptyObject(value) {
const inputValue = value ?? {};
if (
!inputValue ||
typeof inputValue !== 'object' ||
Array.isArray(inputValue) ||
Object.keys(inputValue).length > 0
) {
throw new InvalidParamsError('Expected an empty object.');
}
}
function object(value, label) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidParamsError(`${label} must be an object.`);
}
return value;
}
function string(value, label) {
if (typeof value !== 'string' || !value.trim()) {
throw new InvalidParamsError(`${label} must be a non-empty string.`);
}
return value.trim();
}
function normalizeApiUrl(value) {
const url = new URL(value);
const loopback = ['localhost', '127.0.0.1', '::1'].includes(url.hostname);
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) {
throw new Error('Run tool bridge API URL must use HTTPS or loopback HTTP.');
}
url.pathname = url.pathname.replace(/\/+$/, '');
url.search = '';
url.hash = '';
return url.toString().replace(/\/$/, '');
}
function apiError(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
const envelope =
value.error && typeof value.error === 'object' && !Array.isArray(value.error)
? value.error
: undefined;
const message =
(typeof envelope?.message === 'string' && envelope.message) ||
(typeof value.message === 'string' && value.message) ||
(typeof value.error === 'string' && value.error);
if (!message) return undefined;
const details = envelope?.details ?? value.details;
return details === undefined ? message : `${message} ${JSON.stringify(details)}`;
}
function writeResult(id, result) {
write({ jsonrpc: '2.0', id, result });
}
function writeError(id, code, message) {
write({ jsonrpc: '2.0', id, error: { code, message } });
}
function write(record) {
process.stdout.write(`${JSON.stringify(record)}\n`);
}
class InvalidParamsError extends Error {}

View file

@ -449,6 +449,79 @@ describe('RunLaunchManifestService', () => {
});
});
it('fails closed when the selected provider cannot inject the run tool bridge', () => {
const hermesRuntime = providerRuntimeManifestFixture({
provider: 'hermes-cli',
providerVersion: 'hermes 2026.7.7.2',
});
const hermesEnvelope: TaskEnvelope = {
...taskEnvelope,
launchManifest: {
schemaVersion: hermesRuntime.schemaVersion,
digest: hermesRuntime.digest,
provider: hermesRuntime.provider,
adapter: hermesRuntime.adapter,
protocolVersion: hermesRuntime.protocolVersion,
},
};
const catalogInput = {
...brokeredCatalog(),
provider: 'hermes-cli' as const,
providerRuntimeManifestDigest: hermesRuntime.digest,
};
const runToolCatalog = {
...catalogInput,
digest: calculateRunToolCatalogDigest(catalogInput),
};
const brokeredPolicy: SandboxPolicyDryRunResult = {
...sandboxPolicy,
provider: 'hermes-cli',
preset: {
...sandboxPolicy.preset,
credentials: { mode: 'brokered', brokerRefs: ['github-token'] },
},
effective: {
...sandboxPolicy.effective,
credentialRefs: ['github-token'],
},
};
const manifest = new RunLaunchManifestService().compile(
input({
taskEnvelope: hermesEnvelope,
providerRuntimeManifest: hermesRuntime,
harnessSupport: {
...harnessSupport,
agentType: 'hermes',
profileId: 'hermes-cli',
adapterId: 'hermes-cli',
},
sandboxPolicy: brokeredPolicy,
runToolCatalog,
tools: {
allowed: [],
denied: [],
policyIds: [],
mcpServers: ['github-tools'],
catalogDigest: runToolCatalog.digest,
enforcement: 'enforced',
},
runtime: {
...input().runtime,
command: 'hermes',
credentialReferences: ['github-token'],
},
})
);
expect(manifest.credentials?.brokerState).toBe('supported');
expect(manifest.enforcement).toMatchObject({
enforceable: false,
blockers: expect.arrayContaining([
expect.objectContaining({ code: 'run-tool-bridge-unavailable' }),
]),
});
});
it('fails closed for declared tools, MCP servers, permissions, and health checks without enforcement', () => {
const manifest = new RunLaunchManifestService().compile(
input({

View file

@ -0,0 +1,151 @@
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
import { createServer } from 'node:http';
import type { AddressInfo } from 'node:net';
import path from 'node:path';
import readline from 'node:readline';
import { fileURLToPath } from 'node:url';
import { afterEach, describe, expect, it } from 'vitest';
const HANDLE = `vkbridge_${'h'.repeat(43)}`;
const runtimePath = fileURLToPath(new URL('../../runtime/run-tool-bridge.mjs', import.meta.url));
const children: ChildProcessWithoutNullStreams[] = [];
afterEach(() => {
for (const child of children.splice(0)) {
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
}
});
describe('run tool bridge runtime', () => {
it('serves the narrow MCP contract and derives run identity from its opaque header', async () => {
const requests: Array<{
method?: string;
url?: string;
header?: string;
body?: Record<string, unknown>;
}> = [];
const api = createServer((request, response) => {
let body = '';
request.setEncoding('utf8');
request.on('data', (chunk) => {
body += chunk;
});
request.on('end', () => {
requests.push({
method: request.method,
url: request.url,
header: request.headers['x-vk-run-tool-bridge'] as string | undefined,
...(body ? { body: JSON.parse(body) as Record<string, unknown> } : {}),
});
response.setHeader('content-type', 'application/json');
response.end(
JSON.stringify(
request.url?.endsWith('/catalog')
? { digest: 'catalog-digest' }
: { success: true, data: { operationId: 'operation-1', content: 'found' } }
)
);
});
});
await new Promise<void>((resolve) => api.listen(0, '127.0.0.1', resolve));
const port = (api.address() as AddressInfo).port;
const child = spawn(process.execPath, [runtimePath], {
cwd: path.dirname(runtimePath),
env: {
VK_API_URL: `http://127.0.0.1:${port}`,
VK_RUN_TOOL_BRIDGE_HANDLE: HANDLE,
},
shell: false,
});
children.push(child);
const output = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
const lines: Array<(value: Record<string, unknown>) => void> = [];
output.on('line', (line) => {
lines.shift()?.(JSON.parse(line) as Record<string, unknown>);
});
const rpc = (record: Record<string, unknown>) =>
new Promise<Record<string, unknown>>((resolve) => {
lines.push(resolve);
child.stdin.write(`${JSON.stringify(record)}\n`);
});
const initialized = await rpc({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: { protocolVersion: '2025-06-18' },
});
expect(initialized).toMatchObject({
result: {
protocolVersion: '2025-06-18',
serverInfo: { name: 'veritas-run-tools' },
},
});
const listed = await rpc({
jsonrpc: '2.0',
id: 2,
method: 'tools/list',
params: {},
});
expect(
(listed.result as { tools: Array<{ name: string }> }).tools.map((tool) => tool.name)
).toEqual(['get_run_tool_catalog', 'call_run_tool']);
await rpc({
jsonrpc: '2.0',
id: 3,
method: 'tools/call',
params: { name: 'get_run_tool_catalog', arguments: {} },
});
const called = await rpc({
jsonrpc: '2.0',
id: 4,
method: 'tools/call',
params: {
name: 'call_run_tool',
arguments: {
serverId: 'github-tools',
tool: 'search',
arguments: { query: 'roadmap' },
operationId: 'operation-1',
},
},
});
expect(called).toMatchObject({
result: {
content: [
{
type: 'text',
text: expect.stringContaining('operation-1'),
},
],
},
});
expect(requests).toEqual([
{
method: 'GET',
url: '/api/run-tool-bridge/catalog',
header: HANDLE,
},
{
method: 'POST',
url: '/api/run-tool-bridge/call',
header: HANDLE,
body: {
serverId: 'github-tools',
tool: 'search',
arguments: { query: 'roadmap' },
operationId: 'operation-1',
},
},
]);
expect(JSON.stringify(requests)).not.toContain('taskId');
expect(JSON.stringify(requests)).not.toContain('attemptId');
child.stdin.end();
await new Promise<void>((resolve) => child.once('close', () => resolve()));
await new Promise<void>((resolve, reject) =>
api.close((error) => (error ? reject(error) : resolve()))
);
});
});

View file

@ -0,0 +1,137 @@
import { describe, expect, it } from 'vitest';
import type { ExecutableAgentProvider, RunToolCatalog } from '@veritas-kanban/shared';
import {
RUN_TOOL_BRIDGE_ENV_KEY,
RunToolBridgeService,
runToolBridgeSupport,
} from '../services/run-tool-bridge-service.js';
import { buildSafeCodexEnv } from '../utils/codex-env.js';
const DIGEST = `sha256:${'a'.repeat(64)}`;
const HANDLE = `vkbridge_${'h'.repeat(43)}`;
function service(now = new Date('2026-07-24T12:00:00.000Z')) {
return new RunToolBridgeService({
now: () => now,
randomHandle: () => HANDLE,
ttlMs: 60_000,
entrypoint: '/opt/veritas/server/runtime/run-tool-bridge.mjs',
apiUrl: 'http://127.0.0.1:3001',
});
}
function binding() {
return {
taskId: 'task-970',
attemptId: 'attempt-970',
catalogDigest: DIGEST,
runLaunchManifestDigest: `sha256:${'b'.repeat(64)}`,
};
}
describe('RunToolBridgeService', () => {
it('binds opaque authority to the exact run evidence and allowed method', () => {
const bridge = service();
const launch = bridge.issue(binding());
expect(bridge.authorize(launch.handle, 'catalog.read', binding())).toMatchObject({
...binding(),
handleId: expect.stringMatching(/^vkbridge_[a-f0-9]{16}$/),
allowedMethods: ['catalog.read', 'tool.call'],
});
expect(() => bridge.authorize(launch.handle, 'tool.call', { taskId: 'task-other' })).toThrow(
/taskId does not match/
);
expect(() =>
bridge.authorize(launch.handle, 'tool.call', { attemptId: 'attempt-other' })
).toThrow(/attemptId does not match/);
expect(() =>
bridge.authorize(launch.handle, 'tool.call', {
catalogDigest: `sha256:${'c'.repeat(64)}`,
})
).toThrow(/catalogDigest does not match/);
});
it('rejects revoked, expired, and restart-stale handles', () => {
const bridge = service();
const launch = bridge.issue(binding());
expect(bridge.revokeRun(binding().taskId, binding().attemptId)).toBe(1);
expect(() => bridge.authorize(launch.handle, 'catalog.read')).toThrow(/stale or revoked/);
let clock = new Date('2026-07-24T12:00:00.000Z');
const expiring = new RunToolBridgeService({
now: () => clock,
randomHandle: () => `vkbridge_${'e'.repeat(43)}`,
ttlMs: 1,
entrypoint: '/opt/veritas/server/runtime/run-tool-bridge.mjs',
apiUrl: 'https://veritas.example',
});
const expiringLaunch = expiring.issue(binding());
clock = new Date('2026-07-24T12:00:01.000Z');
expect(() => expiring.authorize(expiringLaunch.handle, 'catalog.read')).toThrow(/expired/);
const oldLaunch = service().issue(binding());
expect(() => service().authorize(oldLaunch.handle, 'catalog.read')).toThrow(/stale or revoked/);
});
it('uses one value-free bridge contract across Codex, Claude Code, and ACP adapters', () => {
const bridge = service();
const launch = bridge.issue(binding());
const codex = bridge.codexConfig(launch);
const codexCli = bridge.codexCliOverride(launch);
const claude = bridge.claudeServer(launch);
const acp = bridge.acpServer(launch);
const environment = bridge.launchEnvironment(
buildSafeCodexEnv({
OPENAI_API_KEY: 'provider-auth',
TASK_CREDENTIAL_TOKEN: 'task-credential-sensitive-value',
}),
launch
);
const serialized = JSON.stringify({ codex, codexCli, claude, acp, environment });
expect(serialized).toContain(HANDLE);
expect(serialized).toContain(RUN_TOOL_BRIDGE_ENV_KEY);
expect(serialized).toContain('/opt/veritas/server/runtime/run-tool-bridge.mjs');
expect(serialized).toContain('get_run_tool_catalog');
expect(serialized).toContain('call_run_tool');
expect(serialized).not.toContain('task-credential-sensitive-value');
expect(codexCli).not.toContain(HANDLE);
});
it('publishes an explicit adapter table and fails closed for unverified injection', () => {
const expected: Record<ExecutableAgentProvider, boolean> = {
'codex-cli': true,
'codex-sdk': true,
'codex-app-server': true,
'claude-code': true,
'acp-stdio': true,
'hermes-cli': false,
openclaw: false,
};
expect(
Object.fromEntries(
Object.keys(expected).map((provider) => [
provider,
runToolBridgeSupport(provider as ExecutableAgentProvider).supported,
])
)
).toEqual(expected);
});
it('detects credential-bound catalogs and rejects insecure remote API URLs', () => {
const bridge = service();
const catalog = {
entries: [{ status: 'ready', credentialBindings: [{ credentialReference: 'ref' }] }],
} as RunToolCatalog;
expect(bridge.requiresBridge(catalog)).toBe(true);
expect(
() =>
new RunToolBridgeService({
apiUrl: 'http://veritas.example',
entrypoint: '/opt/veritas/server/runtime/run-tool-bridge.mjs',
})
).toThrow(/HTTPS or loopback HTTP/);
});
});

View file

@ -0,0 +1,87 @@
import { Router, type Request, type Router as RouterType } from 'express';
import type { ToolInvocationRequest } from '@veritas-kanban/shared';
import { asyncHandler } from '../middleware/async-handler.js';
import { ConflictError, NotFoundError } from '../middleware/error-handler.js';
import { validate, type ValidatedRequest } from '../middleware/validate.js';
import { toolInvocationRequestSchema } from '../schemas/tool-control-plane-schemas.js';
import { getRunToolBridgeService } from '../services/run-tool-bridge-service.js';
import { getTaskService } from '../services/task-service.js';
import { getToolControlPlaneService } from '../services/tool-control-plane-service.js';
const router: RouterType = Router();
const bridge = getRunToolBridgeService();
const tools = getToolControlPlaneService();
const bridgeCallSchema = toolInvocationRequestSchema.omit({
taskId: true,
attemptId: true,
});
function handle(req: Request): string | undefined {
const value = req.headers['x-vk-run-tool-bridge'];
return typeof value === 'string' ? value : undefined;
}
router.get(
'/catalog',
asyncHandler(async (req, res) => {
const authority = bridge.authorize(handle(req), 'catalog.read');
const { catalog } = await activeContext(authority);
res.json(catalog);
})
);
router.post(
'/call',
validate({ body: bridgeCallSchema }),
asyncHandler(
async (
req: ValidatedRequest<unknown, unknown, Omit<ToolInvocationRequest, 'taskId' | 'attemptId'>>,
res
) => {
const authority = bridge.authorize(handle(req), 'tool.call');
const input = req.validated.body as Omit<ToolInvocationRequest, 'taskId' | 'attemptId'>;
const { task } = await activeContext(authority);
res.json(
await tools.invoke(
{
...input,
taskId: authority.taskId,
attemptId: authority.attemptId,
},
authority.handleId,
task.git?.worktreePath,
authority.runLaunchManifestDigest
)
);
}
)
);
async function activeContext(authority: ReturnType<typeof bridge.authorize>) {
const task = await getTaskService().getTask(authority.taskId);
if (!task) {
bridge.revokeRun(authority.taskId, authority.attemptId);
throw new NotFoundError('Run tool bridge task not found.');
}
if (task.attempt?.id !== authority.attemptId || task.attempt.status !== 'running') {
bridge.revokeRun(authority.taskId, authority.attemptId);
throw new ConflictError('Run tool bridge authority does not match an active attempt.');
}
const catalog = await tools
.getRunCatalog(authority.taskId, authority.attemptId)
.catch((error) => {
bridge.revokeRun(authority.taskId, authority.attemptId);
throw error;
});
if (
catalog.digest !== authority.catalogDigest ||
task.attempt.runLaunchManifest?.digest !== authority.runLaunchManifestDigest ||
task.attempt.runLaunchManifest.tools.catalogDigest !== authority.catalogDigest
) {
bridge.revokeRun(authority.taskId, authority.attemptId);
throw new ConflictError('Run tool bridge launch evidence drifted from the active attempt.');
}
return { task, catalog };
}
export { router as runToolBridgeRoutes };

View file

@ -81,6 +81,7 @@ import { startAfterInitialization } from './utils/startup-gate.js';
import { getCommunicationAdapterService } from './services/communication-adapter-service.js';
import { getRunEventJournalService } from './services/run-event-journal-service.js';
import { getToolControlPlaneService } from './services/tool-control-plane-service.js';
import { runToolBridgeRoutes } from './routes/run-tool-bridge.js';
const log = createLogger('server');
@ -417,6 +418,10 @@ app.use('/api', apiRateLimit);
// Unauthenticated webhook routes (registered BEFORE authenticate middleware)
app.use('/api/webhook', webhookN8nRouter);
// Opaque run-scoped authority. This route is intentionally outside broad API
// authentication and exposes only the immutable catalog and mediated tool call.
app.use('/api/run-tool-bridge', runToolBridgeRoutes);
// Apply authentication to all API routes (except /api/auth which is handled above)
app.use('/api', authenticate);

View file

@ -213,6 +213,13 @@ import {
getToolControlPlaneService,
type ToolControlPlaneService,
} from './tool-control-plane-service.js';
import {
getRunToolBridgeService,
RUN_TOOL_BRIDGE_ENV_KEY,
RUN_TOOL_BRIDGE_SERVER_ID,
type RunToolBridgeLaunch,
type RunToolBridgeService,
} from './run-tool-bridge-service.js';
import { getToolPolicyService } from './tool-policy-service.js';
const log = createLogger('clawdbot-agent-service');
@ -361,6 +368,7 @@ interface PendingAgent {
close(): void;
};
acpControl?: AcpStdioControl;
runToolBridge?: RunToolBridgeLaunch;
/** Durable session key returned by OpenClaw sessions_spawn (openclaw provider only) */
openclawSessionKey?: string;
/** Hermes session identity captured from process output (hermes-cli provider only) */
@ -451,6 +459,7 @@ export class ClawdbotAgentService {
private runSupervisor: RunSupervisorService;
private conversationLifecycle: ConversationLifecycleService;
private toolControlPlane: ToolControlPlaneService;
private runToolBridge: RunToolBridgeService;
private logsDir: string;
constructor(
@ -465,7 +474,8 @@ export class ClawdbotAgentService {
approvalBroker: RunApprovalBrokerService = getRunApprovalBrokerService(),
runSupervisor: RunSupervisorService = getRunSupervisorService(),
conversationLifecycle = new ConversationLifecycleService(),
toolControlPlane: ToolControlPlaneService = getToolControlPlaneService()
toolControlPlane: ToolControlPlaneService = getToolControlPlaneService(),
runToolBridge: RunToolBridgeService = getRunToolBridgeService()
) {
this.configService = new ConfigService();
this.taskService = new TaskService();
@ -487,6 +497,7 @@ export class ClawdbotAgentService {
this.runSupervisor = runSupervisor;
this.conversationLifecycle = conversationLifecycle;
this.toolControlPlane = toolControlPlane;
this.runToolBridge = runToolBridge;
this.logsDir = getLogsDir();
this.ensureLogsDir();
}
@ -1398,7 +1409,6 @@ export class ClawdbotAgentService {
conversationRequest.forkTurnId,
conversationRequest.intent
);
// Create event emitter for status updates
const emitter = new EventEmitter();
@ -1481,6 +1491,19 @@ export class ClawdbotAgentService {
task = claimedTask;
}
try {
const pending = pendingAgents.get(taskId);
if (!pending || pending.attemptId !== attemptId) {
throw new ConflictError('Run launch no longer matches the pending attempt.');
}
pending.runToolBridge =
runToolCatalog && this.runToolBridge.requiresBridge(runToolCatalog)
? this.runToolBridge.issue({
taskId,
attemptId,
catalogDigest: runToolCatalog.digest,
runLaunchManifestDigest: runLaunchManifest.digest,
})
: undefined;
await this.taskService.updateTask(taskId, {
status: 'in-progress',
attempt,
@ -1488,6 +1511,7 @@ export class ClawdbotAgentService {
});
} catch (error) {
pendingAgents.delete(taskId);
this.runToolBridge.revokeRun(taskId, attemptId);
if (usesManagedWorktree) {
await this.worktrees.releaseOwnership(taskId, attemptId).catch((releaseError) => {
log.error(
@ -2645,12 +2669,16 @@ export class ClawdbotAgentService {
: status === 'interrupted'
? 'run-interrupted'
: 'run-failed';
await this.credentialLeases.revokeRun({
taskId,
attemptId,
...(runLaunchManifestDigest ? { runLaunchManifestDigest } : {}),
reason,
});
try {
await this.credentialLeases.revokeRun({
taskId,
attemptId,
...(runLaunchManifestDigest ? { runLaunchManifestDigest } : {}),
reason,
});
} finally {
this.runToolBridge.revokeRun(taskId, attemptId);
}
}
private async persistPendingCompletion(
@ -3506,7 +3534,8 @@ export class ClawdbotAgentService {
attemptId,
startedAt,
emitter,
sandboxPolicy
sandboxPolicy,
runLaunchManifest
);
},
stop: ({ pending }) => {
@ -3546,7 +3575,8 @@ export class ClawdbotAgentService {
startedAt,
emitter,
abortController,
sandboxPolicy
sandboxPolicy,
runLaunchManifest
).catch(async (error: unknown) => {
const current = pendingAgents.get(task.id);
if (!current || current.attemptId !== attemptId) return;
@ -4070,6 +4100,9 @@ export class ClawdbotAgentService {
throw new ConflictError('ACP run tool catalog does not match launch evidence.');
}
const mcpServers = runToolCatalog ? await this.toolControlPlane.acpConfig(runToolCatalog) : [];
if (pending.runToolBridge) {
mcpServers.push(this.runToolBridge.acpServer(pending.runToolBridge));
}
const toolEnvironmentKeys = runToolCatalog
? await this.toolControlPlane.environmentKeys(runToolCatalog)
: [];
@ -4322,15 +4355,25 @@ export class ClawdbotAgentService {
if (!worktreePath) {
throw new Error('Task worktree path is required for Codex app-server');
}
const pending = pendingAgents.get(task.id);
if (!pending || pending.attemptId !== attemptId) {
throw new ConflictError('Codex app-server launch was cancelled before process spawn.', {
taskId: task.id,
attemptId,
});
}
const runToolCatalog = runLaunchManifest.tools.catalogDigest
? await this.toolControlPlane.getRunCatalog(task.id, attemptId)
: undefined;
if (runToolCatalog && runToolCatalog.digest !== runLaunchManifest.tools.catalogDigest) {
throw new ConflictError('Run tool catalog does not match launch evidence.');
}
const mcpServers = runToolCatalog
const mcpServers: Record<string, unknown> = runToolCatalog
? await this.toolControlPlane.providerConfig(runToolCatalog)
: undefined;
: {};
if (pending.runToolBridge) {
mcpServers[RUN_TOOL_BRIDGE_SERVER_ID] = this.runToolBridge.codexServer(pending.runToolBridge);
}
const toolEnvironmentKeys = runToolCatalog
? await this.toolControlPlane.environmentKeys(runToolCatalog)
: [];
@ -4338,21 +4381,16 @@ export class ClawdbotAgentService {
const args = buildCodexAppServerArgs(agentConfig?.args);
const child = spawn(command, args, {
cwd: worktreePath,
env: buildSafeCodexAppServerEnv(process.env, [
...(sandboxPolicy?.effective.envPassthrough ?? []),
...toolEnvironmentKeys,
]),
env: this.runToolBridge.launchEnvironment(
buildSafeCodexAppServerEnv(process.env, [
...(sandboxPolicy?.effective.envPassthrough ?? []),
...toolEnvironmentKeys,
]),
pending.runToolBridge
),
shell: false,
detached: process.platform !== 'win32',
});
const pending = pendingAgents.get(task.id);
if (!pending || pending.attemptId !== attemptId) {
child.kill('SIGTERM');
throw new ConflictError('Codex app-server launch was cancelled before process spawn.', {
taskId: task.id,
attemptId,
});
}
pending.process = child;
await this.attachSpawnedProcess(pending, child);
@ -5128,9 +5166,26 @@ export class ClawdbotAgentService {
if (runToolCatalog && runToolCatalog.digest !== runLaunchManifest.tools.catalogDigest) {
throw new ConflictError('Run tool catalog does not match launch evidence.');
}
const claudeMcp = runToolCatalog
let claudeMcp = runToolCatalog
? await this.toolControlPlane.claudeConfig(runToolCatalog)
: undefined;
if (pending.runToolBridge) {
const bridgeMcp = this.runToolBridge.claudeServer(pending.runToolBridge);
const nativeServers =
claudeMcp?.config.mcpServers &&
typeof claudeMcp.config.mcpServers === 'object' &&
!Array.isArray(claudeMcp.config.mcpServers)
? (claudeMcp.config.mcpServers as Record<string, unknown>)
: {};
const bridgeServers = bridgeMcp.config.mcpServers as Record<string, unknown>;
claudeMcp = {
config: { mcpServers: { ...nativeServers, ...bridgeServers } },
allowedToolNames: [
...(claudeMcp?.allowedToolNames ?? []),
...bridgeMcp.allowedToolNames,
].sort(),
};
}
const toolEnvironmentKeys = runToolCatalog
? await this.toolControlPlane.environmentKeys(runToolCatalog)
: [];
@ -5761,7 +5816,8 @@ export class ClawdbotAgentService {
attemptId: string,
startedAt: string,
emitter: EventEmitter,
sandboxPolicy: SandboxPolicyDryRunResult | undefined
sandboxPolicy: SandboxPolicyDryRunResult | undefined,
runLaunchManifest: RunLaunchManifest
): Promise<void> {
const worktreePath = this.expandPath(task.git?.worktreePath || '');
if (!worktreePath) {
@ -5775,6 +5831,9 @@ export class ClawdbotAgentService {
attemptId,
});
}
if (pending.runLaunchManifest.digest !== runLaunchManifest.digest) {
throw new ConflictError('Codex CLI bridge launch evidence changed before dispatch.');
}
const command = agentConfig?.command || 'codex';
const args = this.buildCodexArgs(
agentConfig,
@ -5782,11 +5841,15 @@ export class ClawdbotAgentService {
logPath,
attemptId,
sandboxPolicy,
pending.conversation
pending.conversation,
pending.runToolBridge ? this.runToolBridge.codexCliOverride(pending.runToolBridge) : undefined
);
const child = spawn(command, args, {
cwd: worktreePath,
env: buildSafeCodexEnv(process.env, sandboxPolicy?.effective.envPassthrough),
env: this.runToolBridge.launchEnvironment(
buildSafeCodexEnv(process.env, sandboxPolicy?.effective.envPassthrough),
pending.runToolBridge
),
shell: false,
detached: process.platform !== 'win32',
});
@ -5982,7 +6045,8 @@ export class ClawdbotAgentService {
logPath: string,
attemptId: string,
sandboxPolicy?: SandboxPolicyDryRunResult,
conversation?: ConversationLifecycleRecord
conversation?: ConversationLifecycleRecord,
runToolBridgeOverride?: string
): string[] {
const configured = agentConfig?.args?.length ? [...agentConfig.args] : ['exec'];
const args = configured.includes('exec') ? configured : ['exec', ...configured];
@ -5997,6 +6061,7 @@ export class ClawdbotAgentService {
if (!args.includes('--output-last-message')) {
args.push('--output-last-message', this.getCodexFinalPath(logPath, attemptId));
}
if (runToolBridgeOverride) args.push('-c', runToolBridgeOverride);
if (conversation?.mode === 'resume') {
if (!conversation.conversationId) {
throw new ConflictError('Codex CLI resume requires an exact conversation ID.');
@ -6021,20 +6086,14 @@ export class ClawdbotAgentService {
startedAt: string,
emitter: EventEmitter,
abortController: AbortController,
sandboxPolicy: SandboxPolicyDryRunResult | undefined
sandboxPolicy: SandboxPolicyDryRunResult | undefined,
runLaunchManifest: RunLaunchManifest
): Promise<void> {
const worktreePath = this.expandPath(task.git?.worktreePath || '');
if (!worktreePath) {
throw new Error('Task worktree path is required for Codex SDK');
}
const sdkExecutable = this.resolveCodexSdkExecutable(agentConfig);
const { Codex } = await import('@openai/codex-sdk');
const codex = new Codex({
codexPathOverride: sdkExecutable.codexPathOverride,
env: buildSafeCodexEnv(process.env, sandboxPolicy?.effective.envPassthrough),
});
const pending = pendingAgents.get(task.id);
if (!pending || pending.attemptId !== attemptId) {
throw new ConflictError('Codex SDK launch was cancelled before thread creation.', {
@ -6042,6 +6101,25 @@ export class ClawdbotAgentService {
attemptId,
});
}
if (pending.runLaunchManifest.digest !== runLaunchManifest.digest) {
throw new ConflictError('Codex SDK bridge launch evidence changed before dispatch.');
}
const sdkExecutable = this.resolveCodexSdkExecutable(agentConfig);
const { Codex } = await import('@openai/codex-sdk');
const codex = new Codex({
codexPathOverride: sdkExecutable.codexPathOverride,
env: this.runToolBridge.launchEnvironment(
buildSafeCodexEnv(process.env, sandboxPolicy?.effective.envPassthrough),
pending.runToolBridge
),
...(pending.runToolBridge
? {
config: this.runToolBridge.codexConfig(pending.runToolBridge) as NonNullable<
ConstructorParameters<typeof Codex>[0]
>['config'],
}
: {}),
});
const threadSettings = {
workingDirectory: worktreePath,
...this.buildCodexSdkThreadSettings(sandboxPolicy),
@ -7289,6 +7367,10 @@ export class ClawdbotAgentService {
...new Set([
...runtime.environmentKeys,
...(await this.toolControlPlane.environmentKeys(input.runToolCatalog)),
...(this.runToolBridge.requiresBridge(input.runToolCatalog) &&
this.runToolBridge.support(input.provider).injection === 'codex-config'
? [RUN_TOOL_BRIDGE_ENV_KEY]
: []),
]),
].sort();
if (input.provider === 'claude-code') {

View file

@ -1,6 +1,7 @@
import { Buffer } from 'node:buffer';
import type {
AgentBudgetPolicy,
ExecutableAgentProvider,
HarnessSupportStatus,
ProviderRuntimeManifest,
RunLaunchInstructionReference,
@ -34,6 +35,7 @@ import {
import { sanitizeProviderRuntimeDiagnostic } from '../utils/provider-runtime-manifest-sanitize.js';
import { calculateRunToolCatalogDigest } from '../utils/tool-control-plane-digest.js';
import { compileProviderLaunchCredentialPlan } from './provider-launch-credential-plan-service.js';
import { runToolBridgeSupport, runToolCatalogRequiresBridge } from './run-tool-bridge-service.js';
export interface RunLaunchManifestInstructionInput {
id: string;
@ -514,6 +516,19 @@ function collectBlockers({
'Select a credential-free preset until a controlled tool or egress boundary is configured.'
);
}
if (runToolCatalogRequiresBridge(input.runToolCatalog)) {
const support = runToolBridgeSupport(
input.providerRuntimeManifest.provider as ExecutableAgentProvider
);
if (!support.supported) {
add(
'run-tool-bridge-unavailable',
'tools.catalogDigest',
`Credential-bound tools require the system-owned Veritas bridge. ${support.reason}`,
'Select a provider with verified run-scoped MCP injection or use a credential-free tool preset.'
);
}
}
if (
tools.enforcement !== 'enforced' &&
(tools.allowed.length > 0 || tools.denied.length > 0 || tools.policyIds.length > 0)

View file

@ -0,0 +1,322 @@
import { createHash, randomBytes } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import type { AcpMcpServer, ExecutableAgentProvider, RunToolCatalog } from '@veritas-kanban/shared';
import { ConflictError, ForbiddenError } from '../middleware/error-handler.js';
export const RUN_TOOL_BRIDGE_ENV_KEY = 'VK_RUN_TOOL_BRIDGE_HANDLE';
export const RUN_TOOL_BRIDGE_SERVER_ID = 'veritas-run';
export const RUN_TOOL_BRIDGE_METHODS = ['catalog.read', 'tool.call'] as const;
export type RunToolBridgeMethod = (typeof RUN_TOOL_BRIDGE_METHODS)[number];
export interface RunToolBridgeBinding {
taskId: string;
attemptId: string;
catalogDigest: string;
runLaunchManifestDigest: string;
}
export interface RunToolBridgeLaunch extends RunToolBridgeBinding {
/** Opaque, run-local bearer authority. Never persist or log this value. */
handle: string;
handleId: string;
expiresAt: string;
}
export interface RunToolBridgeSupport {
provider: ExecutableAgentProvider;
supported: boolean;
injection: 'codex-config' | 'claude-config' | 'acp-session' | 'unavailable';
reason: string;
}
interface StoredRunToolBridgeAuthority extends RunToolBridgeBinding {
handleId: string;
allowedMethods: readonly RunToolBridgeMethod[];
expiresAt: string;
}
const SUPPORT: Record<ExecutableAgentProvider, Omit<RunToolBridgeSupport, 'provider'>> = {
'codex-cli': {
supported: true,
injection: 'codex-config',
reason: 'Codex CLI accepts system-owned MCP config overrides and inherited named variables.',
},
'codex-sdk': {
supported: true,
injection: 'codex-config',
reason: 'Codex SDK accepts the same system-owned Codex config object and launch environment.',
},
'codex-app-server': {
supported: true,
injection: 'codex-config',
reason: 'Codex app-server accepts thread-scoped MCP configuration.',
},
'claude-code': {
supported: true,
injection: 'claude-config',
reason: 'Claude Code accepts a strict system-owned MCP configuration.',
},
'acp-stdio': {
supported: true,
injection: 'acp-session',
reason: 'ACP stdio accepts run-scoped stdio MCP servers during session creation.',
},
'hermes-cli': {
supported: false,
injection: 'unavailable',
reason: 'The certified Hermes one-shot interface has no verified system-owned MCP injection.',
},
openclaw: {
supported: false,
injection: 'unavailable',
reason: 'The certified OpenClaw gateway dispatch cannot bind a local run-scoped MCP process.',
},
};
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1_000;
export interface RunToolBridgeServiceOptions {
now?: () => Date;
randomHandle?: () => string;
ttlMs?: number;
entrypoint?: string;
apiUrl?: string;
}
export function runToolBridgeSupport(provider: ExecutableAgentProvider): RunToolBridgeSupport {
return { provider, ...SUPPORT[provider] };
}
export function runToolCatalogRequiresBridge(catalog: RunToolCatalog | undefined): boolean {
return Boolean(
catalog?.entries.some(
(entry) => entry.status === 'ready' && (entry.credentialBindings?.length ?? 0) > 0
)
);
}
export class RunToolBridgeService {
private readonly authorities = new Map<string, StoredRunToolBridgeAuthority>();
private readonly now: () => Date;
private readonly randomHandle: () => string;
private readonly ttlMs: number;
private readonly entrypoint: string;
private readonly apiUrl: string;
constructor(options: RunToolBridgeServiceOptions = {}) {
this.now = options.now ?? (() => new Date());
this.randomHandle =
options.randomHandle ?? (() => `vkbridge_${randomBytes(32).toString('base64url')}`);
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
this.entrypoint = path.resolve(options.entrypoint ?? defaultBridgeEntrypoint());
this.apiUrl = normalizeBridgeApiUrl(
options.apiUrl ?? process.env.VK_API_URL ?? 'http://localhost:3001'
);
}
support(provider: ExecutableAgentProvider): RunToolBridgeSupport {
return runToolBridgeSupport(provider);
}
requiresBridge(catalog: RunToolCatalog | undefined): boolean {
return runToolCatalogRequiresBridge(catalog);
}
issue(binding: RunToolBridgeBinding): RunToolBridgeLaunch {
if (!binding.taskId || !binding.attemptId) {
throw new ConflictError('Run tool bridge authority requires an exact task and attempt.');
}
const handle = this.randomHandle();
if (!/^vkbridge_[A-Za-z0-9_-]{32,}$/.test(handle)) {
throw new ConflictError('Run tool bridge handle source returned an invalid opaque handle.');
}
const handleHash = hashHandle(handle);
if (this.authorities.has(handleHash)) {
throw new ConflictError('Run tool bridge handle source returned a duplicate handle.');
}
const handleId = `vkbridge_${handleHash.slice(0, 16)}`;
const expiresAt = new Date(this.now().getTime() + this.ttlMs).toISOString();
this.authorities.set(handleHash, {
...binding,
handleId,
allowedMethods: RUN_TOOL_BRIDGE_METHODS,
expiresAt,
});
return { ...binding, handle, handleId, expiresAt };
}
authorize(
handle: string | undefined,
method: RunToolBridgeMethod,
expected: Partial<RunToolBridgeBinding> = {}
): StoredRunToolBridgeAuthority {
if (!handle?.startsWith('vkbridge_')) {
throw new ForbiddenError('Run tool bridge authority is missing or invalid.');
}
const authority = this.authorities.get(hashHandle(handle));
if (!authority) {
throw new ForbiddenError('Run tool bridge authority is stale or revoked.');
}
if (Date.parse(authority.expiresAt) <= this.now().getTime()) {
this.authorities.delete(hashHandle(handle));
throw new ForbiddenError('Run tool bridge authority expired.');
}
if (!authority.allowedMethods.includes(method)) {
throw new ForbiddenError('Run tool bridge method is outside the run authority.');
}
for (const field of [
'taskId',
'attemptId',
'catalogDigest',
'runLaunchManifestDigest',
] as const) {
if (expected[field] && expected[field] !== authority[field]) {
throw new ForbiddenError(`Run tool bridge ${field} does not match the active authority.`);
}
}
return { ...authority };
}
revokeRun(taskId: string, attemptId: string): number {
let revoked = 0;
for (const [handleHash, authority] of this.authorities) {
if (authority.taskId === taskId && authority.attemptId === attemptId) {
this.authorities.delete(handleHash);
revoked += 1;
}
}
return revoked;
}
launchEnvironment(
source: Record<string, string>,
launch: RunToolBridgeLaunch | undefined
): Record<string, string> {
return launch
? {
...source,
VK_API_URL: this.apiUrl,
[RUN_TOOL_BRIDGE_ENV_KEY]: launch.handle,
}
: source;
}
codexServer(launch: RunToolBridgeLaunch): Record<string, unknown> {
this.assertLaunch(launch);
return {
command: process.execPath,
args: [this.entrypoint],
enabled: true,
required: true,
env_vars: ['VK_API_URL', RUN_TOOL_BRIDGE_ENV_KEY],
default_tools_approval_mode: 'approve',
enabled_tools: ['get_run_tool_catalog', 'call_run_tool'],
disabled_tools: [],
};
}
codexConfig(launch: RunToolBridgeLaunch): Record<string, unknown> {
return {
mcp_servers: {
[RUN_TOOL_BRIDGE_SERVER_ID]: this.codexServer(launch),
},
};
}
codexCliOverride(launch: RunToolBridgeLaunch): string {
this.assertLaunch(launch);
return `mcp_servers.${RUN_TOOL_BRIDGE_SERVER_ID}=${tomlInlineTable({
command: process.execPath,
args: [this.entrypoint],
enabled: true,
required: true,
env_vars: ['VK_API_URL', RUN_TOOL_BRIDGE_ENV_KEY],
enabled_tools: ['get_run_tool_catalog', 'call_run_tool'],
})}`;
}
claudeServer(launch: RunToolBridgeLaunch): {
config: Record<string, unknown>;
allowedToolNames: string[];
} {
this.assertLaunch(launch);
return {
config: {
mcpServers: {
[RUN_TOOL_BRIDGE_SERVER_ID]: {
command: process.execPath,
args: [this.entrypoint],
env: {
VK_API_URL: this.apiUrl,
[RUN_TOOL_BRIDGE_ENV_KEY]: launch.handle,
},
},
},
},
allowedToolNames: [
`mcp__${RUN_TOOL_BRIDGE_SERVER_ID}__get_run_tool_catalog`,
`mcp__${RUN_TOOL_BRIDGE_SERVER_ID}__call_run_tool`,
],
};
}
acpServer(launch: RunToolBridgeLaunch): AcpMcpServer {
this.assertLaunch(launch);
return {
name: 'Veritas run tools',
command: process.execPath,
args: [this.entrypoint],
env: [
{ name: 'VK_API_URL', value: this.apiUrl },
{ name: RUN_TOOL_BRIDGE_ENV_KEY, value: launch.handle },
],
};
}
private assertLaunch(launch: RunToolBridgeLaunch): void {
this.authorize(launch.handle, 'catalog.read', launch);
}
}
function defaultBridgeEntrypoint(): string {
return fileURLToPath(new URL('../../runtime/run-tool-bridge.mjs', import.meta.url));
}
function hashHandle(handle: string): string {
return createHash('sha256').update(handle).digest('hex');
}
function normalizeBridgeApiUrl(value: string): string {
const url = new URL(value);
const loopback = ['localhost', '127.0.0.1', '::1'].includes(url.hostname);
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) {
throw new ConflictError('Run tool bridge API URL must use HTTPS or loopback HTTP.');
}
url.pathname = url.pathname.replace(/\/+$/, '');
url.search = '';
url.hash = '';
return url.toString().replace(/\/$/, '');
}
function tomlInlineTable(value: Record<string, string | boolean | string[]>): string {
return `{${Object.entries(value)
.map(([key, child]) => {
if (Array.isArray(child))
return `${key}=[${child.map((item) => JSON.stringify(item)).join(',')}]`;
return `${key}=${typeof child === 'string' ? JSON.stringify(child) : String(child)}`;
})
.join(',')}}`;
}
let singleton: RunToolBridgeService | undefined;
export function getRunToolBridgeService(): RunToolBridgeService {
singleton ??= new RunToolBridgeService();
return singleton;
}
export function resetRunToolBridgeService(): void {
singleton = undefined;
}