Skip to main content

LucidLink Admin Assistant

Desktop app

A desktop app for LucidLink administrators. Manage users, groups, and permissions from a simple interface, and run Python scripts to automate your own workflows.

Scripts let you add your own automation to the Admin Assistant. Each script is a self-contained folder that appears automatically in the Scripts section of the Tools page, and can also be run on its own from the terminal.

Script folder structure

my-script/├── plugin.json       ← required: manifest (name, description, version, inputs)├── plugin.py         ← required: the script (filename can be overridden via "script")└── README.md         ← optional: shown in the app's Script Info panel

Drop the folder into your scripts directory (Settings → Scripts Directory, default ~/LucidLink/plugins) and click Reload Scripts.

Scripts included with the app are copied into your scripts directory on first launch, with the same folder names, plugin.json, and README.md intact.

plugin.json - the manifest

{  "name": "My Tool",  "description": "A short description shown on the tool card.",  "version": "1.0",  "author": "Your Name",  "inputs": [    {      "id": "inputId",      "label": "Display Label",      "type": "text",      "required": true,      "default": "some default",      "placeholder": "hint text"    }  ]}

Input types

type

Description

UI element

text

Plain text

Text input

password

Secret value

Masked input

folder

Directory path

Text input + Browse button (native folder picker)

file

File path

Text input + Browse button (native file picker)

select

Fixed choices

Dropdown

filespace

Auto-injected, not shown in form

None

text

Description

Plain text

UI element

Text input

password

Description

Secret value

UI element

Masked input

folder

Description

Directory path

UI element

Text input + Browse button (native folder picker)

file

Description

File path

UI element

Text input + Browse button (native file picker)

select

Description

Fixed choices

UI element

Dropdown

filespace

Description

Auto-injected, not shown in form

UI element

None

For select inputs, include an options array:

{  "id": "format",  "label": "Output Format",  "type": "select",  "options": [    { "value": "csv", "label": "CSV" },    { "value": "json", "label": "JSON" }  ]}

The filespace type signals that your script uses the currently connected filespace. The app injects --filespace-id automatically, so you do not need to include this input in your form, just reference it in your script.

plugin.py - the script

How the app calls your script

When someone runs your script from the app, the bearer token and every manifest input are written to your script's stdin as a single JSON object. argv only carries non-sensitive operational context:

# What the app actually runs:python plugin.py \  --base-url http://localhost:3003/api/v1 \  --filespace-id <filespace-id>
{  "schemaVersion": 1,  "token": "<bearer-token>",  "baseUrl": "http://localhost:3003/api/v1",  "filespaceId": "<filespace-id>",  "<inputId1>": "<value1>",  "<inputId2>": "<value2>"}

The token and every manifest input are delivered over stdin only, never on argv. argv is visible to any same-uid process via ps or /proc/<pid>/cmdline for the entire run, which may last hours, so secrets and user-supplied input values must not appear there.

Manifest input IDs are camelCase JSON keys (matching plugin.json → inputs[].id). When you also expose them as argparse flags for standalone use, follow the camelCase → kebab-case convention:

  • outputDir → --output-dir
  • fromDate → --from-date
  • adminPassword → --admin-password

Standalone (interactive) mode

When run directly without arguments from a TTY, your script falls back to argparse plus interactive prompts. The recommended pattern below handles both paths.

python plugin.py          # interactive prompts for token, filespace, inputspython plugin.py --help   # shows usagepython plugin.py --token X --base-url Y --filespace-id Z   # argv override

Minimum boilerplate

#!/usr/bin/env python3import argparseimport getpassimport jsonimport sys

def _read_runner_input():    """Read the JSON payload the Admin Assistant writes to stdin.    Returns {} for standalone CLI use (TTY) so argparse on argv still works."""    try:        if sys.stdin.isatty():            return {}    except (ValueError, OSError):        return {}    try:        raw = sys.stdin.read()    except (OSError, ValueError):        return {}    if not raw:        return {}    try:        payload = json.loads(raw)    except json.JSONDecodeError:        return {}    return payload if isinstance(payload, dict) else {}

def main():    runner = _read_runner_input()    parser = argparse.ArgumentParser(description="My script.")    parser.add_argument("--base-url", default=runner.get("baseUrl") or "http://localhost:3003/api/v1")    parser.add_argument("--token", default=runner.get("token"))    parser.add_argument("--filespace-id", default=runner.get("filespaceId"))    # Add your manifest inputs. Camel-case → kebab-case; default reads from    # the runner payload first, then falls back to a hardcoded default.    parser.add_argument("--output-dir", default=runner.get("outputDir") or "~/Desktop")    args = parser.parse_args()
    # Fall back to interactive prompts if values are still missing (standalone use).    token = args.token    if not token:        token = getpass.getpass("Enter API Bearer token: ").strip()        if not token:            print("Error: token is required.", file=sys.stderr)            sys.exit(1)
    # Your logic here ...    print("[1/3] Starting...")    print("[2/3] Processing...")    print("[3/3] Done!")

if __name__ == "__main__":    main()

