Compare commits

..

7 Commits

Author SHA1 Message Date
jiantw83 6036ce45c4 docs(README): 說明 OpenCode TLS 驗證預設值
AI / 計算版本號 (pull_request) Successful in 2s
AI / Code Review (pull_request) Successful in 1m10s
2026-06-20 13:56:00 +00:00
jiantw83 648334d153 test(OpenCode TLS): 覆蓋預設跳過驗證行為 2026-06-20 13:56:00 +00:00
jiantw83 9d759464c2 feat(OpenCode TLS): 預設跳過 TLS 驗證 2026-06-20 13:56:00 +00:00
jiantw83 12d7403a0e chore(ai-review 狀態): 移除已解決的 OpenCode TLS finding
AI / 計算版本號 (pull_request) Successful in 2s
AI / Code Review (pull_request) Successful in 29s
2026-06-20 13:42:25 +00:00
jiantw83 babe599977 refactor(OpenCode TLS): 共用 httpsAgent 建立邏輯 2026-06-20 13:42:25 +00:00
admin c662a2e362 Merge pull request '新增 OpenCode 自簽憑證略過設定' (#20) from ai-review-resolve/20260620131600 into develop
Reviewed-on: #20
2026-06-20 13:18:16 +00:00
AI Review Bot 7a79cd45be chore: update ai-review findings [ai-review-bot][success]
AI / 計算版本號 (pull_request) Successful in 2s
AI / Code Review (pull_request) Successful in 5s
2026-06-20 13:17:36 +00:00
8 changed files with 56 additions and 15 deletions
+2 -2
View File
@@ -224,8 +224,8 @@ jobs:
OPENCODE_BASE_URL: http://192.168.3.124:4096 OPENCODE_BASE_URL: http://192.168.3.124:4096
OPENCODE_PROVIDER: google OPENCODE_PROVIDER: google
OPENCODE_MODEL: gemini-2.5-flash OPENCODE_MODEL: gemini-2.5-flash
# OpenCode server 使用自簽憑證才需要提供 # 預設會跳過 OpenCode TLS 驗證;若要強制驗證憑證才需要設定
# OPENCODE_SKIP_TLS_VERIFY: true # OPENCODE_SKIP_TLS_VERIFY: false
# 若 OpenCode server 有設定 OPENCODE_SERVER_PASSWORD,才需要提供: # 若 OpenCode server 有設定 OPENCODE_SERVER_PASSWORD,才需要提供:
# OPENCODE_SERVER_USERNAME: opencode # OPENCODE_SERVER_USERNAME: opencode
# OPENCODE_SERVER_PASSWORD: ${{ secrets.OPENCODE_SERVER_PASSWORD }} # OPENCODE_SERVER_PASSWORD: ${{ secrets.OPENCODE_SERVER_PASSWORD }}
+2 -2
View File
@@ -96,9 +96,9 @@ inputs:
description: 'OpenCode server Basic Auth password' description: 'OpenCode server Basic Auth password'
required: false required: false
OPENCODE_SKIP_TLS_VERIFY: OPENCODE_SKIP_TLS_VERIFY:
description: '跳過 OpenCode server SSL/TLS 憑證驗證(自簽憑證時使用)' description: '跳過 OpenCode server SSL/TLS 憑證驗證'
required: false required: false
default: 'false' default: 'true'
runs: runs:
using: 'docker' using: 'docker'
+7 -1
View File
@@ -1,3 +1,5 @@
import https from 'https';
export const GITEA_TOKEN = process.env.GITEA_TOKEN || ''; export const GITEA_TOKEN = process.env.GITEA_TOKEN || '';
export const GITEA_COMMENT_TOKEN = process.env.GITEA_COMMENT_TOKEN || ''; export const GITEA_COMMENT_TOKEN = process.env.GITEA_COMMENT_TOKEN || '';
export const GITEA_SERVER_URL = process.env.GITEA_SERVER_URL || 'https://gitea.com'; export const GITEA_SERVER_URL = process.env.GITEA_SERVER_URL || 'https://gitea.com';
@@ -12,7 +14,11 @@ export const FINDINGS_PATH = '.gitea/ai-review/findings.json';
export const EXCLUSIONS_PATH = '.gitea/ai-review/exclusions.json'; export const EXCLUSIONS_PATH = '.gitea/ai-review/exclusions.json';
export function shouldSkipOpenCodeTLSVerify() { export function shouldSkipOpenCodeTLSVerify() {
return process.env.OPENCODE_SKIP_TLS_VERIFY === 'true'; return process.env.OPENCODE_SKIP_TLS_VERIFY !== 'false';
}
export function getOpenCodeHttpsAgent() {
return shouldSkipOpenCodeTLSVerify() ? new https.Agent({ rejectUnauthorized: false }) : undefined;
} }
/** 將逗號分隔的 API key 字串拆成陣列 */ /** 將逗號分隔的 API key 字串拆成陣列 */
+11 -1
View File
@@ -1,6 +1,6 @@
import { describe, it, beforeEach, afterEach } from 'node:test'; import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { getLLMConfig } from './config.js'; import { getLLMConfig, shouldSkipOpenCodeTLSVerify } from './config.js';
const ENV_KEYS = [ const ENV_KEYS = [
'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL', 'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL',
@@ -10,6 +10,7 @@ const ENV_KEYS = [
'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL', 'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL',
'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER', 'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER',
'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD', 'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD',
'OPENCODE_SKIP_TLS_VERIFY',
]; ];
let saved = {}; let saved = {};
@@ -104,6 +105,15 @@ describe('getLLMConfig', () => {
assert.equal(cfg.model, 'google/gemini-2.5-pro'); assert.equal(cfg.model, 'google/gemini-2.5-pro');
}); });
it('skips OpenCode TLS verification by default', () => {
assert.equal(shouldSkipOpenCodeTLSVerify(), true);
});
it('allows explicitly enabling OpenCode TLS verification', () => {
process.env.OPENCODE_SKIP_TLS_VERIFY = 'false';
assert.equal(shouldSkipOpenCodeTLSVerify(), false);
});
it('openai takes priority over gemini when both set', () => { it('openai takes priority over gemini when both set', () => {
process.env.OPENAI_API_KEY = 'sk-test'; process.env.OPENAI_API_KEY = 'sk-test';
process.env.GEMINI_API_KEY = 'gemini-key'; process.env.GEMINI_API_KEY = 'gemini-key';
+2 -3
View File
@@ -1,6 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import https from 'https'; import { getLLMConfig, getOpenCodeHttpsAgent } from './config.js';
import { getLLMConfig, shouldSkipOpenCodeTLSVerify } from './config.js';
import { line, error } from './log.js'; import { line, error } from './log.js';
function isOpenAIGpt55(provider, model) { function isOpenAIGpt55(provider, model) {
@@ -50,7 +49,7 @@ function applyOpenCodeAuth(headers) {
function opencodeAxiosOptions(headers) { function opencodeAxiosOptions(headers) {
return { return {
headers, headers,
httpsAgent: shouldSkipOpenCodeTLSVerify() ? new https.Agent({ rejectUnauthorized: false }) : undefined, httpsAgent: getOpenCodeHttpsAgent(),
}; };
} }
+14 -2
View File
@@ -164,9 +164,8 @@ describe('chat - key rotation', async () => {
assert.equal(headers[0]['Authorization'], `Basic ${Buffer.from('opencode:secret').toString('base64')}`); assert.equal(headers[0]['Authorization'], `Basic ${Buffer.from('opencode:secret').toString('base64')}`);
}); });
it('passes an insecure https agent to OpenCode when TLS verification is disabled', async () => { it('passes an insecure https agent to OpenCode by default', async () => {
process.env.OPENCODE_BASE_URL = 'https://opencode.local:4096'; process.env.OPENCODE_BASE_URL = 'https://opencode.local:4096';
process.env.OPENCODE_SKIP_TLS_VERIFY = 'true';
const agents = []; const agents = [];
mock.method(axios, 'post', async (url, _payload, opts) => { mock.method(axios, 'post', async (url, _payload, opts) => {
agents.push(opts.httpsAgent); agents.push(opts.httpsAgent);
@@ -179,6 +178,19 @@ describe('chat - key rotation', async () => {
assert.equal(agents[1].options.rejectUnauthorized, false); assert.equal(agents[1].options.rejectUnauthorized, false);
}); });
it('does not pass an insecure https agent to OpenCode when TLS verification is enabled', async () => {
process.env.OPENCODE_BASE_URL = 'https://opencode.local:4096';
process.env.OPENCODE_SKIP_TLS_VERIFY = 'false';
const agents = [];
mock.method(axios, 'post', async (url, _payload, opts) => {
agents.push(opts.httpsAgent);
if (url.endsWith('/session')) return { data: { id: 'ses_test' } };
return { data: { parts: [{ type: 'text', text: 'ok' }] } };
});
await chat('sys', 'user');
assert.deepEqual(agents, [undefined, undefined]);
});
it('uses Responses API for openai GPT-5.5', async () => { it('uses Responses API for openai GPT-5.5', async () => {
process.env.OPENAI_API_KEY = 'sk-test'; process.env.OPENAI_API_KEY = 'sk-test';
process.env.OPENAI_MODEL = 'GPT-5.5'; process.env.OPENAI_MODEL = 'GPT-5.5';
+2 -2
View File
@@ -7,8 +7,8 @@ import {
GITEA_REPOSITORY, GITEA_REPOSITORY,
GITEA_SKIP_TLS_VERIFY, GITEA_SKIP_TLS_VERIFY,
PR_NUMBER, PR_NUMBER,
getOpenCodeHttpsAgent,
getLLMConfig, getLLMConfig,
shouldSkipOpenCodeTLSVerify,
} from './config.js'; } from './config.js';
import { verifyRemoteAccess } from './git.js'; import { verifyRemoteAccess } from './git.js';
import { step, line, ok, error } from './log.js'; import { step, line, ok, error } from './log.js';
@@ -30,7 +30,7 @@ const applyOpenCodeAuth = (headers) => {
const opencodeAxiosOptions = (headers) => ({ const opencodeAxiosOptions = (headers) => ({
headers, headers,
timeout: 30000, timeout: 30000,
httpsAgent: shouldSkipOpenCodeTLSVerify() ? new https.Agent({ rejectUnauthorized: false }) : undefined, httpsAgent: getOpenCodeHttpsAgent(),
}); });
function giteaErr(e) { function giteaErr(e) {
+16 -2
View File
@@ -183,10 +183,9 @@ describe('verifyLLM', () => {
assert.deepEqual(urls, ['http://opencode.local:4096/global/health', 'http://opencode.local:4096/config/providers']); assert.deepEqual(urls, ['http://opencode.local:4096/global/health', 'http://opencode.local:4096/config/providers']);
}); });
it('passes an insecure https agent for opencode when TLS verification is disabled', async () => { it('passes an insecure https agent for opencode by default', async () => {
clearLLMEnv(); clearLLMEnv();
process.env.OPENCODE_BASE_URL = 'https://opencode.local:4096'; process.env.OPENCODE_BASE_URL = 'https://opencode.local:4096';
process.env.OPENCODE_SKIP_TLS_VERIFY = 'true';
const agents = []; const agents = [];
mock.method(axios, 'get', async (url, opts) => { mock.method(axios, 'get', async (url, opts) => {
agents.push(opts.httpsAgent); agents.push(opts.httpsAgent);
@@ -200,6 +199,21 @@ describe('verifyLLM', () => {
assert.equal(agents[1].options.rejectUnauthorized, false); assert.equal(agents[1].options.rejectUnauthorized, false);
}); });
it('does not pass an insecure https agent for opencode when TLS verification is enabled', async () => {
clearLLMEnv();
process.env.OPENCODE_BASE_URL = 'https://opencode.local:4096';
process.env.OPENCODE_SKIP_TLS_VERIFY = 'false';
const agents = [];
mock.method(axios, 'get', async (url, opts) => {
agents.push(opts.httpsAgent);
if (url.endsWith('/global/health')) return { data: { healthy: true } };
return { data: { providers: [{ id: 'google', models: { 'gemini-2.5-flash': { id: 'gemini-2.5-flash' } } }] } };
});
const result = await verifyLLM();
assert.equal(result.ok, true);
assert.deepEqual(agents, [undefined, undefined]);
});
it('checks openai GPT-5.5 with Responses API', async () => { it('checks openai GPT-5.5 with Responses API', async () => {
clearLLMEnv(); clearLLMEnv();
process.env.OPENAI_API_KEY = 'sk-test'; process.env.OPENAI_API_KEY = 'sk-test';