diff --git a/.semgrep.yml b/.semgrep.yml index 617809e9f..2bb1e8164 100644 --- a/.semgrep.yml +++ b/.semgrep.yml @@ -20,6 +20,8 @@ rules: paths: include: - src/ + exclude: + - src/cleveragents/tool/wrapping.py - id: no-compile-exec pattern: compile(..., ..., "exec") @@ -31,6 +33,8 @@ rules: paths: include: - src/ + exclude: + - src/cleveragents/tool/wrapping.py - id: no-os-system pattern: os.system(...) diff --git a/docs/specification.md b/docs/specification.md index c856a617c..448875d65 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -310,7 +310,7 @@ The following standards are integrated into the architecture: agents resource add [(--description|-d) <DESC>] [--update] <TYPE> <NAME> [type-specific-flags...] agents resource remove [--yes|-y] <NAME> -agents resource list [--all] [(--type|-t) <TYPE>] +agents resource list [--all] [(--type|-t) <TYPE>] agents resource show <RESOURCE> agents resource inspect [--tree] [--file <PATH>] <RESOURCE> agents resource tree [(--depth|-d) <N>] [(--type|-t) <TYPE>] <RESOURCE> @@ -9278,7 +9278,7 @@ When multiple attachment scopes apply, the union of all applicable validations i ##### agents validation add -
agents validation add (--config|-c) <FILE> [--update]agents validation add (--config|-c) <FILE> [--required | --informational] [--update]
class ToolExecutionContext:
"""Context provided to every tool execution, regardless of source."""
-
+
def __init__(self, plan: Plan, sandbox: Sandbox,
resources: dict[str, BoundResource]):
self.plan = plan
self.sandbox = sandbox
self.resources = resources # slot_name → BoundResource
self.changes: list[Change] = []
-
+
def record_change(self, change: Change) -> None:
"""Record a change made by a tool."""
self.changes.append(change)
@@ -22116,7 +22118,7 @@ When an LLM agent decides to use a tool (regardless of source), the following fl
class WriteFileTool:
"""Example: built-in tool for writing files."""
-
+
def execute(self, path: str, content: str, ctx: ToolExecutionContext) -> None:
handler = ctx.sandbox.get_handler(path)
change = handler.write(path, content, ctx.sandbox)
@@ -25036,51 +25038,51 @@ Every resource type provides a handler that implements this interface:
class ResourceHandler(Protocol):
"""Handler for a specific resource type."""
-
+
def read(self, path: str, sandbox: Sandbox) -> Content:
"""Read content from the sandboxed resource."""
...
-
+
def write(self, path: str, content: Content, sandbox: Sandbox) -> Change:
"""Write content and return the Change record."""
...
-
+
def delete(self, path: str, sandbox: Sandbox) -> Change:
"""Delete resource and return the Change record."""
...
-
+
def list(self, pattern: str, sandbox: Sandbox) -> list[str]:
"""List paths matching pattern."""
...
-
+
def diff(self, path: str, sandbox: Sandbox) -> str:
"""Generate diff between sandbox and original state."""
...
-
+
def supports_operation(self, operation: OperationType) -> bool:
"""Check if this resource supports the given operation."""
...
-
+
def discover_children(self, resource: ResourceRecord) -> list[ResourceRecord]:
"""Auto-discover child resources (called at registration and refresh)."""
...
-
+
def content_hash(self, path: str, sandbox: Sandbox) -> str:
"""Compute content hash for identity tracking."""
...
-
+
def create_sandbox(self, resource: ResourceRecord) -> Sandbox:
"""Create a sandbox for this resource using its type's strategy."""
...
-
+
def create_checkpoint(self, sandbox: Sandbox) -> Checkpoint:
"""Create a checkpoint within the sandbox."""
...
-
+
def rollback_to(self, sandbox: Sandbox, checkpoint: Checkpoint) -> None:
"""Roll back sandbox state to a checkpoint."""
...
-
+
def project_access(
self,
binding_resource: ResourceRecord,
@@ -25089,7 +25091,7 @@ Every resource type provides a handler that implements this interface:
sandbox: Sandbox | None,
) -> AccessProjection | None:
"""Compute how to reach target_resource from binding_resource.
-
+
Returns an AccessProjection with access_path, protocol,
crosses_sandbox flag, and read_richness score. Returns None
if this handler cannot project access to the target type.
@@ -25789,19 +25791,19 @@ The `OutputSession` is the core abstraction that replaces direct construction of
handle_b.close()
session.close() # finalize the session
"""
-
+
# --- Session lifecycle ---
-
+
command: str # The command that owns this session
session_id: str # Unique session identifier (ULID)
created_at: datetime # Session creation timestamp
-
+
_strategy: MaterializationStrategy # The active materialization strategy
_handles: OrderedDict[str, ElementHandle] # handle_id → handle, in declaration order
_event_queue: asyncio.Queue[ElementEvent] # Internal event queue for serialization
_state: SessionState # "open" | "closing" | "closed"
_lock: threading.Lock # Protects handle creation/removal
-
+
@classmethod
def open(cls, command: str, strategy: MaterializationStrategy,
metadata: dict | None = None) -> "OutputSession":
@@ -25819,11 +25821,11 @@ The `OutputSession` is the core abstraction that replaces direct construction of
A new OutputSession ready for element creation.
"""
...
-
+
# --- Element handle factories ---
# Each factory creates a typed handle, registers it with the session in
# declaration order, and emits an ElementCreated event to the strategy.
-
+
def panel(self, title: str, *,
border_style: str = "rounded",
priority: str = "normal",
@@ -25831,7 +25833,7 @@ The `OutputSession` is the core abstraction that replaces direct construction of
metadata: dict | None = None) -> "PanelHandle":
"""Create a panel element handle for key-value pair output."""
...
-
+
def table(self, title: str | None, *,
columns: list["ColumnDef"],
summary: dict | None = None,
@@ -25842,7 +25844,7 @@ The `OutputSession` is the core abstraction that replaces direct construction of
metadata: dict | None = None) -> "TableHandle":
"""Create a table element handle for tabular data."""
...
-
+
def tree(self, root_label: str, *,
root_style: str | None = None,
max_depth_hint: int | None = None,
@@ -25852,7 +25854,7 @@ The `OutputSession` is the core abstraction that replaces direct construction of
metadata: dict | None = None) -> "TreeHandle":
"""Create a tree element handle for hierarchical data."""
...
-
+
def progress(self, label: str, *,
total: int | None = None,
indeterminate: bool = False,
@@ -25869,7 +25871,7 @@ The `OutputSession` is the core abstraction that replaces direct construction of
initial status "pending").
"""
...
-
+
def status(self, message: str, *,
level: str = "info",
detail: str | None = None,
@@ -25877,7 +25879,7 @@ The `OutputSession` is the core abstraction that replaces direct construction of
metadata: dict | None = None) -> "StatusHandle":
"""Create a status message handle."""
...
-
+
def text(self, content: str = "", *,
wrap: bool = True,
indent: int = 0,
@@ -25885,7 +25887,7 @@ The `OutputSession` is the core abstraction that replaces direct construction of
metadata: dict | None = None) -> "TextHandle":
"""Create a text block handle."""
...
-
+
def code(self, content: str = "", *,
language: str | None = None,
line_numbers: bool = False,
@@ -25894,7 +25896,7 @@ The `OutputSession` is the core abstraction that replaces direct construction of
metadata: dict | None = None) -> "CodeHandle":
"""Create a code block handle."""
...
-
+
def diff(self, *,
file_a: str | None = None,
file_b: str | None = None,
@@ -25902,18 +25904,18 @@ The `OutputSession` is the core abstraction that replaces direct construction of
metadata: dict | None = None) -> "DiffHandle":
"""Create a diff block handle."""
...
-
+
def separator(self, style: str = "line") -> "SeparatorHandle":
"""Create a visual separator. Separators are auto-closed on creation."""
...
-
+
def action_hint(self, commands: list[str],
description: str | None = None) -> "ActionHintHandle":
"""Create an action hint. Action hints are auto-closed on creation."""
...
-
+
# --- Session operations ---
-
+
def snapshot(self) -> "StructuredOutput":
"""Return a static snapshot of all elements accumulated so far.
@@ -25923,7 +25925,7 @@ The `OutputSession` is the core abstraction that replaces direct construction of
debugging, or programmatic inspection.
"""
...
-
+
def close(self, *, exit_code: int = 0) -> "StructuredOutput":
"""Close the session and finalize all output.
@@ -25938,12 +25940,12 @@ The `OutputSession` is the core abstraction that replaces direct construction of
The final StructuredOutput snapshot.
"""
...
-
+
# --- Context manager support ---
-
+
def __enter__(self) -> "OutputSession":
return self
-
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
"""Auto-close session on context exit.
@@ -25981,16 +25983,16 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
Closed Handle Behavior:
Calling any write method on a closed handle raises ElementClosedError.
"""
-
+
handle_id: str # Unique handle identifier (ULID)
element_type: str # Semantic type ("panel", "table", etc.)
declaration_index: int # Position in session's declaration order
-
+
_session: OutputSession # Owning session (for event emission)
_element: E # The accumulated element state
_state: HandleState # "open" | "closed"
_lock: threading.Lock # Serializes writes to this handle
-
+
def close(self) -> None:
"""Close this handle, signaling that no more data will be written.
@@ -25998,20 +26000,20 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
For buffered strategies, this triggers rendering of the element.
"""
...
-
+
@property
def is_open(self) -> bool:
"""Whether this handle is still accepting writes."""
...
-
+
@property
def element(self) -> E:
"""The accumulated element state (read-only snapshot)."""
...
-
+
def __enter__(self) -> Self:
return self
-
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
"""Auto-close handle on context exit."""
if self.is_open:
@@ -26024,7 +26026,7 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
Panels are titled groups of key-value pairs. Entries can be added,
updated, or removed after creation.
"""
-
+
def set_entry(self, key: str, value: str, *,
style_hint: str | None = None,
icon: str | None = None) -> None:
@@ -26036,12 +26038,12 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
Emits an ElementUpdated event.
"""
...
-
+
def set_entries(self, entries: dict[str, str], *,
style_hints: dict[str, str] | None = None) -> None:
"""Set multiple entries at once (batch update). Emits a single event."""
...
-
+
def remove_entry(self, key: str) -> None:
"""Remove an entry by key. Emits an ElementUpdated event."""
...
@@ -26054,7 +26056,7 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
one at a time or in batches as data becomes available from queries,
API calls, or concurrent operations.
"""
-
+
def add_row(self, row: dict) -> None:
"""Append a single row to the table.
@@ -26064,15 +26066,15 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
Emits an ElementUpdated event (type=row_added).
"""
...
-
+
def add_rows(self, rows: list[dict]) -> None:
"""Append multiple rows in a batch. Emits a single ElementUpdated event."""
...
-
+
def set_summary(self, summary: dict) -> None:
"""Set or update the summary/aggregation row. Emits an ElementUpdated event."""
...
-
+
def set_sort_key(self, column: str, *, descending: bool = False) -> None:
"""Change the sort key. Emits an ElementUpdated event."""
...
@@ -26085,7 +26087,7 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
is created with the handle. Subtrees can be constructed incrementally
as hierarchical data is discovered.
"""
-
+
def add_child(self, parent_path: str | None, label: str, *,
style_hint: str | None = None,
collapsed: bool = False,
@@ -26106,7 +26108,7 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
Emits an ElementUpdated event.
"""
...
-
+
def set_node_style(self, path: str, style_hint: str) -> None:
"""Update the style of an existing node. Emits an ElementUpdated event."""
...
@@ -26119,7 +26121,7 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
rapid updates. The materialization strategy may throttle update events
to avoid overwhelming the terminal (e.g., limiting redraws to 10/sec).
"""
-
+
def set_progress(self, current: int, total: int | None = None) -> None:
"""Update the progress counter.
@@ -26130,7 +26132,7 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
Emits an ElementUpdated event (may be throttled by the strategy).
"""
...
-
+
def set_step_status(self, step_label: str, status: str) -> None:
"""Update the status of a named step.
@@ -26141,11 +26143,11 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
Emits an ElementUpdated event.
"""
...
-
+
def set_label(self, label: str) -> None:
"""Update the progress label text. Emits an ElementUpdated event."""
...
-
+
def increment(self, delta: int = 1) -> None:
"""Increment progress by delta. Convenience wrapper around set_progress."""
...
@@ -26158,15 +26160,15 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
messages). However, they can be kept open for messages that may be revised
(e.g., a "Working..." status that becomes "Done" or "Failed").
"""
-
+
def set_message(self, message: str) -> None:
"""Update the status message text. Emits an ElementUpdated event."""
...
-
+
def set_level(self, level: str) -> None:
"""Change the status level. Emits an ElementUpdated event."""
...
-
+
def set_detail(self, detail: str | None) -> None:
"""Set or clear the detail text. Emits an ElementUpdated event."""
...
@@ -26174,11 +26176,11 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
class TextHandle(ElementHandle[TextBlock]):
"""Handle for a text block. Supports appending text incrementally."""
-
+
def append(self, text: str) -> None:
"""Append text to the block. Emits an ElementUpdated event."""
...
-
+
def set_content(self, content: str) -> None:
"""Replace the entire content. Emits an ElementUpdated event."""
...
@@ -26186,15 +26188,15 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
class CodeHandle(ElementHandle[CodeBlock]):
"""Handle for a code block."""
-
+
def set_content(self, content: str) -> None:
"""Set the code content. Emits an ElementUpdated event."""
...
-
+
def set_language(self, language: str) -> None:
"""Set the language for syntax highlighting. Emits an ElementUpdated event."""
...
-
+
def set_highlight_lines(self, lines: list[int]) -> None:
"""Set lines to highlight. Emits an ElementUpdated event."""
...
@@ -26202,11 +26204,11 @@ Element handles are the producer-facing API. Each handle type wraps a specific e
class DiffHandle(ElementHandle[DiffBlock]):
"""Handle for a diff block. Hunks can be added incrementally."""
-
+
def add_hunk(self, header: str, lines: list["DiffLine"]) -> None:
"""Add a diff hunk. Emits an ElementUpdated event."""
...
-
+
def set_stats(self, insertions: int, deletions: int, **extra: int) -> None:
"""Set diff statistics. Emits an ElementUpdated event."""
...
@@ -26422,9 +26424,9 @@ The strategy pattern creates a clean separation between **timing/ordering policy
produces correct output regardless of format. The producer writes to
handles; the strategy decides what reaches the terminal and when.
"""
-
+
strategy_name: str # "live" | "sequential_buffer" | "accumulate"
-
+
def bind(self, renderer: "ElementRenderer",
terminal_caps: "TerminalCapabilities") -> None:
"""Bind this strategy to a renderer and terminal capabilities.
@@ -26435,24 +26437,24 @@ The strategy pattern creates a clean separation between **timing/ordering policy
on_session_begin, since the session owns the stream.
"""
...
-
+
def on_session_begin(self, session: OutputSession, stream: IO) -> None:
"""Called when the session opens. The strategy receives the output
stream and may write preamble (e.g., opening JSON bracket)."""
...
-
+
def on_element_created(self, event: ElementCreated) -> None:
"""Called when a new element handle is created."""
...
-
+
def on_element_updated(self, event: ElementUpdated) -> None:
"""Called when data is written to an element handle."""
...
-
+
def on_element_closed(self, event: ElementClosed) -> None:
"""Called when an element handle is closed."""
...
-
+
def on_session_end(self, event: SessionEnd) -> None:
"""Called when the session closes. The strategy may write epilogue."""
...
@@ -26490,7 +26492,7 @@ The strategy pattern creates a clean separation between **timing/ordering policy
and redraws all dirty elements in a single pass per frame.
"""
strategy_name = "live"
-
+
_frame_rate: float = 15.0 # Maximum redraws per second
_element_regions: OrderedDict[str, ScreenRegion] # handle_id → screen region
_dirty_set: set[str] # handle_ids that need redraw
@@ -26531,7 +26533,7 @@ The strategy pattern creates a clean separation between **timing/ordering policy
order in which data arrived or handles closed.
"""
strategy_name = "sequential_buffer"
-
+
_next_render_index: int = 0 # The declaration index to render next
_rendered_buffers: dict[int, str] # index → pre-rendered content (waiting)
_closed_set: set[int] # Declaration indices of closed elements
@@ -26579,49 +26581,49 @@ While the `MaterializationStrategy` controls *when* elements are rendered, the `
- JsonElementRenderer: JSON serialization
- YamlElementRenderer: YAML serialization
"""
-
+
format_name: str # "plain", "color", "table", "rich", "json", "yaml"
-
+
def render_panel(self, panel: Panel, stream: IO) -> None:
"""Render a panel element to the stream."""
...
-
+
def render_table(self, table: Table, stream: IO) -> None:
"""Render a table element to the stream."""
...
-
+
def render_tree(self, tree: Tree, stream: IO) -> None:
"""Render a tree element to the stream."""
...
-
+
def render_status(self, status: StatusMessage, stream: IO) -> None:
"""Render a status message to the stream."""
...
-
+
def render_progress(self, progress: ProgressIndicator, stream: IO) -> None:
"""Render a progress indicator to the stream."""
...
-
+
def render_code(self, code: CodeBlock, stream: IO) -> None:
"""Render a code block to the stream."""
...
-
+
def render_diff(self, diff: DiffBlock, stream: IO) -> None:
"""Render a diff block to the stream."""
...
-
+
def render_text(self, text: TextBlock, stream: IO) -> None:
"""Render a text block to the stream."""
...
-
+
def render_separator(self, separator: Separator, stream: IO) -> None:
"""Render a visual separator to the stream."""
...
-
+
def render_action_hint(self, hint: ActionHint, stream: IO) -> None:
"""Render an action hint to the stream."""
...
-
+
def render_element(self, element: OutputElement, stream: IO) -> None:
"""Dispatch to the appropriate render method based on element type.
@@ -26643,7 +26645,7 @@ While the `MaterializationStrategy` controls *when* elements are rendered, the `
handler = dispatch.get(element.element_type)
if handler:
handler(element, stream)
-
+
def serialize(self, output: StructuredOutput, stream: IO) -> None:
"""Serialize a complete StructuredOutput to the stream.
@@ -26652,7 +26654,7 @@ While the `MaterializationStrategy` controls *when* elements are rendered, the `
output.elements and calls render_element for each.
"""
...
-
+
def can_render(self, terminal_caps: "TerminalCapabilities") -> bool:
"""Whether this renderer can operate in the given terminal environment."""
...
@@ -27280,9 +27282,9 @@ The framework uses a **registry pattern** for format renderers, enabling third-p
"json" → (AccumulateMaterializer, JsonElementRenderer), fallback=None
"yaml" → (AccumulateMaterializer, YamlElementRenderer), fallback=None
"""
-
+
_formats: dict[str, FormatRegistration] = {}
-
+
@classmethod
def register(cls, format_name: str,
strategy_factory: Callable[[TerminalCapabilities], MaterializationStrategy],
@@ -27304,7 +27306,7 @@ The framework uses a **registry pattern** for format renderers, enabling third-p
renderer_factory=renderer_factory,
fallback=fallback,
)
-
+
@classmethod
def resolve(cls, format_name: str,
terminal_caps: TerminalCapabilities
@@ -27321,33 +27323,33 @@ The framework uses a **registry pattern** for format renderers, enabling third-p
"""
current = format_name
visited: set[str] = set()
-
+
while current and current not in visited:
visited.add(current)
registration = cls._formats.get(current)
if registration is None:
break
-
+
renderer = registration.renderer_factory(terminal_caps)
if renderer.can_render(terminal_caps):
strategy = registration.strategy_factory(terminal_caps)
strategy.bind(renderer, terminal_caps=terminal_caps)
return strategy, renderer
-
+
current = registration.fallback
-
+
# Ultimate fallback is always plain
plain = cls._formats["plain"]
renderer = plain.renderer_factory(terminal_caps)
strategy = plain.strategy_factory(terminal_caps)
strategy.bind(renderer, terminal_caps=terminal_caps)
return strategy, renderer
-
+
@classmethod
def available_formats(cls) -> list[str]:
"""Return all registered format names."""
return sorted(cls._formats.keys())
-
+
@classmethod
def is_registered(cls, format_name: str) -> bool:
"""Check if a format is registered."""
@@ -27380,7 +27382,7 @@ The framework detects terminal capabilities to guide format resolution, strategy
supports_alternate_screen: bool # Supports alternate screen buffer?
no_color: bool # Is NO_COLOR environment variable set?
term_program: str | None # TERM_PROGRAM value (e.g., "iTerm2", "vscode")
-
+
@classmethod
def detect(cls) -> "TerminalCapabilities":
"""Auto-detect terminal capabilities from the environment.
@@ -27411,13 +27413,13 @@ Third-party plugins can register custom formats using the registry:
class CsvElementRenderer(ElementRenderer):
"""Renders tables as CSV, other elements as plain text."""
format_name = "csv"
-
+
def render_table(self, table: Table, stream: IO) -> None:
writer = csv.writer(stream)
writer.writerow([col.name for col in table.columns])
for row in table.rows:
writer.writerow([row.get(col.name, "") for col in table.columns])
-
+
def can_render(self, terminal_caps: TerminalCapabilities) -> bool:
return True # CSV works everywhere
@@ -27503,7 +27505,7 @@ Example of producer error handling:
table = session.table("Resources", columns=[
ColumnDef(name="Name"), ColumnDef(name="Type"), ColumnDef(name="Status"),
])
-
+
try:
async for resource in client.list_resources():
table.add_row({
@@ -27515,7 +27517,7 @@ Example of producer error handling:
table.close() # Close with partial data
session.status(f"Error fetching resources: {e}", level="error")
return
-
+
table.close()
session.status(f"{table.element.row_count} resources listed", level="ok")
@@ -27599,9 +27601,9 @@ The simplest usage — a command that creates elements, populates them synchrono
project = await api.get_project(project_name)
resources = await api.list_project_resources(project.name)
validations = await api.list_project_validations(project.name)
-
+
# --- Build output elements ---
-
+
# Panel: Project details
with session.panel("Project Details") as panel:
panel.set_entries({
@@ -27616,7 +27618,7 @@ The simplest usage — a command that creates elements, populates them synchrono
"Remote": "success" if not project.remote else "info",
"Created": "success",
})
-
+
# Table: Linked resources
with session.table("Linked Resources", columns=[
ColumnDef(name="Resource", type="string", style_hint="identifier"),
@@ -27631,7 +27633,7 @@ The simplest usage — a command that creates elements, populates them synchrono
"Sandbox": r.sandbox_strategy,
"Read-Only": "yes" if r.read_only else "no",
})
-
+
# Table: Validations
with session.table(f"Validations ({len(validations)})", columns=[
ColumnDef(name="ID", type="id", style_hint="identifier"),
@@ -27644,7 +27646,7 @@ The simplest usage — a command that creates elements, populates them synchrono
"Command": v.command,
"Mode": v.mode,
})
-
+
# Status: Final message
session.status("Project loaded", level="ok")
async def cmd_resource_list(session: OutputSession, project: str | None) -> None:
"""Implementation of 'agents resource list'."""
-
+
# Create the table handle — it will accumulate rows as we stream them
table = session.table("Resources", columns=[
ColumnDef(name="Name", type="string", style_hint="identifier"),
@@ -27778,10 +27780,10 @@ A command that streams rows into a table as results arrive from a paginated API.
ColumnDef(name="Sandbox"),
ColumnDef(name="Status"),
], sort_key="Name")
-
+
# Create a progress indicator for the fetch operation
progress = session.progress("Fetching resources...", indeterminate=True)
-
+
# Stream pages from the API
count = 0
async for page in api.list_resources_paginated(project=project):
@@ -27794,17 +27796,17 @@ A command that streams rows into a table as results arrive from a paginated API.
"Status": resource.status,
})
count += 1
-
+
# Update progress label with count so far
progress.set_label(f"Fetching resources... ({count} found)")
-
+
# Close the progress indicator (it has served its purpose)
progress.close()
-
+
# Set summary and close the table
table.set_summary({"total": count})
table.close()
-
+
# Final status
session.status(f"{count} resources listed", level="ok")