Skip to content

docs(kartograph): add ArgoCD PKCE login skill + Agent Sandbox CRD debugging notes#796

Open
aredenba-rh wants to merge 1 commit into
mainfrom
docs/agent-sandbox-crd-skill-notes
Open

docs(kartograph): add ArgoCD PKCE login skill + Agent Sandbox CRD debugging notes#796
aredenba-rh wants to merge 1 commit into
mainfrom
docs/agent-sandbox-crd-skill-notes

Conversation

@aredenba-rh

Copy link
Copy Markdown
Collaborator

Summary

  • Adds the kartograph Cursor skill (.cursor/skills/kartograph/SKILL.md + argocd_pkce_login.py), previously untracked, covering the ArgoCD SSO login workaround and common REST API endpoints/gotchas for debugging stage.
  • Documents the Agent Sandbox CRD (agents.x-k8s.io) as an unowned cluster-side dependency: how to check its live presence via ArgoCD's cluster API-discovery cache, why the openshell-rbac.yaml Role syncing "Healthy" is not evidence the CRD exists, and that no GitOps repo we have access to (hp-fleet-gitops, hybrid-platforms-gitops/infrastructure, ambient-code-gitops) installs or owns it — so it's a platform/SRE ask, not something to chase in this repo.

No application code changes; docs/tooling only.

Test plan

  • N/A (docs-only change, no code paths affected)

Made with Cursor

…ugging notes

Adds the kartograph Cursor skill (ArgoCD SSO workaround via manual PKCE
login against Dex, plus common REST API endpoints/gotchas) and a new
section documenting the Agent Sandbox CRD (agents.x-k8s.io) as an
unowned cluster-side dependency: how to check its live presence via
ArgoCD's cluster API-discovery cache, why RBAC syncing "Healthy" is not
evidence the CRD exists, and that no GitOps repo we have access to
(hp-fleet-gitops, hybrid-platforms-gitops/infrastructure,
ambient-code-gitops) installs or owns it.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Summary by CodeRabbit

  • Documentation

    • Added operational guidance for Kartograph development, GitOps updates, ArgoCD troubleshooting, cluster checks, and secret or pipeline access.
    • Documented limitations and workarounds for common command-line and cluster-access issues.
  • New Features

    • Added a PKCE-based ArgoCD login utility that supports browser authentication and securely captures login tokens.
    • Added guidance for diagnosing missing or unsupported Agent Sandbox resources and related deployment symptoms.

Walkthrough

Adds a Kartograph Cursor skill covering repository, GitOps, ArgoCD, Agent Sandbox, Vault, and Konflux operations. Adds an executable Python utility that performs Dex OAuth2/PKCE authentication, receives the local callback, validates CSRF state, exchanges the authorization code, and writes the resulting ArgoCD ID token.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant argocd_pkce_login.py
  participant Browser
  participant Dex
  participant ArgoCD API
  Operator->>argocd_pkce_login.py: Run login script
  argocd_pkce_login.py->>Browser: Display Dex authorization URL
  Browser->>Dex: Complete authorization
  Dex->>argocd_pkce_login.py: Send callback code and state
  argocd_pkce_login.py->>Dex: Exchange code with PKCE verifier
  Dex-->>argocd_pkce_login.py: Return id_token
  argocd_pkce_login.py->>ArgoCD API: Use token for operational requests
