dreadnode 2.0.45__py3-none-any.whl → 2.0.46__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dreadnode/agents/agent.py +15 -4
- dreadnode/agents/engines/base.py +4 -2
- dreadnode/agents/engines/claude_code.py +5 -1
- dreadnode/agents/engines/native.py +3 -1
- dreadnode/app/api/client.py +211 -4
- dreadnode/app/api/models.py +15 -0
- dreadnode/app/cli/capability.py +17 -1
- dreadnode/app/cli/environment.py +39 -7
- dreadnode/app/cli/main.py +14 -0
- dreadnode/app/cli/workflow.py +593 -0
- dreadnode/app/client/managed_client.py +34 -6
- dreadnode/app/client/runtime_client.py +45 -2
- dreadnode/app/config.py +31 -0
- dreadnode/app/print_mode.py +24 -4
- dreadnode/app/server/app.py +177 -30
- dreadnode/app/server/capability_manager.py +57 -16
- dreadnode/app/server/runtime_events.py +3 -0
- dreadnode/app/server/session_hydrator.py +1 -1
- dreadnode/app/server/turn_coordinator.py +26 -0
- dreadnode/app/server/websocket.py +20 -2
- dreadnode/app/server/workflow_manager.py +207 -0
- dreadnode/app/tui/app.py +49 -44
- dreadnode/app/tui/command_dispatcher.py +24 -6
- dreadnode/app/tui/commands.py +5 -0
- dreadnode/app/tui/dreadnode.tcss +5 -0
- dreadnode/app/tui/widgets/__init__.py +0 -13
- dreadnode/app/tui/widgets/composer.py +108 -27
- dreadnode/app/tui/widgets/context_bar.py +2 -1
- dreadnode/app/tui/widgets/conversation.py +142 -3
- dreadnode/app/tui/widgets/selection.py +207 -0
- dreadnode/app/tui/widgets/tool.py +31 -16
- dreadnode/builtin_capabilities/dreadnode/agents/dreadnode.md +3 -1
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/SKILL.md +29 -12
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/assets/workflows/agent_review.py +35 -0
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/assets/workflows/branch.py +41 -0
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/assets/workflows/fanout.py +40 -0
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/assets/workflows/fork.py +52 -0
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/assets/workflows/linear.py +28 -0
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/quality-bar.md +17 -0
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/INDEX.md +1 -0
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/capabilities/agent-output.md +0 -4
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/capabilities/manifest.md +42 -10
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/capabilities/overview.md +13 -11
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/capabilities/publishing.md +3 -1
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/capabilities/workflows.md +293 -0
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/sdk/capabilities.md +56 -5
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/workflows-authoring.md +242 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/SKILL.md +4 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/INDEX.md +1 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/cli/airt.md +3 -3
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/cli/capability.md +1 -1
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/cli/env.md +9 -5
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/cli/judge.md +1 -1
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/cli/optimize.md +1 -1
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/cli/train.md +2 -2
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/cli/workflow.md +186 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/cli/worlds.md +1 -1
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-concepts/references/capabilities/overview.md +13 -11
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-concepts/references/platform/credits.md +8 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-concepts/references/platform/organizations.md +1 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-concepts/references/platform/settings.md +6 -6
- dreadnode/capabilities/loader.py +22 -1
- dreadnode/capabilities/types.py +18 -0
- dreadnode/capabilities/worker_runner.py +4 -4
- dreadnode/packaging/manifest.py +17 -0
- dreadnode/packaging/oci.py +60 -0
- dreadnode/tracing/constants.py +7 -0
- dreadnode/tracing/span.py +34 -0
- dreadnode/workflows/__init__.py +118 -0
- dreadnode/workflows/compile.py +1330 -0
- dreadnode/workflows/context.py +115 -0
- dreadnode/workflows/errors.py +87 -0
- dreadnode/workflows/host.py +856 -0
- dreadnode/workflows/loader.py +163 -0
- dreadnode/workflows/workflow.py +198 -0
- {dreadnode-2.0.45.dist-info → dreadnode-2.0.46.dist-info}/METADATA +1 -1
- {dreadnode-2.0.45.dist-info → dreadnode-2.0.46.dist-info}/RECORD +86 -67
- dreadnode_workflow_core/__init__.py +122 -0
- dreadnode_workflow_core/events.py +148 -0
- dreadnode_workflow_core/facts.py +124 -0
- dreadnode_workflow_core/scheduler.py +486 -0
- dreadnode_workflow_core/state.py +269 -0
- dreadnode_workflow_core/topology.py +252 -0
- dreadnode/app/tui/widgets/header_bar.py +0 -59
- dreadnode/app/tui/widgets/permission_prompt.py +0 -112
- dreadnode/app/tui/widgets/prompt_info.py +0 -64
- dreadnode/app/tui/widgets/session_sidebar.py +0 -111
- dreadnode/app/tui/widgets/throbber.py +0 -87
- {dreadnode-2.0.45.dist-info → dreadnode-2.0.46.dist-info}/WHEEL +0 -0
- {dreadnode-2.0.45.dist-info → dreadnode-2.0.46.dist-info}/entry_points.txt +0 -0
- {dreadnode-2.0.45.dist-info → dreadnode-2.0.46.dist-info}/licenses/LICENSE +0 -0
dreadnode/agents/agent.py
CHANGED
|
@@ -838,7 +838,10 @@ class Agent(Executor[AgentEvent, Trajectory]):
|
|
|
838
838
|
raise
|
|
839
839
|
|
|
840
840
|
async def _stream(
|
|
841
|
-
self,
|
|
841
|
+
self,
|
|
842
|
+
trajectory: Trajectory | None = None,
|
|
843
|
+
*,
|
|
844
|
+
message_metadata: dict[str, t.Any] | None = None,
|
|
842
845
|
) -> t.AsyncGenerator[AgentEvent, None]:
|
|
843
846
|
"""Drive the loop through the resolved :class:`AgentEngine`.
|
|
844
847
|
|
|
@@ -855,6 +858,7 @@ class Agent(Executor[AgentEvent, Trajectory]):
|
|
|
855
858
|
goal=self._current_input,
|
|
856
859
|
dispatch=self._dispatch,
|
|
857
860
|
permission=self._permission_bridge,
|
|
861
|
+
message_metadata=message_metadata,
|
|
858
862
|
)
|
|
859
863
|
async for event in engine.run_loop(ctx):
|
|
860
864
|
yield event
|
|
@@ -864,7 +868,10 @@ class Agent(Executor[AgentEvent, Trajectory]):
|
|
|
864
868
|
return resolve_engine(self.engine)
|
|
865
869
|
|
|
866
870
|
async def _native_run_loop(
|
|
867
|
-
self,
|
|
871
|
+
self,
|
|
872
|
+
trajectory: Trajectory | None = None,
|
|
873
|
+
*,
|
|
874
|
+
message_metadata: dict[str, t.Any] | None = None,
|
|
868
875
|
) -> t.AsyncGenerator[AgentEvent, None]:
|
|
869
876
|
"""
|
|
870
877
|
Core agent execution loop with inline tracing.
|
|
@@ -882,7 +889,7 @@ class Agent(Executor[AgentEvent, Trajectory]):
|
|
|
882
889
|
Message(
|
|
883
890
|
"user",
|
|
884
891
|
str(self._current_input),
|
|
885
|
-
metadata={"agent": self.name, "model": self.model_name},
|
|
892
|
+
metadata={**(message_metadata or {}), "agent": self.name, "model": self.model_name},
|
|
886
893
|
),
|
|
887
894
|
]
|
|
888
895
|
messages = inject_system_content(messages, self.instructions)
|
|
@@ -1316,12 +1323,14 @@ class Agent(Executor[AgentEvent, Trajectory]):
|
|
|
1316
1323
|
*,
|
|
1317
1324
|
reset: bool = True,
|
|
1318
1325
|
trajectory: Trajectory | None = None,
|
|
1326
|
+
message_metadata: dict[str, t.Any] | None = None,
|
|
1319
1327
|
) -> t.AsyncIterator[t.AsyncGenerator[AgentEvent, None]]:
|
|
1320
1328
|
"""
|
|
1321
1329
|
Stream agent execution.
|
|
1322
1330
|
|
|
1323
1331
|
Args:
|
|
1324
1332
|
goal: Input message for the agent.
|
|
1333
|
+
message_metadata: Informational metadata attached to this turn's user message.
|
|
1325
1334
|
reset: If True, start new conversation. If False, continue existing.
|
|
1326
1335
|
Ignored when *trajectory* is provided.
|
|
1327
1336
|
trajectory: External trajectory to operate on. When provided the
|
|
@@ -1369,7 +1378,9 @@ class Agent(Executor[AgentEvent, Trajectory]):
|
|
|
1369
1378
|
with ctx as parent_span:
|
|
1370
1379
|
|
|
1371
1380
|
async def _events() -> t.AsyncGenerator[AgentEvent, None]:
|
|
1372
|
-
async for event in self._stream(
|
|
1381
|
+
async for event in self._stream(
|
|
1382
|
+
active_trajectory, message_metadata=message_metadata
|
|
1383
|
+
):
|
|
1373
1384
|
# Emit agent-level events to parent span
|
|
1374
1385
|
if parent_span and isinstance(
|
|
1375
1386
|
event, (AgentStart, AgentEnd, AgentStalled, AgentError)
|
dreadnode/agents/engines/base.py
CHANGED
|
@@ -104,8 +104,8 @@ class PermissionBridge(t.Protocol):
|
|
|
104
104
|
class EngineContext:
|
|
105
105
|
"""Everything an engine needs to run one turn.
|
|
106
106
|
|
|
107
|
-
The native engine uses
|
|
108
|
-
|
|
107
|
+
The native engine uses ``agent``, ``trajectory``, and ``message_metadata``.
|
|
108
|
+
Foreign engines additionally use ``goal``, ``dispatch``
|
|
109
109
|
(to run observational hooks on translated events), and ``permission``.
|
|
110
110
|
"""
|
|
111
111
|
|
|
@@ -116,6 +116,8 @@ class EngineContext:
|
|
|
116
116
|
"""Run an event through the agent's hooks (metrics + reactions). Foreign
|
|
117
117
|
engines call this per translated event; the native engine dispatches inline."""
|
|
118
118
|
permission: "PermissionBridge | None" = None
|
|
119
|
+
message_metadata: dict[str, t.Any] | None = None
|
|
120
|
+
"""Informational metadata for the initiating user message, never authorization."""
|
|
119
121
|
|
|
120
122
|
|
|
121
123
|
class AgentEngine(ABC):
|
|
@@ -393,7 +393,11 @@ class ClaudeCodeEngine(AgentEngine):
|
|
|
393
393
|
Message(
|
|
394
394
|
"user",
|
|
395
395
|
str(ctx.goal or ""),
|
|
396
|
-
metadata={
|
|
396
|
+
metadata={
|
|
397
|
+
**(ctx.message_metadata or {}),
|
|
398
|
+
"agent": agent.name,
|
|
399
|
+
"model": agent.model_name,
|
|
400
|
+
},
|
|
397
401
|
)
|
|
398
402
|
)
|
|
399
403
|
step_messages.append(assistant_msg)
|
|
@@ -22,5 +22,7 @@ class NativeEngine(AgentEngine):
|
|
|
22
22
|
dispatches_internally = True
|
|
23
23
|
|
|
24
24
|
async def run_loop(self, ctx: EngineContext) -> "t.AsyncIterator[AgentEvent]":
|
|
25
|
-
async for event in ctx.agent._native_run_loop(
|
|
25
|
+
async for event in ctx.agent._native_run_loop(
|
|
26
|
+
ctx.trajectory, message_metadata=ctx.message_metadata
|
|
27
|
+
):
|
|
26
28
|
yield event
|
dreadnode/app/api/client.py
CHANGED
|
@@ -3325,10 +3325,12 @@ class ApiClient:
|
|
|
3325
3325
|
"""Find a session's workspace and project by searching across all org workspaces.
|
|
3326
3326
|
|
|
3327
3327
|
Returns the full API response payload (normally containing a
|
|
3328
|
-
``session`` dict with ``workspace
|
|
3329
|
-
first workspace where the session is
|
|
3330
|
-
any workspace. Used by ``--resume``
|
|
3331
|
-
different workspace than the
|
|
3328
|
+
``session`` dict with ``workspace``, ``project_key`` and
|
|
3329
|
+
``project_name`` keys) from the first workspace where the session is
|
|
3330
|
+
found, or ``None`` if not found in any workspace. Used by ``--resume``
|
|
3331
|
+
to locate sessions that belong to a different workspace than the
|
|
3332
|
+
current profile default — which scopes off ``project_key``, the
|
|
3333
|
+
addressable slug, not the ``project_name`` display label.
|
|
3332
3334
|
|
|
3333
3335
|
Raises :class:`AuthenticationError` on 401 so callers can surface
|
|
3334
3336
|
the auth failure rather than silently returning "not found".
|
|
@@ -4274,6 +4276,211 @@ class ApiClient:
|
|
|
4274
4276
|
)
|
|
4275
4277
|
return t.cast("dict[str, t.Any]", response.json())
|
|
4276
4278
|
|
|
4279
|
+
# ------------------------------------------------------------------
|
|
4280
|
+
# Workflows - authored graphs, runs, and approvals
|
|
4281
|
+
# ------------------------------------------------------------------
|
|
4282
|
+
|
|
4283
|
+
def list_workflow_definitions(
|
|
4284
|
+
self,
|
|
4285
|
+
org: str,
|
|
4286
|
+
workspace: str,
|
|
4287
|
+
*,
|
|
4288
|
+
capability: str | None = None,
|
|
4289
|
+
name: str | None = None,
|
|
4290
|
+
agent: str | None = None,
|
|
4291
|
+
limit: int = 100,
|
|
4292
|
+
) -> list[dict[str, t.Any]]:
|
|
4293
|
+
"""GET workspace workflow definitions."""
|
|
4294
|
+
params: dict[str, t.Any] = {"limit": limit}
|
|
4295
|
+
params.update(
|
|
4296
|
+
{
|
|
4297
|
+
key: value
|
|
4298
|
+
for key, value in (
|
|
4299
|
+
("capability", capability),
|
|
4300
|
+
("name", name),
|
|
4301
|
+
("agent", agent),
|
|
4302
|
+
)
|
|
4303
|
+
if value is not None
|
|
4304
|
+
}
|
|
4305
|
+
)
|
|
4306
|
+
response = self.request(
|
|
4307
|
+
"GET",
|
|
4308
|
+
f"/org/{org}/ws/{workspace}/workflows/definitions",
|
|
4309
|
+
params=params,
|
|
4310
|
+
)
|
|
4311
|
+
return t.cast("list[dict[str, t.Any]]", response.json())
|
|
4312
|
+
|
|
4313
|
+
def get_workflow_definition(
|
|
4314
|
+
self, org: str, workspace: str, definition_id: str
|
|
4315
|
+
) -> dict[str, t.Any]:
|
|
4316
|
+
"""Get a workspace-visible definition with its full topology."""
|
|
4317
|
+
response = self.request(
|
|
4318
|
+
"GET",
|
|
4319
|
+
f"/org/{org}/ws/{workspace}/workflows/definitions/{definition_id}",
|
|
4320
|
+
)
|
|
4321
|
+
return t.cast("dict[str, t.Any]", response.json())
|
|
4322
|
+
|
|
4323
|
+
def create_workflow_run(
|
|
4324
|
+
self,
|
|
4325
|
+
org: str,
|
|
4326
|
+
workspace: str,
|
|
4327
|
+
*,
|
|
4328
|
+
definition_id: str,
|
|
4329
|
+
project_id: str,
|
|
4330
|
+
input_data: dict[str, t.Any] | None = None,
|
|
4331
|
+
host_kind: str = "runtime",
|
|
4332
|
+
runtime_id: str | None = None,
|
|
4333
|
+
) -> dict[str, t.Any]:
|
|
4334
|
+
"""Start a workflow run in a workspace project."""
|
|
4335
|
+
if host_kind == "runtime" and runtime_id is None:
|
|
4336
|
+
raise ValueError("runtime_id is required for runtime-hosted workflows")
|
|
4337
|
+
if host_kind == "local" and runtime_id is not None:
|
|
4338
|
+
raise ValueError("runtime_id is not allowed for local workflows")
|
|
4339
|
+
payload: dict[str, t.Any] = {
|
|
4340
|
+
"definition_id": definition_id,
|
|
4341
|
+
"project_id": project_id,
|
|
4342
|
+
"input": input_data or {},
|
|
4343
|
+
"host_kind": host_kind,
|
|
4344
|
+
}
|
|
4345
|
+
if runtime_id is not None:
|
|
4346
|
+
payload["runtime_id"] = runtime_id
|
|
4347
|
+
response = self.request(
|
|
4348
|
+
"POST",
|
|
4349
|
+
f"/org/{org}/ws/{workspace}/workflows/runs",
|
|
4350
|
+
json_data=payload,
|
|
4351
|
+
)
|
|
4352
|
+
return t.cast("dict[str, t.Any]", response.json())
|
|
4353
|
+
|
|
4354
|
+
def list_workflow_runs(
|
|
4355
|
+
self,
|
|
4356
|
+
org: str,
|
|
4357
|
+
workspace: str,
|
|
4358
|
+
*,
|
|
4359
|
+
project_id: str | None = None,
|
|
4360
|
+
runtime_id: str | None = None,
|
|
4361
|
+
definition_id: str | None = None,
|
|
4362
|
+
status: str | None = None,
|
|
4363
|
+
limit: int = 100,
|
|
4364
|
+
) -> list[dict[str, t.Any]]:
|
|
4365
|
+
"""List workflow runs in a workspace project."""
|
|
4366
|
+
if project_id is None and runtime_id is None:
|
|
4367
|
+
raise ValueError("project_id is required unless polling by runtime_id")
|
|
4368
|
+
params: dict[str, t.Any] = {"limit": limit}
|
|
4369
|
+
if project_id is not None:
|
|
4370
|
+
params["project_id"] = project_id
|
|
4371
|
+
if runtime_id is not None:
|
|
4372
|
+
params["runtime_id"] = runtime_id
|
|
4373
|
+
if definition_id is not None:
|
|
4374
|
+
params["definition_id"] = definition_id
|
|
4375
|
+
if status is not None:
|
|
4376
|
+
params["status"] = status
|
|
4377
|
+
response = self.request(
|
|
4378
|
+
"GET",
|
|
4379
|
+
f"/org/{org}/ws/{workspace}/workflows/runs",
|
|
4380
|
+
params=params,
|
|
4381
|
+
)
|
|
4382
|
+
return t.cast("list[dict[str, t.Any]]", response.json())
|
|
4383
|
+
|
|
4384
|
+
def claim_workflow_run(
|
|
4385
|
+
self,
|
|
4386
|
+
org: str,
|
|
4387
|
+
workspace: str,
|
|
4388
|
+
run_id: str,
|
|
4389
|
+
*,
|
|
4390
|
+
execution_id: str,
|
|
4391
|
+
runtime_id: str | None = None,
|
|
4392
|
+
) -> dict[str, t.Any]:
|
|
4393
|
+
"""Atomically claim one pending workflow run for this host."""
|
|
4394
|
+
payload: dict[str, t.Any] = {"execution_id": execution_id}
|
|
4395
|
+
if runtime_id is not None:
|
|
4396
|
+
payload["runtime_id"] = runtime_id
|
|
4397
|
+
response = self.request(
|
|
4398
|
+
"POST",
|
|
4399
|
+
f"/org/{org}/ws/{workspace}/workflows/runs/{run_id}/claim",
|
|
4400
|
+
json_data=payload,
|
|
4401
|
+
)
|
|
4402
|
+
return t.cast("dict[str, t.Any]", response.json())
|
|
4403
|
+
|
|
4404
|
+
def get_workflow_run(self, org: str, workspace: str, run_id: str) -> dict[str, t.Any]:
|
|
4405
|
+
"""Get a workflow run with nodes and pinned topology."""
|
|
4406
|
+
response = self.request("GET", f"/org/{org}/ws/{workspace}/workflows/runs/{run_id}")
|
|
4407
|
+
return t.cast("dict[str, t.Any]", response.json())
|
|
4408
|
+
|
|
4409
|
+
def append_workflow_facts(
|
|
4410
|
+
self,
|
|
4411
|
+
org: str,
|
|
4412
|
+
workspace: str,
|
|
4413
|
+
run_id: str,
|
|
4414
|
+
facts: list[dict[str, t.Any]],
|
|
4415
|
+
*,
|
|
4416
|
+
execution_id: str,
|
|
4417
|
+
) -> dict[str, t.Any]:
|
|
4418
|
+
"""Append host-sequenced facts to a workspace workflow run."""
|
|
4419
|
+
response = self.request(
|
|
4420
|
+
"POST",
|
|
4421
|
+
f"/org/{org}/ws/{workspace}/workflows/runs/{run_id}/facts",
|
|
4422
|
+
json_data={"execution_id": execution_id, "facts": facts},
|
|
4423
|
+
)
|
|
4424
|
+
return t.cast("dict[str, t.Any]", response.json())
|
|
4425
|
+
|
|
4426
|
+
def heartbeat_workflow_run(
|
|
4427
|
+
self,
|
|
4428
|
+
org: str,
|
|
4429
|
+
workspace: str,
|
|
4430
|
+
run_id: str,
|
|
4431
|
+
*,
|
|
4432
|
+
execution_id: str,
|
|
4433
|
+
) -> dict[str, t.Any]:
|
|
4434
|
+
"""Report workflow-run liveness."""
|
|
4435
|
+
response = self.request(
|
|
4436
|
+
"POST",
|
|
4437
|
+
f"/org/{org}/ws/{workspace}/workflows/runs/{run_id}/heartbeat",
|
|
4438
|
+
json_data={"execution_id": execution_id},
|
|
4439
|
+
)
|
|
4440
|
+
return t.cast("dict[str, t.Any]", response.json())
|
|
4441
|
+
|
|
4442
|
+
def cancel_workflow_run(
|
|
4443
|
+
self, org: str, workspace: str, run_id: str, *, project_id: str
|
|
4444
|
+
) -> dict[str, t.Any]:
|
|
4445
|
+
"""Cancel a workflow run in one project."""
|
|
4446
|
+
response = self.request(
|
|
4447
|
+
"POST",
|
|
4448
|
+
f"/org/{org}/ws/{workspace}/workflows/runs/{run_id}/cancel",
|
|
4449
|
+
params={"project_id": project_id},
|
|
4450
|
+
)
|
|
4451
|
+
return t.cast("dict[str, t.Any]", response.json())
|
|
4452
|
+
|
|
4453
|
+
def list_workflow_approvals(
|
|
4454
|
+
self, org: str, workspace: str, *, project_id: str, limit: int = 100
|
|
4455
|
+
) -> list[dict[str, t.Any]]:
|
|
4456
|
+
"""List pending workflow approvals in a workspace project."""
|
|
4457
|
+
response = self.request(
|
|
4458
|
+
"GET",
|
|
4459
|
+
f"/org/{org}/ws/{workspace}/workflows/approvals",
|
|
4460
|
+
params={"project_id": project_id, "limit": limit},
|
|
4461
|
+
)
|
|
4462
|
+
return t.cast("list[dict[str, t.Any]]", response.json())
|
|
4463
|
+
|
|
4464
|
+
def decide_workflow_approval(
|
|
4465
|
+
self,
|
|
4466
|
+
org: str,
|
|
4467
|
+
workspace: str,
|
|
4468
|
+
approval_id: str,
|
|
4469
|
+
*,
|
|
4470
|
+
outcome: str,
|
|
4471
|
+
note: str | None = None,
|
|
4472
|
+
) -> dict[str, t.Any]:
|
|
4473
|
+
"""Decide a workspace workflow approval."""
|
|
4474
|
+
payload: dict[str, t.Any] = {"outcome": outcome}
|
|
4475
|
+
if note is not None:
|
|
4476
|
+
payload["note"] = note
|
|
4477
|
+
response = self.request(
|
|
4478
|
+
"POST",
|
|
4479
|
+
f"/org/{org}/ws/{workspace}/workflows/approvals/{approval_id}/decision",
|
|
4480
|
+
json_data=payload,
|
|
4481
|
+
)
|
|
4482
|
+
return t.cast("dict[str, t.Any]", response.json())
|
|
4483
|
+
|
|
4277
4484
|
|
|
4278
4485
|
def create_api_client(*, profile: str | None = None) -> ApiClient:
|
|
4279
4486
|
"""Create an authenticated API client using stored API key configuration data."""
|
dreadnode/app/api/models.py
CHANGED
|
@@ -95,11 +95,18 @@ class PlatformConfig(BaseModel):
|
|
|
95
95
|
# web UI defaults it the other way and fails closed, but nothing in the SDK
|
|
96
96
|
# gates on it today — it is mirrored so the drift check stays meaningful.
|
|
97
97
|
airgapped: bool = False
|
|
98
|
+
email_configured: bool = False
|
|
98
99
|
recaptcha_enabled: bool = False
|
|
99
100
|
recaptcha_site_key: str | None = None
|
|
101
|
+
account_defender_enabled: bool = False
|
|
102
|
+
account_defender_site_key: str | None = None
|
|
100
103
|
credits_enabled: bool = True
|
|
101
104
|
feedback_enabled: bool = False
|
|
102
105
|
integrations_enabled: bool = True
|
|
106
|
+
# Whether the operator has enabled the Slack integration for this
|
|
107
|
+
# deployment. Absent (False) on older platforms, which have no Slack routes
|
|
108
|
+
# at all, so a client that does not receive it shows nothing (SLK-GATE-003).
|
|
109
|
+
slack_enabled: bool = False
|
|
103
110
|
# False on older platforms whose /feedback rejects diagnostic-report fields.
|
|
104
111
|
feedback_diagnostics: bool = False
|
|
105
112
|
website_url: str
|
|
@@ -299,6 +306,14 @@ class Organization(BaseModel):
|
|
|
299
306
|
"""URL-friendly identifier for the organization."""
|
|
300
307
|
description: str | None = None
|
|
301
308
|
"""Description of the organization."""
|
|
309
|
+
avatar_preset: t.Literal["slate", "blue", "violet", "green", "amber"] | None = None
|
|
310
|
+
"""Selected avatar color preset."""
|
|
311
|
+
avatar_source: t.Literal["image", "preset"] | None = None
|
|
312
|
+
"""Which avatar is active; None renders the organization name initial."""
|
|
313
|
+
avatar_image_available: bool = False
|
|
314
|
+
"""Whether the organization has an uploaded avatar image."""
|
|
315
|
+
avatar_image_revision: str | None = None
|
|
316
|
+
"""Revision of the uploaded avatar image."""
|
|
302
317
|
is_active: bool = True
|
|
303
318
|
"""Is the organization active?"""
|
|
304
319
|
allow_external_invites: bool = False
|
dreadnode/app/cli/capability.py
CHANGED
|
@@ -350,7 +350,22 @@ def _summarize_capability(p: dict[str, t.Any]) -> str:
|
|
|
350
350
|
visibility = "public" if p.get("is_public") else "private"
|
|
351
351
|
description = p.get("description")
|
|
352
352
|
suffix = f" [dim]-[/dim] {description}" if description else ""
|
|
353
|
-
|
|
353
|
+
secrets = p.get("secrets")
|
|
354
|
+
required_secrets = (
|
|
355
|
+
[
|
|
356
|
+
secret.get("name")
|
|
357
|
+
for secret in secrets
|
|
358
|
+
if isinstance(secret, dict) and secret.get("required", True)
|
|
359
|
+
]
|
|
360
|
+
if isinstance(secrets, list)
|
|
361
|
+
else []
|
|
362
|
+
)
|
|
363
|
+
credential_notice = (
|
|
364
|
+
f" [yellow]requires credentials: {', '.join(str(secret) for secret in required_secrets)}[/yellow]"
|
|
365
|
+
if required_secrets
|
|
366
|
+
else ""
|
|
367
|
+
)
|
|
368
|
+
return f"[cyan]{name}[/cyan]@[dim]{version}[/dim] {_visibility_markup(visibility)}{suffix}{credential_notice}"
|
|
354
369
|
|
|
355
370
|
|
|
356
371
|
_CAPABILITY_LIST_ROW_FIELDS: tuple[str, ...] = (
|
|
@@ -360,6 +375,7 @@ _CAPABILITY_LIST_ROW_FIELDS: tuple[str, ...] = (
|
|
|
360
375
|
"author_name",
|
|
361
376
|
"license",
|
|
362
377
|
"component_counts",
|
|
378
|
+
"secrets",
|
|
363
379
|
"version_count",
|
|
364
380
|
"versions",
|
|
365
381
|
"created_at",
|
dreadnode/app/cli/environment.py
CHANGED
|
@@ -11,6 +11,7 @@ import typing as t
|
|
|
11
11
|
|
|
12
12
|
import cyclopts
|
|
13
13
|
|
|
14
|
+
from dreadnode.app.api.client import NotFoundError
|
|
14
15
|
from dreadnode.app.cli.args import PlatformScopeArgs
|
|
15
16
|
from dreadnode.app.cli.shared import (
|
|
16
17
|
_FLAG_STATE,
|
|
@@ -137,7 +138,33 @@ def _print_environment_detail(payload: dict[str, t.Any]) -> None:
|
|
|
137
138
|
# ---------------------------------------------------------------------------
|
|
138
139
|
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
def _create_env_with_hint(
|
|
142
|
+
api: t.Any, org: str, workspace: str, body: dict[str, t.Any], task_ref: str
|
|
143
|
+
) -> dict[str, t.Any]:
|
|
144
|
+
"""Create the environment, adding a resolution hint to a task-not-found 404.
|
|
145
|
+
|
|
146
|
+
A bare name resolves to a task in the caller's org or any public task; a task
|
|
147
|
+
owned by another org resolves only when it is public or owned by the caller
|
|
148
|
+
(server-side visibility rule). We do not retry client-side - a public task
|
|
149
|
+
already resolves by bare name, and a private cross-org task 404s the same way
|
|
150
|
+
whether or not it is qualified. Only a task-not-found 404 gets the hint; any
|
|
151
|
+
other 404 (e.g. a bad ``--project-id``) keeps the server's own detail message.
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
return api.create_environment(org, workspace, body)
|
|
155
|
+
except NotFoundError as exc:
|
|
156
|
+
# provision() can 404 on the project lookup (bad --project-id) before it
|
|
157
|
+
# ever resolves the task, so only rewrite genuine task-not-found errors.
|
|
158
|
+
if "task not found" not in str(exc).lower():
|
|
159
|
+
raise
|
|
160
|
+
raise NotFoundError(
|
|
161
|
+
f"Task '{task_ref}' not found. A bare name resolves to a task in your org or "
|
|
162
|
+
f"any public task; a task owned by another org must be public or owned by you. "
|
|
163
|
+
f"Check the name and version, or qualify it as <org>/<name>."
|
|
164
|
+
) from exc
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@cli.command(alias="provision")
|
|
141
168
|
def create(
|
|
142
169
|
task_ref: str,
|
|
143
170
|
*,
|
|
@@ -207,14 +234,19 @@ def create(
|
|
|
207
234
|
as_json: t.Annotated[bool, cyclopts.Parameter(name="--json", negative=())] = False,
|
|
208
235
|
platform: PlatformScopeArgs = PlatformScopeArgs(),
|
|
209
236
|
) -> None:
|
|
210
|
-
"""Provision a task environment.
|
|
237
|
+
"""Provision a task environment. (Alias: ``dreadnode env provision``.)
|
|
211
238
|
|
|
212
239
|
``task_ref`` follows the canonical ``[org/]name[@version]`` format:
|
|
213
240
|
|
|
214
|
-
- ``my-task``
|
|
215
|
-
- ``my-task@1.0.0``
|
|
216
|
-
- ``acme/my-task``
|
|
217
|
-
- ``acme/my-task@1.0.0``
|
|
241
|
+
- ``my-task`` : latest version visible to you
|
|
242
|
+
- ``my-task@1.0.0`` : exact version
|
|
243
|
+
- ``acme/my-task`` : cross-org (must be public or owned by you)
|
|
244
|
+
- ``acme/my-task@1.0.0`` : cross-org exact version
|
|
245
|
+
|
|
246
|
+
A bare name resolves to a task in your org or any public task, so bundled
|
|
247
|
+
public targets (e.g. ``ml-extraction-fraud-tabular``, ``finops-mesh``) work by
|
|
248
|
+
bare name. A task owned by another org resolves only when it is public or
|
|
249
|
+
owned by you; otherwise qualify it as ``<org>/<name>``.
|
|
218
250
|
|
|
219
251
|
Use ``--input name=value`` repeatedly to bind template variables (values
|
|
220
252
|
are JSON-decoded when possible, falling back to plain strings).
|
|
@@ -245,7 +277,7 @@ def create(
|
|
|
245
277
|
if env_models is not None:
|
|
246
278
|
body["model_overrides"] = env_models
|
|
247
279
|
|
|
248
|
-
payload = api
|
|
280
|
+
payload = _create_env_with_hint(api, profile.org_key, profile.workspace_key, body, task_ref)
|
|
249
281
|
env_id = str(payload.get("id", ""))
|
|
250
282
|
|
|
251
283
|
if wait and env_id:
|
dreadnode/app/cli/main.py
CHANGED
|
@@ -149,6 +149,7 @@ _LAZY_COMMANDS: dict[str, tuple[str, str | None]] = {
|
|
|
149
149
|
"task": ("dreadnode.app.cli.task", None),
|
|
150
150
|
"task-set": ("dreadnode.app.cli.task_set", None),
|
|
151
151
|
"train": ("dreadnode.app.cli.train", None),
|
|
152
|
+
"workflow": ("dreadnode.app.cli.workflow", None),
|
|
152
153
|
"worlds": ("dreadnode.app.cli.worlds", None),
|
|
153
154
|
}
|
|
154
155
|
|
|
@@ -336,9 +337,22 @@ def _run_print(tui_args: TuiArgs) -> None:
|
|
|
336
337
|
if not tui_args.prompt:
|
|
337
338
|
raise ValueError("--print requires --prompt")
|
|
338
339
|
|
|
340
|
+
# Resolve the same way the TUI does, so ``--profile`` and the scope
|
|
341
|
+
# flags reach the headless runtime instead of being re-derived from
|
|
342
|
+
# whichever profile happens to be active on disk.
|
|
343
|
+
from dreadnode.app.config import ProfileError
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
profile = tui_args.resolve()
|
|
347
|
+
except ProfileError as exc:
|
|
348
|
+
raise ValueError(str(exc)) from exc
|
|
349
|
+
except Exception:
|
|
350
|
+
profile = None
|
|
351
|
+
|
|
339
352
|
asyncio.run(
|
|
340
353
|
run_print_mode(
|
|
341
354
|
prompt=tui_args.prompt,
|
|
355
|
+
profile=profile,
|
|
342
356
|
model=resolve_model(tui_args.model) if tui_args.model else None,
|
|
343
357
|
agent=tui_args.agent,
|
|
344
358
|
capabilities_dirs=tui_args.capabilities_dirs,
|