diff --git a/.github/scripts/check-agent-gateway-migrations.py b/.github/scripts/check-agent-gateway-migrations.py new file mode 100644 index 000000000000..e038e6637d23 --- /dev/null +++ b/.github/scripts/check-agent-gateway-migrations.py @@ -0,0 +1,93 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Check Agent Gateway seed parity; not a native database syntax validator.""" + +import re +import sys +from pathlib import Path + +DIALECTS = ("mysql", "ob", "og", "oracle", "pg") +EXPECTED = { + "plugin": {"68"}, + "plugin_handle": {str(1960000000000001000 + i) for i in range(2)}, + "resource": {str(1960000000000001010 + i) for i in range(10)}, + "permission": {str(1960000000000001020 + i) for i in range(10)}, + "namespace_plugin_rel": {"1960000000000001030"}, +} +# The seed statements use SQL literals, including JSON strings and Oracle hints. +# Tokenize quoted text before stripping comments or normalizing whitespace. +TOKEN = re.compile(r"'(?:''|[^'])*'|/\*.*?\*/|--[^\n]*|[^\s]", re.S) +INSERT = re.compile( + r'INSERT\s+(?:/\*.*?\*/\s*)?INTO\s+(?:"public"\.)?' + r'[`"]?(\w+)[`"]?\s*(.*?)\bVALUES\s*\(\s*\'(\d+)\'', re.I | re.S +) + + +def seeds(sql): + """Extract exactly the reserved seed keys, rejecting duplicates and omissions.""" + result = {} + # Remove disabled statements without touching quoted values or Oracle hints. + sql = re.sub(r"'(?:''|[^'])*'|/\*.*?\*/|--[^\n]*", + lambda m: m.group() if m.group().startswith(("'", "/*+")) else " ", sql, flags=re.S) + for statement in re.finditer( + r"INSERT\s+(?:/\*.*?\*/\s*)?INTO\s+(?:'(?:''|[^'])*'|[^;'])*;", sql, re.I | re.S): + text = statement.group() + match = INSERT.match(text) + if not match: + continue + table, _, row_id = match.groups() + table = table.lower() + if table not in EXPECTED or row_id not in EXPECTED[table]: + continue + key = (table, row_id) + if key in result: + raise ValueError(f"duplicate seed {key}") + # Ignore formatting only; keep column lists, values, case and SQL hints. + result[key] = tuple(m.group() for m in TOKEN.finditer(text)) + required = {(table, row_id) for table, ids in EXPECTED.items() for row_id in ids} + if result.keys() != required: + raise ValueError(f"missing seeds: {sorted(required - result.keys())}") + return result + + +def check_pair(schema, migration): + """Compare every value and column list in each dialect's own seed records.""" + expected, actual = seeds(schema), seeds(migration) + for key in expected: + if expected[key] != actual[key]: + raise ValueError(f"fresh-install/upgrade mismatch: {key}") + return len(expected) + + +def check(root): + """Verify all supported upgrade dialects, regardless of working directory.""" + for dialect in DIALECTS: + filename = "create-table.sql" if dialect in ("og", "pg") else "schema.sql" + schema = root / "db" / "init" / dialect / filename + migration = root / "db" / "upgrade" / f"2.7.1-upgrade-2.7.2-{dialect}.sql" + try: + count = check_pair(schema.read_text(encoding="utf-8"), migration.read_text(encoding="utf-8")) + except ValueError as error: + raise ValueError(f"{dialect}: {error}") from error + print(f"{dialect}: {count} Agent Gateway seed rows match") + + +if __name__ == "__main__": + try: + check(Path(__file__).resolve().parents[2]) + except (OSError, ValueError) as error: + print(str(error), file=sys.stderr) + sys.exit(1) diff --git a/.github/scripts/test-agent-gateway-migrations.py b/.github/scripts/test-agent-gateway-migrations.py new file mode 100644 index 000000000000..c5ccd182834c --- /dev/null +++ b/.github/scripts/test-agent-gateway-migrations.py @@ -0,0 +1,76 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Regression tests for Agent Gateway migration/schema consistency checks.""" + +import importlib.util +from pathlib import Path +import sys +import unittest + +sys.dont_write_bytecode = True + +HERE = Path(__file__).resolve().parent +SPEC = importlib.util.spec_from_file_location("migration_check", HERE / "check-agent-gateway-migrations.py") +CHECK = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(CHECK) +ROOT = HERE.parents[1] + + +class MigrationConsistencyTest(unittest.TestCase): + def setUp(self): + self.schema = (ROOT / "db/init/mysql/schema.sql").read_text(encoding="utf-8") + self.migration = (ROOT / "db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql").read_text(encoding="utf-8") + + def test_all_dialects_match(self): + CHECK.check(ROOT) + + def test_missing_each_required_record_is_detected(self): + for table, ids in CHECK.EXPECTED.items(): + for row_id in ids: + with self.subTest(table=table, row_id=row_id): + changed = self.migration.replace(f"VALUES ('{row_id}'", "VALUES ('missing'", 1) + with self.assertRaisesRegex(ValueError, "missing seeds"): + CHECK.check_pair(self.schema, changed) + + def test_duplicate_is_detected(self): + with self.assertRaisesRegex(ValueError, "duplicate seed"): + CHECK.check_pair(self.schema, self.migration + self.migration) + + def test_changed_permission_is_detected(self): + changed = self.migration.replace("plugin:agentGatewayRule:delete", "plugin:agentGatewayRule:edit") + with self.assertRaisesRegex(ValueError, "mismatch"): + CHECK.check_pair(self.schema, changed) + + def test_changed_default_is_detected(self): + changed = self.migration.replace('"defaultValue":"LLM"', '"defaultValue":"MCP"') + with self.assertRaisesRegex(ValueError, "mismatch"): + CHECK.check_pair(self.schema, changed) + + def test_formatting_is_ignored(self): + self.assertEqual(24, CHECK.check_pair(self.schema, self.migration.replace("VALUES (", "VALUES\n ("))) + + def test_commented_out_migration_is_rejected(self): + disabled = "\n".join("-- " + line for line in self.migration.splitlines()) + with self.assertRaisesRegex(ValueError, "missing seeds"): + CHECK.check_pair(self.schema, disabled) + + def test_both_sides_missing_is_rejected(self): + with self.assertRaisesRegex(ValueError, "missing seeds"): + CHECK.check_pair("", "") + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e96958508605..2c1db1990bdf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,16 @@ concurrency: cancel-in-progress: true jobs: + agent_gateway_migrations: + if: github.repository == 'apache/shenyu' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check Agent Gateway upgrade and schema consistency + run: | + python3 .github/scripts/check-agent-gateway-migrations.py + python3 .github/scripts/test-agent-gateway-migrations.py + changes: if: github.repository == 'apache/shenyu' runs-on: ubuntu-latest diff --git a/db/init/mysql/schema.sql b/db/init/mysql/schema.sql index db91d5f12b19..e0f6ccc0d2bc 100644 --- a/db/init/mysql/schema.sql +++ b/db/init/mysql/schema.sql @@ -940,6 +940,16 @@ INSERT INTO `permission` VALUES ('1697146861569542757', '1346358560427216896', ' INSERT INTO `permission` VALUES ('1697146861569542758', '1346358560427216896', '1844026199075534867', '2023-08-31 07:18:37', '2023-08-31 07:18:37'); INSERT INTO `permission` VALUES ('1697146861569542759', '1346358560427216896', '1844026199075534868', '2023-08-31 07:18:37', '2023-08-31 07:18:37'); INSERT INTO `permission` VALUES ('1697146861569542760', '1346358560427216896', '1844026199075534869', '2023-08-31 07:18:37', '2023-08-31 07:18:37'); +INSERT INTO `permission` VALUES ('1960000000000001020', '1346358560427216896', '1960000000000001010', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001021', '1346358560427216896', '1960000000000001011', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001022', '1346358560427216896', '1960000000000001012', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001023', '1346358560427216896', '1960000000000001013', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001024', '1346358560427216896', '1960000000000001014', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001025', '1346358560427216896', '1960000000000001015', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001026', '1346358560427216896', '1960000000000001016', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001027', '1346358560427216896', '1960000000000001017', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001028', '1346358560427216896', '1960000000000001018', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001029', '1346358560427216896', '1960000000000001019', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); INSERT INTO `permission` VALUES ('1697146860569743000', '1346358560427216896', '1844026099075565000', '2023-08-31 06:59:01', '2023-08-31 06:59:01'); INSERT INTO `permission` VALUES ('1697146860569743001', '1346358560427216896', '1844026099075565001', '2023-08-31 07:22:07', '2023-08-31 07:22:07'); @@ -1019,6 +1029,7 @@ INSERT INTO `plugin` VALUES ('52', 'aiPrompt', null, 'Ai', 170, 0, '2023-12-20 1 INSERT INTO `plugin` VALUES ('53', 'aiRequestTransformer', NULL, 'Ai', 65, 0, '2023-12-20 18:02:53', '2023-12-20 18:02:53', null); INSERT INTO `plugin` VALUES ('61', 'mcpServer', null, 'MCP', 180, 0, '2023-12-20 18:02:53', '2023-12-20 18:02:53', null); +INSERT INTO `plugin` VALUES ('68', 'agentGateway', null, 'Ai', 198, 0, '2026-09-19 00:00:00', '2026-09-19 00:00:00', null); INSERT INTO `plugin` VALUES ('66', 'aiResponseTransformer', NULL, 'Ai', 66, 0, '2023-12-20 18:02:53', '2023-12-20 18:02:53', null); -- ---------------------------- @@ -1473,6 +1484,8 @@ INSERT INTO `plugin_handle` VALUES ('1722804548510507248', '53', 'model', 'model INSERT INTO `plugin_handle` VALUES ('1722804548510507249', '53', 'content', 'content', 2, 2, 4, '{"required":"0","rule":""}', '2025-03-12 06:02:32.450', '2025-03-12 06:02:32.450'); INSERT INTO `plugin_handle` VALUES ('1942847622591684608', '61', 'messageEndpoint', 'messageEndpoint', 2, 1, 0, '{"required":"0","defaultValue":"/message","rule":""}', '2025-07-09 07:25:44.249', '2025-07-09 07:25:44.249'); +INSERT INTO `plugin_handle` VALUES ('1960000000000001000', '68', 'trafficType', 'trafficType', 2, 2, 0, '{"required":"1","defaultValue":"LLM","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `plugin_handle` VALUES ('1960000000000001001', '68', 'responseRequestId', 'responseRequestId', 3, 2, 1, '{"required":"0","defaultValue":"false","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); INSERT INTO `plugin_handle` VALUES ('1722804548510507350', '66', 'provider', 'provider', 3, 3, 0, '{"required":"0","rule":""}', '2025-03-12 06:01:49.725', '2025-03-12 06:07:49.856'); INSERT INTO `plugin_handle` VALUES ('1722804548510507351', '66', 'baseUrl', 'baseUrl', 2, 3, 1, '{"required":"0","rule":""}', '2025-03-12 06:02:04.155', '2025-03-12 06:02:04.155'); @@ -2071,6 +2084,17 @@ INSERT INTO `resource` VALUES ('1844026199075534867', '1844026199075534860', 'SH INSERT INTO `resource` VALUES ('1844026199075534868', '1844026199075534860', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:mcpServerRule:delete', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); INSERT INTO `resource` VALUES ('1844026199075534869', '1844026199075534860', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:mcpServer:modify', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); +INSERT INTO `resource` VALUES ('1960000000000001010', '1346775491550474240', 'agentGateway', 'agentGateway', '/plug/agentGateway', 'agentGateway', 1, 0, 'pic-center', 0, 0, '', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001011', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001012', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001013', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001014', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001015', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001016', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001017', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001018', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001019', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGateway:modify', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); + INSERT INTO `resource` VALUES ('1844026099075565000', '1346775491550474240', 'aiResponseTransformer', 'aiResponseTransformer', '/plug/aiResponseTransformer', 'aiResponseTransformer', 1, 0, 'pic-center', 0, 0, '', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); INSERT INTO `resource` VALUES ('1844026099075565001', '1844026099075565000', 'SHENYU.BUTTON.PLUGIN.SELECTOR.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:aiResponseTransformerSelector:add', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); INSERT INTO `resource` VALUES ('1844026099075565002', '1844026099075565000', 'SHENYU.BUTTON.PLUGIN.SELECTOR.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:aiResponseTransformerSelector:query', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); @@ -2645,6 +2669,7 @@ INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, ` INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES ('1801816010882822190','649330b6-c2d7-4edc-be8e-8a54df9eb385','53', NULL, 65, 0, '2022-05-25 18:02:53.000', '2022-05-25 18:02:53.000'); INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES ('1801816010882832189','649330b6-c2d7-4edc-be8e-8a54df9eb385','61', NULL, 180, 0, '2022-05-25 18:02:53.000', '2022-05-25 18:02:53.000'); +INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES ('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', NULL, 198, 0, '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES ('1801816010882822300','649330b6-c2d7-4edc-be8e-8a54df9eb385','66', NULL, 66, 0, '2022-05-25 18:02:53.000', '2022-05-25 18:02:53.000'); diff --git a/db/init/ob/schema.sql b/db/init/ob/schema.sql index e54e876f4446..159ff514e4a5 100644 --- a/db/init/ob/schema.sql +++ b/db/init/ob/schema.sql @@ -923,6 +923,16 @@ INSERT INTO `permission` VALUES ('1697146861569542757', '1346358560427216896', ' INSERT INTO `permission` VALUES ('1697146861569542758', '1346358560427216896', '1844026199075534867', '2023-08-31 07:18:37', '2023-08-31 07:18:37'); INSERT INTO `permission` VALUES ('1697146861569542759', '1346358560427216896', '1844026199075534868', '2023-08-31 07:18:37', '2023-08-31 07:18:37'); INSERT INTO `permission` VALUES ('1697146861569542760', '1346358560427216896', '1844026199075534869', '2023-08-31 07:18:37', '2023-08-31 07:18:37'); +INSERT INTO `permission` VALUES ('1960000000000001020', '1346358560427216896', '1960000000000001010', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001021', '1346358560427216896', '1960000000000001011', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001022', '1346358560427216896', '1960000000000001012', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001023', '1346358560427216896', '1960000000000001013', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001024', '1346358560427216896', '1960000000000001014', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001025', '1346358560427216896', '1960000000000001015', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001026', '1346358560427216896', '1960000000000001016', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001027', '1346358560427216896', '1960000000000001017', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001028', '1346358560427216896', '1960000000000001018', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001029', '1346358560427216896', '1960000000000001019', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); -- ---------------------------- -- Table structure for plugin -- ---------------------------- @@ -987,6 +997,7 @@ INSERT INTO `plugin` VALUES ('50', 'aiProxy', '{"provider":"OpenAI","baseUrl":"h INSERT INTO `plugin` VALUES ('51', 'aiTokenLimiter', NULL, 'Ai', 171, 0, '2023-12-20 18:02:53', '2023-12-20 18:02:53', null); INSERT INTO `plugin` VALUES ('53', 'aiRequestTransformer', NULL, 'Ai', 65, 0, '2023-12-20 18:02:53', '2023-12-20 18:02:53', null); INSERT INTO `plugin` VALUES ('61', 'mcpServer', null, 'MCP', 180, 0, '2023-12-20 18:02:53', '2023-12-20 18:02:53', null); +INSERT INTO `plugin` VALUES ('68', 'agentGateway', NULL, 'Ai', 198, 0, '2026-09-19 00:00:00', '2026-09-19 00:00:00', null); INSERT INTO `plugin` VALUES ('66', 'aiResponseTransformer', NULL, 'Ai', 66, 0, '2023-12-20 18:02:53', '2023-12-20 18:02:53', null); -- ---------------------------- -- Table structure for plugin_handle @@ -1434,6 +1445,8 @@ INSERT INTO `plugin_handle` VALUES ('1722804548510507248', '53', 'model', 'model INSERT INTO `plugin_handle` VALUES ('1722804548510507249', '53', 'content', 'content', 2, 2, 4, '{"required":"0","rule":""}', '2025-03-12 06:02:32.450', '2025-03-12 06:02:32.450'); INSERT INTO `plugin_handle` VALUES ('1942847622591684608', '61', 'messageEndpoint', 'messageEndpoint', 2, 1, 0, '{"required":"0","defaultValue":"/message","rule":""}', '2025-07-09 07:25:44.249', '2025-07-09 07:25:44.249'); +INSERT INTO `plugin_handle` VALUES ('1960000000000001000', '68', 'trafficType', 'trafficType', 2, 2, 0, '{"required":"1","defaultValue":"LLM","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `plugin_handle` VALUES ('1960000000000001001', '68', 'responseRequestId', 'responseRequestId', 3, 2, 1, '{"required":"0","defaultValue":"false","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); -- ---------------------------- -- Table structure for resource -- ---------------------------- @@ -2002,6 +2015,17 @@ INSERT INTO `resource` VALUES ('1844026199075534867', '1844026199075534860', 'SH INSERT INTO `resource` VALUES ('1844026199075534868', '1844026199075534860', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:mcpServerRule:delete', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); INSERT INTO `resource` VALUES ('1844026199075534869', '1844026199075534860', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:mcpServer:modify', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); +INSERT INTO `resource` VALUES ('1960000000000001010', '1346775491550474240', 'agentGateway', 'agentGateway', '/plug/agentGateway', 'agentGateway', 1, 0, 'pic-center', 0, 0, '', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001011', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001012', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001013', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001014', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001015', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001016', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001017', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001018', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001019', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGateway:modify', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); + INSERT INTO `resource` VALUES ('1844026099075565000', '1346775491550474240', 'aiResponseTransformer', 'aiResponseTransformer', '/plug/aiResponseTransformer', 'aiResponseTransformer', 1, 0, 'pic-center', 0, 0, '', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); INSERT INTO `resource` VALUES ('1844026099075565001', '1844026099075565000', 'SHENYU.BUTTON.PLUGIN.SELECTOR.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:aiResponseTransformerSelector:add', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); INSERT INTO `resource` VALUES ('1844026099075565002', '1844026099075565000', 'SHENYU.BUTTON.PLUGIN.SELECTOR.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:aiResponseTransformerSelector:query', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); @@ -2569,6 +2593,7 @@ INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, ` INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES ('1801816010882822188','649330b6-c2d7-4edc-be8e-8a54df9eb385','51', NULL, 171, 0, '2022-05-25 18:02:53.000', '2022-05-25 18:02:53.000'); INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES ('1801816010882832189','649330b6-c2d7-4edc-be8e-8a54df9eb385','61', NULL, 180, 0, '2022-05-25 18:02:53.000', '2022-05-25 18:02:53.000'); INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES ('1801816010882822300','649330b6-c2d7-4edc-be8e-8a54df9eb385','66', NULL, 66, 0, '2022-05-25 18:02:53.000', '2022-05-25 18:02:53.000'); +INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES ('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', NULL, 198, 0, '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); INSERT INTO `resource` (`id`, `parent_id`, `title`, `name`, `url`, `component`, `resource_type`, `sort`, `icon`, `is_leaf`, `is_route`, `perms`, `status`, `date_created`, `date_updated`) VALUES ('1792749362445840479', '1357956838021890048', 'SHENYU.MENU.SYSTEM.MANAGMENT.NAMESPACEPLUGIN', 'namespacePlugin', '/config/namespacePlugin', 'namespacePlugin', 1, 2, 'build', 0, 0, '', 1, '2024-06-25 18:02:53.000', '2024-06-25 18:02:53.000'); INSERT INTO `resource` (`id`, `parent_id`, `title`, `name`, `url`, `component`, `resource_type`, `sort`, `icon`, `is_leaf`, `is_route`, `perms`, `status`, `date_created`, `date_updated`) VALUES ('1792749362445840480', '1792749362445840479', 'SHENYU.BUTTON.SYSTEM.LIST', '', '', '', 2, 0, '', 1, 0, 'system:namespacePlugin:list', 1, '2024-06-25 18:02:53.000', '2024-06-25 18:02:53.000'); diff --git a/db/init/og/create-table.sql b/db/init/og/create-table.sql index 4d1df63b5c2b..79d9efa27938 100644 --- a/db/init/og/create-table.sql +++ b/db/init/og/create-table.sql @@ -3051,8 +3051,12 @@ INSERT INTO "public"."permission" VALUES ('1697146860569742759', '13463585604272 INSERT INTO "public"."permission" VALUES ('1697146860569742760', '1346358560427216896', '1844026099075564868', '2023-08-31 07:18:37', '2023-08-31 07:18:37'); INSERT INTO "public"."plugin" VALUES ('61', 'mcpServer', NULL, 'MCP', 180, 0, '2023-12-20 18:02:53', '2023-12-20 18:02:53', NULL); +INSERT INTO "public"."plugin" VALUES ('68', 'agentGateway', NULL, 'Ai', 198, 0, '2026-09-19 00:00:00', '2026-09-19 00:00:00', NULL); +INSERT INTO "public"."namespace_plugin_rel" VALUES ('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', NULL, 198, 0, '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); INSERT INTO "public"."namespace_plugin_rel" VALUES ('1801816010882832189', '649330b6-c2d7-4edc-be8e-8a54df9eb385', '61', NULL, 180, 0, '2022-05-25 18:02:53.000', '2022-05-25 18:02:53.000'); INSERT INTO "public"."plugin_handle" VALUES ('1942847622591684608', '61', 'messageEndpoint', 'messageEndpoint', 2, 1, 0, '{"required":"0","defaultValue":"/message","rule":""}', '2025-07-09 07:25:44.249', '2025-07-09 07:25:44.249'); +INSERT INTO "public"."plugin_handle" VALUES ('1960000000000001000', '68', 'trafficType', 'trafficType', 2, 2, 0, '{"required":"1","defaultValue":"LLM","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."plugin_handle" VALUES ('1960000000000001001', '68', 'responseRequestId', 'responseRequestId', 3, 2, 1, '{"required":"0","defaultValue":"false","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); INSERT INTO "public"."resource" VALUES ('1844026199075534860', '1346775491550474240', 'mcpServer', 'mcpServer', '/plug/mcpServer', 'mcpServer', 1, 0, 'pic-left', 0, 0, '', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); INSERT INTO "public"."resource" VALUES ('1844026199075534861', '1844026199075534860', 'SHENYU.BUTTON.PLUGIN.SELECTOR.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:mcpServerSelector:add', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); INSERT INTO "public"."resource" VALUES ('1844026199075534862', '1844026199075534860', 'SHENYU.BUTTON.PLUGIN.SELECTOR.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:mcpServerSelector:query', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); @@ -3064,6 +3068,17 @@ INSERT INTO "public"."resource" VALUES ('1844026199075534867', '1844026199075534 INSERT INTO "public"."resource" VALUES ('1844026199075534868', '1844026199075534860', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:mcpServerRule:delete', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); INSERT INTO "public"."resource" VALUES ('1844026199075534869', '1844026199075534860', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:mcpServer:modify', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); +INSERT INTO "public"."resource" VALUES ('1960000000000001010', '1346775491550474240', 'agentGateway', 'agentGateway', '/plug/agentGateway', 'agentGateway', 1, 0, 'pic-center', 0, 0, '', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001011', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001012', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001013', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001014', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001015', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001016', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001017', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001018', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001019', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGateway:modify', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); + INSERT INTO "public"."permission" VALUES ('1697146861569542751', '1346358560427216896', '1844026199075534860', '2023-08-31 06:59:01', '2023-08-31 06:59:01'); INSERT INTO "public"."permission" VALUES ('1697146861569542752', '1346358560427216896', '1844026199075534861', '2023-08-31 07:22:07', '2023-08-31 07:22:07'); INSERT INTO "public"."permission" VALUES ('1697146861569542753', '1346358560427216896', '1844026199075534862', '2023-08-31 07:14:26', '2023-08-31 07:14:26'); @@ -3074,6 +3089,16 @@ INSERT INTO "public"."permission" VALUES ('1697146861569542757', '13463585604272 INSERT INTO "public"."permission" VALUES ('1697146861569542758', '1346358560427216896', '1844026199075534867', '2023-08-31 07:18:37', '2023-08-31 07:18:37'); INSERT INTO "public"."permission" VALUES ('1697146861569542759', '1346358560427216896', '1844026199075534868', '2023-08-31 07:18:37', '2023-08-31 07:18:37'); INSERT INTO "public"."permission" VALUES ('1697146861569542760', '1346358560427216896', '1844026199075534869', '2023-08-31 07:18:37', '2023-08-31 07:18:37'); +INSERT INTO "public"."permission" VALUES ('1960000000000001020', '1346358560427216896', '1960000000000001010', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001021', '1346358560427216896', '1960000000000001011', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001022', '1346358560427216896', '1960000000000001012', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001023', '1346358560427216896', '1960000000000001013', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001024', '1346358560427216896', '1960000000000001014', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001025', '1346358560427216896', '1960000000000001015', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001026', '1346358560427216896', '1960000000000001016', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001027', '1346358560427216896', '1960000000000001017', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001028', '1346358560427216896', '1960000000000001018', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001029', '1346358560427216896', '1960000000000001019', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); DROP TABLE IF EXISTS "public"."registry_config"; CREATE TABLE "public"."registry_config" ( diff --git a/db/init/oracle/schema.sql b/db/init/oracle/schema.sql index 635d80fa648b..89904f4da8ed 100644 --- a/db/init/oracle/schema.sql +++ b/db/init/oracle/schema.sql @@ -3748,12 +3748,20 @@ VALUES ('1722804548510507240', '53', 'provider', 'provider', 3, 3, 0, '{"require INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(plugin(id)) */ INTO plugin (id, name, config, role, sort, enabled, date_created, date_updated) VALUES ('61', 'mcpServer', NULL, 'MCP', 180, 0, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(plugin(id)) */ INTO plugin (id, name, config, role, sort, enabled, date_created, date_updated) +VALUES ('68', 'agentGateway', NULL, 'Ai', 198, 0, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX (namespace_plugin_rel(id)) */ INTO namespace_plugin_rel (id,namespace_id,plugin_id, config, sort, enabled) +VALUES ('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', NULL, 198, 0); INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(namespace_plugin_rel(id)) */ INTO namespace_plugin_rel (id, namespace_id, plugin_id, config, sort, enabled, date_created, date_updated) VALUES ('1801816010882832189', '649330b6-c2d7-4edc-be8e-8a54df9eb385', '61', NULL, 180, 0, sysdate, sysdate); INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(plugin_handle(id)) */ INTO plugin_handle (id, plugin_id, field, label, data_type, type, sort, ext_obj, date_created, date_updated) VALUES ('1942847622591684608', '61', 'messageEndpoint', 'messageEndpoint', 2, 1, 0, '{"required":"0","defaultValue":"/message","rule":""}', sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(plugin_handle(id)) */ INTO plugin_handle (id, plugin_id, field, label, data_type, type, sort, ext_obj, date_created, date_updated) +VALUES ('1960000000000001000', '68', 'trafficType', 'trafficType', 2, 2, 0, '{"required":"1","defaultValue":"LLM","rule":""}', sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(plugin_handle(id)) */ INTO plugin_handle (id, plugin_id, field, label, data_type, type, sort, ext_obj, date_created, date_updated) +VALUES ('1960000000000001001', '68', 'responseRequestId', 'responseRequestId', 3, 2, 1, '{"required":"0","defaultValue":"false","rule":""}', sysdate, sysdate); INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) VALUES ('1844026199075534860', '1346775491550474240', 'mcpServer', 'mcpServer', '/plug/mcpServer', 'mcpServer', 1, 0, 'pic-left', 0, 0, '', 1, sysdate, sysdate); @@ -3775,6 +3783,26 @@ INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, pare VALUES ('1844026199075534868', '1844026199075534860', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:mcpServerRule:delete', 1, sysdate, sysdate); INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) VALUES ('1844026199075534869', '1844026199075534860', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:mcpServer:modify', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001010', '1346775491550474240', 'agentGateway', 'agentGateway', '/plug/agentGateway', 'agentGateway', 1, 0, 'pic-center', 0, 0, '', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001011', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:add', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001012', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:query', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001013', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:edit', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001014', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:delete', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001015', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:add', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001016', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:query', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001017', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:edit', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001018', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:delete', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001019', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGateway:modify', 1, sysdate, sysdate); INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1697146861569542751', '1346358560427216896', '1844026199075534860', sysdate, sysdate); INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1697146861569542752', '1346358560427216896', '1844026199075534861', sysdate, sysdate); @@ -3786,6 +3814,16 @@ INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1697146861569542758', '1346358560427216896', '1844026199075534867', sysdate, sysdate); INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1697146861569542759', '1346358560427216896', '1844026199075534868', sysdate, sysdate); INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1697146861569542760', '1346358560427216896', '1844026199075534869', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001020', '1346358560427216896', '1960000000000001010', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001021', '1346358560427216896', '1960000000000001011', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001022', '1346358560427216896', '1960000000000001012', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001023', '1346358560427216896', '1960000000000001013', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001024', '1346358560427216896', '1960000000000001014', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001025', '1346358560427216896', '1960000000000001015', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001026', '1346358560427216896', '1960000000000001016', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001027', '1346358560427216896', '1960000000000001017', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001028', '1346358560427216896', '1960000000000001018', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001029', '1346358560427216896', '1960000000000001019', sysdate, sysdate); CREATE TABLE registry_config ( id varchar2(128) NOT NULL, diff --git a/db/init/pg/create-table.sql b/db/init/pg/create-table.sql index cc05bae144a4..ada3867541e9 100644 --- a/db/init/pg/create-table.sql +++ b/db/init/pg/create-table.sql @@ -3181,8 +3181,12 @@ INSERT INTO "public"."permission" VALUES ('1697146860569742759', '13463585604272 INSERT INTO "public"."permission" VALUES ('1697146860569742760', '1346358560427216896', '1844026099075564868', '2023-08-31 07:18:37', '2023-08-31 07:18:37'); INSERT INTO "public"."plugin" VALUES ('61', 'mcpServer', NULL, 'MCP', 180, 0, '2023-12-20 18:02:53', '2023-12-20 18:02:53', NULL); +INSERT INTO "public"."plugin" VALUES ('68', 'agentGateway', NULL, 'Ai', 198, 0, '2026-09-19 00:00:00', '2026-09-19 00:00:00', NULL); +INSERT INTO "public"."namespace_plugin_rel" VALUES ('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', NULL, 198, 0, '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); INSERT INTO "public"."namespace_plugin_rel" VALUES ('1801816010882832189', '649330b6-c2d7-4edc-be8e-8a54df9eb385', '61', NULL, 180, 0, '2022-05-25 18:02:53.000', '2022-05-25 18:02:53.000'); INSERT INTO "public"."plugin_handle" VALUES ('1942847622591684608', '61', 'messageEndpoint', 'messageEndpoint', 2, 1, 0, '{"required":"0","defaultValue":"/message","rule":""}', '2025-07-09 07:25:44.249', '2025-07-09 07:25:44.249'); +INSERT INTO "public"."plugin_handle" VALUES ('1960000000000001000', '68', 'trafficType', 'trafficType', 2, 2, 0, '{"required":"1","defaultValue":"LLM","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."plugin_handle" VALUES ('1960000000000001001', '68', 'responseRequestId', 'responseRequestId', 3, 2, 1, '{"required":"0","defaultValue":"false","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); INSERT INTO "public"."resource" VALUES ('1844026199075534860', '1346775491550474240', 'mcpServer', 'mcpServer', '/plug/mcpServer', 'mcpServer', 1, 0, 'pic-left', 0, 0, '', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); INSERT INTO "public"."resource" VALUES ('1844026199075534861', '1844026199075534860', 'SHENYU.BUTTON.PLUGIN.SELECTOR.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:mcpServerSelector:add', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); @@ -3195,6 +3199,17 @@ INSERT INTO "public"."resource" VALUES ('1844026199075534867', '1844026199075534 INSERT INTO "public"."resource" VALUES ('1844026199075534868', '1844026199075534860', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:mcpServerRule:delete', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); INSERT INTO "public"."resource" VALUES ('1844026199075534869', '1844026199075534860', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:mcpServer:modify', 1, '2022-05-25 18:02:58', '2022-05-25 18:02:58'); +INSERT INTO "public"."resource" VALUES ('1960000000000001010', '1346775491550474240', 'agentGateway', 'agentGateway', '/plug/agentGateway', 'agentGateway', 1, 0, 'pic-center', 0, 0, '', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001011', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001012', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001013', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001014', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001015', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001016', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001017', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001018', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001019', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGateway:modify', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); + INSERT INTO "public"."permission" VALUES ('1697146861569542751', '1346358560427216896', '1844026199075534860', '2023-08-31 06:59:01', '2023-08-31 06:59:01'); INSERT INTO "public"."permission" VALUES ('1697146861569542752', '1346358560427216896', '1844026199075534861', '2023-08-31 07:22:07', '2023-08-31 07:22:07'); INSERT INTO "public"."permission" VALUES ('1697146861569542753', '1346358560427216896', '1844026199075534862', '2023-08-31 07:14:26', '2023-08-31 07:14:26'); @@ -3205,6 +3220,16 @@ INSERT INTO "public"."permission" VALUES ('1697146861569542757', '13463585604272 INSERT INTO "public"."permission" VALUES ('1697146861569542758', '1346358560427216896', '1844026199075534867', '2023-08-31 07:18:37', '2023-08-31 07:18:37'); INSERT INTO "public"."permission" VALUES ('1697146861569542759', '1346358560427216896', '1844026199075534868', '2023-08-31 07:18:37', '2023-08-31 07:18:37'); INSERT INTO "public"."permission" VALUES ('1697146861569542760', '1346358560427216896', '1844026199075534869', '2023-08-31 07:18:37', '2023-08-31 07:18:37'); +INSERT INTO "public"."permission" VALUES ('1960000000000001020', '1346358560427216896', '1960000000000001010', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001021', '1346358560427216896', '1960000000000001011', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001022', '1346358560427216896', '1960000000000001012', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001023', '1346358560427216896', '1960000000000001013', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001024', '1346358560427216896', '1960000000000001014', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001025', '1346358560427216896', '1960000000000001015', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001026', '1346358560427216896', '1960000000000001016', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001027', '1346358560427216896', '1960000000000001017', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001028', '1346358560427216896', '1960000000000001018', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001029', '1346358560427216896', '1960000000000001019', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); DROP TABLE IF EXISTS "public"."registry_config"; CREATE TABLE "public"."registry_config" ( diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql b/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql index b20dabd01c69..f049f0b5939a 100644 --- a/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql +++ b/db/upgrade/2.7.1-upgrade-2.7.2-mysql.sql @@ -54,6 +54,32 @@ INSERT INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `dat INSERT INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1953049887387303974', '1346358560427216896', '1953048313980116914', '2026-09-21 00:00:00', '2026-09-21 00:00:00'); INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES ('1907261515594055681', '649330b6-c2d7-4edc-be8e-8a54df9eb385', '67', null, 197, 0, '2026-09-21 00:00:00', '2026-09-21 00:00:00'); +-- Agent Gateway: keep upgrade seeds consistent with the fresh-install schema. +INSERT INTO `plugin` VALUES ('68', 'agentGateway', null, 'Ai', 198, 0, '2026-09-19 00:00:00', '2026-09-19 00:00:00', null); +INSERT INTO `plugin_handle` VALUES ('1960000000000001000', '68', 'trafficType', 'trafficType', 2, 2, 0, '{"required":"1","defaultValue":"LLM","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `plugin_handle` VALUES ('1960000000000001001', '68', 'responseRequestId', 'responseRequestId', 3, 2, 1, '{"required":"0","defaultValue":"false","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001010', '1346775491550474240', 'agentGateway', 'agentGateway', '/plug/agentGateway', 'agentGateway', 1, 0, 'pic-center', 0, 0, '', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001011', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001012', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001013', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001014', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001015', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001016', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001017', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001018', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001019', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGateway:modify', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001020', '1346358560427216896', '1960000000000001010', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001021', '1346358560427216896', '1960000000000001011', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001022', '1346358560427216896', '1960000000000001012', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001023', '1346358560427216896', '1960000000000001013', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001024', '1346358560427216896', '1960000000000001014', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001025', '1346358560427216896', '1960000000000001015', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001026', '1346358560427216896', '1960000000000001016', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001027', '1346358560427216896', '1960000000000001017', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001028', '1346358560427216896', '1960000000000001018', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001029', '1346358560427216896', '1960000000000001019', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES ('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', NULL, 198, 0, '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); + -- add index to speed up the meta data path uniqueness check ALTER TABLE `meta_data` ADD INDEX `idx_meta_data_namespace_path` (`namespace_id`, `path`) USING BTREE; -- Secondary indexes for namespace filtering, relation lookups and ordered listings. diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql b/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql index 563fd8765b19..2a807e7739d2 100644 --- a/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql +++ b/db/upgrade/2.7.1-upgrade-2.7.2-ob.sql @@ -54,5 +54,30 @@ INSERT INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `dat INSERT INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1953049887387303974', '1346358560427216896', '1953048313980116914', '2026-09-21 00:00:00', '2026-09-21 00:00:00'); INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES ('1907261515594055681', '649330b6-c2d7-4edc-be8e-8a54df9eb385', '67', NULL, 197, 0, '2026-09-21 00:00:00', '2026-09-21 00:00:00'); +-- Agent Gateway: keep upgrade seeds consistent with the fresh-install schema. +INSERT INTO `plugin` VALUES ('68', 'agentGateway', NULL, 'Ai', 198, 0, '2026-09-19 00:00:00', '2026-09-19 00:00:00', null); +INSERT INTO `plugin_handle` VALUES ('1960000000000001000', '68', 'trafficType', 'trafficType', 2, 2, 0, '{"required":"1","defaultValue":"LLM","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `plugin_handle` VALUES ('1960000000000001001', '68', 'responseRequestId', 'responseRequestId', 3, 2, 1, '{"required":"0","defaultValue":"false","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001010', '1346775491550474240', 'agentGateway', 'agentGateway', '/plug/agentGateway', 'agentGateway', 1, 0, 'pic-center', 0, 0, '', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001011', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001012', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001013', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001014', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001015', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001016', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001017', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001018', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `resource` VALUES ('1960000000000001019', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGateway:modify', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001020', '1346358560427216896', '1960000000000001010', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001021', '1346358560427216896', '1960000000000001011', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001022', '1346358560427216896', '1960000000000001012', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001023', '1346358560427216896', '1960000000000001013', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001024', '1346358560427216896', '1960000000000001014', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001025', '1346358560427216896', '1960000000000001015', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001026', '1346358560427216896', '1960000000000001016', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001027', '1346358560427216896', '1960000000000001017', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001028', '1346358560427216896', '1960000000000001018', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `permission` VALUES ('1960000000000001029', '1346358560427216896', '1960000000000001019', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES ('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', NULL, 198, 0, '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); -- add index to speed up the meta data path uniqueness check ALTER TABLE `meta_data` ADD INDEX `idx_meta_data_namespace_path` (`namespace_id`, `path`) USING BTREE; diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-og.sql b/db/upgrade/2.7.1-upgrade-2.7.2-og.sql index 16767705de83..52ca18a73cbf 100644 --- a/db/upgrade/2.7.1-upgrade-2.7.2-og.sql +++ b/db/upgrade/2.7.1-upgrade-2.7.2-og.sql @@ -54,5 +54,30 @@ INSERT INTO "public"."permission" VALUES ('1953049887387303973', '13463585604272 INSERT INTO "public"."permission" VALUES ('1953049887387303974', '1346358560427216896', '1953048313980116914', '2026-09-21 00:00:00.000', '2026-09-21 00:00:00.000'); INSERT INTO "public"."namespace_plugin_rel" VALUES ('1907261515594055681', '649330b6-c2d7-4edc-be8e-8a54df9eb385', '67', NULL, 197, 0, '2026-09-21 00:00:00.000', '2026-09-21 00:00:00.000'); +-- Agent Gateway: keep upgrade seeds consistent with the fresh-install schema. +INSERT INTO "public"."plugin" VALUES ('68', 'agentGateway', NULL, 'Ai', 198, 0, '2026-09-19 00:00:00', '2026-09-19 00:00:00', NULL); +INSERT INTO "public"."plugin_handle" VALUES ('1960000000000001000', '68', 'trafficType', 'trafficType', 2, 2, 0, '{"required":"1","defaultValue":"LLM","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."plugin_handle" VALUES ('1960000000000001001', '68', 'responseRequestId', 'responseRequestId', 3, 2, 1, '{"required":"0","defaultValue":"false","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001010', '1346775491550474240', 'agentGateway', 'agentGateway', '/plug/agentGateway', 'agentGateway', 1, 0, 'pic-center', 0, 0, '', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001011', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001012', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001013', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001014', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001015', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001016', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001017', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001018', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001019', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGateway:modify', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001020', '1346358560427216896', '1960000000000001010', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001021', '1346358560427216896', '1960000000000001011', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001022', '1346358560427216896', '1960000000000001012', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001023', '1346358560427216896', '1960000000000001013', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001024', '1346358560427216896', '1960000000000001014', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001025', '1346358560427216896', '1960000000000001015', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001026', '1346358560427216896', '1960000000000001016', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001027', '1346358560427216896', '1960000000000001017', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001028', '1346358560427216896', '1960000000000001018', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001029', '1346358560427216896', '1960000000000001019', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."namespace_plugin_rel" VALUES ('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', NULL, 198, 0, '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); -- add index to speed up the meta data path uniqueness check CREATE INDEX "idx_meta_data_namespace_path" ON "public"."meta_data" USING btree ("namespace_id","path"); diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql b/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql index 9813ab9c3975..e7d6b2f4462a 100644 --- a/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql +++ b/db/upgrade/2.7.1-upgrade-2.7.2-oracle.sql @@ -54,5 +54,44 @@ INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1953049887387303974', '1346358560427216896', '1953048313980116914', sysdate, sysdate); INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(namespace_plugin_rel(id)) */ INTO namespace_plugin_rel (id, namespace_id, plugin_id, config, sort, enabled, date_created, date_updated) VALUES ('1907261515594055681', '649330b6-c2d7-4edc-be8e-8a54df9eb385', '67', NULL, 197, 0, sysdate, sysdate); +-- Agent Gateway: keep upgrade seeds consistent with the fresh-install schema. +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(plugin(id)) */ INTO plugin (id, name, config, role, sort, enabled, date_created, date_updated) +VALUES ('68', 'agentGateway', NULL, 'Ai', 198, 0, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(plugin_handle(id)) */ INTO plugin_handle (id, plugin_id, field, label, data_type, type, sort, ext_obj, date_created, date_updated) +VALUES ('1960000000000001000', '68', 'trafficType', 'trafficType', 2, 2, 0, '{"required":"1","defaultValue":"LLM","rule":""}', sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(plugin_handle(id)) */ INTO plugin_handle (id, plugin_id, field, label, data_type, type, sort, ext_obj, date_created, date_updated) +VALUES ('1960000000000001001', '68', 'responseRequestId', 'responseRequestId', 3, 2, 1, '{"required":"0","defaultValue":"false","rule":""}', sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001010', '1346775491550474240', 'agentGateway', 'agentGateway', '/plug/agentGateway', 'agentGateway', 1, 0, 'pic-center', 0, 0, '', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001011', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:add', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001012', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:query', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001013', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:edit', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001014', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:delete', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001015', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:add', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001016', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:query', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001017', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:edit', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001018', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:delete', 1, sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(resource(id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status, date_created, date_updated) +VALUES ('1960000000000001019', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGateway:modify', 1, sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001020', '1346358560427216896', '1960000000000001010', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001021', '1346358560427216896', '1960000000000001011', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001022', '1346358560427216896', '1960000000000001012', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001023', '1346358560427216896', '1960000000000001013', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001024', '1346358560427216896', '1960000000000001014', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001025', '1346358560427216896', '1960000000000001015', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001026', '1346358560427216896', '1960000000000001016', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001027', '1346358560427216896', '1960000000000001017', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001028', '1346358560427216896', '1960000000000001018', sysdate, sysdate); +INSERT INTO permission (id, object_id, resource_id, date_created, date_updated) VALUES ('1960000000000001029', '1346358560427216896', '1960000000000001019', sysdate, sysdate); +INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX (namespace_plugin_rel(id)) */ INTO namespace_plugin_rel (id,namespace_id,plugin_id, config, sort, enabled) +VALUES ('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', NULL, 198, 0); -- add index to speed up the meta data path uniqueness check create index idx_meta_data_namespace_path on meta_data (namespace_id, path); diff --git a/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql b/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql index cde896825527..fcfe36f1b3dd 100644 --- a/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql +++ b/db/upgrade/2.7.1-upgrade-2.7.2-pg.sql @@ -54,5 +54,30 @@ INSERT INTO "public"."permission" VALUES ('1953049887387303973', '13463585604272 INSERT INTO "public"."permission" VALUES ('1953049887387303974', '1346358560427216896', '1953048313980116914', '2026-09-21 00:00:00.000', '2026-09-21 00:00:00.000'); INSERT INTO "public"."namespace_plugin_rel" VALUES ('1907261515594055681', '649330b6-c2d7-4edc-be8e-8a54df9eb385', '67', NULL, 197, 0, '2026-09-21 00:00:00.000', '2026-09-21 00:00:00.000'); +-- Agent Gateway: keep upgrade seeds consistent with the fresh-install schema. +INSERT INTO "public"."plugin" VALUES ('68', 'agentGateway', NULL, 'Ai', 198, 0, '2026-09-19 00:00:00', '2026-09-19 00:00:00', NULL); +INSERT INTO "public"."plugin_handle" VALUES ('1960000000000001000', '68', 'trafficType', 'trafficType', 2, 2, 0, '{"required":"1","defaultValue":"LLM","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."plugin_handle" VALUES ('1960000000000001001', '68', 'responseRequestId', 'responseRequestId', 3, 2, 1, '{"required":"0","defaultValue":"false","rule":""}', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001010', '1346775491550474240', 'agentGateway', 'agentGateway', '/plug/agentGateway', 'agentGateway', 1, 0, 'pic-center', 0, 0, '', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001011', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001012', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001013', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001014', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SELECTOR.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewaySelector:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001015', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.ADD', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:add', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001016', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.QUERY', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:query', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001017', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.EDIT', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:edit', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001018', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.RULE.DELETE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGatewayRule:delete', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."resource" VALUES ('1960000000000001019', '1960000000000001010', 'SHENYU.BUTTON.PLUGIN.SYNCHRONIZE', '', '', '', 2, 0, '', 1, 0, 'plugin:agentGateway:modify', 1, '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001020', '1346358560427216896', '1960000000000001010', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001021', '1346358560427216896', '1960000000000001011', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001022', '1346358560427216896', '1960000000000001012', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001023', '1346358560427216896', '1960000000000001013', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001024', '1346358560427216896', '1960000000000001014', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001025', '1346358560427216896', '1960000000000001015', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001026', '1346358560427216896', '1960000000000001016', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001027', '1346358560427216896', '1960000000000001017', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001028', '1346358560427216896', '1960000000000001018', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."permission" VALUES ('1960000000000001029', '1346358560427216896', '1960000000000001019', '2026-09-19 00:00:00', '2026-09-19 00:00:00'); +INSERT INTO "public"."namespace_plugin_rel" VALUES ('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', NULL, 198, 0, '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); -- add index to speed up the meta data path uniqueness check CREATE INDEX "idx_meta_data_namespace_path" ON "public"."meta_data" USING btree ("namespace_id","path"); diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ConfigsServiceImpl.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ConfigsServiceImpl.java index 2a10c3ce3182..1c99cfa3e3fe 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ConfigsServiceImpl.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ConfigsServiceImpl.java @@ -53,6 +53,7 @@ import org.apache.shenyu.admin.service.RuleService; import org.apache.shenyu.admin.service.SelectorService; import org.apache.shenyu.admin.service.ShenyuDictService; +import org.apache.shenyu.admin.service.configs.ConfigsExportImportEnum; import org.apache.shenyu.admin.service.configs.ConfigsExportImportHandler; import org.apache.shenyu.admin.service.configs.ConfigsImportContext; import org.apache.shenyu.admin.utils.ZipUtil; @@ -68,10 +69,12 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; /** * Implementation of the {@link org.apache.shenyu.admin.service.ConfigsService}. @@ -81,6 +84,13 @@ public class ConfigsServiceImpl implements ConfigsService { private static final Logger LOG = LoggerFactory.getLogger(ConfigsServiceImpl.class); + private static final Map IMPORT_ORDER = Arrays.stream(ConfigsExportImportEnum.values()) + .collect(Collectors.toMap(ConfigsExportImportEnum::getConfigName, ConfigsExportImportEnum::getImportOrder)); + + static { + IMPORT_ORDER.put(ExportImportConstants.PLUGIN_JSON, ConfigsExportImportEnum.PluginTemplate.getImportOrder()); + } + /** * The max entry size for unzip. */ @@ -374,7 +384,10 @@ public ShenyuAdminResult configsImport(final byte[] source) { return ShenyuAdminResult.success(); } Map result = Maps.newHashMap(); - for (ZipUtil.ZipItem zipItem : zipItemList) { + // Import parents before rules regardless of ZIP entry order. + for (ZipUtil.ZipItem zipItem : zipItemList.stream() + .sorted(Comparator.comparingInt(item -> IMPORT_ORDER.getOrDefault(item.getItemName(), Integer.MAX_VALUE))) + .toList()) { switch (zipItem.getItemName()) { case ExportImportConstants.AUTH_JSON: importAuthData(result, zipItem); diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/RuleServiceImpl.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/RuleServiceImpl.java index 49855adc57e6..460bfa777e3e 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/RuleServiceImpl.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/RuleServiceImpl.java @@ -25,6 +25,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.shenyu.admin.aspect.annotation.DataPermission; import org.apache.shenyu.admin.aspect.annotation.Pageable; +import org.apache.shenyu.admin.exception.ShenyuAdminException; import org.apache.shenyu.admin.mapper.PluginMapper; import org.apache.shenyu.admin.mapper.RuleConditionMapper; import org.apache.shenyu.admin.mapper.RuleMapper; @@ -52,10 +53,12 @@ import org.apache.shenyu.admin.transfer.ConditionTransfer; import org.apache.shenyu.admin.utils.Assert; import org.apache.shenyu.admin.utils.SessionUtil; +import org.apache.shenyu.admin.validation.validator.AgentGatewayRuleHandleValidator; import org.apache.shenyu.common.constant.AdminConstants; import org.apache.shenyu.common.dto.ConditionData; import org.apache.shenyu.common.dto.RuleData; import org.apache.shenyu.common.enums.MatchModeEnum; +import org.apache.shenyu.common.enums.PluginEnum; import org.apache.shenyu.common.utils.JsonUtils; import org.apache.shenyu.common.utils.ListUtil; import org.apache.shenyu.common.utils.UUIDUtils; @@ -152,6 +155,7 @@ public String registerDefault(final RuleDTO ruleDTO) { if (Objects.nonNull(ruleMapper.findBySelectorIdAndName(ruleDTO.getSelectorId(), ruleDTO.getName()))) { return ""; } + validateRuleHandle(ruleDTO); RuleDO ruleDO = RuleDO.buildRuleDO(ruleDTO); if (StringUtils.isEmpty(ruleDTO.getId())) { ruleMapper.insertSelective(ruleDO); @@ -176,6 +180,7 @@ public int createOrUpdate(final RuleDTO ruleDTO) { @Override public int create(final RuleDTO ruleDTO) { + validateRuleHandle(ruleDTO); RuleDO ruleDO = RuleDO.buildRuleDO(ruleDTO); final int ruleCount = ruleMapper.insertSelective(ruleDO); addCondition(ruleDO, ruleDTO.getRuleConditions()); @@ -187,6 +192,7 @@ public int create(final RuleDTO ruleDTO) { @Override public int update(final RuleDTO ruleDTO) { + validateRuleHandle(ruleDTO); final RuleDO before = ruleMapper.selectById(ruleDTO.getId()); Assert.notNull(before, "the updated rule is not found"); RuleDO ruleDO = RuleDO.buildRuleDO(ruleDTO); @@ -318,6 +324,7 @@ public ConfigImportResult importData(final List ruleList) { .append(","); continue; } + validateRuleHandle(ruleDTO); RuleDO ruleDO = RuleDO.buildRuleDO(ruleDTO); final int ruleCount = ruleMapper.insertSelective(ruleDO); addCondition(ruleDO, ruleDTO.getRuleConditions()); @@ -370,6 +377,7 @@ public ConfigImportResult importData(final String namespace, final List .append(","); continue; } + validateRuleHandle(newSelectorId, ruleDTO.getHandle()); ruleDTO.setNamespaceId(namespace); ruleDTO.setSelectorId(newSelectorId); String ruleId = UUIDUtils.getInstance().generateShortUuid(); @@ -393,6 +401,24 @@ public ConfigImportResult importData(final String namespace, final List } return ConfigImportResult.success(successCount); } + + private void validateRuleHandle(final RuleDTO ruleDTO) { + validateRuleHandle(ruleDTO.getSelectorId(), ruleDTO.getHandle()); + } + + private void validateRuleHandle(final String selectorId, final String handle) { + final SelectorDO selector = selectorMapper.selectById(selectorId); + if (Objects.isNull(selector)) { + throw new ShenyuAdminException("rule selector is not found"); + } + final PluginDO plugin = pluginMapper.selectById(selector.getPluginId()); + if (Objects.isNull(plugin)) { + throw new ShenyuAdminException("rule plugin is not found"); + } + if (PluginEnum.AGENT_GATEWAY.getName().equals(plugin.getName())) { + AgentGatewayRuleHandleValidator.validate(handle); + } + } @Override @Transactional(rollbackFor = Exception.class) diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/validation/validator/AgentGatewayRuleHandleValidator.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/validation/validator/AgentGatewayRuleHandleValidator.java new file mode 100644 index 000000000000..f38167507cfd --- /dev/null +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/validation/validator/AgentGatewayRuleHandleValidator.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.admin.validation.validator; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import org.apache.commons.lang3.StringUtils; +import org.apache.shenyu.admin.exception.ShenyuAdminException; + +import java.util.Objects; +import java.util.Set; + +/** + * Validates agent gateway rule handles before Admin stores them. + */ +public final class AgentGatewayRuleHandleValidator { + + private static final String TRAFFIC_TYPE = "trafficType"; + + private static final String RESPONSE_REQUEST_ID = "responseRequestId"; + + private static final Set SUPPORTED_FIELDS = Set.of(TRAFFIC_TYPE, RESPONSE_REQUEST_ID); + + private AgentGatewayRuleHandleValidator() { + } + + /** + * Validate the complete rule handle, including unknown fields. + * + * @param handle rule handle supplied to Admin + */ + public static void validate(final String handle) { + if (StringUtils.isBlank(handle)) { + throw new ShenyuAdminException("agentGateway rule handle must not be empty"); + } + final JsonElement element; + try { + element = JsonParser.parseString(handle); + } catch (JsonParseException | IllegalStateException ex) { + throw new ShenyuAdminException("agentGateway rule handle must be valid JSON"); + } + if (!element.isJsonObject()) { + throw new ShenyuAdminException("agentGateway rule handle must be a JSON object"); + } + final JsonObject object = element.getAsJsonObject(); + for (String field : object.keySet()) { + if (!SUPPORTED_FIELDS.contains(field)) { + throw new ShenyuAdminException("agentGateway rule handle contains an unknown field; supported fields: trafficType, responseRequestId"); + } + } + final JsonElement trafficType = object.get(TRAFFIC_TYPE); + if (Objects.isNull(trafficType) || !trafficType.isJsonPrimitive() || !trafficType.getAsJsonPrimitive().isString() + || !"LLM".equals(trafficType.getAsString())) { + throw new ShenyuAdminException("agentGateway trafficType must be LLM"); + } + if (object.has(RESPONSE_REQUEST_ID)) { + final JsonElement responseRequestId = object.get(RESPONSE_REQUEST_ID); + if (!responseRequestId.isJsonPrimitive() || !responseRequestId.getAsJsonPrimitive().isBoolean()) { + throw new ShenyuAdminException("agentGateway responseRequestId must be a boolean"); + } + } + } +} diff --git a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql index 8987311a85b3..4fcb873aead3 100644 --- a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql +++ b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql @@ -611,6 +611,7 @@ INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `sort`, `config`, `enabled`) INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `sort`, `config`, `enabled`) VALUES ('53', 'aiRequestTransformer', 'Ai', 65, null, '0'); INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `sort`, `config`, `enabled`) VALUES ('61', 'mcpServer', 'MCP', 180, null, '0'); +INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `sort`, `config`, `enabled`) VALUES ('68', 'agentGateway', 'Ai', 198, null, '0'); /*insert plugin_handle data for sentinel*/ INSERT IGNORE INTO plugin_handle (`id`, `plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES ('1529402613195784246', '10', 'flowRuleGrade', 'flowRuleGrade', 3, 2, 8, '{"required":"1","defaultValue":"1","rule":""}'); @@ -1041,6 +1042,8 @@ INSERT IGNORE INTO plugin_handle (`id`, `plugin_id`,`field`,`label`,`data_type`, INSERT IGNORE INTO plugin_handle (`id`, `plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES ('1722804548510507242', '53', 'apiKey', 'apiKey', 2, 3, 2, '{"required":"0","rule":""}'); INSERT IGNORE INTO plugin_handle (`id`, `plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES ('1722804548510507243', '53', 'model', 'model', 2, 3, 3, '{"required":"0","rule":""}'); INSERT IGNORE INTO plugin_handle (`id`, `plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES ('1722804548510507244', '53', 'content', 'content', 2, 3, 4, '{"required":"0","rule":""}'); +INSERT IGNORE INTO plugin_handle (`id`, `plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES ('1960000000000001000', '68', 'trafficType', 'trafficType', 2, 2, 0, '{"required":"1","defaultValue":"LLM","rule":""}'); +INSERT IGNORE INTO plugin_handle (`id`, `plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES ('1960000000000001001', '68', 'responseRequestId', 'responseRequestId', 3, 2, 1, '{"required":"0","defaultValue":"false","rule":""}'); INSERT IGNORE INTO plugin_handle (`id`, `plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES ('1722804548510507245', '53', 'provider', 'provider', 3, 2, 0, '{"required":"0","rule":""}'); INSERT IGNORE INTO plugin_handle (`id`, `plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES ('1722804548510507246', '53', 'baseUrl', 'baseUrl', 2, 2, 1, '{"required":"0","rule":""}'); @@ -1484,6 +1487,7 @@ INSERT IGNORE INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `con INSERT IGNORE INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES ('1907261515594055680','649330b6-c2d7-4edc-be8e-8a54df9eb385','61', NULL, 180, 0, '2025-04-02 10:39:15.000', '2025-04-02 10:39:15.000'); +INSERT IGNORE INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`, `config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES ('1960000000000001030','649330b6-c2d7-4edc-be8e-8a54df9eb385','68', NULL, 198, 0, '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); @@ -1632,6 +1636,19 @@ INSERT IGNORE INTO `resource` (`id`,`parent_id`,`title`,`name`,`url`,`component` ('1844026099075534903','1844026099075534849','SHENYU.BUTTON.AI.PROXY.APIKEY.DELETE','','','',2,3,'',1,0,'system:aiProxyApiKey:delete',1,'2022-05-25 18:02:58.000','2022-05-25 18:02:58.000'), ('1844026099075534904','1844026099075534849','SHENYU.BUTTON.AI.PROXY.APIKEY.DISABLE','','','',2,4,'',1,0,'system:aiProxyApiKey:disable',1,'2022-05-25 18:02:58.000','2022-05-25 18:02:58.000'); +-- agentGateway menu and buttons +INSERT IGNORE INTO `resource` (`id`, `parent_id`, `title`, `name`, `url`, `component`, `resource_type`, `sort`, `icon`, `is_leaf`, `is_route`, `perms`, `status`, `date_created`, `date_updated`) VALUES ('1960000000000001010', '1346775491550474240', 'agentGateway', 'agentGateway', '/plug/agentGateway', 'agentGateway', 1, 0, 'pic-center', 0, 0, '', 1, '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); +INSERT IGNORE INTO `resource` (`id`,`parent_id`,`title`,`name`,`url`,`component`,`resource_type`,`sort`,`icon`,`is_leaf`,`is_route`,`perms`,`status`,`date_created`,`date_updated`) VALUES +('1960000000000001011','1960000000000001010','SHENYU.BUTTON.PLUGIN.SELECTOR.ADD','','','',2,0,'',1,0,'plugin:agentGatewaySelector:add',1,'2026-09-19 00:00:00.000','2026-09-19 00:00:00.000'), +('1960000000000001012','1960000000000001010','SHENYU.BUTTON.PLUGIN.SELECTOR.QUERY','','','',2,0,'',1,0,'plugin:agentGatewaySelector:query',1,'2026-09-19 00:00:00.000','2026-09-19 00:00:00.000'), +('1960000000000001013','1960000000000001010','SHENYU.BUTTON.PLUGIN.SELECTOR.EDIT','','','',2,0,'',1,0,'plugin:agentGatewaySelector:edit',1,'2026-09-19 00:00:00.000','2026-09-19 00:00:00.000'), +('1960000000000001014','1960000000000001010','SHENYU.BUTTON.PLUGIN.SELECTOR.DELETE','','','',2,0,'',1,0,'plugin:agentGatewaySelector:delete',1,'2026-09-19 00:00:00.000','2026-09-19 00:00:00.000'), +('1960000000000001015','1960000000000001010','SHENYU.BUTTON.PLUGIN.RULE.ADD','','','',2,0,'',1,0,'plugin:agentGatewayRule:add',1,'2026-09-19 00:00:00.000','2026-09-19 00:00:00.000'), +('1960000000000001016','1960000000000001010','SHENYU.BUTTON.PLUGIN.RULE.QUERY','','','',2,0,'',1,0,'plugin:agentGatewayRule:query',1,'2026-09-19 00:00:00.000','2026-09-19 00:00:00.000'), +('1960000000000001017','1960000000000001010','SHENYU.BUTTON.PLUGIN.RULE.EDIT','','','',2,0,'',1,0,'plugin:agentGatewayRule:edit',1,'2026-09-19 00:00:00.000','2026-09-19 00:00:00.000'), +('1960000000000001018','1960000000000001010','SHENYU.BUTTON.PLUGIN.RULE.DELETE','','','',2,0,'',1,0,'plugin:agentGatewayRule:delete',1,'2026-09-19 00:00:00.000','2026-09-19 00:00:00.000'), +('1960000000000001019','1960000000000001010','SHENYU.BUTTON.PLUGIN.SYNCHRONIZE','','','',2,0,'',1,0,'plugin:agentGateway:modify',1,'2026-09-19 00:00:00.000','2026-09-19 00:00:00.000'); + -- grant aiProxy menu to admin role INSERT IGNORE INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1697146860569542741', '1346358560427216896', '1844026099075534849', '2023-08-31 06:59:01.000', '2023-08-31 06:59:01.000'); @@ -1652,6 +1669,17 @@ INSERT IGNORE INTO `permission` (`id`, `object_id`, `resource_id`, `date_created INSERT IGNORE INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1953049887387303918', '1346358560427216896', '1844026099075534903', '2023-08-31 06:59:01.000', '2023-08-31 06:59:01.000'); INSERT IGNORE INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1953049887387303919', '1346358560427216896', '1844026099075534904', '2023-08-31 06:59:01.000', '2023-08-31 06:59:01.000'); +-- grant agentGateway menu and buttons to admin role +INSERT IGNORE INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1960000000000001020', '1346358560427216896', '1960000000000001010', '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); +INSERT IGNORE INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1960000000000001021', '1346358560427216896', '1960000000000001011', '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); +INSERT IGNORE INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1960000000000001022', '1346358560427216896', '1960000000000001012', '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); +INSERT IGNORE INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1960000000000001023', '1346358560427216896', '1960000000000001013', '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); +INSERT IGNORE INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1960000000000001024', '1346358560427216896', '1960000000000001014', '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); +INSERT IGNORE INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1960000000000001025', '1346358560427216896', '1960000000000001015', '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); +INSERT IGNORE INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1960000000000001026', '1346358560427216896', '1960000000000001016', '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); +INSERT IGNORE INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1960000000000001027', '1346358560427216896', '1960000000000001017', '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); +INSERT IGNORE INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1960000000000001028', '1346358560427216896', '1960000000000001018', '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); +INSERT IGNORE INTO `permission` (`id`, `object_id`, `resource_id`, `date_created`, `date_updated`) VALUES ('1960000000000001029', '1346358560427216896', '1960000000000001019', '2026-09-19 00:00:00.000', '2026-09-19 00:00:00.000'); /*insert data for the sensitive word plugin*/ INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `sort`, `config`, `enabled`) VALUES ('67', 'sensitiveWord', 'Ai', 197, null, '0'); INSERT IGNORE INTO plugin_handle (`id`, `plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES ('1942847622591684609', '67', 'url', 'url', 2, 3, 0, '{\"required\":\"0\",\"rule\":\"\"}'); diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ConfigsServiceTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ConfigsServiceTest.java index b113f75b311d..50422132b65b 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ConfigsServiceTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ConfigsServiceTest.java @@ -18,12 +18,23 @@ package org.apache.shenyu.admin.service; import com.google.common.collect.Lists; +import org.apache.shenyu.admin.exception.ShenyuAdminException; +import org.apache.shenyu.admin.mapper.PluginMapper; +import org.apache.shenyu.admin.mapper.RuleConditionMapper; +import org.apache.shenyu.admin.mapper.RuleMapper; +import org.apache.shenyu.admin.mapper.SelectorMapper; +import org.apache.shenyu.admin.model.dto.RuleDTO; +import org.apache.shenyu.admin.model.entity.PluginDO; +import org.apache.shenyu.admin.model.entity.SelectorDO; import org.apache.shenyu.admin.model.result.ConfigImportResult; import org.apache.shenyu.admin.model.result.ShenyuAdminResult; import org.apache.shenyu.admin.service.impl.ConfigsServiceImpl; +import org.apache.shenyu.admin.service.impl.RuleServiceImpl; +import org.apache.shenyu.admin.service.publish.RuleEventPublisher; import org.apache.shenyu.admin.utils.ZipUtil; import org.apache.shenyu.common.constant.ExportImportConstants; import org.apache.shenyu.common.exception.CommonErrorCode; +import org.apache.shenyu.common.utils.JsonUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -37,7 +48,13 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** @@ -147,6 +164,47 @@ public void testConfigsImport() { assertThat(resultData.get(ExportImportConstants.DISCOVERY_IMPORT_SUCCESS_COUNT), is(1)); } + @Test + public void testRuleBeforeSelectorInZipCannotBypassHandleValidation() { + RuleMapper ruleMapper = mock(RuleMapper.class); + SelectorMapper selectorMapper = mock(SelectorMapper.class); + PluginMapper pluginMapper = mock(PluginMapper.class); + RuleService ruleServiceImpl = new RuleServiceImpl(ruleMapper, mock(RuleConditionMapper.class), + selectorMapper, pluginMapper, mock(RuleEventPublisher.class)); + ConfigsServiceImpl importService = new ConfigsServiceImpl(appAuthService, pluginService, namespacePluginService, + pluginHandleService, selectorService, ruleServiceImpl, metaDataService, shenyuDictService, + proxySelectorService, discoveryService, discoveryUpstreamService, Collections.emptyList()); + ReflectionTestUtils.setField(importService, "maxEntrySize", MAX_ENTRY_SIZE); + ReflectionTestUtils.setField(importService, "maxTotalSize", MAX_TOTAL_SIZE); + ReflectionTestUtils.setField(importService, "maxEntryCount", MAX_ENTRY_COUNT); + + when(pluginService.importData(any(), isNull())).thenAnswer(invocation -> { + when(pluginMapper.selectById("plugin-1")).thenReturn(PluginDO.builder() + .id("plugin-1").name("agentGateway").build()); + return ConfigImportResult.success(1); + }); + when(selectorService.importData(any())).thenAnswer(invocation -> { + when(selectorMapper.selectById("selector-1")).thenReturn(SelectorDO.builder() + .id("selector-1").pluginId("plugin-1").build()); + return ConfigImportResult.success(1); + }); + when(ruleMapper.selectAll()).thenReturn(Collections.emptyList()); + + RuleDTO rule = RuleDTO.builder().selectorId("selector-1").name("agent-rule") + .handle("{\"trafficType\":\"LLM\",\"unexpected\":true}") + .ruleConditions(Collections.emptyList()).build(); + byte[] zip = ZipUtil.zip(Lists.newArrayList( + new ZipUtil.ZipItem(ExportImportConstants.RULE_JSON, JsonUtils.toJson(Collections.singletonList(rule))), + new ZipUtil.ZipItem(ExportImportConstants.SELECTOR_JSON, "[]"), + new ZipUtil.ZipItem(ExportImportConstants.PLUGIN_JSON, "[]"))); + + ShenyuAdminException exception = assertThrows(ShenyuAdminException.class, () -> importService.configsImport(zip)); + assertTrue(exception.getMessage().contains("unknown field")); + verify(pluginService).importData(any(), isNull()); + verify(selectorService).importData(any()); + verify(ruleMapper, never()).insertSelective(any()); + } + private byte[] buildImportData() { List zipItemList = Lists.newArrayList(); diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/RuleServiceTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/RuleServiceTest.java index d8fbb27e8982..44df60f0360e 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/RuleServiceTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/RuleServiceTest.java @@ -22,6 +22,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.apache.commons.lang3.StringUtils; +import org.apache.shenyu.admin.exception.ShenyuAdminException; import org.apache.shenyu.admin.mapper.DataPermissionMapper; import org.apache.shenyu.admin.mapper.PluginMapper; import org.apache.shenyu.admin.mapper.RuleConditionMapper; @@ -44,10 +45,12 @@ import org.apache.shenyu.admin.model.query.RuleQueryCondition; import org.apache.shenyu.admin.model.result.ConfigImportResult; import org.apache.shenyu.admin.model.vo.RuleVO; +import org.apache.shenyu.admin.service.configs.ConfigsImportContext; import org.apache.shenyu.admin.service.impl.RuleServiceImpl; import org.apache.shenyu.admin.service.publish.RuleEventPublisher; import org.apache.shenyu.admin.utils.JwtUtils; import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.enums.PluginEnum; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -75,6 +78,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; @@ -205,7 +209,8 @@ public void testImportData() { final List ruleDTOS = Collections.singletonList(buildRuleDTO("123")); given(this.ruleMapper.insertSelective(any())).willReturn(1); - given(this.pluginMapper.selectById(any())).willReturn(buildPluginDO()); + given(this.selectorMapper.selectById("456")).willReturn(buildSelectorDO()); + given(this.pluginMapper.selectById("789")).willReturn(buildPluginDO()); ConfigImportResult configImportResult = this.ruleService.importData(ruleDTOS); @@ -214,6 +219,96 @@ public void testImportData() { } + @Test + public void testAgentGatewayCreateRejectsUnknownHandleFieldBeforeWrite() { + mockAgentGatewayPlugin(); + RuleDTO ruleDTO = buildRuleDTO(""); + ruleDTO.setHandle("{\"trafficType\":\"LLM\",\"unexpected\":true}"); + + assertThrows(ShenyuAdminException.class, () -> ruleService.createOrUpdate(ruleDTO)); + + verify(ruleMapper, never()).insertSelective(any()); + verify(ruleEventPublisher, never()).onCreated(any(), any()); + } + + @Test + public void testAgentGatewayUpdateRejectsUnknownHandleFieldBeforeWrite() { + mockAgentGatewayPlugin(); + RuleDTO ruleDTO = buildRuleDTO("123"); + ruleDTO.setHandle("{\"trafficType\":\"LLM\",\"unexpected\":true}"); + + assertThrows(ShenyuAdminException.class, () -> ruleService.createOrUpdate(ruleDTO)); + + verify(ruleMapper, never()).updateSelective(any()); + verify(ruleEventPublisher, never()).onUpdated(any(), any(), any(), any()); + } + + @Test + public void testAgentGatewayCreateAcceptsValidHandle() { + mockAgentGatewayPlugin(); + RuleDTO ruleDTO = buildRuleDTO(""); + ruleDTO.setHandle("{\"trafficType\":\"LLM\",\"responseRequestId\":true}"); + given(ruleMapper.insertSelective(any())).willReturn(1); + + assertEquals(1, ruleService.createOrUpdate(ruleDTO)); + verify(ruleEventPublisher).onCreated(any(), any()); + } + + @Test + public void testAgentGatewayImportRejectsUnknownHandleFieldBeforeWrite() { + mockAgentGatewayPlugin(); + given(ruleMapper.selectAll()).willReturn(Collections.emptyList()); + RuleDTO ruleDTO = buildRuleDTO(""); + ruleDTO.setHandle("{\"trafficType\":\"LLM\",\"unexpected\":true}"); + + assertThrows(ShenyuAdminException.class, () -> ruleService.importData(Collections.singletonList(ruleDTO))); + verify(ruleMapper, never()).insertSelective(any()); + } + + @Test + public void testAgentGatewayNamespaceImportValidatesMappedSelectorBeforeWrite() { + mockAgentGatewayPlugin(); + given(ruleMapper.selectAllByNamespaceId(SYS_DEFAULT_NAMESPACE_ID)).willReturn(Collections.emptyList()); + ConfigsImportContext context = new ConfigsImportContext(); + context.getSelectorIdMapping().put("source-selector", "456"); + RuleDTO ruleDTO = buildRuleDTO(""); + ruleDTO.setSelectorId("source-selector"); + ruleDTO.setHandle("{\"trafficType\":\"LLM\",\"unexpected\":true}"); + + assertThrows(ShenyuAdminException.class, () -> ruleService.importData( + SYS_DEFAULT_NAMESPACE_ID, Collections.singletonList(ruleDTO), context)); + verify(ruleMapper, never()).insertSelective(any()); + } + + @Test + public void testImportRejectsRuleWhenSelectorIsMissing() { + given(ruleMapper.selectAll()).willReturn(Collections.emptyList()); + + assertThrows(ShenyuAdminException.class, () -> ruleService.importData( + Collections.singletonList(buildRuleDTO("")))); + + verify(ruleMapper, never()).insertSelective(any()); + } + + @Test + public void testImportRejectsRuleWhenPluginIsMissing() { + given(ruleMapper.selectAll()).willReturn(Collections.emptyList()); + given(selectorMapper.selectById("456")).willReturn(buildSelectorDO()); + + assertThrows(ShenyuAdminException.class, () -> ruleService.importData( + Collections.singletonList(buildRuleDTO("")))); + + verify(ruleMapper, never()).insertSelective(any()); + } + + private void mockAgentGatewayPlugin() { + given(selectorMapper.selectById("456")).willReturn(buildSelectorDO()); + given(pluginMapper.selectById("789")).willReturn(PluginDO.builder() + .id("789") + .name(PluginEnum.AGENT_GATEWAY.getName()) + .build()); + } + @Test public void testListAllWithSelectorNull() { mockFindSelectorIsNull(); diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/validation/validator/AgentGatewayRuleHandleValidatorTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/validation/validator/AgentGatewayRuleHandleValidatorTest.java new file mode 100644 index 000000000000..b55e953b3715 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/validation/validator/AgentGatewayRuleHandleValidatorTest.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.admin.validation.validator; + +import org.apache.shenyu.admin.exception.ShenyuAdminException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for agent gateway rule handle validation at the Admin write boundary. + */ +public final class AgentGatewayRuleHandleValidatorTest { + + @Test + public void testValidHandles() { + assertDoesNotThrow(() -> AgentGatewayRuleHandleValidator.validate("{\"trafficType\":\"LLM\"}")); + assertDoesNotThrow(() -> AgentGatewayRuleHandleValidator.validate("{\"trafficType\":\"LLM\",\"responseRequestId\":false}")); + } + + @ParameterizedTest + @ValueSource(strings = { + "", + "not-json", + "[]", + "{}", + "{\"trafficType\":\"MCP\"}", + "{\"trafficType\":null}", + "{\"trafficType\":true}", + "{\"trafficType\":\"LLM\",\"responseRequestId\":\"true\"}", + "{\"trafficType\":\"LLM\",\"responseRequestId\":null}", + "{\"trafficType\":\"LLM\",\"extra\":42}" + }) + public void testInvalidHandles(final String handle) { + assertThrows(ShenyuAdminException.class, () -> AgentGatewayRuleHandleValidator.validate(handle)); + } + + @Test + public void testNullHandle() { + assertThrows(ShenyuAdminException.class, () -> AgentGatewayRuleHandleValidator.validate(null)); + } +} diff --git a/shenyu-bootstrap/pom.xml b/shenyu-bootstrap/pom.xml index caabd7d17f15..504bb9f2bdbe 100644 --- a/shenyu-bootstrap/pom.xml +++ b/shenyu-bootstrap/pom.xml @@ -287,6 +287,14 @@ + + + org.apache.shenyu + shenyu-spring-boot-starter-plugin-agent-gateway + ${project.version} + + + org.apache.shenyu diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/enums/PluginEnum.java b/shenyu-common/src/main/java/org/apache/shenyu/common/enums/PluginEnum.java index 0ad0f12febc3..9437174079e4 100644 --- a/shenyu-common/src/main/java/org/apache/shenyu/common/enums/PluginEnum.java +++ b/shenyu-common/src/main/java/org/apache/shenyu/common/enums/PluginEnum.java @@ -282,6 +282,11 @@ public enum PluginEnum { */ AI_PROXY(199, 0, "aiProxy"), + /** + * Agent gateway plugin enum. + */ + AGENT_GATEWAY(198, 0, "agentGateway"), + /** * Ai-token-limiter plugin enum. */ diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-logging-rocketmq/src/test/java/org/apache/shenyu/e2e/testcase/logging/rocketmq/DividePluginTest.java b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-logging-rocketmq/src/test/java/org/apache/shenyu/e2e/testcase/logging/rocketmq/DividePluginTest.java index 66f6ee5bbe27..1cedb6e687d2 100644 --- a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-logging-rocketmq/src/test/java/org/apache/shenyu/e2e/testcase/logging/rocketmq/DividePluginTest.java +++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-logging-rocketmq/src/test/java/org/apache/shenyu/e2e/testcase/logging/rocketmq/DividePluginTest.java @@ -30,6 +30,8 @@ import org.apache.shenyu.e2e.enums.ServiceTypeEnum; import org.apache.shenyu.e2e.model.ResourcesData; import org.apache.shenyu.e2e.model.data.BindingData; +import org.apache.shenyu.e2e.model.data.RuleCacheData; +import org.apache.shenyu.e2e.model.data.SelectorCacheData; import org.apache.shenyu.e2e.model.response.SelectorDTO; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -37,12 +39,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; import static org.apache.shenyu.e2e.constant.Constants.SYS_DEFAULT_NAMESPACE_NAMESPACE_ID; +import static org.awaitility.Awaitility.await; @ShenYuTest(environments = { @ShenYuTest.Environment( @@ -75,12 +80,13 @@ void before(final AdminClient client, final GatewayClient gateway, final BeforeE spec.getChecker().check(gateway); ResourcesData resources = spec.getResources(); + List ruleIds = Lists.newArrayList(); for (ResourcesData.Resource res : resources.getResources()) { SelectorDTO dto = client.create(res.getSelector()); selectorIds.add(dto.getId()); res.getRules().forEach(rule -> { rule.setSelectorId(dto.getId()); - client.create(rule); + ruleIds.add(client.create(rule).getId()); }); BindingData bindingData = res.getBindingData(); if (Objects.nonNull(bindingData)) { @@ -90,6 +96,10 @@ void before(final AdminClient client, final GatewayClient gateway, final BeforeE } } + // Admin creation does not imply that the gateway has received the logging rules. + if (!selectorIds.isEmpty()) { + waitForLoggingRules(gateway, selectorIds, ruleIds); + } spec.getWaiting().waitFor(gateway); } @@ -100,6 +110,13 @@ void after(final AdminClient client, final GatewayClient gateway, final AfterEac selectorIds = Lists.newArrayList(); } + static void waitForLoggingRules(final GatewayClient gateway, final List selectors, final List rules) { + await().alias("RocketMQ selectors and rules synchronized to gateway") + .atMost(Duration.ofSeconds(30)) + .until(() -> gateway.getSelectorCache().stream().map(SelectorCacheData::getId).collect(Collectors.toSet()).containsAll(selectors) + && gateway.getRuleCache().stream().map(RuleCacheData::getId).collect(Collectors.toSet()).containsAll(rules)); + } + @BeforeAll void setup(final AdminClient adminClient, final GatewayClient gatewayClient) throws Exception { adminClient.login(); diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-logging-rocketmq/src/test/java/org/apache/shenyu/e2e/testcase/logging/rocketmq/LoggingRuleSyncTest.java b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-logging-rocketmq/src/test/java/org/apache/shenyu/e2e/testcase/logging/rocketmq/LoggingRuleSyncTest.java new file mode 100644 index 000000000000..b0403346f9d8 --- /dev/null +++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-logging-rocketmq/src/test/java/org/apache/shenyu/e2e/testcase/logging/rocketmq/LoggingRuleSyncTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.e2e.testcase.logging.rocketmq; + +import org.apache.shenyu.e2e.client.gateway.GatewayClient; +import org.apache.shenyu.e2e.model.data.RuleCacheData; +import org.apache.shenyu.e2e.model.data.SelectorCacheData; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LoggingRuleSyncTest { + + @Test + void waitsForNewIdsEvenWhenCacheSizesMatch() { + AtomicInteger selectorReads = new AtomicInteger(); + AtomicInteger ruleReads = new AtomicInteger(); + GatewayClient gateway = new GatewayClient("sync-test", "gateway", "http://localhost", new Properties()) { + @Override + public List getSelectorCache() { + SelectorCacheData selector = new SelectorCacheData(); + selector.setId(selectorReads.incrementAndGet() < 3 ? "old-selector" : "new-selector"); + return List.of(selector); + } + + @Override + public List getRuleCache() { + RuleCacheData rule = new RuleCacheData(); + rule.setId(ruleReads.incrementAndGet() < 3 ? "old-rule" : "new-rule"); + return List.of(rule); + } + }; + + DividePluginTest.waitForLoggingRules(gateway, List.of("new-selector"), List.of("new-rule")); + + assertTrue(selectorReads.get() >= 3); + assertTrue(ruleReads.get() >= 3); + } + + @Test + void acceptsAlreadySynchronizedRulesAlongsideUnrelatedData() { + GatewayClient gateway = new GatewayClient("sync-test", "gateway", "http://localhost", new Properties()) { + @Override + public List getSelectorCache() { + SelectorCacheData selector = new SelectorCacheData(); + selector.setId("new-selector"); + return List.of(selector, new SelectorCacheData()); + } + + @Override + public List getRuleCache() { + RuleCacheData rule = new RuleCacheData(); + rule.setId("new-rule"); + return List.of(rule, new RuleCacheData()); + } + }; + + DividePluginTest.waitForLoggingRules(gateway, List.of("new-selector"), List.of("new-rule")); + } +} diff --git a/shenyu-plugin/pom.xml b/shenyu-plugin/pom.xml index bdcc7061e39e..6732014f2c0f 100644 --- a/shenyu-plugin/pom.xml +++ b/shenyu-plugin/pom.xml @@ -50,6 +50,7 @@ shenyu-plugin-security shenyu-plugin-fault-tolerance shenyu-plugin-ai + shenyu-plugin-agent-gateway shenyu-plugin-mcp-server diff --git a/shenyu-plugin/shenyu-plugin-agent-gateway/README.md b/shenyu-plugin/shenyu-plugin-agent-gateway/README.md new file mode 100644 index 000000000000..51f9ca865dfb --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-agent-gateway/README.md @@ -0,0 +1,98 @@ +# Agent Gateway Plugin + + + +## Overview + +The Agent Gateway plugin adds a request-scoped context to selected LLM traffic +and then continues the existing plugin chain. It does not replace the AI Proxy +implementation, read the request body, or introduce MCP SDK dependencies. + +Agent Gateway 插件为匹配的 LLM 请求建立请求级上下文,然后继续执行现有插件链。它复用现有 AI Proxy,不读取请求体,也不引入 MCP SDK 依赖。 + +## Enablement + +Add the starter dependency: + +```xml + + org.apache.shenyu + shenyu-spring-boot-starter-plugin-agent-gateway + +``` + +Enable local bean assembly explicitly: + +```yaml +shenyu: + plugins: + agent: + gateway: + enabled: true +``` + +The Admin plugin is initialized disabled. Enable `agentGateway` in Admin only +after the starter is loaded. Disabling the Admin plugin stops new context +creation while leaving the existing AI Proxy and MCP plugins unchanged. + +## Rule handle + +The first version uses these rule handle fields: + +```json +{ + "trafficType": "LLM", + "responseRequestId": true +} +``` + +PR1 supports only `LLM` traffic. The existing MCP plugin remains independently +configurable; this Agent Gateway rule cannot yet be used for MCP traffic. +`trafficType` is required and must be exactly `LLM`. `responseRequestId` is an +optional boolean and defaults to `false`. Admin rejects unknown fields, +malformed JSON, missing required fields, invalid field types, and unsupported +traffic types before saving or importing an `agentGateway` rule handle. At +runtime, unknown fields in existing configurations are logged and ignored for +forward compatibility. Malformed JSON, missing required fields, invalid field +types, and unsupported traffic types return HTTP 500 because they are server-side +configuration errors. A selector with `continued=false` has no +rule handle, so this plugin passes it through without creating a context. + +When enabled, the response contains the gateway-generated +`X-Shenyu-Agent-Request-Id` header. A client-provided value with the same name +is never used as the internal request ID. + +## Context contract + +`AgentTrafficContext` is immutable and is created per subscription. It contains +only the generated request ID, traffic type, selector ID, and rule ID. During +downstream execution it is available from the dedicated Reactor context key +`AgentGatewayConstants.REACTOR_CONTEXT_KEY` and the exchange attribute +`AgentGatewayConstants.REQUEST_CONTEXT_ATTRIBUTE`. + +The context is not stored in a global map or thread-local. It does not contain +the request body, credentials, response content, or mutable plugin-chain state. +Each subscription gets its own context. The same exchange should not be +subscribed concurrently, because exchange attributes are shared by that request. + +## Scope + +This module provides request correlation and Reactor context propagation for +LLM traffic. MCP discovery, tool aggregation, callback bridging, usage/cost +accounting, and unified identity or permission governance remain separate +features and are not enabled by this plugin. diff --git a/shenyu-plugin/shenyu-plugin-agent-gateway/pom.xml b/shenyu-plugin/shenyu-plugin-agent-gateway/pom.xml new file mode 100644 index 000000000000..5fafcd299b53 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-agent-gateway/pom.xml @@ -0,0 +1,59 @@ + + + + + + org.apache.shenyu + shenyu-plugin + 2.7.2-SNAPSHOT + + 4.0.0 + shenyu-plugin-agent-gateway + + + + org.apache.shenyu + shenyu-common + ${project.version} + + + org.apache.shenyu + shenyu-plugin-api + ${project.version} + + + org.apache.shenyu + shenyu-plugin-base + ${project.version} + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + diff --git a/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/AgentGatewayConstants.java b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/AgentGatewayConstants.java new file mode 100644 index 000000000000..d564c3799979 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/AgentGatewayConstants.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.plugin.agent.gateway; + +/** + * Constants used by the agent gateway plugin. + */ +public final class AgentGatewayConstants { + + /** + * Exchange attribute containing the current request context. + */ + public static final String REQUEST_CONTEXT_ATTRIBUTE = AgentGatewayConstants.class.getName() + ".REQUEST_CONTEXT"; + + /** + * Reactor context key containing the current request context. + */ + public static final String REACTOR_CONTEXT_KEY = AgentGatewayConstants.class.getName() + ".REACTOR_CONTEXT"; + + /** + * Response header carrying the generated request id. + */ + public static final String REQUEST_ID_HEADER = "X-Shenyu-Agent-Request-Id"; + + /** + * Stable error code used for invalid plugin configuration. + */ + public static final String CONFIG_INVALID_CODE = "AGENT_GATEWAY_CONFIG_INVALID"; + + private AgentGatewayConstants() { + } +} diff --git a/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/AgentGatewayPlugin.java b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/AgentGatewayPlugin.java new file mode 100644 index 000000000000..9dd8b5858cae --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/AgentGatewayPlugin.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.plugin.agent.gateway; + +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.plugin.agent.gateway.handle.AgentGatewayRuleHandle; +import org.apache.shenyu.plugin.agent.gateway.handle.AgentGatewayRuleHandleParser; +import org.apache.shenyu.plugin.agent.gateway.handler.AgentGatewayPluginDataHandler; +import org.apache.shenyu.plugin.api.ShenyuPluginChain; +import org.apache.shenyu.plugin.api.result.ShenyuResultWrap; +import org.apache.shenyu.plugin.api.utils.WebFluxResultUtils; +import org.apache.shenyu.plugin.base.AbstractShenyuPlugin; +import org.apache.shenyu.plugin.base.utils.CacheKeyUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.Objects; +import java.util.UUID; + +/** + * Establishes an isolated request context for agent traffic and continues to + * the existing AI proxy chain. + */ +public class AgentGatewayPlugin extends AbstractShenyuPlugin { + + private final AgentGatewayRuleHandleParser parser = new AgentGatewayRuleHandleParser(); + + @Override + protected Mono doExecute(final ServerWebExchange exchange, final ShenyuPluginChain chain, + final SelectorData selector, final RuleData rule) { + if (Boolean.FALSE.equals(selector.getContinued())) { + return chain.execute(exchange); + } + final AgentGatewayRuleHandle handle = resolveHandle(rule); + if (!handle.isValid()) { + return reject(exchange, handle.getErrorMessage()); + } + return Mono.defer(() -> { + final AgentTrafficContext context = new AgentTrafficContext( + UUID.randomUUID().toString(), handle.getTrafficType(), selector.getId(), rule.getId()); + final Object previousContext = exchange.getAttributes() + .put(AgentGatewayConstants.REQUEST_CONTEXT_ATTRIBUTE, context); + if (handle.isResponseRequestId()) { + registerResponseRequestId(exchange, context.getRequestId()); + } + return chain.execute(exchange) + .contextWrite(reactorContext -> reactorContext.put(AgentGatewayConstants.REACTOR_CONTEXT_KEY, context)) + .doFinally(signal -> restorePreviousContext(exchange, previousContext, context)); + }); + } + + private void restorePreviousContext(final ServerWebExchange exchange, final Object previousContext, + final AgentTrafficContext currentContext) { + // DefaultServerWebExchange uses a ConcurrentHashMap for attributes. The conditional + // Map.remove/replace operations rely on a mutable map that supports these methods; + // they avoid clearing a context installed by another execution on this exchange. + if (Objects.isNull(previousContext)) { + exchange.getAttributes().remove(AgentGatewayConstants.REQUEST_CONTEXT_ATTRIBUTE, currentContext); + } else { + exchange.getAttributes().replace(AgentGatewayConstants.REQUEST_CONTEXT_ATTRIBUTE, currentContext, + previousContext); + } + } + + private AgentGatewayRuleHandle resolveHandle(final RuleData rule) { + final String key = CacheKeyUtils.INST.getKey(rule); + final AgentGatewayRuleHandle cached = AgentGatewayPluginDataHandler.CACHED_HANDLE.get().obtainHandle(key); + if (Objects.nonNull(cached) && Objects.equals(cached.getRawHandle(), rule.getHandle())) { + return cached; + } + final AgentGatewayRuleHandle parsed = parser.parse(rule.getHandle()); + AgentGatewayPluginDataHandler.CACHED_HANDLE.get().cachedHandle(key, parsed); + return parsed; + } + + private void registerResponseRequestId(final ServerWebExchange exchange, final String requestId) { + exchange.getResponse().beforeCommit(() -> { + final HttpHeaders headers = exchange.getResponse().getHeaders(); + headers.set(AgentGatewayConstants.REQUEST_ID_HEADER, requestId); + return Mono.empty(); + }); + } + + private Mono reject(final ServerWebExchange exchange, final String reason) { + exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); + final Object error = ShenyuResultWrap.error(exchange, HttpStatus.INTERNAL_SERVER_ERROR.value(), + AgentGatewayConstants.CONFIG_INVALID_CODE + ": " + reason, null); + return WebFluxResultUtils.result(exchange, error); + } + + @Override + public String named() { + return PluginEnum.AGENT_GATEWAY.getName(); + } + + @Override + public int getOrder() { + return PluginEnum.AGENT_GATEWAY.getCode(); + } +} diff --git a/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/AgentTrafficContext.java b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/AgentTrafficContext.java new file mode 100644 index 000000000000..71348e390706 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/AgentTrafficContext.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.plugin.agent.gateway; + +import java.util.Objects; + +/** + * Immutable request context for one agent traffic request. + * + *

The context is created per subscription and is never stored in a global + * map or a thread local. This keeps concurrent agent requests isolated.

+ */ +public final class AgentTrafficContext { + + private final String requestId; + + private final String trafficType; + + private final String selectorId; + + private final String ruleId; + + public AgentTrafficContext(final String requestId, final String trafficType, + final String selectorId, final String ruleId) { + this.requestId = Objects.requireNonNull(requestId, "requestId"); + this.trafficType = Objects.requireNonNull(trafficType, "trafficType"); + this.selectorId = selectorId; + this.ruleId = ruleId; + } + + public String getRequestId() { + return requestId; + } + + public String getTrafficType() { + return trafficType; + } + + public String getSelectorId() { + return selectorId; + } + + public String getRuleId() { + return ruleId; + } + + @Override + public String toString() { + return "AgentTrafficContext{requestId='" + requestId + "', trafficType='" + trafficType + + "', selectorId='" + selectorId + "', ruleId='" + ruleId + "'}"; + } +} diff --git a/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/handle/AgentGatewayRuleHandle.java b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/handle/AgentGatewayRuleHandle.java new file mode 100644 index 000000000000..f35aacfcf202 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/handle/AgentGatewayRuleHandle.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.plugin.agent.gateway.handle; + +/** + * Immutable, validated rule handle for the agent gateway plugin. + */ +public final class AgentGatewayRuleHandle { + + private final String rawHandle; + + private final String trafficType; + + private final boolean responseRequestId; + + private final boolean valid; + + private final String errorMessage; + + private AgentGatewayRuleHandle(final String rawHandle, final String trafficType, + final boolean responseRequestId, final boolean valid, + final String errorMessage) { + this.rawHandle = rawHandle; + this.trafficType = trafficType; + this.responseRequestId = responseRequestId; + this.valid = valid; + this.errorMessage = errorMessage; + } + + public static AgentGatewayRuleHandle valid(final String rawHandle, final String trafficType, + final boolean responseRequestId) { + return new AgentGatewayRuleHandle(rawHandle, trafficType, responseRequestId, true, null); + } + + public static AgentGatewayRuleHandle invalid(final String rawHandle, final String errorMessage) { + return new AgentGatewayRuleHandle(rawHandle, null, false, false, errorMessage); + } + + public String getRawHandle() { + return rawHandle; + } + + public String getTrafficType() { + return trafficType; + } + + public boolean isResponseRequestId() { + return responseRequestId; + } + + public boolean isValid() { + return valid; + } + + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/handle/AgentGatewayRuleHandleParser.java b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/handle/AgentGatewayRuleHandleParser.java new file mode 100644 index 000000000000..ffc94ef6fa3f --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/handle/AgentGatewayRuleHandleParser.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.plugin.agent.gateway.handle; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Set; + +/** + * Strict parser for agent gateway rule handles. + */ +public final class AgentGatewayRuleHandleParser { + + private static final Logger LOG = LoggerFactory.getLogger(AgentGatewayRuleHandleParser.class); + + private static final String TRAFFIC_TYPE = "trafficType"; + + private static final String RESPONSE_REQUEST_ID = "responseRequestId"; + + private static final Set SUPPORTED_FIELDS = Set.of(TRAFFIC_TYPE, RESPONSE_REQUEST_ID); + + /** + * Parse one rule handle. Unknown fields are ignored for forward compatibility. + * + * @param rawHandle raw rule handle + * @return parsed handle or an invalid handle with a stable reason + */ + public AgentGatewayRuleHandle parse(final String rawHandle) { + if (StringUtils.isBlank(rawHandle)) { + return AgentGatewayRuleHandle.invalid(rawHandle, "rule handle must not be empty"); + } + try { + final JsonElement element = JsonParser.parseString(rawHandle); + if (!element.isJsonObject()) { + return invalid(rawHandle, "rule handle must be a JSON object"); + } + final JsonObject object = element.getAsJsonObject(); + for (String field : object.keySet()) { + if (!SUPPORTED_FIELDS.contains(field)) { + LOG.warn("Ignoring unknown agent gateway rule handle field: {}", field); + } + } + if (!object.has(TRAFFIC_TYPE) || !object.get(TRAFFIC_TYPE).isJsonPrimitive() + || !object.getAsJsonPrimitive(TRAFFIC_TYPE).isString()) { + return invalid(rawHandle, "trafficType must be a string"); + } + final String trafficType = object.get(TRAFFIC_TYPE).getAsString(); + if (!"LLM".equals(trafficType)) { + return invalid(rawHandle, "trafficType must be LLM"); + } + boolean responseRequestId = false; + if (object.has(RESPONSE_REQUEST_ID)) { + final JsonElement responseElement = object.get(RESPONSE_REQUEST_ID); + if (!responseElement.isJsonPrimitive() + || !responseElement.getAsJsonPrimitive().isBoolean()) { + return invalid(rawHandle, "responseRequestId must be a boolean"); + } + responseRequestId = responseElement.getAsBoolean(); + } + return AgentGatewayRuleHandle.valid(rawHandle, "LLM", responseRequestId); + } catch (JsonParseException | IllegalStateException | UnsupportedOperationException ex) { + return invalid(rawHandle, "rule handle is not valid JSON"); + } + } + + private AgentGatewayRuleHandle invalid(final String rawHandle, final String message) { + return AgentGatewayRuleHandle.invalid(rawHandle, message); + } +} diff --git a/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/handler/AgentGatewayPluginDataHandler.java b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/handler/AgentGatewayPluginDataHandler.java new file mode 100644 index 000000000000..ecee5eed16b3 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/handler/AgentGatewayPluginDataHandler.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.plugin.agent.gateway.handler; + +import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.plugin.agent.gateway.handle.AgentGatewayRuleHandle; +import org.apache.shenyu.plugin.agent.gateway.handle.AgentGatewayRuleHandleParser; +import org.apache.shenyu.plugin.base.cache.CommonHandleCache; +import org.apache.shenyu.plugin.base.handler.PluginDataHandler; +import org.apache.shenyu.plugin.base.utils.BeanHolder; +import org.apache.shenyu.plugin.base.utils.CacheKeyUtils; + +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Caches parsed agent gateway rule handles. + */ +public class AgentGatewayPluginDataHandler implements PluginDataHandler { + + public static final Supplier> CACHED_HANDLE = + new BeanHolder<>(CommonHandleCache::new); + + private final AgentGatewayRuleHandleParser parser = new AgentGatewayRuleHandleParser(); + + @Override + public void handlerRule(final RuleData ruleData) { + if (Objects.nonNull(ruleData)) { + CACHED_HANDLE.get().cachedHandle(CacheKeyUtils.INST.getKey(ruleData), parser.parse(ruleData.getHandle())); + } + } + + @Override + public void removeRule(final RuleData ruleData) { + if (Objects.nonNull(ruleData)) { + CACHED_HANDLE.get().removeHandle(CacheKeyUtils.INST.getKey(ruleData)); + } + } + + @Override + public void removeSelector(final SelectorData selectorData) { + if (Objects.nonNull(selectorData) && Objects.nonNull(selectorData.getId())) { + final String selectorPrefix = selectorData.getId() + "_"; + CACHED_HANDLE.get().getAllCache().keySet().removeIf(key -> key.startsWith(selectorPrefix)); + } + } + + @Override + public void removePlugin(final PluginData pluginData) { + CACHED_HANDLE.get().getAllCache().clear(); + } + + @Override + public String pluginNamed() { + return PluginEnum.AGENT_GATEWAY.getName(); + } +} diff --git a/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/AgentGatewayPluginTest.java b/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/AgentGatewayPluginTest.java new file mode 100644 index 000000000000..bf1e841c09a1 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/AgentGatewayPluginTest.java @@ -0,0 +1,314 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.plugin.agent.gateway; + +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.plugin.api.ShenyuPluginChain; +import org.apache.shenyu.plugin.api.result.DefaultShenyuResult; +import org.apache.shenyu.plugin.api.result.ShenyuResult; +import org.apache.shenyu.plugin.api.utils.SpringBeanUtils; +import org.apache.shenyu.plugin.agent.gateway.handler.AgentGatewayPluginDataHandler; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +import java.time.Duration; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AgentGatewayPluginTest { + + private final AgentGatewayPlugin plugin = new AgentGatewayPlugin(); + + @BeforeEach + void initResult() { + ApplicationContext applicationContext = mock(ApplicationContext.class); + when(applicationContext.getBean(ShenyuResult.class)).thenReturn(new DefaultShenyuResult()); + SpringBeanUtils.getInstance().setApplicationContext(applicationContext); + } + + @AfterEach + void clearCache() { + AgentGatewayPluginDataHandler.CACHED_HANDLE.get().getAllCache().clear(); + } + + @Test + void shouldExposePluginIdentity() { + assertEquals(PluginEnum.AGENT_GATEWAY.getName(), plugin.named()); + assertEquals(PluginEnum.AGENT_GATEWAY.getCode(), plugin.getOrder()); + } + + @Test + void shouldPropagateRequestContextThroughReactorContext() { + ServerWebExchange exchange = newExchange(); + AtomicReference reactorContext = new AtomicReference<>(); + AtomicReference exchangeContext = new AtomicReference<>(); + ShenyuPluginChain chain = next -> Mono.deferContextual(context -> { + reactorContext.set(context.get(AgentGatewayConstants.REACTOR_CONTEXT_KEY)); + exchangeContext.set(next.getAttribute(AgentGatewayConstants.REQUEST_CONTEXT_ATTRIBUTE)); + return Mono.empty(); + }); + + StepVerifier.create(plugin.doExecute(exchange, chain, selector(), rule("rule-1"))) + .verifyComplete(); + + assertNotNull(exchangeContext.get()); + assertEquals(exchangeContext.get(), reactorContext.get()); + assertEquals("LLM", exchangeContext.get().getTrafficType()); + assertEquals("selector-1", exchangeContext.get().getSelectorId()); + assertEquals("rule-1", exchangeContext.get().getRuleId()); + assertTrue(Objects.isNull(exchange.getAttribute(AgentGatewayConstants.REQUEST_CONTEXT_ATTRIBUTE))); + } + + @Test + void shouldPreserveExistingContextAcrossAsyncBoundary() { + ServerWebExchange exchange = newExchange(); + AtomicReference existingValue = new AtomicReference<>(); + AtomicReference contextValue = new AtomicReference<>(); + Scheduler scheduler = Schedulers.newSingle("agent-gateway-context-test"); + ShenyuPluginChain chain = next -> Mono.delay(Duration.ofMillis(10), scheduler) + .then(Mono.deferContextual(context -> { + existingValue.set(context.get("existing-key")); + contextValue.set(context.get(AgentGatewayConstants.REACTOR_CONTEXT_KEY)); + return Mono.empty(); + })); + + try { + StepVerifier.create(plugin.doExecute(exchange, chain, selector(), rule("rule-1")) + .contextWrite(context -> context.put("existing-key", "existing-value"))) + .verifyComplete(); + } finally { + scheduler.dispose(); + } + + assertEquals("existing-value", existingValue.get()); + assertNotNull(contextValue.get()); + assertTrue(Objects.isNull(exchange.getAttribute(AgentGatewayConstants.REQUEST_CONTEXT_ATTRIBUTE))); + } + + @Test + void shouldGenerateIndependentContextForConcurrentRequests() { + Set requestIds = new HashSet<>(); + for (int i = 0; i < 12; i++) { + ServerWebExchange exchange = newExchange(); + AtomicReference requestId = new AtomicReference<>(); + ShenyuPluginChain chain = next -> { + captureRequestId(next, requestId); + return Mono.empty(); + }; + StepVerifier.create(plugin.doExecute(exchange, chain, selector(), rule("rule-" + i))) + .verifyComplete(); + assertNotNull(requestId.get()); + assertTrue(requestIds.add(requestId.get())); + } + assertEquals(12, requestIds.size()); + } + + @Test + void shouldNotTrustClientRequestId() { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/agent") + .header(AgentGatewayConstants.REQUEST_ID_HEADER, "client-request-id") + .build()); + + AtomicReference requestId = new AtomicReference<>(); + ShenyuPluginChain chain = next -> { + captureRequestId(next, requestId); + return Mono.empty(); + }; + StepVerifier.create(plugin.doExecute(exchange, chain, selector(), rule("rule-1"))) + .verifyComplete(); + + assertNotNull(requestId.get()); + assertNotEquals("client-request-id", requestId.get()); + } + + @Test + void shouldAddResponseRequestIdOnlyWhenConfigured() { + ServerWebExchange exchange = newExchange(); + AtomicReference requestId = new AtomicReference<>(); + ShenyuPluginChain chain = next -> { + captureRequestId(next, requestId); + return exchange.getResponse().setComplete(); + }; + StepVerifier.create(plugin.doExecute(exchange, chain, selector(), + rule("{\"trafficType\":\"LLM\",\"responseRequestId\":true}"))) + .verifyComplete(); + + assertEquals(requestId.get(), exchange.getResponse().getHeaders() + .getFirst(AgentGatewayConstants.REQUEST_ID_HEADER)); + } + + @Test + void shouldNotAddResponseRequestIdWhenDisabled() { + ServerWebExchange exchange = newExchange(); + ShenyuPluginChain chain = next -> exchange.getResponse().setComplete(); + + StepVerifier.create(plugin.doExecute(exchange, chain, selector(), rule("rule-1"))) + .verifyComplete(); + + assertTrue(Objects.isNull(exchange.getResponse().getHeaders().getFirst(AgentGatewayConstants.REQUEST_ID_HEADER))); + } + + @Test + void shouldUseCurrentRuleWhenCachedSnapshotIsStale() { + ServerWebExchange exchange = newExchange(); + RuleData oldRule = RuleData.builder().id("rule-1").selectorId("selector-1") + .handle("{\"trafficType\":\"LLM\"}").build(); + AgentGatewayPluginDataHandler.CACHED_HANDLE.get().cachedHandle("selector-1_rule-1", + new org.apache.shenyu.plugin.agent.gateway.handle.AgentGatewayRuleHandleParser().parse( + oldRule.getHandle())); + RuleData currentRule = RuleData.builder().id("rule-1").selectorId("selector-1") + .handle("{\"trafficType\":\"LLM\",\"responseRequestId\":true}").build(); + + ShenyuPluginChain chain = next -> exchange.getResponse().setComplete(); + StepVerifier.create(plugin.doExecute(exchange, chain, selector(), currentRule)) + .verifyComplete(); + + assertNotNull(exchange.getResponse().getHeaders().getFirst(AgentGatewayConstants.REQUEST_ID_HEADER)); + } + + @Test + void shouldRejectInvalidHandleBeforeCallingDownstream() { + ServerWebExchange exchange = newExchange(); + AtomicReference calls = new AtomicReference<>(0); + RuleData invalidRule = RuleData.builder() + .id("rule-1") + .selectorId("selector-1") + .handle("{\"trafficType\":\"MCP\"}") + .build(); + + StepVerifier.create(plugin.doExecute(exchange, next -> { + calls.set(calls.get() + 1); + return Mono.empty(); + }, selector(), invalidRule)).verifyComplete(); + + assertEquals(0, calls.get()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, exchange.getResponse().getStatusCode()); + assertNotNull(AgentGatewayPluginDataHandler.CACHED_HANDLE.get().obtainHandle("selector-1_rule-1")); + assertTrue(!AgentGatewayPluginDataHandler.CACHED_HANDLE.get() + .obtainHandle("selector-1_rule-1").isValid()); + } + + @Test + void shouldIgnoreUnknownRuleHandleFieldOnRequestPath() { + ServerWebExchange exchange = newExchange(); + AtomicBoolean called = new AtomicBoolean(); + + StepVerifier.create(plugin.doExecute(exchange, next -> { + called.set(true); + return Mono.empty(); + }, selector(), rule("{\"trafficType\":\"LLM\",\"futureOption\":true}"))) + .verifyComplete(); + + assertTrue(called.get()); + } + + @Test + void shouldCreateFreshContextForEachSubscription() { + ServerWebExchange exchange = newExchange(); + AtomicReference calls = new AtomicReference<>(0); + Set requestIds = new HashSet<>(); + Mono execution = plugin.doExecute(exchange, next -> { + calls.set(calls.get() + 1); + requestIds.add(next.getAttribute( + AgentGatewayConstants.REQUEST_CONTEXT_ATTRIBUTE).getRequestId()); + return Mono.empty(); + }, selector(), rule("rule-1")); + + StepVerifier.create(execution).verifyComplete(); + StepVerifier.create(execution).verifyComplete(); + + assertEquals(2, calls.get()); + assertEquals(2, requestIds.size()); + assertTrue(Objects.isNull(exchange.getAttribute(AgentGatewayConstants.REQUEST_CONTEXT_ATTRIBUTE))); + } + + @Test + void shouldPropagateCancellationAndCleanOnlyCurrentExchange() { + ServerWebExchange exchange = newExchange(); + AtomicBoolean cancelled = new AtomicBoolean(); + ShenyuPluginChain chain = next -> Mono.never().doOnCancel(() -> cancelled.set(true)); + + StepVerifier.create(plugin.doExecute(exchange, chain, selector(), rule("rule-1"))) + .thenCancel() + .verify(); + + assertTrue(cancelled.get()); + assertTrue(Objects.isNull(exchange.getAttribute(AgentGatewayConstants.REQUEST_CONTEXT_ATTRIBUTE))); + } + + @Test + void shouldPassThroughContinuedFalseSelector() { + ServerWebExchange exchange = newExchange(); + SelectorData selector = SelectorData.builder().id("selector-1").continued(false).build(); + AtomicBoolean called = new AtomicBoolean(); + + StepVerifier.create(plugin.doExecute(exchange, next -> { + called.set(true); + assertTrue(Objects.isNull(next.getAttribute(AgentGatewayConstants.REQUEST_CONTEXT_ATTRIBUTE))); + return Mono.empty(); + }, selector, rule("rule-1"))) + .verifyComplete(); + + assertTrue(called.get()); + assertTrue(Objects.isNull(exchange.getResponse().getStatusCode())); + } + + private ServerWebExchange newExchange() { + return MockServerWebExchange.from(MockServerHttpRequest.get("/agent").build()); + } + + private void captureRequestId(final ServerWebExchange exchange, final AtomicReference requestId) { + requestId.set(exchange.getAttribute( + AgentGatewayConstants.REQUEST_CONTEXT_ATTRIBUTE).getRequestId()); + } + + private SelectorData selector() { + return SelectorData.builder().id("selector-1").continued(true).build(); + } + + private RuleData rule(final String ruleHandleOrId) { + final boolean isHandle = ruleHandleOrId.startsWith("{"); + return RuleData.builder() + .id(isHandle ? "rule-1" : ruleHandleOrId) + .selectorId("selector-1") + .handle(isHandle ? ruleHandleOrId : "{\"trafficType\":\"LLM\"}") + .build(); + } +} diff --git a/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/handle/AgentGatewayRuleHandleParserTest.java b/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/handle/AgentGatewayRuleHandleParserTest.java new file mode 100644 index 000000000000..94d89f5ec46c --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/handle/AgentGatewayRuleHandleParserTest.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.plugin.agent.gateway.handle; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AgentGatewayRuleHandleParserTest { + + private final AgentGatewayRuleHandleParser parser = new AgentGatewayRuleHandleParser(); + + @Test + void shouldParseValidLlmHandle() { + AgentGatewayRuleHandle handle = parser.parse("{\"trafficType\":\"LLM\",\"responseRequestId\":true}"); + + assertTrue(handle.isValid()); + assertEquals("LLM", handle.getTrafficType()); + assertTrue(handle.isResponseRequestId()); + } + + @Test + void shouldUseFalseAsDefaultForResponseRequestId() { + AgentGatewayRuleHandle handle = parser.parse("{\"trafficType\":\"LLM\"}"); + + assertTrue(handle.isValid()); + assertFalse(handle.isResponseRequestId()); + } + + @Test + void shouldRejectNonCanonicalTrafficType() { + AgentGatewayRuleHandle handle = parser.parse("{\"trafficType\":\"llm\"}"); + + assertFalse(handle.isValid()); + assertEquals("trafficType must be LLM", handle.getErrorMessage()); + } + + @Test + void shouldRejectEmptyHandle() { + AgentGatewayRuleHandle handle = parser.parse(" "); + + assertFalse(handle.isValid()); + assertEquals("rule handle must not be empty", handle.getErrorMessage()); + } + + @Test + void shouldIgnoreUnknownField() { + AgentGatewayRuleHandle handle = parser.parse("{\"trafficType\":\"LLM\",\"target\":\"x\"}"); + + assertTrue(handle.isValid()); + assertEquals("LLM", handle.getTrafficType()); + } + + @Test + void shouldRejectNonLlmTraffic() { + AgentGatewayRuleHandle handle = parser.parse("{\"trafficType\":\"MCP\"}"); + + assertFalse(handle.isValid()); + assertEquals("trafficType must be LLM", handle.getErrorMessage()); + } + + @Test + void shouldRejectWrongBooleanType() { + AgentGatewayRuleHandle handle = parser.parse("{\"trafficType\":\"LLM\",\"responseRequestId\":\"true\"}"); + + assertFalse(handle.isValid()); + assertEquals("responseRequestId must be a boolean", handle.getErrorMessage()); + } +} diff --git a/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/handler/AgentGatewayPluginDataHandlerTest.java b/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/handler/AgentGatewayPluginDataHandlerTest.java new file mode 100644 index 000000000000..ed0daddced70 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/handler/AgentGatewayPluginDataHandlerTest.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.plugin.agent.gateway.handler; + +import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.plugin.agent.gateway.handle.AgentGatewayRuleHandle; +import org.apache.shenyu.plugin.base.utils.CacheKeyUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AgentGatewayPluginDataHandlerTest { + + private final AgentGatewayPluginDataHandler handler = new AgentGatewayPluginDataHandler(); + + @AfterEach + void clearCache() { + AgentGatewayPluginDataHandler.CACHED_HANDLE.get().getAllCache().clear(); + } + + @Test + void shouldExposePluginName() { + assertEquals(PluginEnum.AGENT_GATEWAY.getName(), handler.pluginNamed()); + } + + @Test + void shouldCacheValidAndInvalidRuleSnapshots() { + RuleData validRule = rule("selector-1", "rule-1", "{\"trafficType\":\"LLM\"}"); + RuleData invalidRule = rule("selector-1", "rule-2", "{\"trafficType\":\"MCP\"}"); + + handler.handlerRule(validRule); + handler.handlerRule(invalidRule); + + AgentGatewayRuleHandle valid = AgentGatewayPluginDataHandler.CACHED_HANDLE.get() + .obtainHandle(CacheKeyUtils.INST.getKey(validRule)); + AgentGatewayRuleHandle invalid = AgentGatewayPluginDataHandler.CACHED_HANDLE.get() + .obtainHandle(CacheKeyUtils.INST.getKey(invalidRule)); + assertTrue(valid.isValid()); + assertFalse(invalid.isValid()); + } + + @Test + void shouldRemoveAllRulesBelongingToSelector() { + RuleData first = rule("selector-1", "rule-1", "{\"trafficType\":\"LLM\"}"); + RuleData second = rule("selector-1", "rule-2", "{\"trafficType\":\"LLM\"}"); + RuleData other = rule("selector-2", "rule-1", "{\"trafficType\":\"LLM\"}"); + handler.handlerRule(first); + handler.handlerRule(second); + handler.handlerRule(other); + + handler.removeSelector(SelectorData.builder().id("selector-1").build()); + + assertEquals(1, AgentGatewayPluginDataHandler.CACHED_HANDLE.get().getAllCache().size()); + assertTrue(Objects.nonNull(AgentGatewayPluginDataHandler.CACHED_HANDLE.get() + .obtainHandle(CacheKeyUtils.INST.getKey(other)))); + } + + @Test + void shouldRemoveAllRulesWhenPluginIsRemoved() { + handler.handlerRule(rule("selector-1", "rule-1", "{\"trafficType\":\"LLM\"}")); + + handler.removePlugin(new PluginData()); + + assertTrue(AgentGatewayPluginDataHandler.CACHED_HANDLE.get().getAllCache().isEmpty()); + } + + @Test + void shouldRemoveOneRuleWithoutTouchingOtherSelectors() { + RuleData first = rule("selector-1", "rule-1", "{\"trafficType\":\"LLM\"}"); + RuleData other = rule("selector-1", "rule-2", "{\"trafficType\":\"LLM\"}"); + handler.handlerRule(first); + handler.handlerRule(other); + + handler.removeRule(first); + + assertTrue(Objects.isNull(AgentGatewayPluginDataHandler.CACHED_HANDLE.get() + .obtainHandle(CacheKeyUtils.INST.getKey(first)))); + assertTrue(Objects.nonNull(AgentGatewayPluginDataHandler.CACHED_HANDLE.get() + .obtainHandle(CacheKeyUtils.INST.getKey(other)))); + } + + private RuleData rule(final String selectorId, final String ruleId, final String handleValue) { + return RuleData.builder().selectorId(selectorId).id(ruleId).handle(handleValue).build(); + } +} diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/pom.xml b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/pom.xml index d096319ffb47..edf659b8adfb 100644 --- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/pom.xml +++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/pom.xml @@ -77,6 +77,7 @@ shenyu-spring-boot-starter-plugin-ai-token-limiter shenyu-spring-boot-starter-plugin-ai-request-transformer shenyu-spring-boot-starter-plugin-ai-response-transformer + shenyu-spring-boot-starter-plugin-agent-gateway shenyu-spring-boot-starter-plugin-mcp-server shenyu-spring-boot-starter-plugin-ai-sensitive-word diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-agent-gateway/pom.xml b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-agent-gateway/pom.xml new file mode 100644 index 000000000000..b625fecde991 --- /dev/null +++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-agent-gateway/pom.xml @@ -0,0 +1,40 @@ + + + + + + org.apache.shenyu + shenyu-spring-boot-starter-plugin + 2.7.2-SNAPSHOT + + 4.0.0 + shenyu-spring-boot-starter-plugin-agent-gateway + + + + org.apache.shenyu + shenyu-plugin-agent-gateway + ${project.version} + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-agent-gateway/src/main/java/org/apache/shenyu/springboot/starter/plugin/agent/gateway/AgentGatewayPluginConfiguration.java b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-agent-gateway/src/main/java/org/apache/shenyu/springboot/starter/plugin/agent/gateway/AgentGatewayPluginConfiguration.java new file mode 100644 index 000000000000..be5804bf3355 --- /dev/null +++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-agent-gateway/src/main/java/org/apache/shenyu/springboot/starter/plugin/agent/gateway/AgentGatewayPluginConfiguration.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.plugin.agent.gateway; + +import org.apache.shenyu.plugin.agent.gateway.AgentGatewayPlugin; +import org.apache.shenyu.plugin.agent.gateway.handler.AgentGatewayPluginDataHandler; +import org.apache.shenyu.plugin.api.ShenyuPlugin; +import org.apache.shenyu.plugin.base.handler.PluginDataHandler; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Agent gateway plugin auto configuration. + */ +@Configuration +@ConditionalOnProperty(value = "shenyu.plugins.agent.gateway.enabled", havingValue = "true", matchIfMissing = false) +public class AgentGatewayPluginConfiguration { + + /** + * Create the agent gateway plugin. + * + * @return the plugin + */ + @Bean + public ShenyuPlugin agentGatewayPlugin() { + return new AgentGatewayPlugin(); + } + + /** + * Create the agent gateway plugin data handler. + * + * @return the data handler + */ + @Bean + public PluginDataHandler agentGatewayPluginDataHandler() { + return new AgentGatewayPluginDataHandler(); + } +} diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-agent-gateway/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-agent-gateway/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000000..ae5136973e44 --- /dev/null +++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-agent-gateway/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.apache.shenyu.springboot.starter.plugin.agent.gateway.AgentGatewayPluginConfiguration diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-agent-gateway/src/test/java/org/apache/shenyu/springboot/starter/plugin/agent/gateway/AgentGatewayPluginConfigurationTest.java b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-agent-gateway/src/test/java/org/apache/shenyu/springboot/starter/plugin/agent/gateway/AgentGatewayPluginConfigurationTest.java new file mode 100644 index 000000000000..ef7e9b452013 --- /dev/null +++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-agent-gateway/src/test/java/org/apache/shenyu/springboot/starter/plugin/agent/gateway/AgentGatewayPluginConfigurationTest.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.plugin.agent.gateway; + +import org.apache.shenyu.plugin.agent.gateway.AgentGatewayPlugin; +import org.apache.shenyu.plugin.api.ShenyuPlugin; +import org.apache.shenyu.plugin.base.handler.PluginDataHandler; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AgentGatewayPluginConfigurationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(AgentGatewayPluginConfiguration.class); + + @Test + void shouldKeepPluginUnassembledByDefault() { + contextRunner.run(context -> { + assertFalse(context.containsBean("agentGatewayPlugin")); + assertFalse(context.containsBean("agentGatewayPluginDataHandler")); + }); + } + + @Test + void shouldAssemblePluginWhenEnabled() { + contextRunner.withPropertyValues("shenyu.plugins.agent.gateway.enabled=true") + .run(context -> { + assertTrue(context.getBean("agentGatewayPlugin") instanceof AgentGatewayPlugin); + assertTrue(context.getBean(ShenyuPlugin.class) instanceof AgentGatewayPlugin); + assertTrue(context.getBean("agentGatewayPluginDataHandler") instanceof PluginDataHandler); + }); + } +}