openvisio-agent 0.18.10 → 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 +4 -1
- package/src/codex-config.mjs +4 -6
- package/src/codex-mcp-proxy.mjs +67 -0
- package/src/watch.mjs +21 -4
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')
|
|
@@ -62,6 +63,8 @@ const assertions = [
|
|
|
62
63
|
['rendered backend replies are checked before every guarded thread post', watcher.includes("callMcpTool('list_message_thread'") && watcher.includes('renderedAgentMessages(live') && watcher.includes('same-content-rendered')],
|
|
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')],
|
|
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')],
|
|
65
68
|
['reply delivery keys survive reconnects', watcher.includes('deliveredReplies: [...deliveredReplies]') && watcher.includes('deliveredReplies.has(deliveryKey)')],
|
|
66
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()')],
|
|
67
70
|
['queued assignments are checked again before model execution', watcher.includes('queued ticket no longer actionable; skipped before model start')],
|
|
@@ -91,7 +94,7 @@ const assertions = [
|
|
|
91
94
|
['OpenCode emits structured events for runtime evidence', watcher.includes("'--format', 'json'") && watcher.includes('opencodeEventEvidence(event)')],
|
|
92
95
|
['OpenCode API-key MCP disables OAuth probing', opencodeConfig.includes("oauth: false") && opencodeConfig.includes('timeout: 15_000')],
|
|
93
96
|
['OpenCode identities use isolated configs outside the code workspace', watcher.includes('opencodeRuntimeLayout({ cfgKey, workdir })') && watcher.includes("'--dir', workspace") && watcher.includes('OPENCODE_CONFIG: opencodeConfigPath') && watcher.includes('OPENCODE_CONFIG_CONTENT: JSON.stringify(opencodeConfig)')],
|
|
94
|
-
['OpenCode backend prompts forbid relay and resource-discovery tools', watcher.includes('BACKEND MCP RULE') &&
|
|
97
|
+
['OpenCode backend prompts forbid relay and resource-discovery tools', watcher.includes('BACKEND MCP RULE') && ['get_marching_orders', 'poll_inbox', 'get_resource', 'list_mcp_resources', 'list_mcp_resource_templates'].every((name) => watcher.includes(name))],
|
|
95
98
|
['OpenCode tool failures retain sanitized diagnostics', events.includes('toolError: toolError.replace') && watcher.includes("opencode tool '") && watcher.includes("split(redactKey).join('[redacted]')")],
|
|
96
99
|
['all runtime acknowledgements cannot satisfy coding completion', watcher.includes('missingRuntimeWorkEvidence(result') && watcher.includes('claudeEventEvidence(o, turnToolUses)') && watcher.includes('releaseTaskForRetry(activeTaskRef, prompt)')],
|
|
97
100
|
['Codex prose cannot masquerade as repository evidence', watcher.includes('codexEventEvidence(event)') && events.includes("item.type === 'agent_message'") && events.includes("event.type === 'item.completed'")],
|
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
|
|
@@ -782,7 +799,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
782
799
|
const codexPushGuide = agent === 'codex' && canCode
|
|
783
800
|
? '\n\nCODEX PR DELIVERY: when the repository exists in the local workspace, use that clone for branch creation, edits, tests, and commits; do not inspect or mutate it through linked-codebase MCP tools. To publish the local agent/* branch, run `openvisio-agent push-pr-branch` from the repository, then open the PR with `gh pr create`. The helper can only push HEAD to the matching agent/* branch on the exact authorized origin. If it reports OPENVISIO_PR_PUSH_AUTH_REQUIRED, do not retry or route around it. Report the one-time command `openvisio-agent authorize-pr-push` as the blocker. Use list_codebases/create_codebase_branch/create_codebase_commit/create_pull_request only as a fallback when the repository cannot be obtained locally.'
|
|
784
801
|
: ''
|
|
785
|
-
const backendToolRule = 'BACKEND MCP RULE: the event and watcher already provide the work source. Never call get_marching_orders, poll_inbox, get_resource, list_mcp_resources,
|
|
802
|
+
const backendToolRule = 'BACKEND MCP RULE: the event and watcher already provide the work source. Never call get_marching_orders, poll_inbox, get_resource, list_mcp_resources, or list_mcp_resource_templates; they are not backend team actions. Codex can defer MCP actions: native tool_search is allowed and must be used to find an expected openvisio-team action that is absent from the initial list. An initial-list miss does not mean the MCP server is unavailable. For live discovery use list_agents, list_projects, list_tasks, list_task_types, list_activity, get_ticket, and list_channels as applicable. Never call comment_ticket unless that exact optional tool is present or tool_search finds it.'
|
|
786
803
|
const fullPrompt = (canCode ? CODE_FULL + codexPushGuide : 'Handle the supplied verified backend ticket with the available OpenVisio tools. Update or comment on the ticket as requested, do not claim repository work in chat-only mode, and stop after the verified action.') + '\n\n' + backendToolRule
|
|
787
804
|
const fastPrompt = (canCode ? CODE_FAST : CYCLE_FAST) + '\n\n' + backendToolRule
|
|
788
805
|
const coordinatePrompt = COORDINATE + '\n\n' + backendToolRule
|