Loading
🚥 Pre-merge checks | ✅ 9 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
No Weak Cryptography ⚠️ Warning CWE-208: the OAuth state token is compared with != instead of constant-time compare; no banned primitives otherwise. Use secrets.compare_digest/equivalent for the state check, keeping the PKCE SHA-256 challenge as-is.
✅ Passed checks (9 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately names the added kartograph skill, PKCE login workaround, and Agent Sandbox CRD debugging notes.
Description check ✅ Passed The description matches the docs/tooling-only change and correctly describes the ArgoCD workaround and CRD ownership notes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Sec-02: Secrets In Log Output ✅ Passed No CWE-532 issue found: prints do not emit token/password/credential/secret values; only auth URL, callback status, and output path are logged.
No Hardcoded Secrets ✅ Passed PASS: no hardcoded secrets/CWE-798 found; the new files only contain hostnames, client_id, and doc references, with no secret/password/token literals or long base64 strings.
No Injection Vectors ✅ Passed No CWE-89/CWE-78/CWE-79/CWE-502 injection patterns appear in the changed files; this PR is docs plus a Python PKCE helper only.
No Privileged Containers ✅ Passed PASS: only a Markdown skill and Python helper changed; no K8s/OpenShift manifests, Helm templates, or Dockerfiles contain privileged/root settings (CWE-276/CWE-284).
No Pii Or Sensitive Data In Logs ✅ Passed No CWE-532 logging exposure found: prints only the auth URL, callback status, token file path, and a doc example; no PII, raw bodies, or secrets are logged.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/agent-sandbox-crd-skill-notes
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch docs/agent-sandbox-crd-skill-notes

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
.cursor/skills/kartograph/SKILL.md (1)

22-27: 🔒 Security & Privacy | 🔵 Trivial

Documents bypassing PR review via direct push to unprotected main.

Encouraging direct pushes to hp-fleet-gitops main as a "faster path" formalizes a review-bypass workflow. This reflects that repo's existing (lack of) branch protection rather than something introduced here, but it's worth flagging as a standing security-posture gap for the GitOps repo (no required review before manifests reach the auto-syncing ArgoCD Application).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.cursor/skills/kartograph/SKILL.md around lines 22 - 27, Revise the
hp-fleet-gitops guidance to remove the recommendation that urgent fixes may be
pushed directly to the unprotected main branch. Keep the automated
update-deploy-tag workflow and ArgoCD synchronization details, and direct users
to the PR-based process rather than documenting a review bypass.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.cursor/skills/kartograph/scripts/argocd_pkce_login.py:
- Around line 47-73: Update wait_for_callback so the 180-second timeout is an
actual overall deadline rather than a per-request timeout. Track a monotonic
deadline before the request loop, recompute the remaining time before each
server.handle_request() call, and exit or raise the established timeout error
when no callback arrives by the deadline; preserve immediate completion when
code or error is received.
- Around line 118-121: Update the token-writing flow around id_token and
args.out to create the output file with owner-only permissions (0600) at
creation time, rather than relying on the process umask. Preserve writing the
token and reporting its length, and ensure existing files are not left with
broader permissions.
- Around line 103-116: Add a finite timeout to the urllib.request.urlopen call
in the Dex token exchange so stalled Dex responses cannot block the login flow
indefinitely. Keep the existing request construction and JSON token parsing
unchanged.

---

Nitpick comments:
In @.cursor/skills/kartograph/SKILL.md:
- Around line 22-27: Revise the hp-fleet-gitops guidance to remove the
recommendation that urgent fixes may be pushed directly to the unprotected main
branch. Keep the automated update-deploy-tag workflow and ArgoCD synchronization
details, and direct users to the PR-based process rather than documenting a
review bypass.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: f48cdb6d-74ff-473d-938f-4c35b1be43bc

📥 Commits

Reviewing files that changed from the base of the PR and between e91a4b4 and 871854a.

📒 Files selected for processing (2)
  • .cursor/skills/kartograph/SKILL.md
  • .cursor/skills/kartograph/scripts/argocd_pkce_login.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • openshift-hyperfleet/architecture (manual)
  • openshift-hyperfleet/hyperfleet-api (manual)
  • openshift-hyperfleet/hyperfleet-sentinel (manual)
  • openshift-hyperfleet/hyperfleet-adapter (manual)
  • openshift-hyperfleet/hyperfleet-broker (manual)

Comment on lines +47 to +73
def wait_for_callback() -> dict[str, str | None]:
result: dict[str, str | None] = {}

class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self) -> None:
parsed = urllib.parse.urlparse(self.path)
if parsed.path != CALLBACK_PATH:
self.send_response(404)
self.end_headers()
return
qs = urllib.parse.parse_qs(parsed.query)
result["code"] = qs.get("code", [None])[0]
result["state"] = qs.get("state", [None])[0]
result["error"] = qs.get("error", [None])[0]
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(b"<html><body>Login captured, you can close this tab.</body></html>")

def log_message(self, *_args: object) -> None:
pass

server = http.server.HTTPServer(("127.0.0.1", CALLBACK_PORT), Handler)
server.timeout = 180
while "code" not in result and "error" not in result:
server.handle_request()
return result

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

server.timeout doesn't bound the wait — loop retries forever (CWE-835-adjacent hang).

server.timeout = 180 only caps a single handle_request() call. If it expires with no request received, handle_timeout() is a no-op and the while condition is still true, so the loop just calls handle_request() again — indefinitely. This is a blocking wait with no actual deadline, contrary to what the 180s value implies.

