Skill: taleintervenor/sjtu-slurm-skill (version 0.2.3)

--- name: sjtu-slurm-skill description: Log in to the SJTU HPC platform (also known as "交我算") as the user to perform job queries, submissions, cancellations, and data management. Use this skill when the user requests operations related to HPC or "交我算". metadata: { "openclaw": { "emoji": "🦞", "homepage": https://github.com/SJTU-HPC/SJTU-SLURM-Skill, } } --- # sjtu-slurm-skill ## Overview Use this skill to log in to the SJTU HPC (交我算) platform as the user, acting on their behalf to perform personal job queries, submissions, cancellations, and data management operations. General Principles: - **Risk-aware operations**: Deleting user's data and interrupting running jobs are risky operations. Before performing any risky operation, always confirm with the user that they clearly understand the impact of the operation and agree to its execution. - private credentials: All user's credential files (like SSH key, certificate, API token, etc...) should be stored in the `credentials` directory under workspace (e.g., `~/.openclaw/workspace/default/credentials`). **Do Not store user's plain text password or contain it in your talk messages**. ## Quick Start 1. Ensure HPC API token file is available in the workspace, which should be stored in `credentials` directory. If not, request a new token. 2. For each user request, analyze whether you need to log in to an HPC entry node to perform the operation remotely. You can ask more questions to clarify any ambiguous parts of the request. - If user wants to know its storage quota usage or update its account (like password, binding Email/jAccount, preferred contact method), then you can meet the requirement directly by calling HPC API with the token. - if user is talking about job or its data, then you have to select an entry node to do remote operation. In this case, take the following steps. - if user want to get a passwordless certificate for SSH login, refer to [certificates section](#ssh-keys-and-certificates). 3. Ensure SSH keys and certificates are available in the workspace, which should be stored in `credentials` directory. If not, request a new SSH certificate for the user. Remind the user that requesting a certificate will trigger two-factor authentication. 4. For each user request need remote operation, identify the node group/partition the user is interested in and the operation type to select the correct entry node. 5. Use the SSH certificate to connect to the corresponding entry node based on the cluster and operation type, execute the user's requested operations on it. ## Ensure Token Available Before calling any HPC API, ensure there is a valid token in the credentials directory. Go through the following steps: 1. Check whether `hpc_token` file exists in the `credentials` directory under workspace. If it exists, goto step 2, otherwise goto step 3. 2. Try to refresh the token with `refresh_token.py --workspace "path_to_workspace"`. If success, then you have ensured a valid token and can safely skip the following steps. If the request is refused (which means the token has expired), then continue to step 3. If the request reports internal error, act according to the rules in [Error Handling section](#error-handling) . 3. Tell the user you are going to request a new token and ask for their HPC username and password. To avoid password leak, advise user to upload a text file instead of providing passwords directly in the chat window. 4. Call script to request a new token with `req_token.py` : - If user uploaded file to provide password, call script with the format like: `req_token.py "username" "password" --workspace "path_to_workspace" --textfile "uploaded_file_path"`. So script can help to remove the file to prevent password leak. - Otherwise skip the `--textfile` option. The token will be saved as `hpc_token` in the `credentials` directory under workspace. If any script failed, act according to the rules in [Error Handling section](#error-handling) . ## HPC API Here are some HPC API path you can call when users want to know their storage quota usage or update their account: - `GET /user`: query user's information. - Response will includ all the attributes defined by posixAccount and some addditional fields like jAccount, email, user type, etc. - CURL example: `curl -L 'https://api.hpc.sjtu.edu.cn/user?name=userA&domain=pi' --header 'Authorization: Bearer ***'` - `PATCH /user`: update user's information. - Only password, Email/jAccount, preferred contact method can be updated by user itself. - Update request will trigger two-factor authentication via JWB APP (交我办) or Email. Ask user whether it have been prepared to do the authentication before you send the request. - Consult the [online documentation](https://api.hpc.sjtu.edu.cn/doc/index.html#/) to understand the calling conventions of this API. - `GET /quota`: Query the storage quota data of user or its related group account. - Call this API does not need authorization. - Always call it with explict `name` parameter. - If user's requirement didn't specify which `quota_type` them need, you should query with `quota_type=account` and `quota_type=user` seperately. The account name can be derived from user's home path, which can be query in `GET /user`. The parent dir of user‘s home is exactly its account name, for example: `"home": "%H/home/acct-hpc/hpcrobot"` -> `account name: acct-hpc`. - CURL example: `curl -L 'https://api.hpc.sjtu.edu.cn/quota?quota_type=account&name=acct-example'` Only these tasks can be done through HPC API. **Do NOT try to use API for any other user requirements.** Use the token in `credentials` directory if authorization is required. Ask for user's HPC name if it is needed and has not been provided during the talk. ## Entry Node Selection Rules Select the appropriate entry node based on the node group and operation type the user is interested in. HPC cluster has 3 node groups: pi, sy (思源) , kp (鲲鹏) . Partitions belong to node groups. If user does not explicitly specify a node group but only provides a target partition or a target storage, look up the corresponding node group from this table: | node group | partition | storage | | ---------- | -------------------------------------------------------------- | ---------- | | pi | cpu, debug, dgx2, huge, 192c6t | lustre | | kp | debugarm, arm128c256g, scnet_arm | | | sy | small, 64c512g, el9, debug64c512g, win32, a100, a800, debug100 | dssg(gpfs) | Then select the entry node according to this table: | business | node group | entry node | | -------- | ---------- | ------------------------ | | job | pi | pilogin.hpc.sjtu.edu.cn | | job | sy | sylogin.hpc.sjtu.edu.cn | | job | kp | armlogin.hpc.sjtu.edu.cn | | data | pi, kp | data.hpc.sjtu.edu.cn | | data | sy | sydata.hpc.sjtu.edu.cn | ## SSH Keys and Certificates SSH login to entry nodes requires the user's passwordless certificate. The agent should store all credentials (tokens, SSH keys, certificates) in the workspace's `credentials` directory. If no certificate available, follow the steps to request a new SSH certificate: 1. Ask for certificate owner: If you have asked username when requesting token, directly use that username and skip the ask. Otherwise ask for user's HPC username. 2. Make sure user is prepared: Tell the user that requesting the certificate will trigger two-factor authentication via JWB APP (交我办) or Email. Ask user whether it have been prepared to do the authentication. 3. Call synchronous script with long timeout: Give the user a prompt like "Requesting certificate now, please check your JWB App (交我办) or Email...", **Do NOT wait for the user's confirmation before proceeding**. Immediately call the script `req_certificate.py <username> --workspace "path_to_workspace"` with `timeout >= 600s` because the script will block waiting for the user to complete two-factor authentication on another channel. 4. **Handle errors**: After script execution, check the exit code. If non-zero, act according to the rules in [Error Handling section](#error-handling) . The SSH key and certificate will be saved to the `credentials` directory under workspace. If user directly asked for the key or certificate, follow the steps to send files to user: 1. Check whether the valid time of the existing certificate meets the user's requirement. If not, request a new certificate with `--valid-time` argument. 2. Check if there is any channel tools or skills can be used to send files directly to user. List all the candidate methods and ask user which one is preferred. 3. Use the selected method to send private key and certificate file to user. Remind user of the security sensitivity of these files and advise user never use them in public environments. ## Error Handling If any script execution or API call failed, parse the error message from stderr and inform the user with clear details: - If the error message indicates "user have not set email or jAccount", **Direct the user** to read the platform documentation: https://docs.hpc.sjtu.edu.cn/accounts/security.html . Ask them to follow the instructions in the documentation to bind their second identity channel (jAccount or email). - If the error message indicates internal error, tell user the session ID in error message, suggest them to ask help from `[REDACTED_SECRET]` with this ID. ## Executing User-Requested Operations on Entry Nodes Use the SSH key and certificate from the workspace credentials directory to log in to the entry node and remotely execute the user's requested operations: ```bash ssh -i "/path_to_workspace/credentials/private_key" -o "CertificateFile=/path_to_workspace/credentials/certificate" user@entry_node "command" ``` Note that user data is distributed across multiple shared storage pools, and visible storage varies depending on the node group. The storage where the user's home directory resides also differs: | node group | mount point | storage information | | ---------- | ----------- | ---------------------------------------------------------------------------------------------------------------- | | pi, kp | /lustre | Hot storage, Lustre, user home directory is here | | sy | /dssg | Hot storage, GPFS, user home directory is here | | pi, sy, kp | /archive | Cold storage A, NFS, for archived data, writable only on data and sydata nodes, read-only on other nodes | | pi, sy, kp | /vault | Cold storage B, NFS, for archived data, writable only on data and sydata nodes, read-only on other nodes | | pi, sy, kp | /union | mergerfs virtual filesystem, combines /archive and /vault, same read/write restrictions as the two cold storages | User personal directory paths are consistent across storage pools. Replace the top-level path of the user's home directory with the target storage to get the corresponding personal directory path. For example: `/lustre/home/acct-hpc/hpcrobot` -> `/archive/home/acct-hpc/hpcrobot` If the user is interested in platform status or personal jobs, use SLURM commands to perform operations like: `sinfo, squeue, sacct, seff, sbatch, scancel, etc...` Due to different storage pool visibility across node groups, job data computed on a partition can only be viewed on entry nodes belonging to the corresponding node group. Each entry node only allows submitting job to partitions within its own node group. Never use cold storage data directly for job. If cold storage data is needed, transfer it to hot storage first before using it for job. Large-scale data transfer operations should be performed on `data.hpc.sjtu.edu.cn` and `sydata.hpc.sjtu.edu.cn`. To aviod connection lost, wrap the remote transfer operation on data nodes with tmux. HPC has multiple cold storages, so before writing data to cold storage, check the free space of these candidates, and select the cold storage with most free space as the write target. If user asks about their data in cold storage and did not clearly specified which one, use the combined virtual filesystem `/union` . ## Resources ### scripts/ - `req_token.py`: Request a new bearer token from HPC API. This is a one-time setup that enables other operations. - `refresh_token.py`: Refresh an existing bearer token to extend the session without re-authenticating. - `req_certificate.py`: Request SSH key and certificate files using an existing token, which can be used to log in to the entry nodes without a password.

path: README.md
content: # SJTU slurm skill

This is the skill to interact with SJTU HPC platform on behalf of a user.

## Skill ability

- Manage your HPC account information like jAccount/Email binding, password update, prefer contact method setting, etc.
- Query job status, partition queue, platform information, storage usage, etc.
- Submit/manage job according to your instructions.
- Help you to migrate data between hot/cold storages.

In a sense, it can help to do all the things you can through SSH login to platform's entry node.

## Notes on using this skill

This skill will request API token and SSH key/certificate for you, and store them in workspace. **Make sure your claw is in safe environment. Never use this skill on a shared machine.**

To fullfill your instructions, claw agent may need to call lots of command, channel clients (like Feishu, weixin, QQ, etc.) cannot display the background command call process in real time. You may have to wait for a while to see the final reply, be patient if your LLM models service is not fast enough.

## Install skill

This skill has been published on [clawhub](https://clawhub.ai/taleintervenor/sjtu-slurm-skill) , so you can simplily ask your claw to install sjtu-slurm-skill skill itself.

Or if you prefer to maintain skills manually:

```
openclaw skills install sjtu-slurm-skill
openclaw skills update sjtu-slurm-skill
```

## Repository

This project is hosted on GitHub at https://github.com/SJTU-HPC/SJTU-SLURM-Skill. If you have any suggestions for improvement, feel free to open an issue or submit a pull request.

path: scripts/refresh_token.py
content: #!/usr/bin/env python3
"""
Refresh bearer token from SJTU HPC API.

This script refreshes an existing bearer token using the PATCH /token endpoint.
A refreshed token can be used to extend the session without re-authenticating
with username and password.

Usage:
    python refresh_token.py [--workspace WORKSPACE]

Options:
    --workspace WORKSPACE  Absolute path to the agent's workspace (default: ~/.openclaw/workspace). Credentials will be read from WORKSPACE/credentials.
"""

import argparse
import json
import os
import sys
import urllib.request
import urllib.error

API_BASE_URL = "https://api.hpc.sjtu.edu.cn"
TOKEN_FILENAME = "hpc_token"


class APIError(Exception):
    """Base exception for API errors."""
    def __init__(self, message: str, http_code: int):
        super().__init__(message)
        self.http_code = http_code


def parse_error_response(e: urllib.error.HTTPError) -> tuple[str, bool]:
    """
    Parse error response from API.
    Returns (error_message, is_internal_error).
    - HTTP 500: internal error, body contains session ID and help info
    - Other codes: rejection, body contains reason
    """
    body = e.read().decode("utf-8")
    http_code = e.code
    return body, http_code == 500


def refresh_token(current_token: str) -> str:
    """
    Refresh bearer token using PATCH /token endpoint.
    
    The PATCH request sends the current token in the Authorization header
    and receives a new token in response.
    """
    url = f"{API_BASE_URL}/token"
    
    # PATCH request with current token to refresh it
    req = urllib.request.Request(
        url,
        data=json.dumps({}).encode("utf-8"),  # Empty body for refresh
        headers={
            "Content-Type": "application/json",
            "Accept": "text/plain",
            "Authorization": f"Bearer {current_token}"
        },
        method="PATCH"
    )

    try:
        with urllib.request.urlopen(req) as response:
            new_token = response.read().decode("utf-8")
            # Token format is "Bearer ..."
            if new_token.startswith("Bearer "):
                new_token = new_token[7:]
            return new_token
    except urllib.error.HTTPError as e:
        error_body, is_internal = parse_error_response(e)
        if is_internal:
            raise APIError(
                f"刷新Token失败: {error_body}",
                e.code
            )
        else:
            raise APIError(
                f"刷新Token被拒绝: {error_body}",
                e.code
            )


def save_token(token_dir: str, token: str) -> str:
    """Save token to a file in the specified directory."""
    os.makedirs(token_dir, exist_ok=True)
    token_path = os.path.join(token_dir, TOKEN_FILENAME)
    with open(token_path, "w") as f:
        f.write(token)
    os.chmod(token_path, 0o600)
    return token_path


def load_token(token_dir: str) -> str | None:
    """Load token from the token directory."""
    token_path = os.path.join(token_dir, TOKEN_FILENAME)
    if os.path.exists(token_path):
        with open(token_path, "r") as f:
            return f.read().strip()
    return None


def main():
    parser = argparse.ArgumentParser(
        description="Refresh bearer token from SJTU HPC API"
    )
    # Default workspace path
    default_workspace = os.path.expanduser("~/.openclaw/workspace")

    parser.add_argument(
        "--workspace",
        default=default_workspace,
        help=f"Absolute path to the agent's workspace (default: {default_workspace}). Credentials will be read from WORKSPACE/credentials."
    )

    args = parser.parse_args()

    # Credentials are always stored in WORKSPACE/credentials
    credentials_dir = os.path.join(args.workspace, "credentials")

    # Load existing token
    current_token = load_token(credentials_dir)
    if not current_token:
        print(
            f"Error: No token found in {credentials_dir}. "
            "Please run req_token.py first to obtain a token.",
            file=sys.stderr
        )
        sys.exit(1)

    try:
        # Refresh token
        new_token = refresh_token(current_token)

        # Save new token
        token_path = save_token(credentials_dir, new_token)

        print(f"Token refreshed successfully and saved to {token_path}")
        print("Note: The old token is now invalid.")

    except APIError as e:
        print(f"{e}", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"未知错误: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()

path: scripts/req_certificate.py
content: #!/usr/bin/env python3
"""
Request SSH key and certificate from SJTU HPC API.

This script performs the following steps using an existing bearer token:
1. Generate SSH key pair using the token
2. Sign the public key to get SSH certificate

Usage:
    python req_certificate.py <hpc_user> [--workspace WORKSPACE] [--valid-time VALID_TIME]

Arguments:
    hpc_user: HPC username (the user who will use the SSH key)

Options:
    --workspace WORKSPACE  Absolute path to the agent's workspace (default: ~/.openclaw/workspace). Credentials will be stored in WORKSPACE/credentials.
    --valid-time VALID_TIME  Certificate validity time in seconds (default: 3600)
"""

import argparse
import json
import os
import sys
import urllib.request
import urllib.error

API_BASE_URL = "https://api.hpc.sjtu.edu.cn"
TOKEN_FILENAME = "hpc_token"


class APIError(Exception):
    """Base exception for API errors."""
    def __init__(self, message: str, http_code: int):
        super().__init__(message)
        self.http_code = http_code


def parse_error_response(e: urllib.error.HTTPError) -> tuple[str, bool]:
    """
    Parse error response from API.
    Returns (error_message, is_internal_error).
    - HTTP 500: internal error, body contains session ID and help info
    - Other codes: rejection, body contains reason
    """
    body = e.read().decode("utf-8")
    http_code = e.code
    return body, http_code == 500


def load_token(token_dir: str) -> str:
    """Load token from the token directory."""
    token_path = os.path.join(token_dir, TOKEN_FILENAME)
    if not os.path.exists(token_path):
        raise APIError(
            f"Token文件不存在: {token_path}。请先运行 req_token.py 获取token。",
            http_code=404
        )
    with open(token_path, "r") as f:
        [REDACTED_SECRET]).strip()
    if not token:
        raise APIError(
            f"Token文件为空: {token_path}。请先运行 req_token.py 获取token。",
            http_code=400
        )
    return token


def generate_key_pair(token: str, hpc_user: str) -> dict:
    """Generate SSH key pair using HPC API."""
    url = f"{API_BASE_URL}/gen_key"
    payload = {
        "domain": "pi",
        "hpc_user": hpc_user
    }

    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(
        url,
        data=data,
        headers={
            "Content-Type": "application/json",
            "Accept": "application/json",
            "Authorization": f"Bearer {token}"
        },
        method="POST"
    )

    try:
        with urllib.request.urlopen(req) as response:
            result = json.loads(response.read().decode("utf-8"))
            return result
    except urllib.error.HTTPError as e:
        error_body, is_internal = parse_error_response(e)
        if is_internal:
            raise APIError(
                f"生成密钥对失败: {error_body}",
                e.code
            )
        else:
            raise APIError(
                f"生成密钥对被拒绝: {error_body}",
                e.code
            )


def sign_cert(token: str, hpc_user: str, public_key: str, valid_time: int = 3600) -> str:
    """Sign SSH public key to get certificate."""
    url = f"{API_BASE_URL}/sign_cert"
    payload = {
        "domain": "pi",
        "hpc_user": hpc_user,
        "principals": [hpc_user],
        "public_key": public_key,
        "valid_time": valid_time
    }

    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(
        url,
        data=data,
        headers={
            "Content-Type": "application/json",
            "Accept": "text/plain",
            "Authorization": f"Bearer {token}"
        },
        method="POST"
    )

    try:
        with urllib.request.urlopen(req) as response:
            cert = response.read().decode("utf-8")
            return cert
    except urllib.error.HTTPError as e:
        error_body, is_internal = parse_error_response(e)
        if is_internal:
            raise APIError(
                f"签名证书失败: {error_body}",
                e.code
            )
        else:
            raise APIError(
                f"签名证书被拒绝: {error_body}",
                e.code
            )


def save_files(output_path: str, private_key: str, public_key: str, certificate: str):
    """Save SSH key and certificate to files."""
    os.makedirs(output_path, exist_ok=True)

    # API always returns Ed25519 keys
    key_name = "id_ed25519"

    private_key_path = os.path.join(output_path, key_name)
    public_key_path = os.path.join(output_path, f"{key_name}.pub")
    cert_path = os.path.join(output_path, f"{key_name}-cert.pub")

    # Save private key
    with open(private_key_path, "w") as f:
        f.write(private_key)
    os.chmod(private_key_path, 0o600)

    # Save public key
    with open(public_key_path, "w") as f:
        f.write(public_key)

    # Save certificate
    with open(cert_path, "w") as f:
        f.write(certificate)

    print(f"SSH key and certificate saved to {output_path}:")
    print(f"  - Private key: {private_key_path}")
    print(f"  - Public key: {public_key_path}")
    print(f"  - Certificate: {cert_path}")


def main():
    parser = argparse.ArgumentParser(
        description="Request SSH key and certificate from SJTU HPC API"
    )
    parser.add_argument("hpc_user", help="HPC username (the user who will use the SSH key)")
    # Default workspace path
    default_workspace = os.path.expanduser("~/.openclaw/workspace")

    parser.add_argument(
        "--workspace",
        default=default_workspace,
        help=f"Absolute path to the agent's workspace (default: {default_workspace}). Credentials will be stored in WORKSPACE/credentials."
    )
    parser.add_argument(
        "--valid-time",
        type=int,
        default=3600,
        help="Certificate validity time in seconds (default: 3600)"
    )

    args = parser.parse_args()

    # Credentials are always stored in WORKSPACE/credentials
    credentials_dir = os.path.join(args.workspace, "credentials")

    # Use credentials_dir as output_path for SSH key/cert (same directory)
    output_path = credentials_dir

    try:
        # Step 1: Load token
        [REDACTED_SECRET])

        # Step 2: Generate key pair
        key_pair = generate_key_pair(token, args.hpc_user)
        private_key = key_pair["private_key"]
        public_key = key_pair["public_key"]

        # Step 3: Sign certificate
        certificate = sign_cert(
            token,
            args.hpc_user,
            public_key,
            args.valid_time
        )

        # Step 4: Save files
        save_files(output_path, private_key, public_key, certificate)

        print(f"Done! SSH key and certificate have been stored into {output_path}")

    except APIError as e:
        print(f"{e}", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"未知错误: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()

path: scripts/req_token.py
content: #!/usr/bin/env python3
"""
Request bearer token from SJTU HPC API.

This script authenticates with the HPC API and obtains a bearer token
which can be used for subsequent operations like key generation and
certificate signing.

Usage:
    python req_token.py <username> <password> [--workspace WORKSPACE] [--textfile TEXTFILE]

Arguments:
    username: HPC account username
    password: HPC account password

Options:
    --workspace WORKSPACE  Absolute path to the agent's workspace (default: ~/.openclaw/workspace). Credentials will be stored in WORKSPACE/credentials.
    --textfile TEXTFILE    Path to a file containing the password. If provided, the file will be deleted after the token is successfully saved to prevent password leak.
"""

import argparse
import json
import os
import sys
import urllib.request
import urllib.error

API_BASE_URL = "https://api.hpc.sjtu.edu.cn"
TOKEN_FILENAME = "hpc_token"


def delete_file_if_exists(file_path: str) -> None:
    """Delete the specified file if it exists."""
    if file_path and os.path.exists(file_path):
        try:
            os.remove(file_path)
            print(f"Deleted password file: {file_path}")
        except Exception as e:
            print(f"Warning: Failed to delete password file {file_path}: {e}", file=sys.stderr)


class APIError(Exception):
    """Base exception for API errors."""
    def __init__(self, message: str, http_code: int):
        super().__init__(message)
        self.http_code = http_code


def parse_error_response(e: urllib.error.HTTPError) -> tuple[str, bool]:
    """
    Parse error response from API.
    Returns (error_message, is_internal_error).
    - HTTP 500: internal error, body contains session ID and help info
    - Other codes: rejection, body contains reason
    """
    body = e.read().decode("utf-8")
    http_code = e.code
    return body, http_code == 500


def request_token(operator: str, password: str) -> str:
    """Request bearer token from HPC API."""
    url = f"{API_BASE_URL}/token"
    payload = {
        "domain": "pi",
        "operator": operator,
        "password": password
    }

    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(
        url,
        data=data,
        headers={
            "Content-Type": "application/json",
            "Accept": "text/plain"
        },
        method="POST"
    )

    try:
        with urllib.request.urlopen(req) as response:
            [REDACTED_SECRET]).decode("utf-8")
            # Token format is "Bearer ..."
            if token.startswith("Bearer "):
                [REDACTED_SECRET]
            return token
    except urllib.error.HTTPError as e:
        error_body, is_internal = parse_error_response(e)
        if is_internal:
            raise APIError(
                f"获取Token失败: {error_body}",
                e.code
            )
        else:
            raise APIError(
                f"获取Token被拒绝: {error_body}",
                e.code
            )


def save_token(token_dir: str, token: str) -> str:
    """Save token to a file in the specified directory."""
    os.makedirs(token_dir, exist_ok=True)
    token_path = os.path.join(token_dir, TOKEN_FILENAME)
    with open(token_path, "w") as f:
        f.write(token)
    os.chmod(token_path, 0o600)
    return token_path


def main():
    parser = argparse.ArgumentParser(
        description="Request bearer token from SJTU HPC API"
    )
    parser.add_argument("username", help="HPC account username")
    parser.add_argument("password", help="HPC account password")
    # Default workspace path
    default_workspace = os.path.expanduser("~/.openclaw/workspace")

    parser.add_argument(
        "--workspace",
        default=default_workspace,
        help=f"Absolute path to the agent's workspace (default: {default_workspace}). Credentials will be stored in WORKSPACE/credentials."
    )

    parser.add_argument(
        "--textfile",
        default=None,
        help="Path to a file containing the password. If provided, the file will be deleted after the token is successfully saved to prevent password leak."
    )

    args = parser.parse_args()

    # Credentials are always stored in WORKSPACE/credentials
    credentials_dir = os.path.join(args.workspace, "credentials")

    try:
        # Request token
        [REDACTED_SECRET], args.password)

        # Save token
        token_path = save_token(credentials_dir, token)

        # Delete the password file if --textfile was specified
        if args.textfile:
            delete_file_if_exists(args.textfile)

        print(f"Token obtained and saved to {token_path}")
        print("Note: Keep this token secure. It can be used for subsequent API operations.")

    except APIError as e:
        print(f"{e}", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"未知错误: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)