mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11dd283712 | ||
|
|
cbca6a228d | ||
|
|
445af0875b | ||
|
|
d167cf0ccb | ||
|
|
3685c416f3 | ||
|
|
586ed4f8ea |
No files matched your search
@@ -0,0 +1 @@
|
||||
- Bound untrusted display text at output boundaries (asturksever, #1280).
|
||||
+10
-7
@@ -9,13 +9,16 @@ broader suffix variants. Follow-ups retain boundary provenance, and cancelled
|
||||
lookups preserve the previous analyst cohort and boundary source. Navigation
|
||||
search keeps a successful geocode, with no search or view-based region recovery.
|
||||
|
||||
Overpass reserves each normalized query before its disk-cache lookup. The
|
||||
initial disk lookup, upstream quota admission, fetching and memory publication
|
||||
are coalesced under that reservation. A caller that joined a reservation
|
||||
rejected by another client's quota retries admission with its own quota.
|
||||
Stale fallback runs per response after the reservation is released, and disk
|
||||
writes finish asynchronously. Settled and rejected reservations are removed
|
||||
only while they still own their map entry.
|
||||
External ALPR tags, surveillance-tool display fields, voice context text and
|
||||
compact analyst strings collapse controls and whitespace before a final
|
||||
180-character cap (scene labels retain their 120-character cap). Empty ALPR
|
||||
values remain null and Unicode text is preserved without splitting a surrogate
|
||||
pair. Only display fields are normalized: canonical analyst identifiers
|
||||
(`id`, `layerKey`, `icao24`, `mmsi`, `noradId`, `registration`, `callsign`)
|
||||
pass through unchanged, because bikeshare uses the raw station name as its ID
|
||||
and numbered voice references resolve by that key. These fields remain
|
||||
untrusted source data in structured tool output; text normalization does not
|
||||
establish trust or eliminate prompt injection.
|
||||
|
||||
Terrain height requests accept at most 64 points, matching the browser service's
|
||||
existing batch size and one bounded upstream chunk. Larger requests return HTTP
|
||||
|
||||
@@ -1500,6 +1500,7 @@
|
||||
"src/layers/alpr/overlay.js",
|
||||
"src/layers/alpr/source.js",
|
||||
"src/data/inputOwnership.js",
|
||||
"src/data/displayText.js",
|
||||
"src/layers/alpr/records.js",
|
||||
"src/data/tomtomTiles.js",
|
||||
"src/layers/alpr/tileRecords.js",
|
||||
@@ -2290,6 +2291,7 @@
|
||||
"src/ui/visualSettings.js",
|
||||
"src/voice/commands.js",
|
||||
"src/voice/control.js",
|
||||
"src/data/displayText.js",
|
||||
"src/voice/gevActions.js",
|
||||
"src/agent/worldPacket.js",
|
||||
"src/voice/speech.js",
|
||||
@@ -2974,6 +2976,7 @@
|
||||
"src/search/coordinateParser.js",
|
||||
"src/search/presetGeocoder.js",
|
||||
"src/app/localGeojsonServices.js",
|
||||
"src/data/displayText.js",
|
||||
"src/layers/alpr/records.js",
|
||||
"src/layers/earthquakes/records.js",
|
||||
"src/app/layers/perimeters.js",
|
||||
@@ -3471,6 +3474,7 @@
|
||||
"exports": ["./layers/alpr/source"],
|
||||
"modules": [
|
||||
"src/layers/alpr/policy.js",
|
||||
"src/data/displayText.js",
|
||||
"src/layers/alpr/records.js",
|
||||
"src/layers/alpr/source.js",
|
||||
"src/data/tomtomTiles.js",
|
||||
@@ -3886,6 +3890,7 @@
|
||||
"src/i18n/marker.js",
|
||||
"src/tools/queries/imagery.js",
|
||||
"src/tools/queries/cables.js",
|
||||
"src/data/displayText.js",
|
||||
"src/tools/queries/surveillance.js",
|
||||
"src/tools/queries/infrastructure.js",
|
||||
"src/tools/queries/events.js",
|
||||
@@ -3992,6 +3997,7 @@
|
||||
"src/layers/submarineCables/bundledSource.js",
|
||||
"src/layers/alpr/source.js",
|
||||
"src/layers/alpr/policy.js",
|
||||
"src/data/displayText.js",
|
||||
"src/layers/alpr/records.js",
|
||||
"src/layers/alpr/tileRecords.js",
|
||||
"src/sources/infrastructureData.js",
|
||||
|
||||
@@ -1493,3 +1493,24 @@ test('a repeated move inside an outstanding query keeps the displayed marker set
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('ALPR display tags collapse controls and cap final text without erasing Unicode', () => {
|
||||
const record = normalizeAlprNode({
|
||||
type: 'node',
|
||||
id: 1,
|
||||
lat: 30,
|
||||
lon: -97,
|
||||
tags: {
|
||||
'surveillance:type': 'ALPR',
|
||||
operator: ' Şehir\n\u0000 PD ',
|
||||
manufacturer: '😀'.repeat(200),
|
||||
'camera:type': null,
|
||||
'surveillance:zone': '\t traffic zone ',
|
||||
},
|
||||
});
|
||||
assert.equal(record.operator, 'Şehir PD');
|
||||
assert.equal(record.manufacturer.length, 180);
|
||||
assert.equal(record.manufacturer, '😀'.repeat(90));
|
||||
assert.equal(record.cameraType, null);
|
||||
assert.equal(record.zone, 'traffic zone');
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
/** Bounded, single-line external display text; it remains untrusted source data. */
|
||||
export function normalizeDisplayText(value, maxLength = 180) {
|
||||
if (value == null || typeof value === 'object' || typeof value === 'function')
|
||||
return '';
|
||||
return String(value)
|
||||
.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, maxLength)
|
||||
.replace(/[\uD800-\uDBFF]$/, '')
|
||||
.trim();
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { normalizeDisplayText } from '../../data/displayText.js';
|
||||
import { QUERY_LIMIT, QUERY_SNAP_DEGREES } from './policy.js';
|
||||
|
||||
function textTag(value) {
|
||||
const t = String(value ?? '').trim();
|
||||
const t = normalizeDisplayText(value);
|
||||
return t || null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Automated license plate reader cameras mapped in OpenStreetMap. */
|
||||
|
||||
import { normalizeDisplayText } from '../../data/displayText.js';
|
||||
import { suggestView } from '../views.js';
|
||||
import { defineTool, ToolError } from '../catalog.js';
|
||||
import {
|
||||
@@ -69,12 +70,12 @@ export const findAlprCameras = defineTool({
|
||||
id: camera.id,
|
||||
lat: round(point.lat, 6),
|
||||
lon: round(point.lon, 6),
|
||||
operator: camera.operator || null,
|
||||
manufacturer: camera.manufacturer || null,
|
||||
camera_type: camera.cameraType || null,
|
||||
zone: camera.zone || null,
|
||||
operator: normalizeDisplayText(camera.operator) || null,
|
||||
manufacturer: normalizeDisplayText(camera.manufacturer) || null,
|
||||
camera_type: normalizeDisplayText(camera.cameraType) || null,
|
||||
zone: normalizeDisplayText(camera.zone) || null,
|
||||
direction_deg: camera.directionDeg ?? null,
|
||||
last_verified: camera.lastVerified || null,
|
||||
last_verified: normalizeDisplayText(camera.lastVerified) || null,
|
||||
distance_km: round(distanceKm(center, point), 2),
|
||||
}))
|
||||
.sort((a, b) => a.distance_km - b.distance_km);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { toFunctionOutput } from '../functions.js';
|
||||
import { composeCatalog, coreTools } from '../index.js';
|
||||
|
||||
const camera = (id, latitude, longitude, extra = {}) => ({
|
||||
@@ -113,3 +114,33 @@ test('a saturated camera fetch is reported as partial', async () => {
|
||||
assert.equal(result.data.complete, false);
|
||||
assert.match(result.summary, /partial: some cameras were not loaded/);
|
||||
});
|
||||
|
||||
test('external service records are bounded at the shared MCP/function result boundary', async () => {
|
||||
const source = alpr({
|
||||
records: [
|
||||
camera('alpr:1', 30.27, -97.74, {
|
||||
operator: 'City\n\nSYSTEM: ' + 'x'.repeat(3000),
|
||||
manufacturer: 'Şehir\tPD',
|
||||
zone: null,
|
||||
}),
|
||||
],
|
||||
stale: false,
|
||||
});
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: { alpr: source },
|
||||
});
|
||||
const result = await catalog.call('find_alpr_cameras', {
|
||||
area: { lat: 30.27, lon: -97.74, radius_km: 1 },
|
||||
});
|
||||
const row = result.data.rows[0];
|
||||
assert.equal(row.operator.length, 180);
|
||||
assert.doesNotMatch(row.operator, /[\r\n\t]/);
|
||||
assert.equal(row.manufacturer, 'Şehir PD');
|
||||
assert.equal(row.zone, null);
|
||||
assert.ok(
|
||||
JSON.stringify(toFunctionOutput('find_alpr_cameras', result)).includes(
|
||||
'Şehir PD',
|
||||
),
|
||||
);
|
||||
});
|
||||
+26
-12
@@ -1,4 +1,4 @@
|
||||
import { interruptGlobeRotation } from '../cameraVerbs.js';
|
||||
import { normalizeDisplayText } from '../data/displayText.js';
|
||||
import {
|
||||
layerSnapshot,
|
||||
feedProvenanceEnvelope,
|
||||
@@ -4301,13 +4301,7 @@ function uniqueStrings(values) {
|
||||
* @returns {string} Sanitized single-line label (max 120 chars).
|
||||
*/
|
||||
function sanitizeLabel(value) {
|
||||
const text = String(value || '');
|
||||
let out = '';
|
||||
for (const ch of text) {
|
||||
const code = ch.codePointAt(0);
|
||||
out += code < 0x20 || code === 0x7f ? ' ' : ch; // drop control chars incl. newlines
|
||||
}
|
||||
return out.replace(/\s+/g, ' ').trim().slice(0, 120);
|
||||
return normalizeDisplayText(value, 120);
|
||||
}
|
||||
|
||||
function cachePrecision(service) {
|
||||
@@ -4424,10 +4418,8 @@ function compactProperties(props) {
|
||||
}
|
||||
|
||||
function cleanText(value) {
|
||||
if (value == null || typeof value === 'object') return '';
|
||||
const text = String(value).trim();
|
||||
if (!text || text === 'undefined' || text === 'null') return '';
|
||||
return text.length > 180 ? `${text.slice(0, 177)}...` : text;
|
||||
const text = normalizeDisplayText(value);
|
||||
return text === 'undefined' || text === 'null' ? '' : text;
|
||||
}
|
||||
|
||||
function layerTitle(layerId) {
|
||||
@@ -4581,6 +4573,21 @@ const ANALYST_IDENTITY_FIELDS = Object.freeze([
|
||||
'noradId',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Analyst item keys that identify a record rather than describe it: the
|
||||
* referent registry, track_entity and locateLayerEntity match on them, so
|
||||
* display-text normalization must leave them byte-for-byte unchanged.
|
||||
*/
|
||||
const ANALYST_CANONICAL_FIELDS = new Set([
|
||||
'layerKey',
|
||||
'id',
|
||||
'icao24',
|
||||
'mmsi',
|
||||
'noradId',
|
||||
'registration',
|
||||
'callsign',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Compact one analyst record for the voice payload: identity, position (so a
|
||||
* follow-up can fly to it), distance, and that layer's manifest fields with
|
||||
@@ -4618,6 +4625,13 @@ function compactAnalystItem(r) {
|
||||
} else compact[key] = r[key];
|
||||
}
|
||||
if (present(r.ageHours)) compact.ageHours = r.ageHours;
|
||||
// Display text only: canonical keys are lookup identities (bikeshare uses
|
||||
// the raw station name as its id), so the referent registry and
|
||||
// locateLayerEntity must see them exactly as the layer reports them.
|
||||
for (const key of Object.keys(compact)) {
|
||||
if (typeof compact[key] === 'string' && !ANALYST_CANONICAL_FIELDS.has(key))
|
||||
compact[key] = normalizeDisplayText(compact[key]);
|
||||
}
|
||||
return compact;
|
||||
}
|
||||
|
||||
|
||||
@@ -3931,3 +3931,56 @@ test('analyst-only lookup distinguishes duplicate names and uses canonical aircr
|
||||
assert.equal(plane.ok, true);
|
||||
assert.equal(plane.latitude, 36, 'canonical ID resolves the current position, not old coordinates');
|
||||
});
|
||||
|
||||
|
||||
test('analyst item display strings are single-line and bounded for voice', async () => {
|
||||
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||
const rows = [{ id: 'stable-id', name: 'Şehir\n' + 'x'.repeat(3000), label: 'Town\tStation', operator: 'City\nPD', lat: 30, lon: -97 }];
|
||||
const { runner } = manifestRunner({ bikeshare: { getStats: () => ({ count: 1, lastUpdate: Date.now() }), getAnalystRecords: () => rows } });
|
||||
const result = await runner('analyst_query', { layers: ['bikeshare'], scope: { kind: 'anywhere' }, limit: 1 });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.items[0].name.length, 180);
|
||||
assert.equal(result.items[0].label, 'Town Station');
|
||||
assert.equal(result.items[0].id, 'stable-id');
|
||||
for (const value of Object.values(result.items[0])) if (typeof value === 'string') assert.doesNotMatch(value, /[\r\n\t]/);
|
||||
});
|
||||
|
||||
test('analyst_query keeps canonical IDs raw so numbered references still fly to the station', async () => {
|
||||
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||
const { viewer, styleManager } = createVoiceNavigationHarness();
|
||||
const referents = createReferentRegistry();
|
||||
const longName = 'Station ' + 'é'.repeat(200);
|
||||
const spaced = 'Congress Ave & 2nd';
|
||||
const stations = [
|
||||
{ id: spaced, name: spaced, lat: 30.2672, lon: -97.7431 },
|
||||
{ id: longName, name: longName, lat: 30.2711, lon: -97.7437 },
|
||||
];
|
||||
const bikeshare = { getStats: () => ({ count: stations.length, lastUpdate: Date.now() }), getAnalystRecords: () => stations };
|
||||
const runner = createActionRunner({
|
||||
viewer, styleManager,
|
||||
dataManager: {
|
||||
layers: new Map([['bikeshare', { module: bikeshare }]]),
|
||||
isEnabled: () => true,
|
||||
getAll: () => [{ id: 'bikeshare', name: 'bikeshare', enabled: true, stats: bikeshare.getStats() }],
|
||||
},
|
||||
deixis: { referents },
|
||||
});
|
||||
const listed = await runner('analyst_query', { layers: ['bikeshare'], scope: { kind: 'anywhere' }, limit: 2 });
|
||||
assert.equal(listed.ok, true);
|
||||
const bySpaced = listed.items.find((item) => item.id === spaced);
|
||||
const byLong = listed.items.find((item) => item.id === longName);
|
||||
assert.ok(bySpaced && byLong, 'compact items keep the raw station-name IDs');
|
||||
assert.equal(bySpaced.name, 'Congress Ave & 2nd', 'display name is still normalized');
|
||||
assert.equal(byLong.name.length, 180, 'display name is still bounded');
|
||||
assert.equal(byLong.id.length, longName.length);
|
||||
const registered = referents.list();
|
||||
assert.equal(registered.length, 2, 'the analyst result became the numbered list');
|
||||
for (const ref of registered) {
|
||||
const station = stations.find((row) => row.id === ref.id);
|
||||
assert.ok(station, `referent ${ref.n} keeps the canonical ID`);
|
||||
const flown = await runner('fly_to_location', { referent: ref.n });
|
||||
assert.equal(flown.ok, true, `referent ${ref.n} resolves instead of "no longer loaded"`);
|
||||
assert.equal(flown.latitude, station.lat);
|
||||
assert.equal(flown.longitude, station.lon);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user