openvisio-agent 0.18.11 → 0.18.13
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/bin/cli.mjs +0 -0
- package/package.json +1 -1
- package/scripts/certify.mjs +3 -0
- package/src/codex-config.mjs +4 -6
- package/src/codex-mcp-proxy.mjs +73 -0
- package/src/events.mjs +11 -0
- package/src/watch.mjs +23 -7
package/bin/cli.mjs
CHANGED
|
File without changes
|
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,9 +64,11 @@ 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')],
|
|
71
|
+
['feature titles cannot be mistaken for coordination commands', watcher.includes('taskIsCoordinationOnly(taskText)') && events.includes('Uploading files as messages') && events.includes('explicitCoordination')],
|
|
69
72
|
['reply runner has no coding workspace or coding charter', watcher.includes("workdir: '', systemPrompt: CHAT_CHARTER") && opencodeConfig.includes("'*': 'deny'")],
|
|
70
73
|
['MCP requests have a deadline including response bodies', mcpHttp.includes('controller.abort()') && mcpHttp.includes('const body = await res.text()')],
|
|
71
74
|
['BYO memory uses real ticket and thread identities', watcher.includes('createByoMemoryGraph') && watcher.includes('memory.context(memoryRefs)') && memory.includes('sameRef(r.projectId, refs.projectId)') && memory.includes('sameRef(r.threadId, refs.threadId)')],
|
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,73 @@
|
|
|
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, request) => {
|
|
28
|
+
const message = String(error?.message || error || 'MCP bridge error').split(apiKey).join('[redacted]')
|
|
29
|
+
if (request?.method === 'tools/call') {
|
|
30
|
+
const safeArgs = { ...(request.params?.arguments || {}) }
|
|
31
|
+
delete safeArgs.agent_api_key
|
|
32
|
+
delete safeArgs.agent_identifier
|
|
33
|
+
process.stderr.write(`OpenVisio MCP bridge call failed: ${String(request.params?.name || 'unknown')} ${JSON.stringify(safeArgs).slice(0, 1000)} — ${message.slice(0, 1000)}\n`)
|
|
34
|
+
}
|
|
35
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32000, message: message.slice(0, 1000) } }) + '\n')
|
|
36
|
+
}
|
|
37
|
+
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity })
|
|
38
|
+
lines.on('line', async (line) => {
|
|
39
|
+
let request
|
|
40
|
+
try { request = JSON.parse(line) } catch { return }
|
|
41
|
+
const id = request.id
|
|
42
|
+
try {
|
|
43
|
+
if (request.method === 'initialize') {
|
|
44
|
+
reply(id, {
|
|
45
|
+
protocolVersion: request.params?.protocolVersion || '2025-03-26',
|
|
46
|
+
capabilities: { tools: { listChanged: false } },
|
|
47
|
+
serverInfo: { name: 'openvisio-agent', version: '1' },
|
|
48
|
+
})
|
|
49
|
+
} else if (request.method === 'notifications/initialized') {
|
|
50
|
+
// Notification: no response.
|
|
51
|
+
} else if (request.method === 'ping') {
|
|
52
|
+
reply(id, {})
|
|
53
|
+
} else if (request.method === 'tools/list') {
|
|
54
|
+
const tools = await client.listTools()
|
|
55
|
+
reply(id, { tools: tools.map(toolWithoutCredentialInputs) })
|
|
56
|
+
} else if (request.method === 'tools/call') {
|
|
57
|
+
reply(id, await client.callTool(request.params?.name, request.params?.arguments || {}))
|
|
58
|
+
} else if (id != null) {
|
|
59
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }) + '\n')
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (id != null) fail(id, error, request)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
68
|
+
try { runCodexMcpProxy() }
|
|
69
|
+
catch (error) {
|
|
70
|
+
process.stderr.write(String(error?.message || error) + '\n')
|
|
71
|
+
process.exitCode = 1
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/events.mjs
CHANGED
|
@@ -471,6 +471,17 @@ export function conversationNeedsCode(value) {
|
|
|
471
471
|
return action && target
|
|
472
472
|
}
|
|
473
473
|
|
|
474
|
+
// Coordination tickets are imperative board/chat actions, not implementation
|
|
475
|
+
// work that merely mentions words such as "message", "status", or "label" in
|
|
476
|
+
// its feature title. Requiring the coordination verb at the start prevents
|
|
477
|
+
// tickets like "Uploading files as messages" from being routed away from the
|
|
478
|
+
// coding lane.
|
|
479
|
+
export function taskIsCoordinationOnly(value) {
|
|
480
|
+
const text = String(value || '').trim()
|
|
481
|
+
const explicitCoordination = /^(?:please\s+)?(?:move|moving|assign|reassign|unassign|comment(?:\s+on)?|reply(?:\s+to)?|send\s+(?:a\s+)?message|triage|prioriti[sz]e|rename|close|reopen|(?:add|remove|change|update)\s+(?:the\s+)?(?:status|column|label))\b/i.test(text)
|
|
482
|
+
return explicitCoordination && !conversationNeedsCode(text)
|
|
483
|
+
}
|
|
484
|
+
|
|
474
485
|
// A mention event means this agent's name appeared somewhere, not necessarily
|
|
475
486
|
// that the request was addressed to it. Reject a later-agent hand-off before a
|
|
476
487
|
// model starts, while keeping explicitly shared requests addressed to both.
|
package/src/watch.mjs
CHANGED
|
@@ -8,9 +8,10 @@ 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
|
-
import { agentAddedByName, agentStateRequest, buildTaskCompletionReport, claudeEventEvidence, classifyConversationTarget, codexEventEvidence, codexPolicyBlock, combineRuntimeWorkEvidence, conversationNeedsCode, mentionDedupeKeys, missingRuntimeWorkEvidence, normalizeRenderedMessageText, opencodeEventEvidence, renderedAgentMessages, shouldSuppressCodexDiagnostic, taskAgentId, taskFromEvent, taskIsAwaitingReview, taskIsCompleted, ticketDisplaySlug } from './events.mjs'
|
|
14
|
+
import { agentAddedByName, agentStateRequest, buildTaskCompletionReport, claudeEventEvidence, classifyConversationTarget, codexEventEvidence, codexPolicyBlock, combineRuntimeWorkEvidence, conversationNeedsCode, mentionDedupeKeys, missingRuntimeWorkEvidence, normalizeRenderedMessageText, opencodeEventEvidence, renderedAgentMessages, shouldSuppressCodexDiagnostic, taskAgentId, taskFromEvent, taskIsAwaitingReview, taskIsCompleted, taskIsCoordinationOnly, ticketDisplaySlug } from './events.mjs'
|
|
14
15
|
import { createByoMemoryGraph } from './memory.mjs'
|
|
15
16
|
import { repositoryHasPrPushAuthorization } from './pr-push.mjs'
|
|
16
17
|
import { createMcpHttpClient } from './mcp-http.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
|
|
@@ -1462,7 +1479,6 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1462
1479
|
|
|
1463
1480
|
// Route only concrete repository requests to the coding lane. Conversation
|
|
1464
1481
|
// ownership is classified separately before this is consulted.
|
|
1465
|
-
const coordinationOnly = (value) => /\b(?:move|moving|status|column|assign|reassign|unassign|comment|reply|message|triage|prioriti[sz]e|label|rename|close|reopen)\b/i.test(String(value || '')) && !conversationNeedsCode(value)
|
|
1466
1482
|
|
|
1467
1483
|
// Engineers change the model under the hood from chat: "/model", "/model sonnet",
|
|
1468
1484
|
// "use model haiku", "switch model to opus". Returns {report} | {set} | {invalid}.
|
|
@@ -1550,13 +1566,13 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1550
1566
|
const ticketSlug = ticketDisplaySlug(ticket)
|
|
1551
1567
|
memory.remember({ key: `ticket:${projectId}:${ticketId}`, kind: 'ticket', state: 'queued', summary: title, refs: { projectId, ticketId }, meta: { sourceEvent: kind } })
|
|
1552
1568
|
const taskText = [title, ticket.description, ticket.type, ticket.kind].filter(Boolean).join(' ')
|
|
1553
|
-
const cycleKind = canCode && !
|
|
1569
|
+
const cycleKind = canCode && !taskIsCoordinationOnly(taskText) ? 'full' : 'coord'
|
|
1554
1570
|
log(kind + ' verified ticket #' + ticketId + ' “' + title + '” -> ' + cycleKind + ' lane')
|
|
1555
1571
|
const activityChannel = await projectStatusChannel(projectId)
|
|
1556
1572
|
if (activityChannel != null) sendStatus(activityChannel, 'thinking')
|
|
1557
1573
|
if (cycleKind === 'full') { pendingCompletionReports.add(key); trimSeen(pendingCompletionReports); persistReplay() }
|
|
1558
1574
|
const humanTicketRef = ticketSlug ? `ticket ${ticketSlug}` : `the ticket “${title}”`
|
|
1559
|
-
void drain(cycleKind, `Authoritative get_ticket verification confirms ${humanTicketRef} is open and assigned to YOU. Internal tool identity: project_id ${projectId}, ticket_id ${ticketId}. Numeric ids are MCP arguments only and must never appear in human-facing text; use ${ticketSlug || 'the ticket title'} instead. Ticket details: ${JSON.stringify({ slug: ticketSlug, title, description: ticket.description, priority: ticket.priority, typeId: ticket.type_id ?? ticket.typeId })}. This assignment has no source thread: do not call post_message yourself. Use list_task_types and update_ticket to move it active, complete and verify the work, open the PR when applicable, then update/move the ticket with evidence. For coding work, the watcher will publish exactly one verified completion result in the project channel.`, activityChannel == null ? [] : [activityChannel], { projectId, ticketId, channelId: activityChannel })
|
|
1575
|
+
void drain(cycleKind, `Authoritative get_ticket verification confirms ${humanTicketRef} is open and assigned to YOU. Internal tool identity: project_id ${projectId}, ticket_id ${ticketId}. For every get_ticket call pass exactly { project_id: ${projectId}, ticket_id: ${ticketId} }; for list_task_types pass exactly { project_id: ${projectId} }. Numeric ids are MCP arguments only and must never appear in human-facing text; use ${ticketSlug || 'the ticket title'} instead. Ticket details: ${JSON.stringify({ slug: ticketSlug, title, description: ticket.description, priority: ticket.priority, typeId: ticket.type_id ?? ticket.typeId })}. This assignment has no source thread: do not call post_message yourself. Use list_task_types and update_ticket to move it active, complete and verify the work, open the PR when applicable, then update/move the ticket with evidence. For coding work, the watcher will publish exactly one verified completion result in the project channel.`, activityChannel == null ? [] : [activityChannel], { projectId, ticketId, channelId: activityChannel })
|
|
1560
1576
|
} catch (e) {
|
|
1561
1577
|
log(kind + ' ticket verification failed for #' + ticketId + ': ' + (e?.message || e))
|
|
1562
1578
|
}
|