CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/nanoclaw-trusted

Rules for trusted NanoClaw groups. Shared memory, session bootstrap, cross-group memory updates. Loaded for trusted and main containers only.

76

Quality

96%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

High

Do not use without reviewing

Overview
Quality
Evals
Security
Files

google-tasks.pyskills/google-ops/scripts/

#!/usr/bin/env python3
"""Headless Google Tasks READS over the native Tasks REST API (nanoclaw#638).

Read-only Tasks ops for trusted-tier ground-truth verification
(`jbaruch/nanoclaw-admin#456`). This copy carries ONLY the read contract —
the task-mutation ops (`patch` / `insert` / `delete`) are admin-tier-only
and are NOT mounted in the trusted tile, so a trusted agent cannot write
to the owner's tasks through this script.

    list-tasklists -> GET /users/@me/lists
    list           -> GET /lists/{tasklist_id}/tasks
    get            -> GET /lists/{tasklist_id}/tasks/{task_id}

`tasklist_id` / `task_id` keep their snake_case stdin names. They were
snake_case because the Composio slug demanded it; they stay snake_case
because that is this script's own contract and every caller already
speaks it. Natively they are path segments, not body fields — this script
lifts them out of stdin and onto the URL.

Output shape — read the resource, not `data`
--------------------------------------------
Composio wrapped every response in `{"data": ..., "successful": bool,
"error": ...}`. That envelope was a Composio invention and dies with it:
this script prints the raw Tasks resource. `list-tasklists` and `list`
put their array in top-level `items`; `get` returns the task resource
itself.

The `successful: false` branch dies too. Composio reported an API-level
failure (task not found, insufficient scope) as HTTP 200 with
`successful: false` in the body, so callers had to test a field to notice.
Google reports those as real HTTP status codes, so they arrive here as an
HTTPError and exit non-zero. A zero exit now means the call succeeded.

Credential model: none in this container. OneCLI's gateway injects the
Bearer on the wire and refreshes it (see `google-rest.py`). The
`COMPOSIO_API_KEY` / `COMPOSIO_USER_ID` preflight is gone with the
credential — the gateway's absence surfaces as `GatewayNotInjecting`
(401) rather than as a missing env var.

Black box per `coding-policy: script-as-black-box`: the consuming skill
names the arguments contract; this script maps the op to its endpoint and
validates per-op required keys before the call. The agent supplies the
computed arguments (task ids, due dates, titles) as a JSON object on
stdin — that computation is reasoning, not a fixed transform.

Usage
-----
    echo '{}' | google-tasks.py list-tasklists
    echo '{"tasklist_id": "..."}' | google-tasks.py list
    echo '{"tasklist_id": "...", "task_id": "..."}' | google-tasks.py get

Output
------
On success: the raw Tasks resource as single-line JSON on stdout, exit 0.

On failure (HTTP 4xx/5xx, gateway not injecting, tier-restricted,
network, timeout, bad JSON), malformed stdin, unknown op, or stdin
missing an op's required keys: a diagnostic on stderr and a non-zero
exit, no stdout.
"""

from __future__ import annotations

import importlib.util
import json
import pathlib
import sys
import urllib.error
import urllib.parse

_SCRIPTS = pathlib.Path(__file__).resolve().parent
GOOGLE_REST_PATH = _SCRIPTS / "google-rest.py"

# Op -> (HTTP method, required stdin keys). Required keys give a malformed
# call an actionable message here rather than an opaque Google 4xx. Reads
# only — the trusted tile never mounts the mutation ops (#456).
OPS = {
    "list-tasklists": ("GET", ()),
    "list": ("GET", ("tasklist_id",)),
    "get": ("GET", ("tasklist_id", "task_id")),
}


def _load_google_rest():
    spec = importlib.util.spec_from_file_location("google_rest", GOOGLE_REST_PATH)
    if spec is None or spec.loader is None:
        raise FileNotFoundError(f"cannot load google-rest from {GOOGLE_REST_PATH}")
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


def _read_stdin_args() -> dict:
    raw = sys.stdin.read().strip()
    if not raw:
        return {}
    args = json.loads(raw)
    if not isinstance(args, dict):
        raise ValueError("stdin must be a JSON object of Tasks arguments")
    return args


def _quote(value):
    """Quote a stdin-supplied id for use as a URL path segment. Task list
    ids are opaque Google strings that can carry URL-significant
    characters; `safe=''` keeps them from being read as path structure."""
    return urllib.parse.quote(str(value), safe="")


def _endpoint(op, args):
    """Build the op's path, consuming the id keys from `args` so what
    remains is the body/query payload."""
    if op == "list-tasklists":
        return "users/@me/lists"
    tasklist = _quote(args.pop("tasklist_id"))
    if op == "list":
        return f"lists/{tasklist}/tasks"
    return f"lists/{tasklist}/tasks/{_quote(args.pop('task_id'))}"


def main() -> int:
    if len(sys.argv) != 2 or sys.argv[1] not in OPS:
        sys.stderr.write(f"google-tasks: usage: google-tasks.py <{'|'.join(OPS)}> (args on stdin)\n")
        return 2
    op = sys.argv[1]
    method, required = OPS[op]

    try:
        args = _read_stdin_args()
    except (json.JSONDecodeError, ValueError) as e:
        sys.stderr.write(f"google-tasks: invalid stdin ({e}).\n")
        return 2

    missing = [k for k in required if not args.get(k)]
    if missing:
        sys.stderr.write(
            f"google-tasks: {op} requires {', '.join(required)}; "
            f"missing/empty: {', '.join(missing)}.\n"
        )
        return 2

    try:
        google_rest = _load_google_rest()
    except (FileNotFoundError, PermissionError, ImportError, OSError) as e:
        sys.stderr.write(
            f"google-tasks: Google REST helper unavailable ({e}) — expected at {GOOGLE_REST_PATH}.\n"
        )
        return 2

    path = _endpoint(op, args)
    payload = {"params": args or None}

    try:
        resource = google_rest.google_request(
            method, google_rest.surface_url("tasks", path), **payload
        )
    except google_rest.GatewayNotInjecting as e:
        sys.stderr.write(f"google-tasks: {op} unauthenticated — {e}\n")
        return 1
    except google_rest.TierAccessRestricted as e:
        sys.stderr.write(f"google-tasks: {op} unavailable at this tier — {e}\n")
        return 1
    except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as e:
        sys.stderr.write(f"google-tasks: {op} call failed ({type(e).__name__}: {e}).\n")
        return 1

    print(json.dumps(resource))
    return 0


if __name__ == "__main__":
    sys.exit(main())

README.md

tile.json