diff --git a/.sqlx/query-e4392329cd965b3f5100b38fb26b828f1bce4e6c9951ed6f4701d52cab3b7de5.json b/.sqlx/query-0b8e9bed137684683d8b6a9ed52c94b6fb3bbcb19c7a621c78c5a6d3698bdf4d.json similarity index 67% rename from .sqlx/query-e4392329cd965b3f5100b38fb26b828f1bce4e6c9951ed6f4701d52cab3b7de5.json rename to .sqlx/query-0b8e9bed137684683d8b6a9ed52c94b6fb3bbcb19c7a621c78c5a6d3698bdf4d.json index b103597..2199915 100644 --- a/.sqlx/query-e4392329cd965b3f5100b38fb26b828f1bce4e6c9951ed6f4701d52cab3b7de5.json +++ b/.sqlx/query-0b8e9bed137684683d8b6a9ed52c94b6fb3bbcb19c7a621c78c5a6d3698bdf4d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "select\n operations.id,\n operations.workspace_id,\n operations.name,\n operations.display_name,\n operations.category,\n operations.protocol,\n ov.target_json,\n operations.status,\n operations.current_draft_version,\n operations.latest_published_version,\n operations.created_at as \"created_at!: time::OffsetDateTime\",\n operations.updated_at as \"updated_at!: time::OffsetDateTime\",\n operations.published_at as \"published_at: time::OffsetDateTime\"\n from operations\n join operation_versions ov\n on ov.operation_id = operations.id\n and ov.version = operations.current_draft_version\n where operations.workspace_id = $1\n order by operations.name asc", + "query": "select\n operations.id,\n operations.workspace_id,\n operations.name,\n operations.display_name,\n operations.category,\n operations.protocol,\n operations.security_level,\n ov.target_json,\n operations.status,\n operations.current_draft_version,\n operations.latest_published_version,\n operations.created_at as \"created_at!: time::OffsetDateTime\",\n operations.updated_at as \"updated_at!: time::OffsetDateTime\",\n operations.published_at as \"published_at: time::OffsetDateTime\"\n from operations\n join operation_versions ov\n on ov.operation_id = operations.id\n and ov.version = operations.current_draft_version\n where operations.workspace_id = $1\n order by operations.name asc", "describe": { "columns": [ { @@ -35,36 +35,41 @@ }, { "ordinal": 6, + "name": "security_level", + "type_info": "Text" + }, + { + "ordinal": 7, "name": "target_json", "type_info": "Jsonb" }, { - "ordinal": 7, + "ordinal": 8, "name": "status", "type_info": "Text" }, { - "ordinal": 8, + "ordinal": 9, "name": "current_draft_version", "type_info": "Int4" }, { - "ordinal": 9, + "ordinal": 10, "name": "latest_published_version", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "created_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 11, + "ordinal": 12, "name": "updated_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 12, + "ordinal": 13, "name": "published_at: time::OffsetDateTime", "type_info": "Timestamptz" } @@ -84,11 +89,12 @@ false, false, false, + false, true, false, false, true ] }, - "hash": "e4392329cd965b3f5100b38fb26b828f1bce4e6c9951ed6f4701d52cab3b7de5" + "hash": "0b8e9bed137684683d8b6a9ed52c94b6fb3bbcb19c7a621c78c5a6d3698bdf4d" } diff --git a/.sqlx/query-eeec56487c44b28624364e17778451924b910c56d7be7da9a361f74de9d9d546.json b/.sqlx/query-10ac4260e678ac458750988f95e54d3a84d9870e10e86bbe0d9c9fd99bf8a1f5.json similarity index 71% rename from .sqlx/query-eeec56487c44b28624364e17778451924b910c56d7be7da9a361f74de9d9d546.json rename to .sqlx/query-10ac4260e678ac458750988f95e54d3a84d9870e10e86bbe0d9c9fd99bf8a1f5.json index 1da764d..fe99c21 100644 --- a/.sqlx/query-eeec56487c44b28624364e17778451924b910c56d7be7da9a361f74de9d9d546.json +++ b/.sqlx/query-10ac4260e678ac458750988f95e54d3a84d9870e10e86bbe0d9c9fd99bf8a1f5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "select\n o.id,\n o.workspace_id,\n o.name,\n o.display_name,\n o.category,\n o.protocol,\n o.created_at as \"operation_created_at!: time::OffsetDateTime\",\n o.updated_at as \"operation_updated_at!: time::OffsetDateTime\",\n o.published_at as \"operation_published_at: time::OffsetDateTime\",\n ov.version,\n ov.status,\n ov.target_json,\n ov.input_schema_json,\n ov.output_schema_json,\n ov.input_mapping_json,\n ov.output_mapping_json,\n ov.execution_config_json,\n ov.tool_description_json,\n ov.samples_json,\n ov.generated_draft_json,\n ov.config_export_json,\n ov.change_note,\n ov.created_at as \"created_at!: time::OffsetDateTime\",\n ov.created_by\n from operation_versions ov\n join operations o on o.id = ov.operation_id\n where o.workspace_id = $1 and ov.operation_id = $2\n order by ov.version asc", + "query": "select\n o.id,\n o.workspace_id,\n o.name,\n o.display_name,\n o.category,\n o.protocol,\n o.security_level,\n o.created_at as \"operation_created_at!: time::OffsetDateTime\",\n o.updated_at as \"operation_updated_at!: time::OffsetDateTime\",\n o.published_at as \"operation_published_at: time::OffsetDateTime\",\n ov.version,\n ov.status,\n ov.target_json,\n ov.input_schema_json,\n ov.output_schema_json,\n ov.input_mapping_json,\n ov.output_mapping_json,\n ov.execution_config_json,\n ov.tool_description_json,\n ov.samples_json,\n ov.generated_draft_json,\n ov.config_export_json,\n ov.change_note,\n ov.created_at as \"created_at!: time::OffsetDateTime\",\n ov.created_by\n from operation_versions ov\n join operations o on o.id = ov.operation_id\n where o.workspace_id = $1 and ov.operation_id = $2\n order by ov.version asc", "describe": { "columns": [ { @@ -35,91 +35,96 @@ }, { "ordinal": 6, + "name": "security_level", + "type_info": "Text" + }, + { + "ordinal": 7, "name": "operation_created_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 7, + "ordinal": 8, "name": "operation_updated_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 9, "name": "operation_published_at: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 9, + "ordinal": 10, "name": "version", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "status", "type_info": "Text" }, { - "ordinal": 11, + "ordinal": 12, "name": "target_json", "type_info": "Jsonb" }, { - "ordinal": 12, + "ordinal": 13, "name": "input_schema_json", "type_info": "Jsonb" }, { - "ordinal": 13, + "ordinal": 14, "name": "output_schema_json", "type_info": "Jsonb" }, { - "ordinal": 14, + "ordinal": 15, "name": "input_mapping_json", "type_info": "Jsonb" }, { - "ordinal": 15, + "ordinal": 16, "name": "output_mapping_json", "type_info": "Jsonb" }, { - "ordinal": 16, + "ordinal": 17, "name": "execution_config_json", "type_info": "Jsonb" }, { - "ordinal": 17, + "ordinal": 18, "name": "tool_description_json", "type_info": "Jsonb" }, { - "ordinal": 18, + "ordinal": 19, "name": "samples_json", "type_info": "Jsonb" }, { - "ordinal": 19, + "ordinal": 20, "name": "generated_draft_json", "type_info": "Jsonb" }, { - "ordinal": 20, + "ordinal": 21, "name": "config_export_json", "type_info": "Jsonb" }, { - "ordinal": 21, + "ordinal": 22, "name": "change_note", "type_info": "Text" }, { - "ordinal": 22, + "ordinal": 23, "name": "created_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 23, + "ordinal": 24, "name": "created_by", "type_info": "Text" } @@ -139,6 +144,7 @@ false, false, false, + false, true, false, false, @@ -157,5 +163,5 @@ true ] }, - "hash": "eeec56487c44b28624364e17778451924b910c56d7be7da9a361f74de9d9d546" + "hash": "10ac4260e678ac458750988f95e54d3a84d9870e10e86bbe0d9c9fd99bf8a1f5" } diff --git a/.sqlx/query-213432827436e3a17fb2c17e392c1f8d7aa7b7624c64185fe0d19a3b2bd7d713.json b/.sqlx/query-213432827436e3a17fb2c17e392c1f8d7aa7b7624c64185fe0d19a3b2bd7d713.json deleted file mode 100644 index 03fe841..0000000 --- a/.sqlx/query-213432827436e3a17fb2c17e392c1f8d7aa7b7624c64185fe0d19a3b2bd7d713.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "select\n k.id,\n k.workspace_id,\n k.name,\n k.prefix,\n k.scopes_json,\n k.status,\n k.created_at as \"created_at!: time::OffsetDateTime\",\n k.last_used_at as \"last_used_at: time::OffsetDateTime\"\n from platform_api_keys k\n join workspaces w on w.id = k.workspace_id\n where w.slug = $1\n and k.secret_hash = $2\n and k.status = 'active'\n limit 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "workspace_id", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "name", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "prefix", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "scopes_json", - "type_info": "Jsonb" - }, - { - "ordinal": 5, - "name": "status", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "created_at!: time::OffsetDateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "last_used_at: time::OffsetDateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true - ] - }, - "hash": "213432827436e3a17fb2c17e392c1f8d7aa7b7624c64185fe0d19a3b2bd7d713" -} diff --git a/.sqlx/query-8ac15e73f704021c8cd23b3d32ebb215caa6eed07b30f4b3f98b97698e7334ee.json b/.sqlx/query-67a73cb5211a0c23d2e8d8361a8e37c488eb2420207e9c4fa682993eda20cd62.json similarity index 70% rename from .sqlx/query-8ac15e73f704021c8cd23b3d32ebb215caa6eed07b30f4b3f98b97698e7334ee.json rename to .sqlx/query-67a73cb5211a0c23d2e8d8361a8e37c488eb2420207e9c4fa682993eda20cd62.json index 4f4a18a..eb676d4 100644 --- a/.sqlx/query-8ac15e73f704021c8cd23b3d32ebb215caa6eed07b30f4b3f98b97698e7334ee.json +++ b/.sqlx/query-67a73cb5211a0c23d2e8d8361a8e37c488eb2420207e9c4fa682993eda20cd62.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "select\n o.id,\n o.workspace_id,\n o.name,\n o.display_name,\n o.category,\n o.protocol,\n o.created_at as \"operation_created_at!: time::OffsetDateTime\",\n o.updated_at as \"operation_updated_at!: time::OffsetDateTime\",\n o.published_at as \"operation_published_at: time::OffsetDateTime\",\n ov.version,\n ov.status,\n ov.target_json,\n ov.input_schema_json,\n ov.output_schema_json,\n ov.input_mapping_json,\n ov.output_mapping_json,\n ov.execution_config_json,\n ov.tool_description_json,\n ov.samples_json,\n ov.generated_draft_json,\n ov.config_export_json,\n ov.change_note,\n ov.created_at as \"created_at!: time::OffsetDateTime\",\n ov.created_by\n from published_operations po\n join operation_versions ov\n on ov.operation_id = po.operation_id and ov.version = po.version\n join operations o on o.id = po.operation_id\n order by o.name asc", + "query": "select\n o.id,\n o.workspace_id,\n o.name,\n o.display_name,\n o.category,\n o.protocol,\n o.security_level,\n o.created_at as \"operation_created_at!: time::OffsetDateTime\",\n o.updated_at as \"operation_updated_at!: time::OffsetDateTime\",\n o.published_at as \"operation_published_at: time::OffsetDateTime\",\n ov.version,\n ov.status,\n ov.target_json,\n ov.input_schema_json,\n ov.output_schema_json,\n ov.input_mapping_json,\n ov.output_mapping_json,\n ov.execution_config_json,\n ov.tool_description_json,\n ov.samples_json,\n ov.generated_draft_json,\n ov.config_export_json,\n ov.change_note,\n ov.created_at as \"created_at!: time::OffsetDateTime\",\n ov.created_by\n from published_operations po\n join operation_versions ov\n on ov.operation_id = po.operation_id and ov.version = po.version\n join operations o on o.id = po.operation_id\n order by o.name asc", "describe": { "columns": [ { @@ -35,91 +35,96 @@ }, { "ordinal": 6, + "name": "security_level", + "type_info": "Text" + }, + { + "ordinal": 7, "name": "operation_created_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 7, + "ordinal": 8, "name": "operation_updated_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 9, "name": "operation_published_at: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 9, + "ordinal": 10, "name": "version", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "status", "type_info": "Text" }, { - "ordinal": 11, + "ordinal": 12, "name": "target_json", "type_info": "Jsonb" }, { - "ordinal": 12, + "ordinal": 13, "name": "input_schema_json", "type_info": "Jsonb" }, { - "ordinal": 13, + "ordinal": 14, "name": "output_schema_json", "type_info": "Jsonb" }, { - "ordinal": 14, + "ordinal": 15, "name": "input_mapping_json", "type_info": "Jsonb" }, { - "ordinal": 15, + "ordinal": 16, "name": "output_mapping_json", "type_info": "Jsonb" }, { - "ordinal": 16, + "ordinal": 17, "name": "execution_config_json", "type_info": "Jsonb" }, { - "ordinal": 17, + "ordinal": 18, "name": "tool_description_json", "type_info": "Jsonb" }, { - "ordinal": 18, + "ordinal": 19, "name": "samples_json", "type_info": "Jsonb" }, { - "ordinal": 19, + "ordinal": 20, "name": "generated_draft_json", "type_info": "Jsonb" }, { - "ordinal": 20, + "ordinal": 21, "name": "config_export_json", "type_info": "Jsonb" }, { - "ordinal": 21, + "ordinal": 22, "name": "change_note", "type_info": "Text" }, { - "ordinal": 22, + "ordinal": 23, "name": "created_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 23, + "ordinal": 24, "name": "created_by", "type_info": "Text" } @@ -136,6 +141,7 @@ false, false, false, + false, true, false, false, @@ -154,5 +160,5 @@ true ] }, - "hash": "8ac15e73f704021c8cd23b3d32ebb215caa6eed07b30f4b3f98b97698e7334ee" + "hash": "67a73cb5211a0c23d2e8d8361a8e37c488eb2420207e9c4fa682993eda20cd62" } diff --git a/.sqlx/query-4402c3661cf519344a81cfcc4e811e789b9a3d827825bc59f3a9cd0036665420.json b/.sqlx/query-7ad59ce20834c3de2b0345c1be28d84377b417efb30c7bdc38d4e3746a0c1a7a.json similarity index 70% rename from .sqlx/query-4402c3661cf519344a81cfcc4e811e789b9a3d827825bc59f3a9cd0036665420.json rename to .sqlx/query-7ad59ce20834c3de2b0345c1be28d84377b417efb30c7bdc38d4e3746a0c1a7a.json index b9d1c5a..fc187bd 100644 --- a/.sqlx/query-4402c3661cf519344a81cfcc4e811e789b9a3d827825bc59f3a9cd0036665420.json +++ b/.sqlx/query-7ad59ce20834c3de2b0345c1be28d84377b417efb30c7bdc38d4e3746a0c1a7a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "select\n o.id,\n o.workspace_id,\n o.name,\n o.display_name,\n o.category,\n o.protocol,\n o.created_at as \"operation_created_at!: time::OffsetDateTime\",\n o.updated_at as \"operation_updated_at!: time::OffsetDateTime\",\n o.published_at as \"operation_published_at: time::OffsetDateTime\",\n ov.version,\n ov.status,\n ov.target_json,\n ov.input_schema_json,\n ov.output_schema_json,\n ov.input_mapping_json,\n ov.output_mapping_json,\n ov.execution_config_json,\n ov.tool_description_json,\n ov.samples_json,\n ov.generated_draft_json,\n ov.config_export_json,\n ov.change_note,\n ov.created_at as \"created_at!: time::OffsetDateTime\",\n ov.created_by\n from published_operations po\n join operation_versions ov\n on ov.operation_id = po.operation_id and ov.version = po.version\n join operations o on o.id = po.operation_id\n where po.operation_id = $1", + "query": "select\n o.id,\n o.workspace_id,\n o.name,\n o.display_name,\n o.category,\n o.protocol,\n o.security_level,\n o.created_at as \"operation_created_at!: time::OffsetDateTime\",\n o.updated_at as \"operation_updated_at!: time::OffsetDateTime\",\n o.published_at as \"operation_published_at: time::OffsetDateTime\",\n ov.version,\n ov.status,\n ov.target_json,\n ov.input_schema_json,\n ov.output_schema_json,\n ov.input_mapping_json,\n ov.output_mapping_json,\n ov.execution_config_json,\n ov.tool_description_json,\n ov.samples_json,\n ov.generated_draft_json,\n ov.config_export_json,\n ov.change_note,\n ov.created_at as \"created_at!: time::OffsetDateTime\",\n ov.created_by\n from published_operations po\n join operation_versions ov\n on ov.operation_id = po.operation_id and ov.version = po.version\n join operations o on o.id = po.operation_id\n where po.operation_id = $1", "describe": { "columns": [ { @@ -35,91 +35,96 @@ }, { "ordinal": 6, + "name": "security_level", + "type_info": "Text" + }, + { + "ordinal": 7, "name": "operation_created_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 7, + "ordinal": 8, "name": "operation_updated_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 9, "name": "operation_published_at: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 9, + "ordinal": 10, "name": "version", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "status", "type_info": "Text" }, { - "ordinal": 11, + "ordinal": 12, "name": "target_json", "type_info": "Jsonb" }, { - "ordinal": 12, + "ordinal": 13, "name": "input_schema_json", "type_info": "Jsonb" }, { - "ordinal": 13, + "ordinal": 14, "name": "output_schema_json", "type_info": "Jsonb" }, { - "ordinal": 14, + "ordinal": 15, "name": "input_mapping_json", "type_info": "Jsonb" }, { - "ordinal": 15, + "ordinal": 16, "name": "output_mapping_json", "type_info": "Jsonb" }, { - "ordinal": 16, + "ordinal": 17, "name": "execution_config_json", "type_info": "Jsonb" }, { - "ordinal": 17, + "ordinal": 18, "name": "tool_description_json", "type_info": "Jsonb" }, { - "ordinal": 18, + "ordinal": 19, "name": "samples_json", "type_info": "Jsonb" }, { - "ordinal": 19, + "ordinal": 20, "name": "generated_draft_json", "type_info": "Jsonb" }, { - "ordinal": 20, + "ordinal": 21, "name": "config_export_json", "type_info": "Jsonb" }, { - "ordinal": 21, + "ordinal": 22, "name": "change_note", "type_info": "Text" }, { - "ordinal": 22, + "ordinal": 23, "name": "created_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 23, + "ordinal": 24, "name": "created_by", "type_info": "Text" } @@ -138,6 +143,7 @@ false, false, false, + false, true, false, false, @@ -156,5 +162,5 @@ true ] }, - "hash": "4402c3661cf519344a81cfcc4e811e789b9a3d827825bc59f3a9cd0036665420" + "hash": "7ad59ce20834c3de2b0345c1be28d84377b417efb30c7bdc38d4e3746a0c1a7a" } diff --git a/.sqlx/query-40a5906682a4f90bb250c9b1809866e287f427fda9e28e0bfbff2c42231cf8b1.json b/.sqlx/query-a24c36f988151016f9ffa834a6a5658f7f094844b892c999e6f58d6f92fbe0c2.json similarity index 72% rename from .sqlx/query-40a5906682a4f90bb250c9b1809866e287f427fda9e28e0bfbff2c42231cf8b1.json rename to .sqlx/query-a24c36f988151016f9ffa834a6a5658f7f094844b892c999e6f58d6f92fbe0c2.json index ca4a57a..638d532 100644 --- a/.sqlx/query-40a5906682a4f90bb250c9b1809866e287f427fda9e28e0bfbff2c42231cf8b1.json +++ b/.sqlx/query-a24c36f988151016f9ffa834a6a5658f7f094844b892c999e6f58d6f92fbe0c2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "select\n o.id,\n o.workspace_id,\n o.name,\n o.display_name,\n o.category,\n o.protocol,\n o.created_at as \"operation_created_at!: time::OffsetDateTime\",\n o.updated_at as \"operation_updated_at!: time::OffsetDateTime\",\n o.published_at as \"operation_published_at: time::OffsetDateTime\",\n ov.version,\n ov.status,\n ov.target_json,\n ov.input_schema_json,\n ov.output_schema_json,\n ov.input_mapping_json,\n ov.output_mapping_json,\n ov.execution_config_json,\n ov.tool_description_json,\n ov.samples_json,\n ov.generated_draft_json,\n ov.config_export_json,\n ov.change_note,\n ov.created_at as \"created_at!: time::OffsetDateTime\",\n ov.created_by\n from operation_versions ov\n join operations o on o.id = ov.operation_id\n where o.workspace_id = $1 and ov.operation_id = $2 and ov.version = $3", + "query": "select\n o.id,\n o.workspace_id,\n o.name,\n o.display_name,\n o.category,\n o.protocol,\n o.security_level,\n o.created_at as \"operation_created_at!: time::OffsetDateTime\",\n o.updated_at as \"operation_updated_at!: time::OffsetDateTime\",\n o.published_at as \"operation_published_at: time::OffsetDateTime\",\n ov.version,\n ov.status,\n ov.target_json,\n ov.input_schema_json,\n ov.output_schema_json,\n ov.input_mapping_json,\n ov.output_mapping_json,\n ov.execution_config_json,\n ov.tool_description_json,\n ov.samples_json,\n ov.generated_draft_json,\n ov.config_export_json,\n ov.change_note,\n ov.created_at as \"created_at!: time::OffsetDateTime\",\n ov.created_by\n from operation_versions ov\n join operations o on o.id = ov.operation_id\n where o.workspace_id = $1 and ov.operation_id = $2 and ov.version = $3", "describe": { "columns": [ { @@ -35,91 +35,96 @@ }, { "ordinal": 6, + "name": "security_level", + "type_info": "Text" + }, + { + "ordinal": 7, "name": "operation_created_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 7, + "ordinal": 8, "name": "operation_updated_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 9, "name": "operation_published_at: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 9, + "ordinal": 10, "name": "version", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "status", "type_info": "Text" }, { - "ordinal": 11, + "ordinal": 12, "name": "target_json", "type_info": "Jsonb" }, { - "ordinal": 12, + "ordinal": 13, "name": "input_schema_json", "type_info": "Jsonb" }, { - "ordinal": 13, + "ordinal": 14, "name": "output_schema_json", "type_info": "Jsonb" }, { - "ordinal": 14, + "ordinal": 15, "name": "input_mapping_json", "type_info": "Jsonb" }, { - "ordinal": 15, + "ordinal": 16, "name": "output_mapping_json", "type_info": "Jsonb" }, { - "ordinal": 16, + "ordinal": 17, "name": "execution_config_json", "type_info": "Jsonb" }, { - "ordinal": 17, + "ordinal": 18, "name": "tool_description_json", "type_info": "Jsonb" }, { - "ordinal": 18, + "ordinal": 19, "name": "samples_json", "type_info": "Jsonb" }, { - "ordinal": 19, + "ordinal": 20, "name": "generated_draft_json", "type_info": "Jsonb" }, { - "ordinal": 20, + "ordinal": 21, "name": "config_export_json", "type_info": "Jsonb" }, { - "ordinal": 21, + "ordinal": 22, "name": "change_note", "type_info": "Text" }, { - "ordinal": 22, + "ordinal": 23, "name": "created_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 23, + "ordinal": 24, "name": "created_by", "type_info": "Text" } @@ -140,6 +145,7 @@ false, false, false, + false, true, false, false, @@ -158,5 +164,5 @@ true ] }, - "hash": "40a5906682a4f90bb250c9b1809866e287f427fda9e28e0bfbff2c42231cf8b1" + "hash": "a24c36f988151016f9ffa834a6a5658f7f094844b892c999e6f58d6f92fbe0c2" } diff --git a/.sqlx/query-aa5a728d3d53aa0730497e9c75ccf1d385f00c6401ce0b17416b0009cf3f6ff3.json b/.sqlx/query-dd4a415a042b863b1034727737f3d14019ba405a07a8360b68e8c9c5597e17ce.json similarity index 72% rename from .sqlx/query-aa5a728d3d53aa0730497e9c75ccf1d385f00c6401ce0b17416b0009cf3f6ff3.json rename to .sqlx/query-dd4a415a042b863b1034727737f3d14019ba405a07a8360b68e8c9c5597e17ce.json index 5f8b3cb..0a463cd 100644 --- a/.sqlx/query-aa5a728d3d53aa0730497e9c75ccf1d385f00c6401ce0b17416b0009cf3f6ff3.json +++ b/.sqlx/query-dd4a415a042b863b1034727737f3d14019ba405a07a8360b68e8c9c5597e17ce.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "select\n w.id as workspace_id,\n w.slug as workspace_slug,\n a.id as agent_id,\n a.slug as agent_slug,\n b.tool_name,\n b.tool_title,\n coalesce(b.tool_description_override, ov.tool_description_json->>'description') as \"tool_description!\",\n o.id,\n o.name,\n o.display_name,\n o.category,\n o.protocol,\n o.created_at as \"operation_created_at!: time::OffsetDateTime\",\n o.updated_at as \"operation_updated_at!: time::OffsetDateTime\",\n o.published_at as \"operation_published_at: time::OffsetDateTime\",\n ov.version,\n ov.status,\n ov.target_json,\n ov.input_schema_json,\n ov.output_schema_json,\n ov.input_mapping_json,\n ov.output_mapping_json,\n ov.execution_config_json,\n ov.tool_description_json,\n ov.samples_json,\n ov.generated_draft_json,\n ov.config_export_json,\n ov.change_note,\n ov.created_at as \"created_at!: time::OffsetDateTime\",\n ov.created_by\n from workspaces w\n join agents a on a.workspace_id = w.id\n join published_agents pa on pa.agent_id = a.id\n join agent_operation_bindings b on b.agent_id = a.id and b.agent_version = pa.version\n join operation_versions ov on ov.operation_id = b.operation_id and ov.version = b.operation_version\n join operations o on o.id = ov.operation_id and o.workspace_id = w.id\n where w.slug = $1 and a.slug = $2 and b.enabled = true\n order by b.tool_name asc", + "query": "select\n w.id as workspace_id,\n w.slug as workspace_slug,\n a.id as agent_id,\n a.slug as agent_slug,\n b.tool_name,\n b.tool_title,\n coalesce(b.tool_description_override, ov.tool_description_json->>'description') as \"tool_description!\",\n o.id,\n o.name,\n o.display_name,\n o.category,\n o.protocol,\n o.security_level,\n o.created_at as \"operation_created_at!: time::OffsetDateTime\",\n o.updated_at as \"operation_updated_at!: time::OffsetDateTime\",\n o.published_at as \"operation_published_at: time::OffsetDateTime\",\n ov.version,\n ov.status,\n ov.target_json,\n ov.input_schema_json,\n ov.output_schema_json,\n ov.input_mapping_json,\n ov.output_mapping_json,\n ov.execution_config_json,\n ov.tool_description_json,\n ov.samples_json,\n ov.generated_draft_json,\n ov.config_export_json,\n ov.change_note,\n ov.created_at as \"created_at!: time::OffsetDateTime\",\n ov.created_by\n from workspaces w\n join agents a on a.workspace_id = w.id\n join published_agents pa on pa.agent_id = a.id\n join agent_operation_bindings b on b.agent_id = a.id and b.agent_version = pa.version\n join operation_versions ov on ov.operation_id = b.operation_id and ov.version = b.operation_version\n join operations o on o.id = ov.operation_id and o.workspace_id = w.id\n where w.slug = $1 and a.slug = $2 and b.enabled = true\n order by b.tool_name asc", "describe": { "columns": [ { @@ -65,91 +65,96 @@ }, { "ordinal": 12, + "name": "security_level", + "type_info": "Text" + }, + { + "ordinal": 13, "name": "operation_created_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 13, + "ordinal": 14, "name": "operation_updated_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 14, + "ordinal": 15, "name": "operation_published_at: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 15, + "ordinal": 16, "name": "version", "type_info": "Int4" }, { - "ordinal": 16, + "ordinal": 17, "name": "status", "type_info": "Text" }, { - "ordinal": 17, + "ordinal": 18, "name": "target_json", "type_info": "Jsonb" }, { - "ordinal": 18, + "ordinal": 19, "name": "input_schema_json", "type_info": "Jsonb" }, { - "ordinal": 19, + "ordinal": 20, "name": "output_schema_json", "type_info": "Jsonb" }, { - "ordinal": 20, + "ordinal": 21, "name": "input_mapping_json", "type_info": "Jsonb" }, { - "ordinal": 21, + "ordinal": 22, "name": "output_mapping_json", "type_info": "Jsonb" }, { - "ordinal": 22, + "ordinal": 23, "name": "execution_config_json", "type_info": "Jsonb" }, { - "ordinal": 23, + "ordinal": 24, "name": "tool_description_json", "type_info": "Jsonb" }, { - "ordinal": 24, + "ordinal": 25, "name": "samples_json", "type_info": "Jsonb" }, { - "ordinal": 25, + "ordinal": 26, "name": "generated_draft_json", "type_info": "Jsonb" }, { - "ordinal": 26, + "ordinal": 27, "name": "config_export_json", "type_info": "Jsonb" }, { - "ordinal": 27, + "ordinal": 28, "name": "change_note", "type_info": "Text" }, { - "ordinal": 28, + "ordinal": 29, "name": "created_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 29, + "ordinal": 30, "name": "created_by", "type_info": "Text" } @@ -175,6 +180,7 @@ false, false, false, + false, true, false, false, @@ -193,5 +199,5 @@ true ] }, - "hash": "aa5a728d3d53aa0730497e9c75ccf1d385f00c6401ce0b17416b0009cf3f6ff3" + "hash": "dd4a415a042b863b1034727737f3d14019ba405a07a8360b68e8c9c5597e17ce" } diff --git a/.sqlx/query-eeab4c7f8abeb26554b340cd5ed935573e1d3f0111fe9f61195bcfab87684959.json b/.sqlx/query-eeab4c7f8abeb26554b340cd5ed935573e1d3f0111fe9f61195bcfab87684959.json deleted file mode 100644 index a4bda08..0000000 --- a/.sqlx/query-eeab4c7f8abeb26554b340cd5ed935573e1d3f0111fe9f61195bcfab87684959.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "select\n id,\n workspace_id,\n name,\n prefix,\n scopes_json,\n status,\n created_at as \"created_at!: time::OffsetDateTime\",\n last_used_at as \"last_used_at: time::OffsetDateTime\"\n from platform_api_keys\n where workspace_id = $1\n order by created_at desc", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "workspace_id", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "name", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "prefix", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "scopes_json", - "type_info": "Jsonb" - }, - { - "ordinal": 5, - "name": "status", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "created_at!: time::OffsetDateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "last_used_at: time::OffsetDateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true - ] - }, - "hash": "eeab4c7f8abeb26554b340cd5ed935573e1d3f0111fe9f61195bcfab87684959" -} diff --git a/.sqlx/query-1f20e68fd913f8585dc63f5044cb2aa2572e441a6655a08a9188bbacad893755.json b/.sqlx/query-f195cc4951ef129f4a1c95d6d9e8f932919d2882f3dcb5a7f865fb4448506d05.json similarity index 68% rename from .sqlx/query-1f20e68fd913f8585dc63f5044cb2aa2572e441a6655a08a9188bbacad893755.json rename to .sqlx/query-f195cc4951ef129f4a1c95d6d9e8f932919d2882f3dcb5a7f865fb4448506d05.json index 4f87fec..3be6a67 100644 --- a/.sqlx/query-1f20e68fd913f8585dc63f5044cb2aa2572e441a6655a08a9188bbacad893755.json +++ b/.sqlx/query-f195cc4951ef129f4a1c95d6d9e8f932919d2882f3dcb5a7f865fb4448506d05.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "select\n operations.id,\n operations.workspace_id,\n operations.name,\n operations.display_name,\n operations.category,\n operations.protocol,\n ov.target_json,\n operations.status,\n operations.current_draft_version,\n operations.latest_published_version,\n operations.created_at as \"created_at!: time::OffsetDateTime\",\n operations.updated_at as \"updated_at!: time::OffsetDateTime\",\n operations.published_at as \"published_at: time::OffsetDateTime\"\n from operations\n join operation_versions ov\n on ov.operation_id = operations.id\n and ov.version = operations.current_draft_version\n where operations.workspace_id = $1 and operations.id = $2", + "query": "select\n operations.id,\n operations.workspace_id,\n operations.name,\n operations.display_name,\n operations.category,\n operations.protocol,\n operations.security_level,\n ov.target_json,\n operations.status,\n operations.current_draft_version,\n operations.latest_published_version,\n operations.created_at as \"created_at!: time::OffsetDateTime\",\n operations.updated_at as \"updated_at!: time::OffsetDateTime\",\n operations.published_at as \"published_at: time::OffsetDateTime\"\n from operations\n join operation_versions ov\n on ov.operation_id = operations.id\n and ov.version = operations.current_draft_version\n where operations.workspace_id = $1 and operations.id = $2", "describe": { "columns": [ { @@ -35,36 +35,41 @@ }, { "ordinal": 6, + "name": "security_level", + "type_info": "Text" + }, + { + "ordinal": 7, "name": "target_json", "type_info": "Jsonb" }, { - "ordinal": 7, + "ordinal": 8, "name": "status", "type_info": "Text" }, { - "ordinal": 8, + "ordinal": 9, "name": "current_draft_version", "type_info": "Int4" }, { - "ordinal": 9, + "ordinal": 10, "name": "latest_published_version", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "created_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 11, + "ordinal": 12, "name": "updated_at!: time::OffsetDateTime", "type_info": "Timestamptz" }, { - "ordinal": 12, + "ordinal": 13, "name": "published_at: time::OffsetDateTime", "type_info": "Timestamptz" } @@ -85,11 +90,12 @@ false, false, false, + false, true, false, false, true ] }, - "hash": "1f20e68fd913f8585dc63f5044cb2aa2572e441a6655a08a9188bbacad893755" + "hash": "f195cc4951ef129f4a1c95d6d9e8f932919d2882f3dcb5a7f865fb4448506d05" } diff --git a/TASKS.md b/TASKS.md index a5bd74b..cb75b49 100644 --- a/TASKS.md +++ b/TASKS.md @@ -2,49 +2,49 @@ ## Current -### `feat/edition-capability-model` +### `feat/community-surface-trim` Status: in_progress Goal: -- ввести capability model по редакциям и перестать смешивать Community и premium surface. +- привести открытую редакцию к честному product surface и убрать расхождение между текущим кодом и edition policy. Main code areas: -- `crates/crank-core/src/*` -- `apps/admin-api/src/service.rs` -- `apps/admin-api/src/app.rs` -- `apps/ui/js/*` -- `apps/ui/html/*` +- `apps/ui/js/wizard.js` +- `apps/ui/js/wizard-model.js` +- `apps/ui/js/agents.js` +- `apps/ui/js/settings.js` +- `apps/ui/js/i18n.js` - `docs/product-editions.md` -- `docs/frontend-roadmap.md` +- `docs/architecture.md` +- `docs/mcp-interface.md` Implementation slices: -1. Ввести серверную модель capabilities: - - protocols - - auth modes - - security levels - - workspace/user/agent limits -2. Отдавать capability flags в `admin-api`. -3. Привести UI к capability gating вместо скрытых продуктовых допущений. -4. Зафиксировать Community defaults в docs и runtime config. +1. Скрыть или честно заблокировать: + - `WebSocket` + - `SOAP` + - `gRPC streaming` + - streaming execution modes + - `elevated/strict` security levels + в Community. +2. Привести copy и feature affordances к open-core границе. +3. Добавить edition-aware explanation в wizard и agents UI. Progress: -- done: shared capability payload exists in `crank-core` and `admin-api`; -- done: authenticated `/api/admin/capabilities` exposes Community defaults to UI; -- done: first UI consumer uses capability payload in `Settings`; -- done: `protocol-capabilities` is edition-aware and Community wizard hides premium protocol cards; -- in_progress: surface Community-only security guidance inside wizard flow; -- pending: add server-side validation so unsupported protocol and security-level drafts are rejected, not only hidden in UI. +- done: Community wizard protocol list hides premium protocol cards; +- done: Community security note is shown inside wizard flow; +- done: server-side Community enforcement rejects unsupported protocols and premium security levels; +- pending: убрать или честно заблокировать оставшиеся Community-incompatible streaming affordances; +- pending: привести Agents/Settings copy к окончательной open-core границе. DoD: -- UI знает, какие функции входят в текущую редакцию; -- Community не показывает рабочие controls для premium-only features; -- capability checks существуют не только в UI, но и на сервере. +- Community UX не создает ложного ожидания наличия premium functionality; +- protocol set и security levels совпадают в docs, UI и runtime contracts. Verification: -- API tests for capability payloads; -- frontend smoke for hidden/locked states; -- manual verification on Community build. +- e2e checks for wizard protocol list; +- manual Community smoke; +- localized copy review. ## Next @@ -83,43 +83,6 @@ Verification: - unit tests for credential-kind enforcement; - docs sync check. -### `feat/community-surface-trim` - -Status: ready - -Goal: -- привести открытую редакцию к честному product surface и убрать расхождение между текущим кодом и edition policy. - -Main code areas: -- `apps/ui/js/wizard.js` -- `apps/ui/js/wizard-model.js` -- `apps/ui/js/agents.js` -- `apps/ui/js/settings.js` -- `apps/ui/js/i18n.js` -- `docs/product-editions.md` -- `docs/architecture.md` -- `docs/mcp-interface.md` - -Implementation slices: -1. Скрыть или честно заблокировать: - - `WebSocket` - - `SOAP` - - `gRPC streaming` - - streaming execution modes - - `elevated/strict` security levels - в Community. -2. Привести copy и feature affordances к open-core границе. -3. Добавить edition-aware explanation в wizard и agents UI. - -DoD: -- Community UX не создает ложного ожидания наличия premium functionality; -- protocol set и security levels совпадают в docs, UI и runtime contracts. - -Verification: -- e2e checks for wizard protocol list; -- manual Community smoke; -- localized copy review. - ## Planned ### `feat/frontend-commercial-polish` diff --git a/apps/admin-api/src/app.rs b/apps/admin-api/src/app.rs index b7d986f..7958f08 100644 --- a/apps/admin-api/src/app.rs +++ b/apps/admin-api/src/app.rs @@ -233,9 +233,9 @@ mod tests { use crank_core::{ AsyncJobHandle, AsyncJobId, DescriptorId, ExecutionConfig, ExecutionMode, GraphqlOperationType, GraphqlTarget, GrpcTarget, HttpMethod, JobStatus, MembershipRole, - OperationId, Protocol, RestTarget, SecretKind, SoapBindingStyle, SoapOperationMetadata, - SoapTarget, SoapVersion, StreamSession, StreamStatus, Target, ToolDescription, - TransportBehavior, WorkspaceId, + OperationId, OperationSecurityLevel, Protocol, RestTarget, SecretKind, SoapBindingStyle, + SoapOperationMetadata, SoapTarget, SoapVersion, StreamSession, StreamStatus, Target, + ToolDescription, TransportBehavior, WebsocketTarget, WorkspaceId, }; use crank_mapping::{MappingRule, MappingSet}; use crank_registry::{CreateAsyncJobRequest, CreateStreamSessionRequest, PostgresRegistry}; @@ -1067,6 +1067,173 @@ mod tests { ); } + #[tokio::test(flavor = "multi_thread")] + #[serial] + async fn rejects_unsupported_protocol_for_community_operation_create() { + let registry = test_registry().await; + let storage_root = test_storage_root("community_protocol_reject"); + let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await; + let client = authorized_client(&base_url).await; + + let response = client + .post(format!("{base_url}/operations")) + .json(&test_websocket_operation_payload( + "wss://example.com/stream", + "telemetry_ws", + )) + .send() + .await + .unwrap(); + let status = response.status(); + let body = response.json::().await.unwrap(); + + assert_eq!(status, reqwest::StatusCode::BAD_REQUEST); + assert_eq!(body["error"]["code"], "validation_error"); + assert_eq!( + body["error"]["message"], + "protocol websocket is not supported in Community" + ); + assert_eq!( + body["error"]["context"], + json!({ + "protocol": "websocket", + "edition": "community", + }) + ); + } + + #[tokio::test(flavor = "multi_thread")] + #[serial] + async fn rejects_premium_security_level_for_community_operation_create() { + let registry = test_registry().await; + let storage_root = test_storage_root("community_security_reject_create"); + let upstream_base_url = spawn_upstream_server().await; + let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await; + let client = authorized_client(&base_url).await; + + let mut payload = test_operation_payload(&upstream_base_url, "crm_elevated"); + payload.security_level = OperationSecurityLevel::Elevated; + + let response = client + .post(format!("{base_url}/operations")) + .json(&payload) + .send() + .await + .unwrap(); + let status = response.status(); + let body = response.json::().await.unwrap(); + + assert_eq!(status, reqwest::StatusCode::BAD_REQUEST); + assert_eq!(body["error"]["code"], "validation_error"); + assert_eq!( + body["error"]["message"], + "security level elevated is not supported in Community" + ); + assert_eq!( + body["error"]["context"], + json!({ + "security_level": "elevated", + "edition": "community", + }) + ); + } + + #[tokio::test(flavor = "multi_thread")] + #[serial] + async fn rejects_premium_security_level_for_community_operation_update() { + let registry = test_registry().await; + let storage_root = test_storage_root("community_security_reject_update"); + let upstream_base_url = spawn_upstream_server().await; + let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await; + let client = authorized_client(&base_url).await; + + let created = client + .post(format!("{base_url}/operations")) + .json(&test_operation_payload( + &upstream_base_url, + "crm_update_elevated", + )) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + let operation_id = created["operation_id"].as_str().unwrap().to_owned(); + + let response = client + .patch(format!("{base_url}/operations/{operation_id}")) + .json(&json!({ + "display_name": "Create Lead", + "category": "sales", + "security_level": "elevated", + "target": { + "kind": "rest", + "base_url": upstream_base_url, + "method": "POST", + "path_template": "/crm/leads", + "static_headers": {} + }, + "input_schema": object_schema("email"), + "output_schema": object_schema("id"), + "input_mapping": { + "rules": [{ + "source": "$.mcp.email", + "target": "$.request.body.email", + "required": true, + "default_value": null, + "transform": null, + "condition": null, + "notes": null + }] + }, + "output_mapping": { + "rules": [{ + "source": "$.response.body.id", + "target": "$.output.id", + "required": true, + "default_value": null, + "transform": null, + "condition": null, + "notes": null + }] + }, + "execution_config": { + "timeout_ms": 1000, + "retry_policy": null, + "auth_profile_ref": null, + "headers": {}, + "protocol_options": null, + "streaming": null + }, + "tool_description": { + "title": "Create Lead", + "description": "Creates a CRM lead", + "tags": ["crm"], + "examples": [] + } + })) + .send() + .await + .unwrap(); + let status = response.status(); + let body = response.json::().await.unwrap(); + + assert_eq!(status, reqwest::StatusCode::BAD_REQUEST); + assert_eq!(body["error"]["code"], "validation_error"); + assert_eq!( + body["error"]["message"], + "security level elevated is not supported in Community" + ); + assert_eq!( + body["error"]["context"], + json!({ + "security_level": "elevated", + "edition": "community", + }) + ); + } + #[tokio::test(flavor = "multi_thread")] #[serial] async fn manages_workspace_access_lifecycle() { @@ -2375,6 +2542,63 @@ mod tests { assert_eq!(imported["import_mode"], "upsert"); } + #[tokio::test(flavor = "multi_thread")] + #[serial] + async fn rejects_premium_security_level_for_community_yaml_upsert() { + let registry = test_registry().await; + let storage_root = test_storage_root("community_yaml_security_reject"); + let upstream_base_url = spawn_upstream_server().await; + let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await; + let client = authorized_client(&base_url).await; + + let created = client + .post(format!("{base_url}/operations")) + .json(&test_operation_payload( + &upstream_base_url, + "crm_yaml_standard", + )) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + let operation_id = created["operation_id"].as_str().unwrap().to_owned(); + + let mut yaml = client + .get(format!("{base_url}/operations/{operation_id}/export")) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + yaml = yaml.replace("security_level: standard", "security_level: elevated"); + + let response = client + .post(format!("{base_url}/operations/import?mode=upsert")) + .body(yaml) + .send() + .await + .unwrap(); + let status = response.status(); + let body = response.json::().await.unwrap(); + + assert_eq!(status, reqwest::StatusCode::BAD_REQUEST); + assert_eq!(body["error"]["code"], "validation_error"); + assert_eq!( + body["error"]["message"], + "security level elevated is not supported in Community" + ); + assert_eq!( + body["error"]["context"], + json!({ + "security_level": "elevated", + "edition": "community", + }) + ); + } + #[tokio::test(flavor = "multi_thread")] #[serial] async fn manages_workspace_secrets_without_exposing_plaintext() { @@ -3069,6 +3293,7 @@ mod tests { display_name: "Create Lead".to_owned(), category: "sales".to_owned(), protocol: Protocol::Rest, + security_level: OperationSecurityLevel::Standard, target: Target::Rest(RestTarget { base_url: base_url.to_owned(), method: HttpMethod::Post, @@ -3122,6 +3347,7 @@ mod tests { display_name: "Create Lead GraphQL".to_owned(), category: "sales".to_owned(), protocol: Protocol::Graphql, + security_level: OperationSecurityLevel::Standard, target: Target::Graphql(GraphqlTarget { endpoint: endpoint.to_owned(), operation_type: GraphqlOperationType::Mutation, @@ -3178,6 +3404,7 @@ mod tests { display_name: "Unary Echo gRPC".to_owned(), category: "support".to_owned(), protocol: Protocol::Grpc, + security_level: OperationSecurityLevel::Standard, target: Target::Grpc(GrpcTarget { server_addr: server_addr.to_owned(), package: "echo".to_owned(), @@ -3325,6 +3552,7 @@ mod tests { display_name: "Create Lead SOAP".to_owned(), category: "sales".to_owned(), protocol: Protocol::Soap, + security_level: OperationSecurityLevel::Standard, target: Target::Soap(SoapTarget { wsdl_ref: "sample_wsdl".into(), service_name: "LeadService".to_owned(), @@ -3383,6 +3611,61 @@ mod tests { } } + fn test_websocket_operation_payload(url: &str, name: &str) -> OperationPayload { + OperationPayload { + name: name.to_owned(), + display_name: "Telemetry WebSocket".to_owned(), + category: "telemetry".to_owned(), + protocol: Protocol::Websocket, + security_level: OperationSecurityLevel::Standard, + target: Target::Websocket(WebsocketTarget { + url: url.to_owned(), + subprotocols: Vec::new(), + subscribe_message_template: Some(json!({ "subscribe": true })), + unsubscribe_message_template: None, + static_headers: BTreeMap::new(), + }), + input_schema: object_schema("channel"), + output_schema: object_schema("event"), + input_mapping: MappingSet { + rules: vec![MappingRule { + source: "$.mcp.channel".to_owned(), + target: "$.request.body.channel".to_owned(), + required: true, + default_value: None, + transform: None, + condition: None, + notes: None, + }], + }, + output_mapping: MappingSet { + rules: vec![MappingRule { + source: "$.response.body.event".to_owned(), + target: "$.output.event".to_owned(), + required: true, + default_value: None, + transform: None, + condition: None, + notes: None, + }], + }, + execution_config: ExecutionConfig { + timeout_ms: 1_000, + retry_policy: None, + auth_profile_ref: None, + headers: BTreeMap::new(), + protocol_options: None, + streaming: None, + }, + tool_description: ToolDescription { + title: "Telemetry WebSocket".to_owned(), + description: "Streams telemetry events".to_owned(), + tags: vec!["telemetry".to_owned(), "websocket".to_owned()], + examples: Vec::new(), + }, + } + } + const SOAP_TEST_WSDL: &str = r#" , @@ -1966,6 +1972,7 @@ impl AdminService { display_name: summary.display_name, category: summary.category, protocol: summary.protocol, + security_level: summary.security_level, status: summary.status, current_draft_version: summary.current_draft_version, latest_published_version: summary.latest_published_version, @@ -2036,6 +2043,7 @@ impl AdminService { display_name: payload.display_name, category: payload.category, protocol: payload.protocol, + security_level: payload.security_level, status: OperationStatus::Draft, version: 1, target: payload.target, @@ -2097,6 +2105,7 @@ impl AdminService { display_name: payload.operation.display_name, category: payload.operation.category, protocol: payload.operation.protocol, + security_level: payload.operation.security_level, status: OperationStatus::Draft, version, target: payload.operation.target, @@ -2160,6 +2169,7 @@ impl AdminService { display_name: payload.display_name, category: payload.category, protocol: existing.snapshot.protocol, + security_level: payload.security_level, status: OperationStatus::Draft, version: existing.version, target: payload.target, @@ -3661,6 +3671,7 @@ impl AdminService { display_name: document.operation.display_name.clone(), category: document.operation.category.clone(), protocol: document.operation.protocol, + security_level: document.operation.security_level, target: document.operation.target.clone(), input_schema: document.operation.input_schema.clone(), output_schema: document.operation.output_schema.clone(), @@ -3841,6 +3852,7 @@ impl AdminService { } fn validate_operation_payload(&self, payload: &OperationPayload) -> Result<(), ApiError> { + self.validate_operation_capabilities(payload.protocol, payload.security_level)?; validate_protocol_target(payload.protocol, &payload.target)?; payload.input_mapping.validate_paths()?; payload.output_mapping.validate_paths()?; @@ -3848,12 +3860,52 @@ impl AdminService { } fn validate_registry_operation(&self, operation: &RegistryOperation) -> Result<(), ApiError> { + self.validate_operation_capabilities(operation.protocol, operation.security_level)?; validate_protocol_target(operation.protocol, &operation.target)?; operation.input_mapping.validate_paths()?; operation.output_mapping.validate_paths()?; Ok(()) } + fn validate_operation_capabilities( + &self, + protocol: Protocol, + security_level: OperationSecurityLevel, + ) -> Result<(), ApiError> { + let supported_protocols = [Protocol::Rest, Protocol::Graphql, Protocol::Grpc]; + if !supported_protocols.contains(&protocol) { + return Err(ApiError::validation_with_context( + format!( + "protocol {} is not supported in Community", + serde_json::to_string(&protocol) + .unwrap_or_else(|_| "\"unknown\"".to_owned()) + .trim_matches('"') + ), + json!({ + "protocol": protocol, + "edition": "community", + }), + )); + } + + if security_level != OperationSecurityLevel::Standard { + return Err(ApiError::validation_with_context( + format!( + "security level {} is not supported in Community", + serde_json::to_string(&security_level) + .unwrap_or_else(|_| "\"unknown\"".to_owned()) + .trim_matches('"') + ), + json!({ + "security_level": security_level, + "edition": "community", + }), + )); + } + + Ok(()) + } + async fn find_operation_by_name( &self, workspace_id: &WorkspaceId, @@ -4665,6 +4717,7 @@ fn demo_rest_operation_payload() -> OperationPayload { display_name: "Create CRM Lead".to_owned(), category: "sales".to_owned(), protocol: Protocol::Rest, + security_level: OperationSecurityLevel::Standard, target: Target::Rest(crank_core::RestTarget { base_url: "https://crm.demo.internal".to_owned(), method: crank_core::HttpMethod::Post, @@ -4718,6 +4771,7 @@ fn demo_graphql_operation_payload() -> OperationPayload { display_name: "Get Invoice Status".to_owned(), category: "finance".to_owned(), protocol: Protocol::Graphql, + security_level: OperationSecurityLevel::Standard, target: Target::Graphql(crank_core::GraphqlTarget { endpoint: "https://billing.demo.internal/graphql".to_owned(), operation_type: crank_core::GraphqlOperationType::Query, @@ -4769,6 +4823,7 @@ fn demo_grpc_operation_payload() -> OperationPayload { display_name: "Lookup Support Ticket".to_owned(), category: "support".to_owned(), protocol: Protocol::Grpc, + security_level: OperationSecurityLevel::Standard, target: Target::Grpc(crank_core::GrpcTarget { server_addr: "http://grpc.demo.internal".to_owned(), package: "echo".to_owned(), @@ -4833,6 +4888,7 @@ fn demo_archived_operation_payload() -> OperationPayload { display_name: "Archive Marketing Contact".to_owned(), category: "marketing".to_owned(), protocol: Protocol::Rest, + security_level: OperationSecurityLevel::Standard, target: Target::Rest(crank_core::RestTarget { base_url: "https://marketing.demo.internal".to_owned(), method: crank_core::HttpMethod::Patch, @@ -5341,6 +5397,7 @@ fn enrich_operation_summary( display_name: summary.display_name, category: summary.category, protocol: summary.protocol, + security_level: summary.security_level, target_url: summary.target_url, target_action: summary.target_action, status: summary.status, diff --git a/apps/mcp-server/src/main.rs b/apps/mcp-server/src/main.rs index a5838bf..e45af80 100644 --- a/apps/mcp-server/src/main.rs +++ b/apps/mcp-server/src/main.rs @@ -2778,6 +2778,7 @@ mod tests { display_name: "Create Lead".to_owned(), category: "sales".to_owned(), protocol: Protocol::Rest, + security_level: crank_core::OperationSecurityLevel::Standard, status: OperationStatus::Published, version: 1, target: Target::Rest(RestTarget { @@ -2840,6 +2841,7 @@ mod tests { display_name: "Create Lead GraphQL".to_owned(), category: "sales".to_owned(), protocol: Protocol::Graphql, + security_level: crank_core::OperationSecurityLevel::Standard, status: OperationStatus::Published, version: 1, target: Target::Graphql(GraphqlTarget { @@ -2907,6 +2909,7 @@ mod tests { display_name: "Unary Echo gRPC".to_owned(), category: "support".to_owned(), protocol: Protocol::Grpc, + security_level: crank_core::OperationSecurityLevel::Standard, status: OperationStatus::Published, version: 1, target: Target::Grpc(GrpcTarget { diff --git a/crates/crank-core/src/edition.rs b/crates/crank-core/src/edition.rs index 667dba2..8c4015a 100644 --- a/crates/crank-core/src/edition.rs +++ b/crates/crank-core/src/edition.rs @@ -26,6 +26,12 @@ pub enum OperationSecurityLevel { Strict, } +impl Default for OperationSecurityLevel { + fn default() -> Self { + Self::Standard + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct EditionLimits { pub max_workspaces: Option, diff --git a/crates/crank-core/src/operation.rs b/crates/crank-core/src/operation.rs index 7d7cad7..b20dbc8 100644 --- a/crates/crank-core/src/operation.rs +++ b/crates/crank-core/src/operation.rs @@ -5,6 +5,7 @@ use serde_json::Value; use time::OffsetDateTime; use crate::{ + edition::OperationSecurityLevel, ids::{AuthProfileId, DescriptorId, OperationId, SampleId}, protocol::{ExportMode, GraphqlOperationType, HttpMethod, Protocol}, soap::{ @@ -17,6 +18,10 @@ fn default_operation_category() -> String { "general".to_owned() } +fn default_operation_security_level() -> OperationSecurityLevel { + OperationSecurityLevel::Standard +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum OperationStatus { @@ -217,6 +222,8 @@ pub struct Operation { #[serde(default = "default_operation_category")] pub category: String, pub protocol: Protocol, + #[serde(default = "default_operation_security_level")] + pub security_level: OperationSecurityLevel, pub status: OperationStatus, pub version: u32, pub target: Target, @@ -272,6 +279,7 @@ mod tests { use crate::{ auth::{AuthConfig, AuthProfile, BearerAuthConfig}, + edition::OperationSecurityLevel, ids::{AuthProfileId, OperationId, SampleId}, operation::{ ConfigExport, ExecutionConfig, GraphqlTarget, Operation, OperationStatus, @@ -382,6 +390,7 @@ mod tests { display_name: "Create Lead".to_owned(), category: "sales".to_owned(), protocol: Protocol::Rest, + security_level: OperationSecurityLevel::Standard, status: OperationStatus::Draft, version: 1, target: Target::Rest(RestTarget { @@ -470,6 +479,7 @@ mod tests { display_name: "Create Lead".to_owned(), category: "sales".to_owned(), protocol: Protocol::Rest, + security_level: OperationSecurityLevel::Standard, status: OperationStatus::Published, version: 3, target: Target::Rest(RestTarget { @@ -514,6 +524,7 @@ mod tests { serde_yaml::from_str(&yaml).unwrap(); assert!(yaml.contains("protocol: rest")); + assert!(yaml.contains("security_level: standard")); assert!(yaml.contains("export_mode: portable")); assert_eq!(restored, operation); } @@ -526,6 +537,7 @@ name: crm_create_lead display_name: Create Lead category: sales protocol: rest +security_level: standard status: draft version: 1 target: @@ -557,6 +569,7 @@ updated_at: 2026-03-25T08:10:00Z let restored: Operation = serde_yaml::from_str(yaml).unwrap(); + assert_eq!(restored.security_level, OperationSecurityLevel::Standard); assert_eq!(restored.published_at, None); } } diff --git a/crates/crank-registry/src/migrations.rs b/crates/crank-registry/src/migrations.rs index 31fcc72..19d5ef0 100644 --- a/crates/crank-registry/src/migrations.rs +++ b/crates/crank-registry/src/migrations.rs @@ -198,6 +198,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> { display_name text not null, category text not null default 'general', protocol text not null, + security_level text not null default 'standard', status text not null, current_draft_version integer not null default 1, latest_published_version integer null, @@ -217,6 +218,11 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> { ) .execute(pool) .await?; + query( + "alter table operations add column if not exists security_level text not null default 'standard'", + ) + .execute(pool) + .await?; query("update operations set workspace_id = 'ws_default' where workspace_id is null") .execute(pool) .await?; diff --git a/crates/crank-registry/src/model.rs b/crates/crank-registry/src/model.rs index cbfc3af..0e32d9d 100644 --- a/crates/crank-registry/src/model.rs +++ b/crates/crank-registry/src/model.rs @@ -2,9 +2,9 @@ use crank_core::{ Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AsyncJobHandle, AsyncJobId, AuthProfile, DescriptorId, ExecutionMode, ExportMode, InvitationToken, InvocationLevel, InvocationLog, InvocationSource, JobStatus, MembershipRole, Operation, OperationId, - OperationStatus, PlatformApiKey, Protocol, SampleId, Secret, SecretId, SecretVersion, - StreamSession, StreamSessionId, StreamStatus, UsagePeriod, UsageRollup, User, UserSessionId, - Workspace, WorkspaceId, + OperationSecurityLevel, OperationStatus, PlatformApiKey, Protocol, SampleId, Secret, SecretId, + SecretVersion, StreamSession, StreamSessionId, StreamStatus, UsagePeriod, UsageRollup, User, + UserSessionId, Workspace, WorkspaceId, }; use crank_mapping::MappingSet; use crank_schema::Schema; @@ -207,6 +207,7 @@ pub struct OperationSummary { pub display_name: String, pub category: String, pub protocol: Protocol, + pub security_level: OperationSecurityLevel, pub target_url: String, pub target_action: String, pub status: OperationStatus, diff --git a/crates/crank-registry/src/postgres/agent.rs b/crates/crank-registry/src/postgres/agent.rs index ae10fa1..079a977 100644 --- a/crates/crank-registry/src/postgres/agent.rs +++ b/crates/crank-registry/src/postgres/agent.rs @@ -452,6 +452,7 @@ impl PostgresRegistry { o.display_name, o.category, o.protocol, + o.security_level, o.created_at as \"operation_created_at!: time::OffsetDateTime\", o.updated_at as \"operation_updated_at!: time::OffsetDateTime\", o.published_at as \"operation_published_at: time::OffsetDateTime\", @@ -523,6 +524,7 @@ impl PostgresRegistry { row.display_name, row.category, row.protocol, + row.security_level, row.operation_created_at, row.operation_updated_at, row.operation_published_at, diff --git a/crates/crank-registry/src/postgres/mod.rs b/crates/crank-registry/src/postgres/mod.rs index 1d5c46c..38ad00e 100644 --- a/crates/crank-registry/src/postgres/mod.rs +++ b/crates/crank-registry/src/postgres/mod.rs @@ -714,6 +714,7 @@ fn build_operation_summary( display_name: String, category: String, protocol: String, + security_level: String, target_json: Value, status: String, current_draft_version: i32, @@ -732,6 +733,7 @@ fn build_operation_summary( display_name, category, protocol: deserialize_enum_text(&protocol, "protocol")?, + security_level: deserialize_enum_text(&security_level, "security_level")?, target_url, target_action, status: deserialize_enum_text(&status, "status")?, @@ -831,6 +833,7 @@ fn build_operation_version_record( display_name: String, category: String, protocol: String, + security_level: String, operation_created_at: OffsetDateTime, operation_updated_at: OffsetDateTime, operation_published_at: Option, @@ -868,6 +871,7 @@ fn build_operation_version_record( display_name, category, protocol: deserialize_enum_text(&protocol, "protocol")?, + security_level: deserialize_enum_text(&security_level, "security_level")?, status, version, target: deserialize_json_value(target_json)?, @@ -1130,10 +1134,10 @@ mod tests { AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig, AsyncJobHandle, AuthConfig, AuthKind, AuthProfile, ConfigExport, ExecutionConfig, ExportMode, GeneratedDraft, GeneratedDraftStatus, HttpMethod, InvocationLog, JobStatus, - MembershipRole, OperationId, OperationStatus, PlatformApiKey, PlatformApiKeyId, - PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, RestTarget, RetryPolicy, Samples, - SecretId, StreamSession, StreamSessionId, StreamStatus, Target, ToolDescription, - ToolExample, User, UserId, UserSessionId, Workspace, WorkspaceId, + MembershipRole, OperationId, OperationSecurityLevel, OperationStatus, PlatformApiKey, + PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, RestTarget, + RetryPolicy, Samples, SecretId, StreamSession, StreamSessionId, StreamStatus, Target, + ToolDescription, ToolExample, User, UserId, UserSessionId, Workspace, WorkspaceId, }; use crank_mapping::{MappingRule, MappingSet}; use crank_schema::{Schema, SchemaKind}; @@ -2345,6 +2349,7 @@ mod tests { display_name: format!("Display {id}"), category: "general".to_owned(), protocol: Protocol::Rest, + security_level: OperationSecurityLevel::Standard, status, version, target: Target::Rest(RestTarget { diff --git a/crates/crank-registry/src/postgres/operation.rs b/crates/crank-registry/src/postgres/operation.rs index 0e8dcca..41dc386 100644 --- a/crates/crank-registry/src/postgres/operation.rs +++ b/crates/crank-registry/src/postgres/operation.rs @@ -34,6 +34,7 @@ impl PostgresRegistry { display_name, category, protocol, + security_level, status, current_draft_version, latest_published_version, @@ -41,10 +42,10 @@ impl PostgresRegistry { updated_at, published_at ) values ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, - $10::timestamptz, + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::timestamptz, - $12::timestamptz + $12::timestamptz, + $13::timestamptz )", ) .bind(snapshot.id.as_str()) @@ -53,6 +54,10 @@ impl PostgresRegistry { .bind(&snapshot.display_name) .bind(&snapshot.category) .bind(serialize_enum_text(&snapshot.protocol, "protocol")?) + .bind(serialize_enum_text( + &snapshot.security_level, + "security_level", + )?) .bind(serialize_enum_text(&snapshot.status, "status")?) .bind(to_db_version(snapshot.version)) .bind( @@ -155,12 +160,17 @@ impl PostgresRegistry { "update operations set display_name = $1, category = $2, - status = $3, - updated_at = $4::timestamptz - where workspace_id = $5 and id = $6", + security_level = $3, + status = $4, + updated_at = $5::timestamptz + where workspace_id = $6 and id = $7", ) .bind(&snapshot.display_name) .bind(&snapshot.category) + .bind(serialize_enum_text( + &snapshot.security_level, + "security_level", + )?) .bind(serialize_enum_text(&snapshot.status, "status")?) .bind(snapshot.updated_at) .bind(workspace_id.as_str()) @@ -316,6 +326,7 @@ impl PostgresRegistry { operations.display_name, operations.category, operations.protocol, + operations.security_level, ov.target_json, operations.status, operations.current_draft_version, @@ -343,6 +354,7 @@ impl PostgresRegistry { row.display_name, row.category, row.protocol, + row.security_level, row.target_json, row.status, row.current_draft_version, @@ -428,6 +440,7 @@ impl PostgresRegistry { operations.display_name, operations.category, operations.protocol, + operations.security_level, ov.target_json, operations.status, operations.current_draft_version, @@ -454,6 +467,7 @@ impl PostgresRegistry { row.display_name, row.category, row.protocol, + row.security_level, row.target_json, row.status, row.current_draft_version, @@ -480,6 +494,7 @@ impl PostgresRegistry { o.display_name, o.category, o.protocol, + o.security_level, o.created_at as \"operation_created_at!: time::OffsetDateTime\", o.updated_at as \"operation_updated_at!: time::OffsetDateTime\", o.published_at as \"operation_published_at: time::OffsetDateTime\", @@ -516,6 +531,7 @@ impl PostgresRegistry { row.display_name, row.category, row.protocol, + row.security_level, row.operation_created_at, row.operation_updated_at, row.operation_published_at, @@ -552,6 +568,7 @@ impl PostgresRegistry { o.display_name, o.category, o.protocol, + o.security_level, o.created_at as \"operation_created_at!: time::OffsetDateTime\", o.updated_at as \"operation_updated_at!: time::OffsetDateTime\", o.published_at as \"operation_published_at: time::OffsetDateTime\", @@ -589,6 +606,7 @@ impl PostgresRegistry { row.display_name, row.category, row.protocol, + row.security_level, row.operation_created_at, row.operation_updated_at, row.operation_published_at, @@ -691,6 +709,7 @@ impl PostgresRegistry { o.display_name, o.category, o.protocol, + o.security_level, o.created_at as \"operation_created_at!: time::OffsetDateTime\", o.updated_at as \"operation_updated_at!: time::OffsetDateTime\", o.published_at as \"operation_published_at: time::OffsetDateTime\", @@ -727,6 +746,7 @@ impl PostgresRegistry { row.display_name, row.category, row.protocol, + row.security_level, row.operation_created_at, row.operation_updated_at, row.operation_published_at, @@ -760,6 +780,7 @@ impl PostgresRegistry { o.display_name, o.category, o.protocol, + o.security_level, o.created_at as \"operation_created_at!: time::OffsetDateTime\", o.updated_at as \"operation_updated_at!: time::OffsetDateTime\", o.published_at as \"operation_published_at: time::OffsetDateTime\", @@ -796,6 +817,7 @@ impl PostgresRegistry { row.display_name, row.category, row.protocol, + row.security_level, row.operation_created_at, row.operation_updated_at, row.operation_published_at, diff --git a/crates/crank-runtime/src/executor.rs b/crates/crank-runtime/src/executor.rs index 72d996c..bdd039f 100644 --- a/crates/crank-runtime/src/executor.rs +++ b/crates/crank-runtime/src/executor.rs @@ -718,9 +718,9 @@ mod tests { use crank_core::{ AggregationMode, DescriptorId, ExecutionConfig, ExecutionMode, GeneratedDraft, GeneratedDraftStatus, GraphqlOperationType, GraphqlTarget, GrpcTarget, HttpMethod, - Operation, OperationId, OperationStatus, Protocol, ProtocolOptions, RestTarget, Samples, - SoapBindingStyle, SoapOperationMetadata, SoapTarget, SoapVersion, StreamingConfig, Target, - ToolDescription, ToolExample, ToolFamilyConfig, TransportBehavior, + Operation, OperationId, OperationSecurityLevel, OperationStatus, Protocol, ProtocolOptions, + RestTarget, Samples, SoapBindingStyle, SoapOperationMetadata, SoapTarget, SoapVersion, + StreamingConfig, Target, ToolDescription, ToolExample, ToolFamilyConfig, TransportBehavior, WebsocketProtocolOptions, WebsocketTarget, }; use crank_mapping::{MappingRule, MappingSet}; @@ -1654,6 +1654,7 @@ mod tests { display_name: "Create Lead".to_owned(), category: "sales".to_owned(), protocol: Protocol::Rest, + security_level: OperationSecurityLevel::Standard, status: OperationStatus::Published, version: 1, target: Target::Rest(RestTarget { @@ -1717,6 +1718,7 @@ mod tests { display_name: "Create Lead GraphQL".to_owned(), category: "sales".to_owned(), protocol: Protocol::Graphql, + security_level: OperationSecurityLevel::Standard, status: OperationStatus::Published, version: 1, target: Target::Graphql(GraphqlTarget { @@ -1793,6 +1795,7 @@ mod tests { display_name: "Unary Echo gRPC".to_owned(), category: "support".to_owned(), protocol: Protocol::Grpc, + security_level: OperationSecurityLevel::Standard, status: OperationStatus::Published, version: 1, target: Target::Grpc(GrpcTarget { @@ -1872,6 +1875,7 @@ mod tests { display_name: "Server Stream Echo gRPC".to_owned(), category: "support".to_owned(), protocol: Protocol::Grpc, + security_level: OperationSecurityLevel::Standard, status: OperationStatus::Published, version: 1, target: Target::Grpc(GrpcTarget { @@ -1950,6 +1954,7 @@ mod tests { display_name: "Create Lead SOAP".to_owned(), category: "sales".to_owned(), protocol: Protocol::Soap, + security_level: OperationSecurityLevel::Standard, status: OperationStatus::Published, version: 1, target: Target::Soap(SoapTarget { @@ -2039,6 +2044,7 @@ mod tests { display_name: "Billing Log Window".to_owned(), category: "ops".to_owned(), protocol: Protocol::Rest, + security_level: OperationSecurityLevel::Standard, status: OperationStatus::Published, version: 1, target: Target::Rest(RestTarget { @@ -2139,6 +2145,7 @@ mod tests { display_name: "Telemetry Window WebSocket".to_owned(), category: "ops".to_owned(), protocol: Protocol::Websocket, + security_level: OperationSecurityLevel::Standard, status: OperationStatus::Published, version: 1, target: Target::Websocket(WebsocketTarget { diff --git a/docs/admin-api.md b/docs/admin-api.md index 90d3fdc..4cbe306 100644 --- a/docs/admin-api.md +++ b/docs/admin-api.md @@ -136,6 +136,13 @@ - у операции задается обязательный `security_level`; - допустимые значения: `standard`, `elevated`, `strict`; - этот параметр определяет минимально допустимый режим машинного доступа при вызове через MCP. +- если поле не передано, используется `standard` для обратной совместимости с уже существующим YAML и draft payload; +- в `Community` backend принимает только: + - `REST` + - `GraphQL` + - `gRPC` + - `security_level = standard` +- попытка создать, обновить или импортировать операцию с неподдерживаемым `protocol` или `security_level` должна завершаться `validation_error` еще на стороне `admin-api`, а не только скрываться в UI. ### 5.4. Samples and descriptors