SecurityHybridFreeActiveMachine-verified· advanced · ~30 min setup

STACK: allow GitHub issue reads and block issue creation

Prepare a narrowly scoped STACK GitHub Passport that permits one issue-list read while denying issue creation, with the submitted runner contract verified offline and live provider enforcement kept as a separate credentialed check.

FlowStacks verification by Shilpa Mitra·Submitted by @randomUsername-01· verified today· v1.0.0

Run this workflow

CI-verified, 6/6 fixtures passing.

Build this with your agent

One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.

Intended Use

A disposable GitHub repository and a deliberately narrow STACK connection where an operator wants to demonstrate that the selected standard-mode agent can list issues through STACK but cannot create one. First run the keyless FlowStacks gate against the immutable example; then, after reviewing current STACK documentation and the exact grant and Passport constraints, run the live check with short-lived credentials and inspect both evidence surfaces.

Not for

  • Treating the green offline gate as proof that STACK or GitHub enforced the policy live; FlowStacks does not hold STACK credentials or contact either authenticated API
  • Running against a production or valuable repository: the live example deliberately attempts issue creation and tells the operator to stop and inspect the repository if the write is not blocked
  • Sending an operator token or Passport to any API origin other than exactly https://api.getstack.run; loopback development is only for an intentional STACK_ALLOW_LOCAL_DEVELOPMENT=1 test setup
  • Granting broad GitHub write access and assuming the Passport alone removes the underlying connection's risk; constrain and review both the current grant and the Passport
  • Treating locally decoded JWT claims as authentication: the runner uses them only for fail-closed preflight checks and relies on STACK to verify the Passport
  • Claiming STACK is an operating-system sandbox or that an agent cannot bypass it through separate network paths, credentials, tools, or direct GitHub access
  • Relying on stale endpoint, pricing, retention, or entitlement details; confirm the current STACK documentation and account terms before a live run

The Stack

Tested Against

submitted Gist@57f804242239a5151152b67786783714385d6160run.py@sha256:a9a65004badb74342c1775228c0b305eaa15016d9fe8a600b72de6a702bb93betest_run.py@sha256:27608cc61aea1a5c215e8fee0a03925ad8dff95bb9537c77e8d0bade95e177c9Python@3.12STACK public API documentation@reviewed 2026-10-08

Side effects & data flow

Network
gist.githubusercontent.com only while CI downloads the four immutable public source files, api.getstack.run during the separate live run; STACK then proxies the permitted GitHub read and is expected to reject the write before GitHub
Writes
./source/, ./upstream-tests.txt, and ./missing-credentials.txt inside the disposable CI directory, STACK audit and security-event records during a live run; if the policy is misconfigured, one clearly labeled issue may be created in the disposable GitHub repository
Credentials
None for FlowStacks CI, STACK_API_TOKEN, STACK_PASSPORT_TOKEN, STACK_AGENT_ID, and STACK_GITHUB_REPO for the separate live check; the connected GitHub credential remains managed by STACK

