openvisio-agent 0.18.11 → 0.18.12
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/package.json +1 -1
- package/scripts/certify.mjs +2 -0
- package/src/codex-config.mjs +4 -6
- package/src/codex-mcp-proxy.mjs +67 -0
- package/src/watch.mjs +20 -3
package/package.json
CHANGED
package/scripts/certify.mjs
CHANGED
|
@@ -26,6 +26,7 @@ const memory = readFileSync(join(root, 'src', 'memory.mjs'), 'utf8')
|
|
|
26
26
|
const mcpHttp = readFileSync(join(root, 'src', 'mcp-http.mjs'), 'utf8')
|
|
27
27
|
const opencodeConfig = readFileSync(join(root, 'src', 'opencode-config.mjs'), 'utf8')
|
|
28
28
|
const codexConfig = readFileSync(join(root, 'src', 'codex-config.mjs'), 'utf8')
|
|
29
|
+
const codexProxy = readFileSync(join(root, 'src', 'codex-mcp-proxy.mjs'), 'utf8')
|
|
29
30
|
const prPush = readFileSync(join(root, 'src', 'pr-push.mjs'), 'utf8')
|
|
30
31
|
const cli = readFileSync(join(root, 'bin', 'cli.mjs'), 'utf8')
|
|
31
32
|
const websocket = readFileSync(join(root, 'src', 'ws.mjs'), 'utf8')
|
|
@@ -63,6 +64,7 @@ const assertions = [
|
|
|
63
64
|
['Codex cannot race the watcher with post_message', watcher.includes("disabledMcpTools: ['post_message']") && codexConfig.includes('disabled_tools = [')],
|
|
64
65
|
['Codex cannot continue without OpenVisio MCP tools', watcher.includes('buildCodexMcpOverride') && codexConfig.includes('required = true') && codexConfig.includes('startup_timeout_sec = 30')],
|
|
65
66
|
['Codex discovers deferred OpenVisio actions before claiming they are missing', watcher.includes('Codex may defer MCP actions') && watcher.includes('use tool_search to find that exact action') && watcher.includes('initial-list miss is NOT evidence that the server is disconnected')],
|
|
67
|
+
['Codex authentication is injected outside the model', codexConfig.includes('OPENVISIO_CODEX_API_KEY') && codexProxy.includes('toolWithoutCredentialInputs') && codexProxy.includes('client.callTool') && watcher.includes('local OpenVisio MCP bridge injects authentication outside the model')],
|
|
66
68
|
['reply delivery keys survive reconnects', watcher.includes('deliveredReplies: [...deliveredReplies]') && watcher.includes('deliveredReplies.has(deliveryKey)')],
|
|
67
69
|
['tickets and guarded replies use the behavior-tested serial queue', watcher.includes('createCycleQueue({') && watcher.includes('queues[laneName].enqueue') && cycleQueue.includes('const pending = new Map()')],
|
|
68
70
|
['queued assignments are checked again before model execution', watcher.includes('queued ticket no longer actionable; skipped before model start')],
|
package/src/codex-config.mjs
CHANGED
|
@@ -5,13 +5,11 @@ const tomlString = (value) => JSON.stringify(String(value))
|
|
|
5
5
|
// its tools, which makes an otherwise connected agent incorrectly claim that it
|
|
6
6
|
// cannot inspect OpenVisio. Fail the subprocess instead so the watcher can retry
|
|
7
7
|
// the event and report a real runtime failure without delivering model guesswork.
|
|
8
|
-
export function buildCodexMcpOverride({ mcpUrl,
|
|
8
|
+
export function buildCodexMcpOverride({ mcpUrl, proxyCommand, proxyPath, disabledMcpTools = [] } = {}) {
|
|
9
9
|
if (!mcpUrl) return ''
|
|
10
|
-
|
|
11
|
-
.map(([key, value]) => `${tomlString(key)} = ${tomlString(value)}`)
|
|
12
|
-
.join(', ')
|
|
10
|
+
if (!proxyCommand || !proxyPath) throw new Error('Codex MCP proxy command and path are required')
|
|
13
11
|
const disabled = Array.isArray(disabledMcpTools) ? disabledMcpTools.filter(Boolean) : []
|
|
14
|
-
const headerConfig = headerEntries ? `, http_headers = { ${headerEntries} }` : ''
|
|
15
12
|
const disabledToolConfig = disabled.length ? `, disabled_tools = [${disabled.map(tomlString).join(', ')}]` : ''
|
|
16
|
-
|
|
13
|
+
const envVars = ['OPENVISIO_CODEX_MCP_URL', 'OPENVISIO_CODEX_API_KEY', 'OPENVISIO_CODEX_IDENTIFIER'].map(tomlString).join(', ')
|
|
14
|
+
return `mcp_servers={ openvisio-team = { command = ${tomlString(proxyCommand)}, args = [${tomlString(proxyPath)}], env_vars = [${envVars}], required = true, startup_timeout_sec = 30${disabledToolConfig} } }`
|
|
17
15
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { createMcpHttpClient } from './mcp-http.mjs'
|
|
5
|
+
|
|
6
|
+
export function toolWithoutCredentialInputs(tool) {
|
|
7
|
+
const copy = structuredClone(tool)
|
|
8
|
+
const schema = copy.inputSchema ?? copy.input_schema
|
|
9
|
+
if (!schema || typeof schema !== 'object') return copy
|
|
10
|
+
if (schema.properties && typeof schema.properties === 'object') {
|
|
11
|
+
delete schema.properties.agent_api_key
|
|
12
|
+
delete schema.properties.agent_identifier
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(schema.required)) {
|
|
15
|
+
schema.required = schema.required.filter((name) => name !== 'agent_api_key' && name !== 'agent_identifier')
|
|
16
|
+
}
|
|
17
|
+
return copy
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function runCodexMcpProxy() {
|
|
21
|
+
const url = process.env.OPENVISIO_CODEX_MCP_URL
|
|
22
|
+
const apiKey = process.env.OPENVISIO_CODEX_API_KEY
|
|
23
|
+
const identifier = process.env.OPENVISIO_CODEX_IDENTIFIER
|
|
24
|
+
if (!url || !apiKey || !identifier) throw new Error('OpenVisio Codex MCP bridge is missing its watcher environment.')
|
|
25
|
+
const client = createMcpHttpClient({ url, apiKey, identifier, clientVersion: 'openvisio-agent-codex-proxy' })
|
|
26
|
+
const reply = (id, result) => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n')
|
|
27
|
+
const fail = (id, error) => {
|
|
28
|
+
const message = String(error?.message || error || 'MCP bridge error').split(apiKey).join('[redacted]')
|
|
29
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32000, message: message.slice(0, 1000) } }) + '\n')
|
|
30
|
+
}
|
|
31
|
+
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity })
|
|
32
|
+
lines.on('line', async (line) => {
|
|
33
|
+
let request
|
|
34
|
+
try { request = JSON.parse(line) } catch { return }
|
|
35
|
+
const id = request.id
|
|
36
|
+
try {
|
|
37
|
+
if (request.method === 'initialize') {
|
|
38
|
+
reply(id, {
|
|
39
|
+
protocolVersion: request.params?.protocolVersion || '2025-03-26',
|
|
40
|
+
capabilities: { tools: { listChanged: false } },
|
|
41
|
+
serverInfo: { name: 'openvisio-agent', version: '1' },
|
|
42
|
+
})
|
|
43
|
+
} else if (request.method === 'notifications/initialized') {
|
|
44
|
+
// Notification: no response.
|
|
45
|
+
} else if (request.method === 'ping') {
|
|
46
|
+
reply(id, {})
|
|
47
|
+
} else if (request.method === 'tools/list') {
|
|
48
|
+
const tools = await client.listTools()
|
|
49
|
+
reply(id, { tools: tools.map(toolWithoutCredentialInputs) })
|
|
50
|
+
} else if (request.method === 'tools/call') {
|
|
51
|
+
reply(id, await client.callTool(request.params?.name, request.params?.arguments || {}))
|
|
52
|
+
} else if (id != null) {
|
|
53
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }) + '\n')
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (id != null) fail(id, error)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
62
|
+
try { runCodexMcpProxy() }
|
|
63
|
+
catch (error) {
|
|
64
|
+
process.stderr.write(String(error?.message || error) + '\n')
|
|
65
|
+
process.exitCode = 1
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/watch.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { spawn, spawnSync } from 'node:child_process'
|
|
|
8
8
|
import { closeSync, writeFileSync, mkdirSync, existsSync, openSync, readFileSync, unlinkSync } from 'node:fs'
|
|
9
9
|
import { homedir } from 'node:os'
|
|
10
10
|
import { join, dirname } from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
11
12
|
import { OV_DIR, DEFAULT_WORKSPACE, readConfig, writeJson, configPath, onPath, fail, ok, info, slugify, stripSlash, chmodSafe } from './lib.mjs'
|
|
12
13
|
import { connectAgentWs, assertWebSocket } from './ws.mjs'
|
|
13
14
|
import { agentAddedByName, agentStateRequest, buildTaskCompletionReport, claudeEventEvidence, classifyConversationTarget, codexEventEvidence, codexPolicyBlock, combineRuntimeWorkEvidence, conversationNeedsCode, mentionDedupeKeys, missingRuntimeWorkEvidence, normalizeRenderedMessageText, opencodeEventEvidence, renderedAgentMessages, shouldSuppressCodexDiagnostic, taskAgentId, taskFromEvent, taskIsAwaitingReview, taskIsCompleted, ticketDisplaySlug } from './events.mjs'
|
|
@@ -439,6 +440,7 @@ function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, ma
|
|
|
439
440
|
function createCodexRunner({ mcpUrl, mcpHeaders, workdir, canCode, maxCycleMs, log, debug, model, onTool, systemPrompt }) {
|
|
440
441
|
const bin = onPath('codex') || 'codex'
|
|
441
442
|
const cwd = workdir || OV_DIR
|
|
443
|
+
const proxyPath = fileURLToPath(new URL('./codex-mcp-proxy.mjs', import.meta.url))
|
|
442
444
|
let cancelActive = null
|
|
443
445
|
|
|
444
446
|
function runCycle(prompt, cycleModel, cycleOptions = {}) {
|
|
@@ -446,7 +448,7 @@ function createCodexRunner({ mcpUrl, mcpHeaders, workdir, canCode, maxCycleMs, l
|
|
|
446
448
|
const m = cycleModel || model
|
|
447
449
|
const full = systemPrompt ? systemPrompt + '\n\n' + prompt : prompt
|
|
448
450
|
const disabledMcpTools = Array.isArray(cycleOptions.disabledMcpTools) ? cycleOptions.disabledMcpTools.filter(Boolean) : []
|
|
449
|
-
const mcpOverride = buildCodexMcpOverride({ mcpUrl,
|
|
451
|
+
const mcpOverride = buildCodexMcpOverride({ mcpUrl, proxyCommand: process.execPath, proxyPath, disabledMcpTools })
|
|
450
452
|
const args = ['exec', '--ignore-user-config', '--skip-git-repo-check', '--ephemeral', '--json', '--color', 'never',
|
|
451
453
|
...(canCode ? ['--approve-for-me'] : ['--sandbox', 'read-only']),
|
|
452
454
|
...(m ? ['--model', m] : []),
|
|
@@ -524,7 +526,19 @@ function createCodexRunner({ mcpUrl, mcpHeaders, workdir, canCode, maxCycleMs, l
|
|
|
524
526
|
try {
|
|
525
527
|
// Always inspect Codex JSONL so a successful process exit cannot be
|
|
526
528
|
// mistaken for completed work. Keep it out of normal logs unless debug.
|
|
527
|
-
child = spawn(bin, args, {
|
|
529
|
+
child = spawn(bin, args, {
|
|
530
|
+
...modelProcessOptions,
|
|
531
|
+
cwd,
|
|
532
|
+
env: {
|
|
533
|
+
...process.env,
|
|
534
|
+
...(mcpUrl ? {
|
|
535
|
+
OPENVISIO_CODEX_MCP_URL: mcpUrl,
|
|
536
|
+
OPENVISIO_CODEX_API_KEY: mcpHeaders?.['x-agent-api-key'] || '',
|
|
537
|
+
OPENVISIO_CODEX_IDENTIFIER: mcpHeaders?.['x-agent-identifier'] || '',
|
|
538
|
+
} : {}),
|
|
539
|
+
},
|
|
540
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
541
|
+
})
|
|
528
542
|
if (child.stdout) child.stdout.on('data', (d) => {
|
|
529
543
|
jsonlBuffer += String(d)
|
|
530
544
|
const lines = jsonlBuffer.split('\n')
|
|
@@ -765,7 +779,10 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
765
779
|
const canCode = !!workdir
|
|
766
780
|
// The Mastra bridge authenticates per-CALL: every openvisio-team tool needs
|
|
767
781
|
// agent_identifier + agent_api_key as arguments. Hand them over up front.
|
|
768
|
-
const
|
|
782
|
+
const authRule = agent === 'codex'
|
|
783
|
+
? 'AUTH: the local OpenVisio MCP bridge injects authentication outside the model. Never add, request, print, or search for agent credentials in tool arguments.'
|
|
784
|
+
: `AUTH: the openvisio-team tools REQUIRE two arguments on EVERY call — agent_identifier: "${identifier}" and agent_api_key: "${apiKey}". Include BOTH on every openvisio-team tool call. The credentials are given here; do NOT hunt for them.`
|
|
785
|
+
const credNote = `${authRule} Codex may defer MCP actions instead of placing them in the initial tool list. If a native tool_search is available and an expected openvisio-team action is not initially visible, use tool_search to find that exact action, then call it; an initial-list miss is NOT evidence that the server is disconnected. Do not ask a teammate to reconnect or provide a slug until tool_search and the applicable live lookup have actually failed. On backend MCP, discover and update work with list_agents, list_projects, list_tasks, list_task_types, get_ticket, update_ticket, list_channels, list_message_thread, and list_activity as applicable. Ticket comments are optional: never invent or call comment_ticket unless that exact tool appears or tool_search finds it. get_marching_orders, poll_inbox, get_resource, list_mcp_resources, and list_mcp_resource_templates are NOT team-action tools here; never call them. Tools may be namespaced — call whichever names actually appear. Bash/git/gh ARE for code work; this rule only forbids searching for keys.`
|
|
769
786
|
// The STATIC charter + creds are the session system prompt (cached, billed once),
|
|
770
787
|
// NOT re-sent in every cycle's user message — the big token saving.
|
|
771
788
|
const systemPrompt = (canCode ? CODE_CHARTER : CHAT_CHARTER) + '\n\n' + credNote
|