mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-19 16:02:31 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
245bbd5d27 | ||
|
|
01b38320ea | ||
|
|
37e493ae66 | ||
|
|
f0d66a42eb | ||
|
|
9d335de87a | ||
|
|
53afecd458 |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "1.814.0"
|
||||
".": "1.815.0"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
# Changelog
|
||||
|
||||
## [1.815.0](https://github.com/windmill-labs/windmill/compare/v1.814.0...v1.815.0) (2026-09-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a workspace toggle that adds its admins and developers to new forks ([#11215](https://github.com/windmill-labs/windmill/issues/11215)) ([9d335de](https://github.com/windmill-labs/windmill/commit/9d335de87a4dbaa51038d55afe8d980761dcdfaf))
|
||||
* add an instance setting to refuse a token in MCP URLs ([#11162](https://github.com/windmill-labs/windmill/issues/11162)) ([37e493a](https://github.com/windmill-labs/windmill/commit/37e493ae66ed5c000ecac492d60fc0fdf4bda71f))
|
||||
* **ai-chat:** render get_run through the run tools' card ([#11204](https://github.com/windmill-labs/windmill/issues/11204)) ([6f9c4dc](https://github.com/windmill-labs/windmill/commit/6f9c4dc29455d13b0e64af05c2d6aa8bd5ff4fd6))
|
||||
* **ai-chat:** tell the chat which kind of app it is looking at ([#11208](https://github.com/windmill-labs/windmill/issues/11208)) ([c8c06d8](https://github.com/windmill-labs/windmill/commit/c8c06d8f79774abf109192e71a8b6fc37c7937ba))
|
||||
* attach files to a flow chat message ([#11185](https://github.com/windmill-labs/windmill/issues/11185)) ([c4c9677](https://github.com/windmill-labs/windmill/commit/c4c9677982b75c63d98ebf85b1904e0c341ba957))
|
||||
* **auth:** 2 h login links and a click-to-sign-in page for emailed ones ([#11203](https://github.com/windmill-labs/windmill/issues/11203)) ([5639187](https://github.com/windmill-labs/windmill/commit/5639187fec6d517a72e82df49d63d7438301127c))
|
||||
* cap user token expiration with an instance setting ([#11159](https://github.com/windmill-labs/windmill/issues/11159)) ([9320312](https://github.com/windmill-labs/windmill/commit/9320312eac56f944c4d31504601293ab4e816ccc))
|
||||
* flow chat job-backed detail, smooth streaming and answer chrome ([#11186](https://github.com/windmill-labs/windmill/issues/11186)) ([e2a91ca](https://github.com/windmill-labs/windmill/commit/e2a91ca2b141a0bee64ddfafe3cbf3ff86b6fb35))
|
||||
* put a data table's connection under Postgres roles ([#11020](https://github.com/windmill-labs/windmill/issues/11020)) ([0e807fb](https://github.com/windmill-labs/windmill/commit/0e807fb1dd80d7536ec144cd49445abc7961e504))
|
||||
* render an AI agent result as its answer, not as raw JSON ([#11051](https://github.com/windmill-labs/windmill/issues/11051)) ([a089928](https://github.com/windmill-labs/windmill/commit/a08992834d45d0211336f4fc32c3421646ca47c5))
|
||||
* support $flow_expr[...] dynamic tags on flow steps ([#11170](https://github.com/windmill-labs/windmill/issues/11170)) ([48f0025](https://github.com/windmill-labs/windmill/commit/48f00259c5e7361d3553dbcb809461e1cde96f8f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** inline only the package version, not the whole package.json ([#11191](https://github.com/windmill-labs/windmill/issues/11191)) ([813e486](https://github.com/windmill-labs/windmill/commit/813e486e166ac6215364817a7732b66f8dc1d463))
|
||||
* keep instance groups when editing auto-invite ([#11217](https://github.com/windmill-labs/windmill/issues/11217)) ([df61dea](https://github.com/windmill-labs/windmill/commit/df61dea5fa8b18d1e0044dc0db6702b053d5119f))
|
||||
* key the large root font size on screen width, not window width ([#11216](https://github.com/windmill-labs/windmill/issues/11216)) ([3b4e13d](https://github.com/windmill-labs/windmill/commit/3b4e13d1c564c6195e30b55b0671f7533e3ce408))
|
||||
* re-encrypt git sync secrets on workspace key rotation ([#11218](https://github.com/windmill-labs/windmill/issues/11218)) ([f0d66a4](https://github.com/windmill-labs/windmill/commit/f0d66a42eba27502767423af0c08b2ca1f16e8df))
|
||||
* re-point cloned fork identities that name nobody in the fork ([#11161](https://github.com/windmill-labs/windmill/issues/11161)) ([9690c44](https://github.com/windmill-labs/windmill/commit/9690c4462cf264a5577b87d07d465b5442b4e09d))
|
||||
* register the job token with the sensitive log masking system ([#10943](https://github.com/windmill-labs/windmill/issues/10943)) ([53afecd](https://github.com/windmill-labs/windmill/commit/53afecd4588247bc1812d3e68a30db1f3c3b2724))
|
||||
* show the New menu's description panel only on hover ([#11199](https://github.com/windmill-labs/windmill/issues/11199)) ([ecd0a6c](https://github.com/windmill-labs/windmill/commit/ecd0a6c77bc3a057b8072dbb0aca731e8bd3d882))
|
||||
* stop picker listing a draft twice when drafts share a friendly path ([#11214](https://github.com/windmill-labs/windmill/issues/11214)) ([72507d5](https://github.com/windmill-labs/windmill/commit/72507d52a021de8101794ee8f4a3392be1ba20c7))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* count completed jobs on the v2_job_completed index with a timeout ([#11211](https://github.com/windmill-labs/windmill/issues/11211)) ([d1a2536](https://github.com/windmill-labs/windmill/commit/d1a25360b070994a31fb033707dbfde1fb553939))
|
||||
|
||||
## [1.814.0](https://github.com/windmill-labs/windmill/compare/v1.813.0...v1.814.0) (2026-09-17)
|
||||
|
||||
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH RECURSIVE descendants AS (\n SELECT id FROM v2_job WHERE parent_job = $1 AND workspace_id = $2\n UNION ALL\n SELECT j.id FROM v2_job j JOIN descendants d ON j.parent_job = d.id\n WHERE j.workspace_id = $2\n ) SELECT d.id AS \"id!\" FROM descendants d JOIN v2_job_queue q ON q.id = d.id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0c2bf6925de6dd4d9f8d47d7fdd9e7801129570106b5acd49c0f708398606771"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace_settings (workspace_id, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url) SELECT $1, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url FROM workspace_settings WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1d8ccd32266637d7f7915f92a8483dce0c9986f3847227c2f66daa84d4109d7d"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO usr (workspace_id, username, email, is_admin, created_at, operator, disabled, role, is_service_account, added_via)\n SELECT $1, username, email, is_admin, created_at, operator, disabled, role, is_service_account,\n CASE WHEN $3 THEN NULL ELSE added_via END\n FROM usr WHERE workspace_id = $2\n AND (NOT $3 OR (NOT operator AND NOT disabled AND NOT is_service_account))\n ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2d6b117324eaf076a0ed06d2cb0ce73279957d6a39fdc6b1ecb6e0a1e02f921f"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT add_admins_and_developers_to_forks FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "add_admins_and_developers_to_forks",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2f39fce0ee700117f3e4c066e0b56ee979e4c392970278304e80368b770bb7b4"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE v2_job_queue SET started_at = now() WHERE id = $1 RETURNING started_at",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "started_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "451f303f4a24d848ba4ccdc2441ca7cb85bdfc66110b6534260f8642f91f006a"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE workspace_settings\n SET\n ai_config = source_ws.ai_config,\n large_file_storage = source_ws.large_file_storage,\n ducklake = source_ws.ducklake,\n dbt_warehouses = source_ws.dbt_warehouses,\n datatable = source_ws.datatable,\n git_app_installations = source_ws.git_app_installations\n FROM workspace_settings source_ws\n WHERE source_ws.workspace_id = $1\n AND workspace_settings.workspace_id = $2\n ",
|
||||
"query": "\n UPDATE workspace_settings\n SET\n ai_config = source_ws.ai_config,\n large_file_storage = source_ws.large_file_storage,\n ducklake = source_ws.ducklake,\n dbt_warehouses = source_ws.dbt_warehouses,\n datatable = source_ws.datatable,\n git_app_installations = source_ws.git_app_installations,\n add_admins_and_developers_to_forks = source_ws.add_admins_and_developers_to_forks\n FROM workspace_settings source_ws\n WHERE source_ws.workspace_id = $1\n AND workspace_settings.workspace_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -11,5 +11,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a6decdebcd9750691f20f874b66a9c6f2ede08c33605c6a3c3dfc213a3eda76a"
|
||||
"hash": "5ccfbd0f345b9b86ca356008def6cc1011a49fc8d8ad046ac572e81cf8938995"
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, status = 'success' AS \"success!\", result AS \"result: Json<Box<RawValue>>\"\n FROM v2_job_completed WHERE workspace_id = $1 AND id = ANY($2)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "success!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "result: Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"UuidArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "789c3f6d29f46fcb17a22fe97405a1754ccb8ddeb08c23795dfe14e9f332e9dc"
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n workspace_id,\n slack_team_id,\n teams_team_id,\n teams_team_name,\n teams_team_guid,\n slack_name,\n slack_command_script,\n teams_command_script,\n slack_email,\n slack_oauth_client_id,\n slack_oauth_client_secret,\n customer_id,\n plan,\n webhook,\n ai_config,\n dbt_warehouses,\n large_file_storage,\n datatable,\n ducklake,\n git_sync,\n deploy_ui,\n default_app,\n default_scripts,\n mute_critical_alerts,\n color,\n operator_settings,\n git_app_installations,\n auto_invite,\n error_handler,\n success_handler,\n public_app_execution_limit_per_minute,\n error_handler_fallback_to_instance_alerts,\n guest_access_enabled,\n guest_jwt_public_key,\n guest_jwt_jwks_url\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ",
|
||||
"query": "\n SELECT\n workspace_id,\n slack_team_id,\n teams_team_id,\n teams_team_name,\n teams_team_guid,\n slack_name,\n slack_command_script,\n teams_command_script,\n slack_email,\n slack_oauth_client_id,\n slack_oauth_client_secret,\n customer_id,\n plan,\n webhook,\n ai_config,\n dbt_warehouses,\n large_file_storage,\n datatable,\n ducklake,\n git_sync,\n deploy_ui,\n default_app,\n default_scripts,\n mute_critical_alerts,\n color,\n operator_settings,\n git_app_installations,\n auto_invite,\n error_handler,\n success_handler,\n public_app_execution_limit_per_minute,\n error_handler_fallback_to_instance_alerts,\n guest_access_enabled,\n guest_jwt_public_key,\n guest_jwt_jwks_url,\n add_admins_and_developers_to_forks\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -177,6 +177,11 @@
|
||||
"ordinal": 34,
|
||||
"name": "guest_jwt_jwks_url",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 35,
|
||||
"name": "add_admins_and_developers_to_forks",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -219,8 +224,9 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "dc4a57df3becc610f631ef22c116450390addbfae85fecc61c991d94167e6e99"
|
||||
"hash": "8ebe054b41793f1a7b85f1f8d29cd21d12fa1207b58bfc46249c250cdcdb5363"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET add_admins_and_developers_to_forks = $1 WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9bd1995747f0073b3a866d1238e78f7e6dfa9185056f7731547ff05b8176b271"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO usr (workspace_id, username, email, is_admin, created_at, operator, disabled, role, is_service_account, added_via)\n SELECT $1, username, email, is_admin, created_at, operator, disabled, role, is_service_account, added_via\n FROM usr WHERE workspace_id = $2\n ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b98844926ff127c528ed3e7bc63bf1ebed0192be267983e8fbd18f79997e6142"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE v2_job_queue SET worker = $1 WHERE id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e459c277c0bc27293d71972d12d26d32b476a43ed391c09d7678215195c4283a"
|
||||
}
|
||||
+10
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n workspace_id,\n slack_team_id,\n slack_name,\n teams_team_id,\n teams_team_name,\n teams_team_guid,\n mute_critical_alerts,\n guest_access_enabled,\n deploy_ui,\n large_file_storage,\n datatable\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ",
|
||||
"query": "\n SELECT\n workspace_id,\n slack_team_id,\n slack_name,\n teams_team_id,\n teams_team_name,\n teams_team_guid,\n mute_critical_alerts,\n guest_access_enabled,\n add_admins_and_developers_to_forks,\n deploy_ui,\n large_file_storage,\n datatable\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -45,16 +45,21 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "add_admins_and_developers_to_forks",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "deploy_ui",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "large_file_storage",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 11,
|
||||
"name": "datatable",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
@@ -73,10 +78,11 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ede15bff96152f209aff756830cbc76b5afa1af6ed324376989117b1054c3447"
|
||||
"hash": "e6e31fdf705896c81f9a0f45d47c1b93db0406aaf278bccba07be00e9f937e2a"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace_settings (workspace_id, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url, add_admins_and_developers_to_forks) SELECT $1, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url, add_admins_and_developers_to_forks FROM workspace_settings WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "eefa0588a6a927fd9b3f65e1df652fb2b4cf7983049d2c490940df360c7e2b30"
|
||||
}
|
||||
Generated
+85
-85
@@ -2311,9 +2311,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.4.6"
|
||||
version = "1.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4"
|
||||
checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -5055,9 +5055,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
||||
checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
@@ -5497,9 +5497,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.8.9"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae"
|
||||
checksum = "54ade96dc9003043bce7c035c85a9df5a858bfb2039c5a2e6fdf00f324f6c551"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
@@ -14793,7 +14793,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -14881,7 +14881,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-ai"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
@@ -14915,7 +14915,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-alerting"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14928,7 +14928,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -15068,7 +15068,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-agent-workers"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15091,7 +15091,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-assets"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15108,7 +15108,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-auth"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -15134,7 +15134,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -15144,7 +15144,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-configs"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15161,7 +15161,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-debug"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"base64 0.22.1",
|
||||
@@ -15183,7 +15183,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-embeddings"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -15206,7 +15206,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flow-conversations"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15222,7 +15222,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flows"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15244,7 +15244,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-groups"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15266,7 +15266,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-inputs"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15280,7 +15280,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-integration-tests"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -15315,7 +15315,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-jobs"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -15340,7 +15340,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-npm-proxy"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15368,7 +15368,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-openapi"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -15390,7 +15390,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-schedule"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15410,7 +15410,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-scripts"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15448,7 +15448,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-settings"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -15477,7 +15477,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-sse"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"serde",
|
||||
@@ -15489,7 +15489,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-users"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum 0.8.9",
|
||||
@@ -15513,7 +15513,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workers"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15527,7 +15527,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workspaces"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15562,7 +15562,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -15576,7 +15576,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -15595,7 +15595,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"aho-corasick",
|
||||
@@ -15702,7 +15702,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-dep-map"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"futures",
|
||||
@@ -15722,7 +15722,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -15739,7 +15739,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -15766,7 +15766,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-jseval"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -15783,7 +15783,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -15799,7 +15799,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-mcp"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15820,7 +15820,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-native-triggers"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15851,7 +15851,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-oauth"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -15876,7 +15876,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-object-store"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -15911,7 +15911,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-operator"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -15929,7 +15929,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -15938,7 +15938,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15950,7 +15950,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15962,7 +15962,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -15974,7 +15974,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15986,7 +15986,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15998,7 +15998,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -16009,7 +16009,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16020,7 +16020,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16032,7 +16032,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -16043,7 +16043,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16065,7 +16065,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16077,7 +16077,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16091,7 +16091,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -16108,7 +16108,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16121,7 +16121,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -16133,7 +16133,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16151,7 +16151,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -16167,7 +16167,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -16183,7 +16183,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16197,7 +16197,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16236,7 +16236,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-runtime-nativets"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"const_format",
|
||||
@@ -16276,7 +16276,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"wasm-bindgen",
|
||||
@@ -16287,7 +16287,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-store"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16322,7 +16322,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-test-utils"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16346,7 +16346,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16379,7 +16379,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-amqp"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16406,7 +16406,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-azure"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16439,7 +16439,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-email"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16459,7 +16459,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-gcp"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16493,7 +16493,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-http"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16529,7 +16529,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-kafka"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16552,7 +16552,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-mqtt"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16576,7 +16576,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-nats"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -16600,7 +16600,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-postgres"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16635,7 +16635,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-sqs"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16663,7 +16663,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-websocket"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16688,7 +16688,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.13.2",
|
||||
@@ -16707,7 +16707,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
@@ -16825,7 +16825,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker-volumes"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -88,7 +88,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
f2fced19fcae81de7f6dac545010ce404c052e1b
|
||||
bc3ef08c8e4233508c023e6ee847a3cd0b8be43b
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE workspace_settings DROP COLUMN add_admins_and_developers_to_forks;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE workspace_settings ADD COLUMN add_admins_and_developers_to_forks BOOLEAN NOT NULL DEFAULT false;
|
||||
+24
-24
@@ -6191,7 +6191,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -6274,7 +6274,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6286,7 +6286,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"serde",
|
||||
@@ -6295,7 +6295,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6307,7 +6307,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6319,7 +6319,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -6331,7 +6331,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6343,7 +6343,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6355,7 +6355,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -6366,7 +6366,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6377,7 +6377,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6389,7 +6389,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -6400,7 +6400,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -6422,7 +6422,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6434,7 +6434,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6448,7 +6448,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case",
|
||||
@@ -6465,7 +6465,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6478,7 +6478,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -6490,7 +6490,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6508,7 +6508,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -6524,7 +6524,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -6540,7 +6540,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.17",
|
||||
@@ -6572,7 +6572,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6586,7 +6586,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
|
||||
@@ -12,7 +12,7 @@ resolver = "2"
|
||||
members = ["."]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.814.0"
|
||||
version = "1.815.0"
|
||||
edition = "2021"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
|
||||
|
||||
+7
-2
@@ -52,7 +52,8 @@ use windmill_common::{
|
||||
INSTANCE_EVENTS_WEBHOOK_SETTING, INSTANCE_PYTHON_VERSION_SETTING,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING,
|
||||
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING, MAVEN_SETTINGS_XML_SETTING,
|
||||
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NO_DEFAULT_MAVEN_SETTING,
|
||||
MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING,
|
||||
NO_DEFAULT_MAVEN_SETTING,
|
||||
NPM_CONFIG_REGISTRY_SETTING, NSJAIL_TMPFS_SIZE_MB_SETTING, NSJAIL_TMP_BACKING_SETTING,
|
||||
NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING, OTEL_TRACES_RETENTION_SECS_SETTING,
|
||||
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
|
||||
@@ -126,7 +127,8 @@ use windmill_worker::{
|
||||
|
||||
use crate::monitor::{
|
||||
initial_load, load_concurrency_key_max_queued, load_disable_password_login,
|
||||
load_fork_workspace_tag_append_fork_suffix, load_keep_job_dir, load_metrics_debug_enabled,
|
||||
load_fork_workspace_tag_append_fork_suffix, load_keep_job_dir,
|
||||
load_mcp_disable_token_query_param, load_metrics_debug_enabled,
|
||||
load_preview_tags_override, load_require_preexisting_user, load_retention_period_overrides,
|
||||
load_tag_per_workspace_enabled, load_tag_per_workspace_workspaces,
|
||||
load_workspace_fairness_duration_secs, load_workspace_fairness_enabled,
|
||||
@@ -2164,6 +2166,9 @@ async fn process_notify_event(
|
||||
DISABLE_PASSWORD_LOGIN_SETTING => {
|
||||
load_disable_password_login(db).await;
|
||||
}
|
||||
MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING => {
|
||||
load_mcp_disable_token_query_param(db).await;
|
||||
}
|
||||
EXPOSE_METRICS_SETTING => {
|
||||
tracing::info!("Metrics setting changed, restarting");
|
||||
spawn_graceful_killpill(tx, db, 30, "metrics setting change", server_mode)
|
||||
|
||||
@@ -65,6 +65,7 @@ use windmill_common::{
|
||||
FORK_WORKSPACE_TAG_APPEND_FORK_SUFFIX_SETTING, HUB_API_SECRET_SETTING,
|
||||
HUB_BASE_URL_SETTING, INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING,
|
||||
JOB_ISOLATION_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING,
|
||||
MCP_DISABLE_TOKEN_QUERY_PARAM, MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING,
|
||||
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING,
|
||||
NSJAIL_TMPFS_SIZE_MB_SETTING, NSJAIL_TMP_BACKING_SETTING, NUGET_CONFIG_SETTING,
|
||||
OTEL_SETTING, OTEL_TRACES_RETENTION_SECS_SETTING, OTEL_TRACING_PROXY_SETTING,
|
||||
@@ -288,6 +289,15 @@ pub async fn initial_load(
|
||||
);
|
||||
|
||||
if let Some(db) = conn.as_sql() {
|
||||
// Outside the `server_mode` block below: a `MODE=mcp` process serves the MCP routes
|
||||
// with `server_mode` false and would otherwise never read this at all. That mode
|
||||
// joins no monitor loop, so there — as for every global setting, `base_url`
|
||||
// included — this pass is the only read, and a change lands on restart.
|
||||
pass.setting(
|
||||
MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING,
|
||||
false,
|
||||
|v| async move { apply_mcp_disable_token_query_param(v) },
|
||||
);
|
||||
pass.setting(DEFAULT_TAGS_PER_WORKSPACE_SETTING, false, |v| async move {
|
||||
apply_tag_per_workspace_enabled(v)
|
||||
});
|
||||
@@ -1617,6 +1627,23 @@ pub fn apply_disable_password_login(value: Option<serde_json::Value>) {
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn load_mcp_disable_token_query_param(db: &DB) {
|
||||
match load_value_from_global_settings(db, MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING).await {
|
||||
Ok(v) => apply_mcp_disable_token_query_param(v),
|
||||
Err(e) => tracing::error!("Error loading mcp_disable_token_query_param setting: {e:#}"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn apply_mcp_disable_token_query_param(value: Option<serde_json::Value>) {
|
||||
match value {
|
||||
Some(serde_json::Value::Bool(t)) => {
|
||||
MCP_DISABLE_TOKEN_QUERY_PARAM.store(t, Ordering::Relaxed)
|
||||
}
|
||||
None => MCP_DISABLE_TOKEN_QUERY_PARAM.store(false, Ordering::Relaxed),
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
struct LogFile {
|
||||
file_path: String,
|
||||
hostname: String,
|
||||
|
||||
@@ -234,7 +234,7 @@ workspace_protection_rule: workspace_id(char), name(char), rules(int), bypass_gr
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
workspace_runnable_dependencies: flow_path(char), runnable_path(char), script_hash(bigint), runnable_is_flow(bool), workspace_id(char), app_path(char), id(bigint), runnable_is_agent(bool)
|
||||
FK: (app_path, workspace_id) -> app(path, workspace_id) | (flow_path, workspace_id) -> flow(path, workspace_id)
|
||||
workspace_settings: workspace_id(char), slack_team_id(char), slack_name(char), slack_command_script(char), slack_email(char), customer_id(char), plan(char), webhook(text), ai_config(jsonb), large_file_storage(jsonb), git_sync(jsonb), default_app(char), default_scripts(jsonb), deploy_ui(jsonb), mute_critical_alerts(bool), color(char), operator_settings(jsonb), teams_command_script(text), teams_team_id(text), teams_team_name(text), git_app_installations(jsonb), ducklake(jsonb), slack_oauth_client_id(char), slack_oauth_client_secret(char), datatable(jsonb), teams_team_guid(text), auto_invite(jsonb), error_handler(jsonb), success_handler(jsonb), public_app_execution_limit_per_minute(int), dbt_warehouses(jsonb), guest_access_enabled(bool), guest_jwt_public_key(text), guest_jwt_jwks_url(text), ai_sessions_backup_generation(int)
|
||||
workspace_settings: workspace_id(char), slack_team_id(char), slack_name(char), slack_command_script(char), slack_email(char), customer_id(char), plan(char), webhook(text), ai_config(jsonb), large_file_storage(jsonb), git_sync(jsonb), default_app(char), default_scripts(jsonb), deploy_ui(jsonb), mute_critical_alerts(bool), color(char), operator_settings(jsonb), teams_command_script(text), teams_team_id(text), teams_team_name(text), git_app_installations(jsonb), ducklake(jsonb), slack_oauth_client_id(char), slack_oauth_client_secret(char), datatable(jsonb), teams_team_guid(text), auto_invite(jsonb), error_handler(jsonb), success_handler(jsonb), public_app_execution_limit_per_minute(int), dbt_warehouses(jsonb), guest_access_enabled(bool), guest_jwt_public_key(text), guest_jwt_jwks_url(text), ai_sessions_backup_generation(int), add_admins_and_developers_to_forks(bool)
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
zombie_job_counter: job_id(uuid), counter(int)
|
||||
FK: (job_id) -> v2_job(id)
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::sync::Notify;
|
||||
use windmill_common::{flows::FlowValue, jobs::JobPayload};
|
||||
use windmill_test_utils::{
|
||||
completed_job, in_test_worker, listen_for_completed_jobs, ApiServer, RunJob, StreamFind,
|
||||
};
|
||||
|
||||
async fn model(Json(body): Json<Value>) -> ([(&'static str, &'static str); 1], String) {
|
||||
let finished = body["messages"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|m| m["role"] == "tool");
|
||||
let delta = if finished {
|
||||
json!({"role":"assistant", "content":"done"})
|
||||
} else {
|
||||
json!({"role":"assistant", "tool_calls": (0..3).map(|i| json!({
|
||||
"index":i, "id":format!("call_{i}"), "type":"function",
|
||||
"function":{"name":format!("tool_{i}"), "arguments":"{}"}
|
||||
})).collect::<Vec<_>>()})
|
||||
};
|
||||
let event = json!({"choices":[{"index":0, "delta":delta, "finish_reason":null}]});
|
||||
let end = json!({"choices":[{"index":0, "delta":{}, "finish_reason":if finished {"stop"} else {"tool_calls"}}]});
|
||||
(
|
||||
[("content-type", "text/event-stream")],
|
||||
format!("data: {event}\n\ndata: {end}\n\ndata: [DONE]\n\n"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn run_batch(db: Pool<Postgres>, parallel: bool, limited: bool) -> anyhow::Result<()> {
|
||||
std::env::set_var("ALLOW_PRIVATE_AI_BASE_URLS", "true");
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
|
||||
let base = format!("http://{}", listener.local_addr()?);
|
||||
let gate = Arc::new(Notify::new());
|
||||
let router = Router::new()
|
||||
.route("/v1/chat/completions", post(model))
|
||||
.route(
|
||||
"/first",
|
||||
get(|State(gate): State<Arc<Notify>>| async move {
|
||||
gate.notified().await;
|
||||
"ok"
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/second",
|
||||
get(|State(gate): State<Arc<Notify>>| async move {
|
||||
gate.notify_one();
|
||||
"ok"
|
||||
}),
|
||||
)
|
||||
.with_state(gate);
|
||||
let stub = tokio::spawn(async move {
|
||||
axum::serve(listener, router).await.unwrap();
|
||||
});
|
||||
let tools: Vec<Value> = (0..3).map(|i| {
|
||||
let wait = if parallel && !limited && i < 2 {
|
||||
format!("await fetch('{base}/{}');", if i == 0 {"first"} else {"second"})
|
||||
} else { String::new() };
|
||||
let finish = if i == 2 { "throw new Error('expected tool failure');".to_string() }
|
||||
else { format!("return {i};") };
|
||||
json!({"id":format!("t{i}"),"summary":format!("tool_{i}"),"value":{
|
||||
"type":"rawscript", "language":"bun", "input_transforms":{},
|
||||
"tag": if parallel { "bun" } else { "unserved-tool-tag" },
|
||||
"concurrent_limit": if limited { Some(1) } else { None },
|
||||
"custom_concurrency_key": if limited { Some("agent-tool-test") } else { None },
|
||||
"content":format!("export async function main() {{ {wait} await Bun.sleep({}); {finish} }}", if i == 0 {200} else {0})
|
||||
}})
|
||||
}).collect();
|
||||
let flow: FlowValue = serde_json::from_value(json!({"modules":[{"id":"agent","value":{
|
||||
"type":"aiagent", "tools":tools, "input_transforms":{
|
||||
"provider":{"type":"static","value":{"kind":"customai","model":"queue-test","resource":{"base_url":format!("{base}/v1")}}},
|
||||
"user_message":{"type":"static","value":"run the tools"},
|
||||
"max_iterations":{"type":"static","value":3}
|
||||
}
|
||||
}}]}))?;
|
||||
let id = RunJob::from(JobPayload::RawFlow {
|
||||
value: flow,
|
||||
path: Some("u/test/agent_queue".into()),
|
||||
restarted_from: None,
|
||||
})
|
||||
.push(&db)
|
||||
.await;
|
||||
let notifications = listen_for_completed_jobs(&db).await;
|
||||
let wait = async {
|
||||
if parallel {
|
||||
in_test_worker(&db, notifications.find(&id), server.addr.port()).await;
|
||||
} else {
|
||||
notifications.find(&id).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(45),
|
||||
in_test_worker(&db, wait, server.addr.port()),
|
||||
)
|
||||
.await?;
|
||||
let result = completed_job(id, &db).await;
|
||||
assert!(result.success, "{:?}", result.result);
|
||||
let result = result.json_result().expect("agent result");
|
||||
let messages: Vec<_> = result["messages"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|m| m["role"] == "tool")
|
||||
.collect();
|
||||
assert_eq!(messages.len(), 3);
|
||||
for (index, message) in messages.iter().enumerate() {
|
||||
assert_eq!(message["tool_call_id"], format!("call_{index}"));
|
||||
}
|
||||
assert_eq!(messages[0]["content"], "0");
|
||||
assert_eq!(messages[1]["content"], "1");
|
||||
assert!(messages[2]["content"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("expected tool failure"));
|
||||
let (parent_id, parent_worker): (uuid::Uuid, String) = sqlx::query_as(
|
||||
"SELECT j.id, c.worker FROM v2_job j JOIN v2_job_completed c USING(id) WHERE j.parent_job = $1"
|
||||
).bind(id).fetch_one(&db).await?;
|
||||
let children: Vec<(String, String, bool)> = sqlx::query_as(
|
||||
"SELECT j.runnable_path, c.worker, c.status = 'success' FROM v2_job j JOIN v2_job_completed c USING(id) WHERE j.parent_job = $1 ORDER BY j.runnable_path"
|
||||
).bind(parent_id).fetch_all(&db).await?;
|
||||
assert_eq!(children.len(), 3);
|
||||
assert_eq!(
|
||||
children[0].1, parent_worker,
|
||||
"first tool must stay on the parent worker"
|
||||
);
|
||||
assert_eq!(
|
||||
children.iter().map(|c| c.2).collect::<Vec<_>>(),
|
||||
vec![true, true, false]
|
||||
);
|
||||
if limited {
|
||||
let overlapping: i64 = sqlx::query_scalar(
|
||||
"SELECT count(*) FROM v2_job j1 JOIN v2_job_completed c1 ON c1.id = j1.id
|
||||
JOIN v2_job j2 ON j2.parent_job = j1.parent_job AND j2.id > j1.id
|
||||
JOIN v2_job_completed c2 ON c2.id = j2.id
|
||||
WHERE j1.parent_job = $1 AND c1.started_at < c2.completed_at AND c2.started_at < c1.completed_at"
|
||||
).bind(parent_id).fetch_one(&db).await?;
|
||||
assert_eq!(
|
||||
overlapping, 0,
|
||||
"the reserved first job must count toward the shared limit"
|
||||
);
|
||||
} else if parallel {
|
||||
assert_ne!(
|
||||
children[1].1, parent_worker,
|
||||
"second tool must unblock the first from another worker"
|
||||
);
|
||||
} else {
|
||||
assert!(children.iter().all(|child| child.1 == parent_worker));
|
||||
}
|
||||
stub.abort();
|
||||
server.close().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[serial_test::serial]
|
||||
async fn parent_drains_tools_without_another_worker(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
run_batch(db, false, false).await
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[serial_test::serial]
|
||||
async fn parent_keeps_first_tool_while_another_worker_runs_siblings(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
run_batch(db, true, false).await
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[serial_test::serial]
|
||||
async fn reserved_tool_obeys_shared_concurrency_limit(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
run_batch(db, true, true).await
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* The job's own token (`$WM_TOKEN`) stays valid well past the job it was minted
|
||||
* for, and job logs are persisted to `job_logs` and optionally to object storage,
|
||||
* so a script that echoes the token would otherwise park a live credential in
|
||||
* durable storage. `run_worker` registers the token with `sensitive_log_masks`
|
||||
* for the job it pulled; this pins that the persisted log carries the masked form.
|
||||
*/
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::{
|
||||
jobs::{JobPayload, RawCode},
|
||||
scripts::ScriptLang,
|
||||
};
|
||||
use windmill_test_utils::*;
|
||||
|
||||
/// Prefix of a serialized job token: `jwt_` plus the base64 of a JWT header.
|
||||
/// The masked form keeps only `jwt` + the last three characters, so it never matches.
|
||||
const RAW_TOKEN_PREFIX: &str = "jwt_ey";
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_job_token_masked_in_persisted_logs(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let job = RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content: "echo \"running with --token $WM_TOKEN\"".to_string(),
|
||||
path: None,
|
||||
lock: None,
|
||||
language: ScriptLang::Bash,
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
modules: None,
|
||||
tag: None,
|
||||
}))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
assert!(job.success, "job should have succeeded");
|
||||
|
||||
let logs =
|
||||
sqlx::query_scalar::<_, Option<String>>("SELECT logs FROM job_logs WHERE job_id = $1")
|
||||
.bind(job.id)
|
||||
.fetch_one(&db)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
assert!(
|
||||
!logs.contains(RAW_TOKEN_PREFIX),
|
||||
"an unmasked job token reached the persisted logs: {logs}"
|
||||
);
|
||||
assert!(
|
||||
logs.contains("secret value was masked"),
|
||||
"expected the masking notice in logs: {logs}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// nativets runs V8 in-process and persists `console.log` output through its own
|
||||
/// channel, so it is masked by a different mechanism than the bash case above and
|
||||
/// needs its own guard.
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_job_token_masked_in_nativets_logs(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let job = RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content: "export async function main() {\n console.log('running with --token ' + process.env.WM_TOKEN);\n return 'ok';\n}".to_string(),
|
||||
path: None,
|
||||
lock: None,
|
||||
language: ScriptLang::Nativets,
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
modules: None,
|
||||
tag: None,
|
||||
}))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
assert!(job.success, "job should have succeeded");
|
||||
|
||||
let logs =
|
||||
sqlx::query_scalar::<_, Option<String>>("SELECT logs FROM job_logs WHERE job_id = $1")
|
||||
.bind(job.id)
|
||||
.fetch_one(&db)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
assert!(
|
||||
!logs.contains(RAW_TOKEN_PREFIX),
|
||||
"an unmasked job token reached the persisted logs: {logs}"
|
||||
);
|
||||
assert!(
|
||||
logs.contains("secret value was masked"),
|
||||
"expected the masking notice in logs: {logs}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
use windmill_test_utils::*;
|
||||
|
||||
/// With `add_admins_and_developers_to_forks` on, a fork starts with the parent's admins and
|
||||
/// developers at their parent role, even when a developer forks it; operators are left out. The
|
||||
/// copies are manual members: a parent membership that came from an instance group must not carry
|
||||
/// that provenance into a fork that does not configure the group.
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
||||
async fn test_fork_adds_parent_admins_and_developers(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let base_url = format!(
|
||||
"http://localhost:{}/api/w/test-workspace/workspaces",
|
||||
server.addr.port()
|
||||
);
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE usr SET operator = true WHERE workspace_id = 'test-workspace' AND username = 'test-user-3'",
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO usr (workspace_id, email, username, is_admin, added_via)
|
||||
VALUES ('test-workspace', 'test4@windmill.dev', 'test-user-4', false,
|
||||
'{\"source\": \"instance_group\", \"group\": \"devs\"}')",
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let resp = client
|
||||
.post(format!(
|
||||
"{base_url}/edit_add_admins_and_developers_to_forks"
|
||||
))
|
||||
.header("Authorization", "Bearer SECRET_TOKEN")
|
||||
.json(&json!({ "add_admins_and_developers_to_forks": true }))
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
resp.status().is_success(),
|
||||
"enabling the setting: {}",
|
||||
resp.text().await?
|
||||
);
|
||||
|
||||
let resp = client
|
||||
.post(format!("{base_url}/create_fork"))
|
||||
.header("Authorization", "Bearer SECRET_TOKEN_2")
|
||||
.json(&json!({ "id": "wm-fork-team", "name": "Team fork" }))
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
resp.status().is_success(),
|
||||
"creating the fork: {}",
|
||||
resp.text().await?
|
||||
);
|
||||
|
||||
let members: Vec<(String, bool, bool)> = sqlx::query_as(
|
||||
"SELECT username, is_admin, added_via IS NULL FROM usr
|
||||
WHERE workspace_id = 'wm-fork-team' ORDER BY username",
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
members,
|
||||
vec![
|
||||
("test-user".to_string(), true, true),
|
||||
("test-user-2".to_string(), false, true),
|
||||
("test-user-4".to_string(), false, true),
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//! The `mcp_disable_token_query_param` switch closes the URL-borne credential path.
|
||||
//!
|
||||
//! The rejection is a middleware layered between the `WWW-Authenticate` decorator and
|
||||
//! everything that reads a token, on both the workspaced and the gateway mount. Each half of
|
||||
//! that sandwich is pinned: the `WWW-Authenticate` header on the refusal catches the layer
|
||||
//! being moved outward (a client would lose the pointer that starts OAuth discovery), and
|
||||
//! refusing a token that was never valid catches it being moved inward past authentication
|
||||
//! (the URL-borne token would be hashed and looked up before anything refused it).
|
||||
#![cfg(feature = "mcp")]
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
use windmill_common::global_settings::MCP_DISABLE_TOKEN_QUERY_PARAM;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
/// Workspace-less with an `mcp:` scope, which is what the gateway mount requires; the
|
||||
/// workspaced mount takes its workspace from the path, so one token reaches both.
|
||||
async fn insert_mcp_token(db: &Pool<Postgres>) -> anyhow::Result<()> {
|
||||
sqlx::query(
|
||||
"INSERT INTO token (token_hash, token_prefix, token, email, label, super_admin, scopes)
|
||||
VALUES (encode(sha256('MCP_TOKEN'::bytea), 'hex'), 'MCP_TOK', 'MCP_TOKEN', 'test@windmill.dev', 'mcp token', true, ARRAY['mcp:all'])",
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A token that is not in `token` at all. Authentication would refuse it on its own, so a
|
||||
/// refusal carrying the middleware's own wording is evidence nothing looked it up first.
|
||||
const BOGUS_TOKEN: &str = "NOT_A_REAL_TOKEN";
|
||||
|
||||
async fn tools_list(url: &str) -> anyhow::Result<reqwest::Response> {
|
||||
Ok(reqwest::Client::new()
|
||||
.post(url)
|
||||
.header("Accept", "application/json, text/event-stream")
|
||||
.json(&json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }))
|
||||
.send()
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
||||
async fn test_mcp_token_query_param_switch(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
set_jwt_secret().await;
|
||||
insert_mcp_token(&db).await?;
|
||||
let server = ApiServer::start_mcp(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
let workspaced =
|
||||
format!("http://localhost:{port}/api/mcp/w/test-workspace/mcp?token=MCP_TOKEN");
|
||||
let gateway = format!("http://localhost:{port}/api/mcp/gateway?token=MCP_TOKEN");
|
||||
|
||||
assert_eq!(
|
||||
tools_list(&workspaced).await?.status(),
|
||||
200,
|
||||
"a URL-borne token is the documented default and must keep working while the switch is off"
|
||||
);
|
||||
assert_eq!(tools_list(&gateway).await?.status(), 200);
|
||||
|
||||
MCP_DISABLE_TOKEN_QUERY_PARAM.store(true, Ordering::Relaxed);
|
||||
|
||||
for url in [&workspaced, &gateway] {
|
||||
let resp = tools_list(url).await?;
|
||||
assert_eq!(
|
||||
resp.status(),
|
||||
401,
|
||||
"{url} still admitted a token in the URL"
|
||||
);
|
||||
// What sends the client into the OAuth flow rather than leaving it stuck on a 401.
|
||||
assert!(
|
||||
resp.headers().contains_key("www-authenticate"),
|
||||
"{url} rejected without pointing at the authorization server"
|
||||
);
|
||||
}
|
||||
|
||||
// Refused before authentication, not after: an invalid token gets the middleware's own
|
||||
// message rather than the generic 401 that looking it up would produce.
|
||||
let resp = tools_list(&format!(
|
||||
"http://localhost:{port}/api/mcp/w/test-workspace/mcp?token={BOGUS_TOKEN}"
|
||||
))
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 401);
|
||||
assert!(
|
||||
resp.text().await?.contains("does not accept a token in the MCP URL"),
|
||||
"an invalid URL token was answered by authentication, so the token was read before \
|
||||
the switch refused it"
|
||||
);
|
||||
|
||||
// The header stays open: it is the channel the OAuth flow itself hands tokens over on.
|
||||
let resp = reqwest::Client::new()
|
||||
.post(format!("http://localhost:{port}/api/mcp/gateway"))
|
||||
.header("Accept", "application/json, text/event-stream")
|
||||
.header("Authorization", "Bearer MCP_TOKEN")
|
||||
.json(&json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 200);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -275,6 +275,67 @@ async fn test_encryption_key_rotation_dispatches_batched_git_sync(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stored repository tokens and webhook secrets are encrypted under the
|
||||
/// workspace key but never synced, so a rotation has to carry them over even
|
||||
/// when the caller skips re-encrypting variables.
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
||||
async fn test_encryption_key_rotation_reencrypts_git_sync_secrets(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
use windmill_common::variables::{build_crypt, crypt_from_key_with_suffix, decrypt, encrypt};
|
||||
initialize_tracing().await;
|
||||
|
||||
create_folder(&db, "28103").await?;
|
||||
create_git_repo_resource(&db).await?;
|
||||
let sync_script_path = "f/28103/test_sync_script_git_secrets";
|
||||
create_sync_script(&db, sync_script_path).await?;
|
||||
setup_git_sync_config(&db, sync_script_path).await?;
|
||||
|
||||
let mc = build_crypt(&db, "test-workspace").await?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE workspace_settings SET
|
||||
git_credentials = jsonb_build_array(jsonb_build_object(
|
||||
'token', $1::text, 'repo_identity', 'https://gitlab.example.com/grp/proj')),
|
||||
git_sync = jsonb_set(git_sync, '{repositories,0,auto_pull}', jsonb_build_object(
|
||||
'enabled', true, 'mode', 'webhook', 'webhook_id', 1, 'webhook_secret', $2::text))
|
||||
WHERE workspace_id = 'test-workspace'
|
||||
"#,
|
||||
)
|
||||
.bind(encrypt(&mc, "stored-token"))
|
||||
.bind(encrypt(&mc, "hook-secret"))
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
let base = format!("http://localhost:{port}/api/w/test-workspace/workspaces");
|
||||
|
||||
let new_key = "c".repeat(64);
|
||||
let resp = authed(client().post(format!("{base}/encryption_key")))
|
||||
.json(&json!({"new_key": new_key, "skip_reencrypt": true}))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(
|
||||
resp.status(),
|
||||
200,
|
||||
"set_encryption_key failed: {}",
|
||||
resp.text().await?
|
||||
);
|
||||
|
||||
let (token, secret): (String, String) = sqlx::query_as(
|
||||
"SELECT git_credentials->0->>'token', git_sync#>>'{repositories,0,auto_pull,webhook_secret}'
|
||||
FROM workspace_settings WHERE workspace_id = 'test-workspace'",
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
let new_mc = crypt_from_key_with_suffix(&new_key, "");
|
||||
assert_eq!(decrypt(&new_mc, token)?, "stored-token");
|
||||
assert_eq!(decrypt(&new_mc, secret)?, "hook-secret");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for the non-debouncing fallback: a workspace whose sync
|
||||
/// script predates hub version 28103 must still receive git-sync jobs for the
|
||||
/// encryption_key entry and every re-encrypted secret. Before the fallback was
|
||||
|
||||
@@ -64,10 +64,11 @@ use windmill_common::{
|
||||
GITHUB_APP_WEBHOOK_BASE_URL_SETTING, HTTP_ROUTE_DEFAULT_ALLOWED_ORIGINS_SETTING,
|
||||
HTTP_ROUTE_WORKSPACED_ROUTE_SETTING, HUB_ACCESSIBLE_URL_SETTING, HUB_BASE_URL_SETTING,
|
||||
INSTANCE_BANNER_SETTING, MAX_RETENTION_OVERRIDE_WORKSPACES,
|
||||
MAX_TOKEN_EXPIRATION_DAYS_SETTING, RETENTION_PERIOD_SECS_OVERRIDES_SETTING,
|
||||
RUFF_CONFIG_SETTING, UNIQUE_ID_SETTING, WORKSPACE_FAIRNESS_DURATION_SECS_SETTING,
|
||||
WORKSPACE_FAIRNESS_ENABLED_SETTING, WORKSPACE_FAIRNESS_MAX_PERCENT_SETTING,
|
||||
WORKSPACE_FAIRNESS_MIN_TOTAL_SETTING, WS_BASE_URL_SETTING,
|
||||
MAX_TOKEN_EXPIRATION_DAYS_SETTING, MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING,
|
||||
RETENTION_PERIOD_SECS_OVERRIDES_SETTING, RUFF_CONFIG_SETTING, UNIQUE_ID_SETTING,
|
||||
WORKSPACE_FAIRNESS_DURATION_SECS_SETTING, WORKSPACE_FAIRNESS_ENABLED_SETTING,
|
||||
WORKSPACE_FAIRNESS_MAX_PERCENT_SETTING, WORKSPACE_FAIRNESS_MIN_TOTAL_SETTING,
|
||||
WS_BASE_URL_SETTING,
|
||||
},
|
||||
instance_config::{self, ApplyMode, InstanceConfig},
|
||||
server::Smtp,
|
||||
@@ -1364,6 +1365,10 @@ pub async fn get_global_setting(
|
||||
&& key != INSTANCE_BANNER_SETTING
|
||||
// The token form reads it to stop offering expirations the server would shorten.
|
||||
&& key != MAX_TOKEN_EXPIRATION_DAYS_SETTING
|
||||
// Whoever is wiring up an MCP client reads it to know whether a URL-borne token
|
||||
// would be refused, and they are usually not a superadmin. Not a secret: pointing
|
||||
// any MCP client at the instance discovers the same answer.
|
||||
&& key != MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING
|
||||
{
|
||||
require_super_admin(&db, &authed).await?;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ use windmill_common::global_settings::HTTP_ROUTE_WORKSPACED_ROUTE;
|
||||
use windmill_common::query_builders::{render_db_quoted_identifier, DbType};
|
||||
use windmill_common::users::username_to_permissioned_as;
|
||||
use windmill_common::variables::{
|
||||
build_crypt, decrypt, encrypt, SECRET_SALT, WORKSPACE_CRYPT_CACHE,
|
||||
crypt_from_key_with_suffix, decrypt, encrypt, WORKSPACE_CRYPT_CACHE,
|
||||
};
|
||||
use windmill_common::worker::{to_raw_value, CLOUD_HOSTED};
|
||||
use windmill_common::workspaces::GitRepositorySettings;
|
||||
@@ -155,6 +155,10 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/edit_deploy_ui_config", post(edit_deploy_ui_config))
|
||||
.route("/edit_default_app", post(edit_default_app))
|
||||
.route("/edit_guest_access", post(edit_guest_access))
|
||||
.route(
|
||||
"/edit_add_admins_and_developers_to_forks",
|
||||
post(edit_add_admins_and_developers_to_forks),
|
||||
)
|
||||
.route("/edit_guest_jwt_key", post(edit_guest_jwt_key))
|
||||
.route("/guest_usage", get(get_guest_usage))
|
||||
.route("/default_app", get(get_default_app))
|
||||
@@ -338,6 +342,7 @@ pub struct WorkspaceSettings {
|
||||
pub guest_jwt_public_key: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub guest_jwt_jwks_url: Option<String>,
|
||||
pub add_admins_and_developers_to_forks: bool,
|
||||
}
|
||||
|
||||
/// Subset of `WorkspaceSettings` that is safe to return to any workspace
|
||||
@@ -363,6 +368,8 @@ pub struct WorkspacePublicSettings {
|
||||
/// Not sensitive, and the app editor needs it to say whether the guest rung is
|
||||
/// live -- an app can be set to `guest` while the workspace has guests off.
|
||||
pub guest_access_enabled: bool,
|
||||
/// Read by the fork dialog, which tells the forker who else the fork will include.
|
||||
pub add_admins_and_developers_to_forks: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deploy_ui: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -1126,7 +1133,8 @@ async fn get_settings(
|
||||
error_handler_fallback_to_instance_alerts,
|
||||
guest_access_enabled,
|
||||
guest_jwt_public_key,
|
||||
guest_jwt_jwks_url
|
||||
guest_jwt_jwks_url,
|
||||
add_admins_and_developers_to_forks
|
||||
FROM
|
||||
workspace_settings
|
||||
WHERE
|
||||
@@ -1168,6 +1176,7 @@ async fn get_public_settings(
|
||||
teams_team_guid,
|
||||
mute_critical_alerts,
|
||||
guest_access_enabled,
|
||||
add_admins_and_developers_to_forks,
|
||||
deploy_ui,
|
||||
large_file_storage,
|
||||
datatable
|
||||
@@ -5052,6 +5061,47 @@ async fn edit_guest_access(
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditAddAdminsAndDevelopersToForks {
|
||||
add_admins_and_developers_to_forks: bool,
|
||||
}
|
||||
|
||||
async fn edit_add_admins_and_developers_to_forks(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(EditAddAdminsAndDevelopersToForks { add_admins_and_developers_to_forks }): Json<
|
||||
EditAddAdminsAndDevelopersToForks,
|
||||
>,
|
||||
) -> Result<String> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET add_admins_and_developers_to_forks = $1 WHERE workspace_id = $2",
|
||||
add_admins_and_developers_to_forks,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"workspaces.edit_add_admins_and_developers_to_forks",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&add_admins_and_developers_to_forks.to_string()),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!(
|
||||
"Adding admins and developers to new forks set to {add_admins_and_developers_to_forks} for workspace {w_id}"
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditGuestJwtKey {
|
||||
/// A PEM public key (RS or ES family), or a JWKS URL, at most one. Both empty clears the
|
||||
@@ -5640,9 +5690,6 @@ async fn set_encryption_key(
|
||||
));
|
||||
}
|
||||
|
||||
// Build the previous cipher before the transaction (reads from cache/pool)
|
||||
let previous_encryption_key = build_crypt(&db, w_id.as_str()).await?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
// Under the row's lock, so two rotations racing serialize and each sees the key the
|
||||
@@ -5676,17 +5723,14 @@ async fn set_encryption_key(
|
||||
None
|
||||
};
|
||||
|
||||
// From the keys read and written under the lock, never from `build_crypt`: its
|
||||
// cache can still hold a key an earlier rotation replaced, and the git-sync
|
||||
// secrets below are skipped rather than failed when they do not decrypt.
|
||||
let previous_encryption_key = crypt_from_key_with_suffix(&previous_key, "");
|
||||
let new_encryption_key = crypt_from_key_with_suffix(&request.new_key, "");
|
||||
|
||||
let mut reencrypted_secret_paths: Vec<String> = Vec::new();
|
||||
if !request.skip_reencrypt.unwrap_or(false) {
|
||||
// Build the new cipher directly from the key string, since the transaction
|
||||
// hasn't committed yet and build_crypt() would read the old key from the pool.
|
||||
let crypt_key = if let Some(ref salt) = SECRET_SALT.as_ref() {
|
||||
format!("{}{}", request.new_key, salt)
|
||||
} else {
|
||||
request.new_key.clone()
|
||||
};
|
||||
let new_encryption_key = magic_crypt::new_magic_crypt!(crypt_key, 256);
|
||||
|
||||
let mut truncated_new_key = request.new_key.clone();
|
||||
truncated_new_key.truncate(8);
|
||||
tracing::warn!(
|
||||
@@ -5726,6 +5770,14 @@ async fn set_encryption_key(
|
||||
}
|
||||
}
|
||||
|
||||
reencrypt_git_sync_secrets(
|
||||
&mut tx,
|
||||
&w_id,
|
||||
&previous_encryption_key,
|
||||
&new_encryption_key,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
// Invalidate the cache only after the transaction has committed
|
||||
@@ -5763,6 +5815,64 @@ async fn set_encryption_key(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Move the git-sync secrets the server keeps under the workspace key (stored
|
||||
/// repository tokens, webhook secrets) to the new key. They are never synced, so
|
||||
/// unlike variables they are still under the old key when the caller skips
|
||||
/// re-encryption.
|
||||
async fn reencrypt_git_sync_secrets(
|
||||
conn: &mut sqlx::PgConnection,
|
||||
w_id: &str,
|
||||
old: &magic_crypt::MagicCrypt256,
|
||||
new: &magic_crypt::MagicCrypt256,
|
||||
) -> Result<()> {
|
||||
let Some((mut credentials, mut git_sync)) =
|
||||
sqlx::query_as::<_, (serde_json::Value, Option<serde_json::Value>)>(
|
||||
"SELECT git_credentials, git_sync FROM workspace_settings WHERE workspace_id = $1 FOR UPDATE",
|
||||
)
|
||||
.bind(w_id)
|
||||
.fetch_optional(&mut *conn)
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let reencrypt = |value: &mut serde_json::Value| {
|
||||
let Some(ciphertext) = value.as_str() else {
|
||||
return;
|
||||
};
|
||||
match decrypt(old, ciphertext.to_string()) {
|
||||
Ok(plain) => *value = serde_json::Value::String(encrypt(new, &plain)),
|
||||
// Left by an earlier rotation and unrecoverable either way; failing here
|
||||
// would block every later rotation of the workspace.
|
||||
Err(e) => tracing::warn!(
|
||||
"a git-sync secret of workspace {w_id} does not decrypt under its current key, leaving it as is: {e}"
|
||||
),
|
||||
}
|
||||
};
|
||||
for entry in credentials.as_array_mut().into_iter().flatten() {
|
||||
if let Some(token) = entry.get_mut("token") {
|
||||
reencrypt(token);
|
||||
}
|
||||
}
|
||||
let repositories = git_sync
|
||||
.as_mut()
|
||||
.and_then(|g| g.get_mut("repositories"))
|
||||
.and_then(|r| r.as_array_mut());
|
||||
for repo in repositories.into_iter().flatten() {
|
||||
if let Some(secret) = repo.pointer_mut("/auto_pull/webhook_secret") {
|
||||
reencrypt(secret);
|
||||
}
|
||||
}
|
||||
sqlx::query(
|
||||
"UPDATE workspace_settings SET git_credentials = $2, git_sync = $3 WHERE workspace_id = $1",
|
||||
)
|
||||
.bind(w_id)
|
||||
.bind(credentials)
|
||||
.bind(git_sync)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UsedTriggers {
|
||||
pub websocket_used: bool,
|
||||
@@ -6712,7 +6822,8 @@ async fn update_workspace_settings(
|
||||
ducklake = source_ws.ducklake,
|
||||
dbt_warehouses = source_ws.dbt_warehouses,
|
||||
datatable = source_ws.datatable,
|
||||
git_app_installations = source_ws.git_app_installations
|
||||
git_app_installations = source_ws.git_app_installations,
|
||||
add_admins_and_developers_to_forks = source_ws.add_admins_and_developers_to_forks
|
||||
FROM workspace_settings source_ws
|
||||
WHERE source_ws.workspace_id = $1
|
||||
AND workspace_settings.workspace_id = $2
|
||||
@@ -6853,14 +6964,21 @@ async fn copy_workspace_members(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
source_workspace_id: &str,
|
||||
target_workspace_id: &str,
|
||||
admins_and_developers_only: bool,
|
||||
) -> Result<()> {
|
||||
// Admins and developers join as manual members: the fork does not inherit the source's
|
||||
// instance-group config, so a copied `instance_group` provenance would let the fork's
|
||||
// reconciliation delete them and their data.
|
||||
sqlx::query!(
|
||||
"INSERT INTO usr (workspace_id, username, email, is_admin, created_at, operator, disabled, role, is_service_account, added_via)
|
||||
SELECT $1, username, email, is_admin, created_at, operator, disabled, role, is_service_account, added_via
|
||||
SELECT $1, username, email, is_admin, created_at, operator, disabled, role, is_service_account,
|
||||
CASE WHEN $3 THEN NULL ELSE added_via END
|
||||
FROM usr WHERE workspace_id = $2
|
||||
AND (NOT $3 OR (NOT operator AND NOT disabled AND NOT is_service_account))
|
||||
ON CONFLICT DO NOTHING",
|
||||
target_workspace_id,
|
||||
source_workspace_id,
|
||||
admins_and_developers_only,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
@@ -8651,8 +8769,19 @@ async fn create_workspace_fork(
|
||||
// intended. Dev creation is already admin-gated, so this is transitively admin-only too. Done before
|
||||
// the explicit creator insert below so the creator (a parent member) is copied with full metadata
|
||||
// (operator/role/is_service_account/added_via), not the bare row the insert alone would leave.
|
||||
// Independently, the parent's admins can have every fork of it start with its admins and
|
||||
// developers; the forker cannot opt out, since the point is that those admins can review it.
|
||||
if nw.copy_members && nw.is_dev_workspace {
|
||||
copy_workspace_members(&mut tx, &parent_workspace_id, &forked_id).await?;
|
||||
copy_workspace_members(&mut tx, &parent_workspace_id, &forked_id, false).await?;
|
||||
} else if sqlx::query_scalar!(
|
||||
"SELECT add_admins_and_developers_to_forks FROM workspace_settings WHERE workspace_id = $1",
|
||||
parent_workspace_id
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.unwrap_or(false)
|
||||
{
|
||||
copy_workspace_members(&mut tx, &parent_workspace_id, &forked_id, true).await?;
|
||||
}
|
||||
|
||||
// Ensure the creator is a member of the fork even without copy_members (or if they aren't a parent
|
||||
|
||||
@@ -113,7 +113,7 @@ pub(crate) async fn change_workspace_id(
|
||||
// Duplicate workspace settings (keep copy in old workspace for reference)
|
||||
info!("Duplicating workspace_settings table");
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace_settings (workspace_id, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url) SELECT $1, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url FROM workspace_settings WHERE workspace_id = $2",
|
||||
"INSERT INTO workspace_settings (workspace_id, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url, add_admins_and_developers_to_forks) SELECT $1, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url, add_admins_and_developers_to_forks FROM workspace_settings WHERE workspace_id = $2",
|
||||
&rw.new_id,
|
||||
&old_id
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.814.0
|
||||
version: 1.815.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -4090,9 +4090,13 @@ paths:
|
||||
guest_access_enabled:
|
||||
type: boolean
|
||||
description: Whether this workspace admits guest sessions. An app's own `guest` execution mode is inert while this is false.
|
||||
add_admins_and_developers_to_forks:
|
||||
type: boolean
|
||||
description: Whether every new fork of this workspace starts with its admins and developers as members, keeping their role.
|
||||
required:
|
||||
- workspace_id
|
||||
- guest_access_enabled
|
||||
- add_admins_and_developers_to_forks
|
||||
|
||||
/w/{workspace}/workspaces/get_settings:
|
||||
get:
|
||||
@@ -4183,6 +4187,9 @@ paths:
|
||||
guest_jwt_jwks_url:
|
||||
type: string
|
||||
description: JWKS URL a guest JWT (`jwt_guest_`) is verified against for this workspace. Mutually exclusive with `guest_jwt_public_key`.
|
||||
add_admins_and_developers_to_forks:
|
||||
type: boolean
|
||||
description: Whether every new fork of this workspace starts with its admins and developers as members, keeping their role.
|
||||
|
||||
/w/{workspace}/workspaces/get_deploy_to:
|
||||
get:
|
||||
@@ -6313,6 +6320,39 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/edit_add_admins_and_developers_to_forks:
|
||||
post:
|
||||
summary: choose whether new forks of this workspace start with its admins and developers
|
||||
description: >-
|
||||
When on, every fork created from this workspace gets the workspace's admins and
|
||||
developers as members, with the role they hold here; operators, disabled users and
|
||||
service accounts are left out. The setting is copied into each fork, so forks of a
|
||||
fork follow it too. Off by default. Workspace-admin gated.
|
||||
operationId: editAddAdminsAndDevelopersToForks
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: Whether new forks start with this workspace's admins and developers
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
add_admins_and_developers_to_forks:
|
||||
type: boolean
|
||||
required:
|
||||
- add_admins_and_developers_to_forks
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/edit_guest_jwt_key:
|
||||
post:
|
||||
summary: set the key guest JWTs are verified against for this workspace
|
||||
|
||||
@@ -560,7 +560,7 @@ pub async fn run_server(
|
||||
if server_mode || mcp_mode {
|
||||
use mcp::{
|
||||
add_www_authenticate_header, add_www_authenticate_header_gateway,
|
||||
extract_workspace_from_token,
|
||||
extract_workspace_from_token, reject_token_query_param,
|
||||
};
|
||||
let (mcp_router, mcp_cancellation_token) = setup_mcp_server(
|
||||
db.clone(),
|
||||
@@ -573,15 +573,17 @@ pub async fn run_server(
|
||||
let workspaced_mcp_router = mcp_router
|
||||
.clone()
|
||||
.route_layer(from_extractor::<ApiAuthed>())
|
||||
.layer(axum::middleware::from_fn(reject_token_query_param))
|
||||
.layer(axum::middleware::from_fn(add_www_authenticate_header))
|
||||
.layer(axum::middleware::from_fn(extract_and_store_workspace_id));
|
||||
// Gateway MCP router — resolves workspace from token
|
||||
let gateway_mcp_router = mcp_router
|
||||
.route_layer(from_extractor::<ApiAuthed>())
|
||||
.layer(axum::middleware::from_fn(extract_workspace_from_token))
|
||||
.layer(axum::middleware::from_fn(reject_token_query_param))
|
||||
.layer(axum::middleware::from_fn(
|
||||
add_www_authenticate_header_gateway,
|
||||
))
|
||||
.layer(axum::middleware::from_fn(extract_workspace_from_token));
|
||||
));
|
||||
(
|
||||
workspaced_mcp_router,
|
||||
gateway_mcp_router,
|
||||
|
||||
@@ -43,11 +43,14 @@ use axum::{
|
||||
extract::{Extension, Path},
|
||||
http::Request,
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use windmill_common::{auth::hash_token, db::GatewayWorkspaceId, error::JsonResult};
|
||||
use windmill_common::{
|
||||
auth::hash_token, db::GatewayWorkspaceId, error::JsonResult,
|
||||
global_settings::MCP_DISABLE_TOKEN_QUERY_PARAM,
|
||||
};
|
||||
|
||||
// McpAuth impl for ApiAuthed is in windmill-api-auth (same crate as the type)
|
||||
|
||||
@@ -446,6 +449,29 @@ pub async fn add_www_authenticate_header(
|
||||
}
|
||||
}
|
||||
|
||||
/// Middleware refusing a credential carried in the MCP URL once the instance sets
|
||||
/// `mcp_disable_token_query_param`. Sits outside everything that reads the token, so neither
|
||||
/// the gateway lookup nor `ApiAuthed` ever sees it, and inside the `WWW-Authenticate` layer,
|
||||
/// whose header is what sends the client into the OAuth flow instead. Refused rather than
|
||||
/// ignored: the URL leaked the token whether or not the request used it.
|
||||
pub async fn reject_token_query_param(request: Request<axum::body::Body>, next: Next) -> Response {
|
||||
let carries_token = MCP_DISABLE_TOKEN_QUERY_PARAM.load(std::sync::atomic::Ordering::Relaxed)
|
||||
&& request
|
||||
.uri()
|
||||
.query()
|
||||
.is_some_and(|q| url::form_urlencoded::parse(q.as_bytes()).any(|(k, _)| k == "token"));
|
||||
if carries_token {
|
||||
return (
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"This instance does not accept a token in the MCP URL. Remove the token query \
|
||||
parameter and let your client sign in through OAuth, or send the token in an \
|
||||
Authorization header.",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
/// Extract the bearer token from either the `Authorization` header or the
|
||||
/// `?token=` query parameter (MCP clients commonly pass it in the URL).
|
||||
fn extract_gateway_token(request: &Request<axum::body::Body>) -> Option<String> {
|
||||
|
||||
@@ -12,5 +12,5 @@ pub mod oauth_server;
|
||||
pub use core::{
|
||||
add_www_authenticate_header, add_www_authenticate_header_gateway,
|
||||
extract_and_store_workspace_id, extract_workspace_from_token, list_tools_service,
|
||||
setup_mcp_server,
|
||||
reject_token_query_param, setup_mcp_server,
|
||||
};
|
||||
|
||||
@@ -102,6 +102,10 @@ pub const HUB_API_SECRET_SETTING: &str = "hub_api_secret";
|
||||
pub const AUTOMATE_USERNAME_CREATION_SETTING: &str = "automate_username_creation";
|
||||
pub const DISABLE_WORKSPACE_INVITE_EMAILS_SETTING: &str = "disable_workspace_invite_emails";
|
||||
pub const DISABLE_PASSWORD_LOGIN_SETTING: &str = "disable_password_login";
|
||||
/// Refuse `?token=` on the MCP endpoints, leaving the `Authorization` header as the only way
|
||||
/// in. A URL-borne credential ends up in browser history, proxy logs and referrers, so an
|
||||
/// instance that cares sends MCP clients through the OAuth flow instead.
|
||||
pub const MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING: &str = "mcp_disable_token_query_param";
|
||||
/// Ceiling, in days, on how far ahead a token minted through `POST /users/tokens/create` or
|
||||
/// `POST /users/tokens/impersonate` may expire; a request asking for more, or for no
|
||||
/// expiration at all, is shortened to it rather than refused. On those routes only: server-side
|
||||
@@ -407,6 +411,7 @@ use std::sync::atomic::AtomicBool;
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref HTTP_ROUTE_WORKSPACED_ROUTE: AtomicBool = AtomicBool::new(false);
|
||||
pub static ref DISABLE_PASSWORD_LOGIN: AtomicBool = AtomicBool::new(false);
|
||||
pub static ref MCP_DISABLE_TOKEN_QUERY_PARAM: AtomicBool = AtomicBool::new(false);
|
||||
/// Origins HTTP routes allow cross-origin when they configure none of their
|
||||
/// own. Empty means unset, which keeps the historical `*`.
|
||||
pub static ref HTTP_ROUTE_DEFAULT_ALLOWED_ORIGINS: arc_swap::ArcSwap<Vec<String>> =
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::RwLock;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Minimum length for a secret to be registered for masking.
|
||||
@@ -20,9 +20,27 @@ const MIN_SECRET_LENGTH: usize = 8;
|
||||
const MASKED_NOTICE: &str =
|
||||
"[windmill] secret value was masked for security reasons, use string transformations to display full value";
|
||||
|
||||
/// The secrets registered for one job, plus the automaton compiled from them.
|
||||
#[derive(Default)]
|
||||
struct JobMasks {
|
||||
secrets: HashSet<String>,
|
||||
/// Built on the first `snapshot` after a change and shared by every later
|
||||
/// snapshot. Every job registers at least its own token, so without this
|
||||
/// cache each log batch of each job would rebuild the automaton.
|
||||
compiled: Option<Arc<CompiledMasks>>,
|
||||
}
|
||||
|
||||
/// Aho-Corasick automaton for O(m) multi-pattern matching in a single pass,
|
||||
/// regardless of the number of secrets registered, with the replacement
|
||||
/// strings indexed to match the automaton's pattern order.
|
||||
struct CompiledMasks {
|
||||
ac: aho_corasick::AhoCorasick,
|
||||
replacements: Vec<String>,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
/// Map of job_id -> set of secret values that should be masked in that job's logs.
|
||||
static ref SENSITIVE_MASKS: RwLock<HashMap<Uuid, HashSet<String>>> =
|
||||
/// Map of job_id -> secret values that should be masked in that job's logs.
|
||||
static ref SENSITIVE_MASKS: RwLock<HashMap<Uuid, JobMasks>> =
|
||||
RwLock::new(HashMap::new());
|
||||
|
||||
/// Set of currently running job IDs on this worker process.
|
||||
@@ -32,13 +50,8 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
|
||||
/// A lock-free snapshot of secrets for a job, taken once per log batch.
|
||||
/// Uses Aho-Corasick for O(m) multi-pattern matching in a single pass,
|
||||
/// regardless of the number of secrets registered.
|
||||
pub struct MaskSnapshot {
|
||||
/// Aho-Corasick automaton for fast matching.
|
||||
ac: aho_corasick::AhoCorasick,
|
||||
/// Replacement strings, indexed to match the automaton's pattern order.
|
||||
replacements: Vec<String>,
|
||||
compiled: Arc<CompiledMasks>,
|
||||
/// Whether the security notice has already been appended for this snapshot.
|
||||
/// Tracked locally to avoid a global write lock on every masked line.
|
||||
notice_shown: std::cell::Cell<bool>,
|
||||
@@ -53,34 +66,104 @@ impl MaskSnapshot {
|
||||
}
|
||||
|
||||
// Single-pass check + replace using the pre-built automaton
|
||||
if !self.ac.is_match(text) {
|
||||
if !self.compiled.ac.is_match(text) {
|
||||
return Cow::Borrowed(text);
|
||||
}
|
||||
|
||||
let mut result = self.ac.replace_all(text, &self.replacements);
|
||||
let mut result = self
|
||||
.compiled
|
||||
.ac
|
||||
.replace_all(text, &self.compiled.replacements);
|
||||
|
||||
// Append the notice only once per snapshot (i.e. per batch)
|
||||
// Append the notice only once per snapshot (i.e. per batch), as its own line.
|
||||
// Callers pass either a bare line (`handle_child`) or a chunk that already ends
|
||||
// in a newline (nativets), and the sinks concatenate what they get verbatim:
|
||||
// assuming either shape welds the notice onto a neighbouring line.
|
||||
if !self.notice_shown.get() {
|
||||
self.notice_shown.set(true);
|
||||
result.push('\n');
|
||||
result.push_str(MASKED_NOTICE);
|
||||
if result.ends_with('\n') {
|
||||
result.push_str(MASKED_NOTICE);
|
||||
result.push('\n');
|
||||
} else {
|
||||
result.push('\n');
|
||||
result.push_str(MASKED_NOTICE);
|
||||
}
|
||||
}
|
||||
|
||||
Cow::Owned(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// A masker for sinks that mask line by line rather than in batches, like nativets
|
||||
/// masking each `console.log` chunk as V8 produces it. `snapshot` per line would
|
||||
/// re-arm the security notice on every one; this keeps it to once per distinct set
|
||||
/// of secrets while still picking up secrets registered mid-run.
|
||||
///
|
||||
/// Masks by job id alone — the caller is the one that knows the text it passes
|
||||
/// belongs to that job.
|
||||
pub struct JobMasker {
|
||||
job_id: Uuid,
|
||||
snapshot: Option<MaskSnapshot>,
|
||||
}
|
||||
|
||||
impl JobMasker {
|
||||
pub fn new(job_id: Uuid) -> Self {
|
||||
JobMasker { job_id, snapshot: snapshot(&job_id) }
|
||||
}
|
||||
|
||||
/// Mask every secret registered for the job. Returns `Cow::Borrowed` when no match.
|
||||
/// Falls back to the masks it last saw once the job is unregistered, so a sink
|
||||
/// still draining past the end of a run does not start emitting secrets.
|
||||
pub fn mask<'a>(&mut self, text: &'a str) -> Cow<'a, str> {
|
||||
if let Some(fresh) = snapshot(&self.job_id) {
|
||||
// Replacing an equivalent snapshot would re-arm the notice, so only take
|
||||
// one built from a secret set we have not seen.
|
||||
let unchanged = self
|
||||
.snapshot
|
||||
.as_ref()
|
||||
.is_some_and(|cur| Arc::ptr_eq(&cur.compiled, &fresh.compiled));
|
||||
if !unchanged {
|
||||
self.snapshot = Some(fresh);
|
||||
}
|
||||
}
|
||||
match self.snapshot.as_ref() {
|
||||
Some(snapshot) => snapshot.mask(text),
|
||||
None => Cow::Borrowed(text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Take a snapshot of the current secrets for a job. Returns `None` if no secrets
|
||||
/// are registered (the caller can then skip masking entirely for the whole batch).
|
||||
///
|
||||
/// Call this once per log batch in `write_lines`, not per line.
|
||||
pub fn snapshot(job_id: &Uuid) -> Option<MaskSnapshot> {
|
||||
let masks = SENSITIVE_MASKS.read().unwrap_or_else(|e| e.into_inner());
|
||||
let secrets = masks.get(job_id)?;
|
||||
if secrets.is_empty() {
|
||||
return None;
|
||||
{
|
||||
let masks = SENSITIVE_MASKS.read().unwrap_or_else(|e| e.into_inner());
|
||||
let job = masks.get(job_id)?;
|
||||
if job.secrets.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Some(compiled) = job.compiled.as_ref() {
|
||||
return Some(MaskSnapshot {
|
||||
compiled: compiled.clone(),
|
||||
notice_shown: std::cell::Cell::new(false),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut masks = SENSITIVE_MASKS.write().unwrap_or_else(|e| e.into_inner());
|
||||
let job = masks.get_mut(job_id)?;
|
||||
if job.secrets.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let compiled = job
|
||||
.compiled
|
||||
.get_or_insert_with(|| Arc::new(compile(&job.secrets)));
|
||||
Some(MaskSnapshot { compiled: compiled.clone(), notice_shown: std::cell::Cell::new(false) })
|
||||
}
|
||||
|
||||
fn compile(secrets: &HashSet<String>) -> CompiledMasks {
|
||||
// Sort longest-first so longer secrets are matched before shorter substrings
|
||||
let mut sorted: Vec<&String> = secrets.iter().collect();
|
||||
sorted.sort_by(|a, b| b.len().cmp(&a.len()));
|
||||
@@ -106,7 +189,7 @@ pub fn snapshot(job_id: &Uuid) -> Option<MaskSnapshot> {
|
||||
.build(sorted.iter().map(|s| s.as_str()))
|
||||
.expect("failed to build aho-corasick automaton");
|
||||
|
||||
Some(MaskSnapshot { ac, replacements, notice_shown: std::cell::Cell::new(false) })
|
||||
CompiledMasks { ac, replacements }
|
||||
}
|
||||
|
||||
/// Register a job as currently running. Call this before `handle_queued_job`.
|
||||
@@ -148,20 +231,110 @@ pub fn register_secret_for_all_running_jobs(secret: &str) {
|
||||
|
||||
let mut masks = SENSITIVE_MASKS.write().unwrap_or_else(|e| e.into_inner());
|
||||
for job_id in job_ids {
|
||||
if let Some(set) = masks.get_mut(&job_id) {
|
||||
set.insert(secret.to_string());
|
||||
if let Some(job) = masks.get_mut(&job_id) {
|
||||
if job.secrets.insert(secret.to_string()) {
|
||||
job.compiled = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a secret value for a specific job.
|
||||
/// Used for `$encrypted:` args where we know the job ID.
|
||||
/// Used for the job's own token and for `$encrypted:` args, where we know the job ID.
|
||||
pub fn register_secret_for_job(job_id: Uuid, secret: &str) {
|
||||
if secret.len() < MIN_SECRET_LENGTH {
|
||||
return;
|
||||
}
|
||||
let mut masks = SENSITIVE_MASKS.write().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(set) = masks.get_mut(&job_id) {
|
||||
set.insert(secret.to_string());
|
||||
if let Some(job) = masks.get_mut(&job_id) {
|
||||
if job.secrets.insert(secret.to_string()) {
|
||||
job.compiled = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The compiled automaton is cached per job, so a secret registered after the
|
||||
/// first snapshot only gets masked if the cache is invalidated.
|
||||
#[test]
|
||||
fn snapshot_rebuilds_after_a_new_secret_is_registered() {
|
||||
let job_id = Uuid::new_v4();
|
||||
register_running_job(job_id);
|
||||
register_secret_for_job(job_id, "firstsecretvalue");
|
||||
let _ = snapshot(&job_id)
|
||||
.expect("secret registered")
|
||||
.mask("firstsecretvalue");
|
||||
|
||||
register_secret_for_job(job_id, "secondsecretvalue");
|
||||
|
||||
let snap = snapshot(&job_id).expect("secrets registered");
|
||||
let masked = snap.mask("firstsecretvalue then secondsecretvalue");
|
||||
assert!(!masked.contains("firstsecretvalue"), "{masked}");
|
||||
assert!(!masked.contains("secondsecretvalue"), "{masked}");
|
||||
unregister_running_job(job_id);
|
||||
}
|
||||
|
||||
/// A line-by-line sink must not repeat the notice on every line, and must still
|
||||
/// pick up a secret registered after the masker was built.
|
||||
#[test]
|
||||
fn job_masker_notices_once_per_secret_set() {
|
||||
let job_id = Uuid::new_v4();
|
||||
register_running_job(job_id);
|
||||
register_secret_for_job(job_id, "firstsecretvalue");
|
||||
let mut masker = JobMasker::new(job_id);
|
||||
|
||||
let first = masker.mask("saw firstsecretvalue").into_owned();
|
||||
assert!(!first.contains("firstsecretvalue"), "{first}");
|
||||
assert!(first.contains(MASKED_NOTICE), "{first}");
|
||||
|
||||
let second = masker.mask("saw firstsecretvalue again").into_owned();
|
||||
assert!(!second.contains("firstsecretvalue"), "{second}");
|
||||
assert!(!second.contains(MASKED_NOTICE), "{second}");
|
||||
|
||||
register_secret_for_job(job_id, "secondsecretvalue");
|
||||
let third = masker.mask("saw secondsecretvalue").into_owned();
|
||||
assert!(!third.contains("secondsecretvalue"), "{third}");
|
||||
unregister_running_job(job_id);
|
||||
}
|
||||
|
||||
/// Unregistration must not turn masking off under a sink that is still emitting:
|
||||
/// the masker keeps working off the masks it last saw rather than going quiet.
|
||||
#[test]
|
||||
fn job_masker_masks_after_the_job_is_unregistered() {
|
||||
let job_id = Uuid::new_v4();
|
||||
register_running_job(job_id);
|
||||
register_secret_for_job(job_id, "supersecretvalue");
|
||||
let mut masker = JobMasker::new(job_id);
|
||||
|
||||
unregister_running_job(job_id);
|
||||
|
||||
let masked = masker.mask("logged supersecretvalue here");
|
||||
assert!(!masked.contains("supersecretvalue"), "{masked}");
|
||||
}
|
||||
|
||||
/// The notice has to end up on a line of its own for both shapes callers pass:
|
||||
/// a bare line (`handle_child`) and a newline-terminated chunk (nativets). The
|
||||
/// sinks concatenate what they are given verbatim, so getting this wrong welds
|
||||
/// the notice onto whichever line follows it.
|
||||
#[test]
|
||||
fn notice_lands_on_its_own_line_for_both_caller_shapes() {
|
||||
let job_id = Uuid::new_v4();
|
||||
register_running_job(job_id);
|
||||
register_secret_for_job(job_id, "supersecretvalue");
|
||||
|
||||
let line = snapshot(&job_id)
|
||||
.expect("secret registered")
|
||||
.mask("tok supersecretvalue");
|
||||
assert_eq!(line, format!("tok s*****e\n{MASKED_NOTICE}"));
|
||||
|
||||
let chunk = snapshot(&job_id)
|
||||
.expect("secret registered")
|
||||
.mask("tok supersecretvalue\n");
|
||||
assert_eq!(chunk, format!("tok s*****e\n{MASKED_NOTICE}\n"));
|
||||
|
||||
unregister_running_job(job_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,6 +775,19 @@ pub fn make_pull_query(tags: &[String]) -> String {
|
||||
query
|
||||
}
|
||||
|
||||
/// Claim a parent's tool jobs without tag filtering. The caller must supply only child IDs
|
||||
/// it owns; this query is an internal scheduling primitive and does not authorize job access.
|
||||
pub fn make_tool_job_pull_query(job_ids: &[uuid::Uuid]) -> String {
|
||||
// pull() binds only the worker name. These literals come from typed UUIDs, never input SQL.
|
||||
let ids = job_ids.iter().map(|id| format!("'{id}'::uuid")).join(", ");
|
||||
format_pull_query(format!(
|
||||
"SELECT id FROM v2_job_queue
|
||||
WHERE running = false AND id = ANY(ARRAY[{ids}]::uuid[]) AND scheduled_for <= now()
|
||||
ORDER BY priority DESC NULLS LAST, scheduled_for
|
||||
FOR UPDATE SKIP LOCKED LIMIT 1"
|
||||
))
|
||||
}
|
||||
|
||||
// Variant of `make_pull_query` that additionally excludes jobs whose workspace_id is in the
|
||||
// overloaded-list bind parameter ($2::text[]). Built as a separate string (rather than reusing
|
||||
// `make_pull_query` with an always-bound array) so the planner can keep using the same indexes
|
||||
|
||||
@@ -4399,6 +4399,45 @@ pub fn has_active_concurrency_limit(concurrent_limit: Option<i32>) -> bool {
|
||||
concurrent_limit.is_some_and(|n| n > 0)
|
||||
}
|
||||
|
||||
/// Admit a job already owned by a worker without releasing its queue reservation.
|
||||
/// The caller must maintain its heartbeat while waiting and complete it on failure.
|
||||
pub async fn try_admit_owned_job(db: &DB, job: &MiniPulledJob) -> error::Result<bool> {
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
{
|
||||
let settings = windmill_common::runnable_settings::prefetch_cached_from_handle(
|
||||
job.runnable_settings_handle,
|
||||
db,
|
||||
)
|
||||
.await?
|
||||
.1
|
||||
.maybe_fallback(None, job.concurrent_limit, job.concurrency_time_window_s);
|
||||
if has_active_concurrency_limit(settings.concurrent_limit)
|
||||
&& !*DISABLE_CONCURRENCY_LIMIT
|
||||
&& job.canceled_by.is_none()
|
||||
{
|
||||
let key = concurrency_key(db, &job.id).await?.ok_or_else(|| {
|
||||
Error::internal_err(format!("No concurrency key found for job {}", job.id))
|
||||
})?;
|
||||
if !key.is_empty() {
|
||||
return Ok(crate::jobs_ee::update_concurrency_counter(
|
||||
db,
|
||||
&job.id,
|
||||
key,
|
||||
serde_json::json!({ job.id.to_string(): {} }),
|
||||
job.id.to_string(),
|
||||
settings.concurrency_time_window_s.unwrap_or(0),
|
||||
settings.concurrent_limit.unwrap_or_default(),
|
||||
)
|
||||
.await?
|
||||
.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(all(feature = "private", feature = "enterprise")))]
|
||||
let _ = (db, job);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn custom_concurrency_key(
|
||||
db: &Pool<Postgres>,
|
||||
job_id: &Uuid,
|
||||
|
||||
@@ -838,6 +838,11 @@ pub async fn eval_fetch_timeout(
|
||||
}
|
||||
}
|
||||
let w_id_for_tracing = w_id_for_tracing;
|
||||
// nativets delivers logs in-process, so they never reach the masking in
|
||||
// `handle_child::write_lines` and a `console.log` of `$WM_TOKEN` would be
|
||||
// persisted verbatim. Mask here rather than in the detached task draining into
|
||||
// `append_logs`: this loop normally runs while the job is still registered.
|
||||
let mut masker = windmill_common::sensitive_log_masks::JobMasker::new(job_id);
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut result_stream = String::new();
|
||||
let mut is_stream = false;
|
||||
@@ -845,10 +850,20 @@ pub async fn eval_fetch_timeout(
|
||||
use windmill_common::result_stream::extract_stream_from_logs;
|
||||
use windmill_common::tracing_init::{OTEL_JOB_LOGS, OTEL_PREFIX};
|
||||
|
||||
let stream = extract_stream_from_logs(&log.trim_end_matches("\n"));
|
||||
|
||||
// A stream chunk is result data, not a log line — it never reaches
|
||||
// `job_logs`, and `merge_result_stream` can make it the job's result —
|
||||
// so it stays raw wherever it goes, here and in the mirror below.
|
||||
// Deliberately unlike `handle_child`, which streams the masked text.
|
||||
// Routed before masking because the notice is one-shot: spent on a chunk
|
||||
// no sink persists, a later redaction in `job_logs` would go unexplained.
|
||||
let logged = stream.is_none().then(|| masker.mask(&log).into_owned());
|
||||
|
||||
// Mirror `process_streaming_log_lines` (EE) + the OTEL_JOB_LOGS
|
||||
// hook from handle_child.rs, neither of which runs for nativets
|
||||
// since nativets delivers logs in-process via the log channel.
|
||||
for line in log.lines() {
|
||||
for line in logged.as_deref().unwrap_or(&log).lines() {
|
||||
tracing::info!(
|
||||
target: "windmill:job_log",
|
||||
job_id = ?job_id,
|
||||
@@ -862,7 +877,7 @@ pub async fn eval_fetch_timeout(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(stream) = extract_stream_from_logs(&log.trim_end_matches("\n")) {
|
||||
if let Some(stream) = stream {
|
||||
if !is_stream {
|
||||
is_stream = true;
|
||||
if let Some(ref f) = stream_notifier_update {
|
||||
@@ -874,8 +889,8 @@ pub async fn eval_fetch_timeout(
|
||||
if let Err(e) = result_stream_sender.send(stream) {
|
||||
tracing::error!("failed to send result stream: {e}");
|
||||
}
|
||||
} else {
|
||||
if let Err(e) = append_logs_sender.send(log) {
|
||||
} else if let Some(logged) = logged {
|
||||
if let Err(e) = append_logs_sender.send(logged) {
|
||||
tracing::error!("failed to send log: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,12 @@ use crate::result_processor::handle_non_flow_job_error;
|
||||
use crate::worker_flow::{
|
||||
evaluate_input_transform, raw_script_to_payload, script_to_payload, JobPayloadWithTag,
|
||||
};
|
||||
use crate::{
|
||||
create_job_dir, handle_queued_job, JobCompletedReceiver, JobCompletedSender, SendResult,
|
||||
SendResultPayload,
|
||||
};
|
||||
use crate::{create_job_dir, handle_queued_job, JobCompletedSender};
|
||||
use anyhow::Context;
|
||||
use mappable_rc::Marc;
|
||||
use serde_json::value::RawValue;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use sqlx::types::Json;
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use uuid::Uuid;
|
||||
use windmill_ai::{ai_types::OpenAIToolCall, query_builder::StreamEventSink, types::*};
|
||||
use windmill_common::jobs::JobPayload;
|
||||
@@ -36,11 +34,11 @@ use windmill_common::{
|
||||
flow_conversations::{MessageExtras, MessageType},
|
||||
flow_status::AgentAction,
|
||||
flows::FlowModuleValue,
|
||||
worker::{to_raw_value, Connection},
|
||||
worker::{make_tool_job_pull_query, to_raw_value, Connection},
|
||||
};
|
||||
use windmill_queue::{
|
||||
add_completed_job, add_completed_job_error, get_mini_pulled_job, push, MiniCompletedJob,
|
||||
MiniPulledJob, PushArgs, PushIsolationLevel,
|
||||
get_mini_pulled_job, pull, push, try_admit_owned_job, MiniCompletedJob, MiniPulledJob,
|
||||
PushArgs, PushIsolationLevel,
|
||||
};
|
||||
|
||||
/// Shared collection of abort handles for spawned tool tasks.
|
||||
@@ -82,6 +80,7 @@ pub struct ToolExecutionContext<'a> {
|
||||
|
||||
// Abort handles for spawned tool tasks (used for force-cancel cleanup)
|
||||
pub tool_abort_handles: ToolAbortHandles,
|
||||
pub job_completed_tx: JobCompletedSender,
|
||||
}
|
||||
|
||||
/// Execute all tool calls from an AI response
|
||||
@@ -98,7 +97,8 @@ pub async fn execute_tool_calls(
|
||||
let mut used_structured_output_tool = false;
|
||||
let mut final_content = None;
|
||||
|
||||
for tool_call in tool_calls.iter() {
|
||||
let mut calls = tool_calls.iter().peekable();
|
||||
while let Some(tool_call) = calls.next() {
|
||||
// Stream tool call progress
|
||||
if let Some(stream_event_processor) = ctx.stream_event_processor {
|
||||
let event = StreamingEvent::ToolExecution {
|
||||
@@ -150,15 +150,24 @@ pub async fn execute_tool_calls(
|
||||
)
|
||||
.await?;
|
||||
} else if tool.module.is_some() {
|
||||
execute_windmill_tool(
|
||||
&mut ctx,
|
||||
tool_call,
|
||||
tool,
|
||||
actions,
|
||||
&mut messages,
|
||||
final_events_str,
|
||||
)
|
||||
.await?;
|
||||
let mut batch = vec![(tool_call, tool)];
|
||||
while let Some(next_call) = calls.peek() {
|
||||
if structured_output_tool_name.as_deref()
|
||||
== Some(next_call.function.name.as_str())
|
||||
{
|
||||
break;
|
||||
}
|
||||
let Some(next_tool) = tools.iter().find(|t| {
|
||||
t.def.function.name == next_call.function.name
|
||||
&& t.mcp_source.is_none()
|
||||
&& t.module.is_some()
|
||||
}) else {
|
||||
break;
|
||||
};
|
||||
batch.push((calls.next().unwrap(), next_tool));
|
||||
}
|
||||
execute_windmill_tools(&mut ctx, &batch, actions, &mut messages, final_events_str)
|
||||
.await?;
|
||||
} else {
|
||||
return Err(Error::internal_err(format!(
|
||||
"Tool type not supported: {}",
|
||||
@@ -313,14 +322,13 @@ async fn execute_mcp_tool_call(
|
||||
}
|
||||
|
||||
/// Execute a Windmill tool (script or flow)
|
||||
async fn execute_windmill_tool(
|
||||
ctx: &mut ToolExecutionContext<'_>,
|
||||
async fn enqueue_windmill_tool(
|
||||
ctx: &ToolExecutionContext<'_>,
|
||||
tool_call: &OpenAIToolCall,
|
||||
tool: &Tool,
|
||||
actions: &mut Vec<AgentAction>,
|
||||
messages: &mut Vec<OpenAIMessage>,
|
||||
final_events_str: &mut String,
|
||||
) -> Result<(), Error> {
|
||||
reserved: bool,
|
||||
) -> Result<Uuid, Error> {
|
||||
// Regular Windmill tools must have a module
|
||||
let tool_module = tool.module.as_ref().ok_or_else(|| {
|
||||
Error::internal_err(format!("Tool {} has no module", tool_call.function.name))
|
||||
@@ -402,8 +410,6 @@ async fn execute_windmill_tool(
|
||||
tool_call_args.insert(key.clone(), result);
|
||||
}
|
||||
|
||||
let is_ai_agent_tool = matches!(tool_value, FlowModuleValue::AIAgent { .. });
|
||||
|
||||
let job_payload = match tool_value {
|
||||
FlowModuleValue::Script { path: script_path, hash: script_hash, tag_override, .. } => {
|
||||
script_to_payload(
|
||||
@@ -473,8 +479,6 @@ async fn execute_windmill_tool(
|
||||
));
|
||||
}
|
||||
let path = format!("{}/tools/{}", ctx.job.runnable_path(), tool_module.id);
|
||||
// tool jobs are pushed with the parent agent job's tag and executed inline on the
|
||||
// same worker, so a tag override on a nested agent tool does not apply here
|
||||
JobPayloadWithTag {
|
||||
payload: JobPayload::AIAgent { path },
|
||||
tag: None,
|
||||
@@ -532,44 +536,52 @@ async fn execute_windmill_tool(
|
||||
false,
|
||||
None,
|
||||
ctx.job.visible_to_owner,
|
||||
Some(ctx.job.tag.clone()),
|
||||
job_payload.tag,
|
||||
job_payload.timeout,
|
||||
None,
|
||||
job_priority,
|
||||
job_perms.as_ref(),
|
||||
true,
|
||||
reserved,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut tx = tx;
|
||||
if reserved {
|
||||
// Running ownership reserves the first child; its normal tag allows zombie recovery.
|
||||
sqlx::query!(
|
||||
"UPDATE v2_job_queue SET worker = $1 WHERE id = $2",
|
||||
ctx.worker_name,
|
||||
uuid,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query!("UPDATE v2_job_runtime SET ping = now() WHERE id = $1", uuid)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
Ok(uuid)
|
||||
}
|
||||
|
||||
let tool_job = get_mini_pulled_job(ctx.db, &uuid).await?;
|
||||
|
||||
let Some(tool_job) = tool_job else {
|
||||
return Err(Error::internal_err("Tool job not found".to_string()));
|
||||
};
|
||||
|
||||
let tool_job = Arc::new(tool_job);
|
||||
|
||||
let (inner_job_completed_tx, inner_job_completed_rx) = JobCompletedSender::new(ctx.conn, 1);
|
||||
|
||||
let inner_job_completed_rx = inner_job_completed_rx.expect(
|
||||
"inner_job_completed_tx should be set as agent jobs are not supported on agent workers",
|
||||
);
|
||||
fn spawn_local_tool(
|
||||
ctx: &ToolExecutionContext<'_>,
|
||||
tool_job: MiniPulledJob,
|
||||
reserved: bool,
|
||||
) -> tokio::task::JoinHandle<Result<OccupancyMetrics, Error>> {
|
||||
let mut tool_job = Arc::new(tool_job);
|
||||
|
||||
// Spawn handle_queued_job on separate task to prevent tokio stack overflow
|
||||
// Clone everything needed for the spawned task
|
||||
let tool_job_spawn = tool_job.clone();
|
||||
let db = ctx.db.clone();
|
||||
let conn_spawn = ctx.conn.clone();
|
||||
let client_spawn = ctx.client.clone();
|
||||
let hostname_spawn = ctx.hostname.to_string();
|
||||
let worker_name_spawn = ctx.worker_name.to_string();
|
||||
let worker_dir_spawn = ctx.worker_dir.to_string();
|
||||
let base_internal_url_spawn = ctx.base_internal_url.to_string();
|
||||
let inner_job_completed_tx_spawn = inner_job_completed_tx.clone();
|
||||
let job_completed_tx = ctx.job_completed_tx.clone();
|
||||
let mut occupancy_metrics_spawn = ctx.occupancy_metrics.clone();
|
||||
let mut killpill_rx_spawn = ctx.killpill_rx.resubscribe();
|
||||
|
||||
@@ -578,147 +590,266 @@ async fn execute_windmill_tool(
|
||||
#[cfg(feature = "benchmark")]
|
||||
let mut bench_spawn = windmill_common::bench::BenchmarkIter::new();
|
||||
|
||||
let job_dir = create_job_dir(&worker_dir_spawn, tool_job_spawn.id).await;
|
||||
let result = async {
|
||||
if reserved {
|
||||
loop {
|
||||
let queued = get_mini_pulled_job(&db, &tool_job.id)
|
||||
.await?
|
||||
.ok_or_else(|| Error::AlreadyCompleted("Tool job already completed".to_string()))?;
|
||||
tool_job = Arc::new(queued);
|
||||
sqlx::query!(
|
||||
"UPDATE v2_job_runtime SET ping = now() WHERE id = $1",
|
||||
tool_job.id,
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
if try_admit_owned_job(&db, &tool_job).await? {
|
||||
let started_at = sqlx::query_scalar!(
|
||||
"UPDATE v2_job_queue SET started_at = now() WHERE id = $1 RETURNING started_at",
|
||||
tool_job.id,
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.flatten();
|
||||
Arc::make_mut(&mut tool_job).started_at = started_at;
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
let perms =
|
||||
windmill_common::auth::get_job_perms(&db, &tool_job.id, &tool_job.workspace_id).await?;
|
||||
let token = windmill_queue::create_token(&db, &tool_job, perms).await;
|
||||
let client_spawn = AuthedClient::new(
|
||||
base_internal_url_spawn.clone(),
|
||||
tool_job.workspace_id.clone(),
|
||||
token,
|
||||
None,
|
||||
);
|
||||
let job_dir = create_job_dir(&worker_dir_spawn, tool_job.id).await;
|
||||
|
||||
let result = handle_queued_job(
|
||||
tool_job_spawn,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&conn_spawn,
|
||||
&client_spawn,
|
||||
&hostname_spawn,
|
||||
&worker_name_spawn,
|
||||
&worker_dir_spawn,
|
||||
&job_dir,
|
||||
None,
|
||||
&base_internal_url_spawn,
|
||||
inner_job_completed_tx_spawn,
|
||||
&mut occupancy_metrics_spawn,
|
||||
&mut killpill_rx_spawn,
|
||||
None,
|
||||
None,
|
||||
#[cfg(feature = "benchmark")]
|
||||
&mut bench_spawn,
|
||||
)
|
||||
handle_queued_job(
|
||||
tool_job.clone(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&conn_spawn,
|
||||
&client_spawn,
|
||||
&hostname_spawn,
|
||||
&worker_name_spawn,
|
||||
&worker_dir_spawn,
|
||||
&job_dir,
|
||||
None,
|
||||
&base_internal_url_spawn,
|
||||
job_completed_tx,
|
||||
&mut occupancy_metrics_spawn,
|
||||
&mut killpill_rx_spawn,
|
||||
None,
|
||||
None,
|
||||
#[cfg(feature = "benchmark")]
|
||||
&mut bench_spawn,
|
||||
)
|
||||
.await
|
||||
}
|
||||
.await;
|
||||
|
||||
// Return both result and updated metrics
|
||||
(result, occupancy_metrics_spawn)
|
||||
match result {
|
||||
Err(err) => {
|
||||
let err_string = format!("{}: {}", err.name(), err);
|
||||
handle_non_flow_job_error(
|
||||
&db,
|
||||
&MiniCompletedJob::from(tool_job),
|
||||
0,
|
||||
None,
|
||||
err_string,
|
||||
windmill_common::worker::error_to_value(&err),
|
||||
&worker_name_spawn,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(_) => {}
|
||||
}
|
||||
Ok(occupancy_metrics_spawn)
|
||||
});
|
||||
|
||||
// Register abort handle so the task can be killed on force-cancel
|
||||
let abort_handle = join_handle.abort_handle();
|
||||
// unwrap safe: lock is only held briefly for push/drain, no panic possible inside
|
||||
ctx.tool_abort_handles.lock().unwrap().push(abort_handle);
|
||||
|
||||
// Await the spawned task
|
||||
let (handle_result, updated_occupancy) = join_handle.await.map_err(|e| {
|
||||
if e.is_cancelled() {
|
||||
Error::ExecutionErr("Tool execution task was cancelled".to_string())
|
||||
} else {
|
||||
Error::internal_err(format!("Tool execution task failed: {}", e))
|
||||
}
|
||||
})?;
|
||||
|
||||
// Merge occupancy metrics back
|
||||
ctx.occupancy_metrics.total_duration_of_running_jobs =
|
||||
updated_occupancy.total_duration_of_running_jobs;
|
||||
|
||||
match handle_result {
|
||||
Err(err) => {
|
||||
handle_tool_execution_error(
|
||||
ctx,
|
||||
tool_call,
|
||||
tool_module,
|
||||
&MiniCompletedJob::from(tool_job),
|
||||
job_id,
|
||||
err,
|
||||
messages,
|
||||
final_events_str,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(outcome) => {
|
||||
handle_tool_execution_success(
|
||||
ctx,
|
||||
tool_call,
|
||||
tool_module,
|
||||
job_id,
|
||||
outcome.is_success(),
|
||||
is_ai_agent_tool,
|
||||
inner_job_completed_rx,
|
||||
messages,
|
||||
final_events_str,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
join_handle
|
||||
}
|
||||
|
||||
/// Handle tool execution error
|
||||
async fn handle_tool_execution_error(
|
||||
async fn execute_windmill_tools(
|
||||
ctx: &mut ToolExecutionContext<'_>,
|
||||
tool_call: &OpenAIToolCall,
|
||||
tool_module: &windmill_common::flows::FlowModule,
|
||||
tool_job: &MiniCompletedJob,
|
||||
job_id: Uuid,
|
||||
err: Error,
|
||||
batch: &[(&OpenAIToolCall, &Tool)],
|
||||
actions: &mut Vec<AgentAction>,
|
||||
messages: &mut Vec<OpenAIMessage>,
|
||||
final_events_str: &mut String,
|
||||
) -> Result<(), Error> {
|
||||
let err_string = format!("{}: {}", err.name(), err.to_string());
|
||||
let err_json = windmill_common::worker::error_to_value(&err);
|
||||
let _ = handle_non_flow_job_error(
|
||||
ctx.db,
|
||||
tool_job,
|
||||
0,
|
||||
None,
|
||||
err_string.clone(),
|
||||
err_json,
|
||||
ctx.worker_name,
|
||||
)
|
||||
.await;
|
||||
|
||||
let error_message = format!("Error running tool: {}", err_string);
|
||||
messages.push(OpenAIMessage {
|
||||
role: "tool".to_string(),
|
||||
content: Some(OpenAIContent::Text(error_message.clone())),
|
||||
tool_call_id: Some(tool_call.id.clone()),
|
||||
agent_action: Some(AgentAction::ToolCall {
|
||||
job_id,
|
||||
function_name: tool_call.function.name.clone(),
|
||||
module_id: tool_module.id.clone(),
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Stream tool result (error case)
|
||||
if let Some(stream_event_processor) = ctx.stream_event_processor {
|
||||
let tool_result_event = StreamingEvent::ToolResult {
|
||||
call_id: tool_call.id.clone(),
|
||||
function_name: tool_call.function.name.clone(),
|
||||
result: error_message.clone(),
|
||||
success: false,
|
||||
};
|
||||
stream_event_processor
|
||||
.send(tool_result_event, final_events_str)
|
||||
.await?;
|
||||
let mut job_ids = Vec::with_capacity(batch.len());
|
||||
let mut local = None;
|
||||
for (index, (call, tool)) in batch.iter().enumerate() {
|
||||
if index > 0 {
|
||||
if let Some(processor) = ctx.stream_event_processor {
|
||||
processor
|
||||
.send(
|
||||
StreamingEvent::ToolExecution {
|
||||
call_id: call.id.clone(),
|
||||
function_name: call.function.name.clone(),
|
||||
},
|
||||
final_events_str,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
job_ids.push(enqueue_windmill_tool(ctx, call, tool, actions, index == 0).await?);
|
||||
if index == 0 {
|
||||
let job = get_mini_pulled_job(ctx.db, &job_ids[0])
|
||||
.await?
|
||||
.ok_or_else(|| Error::internal_err("Reserved tool job not found".to_string()))?;
|
||||
local = Some(spawn_local_tool(ctx, job, true));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parent_job) = ctx.parent_job {
|
||||
update_flow_status_module_with_actions_success(ctx.db, parent_job, false).await?;
|
||||
let mut results: HashMap<Uuid, (bool, String)> = HashMap::new();
|
||||
let mut next_result = 0;
|
||||
let mut poll = tokio::time::interval(Duration::from_millis(100));
|
||||
poll.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
|
||||
while next_result < batch.len() || local.is_some() {
|
||||
tokio::select! {
|
||||
result = async { local.as_mut().unwrap().await }, if local.is_some() => {
|
||||
local = None;
|
||||
let metrics = result.map_err(|e| Error::internal_err(format!("Tool task failed: {e}")))??;
|
||||
ctx.occupancy_metrics.total_duration_of_running_jobs = metrics.total_duration_of_running_jobs;
|
||||
ctx.tool_abort_handles.lock().unwrap().retain(|handle| !handle.is_finished());
|
||||
}
|
||||
_ = poll.tick() => {}
|
||||
}
|
||||
|
||||
let pending: Vec<Uuid> = job_ids[next_result..]
|
||||
.iter()
|
||||
.filter(|id| !results.contains_key(id))
|
||||
.copied()
|
||||
.collect();
|
||||
if !pending.is_empty() {
|
||||
let completed = sqlx::query!(
|
||||
"SELECT id, status = 'success' AS \"success!\", result AS \"result: Json<Box<RawValue>>\"
|
||||
FROM v2_job_completed WHERE workspace_id = $1 AND id = ANY($2)",
|
||||
ctx.job.workspace_id,
|
||||
&pending,
|
||||
).fetch_all(ctx.db).await?;
|
||||
for completed in completed {
|
||||
let index = job_ids
|
||||
.iter()
|
||||
.position(|id| *id == completed.id)
|
||||
.ok_or_else(|| Error::internal_err("Unexpected tool completion".to_string()))?;
|
||||
let (call, tool) = batch[index];
|
||||
let result = completed
|
||||
.result
|
||||
.map(|value| value.0)
|
||||
.unwrap_or_else(|| to_raw_value(&serde_json::Value::Null));
|
||||
let is_agent = tool.module.as_ref().is_some_and(|module| {
|
||||
matches!(module.get_value(), Ok(FlowModuleValue::AIAgent { .. }))
|
||||
});
|
||||
let content = if is_agent && completed.success {
|
||||
extract_ai_agent_output(&result).unwrap_or_else(|| result.get().to_string())
|
||||
} else {
|
||||
result.get().to_string()
|
||||
};
|
||||
if let Some(processor) = ctx.stream_event_processor {
|
||||
processor
|
||||
.send(
|
||||
StreamingEvent::ToolResult {
|
||||
call_id: call.id.clone(),
|
||||
function_name: call.function.name.clone(),
|
||||
result: content.clone(),
|
||||
success: completed.success,
|
||||
},
|
||||
final_events_str,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
results.insert(completed.id, (completed.success, content));
|
||||
}
|
||||
}
|
||||
|
||||
// Transcript rows, model messages, and positional action statuses share call order.
|
||||
while next_result < batch.len() {
|
||||
let job_id = job_ids[next_result];
|
||||
let Some((success, content)) = results.remove(&job_id) else {
|
||||
break;
|
||||
};
|
||||
let (call, tool) = batch[next_result];
|
||||
let module = tool
|
||||
.module
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::internal_err("Windmill tool has no module".to_string()))?;
|
||||
messages.push(OpenAIMessage {
|
||||
role: "tool".to_string(),
|
||||
content: Some(OpenAIContent::Text(content.clone())),
|
||||
tool_call_id: Some(call.id.clone()),
|
||||
agent_action: Some(AgentAction::ToolCall {
|
||||
job_id,
|
||||
function_name: call.function.name.clone(),
|
||||
module_id: module.id.clone(),
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
if let Some(parent) = ctx.parent_job {
|
||||
update_flow_status_module_with_actions_success(ctx.db, parent, success).await?;
|
||||
}
|
||||
let (content, extras) = windmill_tool_row(call, success, &content);
|
||||
add_tool_message_to_chat(ctx, Some(job_id), &content, success, Some(extras)).await;
|
||||
next_result += 1;
|
||||
}
|
||||
|
||||
if local.is_none() && next_result < batch.len() {
|
||||
let pending: Vec<Uuid> = job_ids[next_result..]
|
||||
.iter()
|
||||
.filter(|id| !results.contains_key(id))
|
||||
.copied()
|
||||
.collect();
|
||||
if !pending.is_empty() {
|
||||
local = claim_local_tool(ctx, &pending).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (content, extras) = windmill_tool_row(tool_call, false, &error_message);
|
||||
add_tool_message_to_chat(ctx, Some(job_id), &content, false, Some(extras)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn claim_local_tool(
|
||||
ctx: &ToolExecutionContext<'_>,
|
||||
pending: &[Uuid],
|
||||
) -> Result<Option<tokio::task::JoinHandle<Result<OccupancyMetrics, Error>>>, Error> {
|
||||
let query = (String::new(), make_tool_job_pull_query(pending));
|
||||
#[cfg(feature = "benchmark")]
|
||||
let mut bench = windmill_common::bench::BenchmarkIter::new();
|
||||
let mut pulled = pull(
|
||||
ctx.db,
|
||||
false,
|
||||
ctx.worker_name,
|
||||
Some(&query),
|
||||
#[cfg(feature = "benchmark")]
|
||||
&mut bench,
|
||||
)
|
||||
.await?;
|
||||
if let Err(err) = pulled.maybe_apply_debouncing(ctx.db).await {
|
||||
pulled.error_while_preprocessing = Some(err.to_string());
|
||||
}
|
||||
match pulled.to_pulled_job() {
|
||||
Ok(job) => Ok(job.map(|job| spawn_local_tool(ctx, job.job, false))),
|
||||
Err(
|
||||
windmill_queue::PulledJobResultToJobErr::MissingConcurrencyKey(job)
|
||||
| windmill_queue::PulledJobResultToJobErr::ErrorWhilePreprocessing(job),
|
||||
) => {
|
||||
ctx.job_completed_tx.send_job(job, true).await?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the `output` field of an `AIAgentResult` envelope, serialized back to JSON.
|
||||
/// Returns `None` if the result is not a JSON object carrying an `output` field.
|
||||
fn extract_ai_agent_output(result: &RawValue) -> Option<String> {
|
||||
@@ -728,119 +859,6 @@ fn extract_ai_agent_output(result: &RawValue) -> Option<String> {
|
||||
.map(|output| output.get().to_string())
|
||||
}
|
||||
|
||||
/// Handle tool execution success
|
||||
async fn handle_tool_execution_success(
|
||||
ctx: &mut ToolExecutionContext<'_>,
|
||||
tool_call: &OpenAIToolCall,
|
||||
tool_module: &windmill_common::flows::FlowModule,
|
||||
job_id: Uuid,
|
||||
success: bool,
|
||||
is_ai_agent_tool: bool,
|
||||
inner_job_completed_rx: JobCompletedReceiver,
|
||||
messages: &mut Vec<OpenAIMessage>,
|
||||
final_events_str: &mut String,
|
||||
) -> Result<(), Error> {
|
||||
let send_result = inner_job_completed_rx.bounded_rx.try_recv().ok();
|
||||
|
||||
let (result, job_success) = if let Some(SendResult {
|
||||
result: SendResultPayload::JobCompleted(ref jc),
|
||||
..
|
||||
}) = send_result
|
||||
{
|
||||
let result = jc.result.clone();
|
||||
// Write tool completion to the DB inline instead of forwarding through
|
||||
// the parent channel. Forwarding would deadlock for nested agents: the
|
||||
// sub-tool result would fill the parent's bounded(1) channel, leaving
|
||||
// no room for the agent's own completion from process_result.
|
||||
if jc.success {
|
||||
add_completed_job(
|
||||
ctx.db,
|
||||
&jc.job,
|
||||
true,
|
||||
false,
|
||||
sqlx::types::Json(&*jc.result),
|
||||
jc.result_columns.clone(),
|
||||
jc.mem_peak,
|
||||
jc.canceled_by.clone(),
|
||||
false,
|
||||
jc.duration,
|
||||
jc.from_cache.unwrap_or(false),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Error::internal_err(format!("Failed to add completed job: {e}")))?;
|
||||
} else {
|
||||
let error_value: serde_json::Value =
|
||||
serde_json::from_str(jc.result.get()).unwrap_or_else(|_| {
|
||||
serde_json::json!({ "message": format!("Non serializable error: {}", jc.result.get()) })
|
||||
});
|
||||
add_completed_job_error(
|
||||
ctx.db,
|
||||
&jc.job,
|
||||
jc.mem_peak,
|
||||
jc.canceled_by.clone(),
|
||||
error_value,
|
||||
ctx.worker_name,
|
||||
false,
|
||||
jc.duration,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Error::internal_err(format!("Failed to add completed job error: {e}")))?;
|
||||
}
|
||||
(result, jc.success)
|
||||
} else {
|
||||
return Err(Error::internal_err(
|
||||
"Tool job completed but no result".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
// A nested agent returns the whole `AIAgentResult` envelope: on top of `output` it carries
|
||||
// the child's entire message history, stream log and token usage. Feeding that back would
|
||||
// grow the caller's context by the child's full transcript on every call, so the caller only
|
||||
// sees `output`. The envelope stays intact in the tool job's completed row.
|
||||
let tool_result = if is_ai_agent_tool && job_success {
|
||||
extract_ai_agent_output(&result).unwrap_or_else(|| result.get().to_string())
|
||||
} else {
|
||||
result.get().to_string()
|
||||
};
|
||||
|
||||
messages.push(OpenAIMessage {
|
||||
role: "tool".to_string(),
|
||||
content: Some(OpenAIContent::Text(tool_result.clone())),
|
||||
tool_call_id: Some(tool_call.id.clone()),
|
||||
agent_action: Some(AgentAction::ToolCall {
|
||||
job_id,
|
||||
function_name: tool_call.function.name.clone(),
|
||||
module_id: tool_module.id.clone(),
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let (content, extras) = windmill_tool_row(tool_call, success, &tool_result);
|
||||
|
||||
// The job ran; whether it ran successfully is `success`, and the row stored below is
|
||||
// worded from it. The stream has to carry the same value, or the card the reader watches
|
||||
// and the row that replaces it describe the same call differently.
|
||||
if let Some(stream_event_processor) = ctx.stream_event_processor {
|
||||
let tool_result_event = StreamingEvent::ToolResult {
|
||||
call_id: tool_call.id.clone(),
|
||||
function_name: tool_call.function.name.clone(),
|
||||
result: tool_result,
|
||||
success,
|
||||
};
|
||||
stream_event_processor
|
||||
.send(tool_result_event, final_events_str)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(parent_job) = ctx.parent_job {
|
||||
update_flow_status_module_with_actions_success(ctx.db, parent_job, success).await?;
|
||||
}
|
||||
|
||||
add_tool_message_to_chat(ctx, Some(job_id), &content, success, Some(extras)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A Windmill tool's conversation row: worded from the tool, carrying the model's call and
|
||||
/// the exact text the model got back, the same text agent memory keeps for that tool
|
||||
/// message, so a card needs no job fetch. The call is the model's arguments, not the job's
|
||||
@@ -905,9 +923,7 @@ async fn add_tool_message_to_chat(
|
||||
.or(ctx.job.flow_step_id.as_deref());
|
||||
let step_name = get_step_name_from_flow(ctx.summary.as_deref(), effective_step_id);
|
||||
|
||||
// Awaited, not spawned: `created_seq` is the transcript's order, so a round's rows
|
||||
// must commit in the order of its calls. Calls run one after another; running them
|
||||
// in parallel would need their rows written in call order all the same.
|
||||
// created_seq defines transcript order, so these writes must stay sequential.
|
||||
if let Err(e) = add_message_to_conversation(
|
||||
ctx.db,
|
||||
&memory_id,
|
||||
|
||||
@@ -504,6 +504,7 @@ pub async fn handle_ai_agent_job(
|
||||
hostname: &str,
|
||||
killpill_rx: &mut tokio::sync::broadcast::Receiver<()>,
|
||||
has_stream: &mut bool,
|
||||
job_completed_tx: crate::JobCompletedSender,
|
||||
) -> Result<Box<RawValue>, Error> {
|
||||
// build_args_map returns None if no $res:/$var: transforms needed, in which case use original args
|
||||
let local_args = match build_args_map(job, client, conn).await? {
|
||||
@@ -1010,6 +1011,7 @@ pub async fn handle_ai_agent_job(
|
||||
omit_output_from_conversation,
|
||||
cancel_rx,
|
||||
tool_abort_handles.clone(),
|
||||
job_completed_tx,
|
||||
);
|
||||
|
||||
let mut occupancy_opt = Some(occupancy_metrics);
|
||||
@@ -1028,13 +1030,10 @@ pub async fn handle_ai_agent_job(
|
||||
cancel_tx,
|
||||
CANCEL_GRACE_PERIOD,
|
||||
)
|
||||
.await?
|
||||
.await
|
||||
};
|
||||
// agent_fut and update_job are now dropped — borrows on mcp_clients and canceled_by released
|
||||
|
||||
// Cleanup MCP clients
|
||||
cleanup_mcp_clients(mcp_clients).await;
|
||||
|
||||
let format_cancel_info = |cb: &Option<CanceledBy>| {
|
||||
cb.as_ref()
|
||||
.map_or(("unknown".to_string(), "unknown".to_string()), |x| {
|
||||
@@ -1045,38 +1044,42 @@ pub async fn handle_ai_agent_job(
|
||||
})
|
||||
};
|
||||
|
||||
match outcome {
|
||||
GracefulPollOutcome::Ok(result) => Ok(result),
|
||||
GracefulPollOutcome::Timeout(ms) => {
|
||||
tracing::error!("AI agent timeout after {}s", ms / 1000);
|
||||
Err(Error::ExecutionErr(format!(
|
||||
"AI agent timeout after (>{}s)",
|
||||
ms / 1000
|
||||
)))
|
||||
}
|
||||
GracefulPollOutcome::Cancelled { canceled_by: cb } => {
|
||||
let (by, reason) = format_cancel_info(&cb);
|
||||
Err(Error::ExecutionErr(format!(
|
||||
"Job cancelled by {by} (reason: {reason})"
|
||||
)))
|
||||
}
|
||||
GracefulPollOutcome::CancelledTimeout { canceled_by: cb } => {
|
||||
let (by, reason) = format_cancel_info(&cb);
|
||||
// Abort any still-running spawned tool tasks
|
||||
// unwrap safe: lock is only held briefly for push/drain, no panic possible inside
|
||||
for handle in tool_abort_handles.lock().unwrap().drain(..) {
|
||||
handle.abort();
|
||||
let result = match outcome {
|
||||
Err(error) => Err(error),
|
||||
Ok(outcome) => match outcome {
|
||||
GracefulPollOutcome::Ok(result) => Ok(result),
|
||||
GracefulPollOutcome::Timeout(ms) => {
|
||||
tracing::error!("AI agent timeout after {}s", ms / 1000);
|
||||
Err(Error::ExecutionErr(format!(
|
||||
"AI agent timeout after (>{}s)",
|
||||
ms / 1000
|
||||
)))
|
||||
}
|
||||
// Hard timeout: clean up orphaned jobs still stuck in v2_job_queue
|
||||
cleanup_orphaned_tool_jobs(db, &job.id, &job.workspace_id, cb).await;
|
||||
Err(Error::ExecutionErr(format!(
|
||||
"Job cancelled by {by} (reason: {reason}, timed out waiting for tool calls)"
|
||||
)))
|
||||
}
|
||||
GracefulPollOutcome::AlreadyCompleted => {
|
||||
Err(Error::AlreadyCompleted("Job already completed".to_string()))
|
||||
GracefulPollOutcome::Cancelled { canceled_by: cb } => {
|
||||
let (by, reason) = format_cancel_info(&cb);
|
||||
Err(Error::ExecutionErr(format!(
|
||||
"Job cancelled by {by} (reason: {reason})"
|
||||
)))
|
||||
}
|
||||
GracefulPollOutcome::CancelledTimeout { canceled_by: cb } => {
|
||||
let (by, reason) = format_cancel_info(&cb);
|
||||
Err(Error::ExecutionErr(format!(
|
||||
"Job cancelled by {by} (reason: {reason}, timed out waiting for tool calls)"
|
||||
)))
|
||||
}
|
||||
GracefulPollOutcome::AlreadyCompleted => {
|
||||
Err(Error::AlreadyCompleted("Job already completed".to_string()))
|
||||
}
|
||||
},
|
||||
};
|
||||
if result.is_err() {
|
||||
for handle in tool_abort_handles.lock().unwrap().drain(..) {
|
||||
handle.abort();
|
||||
}
|
||||
cleanup_orphaned_tool_jobs(db, &job.id, &job.workspace_id, canceled_by.clone()).await;
|
||||
}
|
||||
cleanup_mcp_clients(mcp_clients).await;
|
||||
result
|
||||
}
|
||||
|
||||
/// OpenAI rejects a `prompt_cache_key` over 64 characters
|
||||
@@ -1129,6 +1132,7 @@ pub async fn run_agent(
|
||||
|
||||
// abort handles for spawned tool tasks
|
||||
tool_abort_handles: ToolAbortHandles,
|
||||
job_completed_tx: crate::JobCompletedSender,
|
||||
) -> error::Result<Box<RawValue>> {
|
||||
let output_type = args.output_type.as_ref().unwrap_or(&OutputType::Text);
|
||||
let credentials = args.provider.to_provider_credentials(db).await?;
|
||||
@@ -1859,6 +1863,7 @@ pub async fn run_agent(
|
||||
previous_result: &previous_result,
|
||||
id_context: &id_context,
|
||||
tool_abort_handles: tool_abort_handles.clone(),
|
||||
job_completed_tx: job_completed_tx.clone(),
|
||||
};
|
||||
|
||||
let (tool_messages, tool_content, tool_used_structured_output) =
|
||||
@@ -2758,11 +2763,13 @@ async fn cleanup_orphaned_tool_jobs(
|
||||
)
|
||||
});
|
||||
|
||||
// Find direct child jobs still in v2_job_queue (agent tool jobs are always direct children)
|
||||
let orphaned_ids: Vec<Uuid> = match sqlx::query_scalar!(
|
||||
r#"SELECT j.id FROM v2_job j
|
||||
JOIN v2_job_queue q ON q.id = j.id
|
||||
WHERE j.parent_job = $1 AND j.workspace_id = $2"#,
|
||||
r#"WITH RECURSIVE descendants AS (
|
||||
SELECT id FROM v2_job WHERE parent_job = $1 AND workspace_id = $2
|
||||
UNION ALL
|
||||
SELECT j.id FROM v2_job j JOIN descendants d ON j.parent_job = d.id
|
||||
WHERE j.workspace_id = $2
|
||||
) SELECT d.id AS "id!" FROM descendants d JOIN v2_job_queue q ON q.id = d.id"#,
|
||||
parent_job_id,
|
||||
w_id,
|
||||
)
|
||||
|
||||
@@ -3859,8 +3859,6 @@ pub async fn run_worker(
|
||||
|
||||
let arc_job = Arc::new(job);
|
||||
|
||||
windmill_common::sensitive_log_masks::register_running_job(arc_job.id);
|
||||
|
||||
let span = create_span_with_name(&arc_job, &worker_name, Some(hostname), "job");
|
||||
let log_ctx = log_context_for_job(&arc_job, &worker_name, Some(hostname));
|
||||
|
||||
@@ -3976,8 +3974,6 @@ pub async fn run_worker(
|
||||
_ => {}
|
||||
}
|
||||
|
||||
windmill_common::sensitive_log_masks::unregister_running_job(job_id);
|
||||
|
||||
#[cfg(feature = "prometheus")]
|
||||
if let Some(duration) = _timer.map(|x| x.stop_and_record()) {
|
||||
register_metric(
|
||||
@@ -4512,6 +4508,30 @@ async fn detect_and_store_runtime_assets_from_job_args(
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds a job's entry in the log-masking registry for as long as it executes, so
|
||||
/// that secrets it fetches can be registered against it, and masks the job's own
|
||||
/// token from the start: `$WM_TOKEN` stays valid well past the run, and a script
|
||||
/// that echoes it would otherwise leave a live credential in the persisted logs.
|
||||
///
|
||||
/// Lives here rather than at the call sites so that every way of running a job —
|
||||
/// the poller, the interactive worker shell, an inline AI agent tool — is covered
|
||||
/// by construction.
|
||||
struct RunningJobMasks(Uuid);
|
||||
|
||||
impl RunningJobMasks {
|
||||
fn register(job_id: Uuid, token: &str) -> Self {
|
||||
windmill_common::sensitive_log_masks::register_running_job(job_id);
|
||||
windmill_common::sensitive_log_masks::register_secret_for_job(job_id, token);
|
||||
RunningJobMasks(job_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RunningJobMasks {
|
||||
fn drop(&mut self) {
|
||||
windmill_common::sensitive_log_masks::unregister_running_job(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_queued_job(
|
||||
job: Arc<MiniPulledJob>,
|
||||
raw_code: Option<String>,
|
||||
@@ -4533,6 +4553,8 @@ pub async fn handle_queued_job(
|
||||
flow_runners: Option<Arc<FlowRunners>>,
|
||||
#[cfg(feature = "benchmark")] _bench: &mut BenchmarkIter,
|
||||
) -> windmill_common::error::Result<JobOutcome> {
|
||||
let _masks = RunningJobMasks::register(job.id, &client.token);
|
||||
|
||||
if job.canceled_by.is_some() {
|
||||
return Err(Error::JsonErr(canceled_job_to_result(&job)));
|
||||
}
|
||||
@@ -4951,6 +4973,7 @@ pub async fn handle_queued_job(
|
||||
hostname,
|
||||
killpill_rx,
|
||||
&mut has_stream,
|
||||
job_completed_tx.clone(),
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.814.0";
|
||||
export const VERSION = "v1.815.0";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-chat",
|
||||
"version": "1.814.0",
|
||||
"version": "1.815.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-chat",
|
||||
"version": "1.814.0",
|
||||
"version": "1.815.0",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@ai-sdk/react": "^4.0.102",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-chat",
|
||||
"description": "Build chat interfaces on Windmill flows deployed in chat mode, from any frontend or raw app",
|
||||
"version": "1.814.0",
|
||||
"version": "1.815.0",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://github.com/windmill-labs/windmill/tree/main/chat-sdk#readme",
|
||||
|
||||
@@ -10,4 +10,4 @@ export const WM_FORK_PREFIX = "wm-fork";
|
||||
// (e.g. utils.ts) can read it without importing main.ts and creating a circular
|
||||
// dependency (main → workspace → utils → main) that triggers a TDZ.
|
||||
// Re-exported from main.ts for backwards compatibility.
|
||||
export const VERSION = "1.814.0";
|
||||
export const VERSION = "1.815.0";
|
||||
|
||||
Generated
+59
-4
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.814.0",
|
||||
"version": "1.815.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.814.0",
|
||||
"version": "1.815.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
@@ -1733,6 +1733,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1749,6 +1750,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1765,6 +1767,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1781,6 +1784,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1797,6 +1801,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1813,6 +1818,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1829,6 +1835,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1845,6 +1852,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1861,6 +1869,7 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1877,6 +1886,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1893,6 +1903,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1909,6 +1920,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1925,6 +1937,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1941,6 +1954,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7534,7 +7548,7 @@
|
||||
"version": "1.21.7",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
@@ -8240,6 +8254,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8260,6 +8275,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8280,6 +8296,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8300,6 +8317,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8320,6 +8338,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8340,6 +8359,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8360,6 +8380,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8380,6 +8401,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8400,6 +8422,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8420,6 +8443,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8440,6 +8464,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -13187,6 +13212,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check/node_modules/picomatch": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
||||
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-eslint-parser": {
|
||||
"version": "0.43.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
|
||||
@@ -13397,6 +13437,21 @@
|
||||
"svelte": "^4.2.19 || ^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.70.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz",
|
||||
"integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte/node_modules/esrap": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.7.tgz",
|
||||
@@ -13983,7 +14038,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.814.0",
|
||||
"version": "1.815.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"dev:ui-builder": "mv static/ui_builder static/ui_builder.dev-disabled 2>/dev/null || true ; trap 'mv static/ui_builder.dev-disabled static/ui_builder 2>/dev/null || true' EXIT ; vite dev",
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
import Select from './select/Select.svelte'
|
||||
import { fetchAvailableModels, AI_PROVIDERS } from './copilot/lib'
|
||||
import type { AIProvider, ProviderConfig } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import ResourcePicker from './ResourcePicker.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { saveConfig, removeConfig, isSameAsStoredConfig } from './aiProviderStorage'
|
||||
import AIReasoningEffortPicker from './AIReasoningEffortPicker.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
value: ProviderConfig | undefined
|
||||
@@ -28,7 +26,7 @@
|
||||
workspace = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace ?? '')
|
||||
let effectiveWorkspace = $derived(workspace ?? $workspaceStore ?? '')
|
||||
|
||||
let value = $derived.by(() => {
|
||||
if (!_uncheckedValue || typeof _uncheckedValue !== 'object') return undefined
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ExternalLink, Globe } from 'lucide-svelte'
|
||||
import { JobService, type Job } from '$lib/gen'
|
||||
import { base } from '$lib/base'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { msToReadableTimeShort } from '$lib/utils'
|
||||
import ChatCollapsibleCard from './copilot/chat/ChatCollapsibleCard.svelte'
|
||||
import ToolContentDisplay from './copilot/chat/ToolContentDisplay.svelte'
|
||||
@@ -9,9 +10,6 @@
|
||||
import GfmMarkdown from './GfmMarkdown.svelte'
|
||||
import type { AgentTraceEntry } from './agentTrace'
|
||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
entries: AgentTraceEntry[]
|
||||
@@ -42,7 +40,7 @@
|
||||
try {
|
||||
jobs.set(
|
||||
jobId,
|
||||
await JobService.getJob({ id: jobId, workspace: workspaceId ?? $operatingWorkspace! })
|
||||
await JobService.getJob({ id: jobId, workspace: workspaceId ?? $workspaceStore! })
|
||||
)
|
||||
} catch {
|
||||
// A tool job can be gone (retention) or unreadable. The row still has its
|
||||
@@ -123,7 +121,7 @@
|
||||
{:else if entry.jobId}
|
||||
<a
|
||||
class="text-2xs text-accent inline-flex items-center gap-1 w-fit hover:underline"
|
||||
href="{base}/run/{entry.jobId}?workspace={workspaceId ?? $operatingWorkspace}"
|
||||
href="{base}/run/{entry.jobId}?workspace={workspaceId ?? $workspaceStore}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { OauthService, type ResourceType } from '$lib/gen'
|
||||
import FilesetEditor from './FilesetEditor.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { emptySchema, emptyString } from '$lib/utils'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
@@ -19,9 +20,6 @@
|
||||
import { base } from '$lib/base'
|
||||
import { isDataTableWizardEnabled } from './workspaceSettings/utils.svelte'
|
||||
import { parsePostgresConnectionString } from '$lib/utils/postgresConnectionString'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
resourceType: string
|
||||
@@ -154,7 +152,7 @@
|
||||
}
|
||||
}
|
||||
$effect(() => {
|
||||
$operatingWorkspace && untrack(() => loadSchema())
|
||||
$workspaceStore && untrack(() => loadSchema())
|
||||
})
|
||||
$effect(() => {
|
||||
notFound && rawCode && untrack(() => parseJson())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy'
|
||||
|
||||
import { userStore } from '$lib/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import LabelsInput from './LabelsInput.svelte'
|
||||
import IconedResourceType from './IconedResourceType.svelte'
|
||||
import {
|
||||
@@ -53,9 +53,6 @@
|
||||
import Label from './Label.svelte'
|
||||
import ResourcePathHint from './ResourcePathHint.svelte'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
step?: number
|
||||
@@ -87,7 +84,7 @@
|
||||
fillPath = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace!)
|
||||
let effectiveWorkspace = $derived(workspace ?? $workspaceStore!)
|
||||
|
||||
let isValid = $state(true)
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { ResourceService, VariableService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { copyToClipboard, truncate } from '$lib/utils'
|
||||
import { ClipboardCopy, Expand } from 'lucide-svelte'
|
||||
import Drawer from './common/drawer/Drawer.svelte'
|
||||
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { Button, DrawerContent } from './common'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
value: any
|
||||
@@ -24,14 +22,14 @@
|
||||
|
||||
async function getResource(path: string) {
|
||||
jsonViewerContent = await ResourceService.getResourceValue({
|
||||
workspace: $operatingWorkspace!,
|
||||
workspace: $workspaceStore!,
|
||||
path
|
||||
})
|
||||
}
|
||||
|
||||
async function getVariable(path: string) {
|
||||
jsonViewerContent = await VariableService.getVariableValue({
|
||||
workspace: $operatingWorkspace!,
|
||||
workspace: $workspaceStore!,
|
||||
path
|
||||
})
|
||||
}
|
||||
|
||||
@@ -41,14 +41,12 @@
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
import S3ArgInput from './common/fileUpload/S3ArgInput.svelte'
|
||||
import { base } from '$lib/base'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getJsonSchemaFromResource } from './schema/jsonSchemaResource.svelte'
|
||||
import AIProviderPicker from './AIProviderPicker.svelte'
|
||||
import TextInput from './text_input/TextInput.svelte'
|
||||
import FileInput from './common/fileInput/FileInput.svelte'
|
||||
import { randomUUID } from '$lib/utils/uuid'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
label?: string
|
||||
@@ -825,7 +823,7 @@
|
||||
/>
|
||||
{/await}
|
||||
{:else if inputCat == 'object' && format?.startsWith('jsonschema-')}
|
||||
{#await getJsonSchemaFromResource(format.substring('jsonschema-'.length), workspace ?? $operatingWorkspace ?? '')}
|
||||
{#await getJsonSchemaFromResource(format.substring('jsonschema-'.length), workspace ?? $workspaceStore ?? '')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then schema}
|
||||
{#if !schema || !schema.properties}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { JobService, type FlowValue } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { tryEvery } from '$lib/utils'
|
||||
import { Check, LoaderCircle, Server, X, Cpu } from 'lucide-svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface CredentialsCheckResult {
|
||||
available: boolean
|
||||
@@ -30,7 +28,7 @@
|
||||
apiResult = null
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/w/${$operatingWorkspace}/ai/check_bedrock_credentials`)
|
||||
const response = await fetch(`/api/w/${$workspaceStore}/ai/check_bedrock_credentials`)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error: ${response.status}`)
|
||||
}
|
||||
@@ -80,7 +78,7 @@
|
||||
}
|
||||
|
||||
const job = await JobService.runFlowPreview({
|
||||
workspace: $operatingWorkspace!,
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
value: flowValue as unknown as FlowValue,
|
||||
args: {}
|
||||
@@ -90,7 +88,7 @@
|
||||
tryEvery({
|
||||
tryCode: async () => {
|
||||
const testResult = await JobService.getCompletedJob({
|
||||
workspace: $operatingWorkspace!,
|
||||
workspace: $workspaceStore!,
|
||||
id: job
|
||||
})
|
||||
|
||||
@@ -132,7 +130,7 @@
|
||||
workerStatus = 'error'
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace: $operatingWorkspace!,
|
||||
workspace: $workspaceStore!,
|
||||
id: job,
|
||||
requestBody: {
|
||||
reason: 'Timeout checking Bedrock credentials'
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Select from './select/Select.svelte'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { RefreshCcw } from 'lucide-svelte'
|
||||
import { Button } from './common'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface ChannelItem {
|
||||
channel_id?: string
|
||||
@@ -23,8 +21,8 @@
|
||||
showRefreshButton?: boolean
|
||||
onError?: (error: Error) => void
|
||||
onSelectedChannelChange?: (channel: ChannelItem | undefined) => void
|
||||
/** Workspace to list Teams channels from; defaults to the operating workspace (see
|
||||
* `useOperatingWorkspace`). */
|
||||
/** Workspace to list Teams channels from; defaults to the nav
|
||||
* `$workspaceStore`. A forked session passes its acting workspace. */
|
||||
workspace?: string
|
||||
}
|
||||
|
||||
@@ -42,7 +40,7 @@
|
||||
workspace = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace)
|
||||
let effectiveWorkspace = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
let isFetching = $state(false)
|
||||
let loadedChannels = $state<ChannelItem[]>([])
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import ClipboardPanel from './details/ClipboardPanel.svelte'
|
||||
import Section from './Section.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
let url = $derived(`${window.location.protocol}//${window.location.hostname}/`)
|
||||
</script>
|
||||
@@ -22,7 +20,7 @@
|
||||
<span class="font-medium">Setup the wmill cli for this workspace & remote:</span>
|
||||
<div class="mt-1">
|
||||
<ClipboardPanel
|
||||
content={`wmill workspace add ${$operatingWorkspace} ${$operatingWorkspace} ${url}`}
|
||||
content={`wmill workspace add ${$workspaceStore} ${$workspaceStore} ${url}`}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { superadmin, type DBSchema } from '$lib/stores'
|
||||
import { superadmin, userStore, type DBSchema } from '$lib/stores'
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
EditIcon,
|
||||
@@ -33,10 +33,6 @@
|
||||
import type { DbFeatures } from './apps/components/display/dbtable/dbFeatures'
|
||||
import Star from './Star.svelte'
|
||||
import type { Asset } from '$lib/gen'
|
||||
import { useOperatingUser } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
|
||||
/** Represents a selected table with its schema */
|
||||
export interface SelectedTable {
|
||||
@@ -648,7 +644,7 @@
|
||||
Import schema from database
|
||||
</span>
|
||||
</button>
|
||||
{#if !!actingUser?.is_admin || !!$superadmin}
|
||||
{#if !!$userStore?.is_admin || !!$superadmin}
|
||||
<button
|
||||
onclick={() => onImport('schema_and_data')}
|
||||
class="hover:opacity-70 transition-opacity rounded-md border aspect-square w-52 gap-4 p-4 center-center flex-col"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { dbSchemas, type DBSchema } from '$lib/stores'
|
||||
import { dbSchemas, workspaceStore, type DBSchema } from '$lib/stores'
|
||||
import { sortArray } from '$lib/utils'
|
||||
import { Loader2, RefreshCcw } from 'lucide-svelte'
|
||||
import Alert from './common/alert/Alert.svelte'
|
||||
@@ -29,9 +29,6 @@
|
||||
import { createAsyncConfirmationModal } from './common/confirmationModal/asyncConfirmationModal.svelte'
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
import { outOfOrderRunMessage } from './workspaceSettings/datatableMigrationUtils'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
input?: DbInput
|
||||
@@ -47,8 +44,9 @@
|
||||
/** Tables that are already added and should show as disabled */
|
||||
disabledTables?: SelectedTable[]
|
||||
onImport?: (mode: 'schema_and_data' | 'schema_only') => void
|
||||
/** Workspace the datatable/schema lookups run against. Defaults to the operating
|
||||
* workspace (see `useOperatingWorkspace`). */
|
||||
/** Workspace the datatable/schema lookups run against. Defaults to the
|
||||
* navigation `$workspaceStore`; pass the acting workspace when embedded in
|
||||
* a session preview whose workspace differs from the top nav. */
|
||||
workspace?: string
|
||||
/** Worker tag every job of this manager runs on, overriding the database
|
||||
* language's native tag. Bound so the hints below can offer to set it. */
|
||||
@@ -70,7 +68,7 @@
|
||||
workerTag = $bindable()
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspace ?? $operatingWorkspace)
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
let dbSchema: DBSchema | undefined = $derived(input && $dbSchemas[schemaCacheKey(input)])
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { createGrid, type GridApi, type IDatasource } from 'ag-grid-community'
|
||||
import { transformColumnDefs } from './apps/components/display/table/utils'
|
||||
@@ -81,9 +82,6 @@
|
||||
import 'ag-grid-community/styles/ag-theme-alpine.css'
|
||||
import '$lib/components/apps/components/display/table/theme/windmill-theme.css'
|
||||
import { untrack } from 'svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
type Props = {
|
||||
dbTableOps: IDbTableOps
|
||||
@@ -103,7 +101,7 @@
|
||||
|
||||
let datasource: IDatasource = {
|
||||
getRows: async function (params) {
|
||||
if (!$operatingWorkspace) return params.failCallback()
|
||||
if (!$workspaceStore) return params.failCallback()
|
||||
let lastRow = rowCount && rowCount <= params.endRow ? rowCount : -1
|
||||
|
||||
const items = await dbTableOps.getRows({
|
||||
@@ -134,7 +132,7 @@
|
||||
minWidth: 150,
|
||||
editable: true,
|
||||
onCellValueChanged: (e) => {
|
||||
if (!$operatingWorkspace) return
|
||||
if (!$workspaceStore) return
|
||||
const colDef = e.colDef as unknown as { field: string; datatype: string }
|
||||
dbTableOps
|
||||
.onUpdate?.(
|
||||
@@ -171,7 +169,7 @@
|
||||
|
||||
let prevUpdateKey: any = undefined
|
||||
$effect(() => {
|
||||
if (!$operatingWorkspace || !api) return
|
||||
if (!$workspaceStore || !api) return
|
||||
const key = { quicksearch, colDefs: dbTableOps.colDefs, refreshCount, rowFilter }
|
||||
if (deepEqual(key, prevUpdateKey)) return
|
||||
prevUpdateKey = key
|
||||
@@ -193,7 +191,7 @@
|
||||
),
|
||||
...(dbTableOps.onDelete && {
|
||||
onDelete: (values) => {
|
||||
if (!$operatingWorkspace) return
|
||||
if (!$workspaceStore) return
|
||||
dbTableOps
|
||||
.onDelete?.({ values })
|
||||
.then(() => {
|
||||
@@ -251,7 +249,7 @@
|
||||
columnDefs={dbTableOps.colDefs ?? []}
|
||||
dbType={dbTableOps.dbType}
|
||||
onInsert={(values) => {
|
||||
if (!$operatingWorkspace) return
|
||||
if (!$workspaceStore) return
|
||||
dbTableOps.onInsert?.({ values }).then((result) => {
|
||||
refresh?.()
|
||||
sendUserToast('Row inserted')
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
import Select from './select/Select.svelte'
|
||||
import ExploreAssetButton, { assetCanBeExplored } from './ExploreAssetButton.svelte'
|
||||
import { usePromise } from '$lib/svelte5Utils.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
value?: string | undefined
|
||||
@@ -31,7 +29,7 @@
|
||||
}: Props = $props()
|
||||
|
||||
let datatables = usePromise(() =>
|
||||
WorkspaceService.listDataTables({ workspace: $operatingWorkspace ?? '' }).then((d) =>
|
||||
WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' }).then((d) =>
|
||||
d.map((d) => d.name)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { userStore } from '$lib/stores'
|
||||
import { SettingsIcon } from 'lucide-svelte'
|
||||
import { Button } from './common'
|
||||
import Drawer from './common/drawer/Drawer.svelte'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
import DefaultScriptsInner from './DefaultScriptsInner.svelte'
|
||||
import { useOperatingUser } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
|
||||
interface Props {
|
||||
placement?: 'left' | 'right'
|
||||
@@ -20,7 +17,7 @@
|
||||
let drawer: Drawer | undefined = $state()
|
||||
</script>
|
||||
|
||||
{#if actingUser?.is_admin || actingUser?.is_super_admin}
|
||||
{#if $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
<Drawer bind:this={drawer} {placement}>
|
||||
<DrawerContent title="Edit Default Scripts" on:close={drawer?.closeDrawer}>
|
||||
<DefaultScriptsInner />
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { WorkspaceService, type Script, type WorkspaceDefaultScripts } from '$lib/gen'
|
||||
import { defaultScripts } from '$lib/stores'
|
||||
import { defaultScripts, workspaceStore } from '$lib/stores'
|
||||
import { flip } from 'svelte/animate'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { defaultScriptLanguages } from '$lib/scripts'
|
||||
import Alert from './common/alert/Alert.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
small?: boolean
|
||||
@@ -32,7 +29,7 @@
|
||||
}
|
||||
defaultScripts.update((s) => ({ ...s, order: norder }))
|
||||
await WorkspaceService.editDefaultScripts({
|
||||
workspace: $operatingWorkspace!,
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: $defaultScripts
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
import Select from './select/Select.svelte'
|
||||
import ExploreAssetButton, { assetCanBeExplored } from './ExploreAssetButton.svelte'
|
||||
import { usePromise } from '$lib/svelte5Utils.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
value?: string | undefined
|
||||
@@ -31,8 +29,9 @@
|
||||
}: Props = $props()
|
||||
|
||||
let ducklakes = usePromise(() =>
|
||||
WorkspaceService.listDucklakes({ workspace: $operatingWorkspace ?? '' })
|
||||
WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? '' })
|
||||
)
|
||||
|
||||
</script>
|
||||
|
||||
<div class={className}>
|
||||
@@ -47,6 +46,9 @@
|
||||
{onClear}
|
||||
/>
|
||||
{#if showSchemaExplorer && value && assetCanBeExplored({ kind: 'ducklake', path: value })}
|
||||
<ExploreAssetButton class="mt-1 w-fit" asset={{ kind: 'ducklake', path: value }} />
|
||||
<ExploreAssetButton
|
||||
class="mt-1 w-fit"
|
||||
asset={{ kind: 'ducklake', path: value }}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
const bubble = createBubbler()
|
||||
import type { Schema } from '$lib/common'
|
||||
import { VariableService, type ScriptLang } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Button } from './common'
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
import VariableEditor from './VariableEditor.svelte'
|
||||
@@ -35,9 +36,6 @@
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Editor from './Editor.svelte'
|
||||
import AddPropertyV2 from './schema/AddPropertyV2.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
// export let openEditTab: () => void = () => {}
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -130,7 +128,7 @@
|
||||
workspace = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspace ?? $operatingWorkspace)
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
$effect.pre(() => {
|
||||
if (args == undefined) {
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
import { editorConfig, registerWebviewPaste, updateOptions } from '$lib/editorUtils'
|
||||
import { editorFontSize } from '$lib/editorFontSize.svelte'
|
||||
import { createHash as randomHash } from '$lib/editorLangUtils'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import DdlMigrationGuard from './DdlMigrationGuard.svelte'
|
||||
import {
|
||||
type Preview,
|
||||
@@ -119,9 +120,6 @@
|
||||
import { rawAppLintStore, type MonacoLintError } from './raw_apps/lintStore'
|
||||
import { MarkerSeverity } from 'monaco-editor'
|
||||
import { resource, useDebounce, watch } from 'runed'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
// import EditorTheme from './EditorTheme.svelte'
|
||||
|
||||
let divEl: HTMLDivElement | null = $state(null)
|
||||
@@ -686,7 +684,7 @@
|
||||
// via a short-TTL cache — macros are late-bound, so mild staleness is fine.
|
||||
async function addWorkspaceMacroCompletions() {
|
||||
workspaceMacroCompletor?.dispose()
|
||||
const workspace = $operatingWorkspace
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) return
|
||||
let macros: Awaited<ReturnType<typeof listWorkspaceMacrosCached>> = []
|
||||
try {
|
||||
@@ -741,7 +739,7 @@
|
||||
provideCompletionItems: async function (model, position) {
|
||||
// Read the store per request, not at registration — the provider
|
||||
// outlives a workspace switch.
|
||||
const workspace = $operatingWorkspace
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) return { suggestions: [] }
|
||||
const before = model.getLineContent(position.lineNumber).slice(0, position.column - 1)
|
||||
if (!/^\s*(\/\/|--|#)\s*(column|data_test|on|materialize)\b/.test(before)) {
|
||||
@@ -782,7 +780,7 @@
|
||||
$dbSchemas[resourcePath] = await getDbSchemas(
|
||||
lang === 'graphql' ? 'graphql' : (scriptLang ?? ''),
|
||||
resourcePath,
|
||||
$operatingWorkspace,
|
||||
$workspaceStore,
|
||||
(e) => console.error(`error getting ${lang} (${scriptLang}) db schema`, e),
|
||||
{ customTag }
|
||||
)
|
||||
@@ -1780,9 +1778,9 @@
|
||||
let customTsTypesData = resource([() => lang], async () => {
|
||||
if (lang !== 'typescript') return undefined
|
||||
let datatables = (
|
||||
await WorkspaceService.listDataTables({ workspace: $operatingWorkspace ?? '' })
|
||||
await WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' })
|
||||
).map((d) => d.name)
|
||||
let ducklakes = await WorkspaceService.listDucklakes({ workspace: $operatingWorkspace ?? '' })
|
||||
let ducklakes = await WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? '' })
|
||||
return { datatables, ducklakes }
|
||||
})
|
||||
function setTypescriptCustomTypes() {
|
||||
@@ -1824,7 +1822,7 @@
|
||||
scriptLang === 'nativets')
|
||||
) {
|
||||
const resourceTypes = await ResourceService.listResourceType({
|
||||
workspace: $operatingWorkspace ?? ''
|
||||
workspace: $workspaceStore ?? ''
|
||||
})
|
||||
|
||||
const namespace = formatResourceTypes(
|
||||
@@ -2025,7 +2023,7 @@
|
||||
$lspTokenStore = newToken
|
||||
token = newToken
|
||||
}
|
||||
let root = hostname + '/api/scripts_u/tokened_raw/' + $operatingWorkspace + '/' + token
|
||||
let root = hostname + '/api/scripts_u/tokened_raw/' + $workspaceStore + '/' + token
|
||||
return root
|
||||
}
|
||||
|
||||
@@ -2276,10 +2274,10 @@
|
||||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
<EditorTheme />
|
||||
{#if datatableForMigrations && $operatingWorkspace}
|
||||
{#if datatableForMigrations && $workspaceStore}
|
||||
<DdlMigrationGuard
|
||||
bind:this={ddlGuard}
|
||||
workspace={$operatingWorkspace}
|
||||
workspace={$workspaceStore}
|
||||
datatable={datatableForMigrations}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<script lang="ts">
|
||||
import { ResourceService, VariableService, WorkspaceService, type Script } from '$lib/gen'
|
||||
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { base } from '$lib/base'
|
||||
import type Editor from './Editor.svelte'
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
@@ -75,9 +76,6 @@
|
||||
import FlowInlineScriptAiButton from './copilot/FlowInlineScriptAIButton.svelte'
|
||||
import GitRepoPopoverPicker from './GitRepoPopoverPicker.svelte'
|
||||
import { insertDelegateToGitRepoInCode } from '$lib/ansibleUtils'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
lang: SupportedLanguage | 'bunnative' | undefined
|
||||
@@ -123,8 +121,9 @@
|
||||
right?: import('svelte').Snippet
|
||||
openAiChat?: boolean
|
||||
moduleId?: string
|
||||
// Workspace to scope variable/resource/data-table lookups to. Defaults to the
|
||||
// operating workspace (see `useOperatingWorkspace`).
|
||||
// Workspace to scope variable/resource/data-table lookups to. Defaults to
|
||||
// the nav `$workspaceStore`; an AI-session live editor passes the session's
|
||||
// acting workspace (a fork) so the helper pickers hit the right workspace.
|
||||
workspace?: string
|
||||
}
|
||||
|
||||
@@ -154,7 +153,7 @@
|
||||
workspace = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspace ?? $operatingWorkspace)
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
let contextualVariablePicker: ItemPicker | undefined = $state()
|
||||
let variablePicker: ItemPicker | undefined = $state()
|
||||
|
||||
@@ -15,15 +15,7 @@
|
||||
} from '$lib/components/workspacePicker'
|
||||
import BreadcrumbSegment from '$lib/components/BreadcrumbSegment.svelte'
|
||||
import { isOwner } from '$lib/utils'
|
||||
import { userStore } from '$lib/stores'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
|
||||
interface Props {
|
||||
summary?: string
|
||||
@@ -55,8 +47,9 @@
|
||||
* dropped, leaving only the summary. Used by the condensed session-
|
||||
* preview top bar to save vertical room. */
|
||||
hidePath?: boolean
|
||||
/** Workspace whose items the breadcrumb picker lists; defaults to the operating
|
||||
* workspace (see `useOperatingWorkspace`). */
|
||||
/** Workspace whose items the breadcrumb picker lists. Session live
|
||||
* editors pass their acting workspace so the picker isn't scoped to the
|
||||
* navigation workspace; falls back to $workspaceStore in the picker. */
|
||||
workspaceId?: string
|
||||
}
|
||||
|
||||
@@ -123,7 +116,7 @@
|
||||
// Treat an empty path as ownable so the pen popover lets a user pick the
|
||||
// path for a brand-new item. `Path.reset()` then synthesizes a default
|
||||
// under their own user/folder scope.
|
||||
let own = $derived(!path || isOwner(path, actingUser, $operatingWorkspace))
|
||||
let own = $derived(!path || isOwner(path, $userStore, $workspaceStore))
|
||||
|
||||
// Virtual entry for the picker: surfaces the currently-edited item at its
|
||||
// live path (which may differ from `savedPath` mid-rename, so the picker
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
import type { Schema, SupportedLanguage } from '$lib/common'
|
||||
import { base } from '$lib/base'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import { enterpriseLicense, workspaceStore } from '$lib/stores'
|
||||
import MsTeamsIcon from '$lib/components/icons/MSTeamsIcon.svelte'
|
||||
import { classNames, emptySchema, emptyString, sendUserToast, tryEvery } from '$lib/utils'
|
||||
import MultiSelect from '$lib/components/select/MultiSelect.svelte'
|
||||
@@ -61,9 +61,6 @@
|
||||
import SmtpConfigurationStatus from './common/smtp/SmtpConfigurationStatus.svelte'
|
||||
import { SettingService } from '$lib/gen'
|
||||
import { isSmtpSettingsValid } from './instanceSettings/SmtpSettings.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
const slackRecoveryHandler = hubPaths.slackRecoveryHandler
|
||||
const slackHandlerScriptPath = hubPaths.slackErrorHandler
|
||||
@@ -84,8 +81,9 @@
|
||||
customHandlerKind?: 'flow' | 'script'
|
||||
customTabTooltip?: import('svelte').Snippet
|
||||
noMargin?: boolean
|
||||
/** Workspace for handler lookup / settings / test jobs. Defaults to the operating
|
||||
* workspace (see `useOperatingWorkspace`). */
|
||||
/** Workspace for handler lookup / settings / test jobs. Defaults to the
|
||||
* nav `$workspaceStore`; a trigger editor in a forked session passes its
|
||||
* acting workspace so the handler is resolved and saved there. */
|
||||
workspace?: string
|
||||
/** Offer the instance critical alert channels as a destination. Workspace-level
|
||||
* error handling only: schedules and triggers have no such setting. */
|
||||
@@ -108,12 +106,13 @@
|
||||
showInstanceAlerts = false
|
||||
}: Props = $props()
|
||||
|
||||
let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace)
|
||||
// Carry the workspace onto the "create from template" route, so the handler script is
|
||||
// created where this handler is saved. `customScriptTemplate` already has a query string.
|
||||
let effectiveWorkspace = $derived(workspace ?? $workspaceStore)
|
||||
// Carry the acting workspace onto the "create from template" route when an
|
||||
// explicit override is set, so a forked session creates the handler script
|
||||
// there. `customScriptTemplate` already has a query string (`?hub=…`).
|
||||
let templateHref = $derived(
|
||||
effectiveWorkspace
|
||||
? `${customScriptTemplate}&workspace=${encodeURIComponent(effectiveWorkspace)}`
|
||||
workspace
|
||||
? `${customScriptTemplate}&workspace=${encodeURIComponent(workspace)}`
|
||||
: customScriptTemplate
|
||||
)
|
||||
|
||||
|
||||
@@ -33,18 +33,15 @@
|
||||
import { Button, ButtonType } from '$lib/components/common'
|
||||
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
|
||||
import { VolumeService } from '$lib/gen'
|
||||
import { globalDbManagerDrawer, globalS3FilePickerExplorer } from '$lib/stores'
|
||||
import {
|
||||
globalDbManagerDrawer,
|
||||
globalS3FilePickerExplorer,
|
||||
userStore,
|
||||
workspaceStore
|
||||
} from '$lib/stores'
|
||||
import { isS3Uri } from '$lib/utils'
|
||||
import { Database, File, HardDriveIcon } from 'lucide-svelte'
|
||||
import DucklakeIcon from './icons/DucklakeIcon.svelte'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
|
||||
const {
|
||||
asset,
|
||||
@@ -72,7 +69,7 @@
|
||||
} = $props()
|
||||
|
||||
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
|
||||
let ws = $derived(workspace ?? $operatingWorkspace)
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
const assetUri = $derived(formatAsset(asset))
|
||||
// Contexts with a select/upload flow pass their own picker; everything else
|
||||
// (e.g. the resources list) falls back to the global read-only explorer.
|
||||
@@ -83,7 +80,7 @@
|
||||
</script>
|
||||
|
||||
<Button
|
||||
disabled={actingUser?.operator || disabled}
|
||||
disabled={$userStore?.operator || disabled}
|
||||
unifiedSize={'md'}
|
||||
variant={buttonVariant}
|
||||
wrapperClasses={className}
|
||||
|
||||
@@ -17,7 +17,13 @@
|
||||
linkedAgentToolsVersion,
|
||||
migrateLinkedAgentToolsScope
|
||||
} from '$lib/components/flows/linkedAgentToolsStore.svelte'
|
||||
import { enterpriseLicense, userStore, userWorkspaces, usedTriggerKinds } from '$lib/stores'
|
||||
import {
|
||||
enterpriseLicense,
|
||||
userStore,
|
||||
userWorkspaces,
|
||||
workspaceStore,
|
||||
usedTriggerKinds
|
||||
} from '$lib/stores'
|
||||
import {
|
||||
generateRandomString,
|
||||
orderedJsonStringify,
|
||||
@@ -105,14 +111,6 @@
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { setOpenInSessionHandoff } from './sessions/openInSessionContext'
|
||||
import { getEditorStoragePath, setEditorStoragePath } from './editorStoragePathContext'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
|
||||
let {
|
||||
initialPath = $bindable(''),
|
||||
@@ -160,9 +158,9 @@
|
||||
// and the AutosaveIndicator all target it. Falls back to the global store, so
|
||||
// the full-page editor is unchanged; the sessions preview overrides it to the
|
||||
// session's (forked) workspace, so an embedded editor acts on the session's
|
||||
// fork rather than the navigation workspace (`workspaceStore`, which stays put).
|
||||
// fork rather than the navigation workspace ($workspaceStore, which stays put).
|
||||
// indicatorPath is the matching draft path.
|
||||
const opWorkspace = $derived(autosaveWorkspace ?? $operatingWorkspace)
|
||||
const opWorkspace = $derived(autosaveWorkspace ?? $workspaceStore)
|
||||
const indicatorPath = $derived(autosavePath ?? liveEditorDraftStoragePath)
|
||||
|
||||
let initialPathStore = writable(initialPath)
|
||||
@@ -609,7 +607,7 @@
|
||||
await deployTriggers(
|
||||
triggersToDeploy,
|
||||
opWorkspace,
|
||||
!!actingUser?.is_admin || !!actingUser?.is_super_admin,
|
||||
!!$userStore?.is_admin || !!$userStore?.is_super_admin,
|
||||
usedTriggerKinds,
|
||||
$pathStore,
|
||||
true
|
||||
@@ -620,7 +618,7 @@
|
||||
await deployTriggers(
|
||||
triggersToDeploy,
|
||||
opWorkspace,
|
||||
!!actingUser?.is_admin || !!actingUser?.is_super_admin,
|
||||
!!$userStore?.is_admin || !!$userStore?.is_super_admin,
|
||||
usedTriggerKinds,
|
||||
initialPath
|
||||
)
|
||||
@@ -1396,7 +1394,7 @@
|
||||
<AIChangesWarningModal bind:open={aiChangesWarningOpen} onConfirm={aiChangesConfirmCallback} />
|
||||
|
||||
{#key renderCount}
|
||||
{#if !actingUser?.operator}
|
||||
{#if !$userStore?.operator}
|
||||
{#if $pathStore}
|
||||
<FlowHistory bind:this={flowHistory} path={$pathStore} {onHistoryRestore} />
|
||||
{/if}
|
||||
|
||||
@@ -10,12 +10,10 @@
|
||||
import FlowGraphViewerStep from './FlowGraphViewerStep.svelte'
|
||||
import FlowGraphV2 from './graph/FlowGraphV2.svelte'
|
||||
import { dfs } from './flows/dfs'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { untrack } from 'svelte'
|
||||
import { publishLinkedAgentTools } from './flows/flowState'
|
||||
import { linkedToolsScope } from './flows/linkedAgentToolsStore.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
flow: {
|
||||
@@ -47,7 +45,7 @@
|
||||
noGraph = false,
|
||||
triggerNode = false,
|
||||
stepDetail = $bindable(undefined),
|
||||
workspace = $operatingWorkspace,
|
||||
workspace = $workspaceStore,
|
||||
minHeight = 400,
|
||||
noBorder = false,
|
||||
hideDefaultInputs = false,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import SchemaViewer from './SchemaViewer.svelte'
|
||||
import { scriptPathToHref } from '$lib/scripts'
|
||||
import { cleanExpr, copyToClipboard } from '$lib/utils'
|
||||
import { hubBaseUrlStore } from '$lib/stores'
|
||||
import { hubBaseUrlStore, workspaceStore } from '$lib/stores'
|
||||
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import FlowModuleScript from './flows/content/FlowModuleScript.svelte'
|
||||
@@ -20,9 +20,6 @@
|
||||
import HighlightTheme from './HighlightTheme.svelte'
|
||||
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
|
||||
import FlowGraphViewerStepHeader from './FlowGraphViewerStepHeader.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
schema?: any | undefined
|
||||
@@ -44,7 +41,7 @@
|
||||
workspace = undefined,
|
||||
onBack = undefined
|
||||
}: Props = $props()
|
||||
let ws = $derived(workspace ?? $operatingWorkspace)
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
let codeViewer: Drawer | undefined = $state()
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import PopoverV2 from '$lib/components/meltComponents/Popover.svelte'
|
||||
import HistoricInputs from './HistoricInputs.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { JobService } from '$lib/gen'
|
||||
import { Button } from './common'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
path: string
|
||||
@@ -29,7 +27,7 @@
|
||||
async function loadInitial() {
|
||||
loading = true
|
||||
let jobs = await JobService.listJobs({
|
||||
workspace: $operatingWorkspace!,
|
||||
workspace: $workspaceStore!,
|
||||
scriptPathExact: path,
|
||||
jobKinds: ['flow', 'flowpreview'].join(','),
|
||||
perPage: 1
|
||||
@@ -45,7 +43,7 @@
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if ($operatingWorkspace && !newFlow) {
|
||||
if ($workspaceStore && !newFlow) {
|
||||
untrack(() => loadInitial())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
Keyboard
|
||||
} from 'lucide-svelte'
|
||||
import { base } from '$lib/base'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
|
||||
import LogViewer from './LogViewer.svelte'
|
||||
import FlowLogViewer from './FlowLogViewer.svelte'
|
||||
@@ -25,9 +26,6 @@
|
||||
import { Tooltip } from './meltComponents'
|
||||
import FlowTimelineBar from './FlowTimelineBar.svelte'
|
||||
import { getActiveReplay } from './recording/replay.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
type RootJobData = Partial<Job>
|
||||
|
||||
@@ -98,7 +96,7 @@
|
||||
|
||||
function getJobLink(jobId: string | undefined): string {
|
||||
if (!jobId) return ''
|
||||
return `${base}/run/${jobId}?workspace=${workspaceId ?? $operatingWorkspace}`
|
||||
return `${base}/run/${jobId}?workspace=${workspaceId ?? $workspaceStore}`
|
||||
}
|
||||
|
||||
function getStatusColor(status: FlowStatusModule['type'] | undefined): string {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { type Job, JobService, type FlowModule, type RestartedFrom } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Button } from './common'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
@@ -10,9 +11,6 @@
|
||||
import FlowProgressBar from './flows/FlowProgressBar.svelte'
|
||||
import { CornerDownLeft, Play, RefreshCw, X } from 'lucide-svelte'
|
||||
import type { Schema } from '$lib/common'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
@@ -167,7 +165,7 @@
|
||||
try {
|
||||
jobId &&
|
||||
(await JobService.cancelQueuedJob({
|
||||
workspace: opWorkspace?.() ?? $operatingWorkspace ?? '',
|
||||
workspace: opWorkspace?.() ?? $workspaceStore ?? '',
|
||||
id: jobId,
|
||||
requestBody: {}
|
||||
}))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
type OpenFlow,
|
||||
type ScriptLang
|
||||
} from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Badge, Button } from './common'
|
||||
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
@@ -43,9 +44,6 @@
|
||||
import FlowRestartButton from './FlowRestartButton.svelte'
|
||||
import { useNestedRestartState } from './useNestedRestartState.svelte'
|
||||
import { buildFlowRecording, downloadRecordingJson } from './recording/runRecording'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
previewMode: 'upTo' | 'whole'
|
||||
@@ -140,7 +138,7 @@
|
||||
opWorkspace
|
||||
} = $state(getContext<FlowEditorContext>('FlowEditorContext'))
|
||||
// Acting workspace when previewing inside an AI session; else the nav workspace.
|
||||
let opWs = $derived(opWorkspace?.() ?? $operatingWorkspace)
|
||||
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let renderCount: number = $state(0)
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
import { Play, RefreshCw } from 'lucide-svelte'
|
||||
import { FlowService, JobService, type FlowVersion } from '$lib/gen'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { emptyString, sendUserToast } from '$lib/utils'
|
||||
|
||||
interface Props {
|
||||
@@ -168,7 +166,7 @@
|
||||
flow_version: flowVersion
|
||||
}
|
||||
let run = await JobService.restartFlowAtStep({
|
||||
workspace: $operatingWorkspace!,
|
||||
workspace: $workspaceStore!,
|
||||
id: jobId,
|
||||
requestBody
|
||||
})
|
||||
@@ -180,7 +178,7 @@
|
||||
loadingVersions = true
|
||||
try {
|
||||
flowVersions = await FlowService.getFlowHistory({
|
||||
workspace: $operatingWorkspace!,
|
||||
workspace: $workspaceStore!,
|
||||
path: flowPath
|
||||
})
|
||||
if (flowVersions.length > 0) {
|
||||
|
||||
@@ -4,11 +4,8 @@
|
||||
import { setContext, untrack } from 'svelte'
|
||||
import type { DurationStatus, FlowStatusViewerContext, GraphModuleState } from './graph'
|
||||
import { isOwner as loadIsOwner, type StateStore } from '$lib/utils'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import type { CompletedJob, FlowModule, FlowNote, FlowValue, Job } from '$lib/gen'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
jobId: string
|
||||
@@ -101,7 +98,7 @@
|
||||
})
|
||||
|
||||
function loadOwner(path: string) {
|
||||
isOwner = loadIsOwner(path, $userStore!, workspaceId ?? $operatingWorkspace!)
|
||||
isOwner = loadIsOwner(path, $userStore!, workspaceId ?? $workspaceStore!)
|
||||
}
|
||||
|
||||
async function updateJobId() {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
type FlowNote,
|
||||
type FlowValue
|
||||
} from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { base } from '$lib/base'
|
||||
import FlowJobResult from './FlowJobResult.svelte'
|
||||
import WorkflowTimeline from './WorkflowTimeline.svelte'
|
||||
@@ -68,9 +69,6 @@
|
||||
releaseLinkedToolsScope,
|
||||
retainLinkedToolsScope
|
||||
} from './flows/linkedAgentToolsStore.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
let {
|
||||
flowState: flowStateStore,
|
||||
@@ -170,7 +168,7 @@
|
||||
isSubflow = false,
|
||||
reducedPolling = false,
|
||||
wideResults = false,
|
||||
workspace = $operatingWorkspace,
|
||||
workspace = $workspaceStore,
|
||||
prefix = undefined,
|
||||
topModuleStates = undefined,
|
||||
refreshGlobal,
|
||||
@@ -244,7 +242,7 @@
|
||||
resourceMetadataCache[asset.path] = undefined
|
||||
if (!isReplay) {
|
||||
ResourceService.getResource({
|
||||
workspace: workspace ?? $operatingWorkspace!,
|
||||
workspace: workspace ?? $workspaceStore!,
|
||||
path: asset.path
|
||||
})
|
||||
.then((r) => (resourceMetadataCache[asset.path] = r))
|
||||
@@ -280,7 +278,7 @@
|
||||
// resolving in the navigation one finds nothing, or an unrelated resource sharing the path. The
|
||||
// store scope stays keyed on `workspace` to match what FlowGraphV2 reads — the job id in the key
|
||||
// already makes the bucket unique.
|
||||
let agentFetchWorkspace = $derived(workspaceId ?? job?.workspace_id ?? $operatingWorkspace)
|
||||
let agentFetchWorkspace = $derived(workspaceId ?? job?.workspace_id ?? $workspaceStore)
|
||||
// Hold this scope for as long as the viewer is mounted, so the store's cap can't drop tools the
|
||||
// run still needs (nothing would refetch them — the set of linked steps hasn't changed).
|
||||
$effect(() => {
|
||||
@@ -629,7 +627,7 @@
|
||||
) {
|
||||
if (!isReplay) {
|
||||
JobService.getJob({
|
||||
workspace: workspaceId ?? $operatingWorkspace ?? '',
|
||||
workspace: workspaceId ?? $workspaceStore ?? '',
|
||||
id: mod.job ?? '',
|
||||
noLogs: true,
|
||||
noCode: true
|
||||
@@ -724,7 +722,7 @@
|
||||
})
|
||||
if (!isReplay) {
|
||||
JobService.getStartedAtByIds({
|
||||
workspace: workspaceId ?? $operatingWorkspace ?? '',
|
||||
workspace: workspaceId ?? $workspaceStore ?? '',
|
||||
requestBody: missingStartedAtIds
|
||||
})
|
||||
.then((jobs) => {
|
||||
@@ -1571,7 +1569,7 @@
|
||||
let storedJob = storedListJobs[j]
|
||||
if (!storedJob && !isReplay) {
|
||||
storedJob = await JobService.getJob({
|
||||
workspace: workspaceId ?? $operatingWorkspace ?? '',
|
||||
workspace: workspaceId ?? $workspaceStore ?? '',
|
||||
id: loopJobId,
|
||||
noLogs: true,
|
||||
noCode: true
|
||||
@@ -2137,7 +2135,7 @@
|
||||
id={isReplay ? undefined : job.id}
|
||||
workspace={isReplay
|
||||
? undefined
|
||||
: (job.workspace_id ?? $operatingWorkspace ?? 'no_w')}
|
||||
: (job.workspace_id ?? $workspaceStore ?? 'no_w')}
|
||||
args={job.args}
|
||||
/>
|
||||
{:else}
|
||||
@@ -2204,7 +2202,7 @@
|
||||
id={isReplay ? undefined : node.job_id}
|
||||
workspace={isReplay
|
||||
? undefined
|
||||
: (job.workspace_id ?? $operatingWorkspace ?? 'no_w')}
|
||||
: (job.workspace_id ?? $workspaceStore ?? 'no_w')}
|
||||
args={node.args}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,14 +7,12 @@
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { cleanValueProperties, replaceFalseWithUndefined } from '$lib/utils'
|
||||
import { orderedYamlStringify } from '$lib/utils/orderedYaml'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { watch } from 'runed'
|
||||
|
||||
import HighlightTheme from './HighlightTheme.svelte'
|
||||
import FlowViewerInner from './FlowViewerInner.svelte'
|
||||
import FlowInputViewer from './FlowInputViewer.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface PreviousFlow {
|
||||
summary: string
|
||||
@@ -88,7 +86,7 @@
|
||||
return
|
||||
}
|
||||
previousFlow = await FlowService.getFlowVersion({
|
||||
workspace: $operatingWorkspace!,
|
||||
workspace: $workspaceStore!,
|
||||
version
|
||||
})
|
||||
previousFlowCache[version] = previousFlow
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
import {
|
||||
type Folder,
|
||||
type FolderDefaultPermissionedAs,
|
||||
@@ -90,12 +89,8 @@
|
||||
workspace
|
||||
}: Props = $props()
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const targetWorkspace = $derived(workspace ?? $operatingWorkspace ?? '')
|
||||
const aimedElsewhere = $derived(!!targetWorkspace && targetWorkspace !== $workspaceStore)
|
||||
// The group editor and permission history act on the operating workspace and take no
|
||||
// workspace of their own, so they can only follow a drawer aimed at that one.
|
||||
const offOperating = $derived(!!targetWorkspace && targetWorkspace !== $operatingWorkspace)
|
||||
const targetWorkspace = $derived(workspace ?? $workspaceStore ?? '')
|
||||
const aimedElsewhere = $derived(!!workspace && workspace !== $workspaceStore)
|
||||
|
||||
// `$userStore` describes the workspace the app is *in*. Aimed at another one it answers
|
||||
// the wrong question — a folder admin there would get read-only controls, and a
|
||||
@@ -126,9 +121,9 @@
|
||||
})
|
||||
|
||||
async function loadTargetUser(): Promise<void> {
|
||||
if (!aimedElsewhere) return
|
||||
if (!aimedElsewhere || !workspace) return
|
||||
try {
|
||||
targetUser = await UserService.whoami({ workspace: targetWorkspace })
|
||||
targetUser = await UserService.whoami({ workspace })
|
||||
} catch {
|
||||
// Not a member, or the call failed: no membership means read-only controls,
|
||||
// which is the safe reading — the write would be refused anyway.
|
||||
@@ -556,7 +551,7 @@
|
||||
let loadStarted = false
|
||||
$effect.pre(() => {
|
||||
if (loadStarted) return
|
||||
if (targetWorkspace && $userStore) {
|
||||
if ($workspaceStore && $userStore) {
|
||||
loadStarted = true
|
||||
untrack(() => {
|
||||
load()
|
||||
@@ -680,7 +675,10 @@
|
||||
class="grow min-w-0"
|
||||
>
|
||||
{#snippet endSnippet({ item, close: closeSelect })}
|
||||
{#if ownerKind == 'group' && !offOperating}
|
||||
<!-- GroupEditor reads and writes `$workspaceStore` and takes no workspace of its
|
||||
own, so it cannot follow a drawer aimed at another one: viewing a group
|
||||
there would edit the same-named group in the active workspace. -->
|
||||
{#if ownerKind == 'group' && !aimedElsewhere}
|
||||
<Button
|
||||
title="View group"
|
||||
variant="subtle"
|
||||
@@ -698,7 +696,7 @@
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet bottomSnippet({ close: closeSelect })}
|
||||
{#if ownerKind == 'group' && !offOperating}
|
||||
{#if ownerKind == 'group' && !aimedElsewhere}
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
@@ -840,9 +838,11 @@
|
||||
</Cell>
|
||||
<Cell last actions>
|
||||
<div class="flex items-center justify-end">
|
||||
<!-- The group editor decides on its own whether the group is editable here; a
|
||||
member with no write on it still gets to see who is in it. -->
|
||||
{#if ownerKindOf(perm.owner_name) === 'group' && !offOperating}
|
||||
<!-- The group editor reads `$workspaceStore`, so it can only be opened for the
|
||||
workspace the app is in — see the picker's own buttons. It decides on its
|
||||
own whether the group is editable here; a member with no write on it still
|
||||
gets to see who is in it. -->
|
||||
{#if ownerKindOf(perm.owner_name) === 'group' && !aimedElsewhere}
|
||||
<Button
|
||||
title="Manage group"
|
||||
variant="subtle"
|
||||
@@ -1001,7 +1001,9 @@
|
||||
</CollapseLink>
|
||||
{/if}
|
||||
|
||||
{#if !isNew && !offOperating && reloadHistory > 0}
|
||||
<!-- PermissionHistory fetches against `$workspaceStore`; aimed elsewhere it would show
|
||||
another folder's history entirely. -->
|
||||
{#if !isNew && !aimedElsewhere && reloadHistory > 0}
|
||||
{#key reloadHistory}
|
||||
<PermissionHistory
|
||||
{name}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { FolderService, UserService, type User } from '$lib/gen'
|
||||
import { workspaceStore, userStore } from '$lib/stores'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
import { isDemoWorkspaceRestricted } from '$lib/cloud'
|
||||
import { ChevronDown, Pen, PlusIcon } from 'lucide-svelte'
|
||||
import { Button } from './common'
|
||||
@@ -40,15 +39,14 @@
|
||||
workspace
|
||||
}: Props = $props()
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const targetWorkspace = $derived(workspace ?? $operatingWorkspace ?? '')
|
||||
const targetWorkspace = $derived(workspace ?? $workspaceStore ?? '')
|
||||
|
||||
// `$userStore` describes the workspace the app is *in*. When this picker is aimed
|
||||
// somewhere else, those memberships answer the wrong question — and since a folder
|
||||
// without write access renders disabled, a stale answer makes the real folders
|
||||
// unpickable. Resolve the membership for the workspace actually being listed.
|
||||
let targetUser: User | undefined = $state(undefined)
|
||||
const aimedElsewhere = $derived(!!targetWorkspace && targetWorkspace !== $workspaceStore)
|
||||
const aimedElsewhere = $derived(!!workspace && workspace !== $workspaceStore)
|
||||
const membership = $derived(aimedElsewhere ? targetUser : ($userStore ?? undefined))
|
||||
|
||||
const restricted = $derived(
|
||||
@@ -131,9 +129,9 @@
|
||||
}
|
||||
|
||||
async function loadTargetUser(): Promise<void> {
|
||||
if (!aimedElsewhere) return
|
||||
if (!workspace || workspace === $workspaceStore) return
|
||||
try {
|
||||
targetUser = await UserService.whoami({ workspace: targetWorkspace })
|
||||
targetUser = await UserService.whoami({ workspace })
|
||||
} catch {
|
||||
// Not a member, or the call failed: every folder stays read-only, which is
|
||||
// the safe reading — the import would be refused anyway.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import { workspaceStore, enterpriseLicense, userStore } from '$lib/stores'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import { Loader2, Github, RotateCw, Plus, Minus, Download, AlertTriangle } from 'lucide-svelte'
|
||||
@@ -18,14 +18,6 @@
|
||||
type GitHubAppState
|
||||
} from '$lib/githubApp'
|
||||
import RepositorySelector from './RepositorySelector.svelte'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
|
||||
interface Props {
|
||||
resourceType: string
|
||||
@@ -78,14 +70,14 @@
|
||||
|
||||
let showGitHubApp = $derived(
|
||||
resourceType === 'git_repository' &&
|
||||
$operatingWorkspace &&
|
||||
(actingUser?.is_admin || actingUser?.is_super_admin)
|
||||
$workspaceStore &&
|
||||
($userStore?.is_admin || $userStore?.is_super_admin)
|
||||
)
|
||||
|
||||
// Load GitHub installations when conditions are met
|
||||
$effect(() => {
|
||||
if (showGitHubApp && $enterpriseLicense && $operatingWorkspace) {
|
||||
loadGithubInstallations(githubState, $operatingWorkspace).catch((error) => {
|
||||
if (showGitHubApp && $enterpriseLicense && $workspaceStore) {
|
||||
loadGithubInstallations(githubState, $workspaceStore).catch((error) => {
|
||||
console.error('Failed to load GitHub installations:', error)
|
||||
})
|
||||
}
|
||||
@@ -121,11 +113,11 @@
|
||||
}
|
||||
|
||||
async function handleDeleteInstallation(installationId: number) {
|
||||
if (!$operatingWorkspace) return
|
||||
if (!$workspaceStore) return
|
||||
|
||||
try {
|
||||
await deleteInstallation($operatingWorkspace, installationId, () =>
|
||||
loadGithubInstallations(githubState, $operatingWorkspace!)
|
||||
await deleteInstallation($workspaceStore, installationId, () =>
|
||||
loadGithubInstallations(githubState, $workspaceStore!)
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete installation:', error)
|
||||
@@ -133,11 +125,11 @@
|
||||
}
|
||||
|
||||
async function handleAddInstallation(installationId: number, workspaceId: string) {
|
||||
if (!$operatingWorkspace) return
|
||||
if (!$workspaceStore) return
|
||||
|
||||
try {
|
||||
await addInstallationToWorkspace($operatingWorkspace, installationId, workspaceId, () =>
|
||||
loadGithubInstallations(githubState, $operatingWorkspace!)
|
||||
await addInstallationToWorkspace($workspaceStore, installationId, workspaceId, () =>
|
||||
loadGithubInstallations(githubState, $workspaceStore!)
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to add installation:', error)
|
||||
@@ -145,22 +137,22 @@
|
||||
}
|
||||
|
||||
async function handleExportInstallation(installationId: number) {
|
||||
if (!$operatingWorkspace) return
|
||||
if (!$workspaceStore) return
|
||||
|
||||
try {
|
||||
await exportInstallation($operatingWorkspace, installationId)
|
||||
await exportInstallation($workspaceStore, installationId)
|
||||
} catch (error) {
|
||||
console.error('Failed to export installation:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImportInstallation() {
|
||||
if (!$operatingWorkspace) return
|
||||
if (!$workspaceStore) return
|
||||
|
||||
try {
|
||||
await importInstallation($operatingWorkspace, githubState.importJwt, () => {
|
||||
await importInstallation($workspaceStore, githubState.importJwt, () => {
|
||||
githubState.importJwt = ''
|
||||
loadGithubInstallations(githubState, $operatingWorkspace!)
|
||||
loadGithubInstallations(githubState, $workspaceStore!)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to import installation:', error)
|
||||
@@ -168,17 +160,17 @@
|
||||
}
|
||||
|
||||
function handleRefreshInstallations() {
|
||||
if (!$operatingWorkspace) return
|
||||
if (!$workspaceStore) return
|
||||
|
||||
loadGithubInstallations(githubState, $operatingWorkspace).catch((error) => {
|
||||
loadGithubInstallations(githubState, $workspaceStore).catch((error) => {
|
||||
console.error('Failed to refresh installations:', error)
|
||||
})
|
||||
}
|
||||
|
||||
function handleInstallClickWithPopover() {
|
||||
if (!$operatingWorkspace) return
|
||||
if (!$workspaceStore) return
|
||||
|
||||
handleInstallClick(githubState, $operatingWorkspace, () => {
|
||||
handleInstallClick(githubState, $workspaceStore, () => {
|
||||
githubAppPopover?.open()
|
||||
})
|
||||
}
|
||||
@@ -305,9 +297,9 @@
|
||||
target="_blank"
|
||||
disabled={githubState.isCheckingInstallation}
|
||||
on:click={() => {
|
||||
if ($operatingWorkspace) {
|
||||
startInstallationCheck(githubState, $operatingWorkspace, () =>
|
||||
loadGithubInstallations(githubState, $operatingWorkspace!)
|
||||
if ($workspaceStore) {
|
||||
startInstallationCheck(githubState, $workspaceStore, () =>
|
||||
loadGithubInstallations(githubState, $workspaceStore!)
|
||||
)
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import { workspaceStore, userStore, enterpriseLicense } from '$lib/stores'
|
||||
import { GitSyncService, type GitlabProject } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
@@ -8,14 +8,6 @@
|
||||
import TextInput from './text_input/TextInput.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { GitBranch, Gitlab, Loader2 } from 'lucide-svelte'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
|
||||
interface Props {
|
||||
resourceType: string
|
||||
@@ -38,7 +30,7 @@
|
||||
onArgsUpdate
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspace ?? $operatingWorkspace)
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
let baseUrl = $state('https://gitlab.com')
|
||||
let token = $state('')
|
||||
@@ -58,7 +50,7 @@
|
||||
let show = $derived(
|
||||
resourceType === 'git_repository' &&
|
||||
!!ws &&
|
||||
(actingUser?.is_admin || actingUser?.is_super_admin)
|
||||
($userStore?.is_admin || $userStore?.is_super_admin)
|
||||
)
|
||||
// The project listing is served by an enterprise-only route, so on a build
|
||||
// without it the form's first request would 404. The button still shows,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Popover } from './meltComponents'
|
||||
import { GitBranch, Loader2 } from 'lucide-svelte'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
isOpen?: boolean
|
||||
@@ -22,12 +20,12 @@
|
||||
let gitRepoResources = $state<{ path: string; description?: string }[]>([])
|
||||
|
||||
async function loadGitRepoResources() {
|
||||
if (!$operatingWorkspace || loading) return
|
||||
if (!$workspaceStore || loading) return
|
||||
|
||||
loading = true
|
||||
try {
|
||||
const resources = await ResourceService.listResource({
|
||||
workspace: $operatingWorkspace,
|
||||
workspace: $workspaceStore,
|
||||
resourceType: 'git_repository'
|
||||
})
|
||||
|
||||
@@ -41,7 +39,7 @@
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (isOpen && $operatingWorkspace) {
|
||||
if (isOpen && $workspaceStore) {
|
||||
untrack(() => loadGitRepoResources())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { HelpersService, ResourceService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
import { GitBranch, Loader2, FolderOpen } from 'lucide-svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
@@ -29,7 +27,7 @@
|
||||
workspace: workspaceProp = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspaceProp ?? $operatingWorkspace)
|
||||
let ws = $derived(workspaceProp ?? $workspaceStore)
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
selected: {
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
import { untrack } from 'svelte'
|
||||
import { Alert, Button } from './common'
|
||||
import S3FilePickerInner from './S3FilePickerInner.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { base } from '$lib/base'
|
||||
import { Loader, Loader2, ChevronDown, ChevronRight, ExternalLink } from 'lucide-svelte'
|
||||
import { hubPaths } from '$lib/hub'
|
||||
import { sleep } from '$lib/utils'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
const CLONE_MARKER_FILE = '.windmill_clone_complete'
|
||||
const POLL_INTERVAL_MS = 1500
|
||||
@@ -44,7 +42,7 @@
|
||||
workspace: workspaceProp = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspaceProp ?? $operatingWorkspace)
|
||||
let ws = $derived(workspaceProp ?? $workspaceStore)
|
||||
|
||||
let commitHash = $derived(commitHashInput)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
type Group,
|
||||
type InstanceGroup
|
||||
} from '$lib/gen'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { onMount, tick, untrack } from 'svelte'
|
||||
import { Button } from './common'
|
||||
import Skeleton from './common/skeleton/Skeleton.svelte'
|
||||
@@ -34,14 +34,6 @@
|
||||
type GroupDraft,
|
||||
type GroupRole
|
||||
} from '$lib/groupDraft'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
|
||||
const ROLE_TOOLTIPS = {
|
||||
member:
|
||||
@@ -82,18 +74,10 @@
|
||||
}: Props = $props()
|
||||
|
||||
const restricted = $derived(
|
||||
isDemoWorkspaceRestricted($operatingWorkspace, actingUser?.is_admin, actingUser?.is_super_admin)
|
||||
isDemoWorkspaceRestricted($workspaceStore, $userStore?.is_admin, $userStore?.is_super_admin)
|
||||
)
|
||||
|
||||
let groupPerms = $state<Record<string, boolean> | undefined>(undefined)
|
||||
// A verdict that does not come from a loaded group: a new one is writable, one that failed
|
||||
// to load is not.
|
||||
let writeVerdict = $state<boolean | undefined>(undefined)
|
||||
// Derived for the same reason as the trigger editors: the acting user's role can land after
|
||||
// the group does, and a snapshot taken first would stay read-only.
|
||||
const can_write = $derived(
|
||||
writeVerdict ?? (groupPerms !== undefined && canWrite(name, groupPerms, actingUser))
|
||||
)
|
||||
let can_write = $state(false)
|
||||
let group: Group | undefined
|
||||
let instance_group: InstanceGroup | undefined = $state()
|
||||
let usernames: string[] = $state([])
|
||||
@@ -139,11 +123,11 @@
|
||||
}
|
||||
|
||||
async function loadUsernames(): Promise<void> {
|
||||
usernames = await UserService.listUsernames({ workspace: $operatingWorkspace! })
|
||||
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
|
||||
}
|
||||
|
||||
async function loadGroupNames(): Promise<void> {
|
||||
groupNames = (await GroupService.listGroupNames({ workspace: $operatingWorkspace! })) ?? []
|
||||
groupNames = (await GroupService.listGroupNames({ workspace: $workspaceStore! })) ?? []
|
||||
}
|
||||
|
||||
async function loadInstanceGroup(): Promise<void> {
|
||||
@@ -158,7 +142,7 @@
|
||||
loadAside(loadUsernames)
|
||||
if (isNew) {
|
||||
loadAside(loadGroupNames)
|
||||
writeVerdict = true
|
||||
can_write = true
|
||||
setDraft(emptyDraft())
|
||||
loaded = true
|
||||
} else {
|
||||
@@ -175,9 +159,8 @@
|
||||
const apply = (value: GroupDraft) =>
|
||||
opts?.baselineOnly ? (baseline = structuredClone(value)) : setDraft(value)
|
||||
try {
|
||||
group = await GroupService.getGroup({ workspace: $operatingWorkspace!, name })
|
||||
groupPerms = group.extra_perms ?? {}
|
||||
writeVerdict = undefined
|
||||
group = await GroupService.getGroup({ workspace: $workspaceStore!, name })
|
||||
can_write = canWrite(name, group.extra_perms ?? {}, $userStore)
|
||||
apply({
|
||||
summary: group.summary ?? '',
|
||||
members: Array.from(
|
||||
@@ -196,7 +179,7 @@
|
||||
sendUserToast(e?.body ?? String(e), true)
|
||||
// Only the opening read decides this. Revoking it on a failed reconcile would
|
||||
// disable Save against a draft that is still dirty, with nothing left to reload.
|
||||
if (!opts?.baselineOnly) writeVerdict = false
|
||||
if (!opts?.baselineOnly) can_write = false
|
||||
} finally {
|
||||
loaded = true
|
||||
}
|
||||
@@ -262,7 +245,7 @@
|
||||
* membership goes through the endpoints that name who was added or promoted — which is
|
||||
* what the permission history reads back. The diff itself is in `groupDraft.ts`. */
|
||||
async function applyMemberChanges(next: GroupDraft['members'], prev: GroupDraft['members']) {
|
||||
const workspace = $operatingWorkspace ?? ''
|
||||
const workspace = $workspaceStore ?? ''
|
||||
for (const call of groupMemberDiff(prev, next, $userStore?.username)) {
|
||||
switch (call.kind) {
|
||||
case 'addUser':
|
||||
@@ -306,7 +289,7 @@
|
||||
try {
|
||||
if (created) {
|
||||
await GroupService.createGroup({
|
||||
workspace: $operatingWorkspace ?? '',
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { name, summary: next.summary }
|
||||
})
|
||||
alreadyCreated = true
|
||||
@@ -316,7 +299,7 @@
|
||||
} else {
|
||||
if (next.summary !== prev.summary) {
|
||||
await GroupService.updateGroup({
|
||||
workspace: $operatingWorkspace ?? '',
|
||||
workspace: $workspaceStore ?? '',
|
||||
name,
|
||||
requestBody: { summary: next.summary }
|
||||
})
|
||||
@@ -352,7 +335,7 @@
|
||||
let loadStarted = false
|
||||
$effect.pre(() => {
|
||||
if (loadStarted) return
|
||||
if ($operatingWorkspace && $userStore) {
|
||||
if ($workspaceStore && $userStore) {
|
||||
loadStarted = true
|
||||
untrack(() => {
|
||||
load()
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
import { DataTable } from '$lib/components/table'
|
||||
import HistoricList from './HistoricList.svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
interface Props {
|
||||
runnableId?: string | undefined
|
||||
@@ -34,7 +32,7 @@
|
||||
workspace = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspace ?? $operatingWorkspace)
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
let historicList: HistoricList | undefined = $state(undefined)
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { InputService, type RunnableType } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { onDestroy, untrack } from 'svelte'
|
||||
import InfiniteList from './InfiniteList.svelte'
|
||||
import JobSchemaPicker from './schema/JobSchemaPicker.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
runnableId: string | undefined
|
||||
@@ -54,7 +52,7 @@
|
||||
refreshInterval()
|
||||
loadInputsPageFn = async (page: number, perPage: number) => {
|
||||
const inputs = await InputService.getInputHistory({
|
||||
workspace: $operatingWorkspace!,
|
||||
workspace: $workspaceStore!,
|
||||
runnableId,
|
||||
runnableType,
|
||||
page,
|
||||
@@ -111,7 +109,7 @@
|
||||
if (!id) return
|
||||
const payloadData = await InputService.getArgsFromHistoryOrSavedInput({
|
||||
jobOrInputId: id,
|
||||
workspace: $operatingWorkspace!,
|
||||
workspace: $workspaceStore!,
|
||||
input,
|
||||
allowLarge
|
||||
})
|
||||
@@ -124,11 +122,7 @@
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
$operatingWorkspace &&
|
||||
runnableId &&
|
||||
runnableType &&
|
||||
infiniteList &&
|
||||
untrack(() => initLoadInputs())
|
||||
$workspaceStore && runnableId && runnableType && infiniteList && untrack(() => initLoadInputs())
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { VariableService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { watch } from 'runed'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
import { Button } from './common'
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
import VariableEditor from './VariableEditor.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
/** The transforms being edited. A picked variable is written into `pickForField`'s. */
|
||||
@@ -28,7 +26,7 @@
|
||||
variableEditor = $bindable()
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspace ?? $operatingWorkspace)
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
watch(
|
||||
() => ws,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import { type InputTransform } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { allTrue, type DynamicInput as DynamicInputTypes } from '$lib/utils'
|
||||
import { untrack } from 'svelte'
|
||||
import StepInputsGen from './copilot/StepInputsGen.svelte'
|
||||
@@ -11,9 +12,6 @@
|
||||
import type ItemPicker from './ItemPicker.svelte'
|
||||
import type VariableEditor from './VariableEditor.svelte'
|
||||
import ResizeTransitionWrapper from './common/ResizeTransitionWrapper.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
schema: Schema | { properties?: Record<string, any> }
|
||||
@@ -56,7 +54,7 @@
|
||||
workspace
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspace ?? $operatingWorkspace)
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
let inputCheck: { [id: string]: boolean } = $state({})
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
type WorkflowStatus,
|
||||
type OpenFlow
|
||||
} from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getViewToken } from '$lib/viewToken'
|
||||
import { WM_LOGS_SKIPPED } from '$lib/consts'
|
||||
import { getContext, onDestroy, tick, untrack } from 'svelte'
|
||||
@@ -21,9 +22,6 @@
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { DynamicInput, isScriptPreview } from '$lib/utils'
|
||||
import { getActiveReplay, getReplayStartTime } from './recording/replay.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
// Will be set to number if job is not a flow
|
||||
|
||||
@@ -77,7 +75,7 @@
|
||||
children
|
||||
}: Props = $props()
|
||||
|
||||
let workspace = $derived(workspaceOverride ?? $operatingWorkspace)
|
||||
let workspace = $derived(workspaceOverride ?? $workspaceStore)
|
||||
|
||||
let syncIteration: number = 0
|
||||
let errorIteration = 0
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Alert, Skeleton } from './common'
|
||||
import { Activity } from 'lucide-svelte'
|
||||
import { JobService } from '$lib/gen'
|
||||
import { msToReadableTime } from '$lib/utils'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
// OTEL SpanKind enum values from opentelemetry-proto/opentelemetry/proto/trace/v1/trace.proto
|
||||
const SpanKind = {
|
||||
@@ -56,14 +54,14 @@
|
||||
})
|
||||
|
||||
async function loadTraces() {
|
||||
if (!$operatingWorkspace || !jobId) return
|
||||
if (!$workspaceStore || !jobId) return
|
||||
|
||||
loading = true
|
||||
error = null
|
||||
|
||||
try {
|
||||
const response = await JobService.getJobOtelTraces({
|
||||
workspace: $operatingWorkspace,
|
||||
workspace: $workspaceStore,
|
||||
id: jobId
|
||||
})
|
||||
traces = response as unknown as OtelSpan[]
|
||||
@@ -125,18 +123,12 @@
|
||||
|
||||
function getKindLabel(kind: number): string {
|
||||
switch (kind) {
|
||||
case SpanKind.INTERNAL:
|
||||
return 'Internal'
|
||||
case SpanKind.SERVER:
|
||||
return 'Server'
|
||||
case SpanKind.CLIENT:
|
||||
return 'Client'
|
||||
case SpanKind.PRODUCER:
|
||||
return 'Producer'
|
||||
case SpanKind.CONSUMER:
|
||||
return 'Consumer'
|
||||
default:
|
||||
return 'Unknown'
|
||||
case SpanKind.INTERNAL: return 'Internal'
|
||||
case SpanKind.SERVER: return 'Server'
|
||||
case SpanKind.CLIENT: return 'Client'
|
||||
case SpanKind.PRODUCER: return 'Producer'
|
||||
case SpanKind.CONSUMER: return 'Consumer'
|
||||
default: return 'Unknown'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,15 +182,17 @@
|
||||
<Activity size={48} class="mb-4 opacity-50" />
|
||||
<p class="text-lg font-medium">No HTTP requests captured</p>
|
||||
<p class="text-sm mt-2">
|
||||
This job did not make any HTTP/HTTPS requests, or HTTP Request Tracing is not enabled in
|
||||
instance settings.
|
||||
This job did not make any HTTP/HTTPS requests, or HTTP Request Tracing is not enabled in instance settings.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">Traces ({traces.length} spans)</h3>
|
||||
<button class="text-sm text-blue-600 hover:underline" onclick={loadTraces}>
|
||||
<button
|
||||
class="text-sm text-blue-600 hover:underline"
|
||||
onclick={loadTraces}
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
@@ -225,7 +219,10 @@
|
||||
{@const statusCode = span.status?.code ?? 0}
|
||||
|
||||
<div class="hover:bg-surface-hover">
|
||||
<button class="w-full px-4 py-2 text-left" onclick={() => toggleSpan(span.span_id)}>
|
||||
<button
|
||||
class="w-full px-4 py-2 text-left"
|
||||
onclick={() => toggleSpan(span.span_id)}
|
||||
>
|
||||
<div class="grid grid-cols-12 gap-2 items-center">
|
||||
<div class="col-span-4 flex items-center gap-2">
|
||||
<span class="text-xs text-secondary">
|
||||
@@ -296,9 +293,7 @@
|
||||
{#each Object.entries(parsedAttrs) as [key, value]}
|
||||
<div class="flex gap-2">
|
||||
<span class="text-secondary font-medium shrink-0">{key}:</span>
|
||||
<span class="font-mono break-all"
|
||||
>{typeof value === 'object' ? JSON.stringify(value) : value}</span
|
||||
>
|
||||
<span class="font-mono break-all">{typeof value === 'object' ? JSON.stringify(value) : value}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
import Badge from './common/badge/Badge.svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import { Plus, Tag, X } from 'lucide-svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
interface Props {
|
||||
labels: string[] | undefined
|
||||
@@ -55,7 +53,7 @@
|
||||
|
||||
async function loadExistingLabels() {
|
||||
try {
|
||||
const resp = await fetch(`/api/w/${workspace ?? $operatingWorkspace}/labels/list`)
|
||||
const resp = await fetch(`/api/w/${workspace ?? $workspaceStore}/labels/list`)
|
||||
if (resp.ok) existingLabels = await resp.json()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getContext, untrack } from 'svelte'
|
||||
import DarkModeObserver from './DarkModeObserver.svelte'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
@@ -10,9 +11,6 @@
|
||||
import IconedResourceType from './IconedResourceType.svelte'
|
||||
import { addResourceTitle } from './resourceTypeDisplay'
|
||||
import { loadResourceTypeDisplayName } from './displayNameLoaders'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
interface Props {
|
||||
value: string | undefined
|
||||
@@ -37,7 +35,7 @@
|
||||
let open = $state(false)
|
||||
let refreshCount = $state(0)
|
||||
const appViewerContext = getContext<AppViewerContext>('AppViewerContext')
|
||||
let ws = $derived(workspace ?? appViewerContext?.workspace ?? $operatingWorkspace)
|
||||
let ws = $derived(workspace ?? appViewerContext?.workspace ?? $workspaceStore)
|
||||
|
||||
let collection = $state(value ? [{ value, label: value }] : [])
|
||||
|
||||
@@ -141,7 +139,7 @@
|
||||
title="App connection"
|
||||
class="w-full h-full"
|
||||
src="{base}/embed_connect?resource_type={resourceType}&workspace={appViewerContext?.workspace ??
|
||||
$operatingWorkspace}&express=false"
|
||||
$workspaceStore}&express=false"
|
||||
/> -->
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user