🔒️ Proposed fix: enforce an actual deadline
+import time
+
 def wait_for_callback() -> dict[str, str | None]:
     result: dict[str, str | None] = {}
     ...
     server = http.server.HTTPServer(("127.0.0.1", CALLBACK_PORT), Handler)
     server.timeout = 180
-    while "code" not in result and "error" not in result:
-        server.handle_request()
+    deadline = time.monotonic() + 180
+    while "code" not in result and "error" not in result:
+        if time.monotonic() > deadline:
+            raise SystemExit("Timed out waiting for SSO callback")
+        server.handle_request()
     return result
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def wait_for_callback() -> dict[str, str | None]:
result: dict[str, str | None] = {}
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self) -> None:
parsed = urllib.parse.urlparse(self.path)
if parsed.path != CALLBACK_PATH:
self.send_response(404)
self.end_headers()
return
qs = urllib.parse.parse_qs(parsed.query)
result["code"] = qs.get("code", [None])[0]
result["state"] = qs.get("state", [None])[0]
result["error"] = qs.get("error", [None])[0]
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(b"<html><body>Login captured, you can close this tab.</body></html>")
def log_message(self, *_args: object) -> None:
pass
server = http.server.HTTPServer(("127.0.0.1", CALLBACK_PORT), Handler)
server.timeout = 180
while "code" not in result and "error" not in result:
server.handle_request()
return result
import time
def wait_for_callback() -> dict[str, str | None]:
result: dict[str, str | None] = {}
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self) -> None:
parsed = urllib.parse.urlparse(self.path)
if parsed.path != CALLBACK_PATH:
self.send_response(404)
self.end_headers()
return
qs = urllib.parse.parse_qs(parsed.query)
result["code"] = qs.get("code", [None])[0]
result["state"] = qs.get("state", [None])[0]
result["error"] = qs.get("error", [None])[0]
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(b"<html><body>Login captured, you can close this tab.</body></html>")
def log_message(self, *_args: object) -> None:
pass
server = http.server.HTTPServer(("127.0.0.1", CALLBACK_PORT), Handler)
server.timeout = 180
deadline = time.monotonic() + 180
while "code" not in result and "error" not in result:
if time.monotonic() > deadline:
raise SystemExit("Timed out waiting for SSO callback")
server.handle_request()
return result
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.cursor/skills/kartograph/scripts/argocd_pkce_login.py around lines 47 - 73,
Update wait_for_callback so the 180-second timeout is an actual overall deadline
rather than a per-request timeout. Track a monotonic deadline before the request
loop, recompute the remaining time before each server.handle_request() call, and
exit or raise the established timeout error when no callback arrives by the
deadline; preserve immediate completion when code or error is received.

Comment on lines +103 to +116
token_params = {
"grant_type": "authorization_code",
"code": result["code"],
"redirect_uri": redirect_uri,
"client_id": CLIENT_ID,
"code_verifier": verifier,
}
req = urllib.request.Request(
f"https://{args.server}/api/dex/token",
data=urllib.parse.urlencode(token_params).encode(),
method="POST",
)
with urllib.request.urlopen(req) as resp:
tokens = json.load(resp)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- file excerpt ---'
cat -n .cursor/skills/kartograph/scripts/argocd_pkce_login.py | sed -n '85,130p'

echo
echo '--- search for timeout usage in this script/repo ---'
rg -n "urlopen\\(|timeout=|Content-Type|application/x-www-form-urlencoded|ParseForm|dex/token" .cursor/skills/kartograph/scripts/argocd_pkce_login.py .

echo
echo '--- python urllib behavior probe ---'
python3 - <<'PY'
import urllib.request, urllib.parse

token_params = {"grant_type":"authorization_code","code":"abc","redirect_uri":"https://example","client_id":"cid","code_verifier":"ver"}
req = urllib.request.Request(
    "https://example.invalid/api/dex/token",
    data=urllib.parse.urlencode(token_params).encode(),
    method="POST",
)
print("headers:", dict(req.header_items()))
print("has_content_type_header:", req.get_header("Content-type"))
print("method:", req.method)
print("data_type:", type(req.data).__name__, "len:", len(req.data))
PY

Repository: openshift-hyperfleet/kartograph

Length of output: 22260


🏁 Script executed (no clone):

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import inspect
import urllib.request
import urllib.parse
import urllib

