dev와 dev-corp 요청을 명시적 환경 프로필로 라우팅하고, private override 없이 단일 실행기에서 Edge 검증과 환경별 metadata 정책을 처리한다.
1363 lines
47 KiB
Python
1363 lines
47 KiB
Python
"""Single-entry dev and dev-corp IOP token issuance workflow.
|
|
|
|
Sensitive request data is accepted only as JSON on stdin. The command emits a
|
|
redacted result and copies the raw token to the local macOS clipboard only after
|
|
Edge activation, environment-specific API smoke, metadata synchronization, and
|
|
leak checks pass.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import fcntl
|
|
import hashlib
|
|
import html
|
|
import json
|
|
import os
|
|
import re
|
|
import secrets
|
|
import shlex
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import unicodedata
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
from contextlib import contextmanager
|
|
from html.parser import HTMLParser
|
|
from pathlib import Path
|
|
from typing import Any, NoReturn, cast
|
|
|
|
ATLASSIAN_BASE_URL = "https://lgucorp.atlassian.net"
|
|
CONFLUENCE_FOLDER_ID = "650886407"
|
|
CONFLUENCE_TITLE = "IOP 계정 발급 현황"
|
|
MANAGED_HEADING = "IOP 사용자 토큰 발급 현황"
|
|
TABLE_HEADERS = ("사용자", "principal alias", "token ref", "상태", "동기화 시각")
|
|
SUPPORTED_ENVIRONMENTS = ("dev", "dev-corp")
|
|
|
|
|
|
class WorkflowFailure(RuntimeError):
|
|
def __init__(self, code: str):
|
|
self.code = code
|
|
super().__init__(code)
|
|
|
|
|
|
class ConfluenceHTTPFailure(WorkflowFailure):
|
|
def __init__(self, status_code: int):
|
|
self.status_code = status_code
|
|
super().__init__(f"confluence_http_{status_code}")
|
|
|
|
|
|
class CurlTransportFailure(WorkflowFailure):
|
|
def __init__(self):
|
|
super().__init__("curl_transport_failed")
|
|
|
|
|
|
def fail(code: str) -> NoReturn:
|
|
raise WorkflowFailure(code)
|
|
|
|
|
|
def repo_root() -> Path:
|
|
root = Path(__file__).resolve().parents[5]
|
|
if not (root / ".git").exists():
|
|
fail("repo_root_invalid")
|
|
return root
|
|
|
|
|
|
def load_profile(root: Path, environment: str) -> dict[str, Any]:
|
|
if environment not in SUPPORTED_ENVIRONMENTS:
|
|
fail("environment_not_supported")
|
|
path = (
|
|
root
|
|
/ "agent-ops/skills/project/openai-usage-token-issue/profiles"
|
|
/ f"{environment}.json"
|
|
)
|
|
try:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError):
|
|
fail("environment_profile_invalid")
|
|
if not isinstance(value, dict) or value.get("environment") != environment:
|
|
fail("environment_profile_invalid")
|
|
required_strings = (
|
|
"edge_ssh",
|
|
"secret_store",
|
|
"openai_smoke_transport",
|
|
"openai_base_url",
|
|
"smoke_model",
|
|
"metrics_transport",
|
|
"metrics_url",
|
|
)
|
|
if any(not isinstance(value.get(key), str) or not value[key] for key in required_strings):
|
|
fail("environment_profile_invalid")
|
|
if not isinstance(value.get("confluence_enabled"), bool):
|
|
fail("environment_profile_invalid")
|
|
expected_stores = {
|
|
"dev": "token/.dev-iop-token",
|
|
"dev-corp": "token/.dev-corp-iop-token",
|
|
}
|
|
expected_ssh = {"dev": "toki@toki-labs.com", "dev-corp": "toki@iop.ai.kr"}
|
|
if value["secret_store"] != expected_stores[environment]:
|
|
fail("environment_profile_invalid")
|
|
if value["edge_ssh"] != expected_ssh[environment]:
|
|
fail("environment_profile_invalid")
|
|
parsed_api = urllib.parse.urlsplit(str(value["openai_base_url"]))
|
|
transport = value["openai_smoke_transport"]
|
|
if transport == "public-https":
|
|
if parsed_api.scheme != "https" or not parsed_api.netloc:
|
|
fail("environment_profile_invalid")
|
|
elif transport == "ssh-loopback":
|
|
if parsed_api.scheme != "http" or parsed_api.hostname not in {"127.0.0.1", "::1", "localhost"}:
|
|
fail("environment_profile_invalid")
|
|
else:
|
|
fail("environment_profile_invalid")
|
|
if value["metrics_transport"] not in {"public-http", "ssh-loopback"}:
|
|
fail("environment_profile_invalid")
|
|
if value["confluence_enabled"] and not isinstance(
|
|
value.get("confluence_user_store"), str
|
|
):
|
|
fail("environment_profile_invalid")
|
|
return cast(dict[str, Any], value)
|
|
|
|
|
|
def run_quiet(
|
|
argv: list[str], *, cwd: Path, input_bytes: bytes | None = None, timeout: int = 30
|
|
) -> subprocess.CompletedProcess[bytes]:
|
|
try:
|
|
return subprocess.run(
|
|
argv,
|
|
cwd=cwd,
|
|
input=input_bytes,
|
|
capture_output=True,
|
|
timeout=timeout,
|
|
check=False,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
fail("command_timeout")
|
|
|
|
|
|
def git_is_ignored(root: Path, path: Path) -> bool:
|
|
relative = path.relative_to(root)
|
|
result = run_quiet(["git", "check-ignore", "-q", "--", str(relative)], cwd=root)
|
|
return result.returncode == 0
|
|
|
|
|
|
def git_is_tracked(root: Path, path: Path) -> bool:
|
|
relative = path.relative_to(root)
|
|
result = run_quiet(
|
|
["git", "ls-files", "--error-unmatch", "--", str(relative)], cwd=root
|
|
)
|
|
return result.returncode == 0
|
|
|
|
|
|
def require_secure_secret(root: Path, path: Path, *, must_exist: bool = True) -> None:
|
|
if path.is_symlink() or path.parent.is_symlink():
|
|
fail("secret_store_symlink_invalid")
|
|
try:
|
|
path.parent.resolve().relative_to(root.resolve())
|
|
except ValueError:
|
|
fail("secret_store_path_invalid")
|
|
if must_exist and not path.is_file():
|
|
fail("secret_store_missing")
|
|
if path.exists():
|
|
mode = stat.S_IMODE(path.stat().st_mode)
|
|
if mode != 0o600:
|
|
fail("secret_store_mode_invalid")
|
|
if not git_is_ignored(root, path) or git_is_tracked(root, path):
|
|
fail("secret_store_tracking_invalid")
|
|
|
|
|
|
@contextmanager
|
|
def local_transaction_lock(root: Path, *, shared: bool):
|
|
token_dir = root / "token"
|
|
if (
|
|
not token_dir.is_dir()
|
|
or token_dir.is_symlink()
|
|
or not git_is_ignored(root, token_dir)
|
|
):
|
|
fail("local_transaction_lock_invalid")
|
|
flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
try:
|
|
descriptor = os.open(token_dir, flags)
|
|
except OSError:
|
|
fail("local_transaction_lock_invalid")
|
|
try:
|
|
operation = fcntl.LOCK_SH if shared else fcntl.LOCK_EX
|
|
try:
|
|
fcntl.flock(descriptor, operation | fcntl.LOCK_NB)
|
|
except BlockingIOError:
|
|
fail("local_transaction_busy")
|
|
yield
|
|
finally:
|
|
try:
|
|
fcntl.flock(descriptor, fcntl.LOCK_UN)
|
|
finally:
|
|
os.close(descriptor)
|
|
|
|
|
|
def normalize_alias(principal_ref: str, requested: str | None) -> str:
|
|
source = requested.strip() if requested else principal_ref.split("@", 1)[0]
|
|
ascii_value = (
|
|
unicodedata.normalize("NFKD", source).encode("ascii", "ignore").decode("ascii")
|
|
)
|
|
alias = re.sub(r"[^a-z0-9]+", "-", ascii_value.lower()).strip("-")
|
|
alias = re.sub(r"-+", "-", alias)[:40].rstrip("-")
|
|
if not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,39}", alias):
|
|
fail("principal_alias_invalid")
|
|
return alias
|
|
|
|
|
|
def parse_request(stdin_text: str) -> dict[str, str]:
|
|
try:
|
|
payload = json.loads(stdin_text)
|
|
except json.JSONDecodeError:
|
|
fail("request_json_invalid")
|
|
if not isinstance(payload, dict):
|
|
fail("request_json_invalid")
|
|
if payload.get("operation", "create") != "create":
|
|
fail("operation_not_supported_by_single_entry")
|
|
environment = payload.get("env")
|
|
if not isinstance(environment, str) or environment not in SUPPORTED_ENVIRONMENTS:
|
|
fail("environment_not_supported")
|
|
principal_value = payload.get("principal_ref")
|
|
alias_value = payload.get("principal_alias")
|
|
token_ref_value = payload.get("token_ref")
|
|
if not isinstance(principal_value, str):
|
|
fail("principal_ref_invalid")
|
|
if alias_value is not None and not isinstance(alias_value, str):
|
|
fail("principal_alias_invalid")
|
|
if token_ref_value is not None and not isinstance(token_ref_value, str):
|
|
fail("token_ref_invalid")
|
|
principal_ref = principal_value.strip()
|
|
if (
|
|
not principal_ref
|
|
or len(principal_ref) > 254
|
|
or any(char in principal_ref for char in "\r\n:\0")
|
|
or any(char.isspace() for char in principal_ref)
|
|
):
|
|
fail("principal_ref_invalid")
|
|
alias = normalize_alias(principal_ref, alias_value)
|
|
token_ref = (token_ref_value or f"iop-{environment}-{alias}").strip()
|
|
if not re.fullmatch(r"[a-z0-9][a-z0-9._:-]{2,79}", token_ref):
|
|
fail("token_ref_invalid")
|
|
return {
|
|
"operation": "create",
|
|
"env": environment,
|
|
"principal_ref": principal_ref,
|
|
"principal_alias": alias,
|
|
"token_ref": token_ref,
|
|
}
|
|
|
|
|
|
def read_store(path: Path) -> tuple[bytes, dict[str, str]]:
|
|
if not path.exists():
|
|
return b"", {}
|
|
raw_bytes = path.read_bytes()
|
|
try:
|
|
text = raw_bytes.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
fail("secret_store_encoding_invalid")
|
|
entries: dict[str, str] = {}
|
|
for line in text.splitlines():
|
|
if not line.strip():
|
|
continue
|
|
key, separator, token = line.partition(": ")
|
|
if (
|
|
not separator
|
|
or not key
|
|
or key.strip() != key
|
|
or not token.startswith("iop_")
|
|
or token.strip() != token
|
|
or key in entries
|
|
or "\n" in token
|
|
or "\r" in token
|
|
):
|
|
fail("secret_store_format_invalid")
|
|
entries[key] = token
|
|
return raw_bytes, entries
|
|
|
|
|
|
def write_candidate(path: Path, content: bytes) -> Path:
|
|
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
os.chmod(path.parent, 0o700)
|
|
candidate = (
|
|
path.parent / f".{path.name}.candidate-{os.getpid()}-{secrets.token_hex(4)}"
|
|
)
|
|
descriptor = os.open(candidate, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
try:
|
|
with os.fdopen(descriptor, "wb") as stream:
|
|
stream.write(content)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
except BaseException:
|
|
candidate.unlink(missing_ok=True)
|
|
raise
|
|
return candidate
|
|
|
|
|
|
def atomic_restore(path: Path, content: bytes | None) -> None:
|
|
if content is None:
|
|
path.unlink(missing_ok=True)
|
|
return
|
|
candidate = write_candidate(path, content)
|
|
os.replace(candidate, path)
|
|
os.chmod(path, 0o600)
|
|
|
|
|
|
def sha256_text(value: str) -> str:
|
|
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def curl_config_quote(value: str) -> str:
|
|
return (
|
|
value.replace("\\", "\\\\")
|
|
.replace('"', '\\"')
|
|
.replace("\r", "")
|
|
.replace("\n", "")
|
|
)
|
|
|
|
|
|
def curl_request(
|
|
root: Path,
|
|
method: str,
|
|
url: str,
|
|
headers: list[str],
|
|
payload: dict[str, Any] | None,
|
|
*,
|
|
timeout: int,
|
|
) -> tuple[int, bytes]:
|
|
marker = b"\n__IOP_HTTP_STATUS__"
|
|
config_lines = [
|
|
"silent",
|
|
"show-error",
|
|
'proto = "=https"',
|
|
f"max-time = {timeout}",
|
|
f'request = "{curl_config_quote(method)}"',
|
|
f'url = "{curl_config_quote(url)}"',
|
|
'write-out = "\\n__IOP_HTTP_STATUS__%{http_code}"',
|
|
]
|
|
config_lines.extend(f'header = "{curl_config_quote(header)}"' for header in headers)
|
|
argv = ["curl", "-q", "--config", "-"]
|
|
payload_path: Path | None = None
|
|
if payload is not None:
|
|
payload_bytes = json.dumps(
|
|
payload, ensure_ascii=False, separators=(",", ":")
|
|
).encode("utf-8")
|
|
payload_path = write_candidate(root / "token/.curl-json-payload", payload_bytes)
|
|
argv.extend(["--data-binary", f"@{payload_path}"])
|
|
try:
|
|
result = run_quiet(
|
|
argv,
|
|
cwd=root,
|
|
input_bytes=("\n".join(config_lines) + "\n").encode("utf-8"),
|
|
timeout=timeout + 5,
|
|
)
|
|
finally:
|
|
if payload_path:
|
|
payload_path.unlink(missing_ok=True)
|
|
if result.returncode != 0:
|
|
raise CurlTransportFailure()
|
|
body, separator, status_bytes = result.stdout.rpartition(marker)
|
|
if not separator or not status_bytes.isdigit():
|
|
raise CurlTransportFailure()
|
|
return int(status_bytes), body
|
|
|
|
|
|
def remote_call(
|
|
root: Path,
|
|
profile: dict[str, Any],
|
|
payload: dict[str, Any],
|
|
*,
|
|
timeout: int,
|
|
) -> dict[str, Any]:
|
|
helper = (
|
|
root
|
|
/ "agent-ops/skills/project/openai-usage-token-issue/scripts/remote_edge_transaction.rb"
|
|
)
|
|
source = helper.read_text(encoding="utf-8")
|
|
remote_command = "ruby -e " + shlex.quote(source)
|
|
remote_payload = dict(payload)
|
|
remote_payload["environment"] = profile["environment"]
|
|
result = run_quiet(
|
|
[
|
|
"ssh",
|
|
"-o",
|
|
"BatchMode=yes",
|
|
"-o",
|
|
"ConnectTimeout=8",
|
|
str(profile["edge_ssh"]),
|
|
remote_command,
|
|
],
|
|
cwd=root,
|
|
input_bytes=json.dumps(remote_payload, separators=(",", ":")).encode("utf-8"),
|
|
timeout=timeout,
|
|
)
|
|
parsed: dict[str, Any] | None = None
|
|
for line in reversed(result.stdout.decode("utf-8", "replace").splitlines()):
|
|
try:
|
|
value = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
if isinstance(value, dict) and "status" in value:
|
|
parsed = value
|
|
break
|
|
if parsed is None:
|
|
fail("edge_remote_response_invalid")
|
|
if result.returncode != 0 or parsed.get("status") == "blocked":
|
|
code = str(parsed.get("code", "edge_remote_failed"))
|
|
if not re.fullmatch(r"[a-z0-9_]+", code):
|
|
code = "edge_remote_failed"
|
|
fail(code if code.startswith("edge_") else f"edge_{code}")
|
|
return parsed
|
|
|
|
|
|
class ConfluenceClient:
|
|
def __init__(self, root: Path, username: str, token: str):
|
|
self.root = root
|
|
self.username = username
|
|
self.token = token
|
|
|
|
def request(
|
|
self, method: str, path_or_url: str, payload: dict[str, Any] | None = None
|
|
) -> dict[str, Any]:
|
|
url = (
|
|
path_or_url
|
|
if path_or_url.startswith("https://")
|
|
else ATLASSIAN_BASE_URL + path_or_url
|
|
)
|
|
parsed_url = urllib.parse.urlsplit(url)
|
|
if (
|
|
parsed_url.scheme != "https"
|
|
or parsed_url.netloc != "lgucorp.atlassian.net"
|
|
or not parsed_url.path.startswith("/wiki/")
|
|
):
|
|
fail("confluence_url_invalid")
|
|
try:
|
|
status_code, data = curl_request(
|
|
self.root,
|
|
method,
|
|
url,
|
|
[
|
|
f"Authorization: Basic {base64_basic(self.username, self.token)}",
|
|
"Accept: application/json",
|
|
"Content-Type: application/json",
|
|
],
|
|
payload,
|
|
timeout=20,
|
|
)
|
|
except CurlTransportFailure:
|
|
fail("confluence_network_failed")
|
|
if status_code < 200 or status_code >= 300:
|
|
raise ConfluenceHTTPFailure(status_code)
|
|
try:
|
|
parsed = json.loads(data)
|
|
except json.JSONDecodeError:
|
|
fail("confluence_response_invalid")
|
|
if not isinstance(parsed, dict):
|
|
fail("confluence_response_invalid")
|
|
return parsed
|
|
|
|
|
|
def base64_basic(username: str, token: str) -> str:
|
|
return base64.b64encode(f"{username}:{token}".encode()).decode("ascii")
|
|
|
|
|
|
def confluence_storage(page: dict[str, Any]) -> str:
|
|
body = page.get("body")
|
|
if not isinstance(body, dict):
|
|
fail("confluence_body_missing")
|
|
storage = body.get("storage")
|
|
if not isinstance(storage, dict) or not isinstance(storage.get("value"), str):
|
|
fail("confluence_body_missing")
|
|
return str(storage["value"])
|
|
|
|
|
|
def page_version(page: dict[str, Any]) -> int:
|
|
version = page.get("version")
|
|
if not isinstance(version, dict) or not isinstance(version.get("number"), int):
|
|
fail("confluence_version_invalid")
|
|
return int(version["number"])
|
|
|
|
|
|
def strip_storage_tags(value: str) -> str:
|
|
without_tags = re.sub(r"<[^>]+>", "", value)
|
|
return re.sub(r"\s+", " ", html.unescape(without_tags)).strip()
|
|
|
|
|
|
def managed_section_bounds(storage_body: str) -> tuple[int, int]:
|
|
heading_pattern = re.compile(
|
|
r"<h([1-6])\b[^>]*>.*?</h\1>", re.IGNORECASE | re.DOTALL
|
|
)
|
|
headings = list(heading_pattern.finditer(storage_body))
|
|
matches = [
|
|
item
|
|
for item in headings
|
|
if strip_storage_tags(item.group(0)) == MANAGED_HEADING
|
|
]
|
|
if len(matches) != 1:
|
|
fail("confluence_managed_heading_invalid")
|
|
current = matches[0]
|
|
current_level = int(current.group(1))
|
|
end = len(storage_body)
|
|
for candidate in headings:
|
|
if (
|
|
candidate.start() > current.start()
|
|
and int(candidate.group(1)) <= current_level
|
|
):
|
|
end = candidate.start()
|
|
break
|
|
return current.start(), end
|
|
|
|
|
|
def managed_section(mapping_rows: list[dict[str, str]], synchronized_at: str) -> str:
|
|
parts = [f"<h2>{html.escape(MANAGED_HEADING)}</h2>", "<table><tbody><tr>"]
|
|
parts.extend(f"<th>{html.escape(header)}</th>" for header in TABLE_HEADERS)
|
|
parts.append("</tr>")
|
|
for item in sorted(mapping_rows, key=lambda row: row["principal_ref"]):
|
|
values = (
|
|
item["principal_ref"],
|
|
item["principal_alias"],
|
|
item["token_ref"],
|
|
"active",
|
|
synchronized_at,
|
|
)
|
|
parts.append("<tr>")
|
|
parts.extend(f"<td>{html.escape(value)}</td>" for value in values)
|
|
parts.append("</tr>")
|
|
parts.append("</tbody></table>")
|
|
return "".join(parts)
|
|
|
|
|
|
def replace_managed_section(storage_body: str, replacement: str) -> str:
|
|
if not storage_body.strip():
|
|
return replacement
|
|
start, end = managed_section_bounds(storage_body)
|
|
return storage_body[:start] + replacement + storage_body[end:]
|
|
|
|
|
|
class TableParser(HTMLParser):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.rows: list[list[str]] = []
|
|
self.current_row: list[str] | None = None
|
|
self.current_cell: list[str] | None = None
|
|
|
|
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
del attrs
|
|
if tag.lower() == "tr":
|
|
self.current_row = []
|
|
elif tag.lower() in {"td", "th"} and self.current_row is not None:
|
|
self.current_cell = []
|
|
|
|
def handle_data(self, data: str) -> None:
|
|
if self.current_cell is not None:
|
|
self.current_cell.append(data)
|
|
|
|
def handle_endtag(self, tag: str) -> None:
|
|
lowered = tag.lower()
|
|
if (
|
|
lowered in {"td", "th"}
|
|
and self.current_cell is not None
|
|
and self.current_row is not None
|
|
):
|
|
self.current_row.append(
|
|
re.sub(r"\s+", " ", "".join(self.current_cell)).strip()
|
|
)
|
|
self.current_cell = None
|
|
elif lowered == "tr" and self.current_row is not None:
|
|
self.rows.append(self.current_row)
|
|
self.current_row = None
|
|
|
|
|
|
def parse_managed_table(storage_body: str) -> list[tuple[str, str, str, str, str]]:
|
|
start, end = managed_section_bounds(storage_body)
|
|
section = storage_body[start:end]
|
|
if "iop_" in section or re.search(
|
|
r"(?<![0-9a-f])[0-9a-f]{64}(?![0-9a-f])", section, re.IGNORECASE
|
|
):
|
|
fail("confluence_secret_material_detected")
|
|
parser = TableParser()
|
|
parser.feed(section)
|
|
if not parser.rows or tuple(parser.rows[0]) != TABLE_HEADERS:
|
|
fail("confluence_table_header_invalid")
|
|
records: list[tuple[str, str, str, str, str]] = []
|
|
principals: set[str] = set()
|
|
for row in parser.rows[1:]:
|
|
if len(row) != 5 or not row[0] or not row[4]:
|
|
fail("confluence_table_row_invalid")
|
|
record = cast(tuple[str, str, str, str, str], tuple(row))
|
|
if record[0] in principals:
|
|
fail("confluence_table_principal_duplicate")
|
|
principals.add(record[0])
|
|
records.append(record)
|
|
return records
|
|
|
|
|
|
def verify_managed_table(storage_body: str, mapping_rows: list[dict[str, str]]) -> None:
|
|
records = parse_managed_table(storage_body)
|
|
expected = {
|
|
(item["principal_ref"], item["principal_alias"], item["token_ref"], "active")
|
|
for item in mapping_rows
|
|
}
|
|
actual = {record[:4] for record in records}
|
|
if len(records) != len(mapping_rows) or actual != expected:
|
|
fail("confluence_table_mismatch")
|
|
|
|
|
|
def ensure_create_preserves_rows(
|
|
existing_records: list[tuple[str, str, str, str, str]],
|
|
mapping_rows: list[dict[str, str]],
|
|
) -> None:
|
|
expected_principals = {item["principal_ref"] for item in mapping_rows}
|
|
existing_principals = {record[0] for record in existing_records}
|
|
if not existing_principals.issubset(expected_principals):
|
|
fail("confluence_create_would_remove_user")
|
|
|
|
|
|
def page_in_folder(client: ConfluenceClient, page: dict[str, Any]) -> bool:
|
|
parent_id = str(page.get("parentId") or "")
|
|
parent_type = str(page.get("parentType") or "")
|
|
seen: set[tuple[str, str]] = set()
|
|
for _ in range(20):
|
|
if parent_id == CONFLUENCE_FOLDER_ID:
|
|
return True
|
|
if not parent_id or (parent_type, parent_id) in seen:
|
|
return False
|
|
seen.add((parent_type, parent_id))
|
|
if parent_type == "page":
|
|
parent = client.request(
|
|
"GET", f"/wiki/api/v2/pages/{urllib.parse.quote(parent_id)}"
|
|
)
|
|
else:
|
|
parent = client.request(
|
|
"GET", f"/wiki/api/v2/folders/{urllib.parse.quote(parent_id)}"
|
|
)
|
|
parent_id = str(parent.get("parentId") or "")
|
|
parent_type = str(parent.get("parentType") or "")
|
|
return False
|
|
|
|
|
|
def exact_title_pages(client: ConfluenceClient, space_id: str) -> list[dict[str, Any]]:
|
|
query = urllib.parse.urlencode(
|
|
{
|
|
"space-id": space_id,
|
|
"title": CONFLUENCE_TITLE,
|
|
"status": "current",
|
|
"limit": 250,
|
|
}
|
|
)
|
|
next_url: str | None = f"/wiki/api/v2/pages?{query}"
|
|
matches: list[dict[str, Any]] = []
|
|
while next_url:
|
|
response = client.request("GET", next_url)
|
|
results = response.get("results")
|
|
if not isinstance(results, list):
|
|
fail("confluence_search_invalid")
|
|
for result in results:
|
|
if (
|
|
isinstance(result, dict)
|
|
and result.get("title") == CONFLUENCE_TITLE
|
|
and result.get("status") == "current"
|
|
):
|
|
matches.append(result)
|
|
links = response.get("_links")
|
|
next_value = links.get("next") if isinstance(links, dict) else None
|
|
next_url = str(next_value) if next_value else None
|
|
return matches
|
|
|
|
|
|
def preflight_confluence(
|
|
root: Path,
|
|
client: ConfluenceClient,
|
|
target_cache: Path,
|
|
*,
|
|
read_only: bool,
|
|
) -> dict[str, Any]:
|
|
client.request("GET", "/wiki/rest/api/user/current")
|
|
folder = client.request("GET", f"/wiki/api/v2/folders/{CONFLUENCE_FOLDER_ID}")
|
|
space_id = str(folder.get("spaceId") or "")
|
|
if not space_id:
|
|
fail("confluence_space_invalid")
|
|
candidates = exact_title_pages(client, space_id)
|
|
valid: list[dict[str, Any]] = []
|
|
for page_candidate in candidates:
|
|
page_id = str(page_candidate.get("id") or "")
|
|
if not page_id:
|
|
continue
|
|
detail = client.request(
|
|
"GET",
|
|
f"/wiki/api/v2/pages/{urllib.parse.quote(page_id)}?body-format=storage",
|
|
)
|
|
if page_in_folder(client, detail):
|
|
valid.append(detail)
|
|
if len(valid) > 1:
|
|
fail("confluence_target_ambiguous")
|
|
if not valid:
|
|
if read_only:
|
|
fail("confluence_target_missing")
|
|
initial = managed_section(
|
|
[], time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
)
|
|
created = client.request(
|
|
"POST",
|
|
"/wiki/api/v2/pages",
|
|
{
|
|
"spaceId": space_id,
|
|
"status": "current",
|
|
"title": CONFLUENCE_TITLE,
|
|
"parentId": CONFLUENCE_FOLDER_ID,
|
|
"body": {"representation": "storage", "value": initial},
|
|
},
|
|
)
|
|
page_id = str(created.get("id") or "")
|
|
if not page_id:
|
|
fail("confluence_create_invalid")
|
|
valid = [
|
|
client.request("GET", f"/wiki/api/v2/pages/{page_id}?body-format=storage")
|
|
]
|
|
if not page_in_folder(client, valid[0]):
|
|
fail("confluence_parent_invalid")
|
|
page = valid[0]
|
|
if page.get("title") != CONFLUENCE_TITLE or page.get("status") != "current":
|
|
fail("confluence_target_invalid")
|
|
confluence_storage(page)
|
|
page_version(page)
|
|
if not read_only:
|
|
canonical = (
|
|
f"{ATLASSIAN_BASE_URL}/wiki/spaces/Lab2/pages/{page['id']}\n".encode()
|
|
)
|
|
target_candidate = write_candidate(target_cache, canonical)
|
|
os.replace(target_candidate, target_cache)
|
|
os.chmod(target_cache, 0o600)
|
|
require_secure_secret(root, target_cache)
|
|
return page
|
|
|
|
|
|
def table_rows_from_store(
|
|
store_entries: dict[str, str], remote_mappings: list[dict[str, Any]]
|
|
) -> list[dict[str, str]]:
|
|
rows: list[dict[str, str]] = []
|
|
for principal_ref, raw_token in store_entries.items():
|
|
matches = [
|
|
item
|
|
for item in remote_mappings
|
|
if str(item.get("principal_ref")) == principal_ref
|
|
]
|
|
if len(matches) != 1:
|
|
fail("store_edge_mapping_mismatch")
|
|
item = matches[0]
|
|
if sha256_text(raw_token) != str(item.get("token_hash_sha256", "")).lower():
|
|
fail("store_edge_hash_mismatch")
|
|
rows.append(
|
|
{
|
|
"principal_ref": principal_ref,
|
|
"principal_alias": str(item.get("principal_alias", "")),
|
|
"token_ref": str(item.get("token_ref", "")),
|
|
}
|
|
)
|
|
if len({row["principal_ref"] for row in rows}) != len(rows):
|
|
fail("store_edge_mapping_duplicate")
|
|
return rows
|
|
|
|
|
|
def sync_confluence(
|
|
client: ConfluenceClient,
|
|
page: dict[str, Any],
|
|
mapping_rows: list[dict[str, str]],
|
|
) -> tuple[int, int]:
|
|
page_id = str(page.get("id") or "")
|
|
latest = client.request(
|
|
"GET", f"/wiki/api/v2/pages/{urllib.parse.quote(page_id)}?body-format=storage"
|
|
)
|
|
if not page_in_folder(client, latest):
|
|
fail("confluence_parent_invalid")
|
|
old_version = page_version(latest)
|
|
old_parent = str(latest.get("parentId") or "")
|
|
old_title = str(latest.get("title") or "")
|
|
old_status = str(latest.get("status") or "")
|
|
if old_title != CONFLUENCE_TITLE or old_status != "current":
|
|
fail("confluence_target_changed")
|
|
old_body = confluence_storage(latest)
|
|
records = parse_managed_table(old_body)
|
|
ensure_create_preserves_rows(records, mapping_rows)
|
|
try:
|
|
verify_managed_table(old_body, mapping_rows)
|
|
return old_version, len(mapping_rows)
|
|
except WorkflowFailure as error:
|
|
if error.code != "confluence_table_mismatch":
|
|
raise
|
|
synchronized_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
replacement = managed_section(mapping_rows, synchronized_at)
|
|
new_body = replace_managed_section(old_body, replacement)
|
|
client.request(
|
|
"PUT",
|
|
f"/wiki/api/v2/pages/{urllib.parse.quote(page_id)}",
|
|
{
|
|
"id": page_id,
|
|
"status": old_status,
|
|
"title": old_title,
|
|
"body": {"representation": "storage", "value": new_body},
|
|
"version": {"number": old_version + 1},
|
|
},
|
|
)
|
|
verified = client.request(
|
|
"GET", f"/wiki/api/v2/pages/{urllib.parse.quote(page_id)}?body-format=storage"
|
|
)
|
|
if (
|
|
str(verified.get("id")) != page_id
|
|
or str(verified.get("title")) != old_title
|
|
or str(verified.get("status")) != old_status
|
|
or str(verified.get("parentId") or "") != old_parent
|
|
or page_version(verified) != old_version + 1
|
|
):
|
|
fail("confluence_reread_mismatch")
|
|
verify_managed_table(confluence_storage(verified), mapping_rows)
|
|
return old_version + 1, len(mapping_rows)
|
|
|
|
|
|
def api_json(
|
|
root: Path,
|
|
profile: dict[str, Any],
|
|
path: str,
|
|
raw_token: str,
|
|
payload: dict[str, Any] | None = None,
|
|
timeout: int = 30,
|
|
) -> dict[str, Any]:
|
|
method = "POST" if payload is not None else "GET"
|
|
try:
|
|
status_code, data = curl_request(
|
|
root,
|
|
method,
|
|
str(profile["openai_base_url"]) + path,
|
|
[f"Authorization: Bearer {raw_token}", "Content-Type: application/json"],
|
|
payload,
|
|
timeout=timeout,
|
|
)
|
|
except CurlTransportFailure:
|
|
fail("openai_smoke_network_failed")
|
|
if status_code < 200 or status_code >= 300:
|
|
fail("openai_smoke_http_failed")
|
|
try:
|
|
value = json.loads(data)
|
|
except json.JSONDecodeError:
|
|
fail("openai_smoke_response_invalid")
|
|
if not isinstance(value, dict):
|
|
fail("openai_smoke_response_invalid")
|
|
return value
|
|
|
|
|
|
def api_smoke(root: Path, profile: dict[str, Any], raw_token: str) -> None:
|
|
if profile["openai_smoke_transport"] == "ssh-loopback":
|
|
remote_call(
|
|
root,
|
|
profile,
|
|
{"action": "api-smoke", "raw_token": raw_token},
|
|
timeout=135,
|
|
)
|
|
return
|
|
if profile["openai_smoke_transport"] != "public-https":
|
|
fail("openai_smoke_transport_invalid")
|
|
models = api_json(root, profile, "/models", raw_token, timeout=15)
|
|
if not isinstance(models.get("data"), list) or not models["data"]:
|
|
fail("openai_models_invalid")
|
|
response = api_json(
|
|
root,
|
|
profile,
|
|
"/chat/completions",
|
|
raw_token,
|
|
{
|
|
"model": profile["smoke_model"],
|
|
"messages": [{"role": "user", "content": "Reply with the single word OK."}],
|
|
"max_tokens": 2048,
|
|
"temperature": 0,
|
|
},
|
|
timeout=120,
|
|
)
|
|
choices = response.get("choices")
|
|
if not isinstance(choices, list) or not choices or not isinstance(choices[0], dict):
|
|
fail("openai_chat_invalid")
|
|
message = choices[0].get("message")
|
|
content = message.get("content") if isinstance(message, dict) else None
|
|
if (
|
|
not isinstance(content, str)
|
|
or not content.strip()
|
|
or not choices[0].get("finish_reason")
|
|
):
|
|
fail("openai_chat_invalid")
|
|
|
|
|
|
def metrics_observed(
|
|
root: Path, profile: dict[str, Any], token_ref: str
|
|
) -> bool | None:
|
|
if profile["metrics_transport"] == "ssh-loopback":
|
|
try:
|
|
result = remote_call(
|
|
root,
|
|
profile,
|
|
{"action": "metrics", "token_ref": token_ref},
|
|
timeout=15,
|
|
)
|
|
except WorkflowFailure:
|
|
return None
|
|
observed = result.get("observed")
|
|
return observed if isinstance(observed, bool) else None
|
|
if profile["metrics_transport"] != "public-http":
|
|
return None
|
|
try:
|
|
with urllib.request.urlopen(str(profile["metrics_url"]), timeout=8) as response:
|
|
metrics = response.read().decode("utf-8", "replace")
|
|
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError):
|
|
return None
|
|
return re.search(r'token_ref="' + re.escape(token_ref) + r'"', metrics) is not None
|
|
|
|
|
|
def tracked_leak_check(root: Path, raw_token: str) -> None:
|
|
files = run_quiet(["git", "ls-files", "-z"], cwd=root)
|
|
if files.returncode != 0:
|
|
fail("tracked_file_list_failed")
|
|
needle = raw_token.encode("utf-8")
|
|
for item in files.stdout.split(b"\0"):
|
|
if not item:
|
|
continue
|
|
path = root / os.fsdecode(item)
|
|
try:
|
|
if path.is_file() and needle in path.read_bytes():
|
|
fail("raw_token_leak_detected")
|
|
except OSError:
|
|
fail("tracked_leak_check_failed")
|
|
|
|
|
|
def copy_once_to_clipboard(raw_token: str, root: Path) -> None:
|
|
copied = run_quiet(
|
|
["/usr/bin/pbcopy"], cwd=root, input_bytes=raw_token.encode("utf-8")
|
|
)
|
|
if copied.returncode != 0:
|
|
fail("clipboard_copy_failed")
|
|
pasted = run_quiet(["/usr/bin/pbpaste"], cwd=root)
|
|
if (
|
|
pasted.returncode != 0
|
|
or hashlib.sha256(pasted.stdout).digest()
|
|
!= hashlib.sha256(raw_token.encode()).digest()
|
|
):
|
|
fail("clipboard_verify_failed")
|
|
|
|
|
|
def load_confluence(
|
|
root: Path, profile: dict[str, Any]
|
|
) -> tuple[ConfluenceClient, Path]:
|
|
token_path = root / "token/.lgu-atlassian-token"
|
|
username_path = root / str(profile["confluence_user_store"])
|
|
target_cache = root / "token/.dev-corp-iop-confluence-target"
|
|
require_secure_secret(root, token_path)
|
|
require_secure_secret(root, username_path)
|
|
token = token_path.read_text(encoding="utf-8").strip()
|
|
username = username_path.read_text(encoding="utf-8").strip()
|
|
if not token or "\n" in token or "\r" in token:
|
|
fail("atlassian_token_invalid")
|
|
if (
|
|
not username
|
|
or "\n" in username
|
|
or "\r" in username
|
|
or not re.fullmatch(r"[^@\s]+@[^@\s]+", username)
|
|
):
|
|
fail("atlassian_user_invalid")
|
|
if target_cache.exists():
|
|
require_secure_secret(root, target_cache)
|
|
elif not git_is_ignored(root, target_cache):
|
|
fail("target_cache_ignore_invalid")
|
|
return ConfluenceClient(root, username, token), target_cache
|
|
|
|
|
|
def preflight(request: dict[str, str]) -> dict[str, Any]:
|
|
root = repo_root()
|
|
profile = load_profile(root, request["env"])
|
|
store_path = root / str(profile["secret_store"])
|
|
require_secure_secret(root, store_path, must_exist=False)
|
|
_store_bytes, store_entries = read_store(store_path)
|
|
remote = remote_call(
|
|
root,
|
|
profile,
|
|
{"action": "inspect", "refresh_probe": True},
|
|
timeout=20,
|
|
)
|
|
mappings = remote.get("mappings")
|
|
if (
|
|
not isinstance(mappings, list)
|
|
or not remote.get("candidate_shape")
|
|
or remote.get("refresh_status") != "applied"
|
|
or not remote.get("healthy")
|
|
or not remote.get("listener")
|
|
):
|
|
fail("edge_preflight_invalid")
|
|
remote_mappings = [item for item in mappings if isinstance(item, dict)]
|
|
confluence_status = "not-applicable"
|
|
if profile["confluence_enabled"]:
|
|
client, target_cache = load_confluence(root, profile)
|
|
page = preflight_confluence(root, client, target_cache, read_only=True)
|
|
current_rows = table_rows_from_store(store_entries, remote_mappings)
|
|
ensure_create_preserves_rows(
|
|
parse_managed_table(confluence_storage(page)), current_rows
|
|
)
|
|
confluence_status = "ready"
|
|
request_state = (
|
|
"existing"
|
|
if select_existing(request, store_entries, remote_mappings)
|
|
else "new"
|
|
)
|
|
return {
|
|
"result": "ready",
|
|
"mode": "preflight",
|
|
"environment": request["env"],
|
|
"edge": "healthy",
|
|
"confluence": confluence_status,
|
|
"stored_user_count": len(store_entries),
|
|
"request_state": request_state,
|
|
"raw_token_generated": False,
|
|
}
|
|
|
|
|
|
def select_existing(
|
|
request: dict[str, str],
|
|
store_entries: dict[str, str],
|
|
remote_mappings: list[dict[str, Any]],
|
|
) -> tuple[dict[str, Any], str] | None:
|
|
principal_matches = [
|
|
item
|
|
for item in remote_mappings
|
|
if str(item.get("principal_ref")) == request["principal_ref"]
|
|
]
|
|
ref_matches = [
|
|
item
|
|
for item in remote_mappings
|
|
if str(item.get("token_ref")) == request["token_ref"]
|
|
]
|
|
local_token = store_entries.get(request["principal_ref"])
|
|
if len(principal_matches) > 1 or len(ref_matches) > 1:
|
|
fail("existing_mapping_ambiguous")
|
|
if ref_matches and (
|
|
not principal_matches or ref_matches[0] is not principal_matches[0]
|
|
):
|
|
fail("token_ref_conflict")
|
|
if principal_matches:
|
|
if local_token is None:
|
|
fail("existing_mapping_missing_local_token")
|
|
mapping = principal_matches[0]
|
|
if (
|
|
sha256_text(local_token)
|
|
!= str(mapping.get("token_hash_sha256", "")).lower()
|
|
):
|
|
fail("existing_mapping_hash_mismatch")
|
|
return mapping, local_token
|
|
if local_token is not None:
|
|
fail("local_token_missing_edge_mapping")
|
|
return None
|
|
|
|
|
|
def execute(request: dict[str, str]) -> dict[str, Any]:
|
|
started = time.monotonic()
|
|
root = repo_root()
|
|
profile = load_profile(root, request["env"])
|
|
store_path = root / str(profile["secret_store"])
|
|
require_secure_secret(root, store_path, must_exist=False)
|
|
old_store_bytes, store_entries = read_store(store_path)
|
|
client: ConfluenceClient | None = None
|
|
target_cache: Path | None = None
|
|
page: dict[str, Any] | None = None
|
|
|
|
print("[1/6] preflight", file=sys.stderr, flush=True)
|
|
if profile["confluence_enabled"]:
|
|
client, target_cache = load_confluence(root, profile)
|
|
page = preflight_confluence(root, client, target_cache, read_only=False)
|
|
remote = remote_call(
|
|
root,
|
|
profile,
|
|
{"action": "inspect", "refresh_probe": True},
|
|
timeout=20,
|
|
)
|
|
mappings_value = remote.get("mappings")
|
|
if (
|
|
not isinstance(mappings_value, list)
|
|
or not remote.get("candidate_shape")
|
|
or remote.get("refresh_status") != "applied"
|
|
or not remote.get("healthy")
|
|
or not remote.get("listener")
|
|
):
|
|
fail("edge_preflight_invalid")
|
|
remote_mappings = [item for item in mappings_value if isinstance(item, dict)]
|
|
if page is not None:
|
|
current_rows = table_rows_from_store(store_entries, remote_mappings)
|
|
ensure_create_preserves_rows(
|
|
parse_managed_table(confluence_storage(page)), current_rows
|
|
)
|
|
existing = select_existing(request, store_entries, remote_mappings)
|
|
|
|
edge_state = "existing-verified"
|
|
backup_path: str | None = None
|
|
raw_token: str
|
|
if existing:
|
|
mapping, raw_token = existing
|
|
request["principal_alias"] = str(
|
|
mapping.get("principal_alias") or request["principal_alias"]
|
|
)
|
|
request["token_ref"] = str(mapping.get("token_ref") or request["token_ref"])
|
|
print("[2/6] existing mapping reused", file=sys.stderr, flush=True)
|
|
else:
|
|
print("[2/6] token generated in memory", file=sys.stderr, flush=True)
|
|
raw_token = "iop_" + secrets.token_urlsafe(36)
|
|
token_hash = sha256_text(raw_token)
|
|
new_content = old_store_bytes
|
|
if new_content and not new_content.endswith(b"\n"):
|
|
new_content += b"\n"
|
|
new_content += f"{request['principal_ref']}: {raw_token}\n".encode()
|
|
local_candidate = write_candidate(store_path, new_content)
|
|
try:
|
|
print("[3/6] Edge transaction", file=sys.stderr, flush=True)
|
|
activated = remote_call(
|
|
root,
|
|
profile,
|
|
{
|
|
"action": "apply",
|
|
"entry": {
|
|
"token_ref": request["token_ref"],
|
|
"token_hash_sha256": token_hash,
|
|
"principal_ref": request["principal_ref"],
|
|
"principal_alias": request["principal_alias"],
|
|
},
|
|
},
|
|
timeout=90,
|
|
)
|
|
backup_path = str(activated.get("backup_path") or "")
|
|
if not backup_path:
|
|
fail("edge_backup_reference_missing")
|
|
os.replace(local_candidate, store_path)
|
|
os.chmod(store_path, 0o600)
|
|
require_secure_secret(root, store_path)
|
|
edge_state = "activated"
|
|
except BaseException:
|
|
if backup_path:
|
|
rollback = remote_call(
|
|
root,
|
|
profile,
|
|
{"action": "rollback", "backup_path": backup_path},
|
|
timeout=70,
|
|
)
|
|
if rollback.get("status") != "rolled_back":
|
|
fail("edge_rollback_unconfirmed")
|
|
atomic_restore(store_path, old_store_bytes if old_store_bytes else None)
|
|
raise
|
|
finally:
|
|
local_candidate.unlink(missing_ok=True)
|
|
|
|
print("[4/6] environment API smoke", file=sys.stderr, flush=True)
|
|
try:
|
|
api_smoke(root, profile, raw_token)
|
|
except WorkflowFailure:
|
|
if backup_path:
|
|
rollback = remote_call(
|
|
root,
|
|
profile,
|
|
{"action": "rollback", "backup_path": backup_path},
|
|
timeout=70,
|
|
)
|
|
if rollback.get("status") != "rolled_back":
|
|
fail("edge_rollback_unconfirmed")
|
|
atomic_restore(store_path, old_store_bytes if old_store_bytes else None)
|
|
raise
|
|
|
|
version: int | None = None
|
|
confluence_status = "not-applicable"
|
|
_current_bytes, current_store = read_store(store_path)
|
|
row_count = len(current_store)
|
|
if client is not None and page is not None:
|
|
print("[5/6] Confluence metadata sync", file=sys.stderr, flush=True)
|
|
current_remote = remote_call(root, profile, {"action": "inspect"}, timeout=20)
|
|
current_values = current_remote.get("mappings")
|
|
if not isinstance(current_values, list):
|
|
fail("edge_mapping_reread_invalid")
|
|
rows = table_rows_from_store(
|
|
current_store, [item for item in current_values if isinstance(item, dict)]
|
|
)
|
|
version, row_count = sync_confluence(client, page, rows)
|
|
confluence_status = "updated"
|
|
else:
|
|
print("[5/6] Confluence not applicable", file=sys.stderr, flush=True)
|
|
|
|
print("[6/6] leak check and one-time delivery", file=sys.stderr, flush=True)
|
|
tracked_leak_check(root, raw_token)
|
|
require_secure_secret(root, store_path)
|
|
if target_cache is not None:
|
|
require_secure_secret(root, target_cache)
|
|
copy_once_to_clipboard(raw_token, root)
|
|
observed = metrics_observed(root, profile, request["token_ref"])
|
|
return {
|
|
"result": "completed",
|
|
"operation": "create",
|
|
"environment": request["env"],
|
|
"principal_alias": request["principal_alias"],
|
|
"token_ref": request["token_ref"],
|
|
"edge_mapping": edge_state,
|
|
"api_smoke": "passed",
|
|
"confluence_metadata_sync": confluence_status,
|
|
"confluence_version": version,
|
|
"metadata_row_count": row_count,
|
|
"metrics_observed": observed,
|
|
"raw_token_delivered_once": True,
|
|
"delivery_channel": "local_clipboard",
|
|
"leak_check": "passed",
|
|
"elapsed_seconds": round(time.monotonic() - started, 1),
|
|
}
|
|
|
|
|
|
def selftest() -> dict[str, Any]:
|
|
sample = normalize_alias("A.Name+test@example.invalid", None)
|
|
if sample != "a-name-test":
|
|
fail("selftest_alias_failed")
|
|
if normalize_alias("a@example.invalid", None) != "a":
|
|
fail("selftest_short_alias_failed")
|
|
try:
|
|
parse_request('{"env":"dev","principal_ref":null}')
|
|
except WorkflowFailure as error:
|
|
if error.code != "principal_ref_invalid":
|
|
raise
|
|
else:
|
|
fail("selftest_input_type_failed")
|
|
try:
|
|
parse_request('{"principal_ref":"sample@example.invalid"}')
|
|
except WorkflowFailure as error:
|
|
if error.code != "environment_not_supported":
|
|
raise
|
|
else:
|
|
fail("selftest_environment_required_failed")
|
|
root = repo_root()
|
|
dev_request = parse_request(
|
|
'{"env":"dev","principal_ref":"sample@example.invalid"}'
|
|
)
|
|
corp_request = parse_request(
|
|
'{"env":"dev-corp","principal_ref":"sample@example.invalid"}'
|
|
)
|
|
if dev_request["token_ref"] != "iop-dev-sample":
|
|
fail("selftest_dev_token_ref_failed")
|
|
if corp_request["token_ref"] != "iop-dev-corp-sample":
|
|
fail("selftest_dev_corp_token_ref_failed")
|
|
for environment in SUPPORTED_ENVIRONMENTS:
|
|
profile = load_profile(root, environment)
|
|
if profile["environment"] != environment:
|
|
fail("selftest_profile_failed")
|
|
rows = [
|
|
{
|
|
"principal_ref": "sample@example.invalid",
|
|
"principal_alias": "sample",
|
|
"token_ref": "iop-dev-corp-sample",
|
|
}
|
|
]
|
|
original = "<h1>Intro</h1><p>Keep</p><h2>IOP 사용자 토큰 발급 현황</h2><p>old</p><h2>Next</h2><p>Keep too</p>"
|
|
replaced = replace_managed_section(
|
|
original, managed_section(rows, "2026-01-01T00:00:00Z")
|
|
)
|
|
verify_managed_table(replaced, rows)
|
|
if "<p>Keep</p>" not in replaced or "<p>Keep too</p>" not in replaced:
|
|
fail("selftest_section_failed")
|
|
records = parse_managed_table(replaced)
|
|
try:
|
|
ensure_create_preserves_rows(records, [])
|
|
except WorkflowFailure as error:
|
|
if error.code != "confluence_create_would_remove_user":
|
|
raise
|
|
else:
|
|
fail("selftest_append_only_failed")
|
|
helper = (
|
|
root
|
|
/ "agent-ops/skills/project/openai-usage-token-issue/scripts/remote_edge_transaction.rb"
|
|
)
|
|
for environment in SUPPORTED_ENVIRONMENTS:
|
|
ruby = run_quiet(
|
|
["ruby", str(helper)],
|
|
cwd=root,
|
|
input_bytes=json.dumps(
|
|
{"action": "selftest", "environment": environment},
|
|
separators=(",", ":"),
|
|
).encode(),
|
|
timeout=10,
|
|
)
|
|
parsed = None
|
|
for line in reversed(ruby.stdout.decode("utf-8", "replace").splitlines()):
|
|
try:
|
|
value = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
if isinstance(value, dict):
|
|
parsed = value
|
|
break
|
|
if ruby.returncode != 0 or not parsed or parsed.get("status") != "ok":
|
|
fail("selftest_remote_helper_failed")
|
|
return {"result": "passed", "mode": "self-test", "network_used": False}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Issue one dev or dev-corp IOP user token from stdin JSON."
|
|
)
|
|
mode = parser.add_mutually_exclusive_group()
|
|
mode.add_argument(
|
|
"--preflight", action="store_true", help="Run read-only live readiness checks."
|
|
)
|
|
mode.add_argument(
|
|
"--self-test", action="store_true", help="Run local deterministic tests."
|
|
)
|
|
arguments = parser.parse_args()
|
|
try:
|
|
if arguments.self_test:
|
|
result = selftest()
|
|
else:
|
|
request = parse_request(sys.stdin.read())
|
|
root = repo_root()
|
|
with local_transaction_lock(root, shared=arguments.preflight):
|
|
result = preflight(request) if arguments.preflight else execute(request)
|
|
print(json.dumps(result, ensure_ascii=False, separators=(",", ":")))
|
|
return 0
|
|
except WorkflowFailure as error:
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"result": "blocked",
|
|
"code": error.code,
|
|
"raw_token_reported": False,
|
|
"retry_same_command": True,
|
|
},
|
|
ensure_ascii=False,
|
|
separators=(",", ":"),
|
|
)
|
|
)
|
|
return 2
|
|
except KeyboardInterrupt:
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"result": "blocked",
|
|
"code": "interrupted",
|
|
"raw_token_reported": False,
|
|
"retry_same_command": True,
|
|
},
|
|
separators=(",", ":"),
|
|
)
|
|
)
|
|
return 130
|
|
except Exception: # noqa: BLE001 - never render a traceback that could retain secret request state.
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"result": "blocked",
|
|
"code": "unexpected_local_failure",
|
|
"raw_token_reported": False,
|
|
"retry_same_command": True,
|
|
},
|
|
separators=(",", ":"),
|
|
)
|
|
)
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|