Job 1788305896968

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"}),