Data privacy

  • GitHub Gist delivery ← CI requests the four public files at one immutable revision; no user content or credential is sent (retention: per GitHub's published policies)
  • STACK ← During a live run, the repository identifier, issue-list request and response, denied issue title, agent and Passport identifiers, and operational evidence pass through or are recorded by STACK (retention: per the current STACK plan and privacy policy)
  • GitHub ← The permitted issue-list read; the issue-creation request should not reach GitHub, but may do so if the live control is misconfigured (retention: per the repository owner and GitHub account policies)

Prerequisites

  • Python 3, curl, and Node.js for the keyless FlowStacks verification gate
  • For the separate live check: a current STACK account and plan that supports the required agent, connection, Passport, proxy, audit, and security-event operations
  • A short-lived STACK operator token, scoped Passport token, standard-mode agent ID, and a STACK-managed GitHub connection
  • A disposable owner/repository target whose issue list may be read and where a failed policy could safely create one clearly labeled test issue
  • Human review of the current STACK grant, Passport claims, hosted API documentation, audit output, and the disposable GitHub repository after the run

Steps

  1. 1

    Pin and verify the submitted runner without credentials

    Download the four files from immutable Gist revision 57f8042, enforce exact sizes and SHA-256 digests, verify the MIT notice and SPDX headers, audit the Python imports and origin guard, run all 10 upstream fake-transport tests in an empty environment, and independently probe origin lookalikes, loopback opt-in, redirect refusal, and pre-network failures. This step never contacts STACK or GitHub's authenticated API.

    set -eu
    mkdir source
    curl --proto '=https' --tlsv1.2 --retry 3 --max-filesize 65536 -fsSL https://gist.githubusercontent.com/randomUsername-01/01e2d14c92085d196342e7c275c6051c/raw/57f804242239a5151152b67786783714385d6160/LICENSE -o source/LICENSE
    curl --proto '=https' --tlsv1.2 --retry 3 --max-filesize 65536 -fsSL https://gist.githubusercontent.com/randomUsername-01/01e2d14c92085d196342e7c275c6051c/raw/57f804242239a5151152b67786783714385d6160/README.md -o source/README.md
    curl --proto '=https' --tlsv1.2 --retry 3 --max-filesize 65536 -fsSL https://gist.githubusercontent.com/randomUsername-01/01e2d14c92085d196342e7c275c6051c/raw/57f804242239a5151152b67786783714385d6160/run.py -o source/run.py
    curl --proto '=https' --tlsv1.2 --retry 3 --max-filesize 65536 -fsSL https://gist.githubusercontent.com/randomUsername-01/01e2d14c92085d196342e7c275c6051c/raw/57f804242239a5151152b67786783714385d6160/test_run.py -o source/test_run.py
    node <<'NODE'
    const crypto = require("crypto");
    const fs = require("fs");
    
    function bad(message) { console.error("BAD: " + message); process.exit(1); }
    const expected = {
      "LICENSE": [1070, "fd18c7e0d5d04654b6c05d0de6f7f2226e4284c1ab3655a0602e1181eab74e43"],
      "README.md": [7179, "3a5454d248de5a644532ef79b3033b3a69a911ac8f6d341be0ffeae94488f4f7"],
      "run.py": [12374, "a9a65004badb74342c1775228c0b305eaa15016d9fe8a600b72de6a702bb93be"],
      "test_run.py": [7114, "27608cc61aea1a5c215e8fee0a03925ad8dff95bb9537c77e8d0bade95e177c9"],
    };
    const names = fs.readdirSync("source").sort();
    if (JSON.stringify(names) !== JSON.stringify(Object.keys(expected).sort())) bad("source file set changed");
    for (const [name, [size, digest]] of Object.entries(expected)) {
      const data = fs.readFileSync("source/" + name);
      if (data.length !== size) bad(name + " size changed");
      if (crypto.createHash("sha256").update(data).digest("hex") !== digest) bad(name + " digest changed");
    }
    const license = fs.readFileSync("source/LICENSE", "utf8");
    const runner = fs.readFileSync("source/run.py", "utf8");
    const tests = fs.readFileSync("source/test_run.py", "utf8");
    if (!license.includes("MIT License") || !license.includes("Copyright (c) 2026 Tomi Lupsakko")) bad("MIT notice changed");
    if (!runner.includes("SPDX-License-Identifier: MIT") || !tests.includes("SPDX-License-Identifier: MIT")) bad("SPDX headers missing");
    if (!runner.includes('PRODUCTION_ORIGIN = "https://api.getstack.run"')) bad("production origin guard changed");
    if (!runner.includes('os.environ.get("STACK_ALLOW_LOCAL_DEVELOPMENT") == "1"')) bad("local development opt-in changed");
    console.log("source pinned OK: Gist 57f8042, four MIT files, exact sizes and SHA-256 digests");
    NODE
    PYTHON=$(command -v python3)
    env -i PYTHONDONTWRITEBYTECODE=1 "$PYTHON" -B <<'PY'
    import ast
    from pathlib import Path
    
    allowed = {
        "run.py": {"__future__", "base64", "json", "os", "re", "secrets", "sys", "dataclasses", "typing", "urllib.error", "urllib.parse", "urllib.request"},
        "test_run.py": {"base64", "io", "json", "unittest", "contextlib", "run"},
    }
    for name, expected in allowed.items():
        tree = ast.parse(Path("source", name).read_text(), filename=name)
        found = set()
        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                found.update(alias.name for alias in node.names)
            elif isinstance(node, ast.ImportFrom):
                found.add(node.module or "")
        if found != expected:
            raise SystemExit(f"unexpected imports in {name}: {sorted(found - expected)}; missing: {sorted(expected - found)}")
    print("runner boundary OK: audited Python standard library only; exact production origin; loopback requires explicit opt-in")
    PY
    (cd source && env -i PYTHONDONTWRITEBYTECODE=1 "$PYTHON" -B -m unittest discover -p 'test_*.py' -v) > upstream-tests.txt 2>&1
    cat upstream-tests.txt
    grep -q "Ran 10 tests" upstream-tests.txt
    grep -q '^OK$' upstream-tests.txt
    echo "offline runner tests OK: 10/10"
    env -i PYTHONDONTWRITEBYTECODE=1 "$PYTHON" -B <<'PY'
    import contextlib
    import io
    import os
    import sys
    from unittest.mock import patch
    
    sys.path.insert(0, "source")
    import run as demo
    
    accepted = ["https://api.getstack.run", "https://api.getstack.run/"]
    for url in accepted:
        if demo.StackClient(url, "operator", "passport").base_url != demo.PRODUCTION_ORIGIN:
            raise SystemExit("production origin was not normalized")
    
    rejected = [
        "https://evil.example",
        "https://api.getstack.run.evil.example",
        "https://api.getstack.run:8443",
        "https://api.getstack.run/v1",
        "https://api.getstack.run.",
        "https://api.getstack.run//v1",
        "http://2130706433:3001",
        "https://localhost.evil.example",
    ]
    for url in rejected:
        try:
            demo.StackClient(url, "operator", "passport", allow_local=True)
        except demo.DemoError:
            pass
        else:
            raise SystemExit("untrusted origin accepted: " + url)
    
    for url in ["http://localhost:3001", "http://127.0.0.1:3001", "http://[::1]:3001"]:
        try:
            demo.StackClient(url, "operator", "passport")
        except demo.DemoError:
            pass
        else:
            raise SystemExit("loopback accepted without opt-in: " + url)
        demo.StackClient(url, "operator", "passport", allow_local=True)
    
    if demo.NoRedirects().redirect_request(None, None, 302, "redirect", {}, "https://evil.example") is not None:
        raise SystemExit("redirect handler no longer refuses redirects")
    
    evil_env = {
        "STACK_API_TOKEN": "operator",
        "STACK_PASSPORT_TOKEN": "header.e30.signature",
        "STACK_AGENT_ID": "agt_example",
        "STACK_GITHUB_REPO": "owner/repo",
        "STACK_API_BASE_URL": "https://evil.example",
    }
    stderr = io.StringIO()
    with patch.dict(os.environ, evil_env, clear=True), contextlib.redirect_stderr(stderr):
        status = demo.main()
    if status != 1 or "must be exactly https://api.getstack.run" not in stderr.getvalue():
        raise SystemExit("main did not reject the untrusted origin before a request")
    print("origin regression OK: production exact; lookalikes rejected; loopback opt-in narrow; redirects refused")
    PY
    set +e
    (cd source && env -i PYTHONDONTWRITEBYTECODE=1 "$PYTHON" -B run.py) > missing-credentials.txt 2>&1
    STATUS=$?
    set -e
    if [ "$STATUS" -ne 2 ]; then
      cat missing-credentials.txt
      echo "BAD: missing credentials did not exit 2" >&2
      exit 1
    fi
    grep -q "Missing environment variables: STACK_API_TOKEN, STACK_PASSPORT_TOKEN, STACK_AGENT_ID, STACK_GITHUB_REPO" missing-credentials.txt
    echo "missing-credentials OK: runner exits 2 before network when required variables are absent"
  2. 2

    Create the narrow live grant and Passport in a disposable environment

    Using the current STACK interface and documentation, connect only a disposable GitHub repository, select a standard-mode agent, and constrain both the current GitHub grant and its Passport to method GET plus the exact /repos/OWNER/REPOSITORY/issues path. Keep authority bindings and missions out of this repeatable example, choose short-lived credentials, and inspect the decoded claims without treating local decoding as signature verification.

  3. 3

    Run the live denial check, inspect both evidence surfaces, and revoke

    Set the four required variables and leave STACK_API_BASE_URL unset so the runner uses exactly https://api.getstack.run. Execute run.py only against the disposable repository. Require the issue-list read to return GitHub 200 through STACK, the creation attempt to return STACK 403 for the method constraint, a matching credential.proxy success in the audit log, and a matching credential_outside_scope security event. Then inspect GitHub for an unexpected issue, revoke the Passport and test credentials, and retain only non-sensitive evidence. Do not use STACK_ALLOW_LOCAL_DEVELOPMENT in production.

Eval, 6 fixtures

Last passed: verified today
  • source-pinnedcontainstimeout 120s · max $0

    Expected: source pinned OK: Gist 57f8042, four MIT files, exact sizes and SHA-256 digests

  • runner-boundarycontainstimeout 120s · max $0

    Expected: runner boundary OK: audited Python standard library only; exact production origin; loopback requires explicit opt-in

  • upstream-testscontainstimeout 120s · max $0

    Expected: offline runner tests OK: 10/10

  • origin-regressioncontainstimeout 120s · max $0

    Expected: origin regression OK: production exact; lookalikes rejected; loopback opt-in narrow; redirects refused

  • missing-credentialscontainstimeout 120s · max $0

    Expected: missing-credentials OK: runner exits 2 before network when required variables are absent

  • clean-exitexit_codetimeout 120s · max $0

    Expected: 0

Results

FlowStacks pins all four files from Gist revision 57f8042, verifies their exact sizes and SHA-256 digests, checks the MIT notice and SPDX headers, rejects imports outside the audited Python standard-library boundary, runs the submitter's 10 offline tests in an empty environment, independently exercises production-origin lookalikes and the explicit loopback-only development opt-in, and proves missing credentials and an untrusted API origin fail before any request. This verifies the MIT runner by Tomi Lupsakko, not STACK's hosted policy enforcement or a live GitHub denial.

Did this work for you?

Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.

Related workflows

Liked this workflow?

Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).