Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,623 @@
|
||||
"""Config / projects / setup JSON-RPC handlers (moved verbatim from server.py).
|
||||
|
||||
NOTE: ``config.set`` stays in server.py for now — the in-flight
|
||||
opt/model-resolution-core PR touches it; move it in a follow-up once merged.
|
||||
|
||||
Handler bodies are byte-identical to their pre-split server.py form; they
|
||||
are rebound onto server.py's globals at install time — see method_ctx.py.
|
||||
"""
|
||||
|
||||
from .method_ctx import HandlerRegistry
|
||||
|
||||
from hermes_constants import DEFAULT_INDICATOR_STYLE, INDICATOR_STYLES
|
||||
|
||||
_registry = HandlerRegistry()
|
||||
method = _registry.method
|
||||
_profile_scoped = _registry.profile_scoped
|
||||
|
||||
|
||||
@method("projects.discover_repos")
|
||||
@_profile_scoped
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Repos for the desktop overview: scanned-from-disk (cached) ∪ session-derived."""
|
||||
try:
|
||||
with _profile_db(params) as db:
|
||||
if db is None:
|
||||
return _ok(rid, {"repos": []})
|
||||
from hermes_cli import projects_db as pdb
|
||||
|
||||
policy = _repo_discovery_policy()
|
||||
policy_key = _repo_discovery_policy_key(policy)
|
||||
with pdb.connect_closing() as conn:
|
||||
pdb.reconcile_discovered_repos_policy(
|
||||
conn,
|
||||
policy_key,
|
||||
preserve_unversioned=_repo_discovery_policy_is_default(policy),
|
||||
)
|
||||
# `scan=true` (set by the desktop in remote-gateway mode): run a
|
||||
# backend-side filesystem scan of the policy roots so repos with
|
||||
# zero Hermes sessions still surface. The desktop's native scan
|
||||
# only runs on the local filesystem; on a remote connection it
|
||||
# must ask the host to scan itself (#81723).
|
||||
if params.get("scan") and policy["enabled"]:
|
||||
_scan_discovered_repos_remote(conn, policy)
|
||||
repos = _discover_repos_payload(
|
||||
db, conn=conn, include_cached=policy["enabled"]
|
||||
)
|
||||
return _ok(rid, {"repos": repos, "discovery_policy": policy})
|
||||
except Exception as e:
|
||||
return _err(rid, 5061, str(e))
|
||||
|
||||
|
||||
@method("projects.record_repos")
|
||||
@_profile_scoped
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Persist git repo roots found by the client's filesystem scan, then return
|
||||
the merged repo list. The native crawl runs on the desktop (local fs); this
|
||||
caches the result so later reads are instant instead of re-walking disk."""
|
||||
try:
|
||||
from hermes_cli import projects_db as pdb
|
||||
|
||||
policy = _repo_discovery_policy()
|
||||
policy_key = _repo_discovery_policy_key(policy)
|
||||
incoming_raw = params.get("discovery_policy")
|
||||
incoming_policy = (
|
||||
_repo_discovery_policy(incoming_raw)
|
||||
if isinstance(incoming_raw, dict)
|
||||
else None
|
||||
)
|
||||
incoming_matches = (
|
||||
incoming_policy is not None
|
||||
and _repo_discovery_policy_key(incoming_policy) == policy_key
|
||||
)
|
||||
accept_legacy_default = (
|
||||
incoming_policy is None and _repo_discovery_policy_is_default(policy)
|
||||
)
|
||||
|
||||
pairs: list[tuple[str, str | None]] = []
|
||||
for item in params.get("repos") or []:
|
||||
if isinstance(item, str):
|
||||
pairs.append((item, None))
|
||||
elif isinstance(item, dict) and item.get("root"):
|
||||
pairs.append((str(item["root"]), item.get("label")))
|
||||
|
||||
with pdb.connect_closing() as conn:
|
||||
pdb.reconcile_discovered_repos_policy(
|
||||
conn,
|
||||
policy_key,
|
||||
preserve_unversioned=_repo_discovery_policy_is_default(policy),
|
||||
)
|
||||
accepted = bool(
|
||||
policy["enabled"] and (incoming_matches or accept_legacy_default)
|
||||
)
|
||||
if accepted:
|
||||
pdb.record_discovered_repos(
|
||||
conn, pairs, replace=True, policy_key=policy_key
|
||||
)
|
||||
elif not policy["enabled"]:
|
||||
pdb.clear_discovered_repos(conn, policy_key=policy_key)
|
||||
|
||||
with _profile_db(params) as db:
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"repos": _discover_repos_payload(
|
||||
db, include_cached=policy["enabled"]
|
||||
)
|
||||
if db is not None
|
||||
else [],
|
||||
"accepted": accepted,
|
||||
"discovery_policy": policy,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
return _err(rid, 5061, str(e))
|
||||
|
||||
|
||||
@method("projects.tree")
|
||||
@_profile_scoped
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Authoritative project overview: project -> repo -> lane structure with
|
||||
counts + a few preview sessions per project, plus the flat set of session
|
||||
ids claimed by any project (so the desktop excludes them from flat Recents).
|
||||
Lanes carry no session rows here; drill-in uses ``projects.project_sessions``.
|
||||
"""
|
||||
try:
|
||||
from tui_gateway.project_tree import stamp_profile
|
||||
from tui_gateway.server import _response_profile_name
|
||||
|
||||
with _profile_db(params) as db:
|
||||
if db is None:
|
||||
return _ok(
|
||||
rid, {"projects": [], "active_id": None, "scoped_session_ids": []}
|
||||
)
|
||||
|
||||
tree, active_id = _build_project_tree(
|
||||
db,
|
||||
preview_limit=int(params.get("preview_limit") or 3),
|
||||
hydrate=False,
|
||||
session_limit=int(params.get("session_limit") or 2000),
|
||||
include_discovered=True,
|
||||
)
|
||||
stamp_profile(
|
||||
tree["projects"], _response_profile_name(params.get("profile"))
|
||||
)
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"projects": tree["projects"],
|
||||
"active_id": active_id,
|
||||
"scoped_session_ids": tree["scoped_session_ids"],
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
return _err(rid, 5061, str(e))
|
||||
|
||||
|
||||
@method("projects.project_sessions")
|
||||
@_profile_scoped
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Fully hydrated lanes (repo -> lane -> session rows) for one project,
|
||||
built from the same authoritative grouping as ``projects.tree`` so ids and
|
||||
membership match exactly. Used when the user enters a project."""
|
||||
try:
|
||||
from tui_gateway.project_tree import stamp_profile
|
||||
from tui_gateway.server import _response_profile_name
|
||||
|
||||
project_id = str(params.get("project_id") or "")
|
||||
if not project_id:
|
||||
return _err(rid, 5063, "project_id required")
|
||||
|
||||
with _profile_db(params) as db:
|
||||
if db is None:
|
||||
return _ok(rid, {"project": None})
|
||||
|
||||
# Drill-in only needs the entered project (which has sessions), so skip
|
||||
# the zero-session discovery tier entirely.
|
||||
tree, _active = _build_project_tree(
|
||||
db,
|
||||
preview_limit=0,
|
||||
hydrate=True,
|
||||
session_limit=int(params.get("session_limit") or 5000),
|
||||
include_discovered=False,
|
||||
)
|
||||
stamp_profile(
|
||||
tree["projects"], _response_profile_name(params.get("profile"))
|
||||
)
|
||||
proj = next((p for p in tree["projects"] if p["id"] == project_id), None)
|
||||
return _ok(rid, {"project": proj})
|
||||
except Exception as e:
|
||||
return _err(rid, 5061, str(e))
|
||||
|
||||
|
||||
@method("config.get")
|
||||
@_profile_scoped
|
||||
def _(rid, params: dict) -> dict:
|
||||
key = params.get("key", "")
|
||||
if key == "provider":
|
||||
try:
|
||||
from hermes_cli.models import list_available_providers, normalize_provider
|
||||
|
||||
model = _resolve_model()
|
||||
parts = model.split("/", 1)
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"model": model,
|
||||
"provider": (
|
||||
normalize_provider(parts[0]) if len(parts) > 1 else "unknown"
|
||||
),
|
||||
"providers": list_available_providers(),
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
return _err(rid, 5013, str(e))
|
||||
if key == "profile":
|
||||
from hermes_constants import display_hermes_home
|
||||
|
||||
return _ok(rid, {"home": str(_hermes_home), "display": display_hermes_home()})
|
||||
if key == "project":
|
||||
cfg_terminal = _load_cfg().get("terminal") or {}
|
||||
raw = str(params.get("cwd", "") or cfg_terminal.get("cwd", "") or "").strip()
|
||||
cwd = _completion_cwd({"cwd": raw} if raw else {})
|
||||
return _ok(rid, {"cwd": cwd, "branch": _git_branch_for_cwd(cwd)})
|
||||
if key == "full":
|
||||
return _ok(rid, {"config": _load_cfg()})
|
||||
if key == "prompt":
|
||||
return _ok(rid, {"prompt": _load_cfg().get("custom_prompt", "")})
|
||||
if key == "skin":
|
||||
return _ok(
|
||||
rid, {"value": (_load_cfg().get("display") or {}).get("skin", "default")}
|
||||
)
|
||||
if key == "indicator":
|
||||
# Normalize so a hand-edited config.yaml with stray casing or
|
||||
# an unknown value reads back the SAME value the TUI actually
|
||||
# rendered (frontend's `normalizeIndicatorStyle` falls back to
|
||||
# `DEFAULT_INDICATOR_STYLE` for the same inputs). Otherwise
|
||||
# `/indicator` would print one thing while the UI shows another.
|
||||
raw = (_load_cfg().get("display") or {}).get("tui_status_indicator", "")
|
||||
norm = str(raw).strip().lower()
|
||||
return _ok(
|
||||
rid,
|
||||
{"value": norm if norm in INDICATOR_STYLES else DEFAULT_INDICATOR_STYLE},
|
||||
)
|
||||
if key == "personality":
|
||||
# Report the EFFECTIVE personality via the single owner — a stale or
|
||||
# unknown name in config must not display as active.
|
||||
from hermes_cli.personality import active_personality_name
|
||||
|
||||
return _ok(
|
||||
rid,
|
||||
{"value": active_personality_name(_load_cfg()) or "none"},
|
||||
)
|
||||
if key == "reasoning":
|
||||
cfg = _load_cfg()
|
||||
session = _sessions.get(params.get("session_id", ""))
|
||||
reasoning_config = None
|
||||
if session is not None:
|
||||
if isinstance(session.get("create_reasoning_override"), dict):
|
||||
reasoning_config = session.get("create_reasoning_override")
|
||||
else:
|
||||
agent = session.get("agent")
|
||||
agent_reasoning = getattr(agent, "reasoning_config", None)
|
||||
if isinstance(agent_reasoning, dict):
|
||||
reasoning_config = agent_reasoning
|
||||
|
||||
if isinstance(reasoning_config, dict):
|
||||
if reasoning_config.get("enabled") is False:
|
||||
effort = "none"
|
||||
else:
|
||||
effort = str(reasoning_config.get("effort") or "medium")
|
||||
else:
|
||||
raw_effort = (cfg.get("agent") or {}).get("reasoning_effort", "")
|
||||
if raw_effort is False:
|
||||
# YAML `reasoning_effort: false`/`off`/`no` — thinking
|
||||
# disabled, not "unset, show the medium default".
|
||||
effort = "none"
|
||||
else:
|
||||
effort = str(raw_effort or "medium")
|
||||
display = (
|
||||
"show"
|
||||
if bool((cfg.get("display") or {}).get("show_reasoning", True))
|
||||
else "hide"
|
||||
)
|
||||
return _ok(rid, {"value": effort, "display": display})
|
||||
if key == "fast":
|
||||
# Prefer the session's live/pinned value — `config.set fast` is
|
||||
# session-scoped, so the global key may not reflect this chat. A
|
||||
# pre-build session keeps its pin in create_service_tier_override.
|
||||
session = _sessions.get(params.get("session_id", ""))
|
||||
tier = None
|
||||
if session is not None:
|
||||
agent = session.get("agent")
|
||||
if agent is not None:
|
||||
tier = getattr(agent, "service_tier", None)
|
||||
elif session.get("create_service_tier_override") is not None:
|
||||
tier = session["create_service_tier_override"]
|
||||
if tier is None:
|
||||
tier = _load_service_tier()
|
||||
return _ok(rid, {"value": "fast" if tier == "priority" else "normal"})
|
||||
if key == "busy":
|
||||
return _ok(rid, {"value": _load_busy_input_mode()})
|
||||
if key in {"approval_mode", "approvals.mode"}:
|
||||
try:
|
||||
return _ok(rid, {"value": _load_approval_mode()})
|
||||
except Exception as e:
|
||||
return _err(rid, 5001, str(e))
|
||||
if key == "details_mode":
|
||||
allowed_dm = frozenset({"hidden", "collapsed", "expanded"})
|
||||
raw = (
|
||||
str(
|
||||
(_load_cfg().get("display") or {}).get("details_mode", "collapsed")
|
||||
or "collapsed"
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
nv = raw if raw in allowed_dm else "collapsed"
|
||||
return _ok(rid, {"value": nv})
|
||||
if key == "thinking_mode":
|
||||
allowed_tm = frozenset({"collapsed", "truncated", "full"})
|
||||
cfg = _load_cfg()
|
||||
raw = (
|
||||
str((cfg.get("display") or {}).get("thinking_mode", "") or "")
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
if raw in allowed_tm:
|
||||
nv = raw
|
||||
else:
|
||||
dm = (
|
||||
str(
|
||||
(cfg.get("display") or {}).get("details_mode", "collapsed")
|
||||
or "collapsed"
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
nv = "full" if dm == "expanded" else "collapsed"
|
||||
return _ok(rid, {"value": nv})
|
||||
if key == "density":
|
||||
on = bool((_load_cfg().get("display") or {}).get("tui_compact", False))
|
||||
return _ok(rid, {"value": "on" if on else "off"})
|
||||
if key == "theme":
|
||||
display = _load_cfg().get("display")
|
||||
raw = (
|
||||
str(
|
||||
display.get("tui_theme", "auto")
|
||||
if isinstance(display, dict)
|
||||
else "auto"
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
return _ok(rid, {"value": raw if raw in {"auto", "light", "dark"} else "auto"})
|
||||
if key == "statusbar":
|
||||
display = _load_cfg().get("display")
|
||||
raw = (
|
||||
display.get("tui_statusbar", "top") if isinstance(display, dict) else "top"
|
||||
)
|
||||
return _ok(rid, {"value": _coerce_statusbar(raw)})
|
||||
if key == "focus":
|
||||
display = _load_cfg().get("display")
|
||||
on = (
|
||||
bool(display.get("focus_view", False))
|
||||
if isinstance(display, dict)
|
||||
else False
|
||||
)
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"value": "on" if on else "off",
|
||||
"tool_progress": _load_tool_progress_mode(),
|
||||
},
|
||||
)
|
||||
if key == "mouse":
|
||||
display = _load_cfg().get("display")
|
||||
return _ok(rid, {"value": _display_mouse_tracking(display)})
|
||||
if key == "mtime":
|
||||
cfg_path = _hermes_home / "config.yaml"
|
||||
try:
|
||||
mtime = cfg_path.stat().st_mtime if cfg_path.exists() else 0
|
||||
except Exception:
|
||||
return _ok(rid, {"mtime": 0})
|
||||
# Revision hash of the MCP-relevant config sections. The TUI's
|
||||
# config-change poller uses it to reload MCP servers only when their
|
||||
# config actually changed — a /skin or /statusbar write bumps mtime
|
||||
# but must not cost a multi-second MCP reconnect.
|
||||
return _ok(rid, {"mtime": mtime, "mcp_rev": _compute_mcp_rev()})
|
||||
return _err(rid, 4002, f"unknown config key: {key}")
|
||||
|
||||
|
||||
def _readiness_profile_scope(params: dict):
|
||||
"""Resolve the optional ``profile`` param of the setup readiness RPCs.
|
||||
|
||||
Returns ``(profile, scope)`` where ``scope`` is a context manager binding
|
||||
that profile's HERMES_HOME and ``.env`` secret scope (ContextVars, so
|
||||
concurrent checks for different profiles stay isolated). The launch
|
||||
profile / no param yields ``("", nullcontext())``. A profile unknown to
|
||||
this host raises ``FileNotFoundError`` — a readiness check must never
|
||||
quietly answer for the launch profile instead (#94071).
|
||||
"""
|
||||
import contextlib
|
||||
|
||||
profile = str(params.get("profile") or "").strip() if isinstance(params, dict) else ""
|
||||
if not profile:
|
||||
return "", contextlib.nullcontext()
|
||||
from hermes_cli import profiles as profiles_mod
|
||||
from tui_gateway import server as _server
|
||||
|
||||
if not profiles_mod.profile_exists(profile):
|
||||
raise FileNotFoundError(f"Profile '{profile}' does not exist on this backend.")
|
||||
home = _server._profile_home(profile)
|
||||
if home is None:
|
||||
return profile, contextlib.nullcontext()
|
||||
return profile, _server._session_profile_runtime_scope({"profile_home": str(home)})
|
||||
|
||||
|
||||
@method("setup.status")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Loose provider check; ``profile`` (optional) scopes it to that profile's home."""
|
||||
try:
|
||||
from hermes_cli.main import _has_any_provider_configured
|
||||
from tui_gateway.methods_config import _readiness_profile_scope
|
||||
|
||||
try:
|
||||
profile, scope = _readiness_profile_scope(params)
|
||||
except FileNotFoundError as e:
|
||||
return _ok(rid, {"ok": False, "profile": params.get("profile"), "error": str(e)})
|
||||
with scope:
|
||||
configured = bool(_has_any_provider_configured(strict_profile_scope=bool(profile)))
|
||||
payload = {"provider_configured": configured}
|
||||
if profile:
|
||||
payload["profile"] = profile
|
||||
return _ok(rid, payload)
|
||||
except Exception as e:
|
||||
return _err(rid, 5016, str(e))
|
||||
|
||||
|
||||
@method("setup.runtime_check")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Strict provider check: does the configured/default model actually resolve to a usable runtime?
|
||||
|
||||
Unlike setup.status (which returns True if ANY provider auth state is
|
||||
discoverable, including indirect fallbacks like ``gh auth token`` for
|
||||
Copilot), this runs the same resolve_runtime_provider() call the agent
|
||||
uses on session creation. It returns ok=False with the auth error message
|
||||
when the user's configured model cannot actually be served, so UIs can
|
||||
surface onboarding before the user submits a doomed prompt.
|
||||
|
||||
``profile`` (optional): answer for THAT profile's home on this host — its
|
||||
config.yaml model pin and its ``.env`` — instead of the launch profile's
|
||||
(#94071). A profile unknown to this backend answers ``ok=False`` rather
|
||||
than reporting the launch profile's readiness.
|
||||
"""
|
||||
try:
|
||||
from hermes_cli.runtime_provider import resolve_runtime_provider
|
||||
from hermes_cli.auth import has_usable_secret
|
||||
from hermes_cli.main import _has_any_provider_configured
|
||||
from tui_gateway.methods_config import _readiness_profile_scope
|
||||
|
||||
requested = str(params.get("provider") or "").strip() or None
|
||||
try:
|
||||
profile, scope = _readiness_profile_scope(params)
|
||||
except FileNotFoundError as e:
|
||||
return _ok(rid, {"ok": False, "profile": params.get("profile"), "error": str(e)})
|
||||
with scope:
|
||||
runtime = resolve_runtime_provider(requested=requested)
|
||||
provider_configured = bool(_has_any_provider_configured(strict_profile_scope=bool(profile)))
|
||||
scoped = {"profile": profile} if profile else {}
|
||||
provider = runtime.get("provider") or "provider"
|
||||
source = str(runtime.get("source") or "")
|
||||
if (
|
||||
not provider_configured
|
||||
and provider == "bedrock"
|
||||
and source
|
||||
in {
|
||||
"iam-role",
|
||||
"aws-sdk-default-chain",
|
||||
}
|
||||
):
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"ok": False,
|
||||
"provider": provider,
|
||||
"model": runtime.get("model"),
|
||||
"source": source,
|
||||
"error": "No Hermes provider is configured.",
|
||||
**scoped,
|
||||
},
|
||||
)
|
||||
|
||||
api_key = runtime.get("api_key")
|
||||
api_key_text = "" if callable(api_key) else str(api_key or "").strip()
|
||||
credential_ok = (
|
||||
callable(api_key)
|
||||
or api_key_text in {"aws-sdk", "no-key-required"}
|
||||
or has_usable_secret(api_key_text)
|
||||
or bool(runtime.get("command"))
|
||||
)
|
||||
|
||||
if not credential_ok:
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"ok": False,
|
||||
"provider": provider,
|
||||
"model": runtime.get("model"),
|
||||
"source": runtime.get("source"),
|
||||
"error": f"No usable credentials found for {provider}.",
|
||||
**scoped,
|
||||
},
|
||||
)
|
||||
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"ok": True,
|
||||
"provider": runtime.get("provider"),
|
||||
"model": runtime.get("model"),
|
||||
"source": runtime.get("source"),
|
||||
**scoped,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
return _ok(rid, {"ok": False, "error": str(e)})
|
||||
|
||||
|
||||
@method("diagnostics.share_nous")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Upload a redacted debug bundle to Nous-internal diagnostics storage.
|
||||
|
||||
Desktop's "Send Diagnostics" action (error card / diagnostics UI). Same
|
||||
collection + force-redaction pipeline as ``hermes debug share --nous``
|
||||
(collect_share_bundle → build_nous_bundle → share_to_nous); redaction is
|
||||
NOT client-controllable — this handler always redacts.
|
||||
|
||||
Params (all optional):
|
||||
- ``error_context``: short client-supplied text describing the failure
|
||||
that prompted the report (the error card's layer/code/message blob).
|
||||
Redacted server-side and attached as ``error-context.txt``.
|
||||
- ``extra_files``: {label → text} of client-side artifacts the backend
|
||||
can't see (e.g. the local desktop.log when this backend is remote).
|
||||
Each value is force-redacted server-side before inclusion; labels are
|
||||
sanitized and size-capped.
|
||||
- ``log_lines``: report excerpt length (default 200).
|
||||
|
||||
Consent lives with the CALLER: the desktop shows the privacy notice and
|
||||
an explicit Upload button before invoking this. Structured envelope
|
||||
(``ok``/``error``) rather than JSON-RPC errors so the client can render
|
||||
upload failures inline.
|
||||
"""
|
||||
try:
|
||||
from hermes_cli.debug import (
|
||||
_redact_log_text,
|
||||
build_nous_bundle,
|
||||
collect_share_bundle,
|
||||
)
|
||||
from hermes_cli.diagnostics_upload import share_to_nous
|
||||
|
||||
log_lines = params.get("log_lines")
|
||||
if not isinstance(log_lines, int) or not (10 <= log_lines <= 2000):
|
||||
log_lines = 200
|
||||
|
||||
bundle = collect_share_bundle(log_lines=log_lines, redact=True)
|
||||
|
||||
# Client-supplied text goes through the SAME upload-safe log redactor
|
||||
# as backend-collected logs (_redact_log_text = force secret redaction
|
||||
# + email masking) — never the weaker bare secret pass, so the remote
|
||||
# path can't upload what the CLI pipeline would have removed.
|
||||
error_context = params.get("error_context")
|
||||
if isinstance(error_context, str) and error_context.strip():
|
||||
bundle["error-context.txt"] = _redact_log_text(
|
||||
error_context.strip()[:8_000]
|
||||
)
|
||||
|
||||
# Client-side artifacts (local desktop.log on remote connections).
|
||||
# Bounded: at most 4 files, 512KB of text each, sanitized labels —
|
||||
# this is a diagnostics channel, not an arbitrary upload surface.
|
||||
extra_files = params.get("extra_files")
|
||||
if isinstance(extra_files, dict):
|
||||
for label, text in list(extra_files.items())[:4]:
|
||||
if not isinstance(label, str) or not isinstance(text, str):
|
||||
continue
|
||||
safe_label = "".join(
|
||||
ch for ch in label if ch.isalnum() or ch in "._- ()"
|
||||
).strip()[:64]
|
||||
# Collapse dot-runs and leading dots so traversal-shaped labels
|
||||
# ("../../etc/passwd") can't survive even cosmetically.
|
||||
while ".." in safe_label:
|
||||
safe_label = safe_label.replace("..", ".")
|
||||
safe_label = safe_label.lstrip(".").strip()
|
||||
if not safe_label or not text.strip():
|
||||
continue
|
||||
bundle[f"client/{safe_label}"] = _redact_log_text(text[:524_288])
|
||||
|
||||
blob = build_nous_bundle(bundle, redact=True)
|
||||
res = share_to_nous(blob)
|
||||
view_url = res.get("viewUrl") or res.get("view_url")
|
||||
upload_id = res.get("id")
|
||||
if not view_url and not upload_id:
|
||||
# An upload the user can't reference is useless to support —
|
||||
# surface it as a failure instead of a linkless success.
|
||||
return _ok(
|
||||
rid,
|
||||
{"ok": False, "error": "upload succeeded but returned no view URL or id"},
|
||||
)
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"ok": True,
|
||||
"view_url": view_url,
|
||||
"upload_id": upload_id,
|
||||
"expires_at": res.get("expiresAt") or res.get("expires_at"),
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
return _ok(rid, {"ok": False, "error": str(e)})
|
||||
|
||||
|
||||
def register(server) -> None:
|
||||
"""Bind this module's handlers onto ``server``'s globals and registry."""
|
||||
_registry.install(server)
|
||||
Reference in New Issue
Block a user