Add GST Operator Agent EXE v1.6.0 with Windows path-safe build

This commit is contained in:
A R R R Associates
2026-09-20 17:21:20 +05:30
parent c63fc2773c
commit 1462a823ee
9 changed files with 361 additions and 54 deletions
@@ -1,45 +1,57 @@
ARRR GST Operator Agent 1.5.1
ARRR GST Operator Agent 1.6.0
================================
Purpose
-------
Runs only on the GST operator workstation. It opens visible Chrome/Edge, redeems a short-lived GST job token from ARRR ERP, fills the selected Credential Vault username/password, waits for manual CAPTCHA/OTP, downloads selected GST return data, and uploads the package back to ERP for transfer to the configured Storage Agent.
Primary deployment: self-installing Windows EXE.
Architecture
The Windows setup EXE is built from gst_operator_agent.py using:
powershell -ExecutionPolicy Bypass -File .\build_gst_operator_agent_exe.ps1
Generated file:
dist\ARRR_GST_Operator_Agent_Setup_1.6.0.exe
Operator workstation installation
---------------------------------
1. Download ARRR_GST_Operator_Agent_Setup_1.6.0.exe from ARRR ERP.
2. Double-click the EXE.
3. The EXE copies itself to:
%LOCALAPPDATA%\ARRR\GSTOperatorAgent\ARRR_GST_Operator_Agent.exe
4. It writes config.json for https://office.arrrassociates.com.
5. It registers the current-user Windows protocol:
arrrgst://
6. It creates a normal Windows Installed Apps uninstall registration.
7. Return to GST Return Reconciliation and start the GST download.
No administrator rights are required.
Python is not required on operator workstations.
No localhost web server or trusted local certificate is used.
Runtime flow
------------
ARRR ERP starts the agent through the Windows custom URL protocol arrrgst://. No browser-to-localhost HTTP/HTTPS request is used. No localhost TLS certificate, local web server, Tally access, or client storage access is required on the operator workstation.
ARRR ERP issues a short-lived signed GST operator token.
Windows opens arrrgst://start?token=...
The installed EXE redeems the token over HTTPS.
GST Portal username/password are retrieved from ERP Credential Vault only after token redemption.
The visible Chrome/Edge GST browser is opened by the operator agent.
The operator completes CAPTCHA/OTP.
The agent downloads the selected GST return JSON/ZIP files and uploads the verified package back to ARRR ERP for client storage.
Installation
------------
Run install_gst_operator_agent.ps1 as the Windows user who will operate GST downloads. Administrator rights are not required. Python 3.11+ and installed Chrome or Edge are required.
Source/repair fallback
----------------------
The ERP still exposes a source ZIP for development/repair. The legacy PowerShell source installer remains supported and preserves the previous behavior, but the EXE is the normal workstation installation method from version 1.6.0 onward.
Browser prompt
--------------
The first time ARRR ERP opens arrrgst://, Chrome/Edge may ask whether to open ARRR GST Operator Agent. Choose Open/Allow.
Security
Commands
--------
The custom URL contains only a short-lived signed job token. GST username/password are redeemed by the local agent directly from ARRR ERP and are never inserted in ERP page HTML or in the custom URL. CAPTCHA/OTP remains manual.
Self-test:
ARRR_GST_Operator_Agent.exe --self-test
Uninstall
---------
Run uninstall_gst_operator_agent.ps1. It removes the protocol registration and also cleans legacy v1.0.x localhost listener/startup/certificate state.
Repair/install explicitly:
ARRR_GST_Operator_Agent.exe --install --erp-base-url https://office.arrrassociates.com
V1.5.1 OCTAGST-STYLE RETURN-DASHBOARD EXTRACTION
- Preserves the v1.4.0 Credential Vault login, manual CAPTCHA/OTP, natural Welcome -> Return Dashboard flow, ERP upload and failure diagnostics.
- GSTR-2B no longer transfers the browser to gstr2b.gst.gov.in.
- GSTR-2B is requested from the authenticated return.gst.gov.in Return Dashboard using the GST offline-download API pattern used by the supplied OctaGST extension.
- The agent follows GST-provided download URLs, saves returned ZIP/JSON files, and safely extracts JSON members locally.
- Same-origin GSTR-1 and GSTR-3B API calls remain unchanged.
- No automatic GST logout is performed.
- A job is completed only after every selected period/return has a saved result and ERP/client-storage upload is confirmed.
- Manual JSON/ZIP import remains available in ERP as fallback.
V1.5.1 NATURAL MONTHLY RETURN FLOW + GSTR-2B PENDING POLL
--------------------------------------------------------
- Restores robust Aadhaar/E-KYC Remind me later / Later dismissal.
- GSTR-2B generation-accepted responses are PENDING, not failures; polls up to 25 minutes.
- GSTR-1: selects FY/Quarter/Month on Return Dashboard, clicks Search, opens the actual GSTR-1 card, then performs offline generation/download from the initialized page context.
- GSTR-3B: selects FY/Quarter/Month, clicks Search, opens the actual GSTR-3B card, then reads summary/tax-payable from the initialized return page context.
- Existing Credential Vault, custom protocol, login/CAPTCHA, manual fallback, ERP upload and Storage Agent flow are unchanged.
Uninstall:
ARRR_GST_Operator_Agent.exe --uninstall
@@ -0,0 +1,84 @@
param(
[string]$PythonCommand = "",
[switch]$KeepBuildEnvironment
)
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
$Source = Join-Path $Root "gst_operator_agent.py"
$Requirements = Join-Path $Root "requirements.txt"
$BuildRoot = Join-Path $env:TEMP "ARRR_GST_AGENT_160_BUILD"
$BuildVenv = Join-Path $BuildRoot "venv"
$Work = Join-Path $BuildRoot "work"
$Spec = Join-Path $BuildRoot "spec"
$Dist = Join-Path $Root "dist"
$ExeName = "ARRR_GST_Operator_Agent_Setup_1.6.0.exe"
$Exe = Join-Path $Dist $ExeName
if (-not (Test-Path $Source)) { throw "Missing GST agent source: $Source" }
if (-not (Test-Path $Requirements)) { throw "Missing GST agent requirements: $Requirements" }
if ($PythonCommand) {
$Python = $PythonCommand
$PythonPrefix = @()
} elseif (Get-Command py -ErrorAction SilentlyContinue) {
$Python = "py"
$PythonPrefix = @("-3")
} elseif (Get-Command python -ErrorAction SilentlyContinue) {
$Python = "python"
$PythonPrefix = @()
} else {
throw "Python 3 was not found. Install Python 3.11+ on the development workstation and run this builder again."
}
Write-Host "ARRR GST Operator Agent EXE Builder 1.6.0" -ForegroundColor Cyan
Write-Host "Source : $Source"
Write-Host "Output : $Exe"
if (Test-Path $BuildRoot) { Remove-Item $BuildRoot -Recurse -Force }
New-Item -ItemType Directory -Path $BuildRoot -Force | Out-Null
New-Item -ItemType Directory -Path $Dist -Force | Out-Null
& $Python @PythonPrefix -m venv $BuildVenv
if ($LASTEXITCODE -ne 0) { throw "Could not create the isolated GST EXE build environment." }
$BuildPython = Join-Path $BuildVenv "Scripts\python.exe"
& $BuildPython -m pip install --disable-pip-version-check -r $Requirements "pyinstaller==6.16.0"
if ($LASTEXITCODE -ne 0) { throw "Could not install GST EXE build dependencies." }
& $BuildPython -m PyInstaller `
--noconfirm `
--clean `
--onefile `
--noconsole `
--name "ARRR_GST_Operator_Agent_Setup_1.6.0" `
--distpath $Dist `
--workpath $Work `
--specpath $Spec `
--collect-all playwright `
--hidden-import winreg `
$Source
if ($LASTEXITCODE -ne 0 -or -not (Test-Path $Exe)) {
throw "PyInstaller did not create $ExeName."
}
Write-Host "Running packaged self-test..." -ForegroundColor Yellow
& $Exe --self-test
if ($LASTEXITCODE -ne 0) { throw "Packaged GST Operator Agent self-test failed." }
$Hash = (Get-FileHash -Algorithm SHA256 -Path $Exe).Hash
[System.IO.File]::WriteAllText(
(Join-Path $Dist "ARRR_GST_Operator_Agent_Setup_1.6.0.sha256"),
"$Hash $ExeName`r`n",
[System.Text.UTF8Encoding]::new($false)
)
if (-not $KeepBuildEnvironment) {
Remove-Item $BuildRoot -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Host ""
Write-Host "GST Operator Agent EXE created successfully." -ForegroundColor Green
Write-Host "EXE : $Exe" -ForegroundColor Green
Write-Host "SHA256 : $Hash" -ForegroundColor Green
@@ -0,0 +1 @@
AC2EA1EF8F5D066181E4CD500E579D3796A1AAC2BDC28110404CDBEB86B54AA1 ARRR_GST_Operator_Agent_Setup_1.6.0.exe
@@ -2,8 +2,10 @@ from __future__ import annotations
import io
import json
import os
import re
import shutil
import subprocess
import sys
import time
import traceback
@@ -14,10 +16,16 @@ from urllib.parse import parse_qs, urljoin, urlsplit
import requests
VERSION = "1.5.1"
ROOT = Path(__file__).resolve().parent
DATA = ROOT / "data"
CONFIG_PATH = ROOT / "config.json"
VERSION = "1.6.0"
APP_NAME = "ARRR GST Operator Agent"
PROTOCOL_NAME = "arrrgst"
DEFAULT_ERP_BASE_URL = "https://office.arrrassociates.com"
SOURCE_ROOT = Path(__file__).resolve().parent
INSTALL_ROOT = Path(os.environ.get("LOCALAPPDATA") or (Path.home() / "AppData" / "Local")) / "ARRR" / "GSTOperatorAgent"
RUN_ROOT = INSTALL_ROOT if getattr(sys, "frozen", False) else SOURCE_ROOT
DATA = INSTALL_ROOT / "data" if getattr(sys, "frozen", False) else SOURCE_ROOT / "data"
CONFIG_PATH = INSTALL_ROOT / "config.json" if getattr(sys, "frozen", False) else SOURCE_ROOT / "config.json"
INSTALLED_EXE = INSTALL_ROOT / "ARRR_GST_Operator_Agent.exe"
GST_LOGIN_URL = "https://services.gst.gov.in/services/login"
SERVICES_DASHBOARD_URL = "https://services.gst.gov.in/services/auth/dashboard"
RETURN_DASHBOARD_URL = "https://return.gst.gov.in/returns/auth/dashboard"
@@ -41,7 +49,7 @@ def read_config() -> dict:
try:
return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
except Exception:
return {"erp_base_url": "https://office.arrrassociates.com", "protocol": "arrrgst"}
return {"erp_base_url": DEFAULT_ERP_BASE_URL, "protocol": PROTOCOL_NAME}
def safe(value: str, fallback: str = "item") -> str:
@@ -1205,6 +1213,142 @@ def handle_protocol_url(raw_url: str) -> None:
run_job_foreground(token)
def _message_box(title: str, message: str, error: bool = False) -> None:
try:
import ctypes
flags = 0x10 if error else 0x40
ctypes.windll.user32.MessageBoxW(None, str(message), str(title), flags)
except Exception:
print(f"{title}: {message}")
def _write_config(erp_base_url: str) -> None:
url = str(erp_base_url or DEFAULT_ERP_BASE_URL).strip().rstrip("/")
if not url.startswith("https://"):
raise ValueError("ERP base URL must use HTTPS.")
INSTALL_ROOT.mkdir(parents=True, exist_ok=True)
payload = {"erp_base_url": url, "protocol": PROTOCOL_NAME, "version": VERSION}
CONFIG_PATH.write_text(json.dumps(payload, indent=2), encoding="utf-8")
def _register_protocol(exe_path: Path) -> None:
if os.name != "nt":
raise RuntimeError("Windows is required to register the ARRR GST protocol.")
import winreg
base = rf"Software\Classes\{PROTOCOL_NAME}"
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, base) as key:
winreg.SetValueEx(key, "", 0, winreg.REG_SZ, f"URL:{APP_NAME}")
winreg.SetValueEx(key, "URL Protocol", 0, winreg.REG_SZ, "")
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, base + r"\DefaultIcon") as key:
winreg.SetValueEx(key, "", 0, winreg.REG_SZ, f'"{exe_path}",0')
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, base + r"\shell\open\command") as key:
winreg.SetValueEx(key, "", 0, winreg.REG_SZ, f'"{exe_path}" "%1"')
def _register_uninstall(exe_path: Path) -> None:
if os.name != "nt":
return
import winreg
base = r"Software\Microsoft\Windows\CurrentVersion\Uninstall\ARRRGSTOperatorAgent"
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, base) as key:
values = {
"DisplayName": APP_NAME,
"DisplayVersion": VERSION,
"Publisher": "ARRR & Associates",
"InstallLocation": str(INSTALL_ROOT),
"DisplayIcon": str(exe_path),
"UninstallString": f'"{exe_path}" --uninstall',
"QuietUninstallString": f'"{exe_path}" --uninstall --quiet',
}
for name, value in values.items():
winreg.SetValueEx(key, name, 0, winreg.REG_SZ, value)
winreg.SetValueEx(key, "NoModify", 0, winreg.REG_DWORD, 1)
winreg.SetValueEx(key, "NoRepair", 0, winreg.REG_DWORD, 0)
def _remove_registry_tree(root, subkey: str) -> None:
import winreg
try:
with winreg.OpenKey(root, subkey, 0, winreg.KEY_READ | winreg.KEY_WRITE) as key:
while True:
try:
child = winreg.EnumKey(key, 0)
except OSError:
break
_remove_registry_tree(root, subkey + "\\" + child)
winreg.DeleteKey(root, subkey)
except FileNotFoundError:
pass
def install_self(erp_base_url: str = DEFAULT_ERP_BASE_URL, quiet: bool = False) -> int:
if os.name != "nt":
raise RuntimeError("ARRR GST Operator Agent installer supports Windows only.")
if not getattr(sys, "frozen", False):
raise RuntimeError("Run the compiled ARRR GST Operator Agent Setup EXE for installation.")
INSTALL_ROOT.mkdir(parents=True, exist_ok=True)
source_exe = Path(sys.executable).resolve()
target_exe = INSTALLED_EXE.resolve()
if source_exe != target_exe:
shutil.copy2(source_exe, target_exe)
_write_config(erp_base_url)
_register_protocol(target_exe)
_register_uninstall(target_exe)
# Clean legacy Python/venv installer artifacts without touching job data.
for legacy_name in (".venv", "gst_operator_agent.py", "requirements.txt", "README.txt", "install_gst_operator_agent.ps1", "uninstall_gst_operator_agent.ps1"):
legacy = INSTALL_ROOT / legacy_name
try:
if legacy.is_dir():
shutil.rmtree(legacy, ignore_errors=True)
elif legacy.exists():
legacy.unlink()
except Exception:
pass
if not quiet:
_message_box(APP_NAME, f"{APP_NAME} {VERSION} installed successfully.\n\nProtocol: arrrgst://\nERP: {str(erp_base_url).rstrip('/')}\n\nReturn to ARRR ERP and start the GST download again.")
return 0
def uninstall_self(quiet: bool = False) -> int:
if os.name != "nt":
return 0
import winreg
try:
_remove_registry_tree(winreg.HKEY_CURRENT_USER, rf"Software\Classes\{PROTOCOL_NAME}")
_remove_registry_tree(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Uninstall\ARRRGSTOperatorAgent")
except Exception:
pass
exe = Path(sys.executable).resolve()
cleanup = INSTALL_ROOT / "remove_agent.cmd"
cleanup.write_text(
"@echo off\r\n"
"ping 127.0.0.1 -n 3 >nul\r\n"
f'del /f /q "{exe}" >nul 2>&1\r\n'
f'rmdir /s /q "{INSTALL_ROOT}" >nul 2>&1\r\n'
'del /f /q "%~f0" >nul 2>&1\r\n',
encoding="utf-8",
)
subprocess.Popen(["cmd.exe", "/c", str(cleanup)], creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
if not quiet:
_message_box(APP_NAME, f"{APP_NAME} has been unregistered and will now be removed.")
return 0
def installed_status() -> tuple[bool, str]:
if os.name != "nt":
return False, "Windows required"
try:
import winreg
key_path = rf"Software\Classes\{PROTOCOL_NAME}\shell\open\command"
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path) as key:
command = str(winreg.QueryValueEx(key, "")[0] or "")
ok = INSTALLED_EXE.exists() and str(INSTALLED_EXE).lower() in command.lower()
return ok, command
except Exception as exc:
return False, str(exc)
def self_test() -> int:
cfg = read_config()
erp = str(cfg.get("erp_base_url") or "")
@@ -1224,18 +1368,43 @@ def self_test() -> int:
def main():
DATA.mkdir(parents=True, exist_ok=True)
if len(sys.argv) >= 2 and sys.argv[1] == "--self-test":
args = list(sys.argv[1:])
if "--self-test" in args:
raise SystemExit(self_test())
if len(sys.argv) >= 2 and str(sys.argv[1]).lower().startswith("arrrgst:"):
if "--uninstall" in args:
raise SystemExit(uninstall_self(quiet="--quiet" in args))
if "--install" in args:
erp = DEFAULT_ERP_BASE_URL
if "--erp-base-url" in args:
idx = args.index("--erp-base-url")
if idx + 1 >= len(args):
raise SystemExit("--erp-base-url requires a value")
erp = args[idx + 1]
raise SystemExit(install_self(erp, quiet="--quiet" in args))
if args and str(args[0]).lower().startswith("arrrgst:"):
try:
handle_protocol_url(sys.argv[1])
handle_protocol_url(args[0])
except Exception as exc:
_log(traceback.format_exc())
_show_error(str(exc))
raise SystemExit(1)
return
if getattr(sys, "frozen", False):
# A downloaded setup EXE installs/repairs itself when double-clicked.
if Path(sys.executable).resolve() != INSTALLED_EXE.resolve():
raise SystemExit(install_self(DEFAULT_ERP_BASE_URL, quiet=False))
ok, command = installed_status()
if not ok:
raise SystemExit(install_self(str(read_config().get("erp_base_url") or DEFAULT_ERP_BASE_URL), quiet=False))
_message_box(
APP_NAME,
f"{APP_NAME} {VERSION} is installed and ready.\n\n"
f"ERP: {read_config().get('erp_base_url')}\n"
"Protocol: arrrgst://",
)
return
print(f"ARRR GST Operator Agent {VERSION}")
print("This agent is launched by the arrrgst:// Windows protocol from ARRR ERP.")
print("Source mode. Build the Windows setup EXE using build_gst_operator_agent_exe.ps1.")
if __name__ == "__main__":
@@ -9,7 +9,7 @@ $Startup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup
$ThumbprintFile = Join-Path $InstallRoot "cert_thumbprint.txt"
$ProtocolKey = "HKCU:\Software\Classes\arrrgst"
Write-Host "ARRR GST Operator Agent 1.5.1 - clean installation" -ForegroundColor Cyan
Write-Host "ARRR GST Operator Agent 1.6.0 - source fallback installation" -ForegroundColor Cyan
Write-Host "Install root: $InstallRoot"
# Clean legacy v1.0.x listener/startup/certificate/protocol state first.
@@ -1,9 +1,17 @@
$ErrorActionPreference = "Stop"
$InstallRoot = Join-Path $env:LOCALAPPDATA "ARRR\GSTOperatorAgent"
$InstalledExe = Join-Path $InstallRoot "ARRR_GST_Operator_Agent.exe"
$Startup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup\ARRR GST Operator Agent.cmd"
$ThumbprintFile = Join-Path $InstallRoot "cert_thumbprint.txt"
$ProtocolKey = "HKCU:\Software\Classes\arrrgst"
$UninstallKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\ARRRGSTOperatorAgent"
if (Test-Path $InstalledExe) {
& $InstalledExe --uninstall
exit $LASTEXITCODE
}
# Legacy source-agent cleanup.
try {
Get-NetTCPConnection -LocalPort 8791 -State Listen -ErrorAction SilentlyContinue | ForEach-Object {
if ($_.OwningProcess) { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }
@@ -15,5 +23,6 @@ if (Test-Path $ThumbprintFile) {
}
Remove-Item $Startup -Force -ErrorAction SilentlyContinue
Remove-Item $ProtocolKey -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item $UninstallKey -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item $InstallRoot -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "ARRR GST Operator Agent removed, including legacy localhost certificate/startup state." -ForegroundColor Green
Write-Host "ARRR GST Operator Agent removed, including legacy protocol/certificate/startup state." -ForegroundColor Green