Alternative: stdin-first scripts (no argparse for inputs)

If your script only runs from the app and you do not need standalone CLI ergonomics, you can read the JSON payload directly and skip argparse for the input fields. The rekey scripts follow this pattern:

raw = sys.stdin.read().strip()try:    args = json.loads(raw) if raw else {}except json.JSONDecodeError:    args = {}
target_filespace = args.get("targetFilespace", "").strip()access_key       = args.get("accessKey", "").strip()# ...

Progress output

The app streams your script's stdout line by line into a live log window. Print progress as you go:

print(f"[{i}/{total}] Processing {item}")print("DONE: 42 items exported → ~/Desktop/output.csv")

Lines printed to stderr appear in the log with a red indicator, which is useful for warnings and errors that should not stop the script.

Exit codes:

  • 0 → success (green "Completed successfully" banner)
  • Non-zero → failure (red "Exited with code N" banner)

Receiving credentials from the app

The app passes --base-url and --filespace-id on argv. It does not pass `--token` on argv. The bearer token arrives over stdin as part of the runner JSON payload, so a same-uid process cannot read it out of ps or /proc/<pid>/cmdline while a long migration runs.

Read the token from the runner payload, exactly as the main() skeleton above does, and keep --token as an argv fallback for standalone use:

parser.add_argument("--base-url")   # None when run standalone — prompt or use defaultparser.add_argument("--token", default=runner.get("token"))  # stdin first; None standalone — prompt with getpassparser.add_argument("--filespace-id")  # None when run standalone — list and prompt

Detect whether you are in app mode or standalone:

app_mode = bool(args.base_url and args.token and args.filespace_id)
if not app_mode:    # Interactive prompts    ...else:    # Use injected values directly    ...

Third-party dependencies

If your script needs packages beyond the Python standard library (for example requests or pydantic):

  1. Add a `requirements.txt` to your script folder

    requests>=2.31
  2. Document the install step

    Users must run pip install -r requirements.txt themselves.

  3. Prefer the standard library where you can

    Using urllib avoids the dependency entirely.

The app ships no Python environment. It uses whatever python3 or python is resolved from a fixed list of trusted install prefixes.

Example: minimal script

`hello-world/plugin.json`

{  "name": "Hello World",  "description": "Prints filespace info to stdout.",  "version": "1.0",  "inputs": [    {      "id": "greeting",      "label": "Greeting",      "type": "text",      "required": false,      "default": "Hello",      "placeholder": "Hello"    }  ]}

`hello-world/plugin.py`

#!/usr/bin/env python3import argparseimport jsonimport urllib.request
def api_get(base_url, token, path):    req = urllib.request.Request(        f"{base_url}{path}",        headers={"accept": "application/json", "Authorization": f"Bearer {token}"},    )    with urllib.request.urlopen(req) as resp:        return json.loads(resp.read())
def main():    parser = argparse.ArgumentParser()    parser.add_argument("--base-url", default="http://localhost:3003/api/v1")    parser.add_argument("--token")    parser.add_argument("--filespace-id")    parser.add_argument("--greeting", default="Hello")    args = parser.parse_args()
    import getpass    token = args.token or getpass.getpass("Token: ")    base_url = args.base_url or "http://localhost:3003/api/v1"
    print(f"{args.greeting} from my script!")    if args.filespace_id:        resp = api_get(base_url, token, "/filespaces")        filespaces = resp.get("data", [])        for fs in filespaces:            if fs.get("id") == args.filespace_id:                print(f"Connected filespace: {fs['name']}")    print("DONE")
if __name__ == "__main__":    main()

Integrity: what the app checks at launch

Every published artifact carries a SHA-256 sidecar (<artifact>.sha256, a single lowercase hex line). The versions manifest stamps that hash onto each asset, and the download UI refuses to render a download button for an asset with no hash.

Scripts should also ship a manifest signature (plugin.json.sig, ECDSA P-256). The manifest then lists a files array binding each file in the script folder to its SHA-256:

{  "name": "Example Script",  "version": "1.2",  "inputs": [ /* … */ ],  "script": "plugin.py",  "files": [    { "path": "plugin.py",        "sha256": "<hex>" },    { "path": "requirements.txt", "sha256": "<hex>" }  ]}

Each time a script starts, the Admin Assistant verifies:

  • Manifest signature

    ECDSA over the plugin.json bytes, against the public key embedded in the app.

  • Per-file hashes

    For every entry in files[], the on-disk SHA-256 must match the declared hex.

If you are packaging a script for distribution, generate the files array and the signature as part of your release step so both checks can pass.

Checklist before distributing a script

plugin.json has name, description, version, and inputs

plugin.py uses argparse and handles missing args gracefully

Script exits 0 on success, non-zero on failure

Progress lines are printed to stdout

Errors are printed to stderr (or as regular progress lines prefixed with ERROR:)

Tested standalone: python plugin.py (interactive)

Tested with args: python plugin.py --base-url ... --token ... --filespace-id ...

files[] and plugin.json.sig generated, if you publish the script

(Optional) README.md in the script folder for in-app help