import os, json, urllib.request URL = os.environ["REDMINE_URL"] KEY = os.environ["REDMINE_API_KEY"] cats = {c["name"]: c["id"] for c in json.load(urllib.request.urlopen(urllib.request.Request( URL + "/projects/MOPAC/issue_categories.json", headers={"X-Redmine-API-Key": KEY}), timeout=15))["issue_categories"]} vers = {v["name"]: v["id"] for v in json.load(urllib.request.urlopen(urllib.request.Request( URL + "/projects/MOPAC/versions.json", headers={"X-Redmine-API-Key": KEY}), timeout=15))["versions"]} print("cats:", cats, "versions:", vers) desc = """# Two-level identity model: Linux account + Cloudron accounts ## Objective (Charles, 2026-08-29) The harness must model TWO identity levels everywhere it acts: - **Linux account** = runtime identity (where the daemon runs: reachableceo, TSGBOD, ..., COSWFO). Drives: state dirs, quota/usage attribution (the $200/mo breakdown is per vertical), Redmine scope, briefing rollup. - **Cloudron accounts** = acting identities (N per Linux account, e.g. a dozen+). All actions in TSYS systems happen via API under Cloudron SSO identities; their credentials live in per-cloudron-account Bitwarden machine accounts (#497). ## Scope - harness.toml gains an identity section: linux account name + cloudron account map (label -> credential refs via keyproxy mpk_ placeholders, per system: redmine, discourse, gitea, linkwarden, ...). - Attribution stamped on every outbound action: loop JSONL + REPORT headers record linux account + cloudron identity used. - deploy/accounts.tsv (from #494) gains a cloudron-accounts column; installer templates the identity section. - Usage accounting (from #490) rolls up per linux account AND per cloudron identity. - keyproxy ref lookup stays instance-local; cloudron label selects the BW machine account context (#497). ## Acceptance criteria - A turn executed under a fake 2-account config attributes actions correctly at both levels. - REPORT/JSONL show both identities; README documents the model. ## References SPEC-20260829-charles-brief.md (cloudron accounts section); #497 scale amendment. """ body = json.dumps({"issue": { "project_id": "MOPAC", "subject": "Two-level identity model: Linux account (runtime) + Cloudron accounts (acting)", "tracker_id": 2, "priority_id": 4, "category_id": cats["Infrastructure"], "fixed_version_id": vers["Production"], "estimated_hours": 5, "description": desc}}).encode() req = urllib.request.Request(URL + "/issues.json", data=body, headers={"X-Redmine-API-Key": KEY, "Content-Type": "application/json"}, method="POST") r = json.load(urllib.request.urlopen(req, timeout=15)) print("ticket", r["issue"]["id"], "created")