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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.18.10",
3
+ "version": "0.18.12",
4
4
  "description": "Connect Claude Code, Codex, or OpenCode to an OpenVisio team — MCP tools + optional autonomy — in one command.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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') && watcher.includes('get_marching_orders, poll_inbox, get_resource, list_mcp_resources, list_mcp_resource_templates')],
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'")],
@@ -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, mcpHeaders, disabledMcpTools = [] } = {}) {
8
+ export function buildCodexMcpOverride({ mcpUrl, proxyCommand, proxyPath, disabledMcpTools = [] } = {}) {
9
9
  if (!mcpUrl) return ''
10
- const headerEntries = Object.entries(mcpHeaders || {})
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
- return `mcp_servers={ openvisio-team = { url = ${tomlString(mcpUrl)}, required = true, startup_timeout_sec = 30${headerConfig}${disabledToolConfig} } }`
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, mcpHeaders, disabledMcpTools })
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, { ...modelProcessOptions, cwd, stdio: ['ignore', 'pipe', 'pipe'] })
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 credNote = `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. Use ONLY names shown in the current tool list. 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. 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. The credentials are given here; do NOT hunt for them. Bash/git/gh ARE for code work; this rule only forbids searching for keys.`
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, list_mcp_resource_templates, or other relay/resource-discovery tools; they are not backend team actions. Use only names present in the current openvisio-team tool list. For 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.'
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