sync: auto-sync from HOWARD-HOME at 2026-06-21 20:36:14

Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-21 20:36:14
This commit is contained in:
2026-06-21 20:36:41 -07:00
parent 231fcb6d6f
commit 87688b65da
6 changed files with 349 additions and 14 deletions

View File

@@ -16,12 +16,13 @@ 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).
NOTE (instance state, 2026-06-21): the installed RESTful API Manager extension is
LIMITED — only `GetSessionsByName` exists; other methods 500 "Web method does not
exist". Full control (SendCommandToSession, GetSessions, UpdateSessionCustom-
Properties, ...) requires updating the extension on the instance. The client is
built to expose those methods as soon as the extension is unlocked; `raw()` probes
arbitrary methods in the meantime.
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.
"""
from __future__ import annotations
@@ -205,23 +206,24 @@ class ScreenConnectClient:
return self.call("GetSessionsByName", {"sessionName": session_name})
# ======================================================================
# Methods pending the extension unlock (currently 500 "web method does not
# exist"). Exposed here so the CLI is ready; verify each once unlocked.
# Shapes are best-effort from the RESTful API Manager docs and MUST be
# confirmed by live probing before relying on them.
# 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).
# ======================================================================
def get_session_details(self, session_id: str) -> Any:
"""GetSessionDetailsBySessionID full detail for one session. PENDING UNLOCK."""
"""GetSessionDetailsBySessionID - full detail for one session. VERIFIED."""
return self.call("GetSessionDetailsBySessionID", {"sessionID": session_id})
def send_command_to_session(self, session_id: str, command: str) -> Any:
"""SendCommandToSession run a backstage command on a guest. EXISTS on this
"""SendCommandToSession - run a backstage command on a guest. EXISTS on this
instance. POST body is a POSITIONAL ARRAY [sessionID, command]
(e.g. ["<uuid>", "ipconfig"]). STATE-CHANGING (gate behind --confirm)."""
return self.call("SendCommandToSession", [session_id, command])
def send_message_to_session(self, session_id: str, message: str) -> Any:
"""SendMessageToSession send a chat message to a guest. EXISTS.
"""SendMessageToSession - send a chat message to a guest. EXISTS.
POST body positional array [sessionID, message]. STATE-CHANGING."""
return self.call("SendMessageToSession", [session_id, message])
@@ -236,7 +238,7 @@ class ScreenConnectClient:
return self.call(method, body, http_method=http_method)
# ======================================================================
# INSTALLER BUILDER (no API call constructs the parameterized access
# INSTALLER BUILDER (no API call - constructs the parameterized access
# installer URL so a device self-tags into the right Company/Site/Tag).
# ======================================================================
def build_installer_url(

View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Read-only / gating self-test for the screenconnect skill.
Runs each CLI command as a subprocess and checks exit code + output markers. NO
state-changing API calls (writes are tested only in their --confirm-absent refusal
path). build-installer is a pure URL build (no network). Prints a PASS/FAIL report.
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
SC = os.path.join(HERE, "sc.py")
results = []
def run(args):
env = dict(os.environ)
env.setdefault("CLAUDETOOLS_ROOT", "C:/claudetools")
env["PYTHONIOENCODING"] = "utf-8"
env["SC_SUPPRESS_ERRORLOG"] = "1" # never let the self-test touch errorlog.md
p = subprocess.run([sys.executable, SC] + args, capture_output=True,
text=True, env=env, timeout=120)
return p.returncode, p.stdout, p.stderr
def check(name, args, *, want_rc=None, out_has=None, out_json_ok=False):
rc, out, err = run(args)
problems = []
if want_rc is not None and rc != want_rc:
problems.append(f"rc={rc} want {want_rc}")
if out_has and out_has not in out:
problems.append(f"stdout missing {out_has!r}")
if out_json_ok:
try:
json.loads(out)
except Exception as e:
problems.append(f"stdout not JSON: {e}")
results.append(("PASS" if not problems else "FAIL", name, "; ".join(problems)))
# --- reads: succeed (rc 0) [hit the live instance] ---
check("status", ["status"], want_rc=0, out_has="Authenticated")
check("sessions", ["sessions"], want_rc=0, out_has="Sessions:")
check("sessions json", ["sessions", "--json"], want_rc=0, out_json_ok=True)
# --- build-installer: pure URL build (no network) ---
check("build-installer", ["build-installer", "--name", "HOST", "--company", "AZ Computer Guru",
"--site", "Howard-VM", "--tag", "T1"], want_rc=0, out_has="e=Access")
check("build-installer encodes spaces", ["build-installer", "--company", "A B C"],
want_rc=0, out_has="A%20B%20C")
check("build-installer json", ["build-installer", "--json", "--tag", "X"],
want_rc=0, out_json_ok=True)
# --- gating: writes refuse without --confirm (rc 3, no API call) ---
check("send-command no confirm -> rc3", ["send-command", "--session", "x", "--command", "whoami"],
want_rc=3, out_has="Would")
check("send-message no confirm -> rc3", ["send-message", "--session", "x", "--message", "hi"],
want_rc=3)
check("set-properties no confirm -> rc3",
["set-properties", "--session", "x", "--props-json", '["A","B","C"]'], want_rc=3)
check("set-properties bad json -> rc2",
["set-properties", "--session", "x", "--props-json", "{bad", "--confirm"], want_rc=2)
# --- raw: read ok; destructive method refused without --confirm ---
check("raw read ok", ["raw", "--method", "GetSessionsByName", "--body", '{"sessionName":""}'],
want_rc=0)
check("raw SendCommandToSession no confirm -> rc3",
["raw", "--method", "SendCommandToSession", "--body", "[]"], want_rc=3)
# --- report ---
print("\n==== screenconnect skill self-test ====")
npass = sum(1 for r in results if r[0] == "PASS")
for status, name, prob in results:
print(f"[{status}] {name}" + (f" -> {prob}" if prob else ""))
print(f"\n{npass}/{len(results)} passed, {len(results)-npass} failed")
sys.exit(0 if npass == len(results) else 1)