sync: auto-sync from HOWARD-HOME at 2026-07-06 16:10:24

Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 16:10:24
This commit is contained in:
2026-07-06 16:10:52 -07:00
parent 8f4984c5a6
commit 7dc519827c
6 changed files with 186 additions and 52 deletions

View File

@@ -16,13 +16,20 @@ application/json and the body is the method's parameters (object or array).
Credentials: never hardcoded. api_secret loaded at runtime from the SOPS vault,
or the SCREENCONNECT_API_SECRET env var (testing override).
INSTANCE METHOD SURFACE (probed live 2026-06-22): control IS available -
GetSessionsByName, GetSessionDetailsBySessionID, GetSessionBySessionID (reads,
JSON-object params); SendCommandToSession, SendMessageToSession,
UpdateSessionCustomProperties, CreateSession (writes, POSITIONAL-ARRAY params).
MISSING on this extension version: GetSessions/GetAllSessions/GetSessionGroups
(full-fleet inventory) - "web method does not exist"; pending an admin update of
the RESTful API Manager extension. `raw()` probes arbitrary methods.
INSTANCE METHOD SURFACE (probed live 2026-06-22, extended 2026-07-06): control IS
available - GetSessionsByFilter, GetSessionsByName, GetSessionDetailsBySessionID,
GetSessionBySessionID (reads, JSON-object params); SendCommandToSession,
SendMessageToSession, UpdateSessionCustomProperties, CreateSession (writes,
POSITIONAL-ARRAY params).
FULL-FLEET LISTING (fixed 2026-07-06): GetSessionsByFilter({"sessionFilter": <expr>})
IS exposed and is the enumeration path. It takes a ScreenConnect session-filter
expression, e.g. SessionType = 'Access' (all 958 unattended agents), CustomProperty1
= 'Safesite' (per-client), Name LIKE '*DELL*' (partial name). SessionType 'Access'
(=2) is the agent fleet; 'Support' (=0) the on-demand sessions. The bare aliases
GetSessions/GetAllSessions/GetSessionGroups remain absent, but GetSessionsByFilter
covers the inventory use case fully - no extension update needed. `raw()` probes
arbitrary methods.
"""
from __future__ import annotations
@@ -63,6 +70,10 @@ SKILL_DIR = Path(__file__).resolve().parent.parent
# Custom-property mapping on this instance (from the vault notes).
CUSTOM_PROPERTIES = {"CP1": "Company", "CP2": "Site", "CP3": "Tag"}
# SessionType filter literals (verified live 2026-07-06): 'Access' (=2) is the
# unattended agent fleet; 'Support' (=0) the on-demand sessions.
SESSION_TYPE_LITERALS = {"access": "Access", "support": "Support", "meeting": "Meeting"}
class ScreenConnectError(RuntimeError):
"""Raised for transport or API errors."""
@@ -200,17 +211,72 @@ class ScreenConnectClient:
# VERIFIED methods (work on the current instance)
# ======================================================================
def get_sessions_by_name(self, session_name: str = "") -> Any:
"""List sessions whose Name matches `session_name` (RESTful API Manager
GetSessionsByName). VERIFIED LIVE. Empty string returns sessions with a
blank Name (the unattended access agents on this instance)."""
"""List sessions whose Name EXACTLY equals `session_name` (RESTful API
Manager GetSessionsByName). VERIFIED LIVE. This is an exact match, not a
contains match ("" returns only the blank-Name sessions, not the fleet).
For full-fleet or partial-name listing use list_sessions()/get_sessions_by_filter()."""
return self.call("GetSessionsByName", {"sessionName": session_name})
def get_sessions_by_filter(self, session_filter: str) -> Any:
"""Enumerate sessions matching a ScreenConnect session-filter expression
(GetSessionsByFilter, param `sessionFilter`). VERIFIED LIVE 2026-07-06 -
this IS the full-fleet listing path. Examples:
SessionType = 'Access' -> every unattended agent (the fleet)
CustomProperty1 = 'Safesite' -> one client's machines
Name LIKE '*DELL*' -> partial name (case-insensitive)
Combine with AND/OR. String literals are single-quoted."""
return self.call("GetSessionsByFilter", {"sessionFilter": session_filter})
@staticmethod
def _filter_literal(value: str) -> str:
"""Single-quote a value for the session-filter language, escaping any
embedded single quote by doubling it (SQL-style)."""
return "'" + str(value).replace("'", "''") + "'"
def build_session_filter(
self,
session_type: Optional[str] = "access",
name: Optional[str] = None,
name_like: Optional[str] = None,
company: Optional[str] = None,
site: Optional[str] = None,
tag: Optional[str] = None,
extra: Optional[str] = None,
) -> str:
"""Assemble a session-filter expression from structured parts (AND-joined).
`session_type`: access | support | meeting | all (None/all -> no type clause).
`name` exact, `name_like` partial (wrapped in *...*), company/site/tag map to
CP1/CP2/CP3, `extra` is a raw clause appended verbatim. Defaults to the whole
Access fleet when nothing else is specified."""
clauses: list[str] = []
if session_type and session_type.lower() != "all":
literal = SESSION_TYPE_LITERALS.get(session_type.lower(), session_type)
clauses.append(f"SessionType = {self._filter_literal(literal)}")
if name:
clauses.append(f"Name = {self._filter_literal(name)}")
if name_like:
clauses.append(f"Name LIKE {self._filter_literal('*' + name_like + '*')}")
if company:
clauses.append(f"CustomProperty1 = {self._filter_literal(company)}")
if site:
clauses.append(f"CustomProperty2 = {self._filter_literal(site)}")
if tag:
clauses.append(f"CustomProperty3 = {self._filter_literal(tag)}")
if extra:
clauses.append(extra)
return " AND ".join(clauses) if clauses else "SessionType = 'Access'"
def list_sessions(self, **kwargs) -> Any:
"""Convenience: build a filter from keyword parts and enumerate. See
build_session_filter for the accepted keywords."""
return self.get_sessions_by_filter(self.build_session_filter(**kwargs))
# ======================================================================
# Control + detail methods (all VERIFIED LIVE 2026-06-22 on the ACG instance).
# Reads take a JSON object {sessionID:...}; the POST/write methods take a
# POSITIONAL ARRAY. NOTE: full-fleet inventory (GetSessions) is NOT exposed by
# this extension version (returns "web method does not exist") - pending an
# admin update of the RESTful API Manager extension (see SKILL.md).
# POSITIONAL ARRAY. Full-fleet inventory is served by GetSessionsByFilter
# (see get_sessions_by_filter / list_sessions above) - the bare GetSessions
# alias is absent but not needed.
# ======================================================================
def get_session_details(self, session_id: str) -> Any:
"""GetSessionDetailsBySessionID - full detail for one session. VERIFIED."""