Command: python -c "from pathlib import Path;s=Path('../sheet_relay.py').read_text(encoding='utf-8',errors='replace').splitlines();print('\n'.join(str(i+1)+':'+s[i] for i in range(1310,min(1535,len(s)))))"
Directory: projects
Status: SUCCESS
Exit code: 0
Cancel Job Rerun Command Refresh
1311: return {"ok": False, "error": "invalid_evidence_path"}
1312: path = (EVIDENCE_DIR / requested).resolve()
1313: try:
1314: path.relative_to(EVIDENCE_DIR.resolve())
1315: except ValueError:
1316: return {"ok": False, "error": "evidence_outside_store"}
1317: if not path.is_file():
1318: return {"ok": False, "error": "evidence_not_found"}
1319: try:
1320: max_chars = max(1000, min(int(payload.get("max_chars", MAX_RESULT_CHARS)), MAX_RESULT_CHARS))
1321: except (TypeError, ValueError):
1322: max_chars = MAX_RESULT_CHARS
1323: content = path.read_text(encoding="utf-8")[:max_chars]
1324: return {"ok": True, "path": str(path.relative_to(ROOT)).replace("\\", "/"), "content": content, "truncated": path.stat().st_size > len(content)}
1325:
1326:
1327:def read_bridge_secret():
1328: config = load_json(ROOT / "config.json", {})
1329: secret = config.get("secret") or os.environ.get("BRIDGE_SECRET")
1330: if secret:
1331: return secret.strip()
1332: secret_path = ROOT / "bridge-data" / "secret.txt"
1333: if secret_path.exists():
1334: return secret_path.read_text(encoding="utf-8").strip()
1335: return ""
1336:
1337:
1338:def policy_check(action, payload):
1339: policy = load_json(POLICY_PATH, DEFAULT_POLICY)
1340: if not isinstance(policy, dict):
1341: policy = DEFAULT_POLICY
1342: # Optional ordered policy rules. Explicit denies always win; an "ask"
1343: # rule is satisfied only by the command row's explicit confirmation.
1344: action_rule = policy.get("action_rules", {}).get(action) if isinstance(policy.get("action_rules"), dict) else None
1345: if str(action_rule).lower() == "deny":
1346: return False, f"policy denied action: {action}"
1347: if str(action_rule).lower() == "ask" and str(payload.get("confirm", "")).lower() not in {"1", "yes", "true"}:
1348: return False, f"policy requires confirmation for action: {action}"
1349: command = str(payload.get("cmd", ""))
1350: haystack = command.lower()
1351: for rule in policy.get("command_rules", []):
1352: if not isinstance(rule, dict):
1353: continue
1354: pattern = str(rule.get("pattern", "")).strip().lower()
1355: effect = str(rule.get("effect", "")).strip().lower()
1356: if pattern and fnmatch.fnmatchcase(haystack, pattern):
1357: if effect == "deny":
1358: return False, f"policy denied command rule: {rule.get('pattern')}"
1359: if effect == "ask" and str(payload.get("confirm", "")).lower() not in {"1", "yes", "true"}:
1360: return False, f"policy requires confirmation for command: {rule.get('pattern')}"
1361: for denied in policy.get("deny_commands", DEFAULT_POLICY["deny_commands"]):
1362: if str(denied).lower() in haystack:
1363: return False, f"policy denied command pattern: {denied}"
1364: for key in ("path", "cwd"):
1365: value = str(payload.get(key, "")).lower()
1366: for rule in policy.get("path_rules", []):
1367: if not isinstance(rule, dict):
1368: continue
1369: pattern = str(rule.get("pattern", "")).strip().lower().replace("\\", "/")
1370: effect = str(rule.get("effect", "")).strip().lower()
1371: if pattern and fnmatch.fnmatchcase(value.replace("\\", "/"), pattern):
1372: if effect == "deny":
1373: return False, f"policy denied path rule: {rule.get('pattern')}"
1374: if effect == "ask" and str(payload.get("confirm", "")).lower() not in {"1", "yes", "true"}:
1375: return False, f"policy requires confirmation for path: {rule.get('pattern')}"
1376: for denied in policy.get("deny_paths", DEFAULT_POLICY["deny_paths"]):
1377: if str(denied).lower() in value:
1378: return False, f"policy denied protected path: {denied}"
1379: return True, ""
1380:
1381:
1382:def policy_status():
1383: policy = load_json(POLICY_PATH, DEFAULT_POLICY)
1384: if not isinstance(policy, dict):
1385: policy = DEFAULT_POLICY
1386: return {
1387: "ok": True,
1388: "version": policy.get("version", 1),
1389: "action_rules": policy.get("action_rules", {}),
1390: "command_rules": policy.get("command_rules", []),
1391: "path_rules": policy.get("path_rules", []),
1392: "deny_commands": policy.get("deny_commands", DEFAULT_POLICY["deny_commands"]),
1393: "deny_paths": policy.get("deny_paths", DEFAULT_POLICY["deny_paths"]),
1394: "source": str(POLICY_PATH.relative_to(ROOT)).replace("\\", "/"),
1395: }
1396:
1397:
1398:def policy_explain(payload):
1399: allowed, message = policy_check(str(payload.get("action", "run_command")), payload)
1400: decision = "allow" if allowed else "ask" if message.lower().startswith("policy requires confirmation") else "deny"
1401: return {"ok": True, "decision": decision, "allowed": allowed, "reason": message or "no deny rule matched", "action": str(payload.get("action", "run_command")), "path": str(payload.get("path", "")), "cwd": str(payload.get("cwd", "")), "command_present": bool(str(payload.get("cmd", "")).strip())}
1402:
1403:
1404:def init_config(web_app_url=""):
1405: cfg = load_json(CONFIG_PATH, {})
1406: changed = False
1407: if not cfg.get("relay_key"):
1408: cfg["relay_key"] = secrets.token_urlsafe(32)
1409: changed = True
1410: if not cfg.get("spreadsheet_id"):
1411: cfg["spreadsheet_id"] = DEFAULT_SPREADSHEET_ID
1412: changed = True
1413: if not cfg.get("web_app_url"):
1414: cfg["web_app_url"] = web_app_url
1415: changed = True
1416: if not cfg.get("bridge_base"):
1417: cfg["bridge_base"] = DEFAULT_BRIDGE_BASE
1418: changed = True
1419: if not cfg.get("bridge_secret"):
1420: cfg["bridge_secret"] = read_bridge_secret()
1421: changed = True
1422: if "poll_seconds" not in cfg:
1423: cfg["poll_seconds"] = 3
1424: changed = True
1425: if changed:
1426: save_json(CONFIG_PATH, cfg)
1427: return cfg
1428:
1429:
1430:def require_config():
1431: cfg = load_json(CONFIG_PATH, {})
1432: missing = [k for k in ("relay_key", "web_app_url", "bridge_base", "bridge_secret") if not cfg.get(k)]
1433: if missing:
1434: raise SystemExit(
1435: "Sheet relay is not configured. Run:\n"
1436: " python setup_sheet_relay.py\n"
1437: "Then deploy apps-script/Code.gs as a Web App and run:\n"
1438: " python setup_sheet_relay.py --web-app-url <URL>"
1439: )
1440: return cfg
1441:
1442:
1443:def b64url(text):
1444: return base64.urlsafe_b64encode(text.encode("utf-8")).decode("ascii").rstrip("=")
1445:
1446:
1447:def http_json(url, payload=None, timeout=45):
1448: data = None
1449: headers = {"User-Agent": f"ChatGPT-Bridge-SheetRelay/{REVISION}"}
1450: if payload is not None:
1451: data = json.dumps(payload).encode("utf-8")
1452: headers["Content-Type"] = "application/json"
1453: req = urllib.request.Request(url, data=data, headers=headers)
1454: with urllib.request.urlopen(req, timeout=timeout) as resp:
1455: raw = resp.read().decode("utf-8", errors="replace")
1456: return resp.status, json.loads(raw or "{}")
1457:
1458:
1459:def http_text(url, timeout=60):
1460: req = urllib.request.Request(url, headers={"User-Agent": f"ChatGPT-Bridge-SheetRelay/{REVISION}"})
1461: with urllib.request.urlopen(req, timeout=timeout) as resp:
1462: return resp.status, resp.read().decode("utf-8", errors="replace")
1463:
1464:
1465:def strip_html(text):
1466: text = re.sub(r"(?is)<script.*?</script>|<style.*?</style>", "", text)
1467: text = re.sub(r"(?i)<br\s*/?>", "\n", text)
1468: text = re.sub(r"(?i)</p>|</li>|</tr>|</h[1-6]>", "\n", text)
1469: text = re.sub(r"(?s)<[^>]+>", " ", text)
1470: text = html.unescape(text)
1471: return re.sub(r"\n\s*\n+", "\n\n", re.sub(r"[ \t]+", " ", text)).strip()
1472:
1473:
1474:def mask_sensitive(text):
1475: """Mask common credential-shaped values before returning bridge output."""
1476: value = str(text or "")
1477: value = re.sub(r"(?i)\b(api[_-]?key|access[_-]?token|secret|password|passwd|authorization)\s*[:=]\s*([^\s\"'&,]+)", r"\1=[REDACTED]", value)
1478: value = re.sub(r"(?i)\bBearer\s+[A-Za-z0-9._~+/=-]+", "Bearer [REDACTED]", value)
1479: return value
1480:
1481:
1482:def bridge_url(cfg, route, params=None):
1483: if not route.startswith("/"):
1484: route = "/" + route
1485: base = cfg["bridge_base"].rstrip("/")
1486: secret = cfg["bridge_secret"].strip("/")
1487: query = urllib.parse.urlencode(params or {})
1488: url = f"{base}/b/{secret}{route}"
1489: if query:
1490: url += "?" + query
1491: return url
1492:
1493:
1494:def call_bridge(cfg, route, params=None, timeout=60):
1495: status, text = http_text(bridge_url(cfg, route, params), timeout=timeout)
1496: return {"http_status": status, "text": mask_sensitive(strip_html(text))}
1497:
1498:
1499:def parse_stage_id(text):
1500: # Bridge previews use /apply/{write,patch}; command previews use /run.
1501: match = re.search(r"/(?:run|apply/(?:write|patch))\?[^\"']*id=([0-9a-fA-F]+)", text)
1502: return match.group(1) if match else ""
1503:
1504:
1505:def parse_job_id(text):
1506: match = re.search(r"JOB:\s*([0-9a-fA-F]+)", strip_html(text))
1507: return match.group(1) if match else ""
1508:
1509:
1510:def has_failed_step(text):
1511: """Detect a failed ChangeSet step without depending on one HTML layout."""
1512: plain = strip_html(text)
1513: return bool(re.search(r"(?:step\s*\d+[^\n]{0,160})?\b(?:failed|failure|error)\b", plain, re.I))
1514:
1515:
1516:def extract_step_statuses(text):
1517: plain = strip_html(text)
1518: statuses = []
1519: for line in plain.splitlines():
1520: if re.search(r"\b(?:failed|failure|error)\b", line, re.I):
1521: statuses.append({"status": "failed", "text": line.strip()[:500]})
1522: elif re.search(r"\b(?:success|completed|done|ok)\b", line, re.I) and re.search(r"step\s*\d+", line, re.I):
1523: statuses.append({"status": "success", "text": line.strip()[:500]})
1524: return statuses
1525:
1526:
1527:def direct_bridge_actions():
1528: return {
1529: "tunnel_status": ("/tunnel-status", {}),
1530: "project_dashboard": ("/project", {"path": "path"}),
1531: "workspace_tree": ("/tree", {"path": "path", "depth": "depth"}),
1532: "recent": ("/recent", {}),
1533: "audit": ("/audit", {}),
1534: "compose_new": ("/compose/new", {"kind": "kind", "cwd": "cwd", "path": "path"}),
1535: "compose_show": ("/compose", {"id": "id"}),