print("python:", urllib.__file__)
print("Request has data doc:", urllib.request.Request.__doc__.splitlines()[0])

src = inspect.getsource(urllib.request.Request)
for needle in ["content-type", "Content-type", "application/x-www-form-urlencoded", "has_header", "add_header"]:
    if needle.lower() in src.lower():
        print(f"\n--- matches for {needle} ---")
        for i, line in enumerate(src.splitlines(), 1):
            if needle.lower() in line.lower():
                print(f"{i}: {line}")
PY

Length of output: 309


🏁 Script executed (no clone):

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import inspect, urllib.request

print(inspect.getsource(urllib.request.Request))
PY

Length of output: 3556


🏁 Script executed (no clone):

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import inspect
import urllib.request

for name in ["AbstractHTTPHandler", "HTTPHandler", "HTTPSHandler", "AbstractHTTPHandler.do_request_", "build_opener"]:
    obj = getattr(urllib.request, name.split(".")[0], None)
    if obj is None:
        continue

print("---- AbstractHTTPHandler.do_request_ ----")
print(inspect.getsource(urllib.request.AbstractHTTPHandler.do_request_))

print("---- AbstractHTTPHandler.do_open ----")
print(inspect.getsource(urllib.request.AbstractHTTPHandler.do_open))
PY

Length of output: 4439


🏁 Script executed (no clone):

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import inspect
import urllib.request

print("---- AbstractHTTPHandler.do_request_ ----")
print(inspect.getsource(urllib.request.AbstractHTTPHandler.do_request_))

print("---- AbstractHTTPHandler.do_open ----")
print(inspect.getsource(urllib.request.AbstractHTTPHandler.do_open))
PY

Length of output: 4439


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import inspect
import urllib.request

print("---- urlopen ----")
print(inspect.getsource(urllib.request.urlopen))

print("---- OpenerDirector.open ----")
print(inspect.getsource(urllib.request.OpenerDirector.open))
PY

Repository: openshift-hyperfleet/kartograph

Length of output: 4792


Add a finite timeout to the Dex token exchange. urlopen(req) can hang the login flow indefinitely if Dex stalls (CWE-400).

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 114-114: Request-controlled URL passed to urlopen; validate against an allowlist to prevent SSRF.
Context: urllib.request.urlopen(req)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(urlopen-unsanitized-data)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.cursor/skills/kartograph/scripts/argocd_pkce_login.py around lines 103 -
116, Add a finite timeout to the urllib.request.urlopen call in the Dex token
exchange so stalled Dex responses cannot block the login flow indefinitely. Keep
the existing request construction and JSON token parsing unchanged.

Comment on lines +118 to +121
id_token = tokens["id_token"]
with open(args.out, "w", encoding="utf-8") as f:
f.write(id_token)
print(f"id_token written to {args.out} ({len(id_token)} chars)")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Bearer credential written to disk with default file permissions (CWE-276 / CWE-377).

id_token is a live ArgoCD auth token; open(args.out, "w") creates the file with the process umask (commonly world/group-readable) at a predictable, hardcoded default path (/tmp/argocd_token.txt, flagged by static analysis as CWE-377). Any other local user/process with read access to /tmp can lift the token while it's valid.

🔒️ Proposed fix: restrict permissions at creation
+import os
+
-    with open(args.out, "w", encoding="utf-8") as f:
-        f.write(id_token)
+    fd = os.open(args.out, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
+    with os.fdopen(fd, "w", encoding="utf-8") as f:
+        f.write(id_token)

Static hints on this line (SSRF for urlopen, path traversal for open) are false positives here: args.server/args.out are operator-supplied CLI flags on a local manual tool, not remote/attacker-controlled input.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
id_token = tokens["id_token"]
with open(args.out, "w", encoding="utf-8") as f:
f.write(id_token)
print(f"id_token written to {args.out} ({len(id_token)} chars)")
import os
id_token = tokens["id_token"]
fd = os.open(args.out, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(id_token)
print(f"id_token written to {args.out} ({len(id_token)} chars)")
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 118-118: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(args.out, "w", encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.cursor/skills/kartograph/scripts/argocd_pkce_login.py around lines 118 -
121, Update the token-writing flow around id_token and args.out to create the
output file with owner-only permissions (0600) at creation time, rather than
relying on the process umask. Preserve writing the token and reporting its
length, and ensure existing files are not left with broader permissions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant