mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(code): cli sandboxes, enterprise timeouts, secrets projections, resolver lift, workflow exec cancellations (#6247)
* feat(code): cli sandboxes, enterprise timeouts, secrets projections, resolver lift * fix(execution): harden compatibility and secret diagnostics * fix(execution): harden generated JavaScript literals * fix(execution): align timeout cleanup semantics * fix(tables): decouple stale job cleanup * fix(execution): drain stale workflow backlog * test(sandbox): make deadline assertions timing-safe * fix(execution): lock cleanup candidate batches * fix(execution): preserve cleanup failure metrics * cancel route fixes * separate out mship template and func template * fix * fix(execution): harden secret projection and block runs * fix(workflow): validate draft execution state * run from block ui disabling * feat(copilot): expose Sim sandboxes to mothership * feat(copilot): expose sandbox capability catalog in VFS * Updates * fix legacy logs showing up * fix(copilot): keep sandbox config visible * fix model provenance issues * fix lint' * more lint * more * test(files): align provenance copy query order * consolidate migrations, rollout compat * integration projections * update skills * fix * add provenance linters * fix: address review and compatibility regressions * fix: make tool boundary audit Bun 1.3 compatible --------- Co-authored-by: Siddharth Ganesan <siddharthganesan@gmail.com>
This commit is contained in:
co-authored by
Siddharth Ganesan
parent
5baa7a41ec
commit
117fe3137b
@@ -43,7 +43,7 @@ SimStudioClient(api_key: str, base_url: str = "https://sim.ai")
|
||||
|
||||
#### Methods
|
||||
|
||||
##### execute_workflow(workflow_id, input=None, *, timeout=30.0, stream=None, selected_outputs=None, async_execution=None)
|
||||
##### execute_workflow(workflow_id, input=None, *, timeout=30.0, stream=None, selected_outputs=None, async_execution=None, execution_timeout_seconds=None)
|
||||
|
||||
Execute a workflow with optional input data.
|
||||
|
||||
@@ -55,7 +55,13 @@ result = client.execute_workflow("workflow-id", {"message": "Hello, world!"})
|
||||
result = client.execute_workflow("workflow-id", "NVDA")
|
||||
|
||||
# With options (keyword-only arguments)
|
||||
result = client.execute_workflow("workflow-id", {"message": "Hello"}, timeout=60.0)
|
||||
result = client.execute_workflow(
|
||||
"workflow-id",
|
||||
{"message": "Hello"},
|
||||
timeout=60.0,
|
||||
async_execution=True,
|
||||
execution_timeout_seconds=3600,
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
@@ -65,6 +71,7 @@ result = client.execute_workflow("workflow-id", {"message": "Hello"}, timeout=60
|
||||
- `stream` (bool, keyword-only): Enable streaming responses
|
||||
- `selected_outputs` (list, keyword-only): Block outputs to stream (e.g., `["agent1.content"]`)
|
||||
- `async_execution` (bool, keyword-only): Execute asynchronously and return execution ID
|
||||
- `execution_timeout_seconds` (int, keyword-only): Server-side async execution cap from 1 to 604800 seconds. Requires `async_execution=True` and cannot extend the account policy.
|
||||
|
||||
**Returns:** `WorkflowExecutionResult` or `AsyncExecutionResult`
|
||||
|
||||
@@ -527,4 +534,4 @@ isort simstudio/
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
Apache-2.0
|
||||
|
||||
@@ -12,6 +12,7 @@ import os
|
||||
|
||||
import requests
|
||||
|
||||
MAX_EXECUTION_TIMEOUT_SECONDS = 604_800
|
||||
|
||||
__version__ = "0.1.2"
|
||||
__all__ = [
|
||||
@@ -155,7 +156,8 @@ class SimStudioClient:
|
||||
timeout: float = 30.0,
|
||||
stream: Optional[bool] = None,
|
||||
selected_outputs: Optional[list] = None,
|
||||
async_execution: Optional[bool] = None
|
||||
async_execution: Optional[bool] = None,
|
||||
execution_timeout_seconds: Optional[int] = None
|
||||
) -> Union[WorkflowExecutionResult, AsyncExecutionResult]:
|
||||
"""
|
||||
Execute a workflow with optional input data.
|
||||
@@ -172,6 +174,7 @@ class SimStudioClient:
|
||||
stream: Enable streaming responses (default: None)
|
||||
selected_outputs: Block outputs to stream (e.g., ["agent1.content"])
|
||||
async_execution: Execute asynchronously (default: None)
|
||||
execution_timeout_seconds: Server-side async execution cap in seconds (1-604800)
|
||||
|
||||
Returns:
|
||||
WorkflowExecutionResult or AsyncExecutionResult object
|
||||
@@ -181,10 +184,29 @@ class SimStudioClient:
|
||||
"""
|
||||
url = f"{self.base_url}/api/workflows/{workflow_id}/execute"
|
||||
|
||||
if execution_timeout_seconds is not None:
|
||||
if not async_execution:
|
||||
raise SimStudioError(
|
||||
'execution_timeout_seconds is supported only for async executions',
|
||||
'INVALID_EXECUTION_TIMEOUT'
|
||||
)
|
||||
if (
|
||||
isinstance(execution_timeout_seconds, bool)
|
||||
or not isinstance(execution_timeout_seconds, int)
|
||||
or execution_timeout_seconds < 1
|
||||
or execution_timeout_seconds > MAX_EXECUTION_TIMEOUT_SECONDS
|
||||
):
|
||||
raise SimStudioError(
|
||||
f'execution_timeout_seconds must be an integer between 1 and {MAX_EXECUTION_TIMEOUT_SECONDS}',
|
||||
'INVALID_EXECUTION_TIMEOUT'
|
||||
)
|
||||
|
||||
# Build headers - async execution uses X-Execution-Mode header
|
||||
headers = self._session.headers.copy()
|
||||
if async_execution:
|
||||
headers['X-Execution-Mode'] = 'async'
|
||||
if execution_timeout_seconds is not None:
|
||||
headers['X-Execution-Timeout-Seconds'] = str(execution_timeout_seconds)
|
||||
|
||||
try:
|
||||
# Build JSON body - spread dict inputs at root level, wrap primitives/lists in 'input' field
|
||||
@@ -421,6 +443,7 @@ class SimStudioClient:
|
||||
stream: Optional[bool] = None,
|
||||
selected_outputs: Optional[list] = None,
|
||||
async_execution: Optional[bool] = None,
|
||||
execution_timeout_seconds: Optional[int] = None,
|
||||
max_retries: int = 3,
|
||||
initial_delay: float = 1.0,
|
||||
max_delay: float = 30.0,
|
||||
@@ -436,6 +459,7 @@ class SimStudioClient:
|
||||
stream: Enable streaming responses
|
||||
selected_outputs: Block outputs to stream
|
||||
async_execution: Execute asynchronously
|
||||
execution_timeout_seconds: Server-side async execution cap in seconds (1-604800)
|
||||
max_retries: Maximum number of retries (default: 3)
|
||||
initial_delay: Initial delay in seconds (default: 1.0)
|
||||
max_delay: Maximum delay in seconds (default: 30.0)
|
||||
@@ -458,7 +482,8 @@ class SimStudioClient:
|
||||
timeout=timeout,
|
||||
stream=stream,
|
||||
selected_outputs=selected_outputs,
|
||||
async_execution=async_execution
|
||||
async_execution=async_execution,
|
||||
execution_timeout_seconds=execution_timeout_seconds,
|
||||
)
|
||||
except SimStudioError as e:
|
||||
if e.code != 'RATE_LIMIT_EXCEEDED':
|
||||
@@ -565,4 +590,4 @@ class SimStudioClient:
|
||||
|
||||
|
||||
# For backward compatibility
|
||||
Client = SimStudioClient
|
||||
Client = SimStudioClient
|
||||
|
||||
@@ -171,6 +171,79 @@ def test_async_header_not_set_when_false(mock_post):
|
||||
assert "X-Execution-Mode" not in call_args[1]["headers"]
|
||||
|
||||
|
||||
@patch('simstudio.requests.Session.post')
|
||||
def test_async_execution_timeout_header(mock_post):
|
||||
mock_response = Mock()
|
||||
mock_response.ok = True
|
||||
mock_response.status_code = 202
|
||||
mock_response.json.return_value = {
|
||||
"success": True,
|
||||
"jobId": "job-123",
|
||||
"statusUrl": "/api/jobs/job-123",
|
||||
"async": True,
|
||||
}
|
||||
mock_response.headers.get.return_value = None
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
client = SimStudioClient(api_key="test-api-key")
|
||||
client.execute_workflow(
|
||||
"workflow-id",
|
||||
{},
|
||||
async_execution=True,
|
||||
execution_timeout_seconds=90,
|
||||
)
|
||||
|
||||
headers = mock_post.call_args[1]["headers"]
|
||||
assert headers["X-Execution-Timeout-Seconds"] == "90"
|
||||
|
||||
|
||||
def test_sync_execution_rejects_execution_timeout():
|
||||
client = SimStudioClient(api_key="test-api-key")
|
||||
|
||||
with pytest.raises(SimStudioError) as exc_info:
|
||||
client.execute_workflow("workflow-id", {}, execution_timeout_seconds=90)
|
||||
|
||||
assert exc_info.value.code == "INVALID_EXECUTION_TIMEOUT"
|
||||
|
||||
|
||||
def test_execution_timeout_rejects_more_than_seven_days():
|
||||
client = SimStudioClient(api_key="test-api-key")
|
||||
|
||||
with pytest.raises(SimStudioError) as exc_info:
|
||||
client.execute_workflow(
|
||||
"workflow-id",
|
||||
{},
|
||||
async_execution=True,
|
||||
execution_timeout_seconds=604_801,
|
||||
)
|
||||
|
||||
assert exc_info.value.code == "INVALID_EXECUTION_TIMEOUT"
|
||||
|
||||
|
||||
def test_execute_with_retry_forwards_execution_timeout():
|
||||
client = SimStudioClient(api_key="test-api-key")
|
||||
expected = Mock()
|
||||
|
||||
with patch.object(client, "execute_workflow", return_value=expected) as execute_workflow:
|
||||
result = client.execute_with_retry(
|
||||
"workflow-id",
|
||||
{"message": "hello"},
|
||||
async_execution=True,
|
||||
execution_timeout_seconds=90,
|
||||
)
|
||||
|
||||
assert result is expected
|
||||
execute_workflow.assert_called_once_with(
|
||||
"workflow-id",
|
||||
{"message": "hello"},
|
||||
timeout=30.0,
|
||||
stream=None,
|
||||
selected_outputs=None,
|
||||
async_execution=True,
|
||||
execution_timeout_seconds=90,
|
||||
)
|
||||
|
||||
|
||||
@patch('simstudio.requests.Session.get')
|
||||
def test_get_job_status_success(mock_get):
|
||||
"""Test getting job status."""
|
||||
@@ -534,4 +607,4 @@ def test_execute_workflow_with_dict_input_spreads_at_root(mock_post):
|
||||
|
||||
assert request_body["ticker"] == "NVDA"
|
||||
assert request_body["quantity"] == 100
|
||||
assert "input" not in request_body # Should not wrap in input field
|
||||
assert "input" not in request_body # Should not wrap in input field
|
||||
|
||||
Reference in New Issue
Block a user