fix: patch all 10 code-review findings in agrifine-extension

- agent.js: add anthropic-dangerous-direct-browser-access header to _callAPI
  (root cause of 401 CORS errors in AgriAgent tab)
- agent.js: handle stop_reason=max_tokens alongside end_turn
- ag-refine/index.js: reset isRunning in a try/catch around agent.run()
- tools.js: check res.ok before parsing JSON in toolLookupUSDAsoil
- tools.js: guard p.name?.toLowerCase() against null field names
- field-profile/index.js: guard both lat and lon before toFixed()
- reading-list/index.js: escapeHtml + safeHref to prevent XSS from page titles/urls
- dashboard/index.js: escapeHtml AI answer and all dynamic list content
- data-ingest/index.js: add .xlsx/.xls extension fallbacks for missing MIME type
- storage.js: check chrome.runtime.lastError in all four storage helpers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBD2dN2KEjzz3UQFa9hEpu
This commit is contained in:
Claude 2026-06-27 04:35:32 +00:00
parent b7b832764d
commit 3185b04ed6
No known key found for this signature in database
8 changed files with 67 additions and 24 deletions

View file

@ -61,8 +61,7 @@ export class AgrifineAgent {
// Append assistant turn
messages.push({ role: 'assistant', content: response.content });
if (response.stop_reason === 'end_turn') {
// Extract final text
if (response.stop_reason === 'end_turn' || response.stop_reason === 'max_tokens') {
const text = response.content
.filter((b) => b.type === 'text')
.map((b) => b.text)
@ -113,6 +112,7 @@ export class AgrifineAgent {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
'anthropic-dangerous-direct-browser-access': 'true',
},
body: JSON.stringify(body),
});

View file

@ -153,7 +153,15 @@ export function AgRefineModule() {
},
});
await agent.run(userText);
try {
await agent.run(userText);
} catch (err) {
const idx = messages.findIndex((m) => m.id === thinkingId);
if (idx >= 0) messages.splice(idx, 1);
messages.push({ role: 'error', text: err.message });
isRunning = false;
this._renderMessages(container);
}
},
_renderMessages(container) {

View file

@ -151,7 +151,7 @@ async function toolGetReadingList({ tag } = {}) {
async function toolGetFieldProfiles({ field_name } = {}) {
const profiles = await getFieldProfiles();
const filtered = field_name
? profiles.filter((p) => p.name.toLowerCase().includes(field_name.toLowerCase()))
? profiles.filter((p) => p.name?.toLowerCase().includes(field_name.toLowerCase()))
: profiles;
return {
count: filtered.length,
@ -260,6 +260,7 @@ async function toolLookupUSDAsoil({ latitude, longitude }) {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `request=query&query=${encodeURIComponent(query)}&format=JSON`,
});
if (!res.ok) throw new Error(`USDA SDA API ${res.status}`);
const data = await res.json();
const rows = data.Table ?? [];
return {

View file

@ -3,6 +3,12 @@ import {
} from '../../utils/storage.js';
import { callAnthropic } from '../../utils/api.js';
function escapeHtml(str) {
return String(str ?? '')
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
const CATEGORIES = ['all', 'land', 'equipment', 'harvest', 'finance', 'carbon', 'weather'];
function tagCategory(item) {
@ -118,7 +124,7 @@ export function DashboardModule() {
maxTokens: 512,
});
answerEl.innerHTML = `<p class="font-medium text-agri-700 mb-1">Answer</p>${answer}`;
answerEl.innerHTML = `<p class="font-medium text-agri-700 mb-1">Answer</p><span class="whitespace-pre-wrap">${escapeHtml(answer)}</span>`;
} catch (err) {
answerEl.textContent = `Error: ${err.message}`;
} finally {
@ -167,11 +173,11 @@ export function DashboardModule() {
<div class="flex items-start gap-2">
<span class="text-lg flex-shrink-0">${sourceIcon}</span>
<div class="flex-1 min-w-0">
<p class="text-sm font-semibold text-gray-800 truncate">${title}</p>
${sub ? `<p class="text-xs text-gray-500 mt-0.5 leading-relaxed line-clamp-2">${sub}</p>` : ''}
<p class="text-sm font-semibold text-gray-800 truncate">${escapeHtml(title)}</p>
${sub ? `<p class="text-xs text-gray-500 mt-0.5 leading-relaxed line-clamp-2">${escapeHtml(sub)}</p>` : ''}
<div class="flex items-center gap-2 mt-1.5">
<span class="tag-pill bg-earth-100 text-earth-700">${item._category}</span>
${(item.tags ?? []).filter((t) => t !== item._category).slice(0, 2).map((t) => `<span class="tag-pill">${t}</span>`).join('')}
<span class="tag-pill bg-earth-100 text-earth-700">${escapeHtml(item._category)}</span>
${(item.tags ?? []).filter((t) => t !== item._category).slice(0, 2).map((t) => `<span class="tag-pill">${escapeHtml(t)}</span>`).join('')}
${date ? `<span class="text-xs text-gray-300">${new Date(date).toLocaleDateString()}</span>` : ''}
</div>
</div>

View file

@ -69,7 +69,10 @@ export function DataIngestModule() {
async _processFile(file, container) {
const status = container.querySelector('#ingest-status');
const typeName = SUPPORTED_TYPES[file.type] ?? (file.name.endsWith('.csv') ? 'CSV' : null);
const typeName = SUPPORTED_TYPES[file.type]
?? (file.name.endsWith('.csv') ? 'CSV'
: (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) ? 'Excel'
: null);
if (!typeName) {
status.textContent = 'Unsupported file type.';

View file

@ -148,7 +148,7 @@ export function FieldProfileModule() {
<!-- Expanded detail -->
<div class="fp-detail ${expandedId === p.id ? '' : 'hidden'} mt-3 pt-3 border-t border-gray-100 text-xs text-gray-600 space-y-1">
${p.coordinates?.lat ? `<p>📍 ${p.coordinates.lat.toFixed(4)}, ${p.coordinates.lon.toFixed(4)}</p>` : ''}
${p.coordinates?.lat != null && p.coordinates?.lon != null ? `<p>📍 ${p.coordinates.lat.toFixed(4)}, ${p.coordinates.lon.toFixed(4)}</p>` : ''}
${p.notes ? `<p>📝 ${p.notes}</p>` : ''}
<p class="text-gray-300">Weather data: <span class="coming-soon">Phase 6</span></p>
<p class="text-gray-300">Carbon potential: <span class="coming-soon">Phase 7</span></p>

View file

@ -1,6 +1,19 @@
import { getReadingList, saveReadingItem, deleteReadingItem } from '../../utils/storage.js';
import { callAnthropic, AGRICULTURE_TAGS } from '../../utils/api.js';
function escapeHtml(str) {
return String(str ?? '')
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function safeHref(url) {
try {
const u = new URL(url);
return (u.protocol === 'https:' || u.protocol === 'http:') ? escapeHtml(url) : '#';
} catch (_) { return '#'; }
}
export function ReadingListModule() {
let currentTag = 'all';
@ -126,18 +139,18 @@ export function ReadingListModule() {
}
listEl.innerHTML = filtered.map((item) => `
<div class="agri-card" data-id="${item.id}">
<div class="agri-card" data-id="${escapeHtml(item.id)}">
<div class="flex items-start justify-between gap-2">
<a href="${item.url}" target="_blank" class="text-sm font-semibold text-agri-700 hover:underline leading-snug flex-1">${item.title}</a>
<button class="rl-delete-btn text-gray-300 hover:text-red-400 transition flex-shrink-0" data-id="${item.id}" title="Remove">
<a href="${safeHref(item.url)}" target="_blank" rel="noopener noreferrer" class="text-sm font-semibold text-agri-700 hover:underline leading-snug flex-1">${escapeHtml(item.title)}</a>
<button class="rl-delete-btn text-gray-300 hover:text-red-400 transition flex-shrink-0" data-id="${escapeHtml(item.id)}" title="Remove">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
${item.summary ? `<p class="text-xs text-gray-500 mt-1.5 leading-relaxed">${item.summary}</p>` : ''}
${item.summary ? `<p class="text-xs text-gray-500 mt-1.5 leading-relaxed">${escapeHtml(item.summary)}</p>` : ''}
<div class="mt-2">
${(item.tags ?? []).map((t) => `<span class="tag-pill">${t}</span>`).join('')}
${(item.tags ?? []).map((t) => `<span class="tag-pill">${escapeHtml(t)}</span>`).join('')}
</div>
<p class="text-xs text-gray-300 mt-2">${new Date(item.savedAt).toLocaleDateString()}</p>
</div>

View file

@ -22,26 +22,38 @@ export const KEYS = {
// ── Generic helpers ──────────────────────────────────────────────────────────
export async function localGet(key) {
return new Promise((resolve) => {
chrome.storage.local.get(key, (result) => resolve(result[key] ?? null));
return new Promise((resolve, reject) => {
chrome.storage.local.get(key, (result) => {
if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); return; }
resolve(result[key] ?? null);
});
});
}
export async function localSet(key, value) {
return new Promise((resolve) => {
chrome.storage.local.set({ [key]: value }, resolve);
return new Promise((resolve, reject) => {
chrome.storage.local.set({ [key]: value }, () => {
if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); return; }
resolve();
});
});
}
export async function sessionGet(key) {
return new Promise((resolve) => {
chrome.storage.session.get(key, (result) => resolve(result[key] ?? null));
return new Promise((resolve, reject) => {
chrome.storage.session.get(key, (result) => {
if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); return; }
resolve(result[key] ?? null);
});
});
}
export async function sessionSet(key, value) {
return new Promise((resolve) => {
chrome.storage.session.set({ [key]: value }, resolve);
return new Promise((resolve, reject) => {
chrome.storage.session.set({ [key]: value }, () => {
if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); return; }
resolve();
});
});
}