@@ -147,6 +147,17 @@ function packagedWikipediaRecord(prefix) {
147147 };
148148}
149149
150+ function packagedFrankfurterRecord(prefix) {
151+ return {
152+ id: 'frankfurter-fx',
153+ name: 'Frankfurter FX',
154+ sourceType: 'built-in',
155+ sourceUrl: 'skills/frankfurter-fx.md',
156+ content: fs.readFileSync(path.join(ROOT, prefix, 'skills/frankfurter-fx.md'), 'utf8'),
157+ createdAt: 0,
158+ };
159+ }
160+
150161function packagedChromeWebStoreRecord(prefix) {
151162 return {
152163 id: 'chrome-web-store-release',
@@ -12810,6 +12821,7 @@ test('every bundled skill declares its canonical semantic intents', () => {
1281012821 'open-meteo-weather': ['current_weather', 'weather_forecast', 'location_forecast'],
1281112822 'open-library-books': ['book_search', 'book_metadata', 'isbn_lookup', 'author_lookup'],
1281212823 'wikipedia': ['wikipedia_search', 'encyclopedia_lookup', 'topic_summary', 'definition_lookup'],
12824+ 'frankfurter-fx': ['currency_conversion', 'exchange_rate', 'fx_lookup', 'currency_list'],
1281312825 'temporary-file-share-litterbox': ['temporary_file_share', 'public_upload_link', 'expiring_file_upload'],
1281412826 };
1281512827 for (const [label, prefix, sources, normalizeSkills] of [
@@ -13027,6 +13039,45 @@ test('packaged Open-Meteo and Open Library skills are opt-in with read-only HTTP
1302713039 }
1302813040});
1302913041
13042+ test('packaged Frankfurter FX skill is opt-in with read-only HTTPS tools on api.frankfurter.dev', () => {
13043+ for (const [label, prefix, normalizeSkills, buildPrompt, buildDefs] of [
13044+ ['chrome', 'src/chrome', normalizeCustomSkillsCh, buildCustomSkillsPromptCh, buildSkillToolDefinitionsCh],
13045+ ['firefox', 'src/firefox', normalizeCustomSkillsFx, buildCustomSkillsPromptFx, buildSkillToolDefinitionsFx],
13046+ ]) {
13047+ const defaults = normalizeSkills([packagedFreeSkillzRecord(prefix)]);
13048+ assert.doesNotMatch(buildPrompt(defaults), /Frankfurter FX/, `${label}: Frankfurter skill leaked into default prompt`);
13049+
13050+ const enabled = normalizeSkills([
13051+ ...defaults,
13052+ packagedFrankfurterRecord(prefix),
13053+ ]);
13054+ const prompt = buildPrompt(enabled, { mode: 'ask', tier: 'full', activeSkillIds: new Set(['frankfurter-fx']) });
13055+ assert.match(prompt, /Frankfurter FX/, `${label}: enabled Frankfurter skill missing from prompt`);
13056+ assert.doesNotMatch(prompt, /"endpoint": "https:\/\/api\.frankfurter\.dev\/v1\/latest"/, `${label}: Frankfurter endpoint JSON should stay out of prompt`);
13057+ assert.match(prompt, /api\.frankfurter\.dev/, `${label}: skill body should require non-redirecting Frankfurter host`);
13058+ assert.match(prompt, /api\.frankfurter\.app/, `${label}: skill body should warn against redirecting Frankfurter host`);
13059+
13060+ const fx = enabled.find((skill) => skill.id === 'frankfurter-fx');
13061+ assert.deepEqual(
13062+ fx.tools.map((tool) => tool.name),
13063+ ['list_frankfurter_currencies', 'get_frankfurter_rates'],
13064+ `${label}: Frankfurter manifest tools should parse`,
13065+ );
13066+ assert.equal(fx.tools[0].endpoint, 'https://api.frankfurter.dev/v1/currencies', `${label}: wrong currencies endpoint`);
13067+ assert.equal(fx.tools[1].endpoint, 'https://api.frankfurter.dev/v1/latest', `${label}: wrong rates endpoint`);
13068+ assert.equal(fx.tools[0].readOnly, true, `${label}: currencies tool should be read-only`);
13069+ assert.equal(fx.tools[1].readOnly, true, `${label}: rates tool should be read-only`);
13070+ assert.equal(fx.tools[1].defaultArgs?.base, 'EUR', `${label}: rates default base should be EUR`);
13071+ assert.equal(fx.tools[0].resultPolicy, 'untrusted', `${label}: currencies output should be untrusted`);
13072+ assert.equal(fx.tools[1].resultPolicy, 'untrusted', `${label}: rates output should be untrusted`);
13073+
13074+ const defs = buildDefs(enabled, { mode: 'ask' });
13075+ const names = defs.map((tool) => tool.function.name);
13076+ assert.ok(names.includes('list_frankfurter_currencies'), `${label}: currencies tool missing from ask mode`);
13077+ assert.ok(names.includes('get_frankfurter_rates'), `${label}: rates tool missing from ask mode`);
13078+ }
13079+ });
13080+
1303013081test('custom skills parse tool manifests without injecting manifest JSON into prompt', () => {
1303113082 for (const [label, prefix, normalizeSkills, buildPrompt, buildDefs] of [
1303213083 ['chrome', 'src/chrome', normalizeCustomSkillsCh, buildCustomSkillsPromptCh, buildSkillToolDefinitionsCh],
@@ -48559,6 +48610,7 @@ test('settings exposes custom skills tab and packaged skills resource directory'
4855948610 'open-meteo-weather',
4856048611 'open-library-books',
4856148612 'wikipedia',
48613+ 'frankfurter-fx',
4856248614 ]);
4856348615 assert.deepEqual(PACKAGED_SKILL_SOURCES_FX.map((skill) => skill.id), [
4856448616 'freeskillz-xyz',
@@ -48568,6 +48620,7 @@ test('settings exposes custom skills tab and packaged skills resource directory'
4856848620 'open-meteo-weather',
4856948621 'open-library-books',
4857048622 'wikipedia',
48623+ 'frankfurter-fx',
4857148624 ]);
4857248625 assert.deepEqual(DEFAULT_SKILL_SOURCES_CH.map((skill) => skill.id), [
4857348626 'freeskillz-xyz',
0 commit comments