Files
.pouch/skills/ack/scripts/feishu_bug_intake.py
T

854 lines
37 KiB
Python

#!/usr/bin/env python3
"""Read and review an ACK-ready Feishu Base view through the official lark-cli.
The only mutation is a bounded draft write to configured Coordinator fields.
The adapter never reads the active profile and emits one JSON document only
on success.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import math
import os
import pwd
import re
import resource
import signal
import stat
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from typing import Any
from approval_payload import approval_payload_hash, review_items
from yaml_subset import DuplicateKeyError, YamlSubsetError, load_json_unique, load_yaml_subset, make_unique_pyyaml_loader
REQUIRED_FIELDS = ("title", "actual", "expected", "stepsToReproduce", "acceptance", "priority", "attachments", "updatedAt")
OPTIONAL_FIELDS = ("fixLogic",)
SOURCE_FACT_FIELDS = ("title", "actual", "expected", "updatedAt")
COORDINATOR_FIELDS = ("steps", "acceptance", "priority")
BUG_CONTENT_FIELDS = ("title", "actual", "expected", "fixLogic", *COORDINATOR_FIELDS)
MAX_PAGES = 100
MAX_RECORDS = 10_000
PAGE_SIZE = 100
MAX_CLI_STDOUT = 1024 * 1024
MAX_CLI_STDERR = 64 * 1024
MAX_ATTACHMENTS_PER_RECORD = 10
MAX_TOTAL_ATTACHMENTS = 100
MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024
MAX_TOTAL_ATTACHMENT_BYTES = 200 * 1024 * 1024
MAX_ATTACHMENT_BATCH_SECONDS = 300
MAX_DRAFT_BYTES = 64 * 1024
SAFE_VALUE = re.compile(r"^[^\s\x00-\x1f]{1,256}$")
PROFILE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
RECORD_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$")
SOURCE_REF = re.compile(r"^feishu-base:sha256:[0-9a-f]{64}$")
DRAFT_REVISION = re.compile(r"^sha256:[0-9a-f]{64}$")
WORKFLOWS = {"read-only-v1", "reviewed-writeback-v1"}
class IntakeError(Exception):
pass
def account_home() -> Path:
"""Return the actual account home, never a caller-controlled HOME value."""
try:
home = Path(pwd.getpwuid(os.getuid()).pw_dir).resolve(strict=True)
except (KeyError, OSError) as exc:
raise IntakeError("cannot resolve current account home") from exc
if not home.is_dir():
raise IntakeError("current account home is not a directory")
return home
def trusted_lark_cli_dirs() -> list[Path]:
"""Fixed account and system locations; intentionally never consult PATH."""
home = account_home()
candidates = (
home / ".local" / "bin",
home / ".local" / "share" / "mise" / "shims",
home / ".cargo" / "bin",
Path("/home/linuxbrew/.linuxbrew/bin"),
Path("/usr/local/go/bin"),
Path("/usr/local/bin"),
Path("/usr/bin"),
Path("/bin"),
)
result: list[Path] = []
for candidate in candidates:
try:
resolved = candidate.resolve(strict=True)
except OSError:
continue
if resolved.is_dir() and resolved not in result:
result.append(resolved)
return result
def resolve_lark_cli() -> Path:
"""Resolve a safe lark-cli from the fixed trusted locations only."""
for directory in trusted_lark_cli_dirs():
candidate = directory / "lark-cli"
try:
candidate_metadata = os.lstat(candidate)
resolved = candidate.resolve(strict=True)
metadata = resolved.stat()
except OSError:
continue
if not (stat.S_ISREG(candidate_metadata.st_mode) or stat.S_ISLNK(candidate_metadata.st_mode)):
continue
if candidate_metadata.st_uid not in {0, os.getuid()}:
continue
if not stat.S_ISREG(metadata.st_mode) or not os.access(resolved, os.X_OK):
continue
if metadata.st_uid not in {0, os.getuid()} or stat.S_IMODE(metadata.st_mode) & 0o022:
continue
if resolved.name == "lark-cli":
return resolved
if not stat.S_ISLNK(candidate_metadata.st_mode):
continue
official_binary = official_npm_binary(resolved)
if official_binary is not None:
return official_binary
raise IntakeError("lark-cli is not installed in a trusted account or system directory")
def official_npm_binary(path: Path) -> Path | None:
"""Resolve a validated npm wrapper to its downloaded native CLI binary."""
if path.name != "run.js" or path.parent.name != "scripts":
return None
manifest = path.parent.parent / "package.json"
try:
metadata = manifest.stat()
if not stat.S_ISREG(metadata.st_mode):
return None
if metadata.st_uid not in {0, os.getuid()} or stat.S_IMODE(metadata.st_mode) & 0o022:
return None
package = json.loads(manifest.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
return None
if not (
isinstance(package, dict)
and package.get("name") == "@larksuite/cli"
and isinstance(package.get("bin"), dict)
and package["bin"].get("lark-cli") == "scripts/run.js"
):
return None
native = path.parent.parent / "bin" / "lark-cli"
try:
native_lstat = os.lstat(native)
resolved = native.resolve(strict=True)
metadata = resolved.stat()
except OSError:
return None
if not stat.S_ISREG(native_lstat.st_mode) or resolved.name != "lark-cli":
return None
if not stat.S_ISREG(metadata.st_mode) or not os.access(resolved, os.X_OK):
return None
if metadata.st_uid not in {0, os.getuid()} or stat.S_IMODE(metadata.st_mode) & 0o022:
return None
return resolved
def cli_environment() -> dict[str, str]:
"""Build a minimal environment so env credentials cannot override `--profile`."""
environment = {
"HOME": str(account_home()),
"PATH": os.pathsep.join(str(path) for path in trusted_lark_cli_dirs()),
}
for name in ("LANG", "LC_ALL", "LC_CTYPE"):
value = os.environ.get(name)
if value and "\x00" not in value and len(value) <= 256:
environment[name] = value
return environment
def load_board(path: Path) -> dict[str, Any]:
try:
text = path.read_text(encoding="utf-8")
if path.suffix.lower() == ".json":
value = load_json_unique(text)
else:
try:
import yaml # type: ignore
value = yaml.load(text, Loader=make_unique_pyyaml_loader(yaml))
except ImportError:
value = load_yaml_subset(text)
except (OSError, json.JSONDecodeError, DuplicateKeyError, YamlSubsetError) as exc:
raise IntakeError(f"cannot read task board: {exc}") from exc
except Exception as exc: # PyYAML errors are intentionally not exposed verbatim.
raise IntakeError("cannot parse task board") from exc
if not isinstance(value, dict):
raise IntakeError("task board must be an object")
return value
def load_draft(path: Path) -> dict[str, Any]:
"""Load one bounded, regular JSON file with the two writable draft fields."""
descriptor: int | None = None
try:
before = path.lstat()
if not stat.S_ISREG(before.st_mode) or path.is_symlink():
raise IntakeError("draft input must be a regular file")
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(path, flags)
metadata = os.fstat(descriptor)
if (
not stat.S_ISREG(metadata.st_mode)
or (before.st_dev, before.st_ino) != (metadata.st_dev, metadata.st_ino)
):
raise IntakeError("draft input changed while opening")
if metadata.st_size <= 0 or metadata.st_size > MAX_DRAFT_BYTES:
raise IntakeError("draft input size is invalid")
chunks: list[bytes] = []
total = 0
while total <= MAX_DRAFT_BYTES:
chunk = os.read(descriptor, min(64 * 1024, MAX_DRAFT_BYTES + 1 - total))
if not chunk:
break
chunks.append(chunk)
total += len(chunk)
content = b"".join(chunks)
if len(content) != metadata.st_size:
raise IntakeError("draft input changed while reading")
value = load_json_unique(content.decode("utf-8"))
except IntakeError:
raise
except (OSError, UnicodeError, json.JSONDecodeError, DuplicateKeyError) as exc:
raise IntakeError("cannot read draft input") from exc
finally:
if descriptor is not None:
os.close(descriptor)
if not isinstance(value, dict) or set(value) != {"fixLogic", "acceptance"}:
raise IntakeError("draft input must contain exactly fixLogic and acceptance")
fix_logic = value["fixLogic"]
acceptance = value["acceptance"]
if not isinstance(fix_logic, str) or not fix_logic.strip():
raise IntakeError("draft fixLogic must be a non-empty string")
if (
not isinstance(acceptance, list)
or not acceptance
or any(not isinstance(item, str) or not item.strip() for item in acceptance)
):
raise IntakeError("draft acceptance must be a non-empty string list")
return {
"fixLogic": fix_logic.strip(),
"acceptance": [item.strip() for item in acceptance],
}
def config_from_board(board: dict[str, Any]) -> dict[str, Any]:
project = board.get("project")
if not isinstance(project, dict) or "bugIntake" not in project:
raise IntakeError("project.bugIntake is not configured")
config = project["bugIntake"]
if not isinstance(config, dict):
raise IntakeError("project.bugIntake must be an object")
allowed = {"provider", "workflow", "profile", "baseToken", "tableId", "viewId", "fields"}
unknown = sorted(set(config) - allowed)
if unknown:
raise IntakeError("bugIntake has unknown fields")
if config.get("provider") != "feishu-base":
raise IntakeError("bugIntake.provider must be feishu-base")
workflow = config.get("workflow", "read-only-v1")
if workflow not in WORKFLOWS:
raise IntakeError("bugIntake.workflow is invalid")
profile = config.get("profile")
if not isinstance(profile, str) or not PROFILE.fullmatch(profile):
raise IntakeError("bugIntake.profile is invalid")
for key in ("baseToken", "tableId", "viewId"):
value = config.get(key)
if not isinstance(value, str) or not SAFE_VALUE.fullmatch(value):
raise IntakeError(f"bugIntake.{key} is invalid")
fields = config.get("fields")
supported = set(REQUIRED_FIELDS) | set(OPTIONAL_FIELDS)
if (
not isinstance(fields, dict)
or not set(REQUIRED_FIELDS).issubset(fields)
or not set(fields).issubset(supported)
):
raise IntakeError("bugIntake.fields must map all required and only supported logical fields")
if any(not isinstance(value, str) or not SAFE_VALUE.fullmatch(value) for value in fields.values()):
raise IntakeError("bugIntake.fields values are invalid")
if len(set(fields.values())) != len(fields):
raise IntakeError("bugIntake.fields values must be unique")
if workflow == "reviewed-writeback-v1" and "fixLogic" not in fields:
raise IntakeError("reviewed writeback requires bugIntake.fields.fixLogic")
return config
def limit_child_file_size(limit: int) -> None:
"""Bound regular-file writes by the CLI and any child spawned by its wrapper."""
_, hard = resource.getrlimit(resource.RLIMIT_FSIZE)
bounded = limit if hard == resource.RLIM_INFINITY else min(limit, hard)
resource.setrlimit(resource.RLIMIT_FSIZE, (bounded, bounded))
def run_cli(
args: list[str], *, allow_profile_list: bool = False, max_file_bytes: int = 0,
timeout: float = 60, cwd: Path | None = None,
) -> Any:
"""Run the official CLI and accept only explicit successful JSON shapes."""
executable = resolve_lark_cli()
file_limit = max(MAX_CLI_STDOUT, MAX_CLI_STDERR, max_file_bytes)
with tempfile.TemporaryFile() as stdout_file, tempfile.TemporaryFile() as stderr_file:
try:
process = subprocess.Popen(
[str(executable), *args],
shell=False,
stdin=subprocess.DEVNULL,
stdout=stdout_file,
stderr=stderr_file,
env=cli_environment(),
cwd=cwd,
start_new_session=True,
preexec_fn=lambda: limit_child_file_size(file_limit),
)
try:
returncode = process.wait(timeout=timeout)
except subprocess.TimeoutExpired as exc:
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
pass
process.wait()
raise IntakeError("lark-cli failed to execute") from exc
except (OSError, subprocess.SubprocessError) as exc:
raise IntakeError("lark-cli failed to execute") from exc
stdout_size = os.fstat(stdout_file.fileno()).st_size
stderr_size = os.fstat(stderr_file.fileno()).st_size
if stdout_size > MAX_CLI_STDOUT or stderr_size > MAX_CLI_STDERR:
raise IntakeError("lark-cli output exceeded the safety limit")
if returncode:
raise IntakeError("lark-cli command failed")
stdout_file.seek(0)
try:
stdout = stdout_file.read(MAX_CLI_STDOUT + 1).decode("utf-8")
except UnicodeDecodeError as exc:
raise IntakeError("lark-cli returned malformed JSON") from exc
try:
value = json.loads(
stdout,
parse_constant=lambda value: (_ for _ in ()).throw(
ValueError(f"non-finite JSON constant: {value}")
),
)
except (json.JSONDecodeError, ValueError) as exc:
raise IntakeError("lark-cli returned malformed JSON") from exc
if isinstance(value, list):
if allow_profile_list:
return value
raise IntakeError("lark-cli returned an unexpected JSON array")
if not isinstance(value, dict):
raise IntakeError("lark-cli returned an invalid JSON response")
if "ok" in value and value["ok"] is not True:
raise IntakeError("lark-cli returned an error response")
if "code" in value and (not isinstance(value["code"], int) or isinstance(value["code"], bool) or value["code"] != 0):
raise IntakeError("lark-cli returned an error response")
if "ok" not in value and "code" not in value:
raise IntakeError("lark-cli returned an ambiguous JSON response")
return value
def profile_check(config: dict[str, Any]) -> None:
# `profile list` is the official non-mutating profile inspection command.
value = run_cli(["profile", "list"], allow_profile_list=True)
if isinstance(value, list):
profiles = value
else:
# Compatibility wrapper: only a successful envelope with a direct list
# is accepted. Do not loosen this into arbitrary nested objects.
profiles = value.get("data")
if not isinstance(profiles, list):
raise IntakeError("profile check returned an invalid response")
matching_profile: dict[str, Any] | None = None
for item in profiles:
# Match the official profile-list item shape. `user` and
# `tokenStatus` are optional and deliberately never propagated.
if (
not isinstance(item, dict)
or not isinstance(item.get("name"), str)
or not PROFILE.fullmatch(item["name"])
or not isinstance(item.get("appId"), str)
or not isinstance(item.get("brand"), str)
or not isinstance(item.get("active"), bool)
):
raise IntakeError("profile check returned an invalid profile entry")
if item["name"] == config["profile"]:
matching_profile = item
if matching_profile is None:
raise IntakeError("configured lark-cli profile does not exist")
if matching_profile["brand"] != "feishu":
raise IntakeError("configured lark-cli profile must use the feishu brand")
def text(value: Any) -> str:
if value is None:
return ""
if isinstance(value, str):
return " ".join(value.split())
if isinstance(value, float) and not math.isfinite(value):
raise IntakeError("text field contains a non-finite number")
if isinstance(value, (int, float, bool)):
return str(value)
if isinstance(value, list):
return "\n".join(part for part in (text(item) for item in value) if part)
if isinstance(value, dict):
for key in ("text", "name", "value"):
if key in value:
return text(value[key])
raise IntakeError("text field contains an unsupported object")
raise IntakeError("text field contains an unsupported value")
def attachment_items(value: Any) -> list[tuple[dict[str, Any], str]]:
if value in (None, ""):
return []
if not isinstance(value, list):
raise IntakeError("attachments cell must be a list")
if len(value) > MAX_ATTACHMENTS_PER_RECORD:
raise IntakeError("record exceeded the attachment count limit")
attachments: list[tuple[dict[str, Any], str]] = []
for item in value:
if not isinstance(item, dict):
raise IntakeError("attachment metadata must be an object")
token = item.get("file_token", item.get("token"))
if not isinstance(token, str) or not SAFE_VALUE.fullmatch(token):
raise IntakeError("attachment token is invalid")
metadata = {"name": text(item.get("name")), "type": text(item.get("type", item.get("mime_type"))), "size": item.get("size")}
if (
not isinstance(metadata["size"], int)
or isinstance(metadata["size"], bool)
or metadata["size"] < 0
or metadata["size"] > MAX_ATTACHMENT_BYTES
):
raise IntakeError("attachment size is invalid")
attachments.append((metadata, token))
return attachments
def matrix_from_response(response: dict[str, Any], field_ids: list[str]) -> tuple[list[str], list[list[Any]]]:
data = response.get("data", response)
if not isinstance(data, dict):
raise IntakeError("record list data is invalid")
fields = data.get("fields")
ids = data.get("record_id_list", data.get("recordIds"))
rows = data.get("data", data.get("records", data.get("items", data.get("rows"))))
if not isinstance(fields, list) or not all(isinstance(x, str) for x in fields):
raise IntakeError("record list fields are invalid")
if fields != field_ids:
raise IntakeError("record list fields do not match configured projection")
if not isinstance(ids, list) or not all(isinstance(x, str) and RECORD_ID.fullmatch(x) for x in ids):
raise IntakeError("record list record_id_list is invalid")
if not isinstance(rows, list) or len(rows) != len(ids) or any(not isinstance(row, list) or len(row) != len(fields) for row in rows):
raise IntakeError("record list matrix does not match fields and record_id_list")
return ids, rows
def fetch_pages(config: dict[str, Any]) -> list[tuple[str, list[Any]]]:
logical_fields = [
logical for logical in (*REQUIRED_FIELDS, *OPTIONAL_FIELDS)
if logical in config["fields"]
]
field_ids = [config["fields"][logical] for logical in logical_fields]
all_rows: list[tuple[str, list[Any]]] = []
offset = 0
for _ in range(MAX_PAGES):
args = ["base", "+record-list", "--profile", config["profile"], "--base-token", config["baseToken"], "--table-id", config["tableId"], "--view-id", config["viewId"], "--format", "json", "--offset", str(offset), "--limit", str(PAGE_SIZE)]
for field_id in field_ids:
args.extend(["--field-id", field_id])
response = run_cli(args)
ids, rows = matrix_from_response(response, field_ids)
if len(ids) > PAGE_SIZE:
raise IntakeError("record list exceeded requested page size")
all_rows.extend(zip(ids, rows))
if len(all_rows) > MAX_RECORDS:
raise IntakeError("record list exceeded record limit")
data = response.get("data", response)
has_more = data.get("has_more", data.get("hasMore", False))
if not isinstance(has_more, bool):
raise IntakeError("record list pagination marker is invalid")
if not has_more:
return all_rows
if not ids:
raise IntakeError("record list pagination made no progress")
offset += len(ids)
raise IntakeError("record list exceeded page limit")
def download(
config: dict[str, Any], record_id: str, file_token: str,
output_dir: Path, expected_size: int, timeout: float,
) -> str:
try:
output_dir.mkdir(parents=True, exist_ok=False)
except OSError as exc:
raise IntakeError("attachment output directory is unsafe") from exc
if not output_dir.is_dir() or output_dir.is_symlink():
raise IntakeError("attachment output directory is unsafe")
run_cli(
["base", "+record-download-attachment", "--profile", config["profile"], "--base-token", config["baseToken"], "--table-id", config["tableId"], "--record-id", record_id, "--file-token", file_token, "--output", output_dir.name],
max_file_bytes=expected_size,
timeout=timeout,
cwd=output_dir.parent,
)
try:
created = list(output_dir.iterdir())
except OSError as exc:
raise IntakeError("attachment download output is unreadable") from exc
if len(created) != 1 or not created[0].is_file() or created[0].is_symlink():
raise IntakeError("attachment download did not produce one safe file")
root = output_dir.resolve()
resolved = created[0].resolve()
if root not in resolved.parents:
raise IntakeError("attachment download escaped output directory")
if resolved.stat().st_size != expected_size:
raise IntakeError("attachment download size did not match metadata")
return str(resolved)
def source_ref(config: dict[str, Any], record_id: str) -> str:
"""Return a stable opaque identity without serializing configured identifiers."""
identity = "\x1f".join(("ack-feishu-base-source-ref-v1", config["profile"], config["baseToken"], config["tableId"], record_id))
return f"feishu-base:sha256:{hashlib.sha256(identity.encode('utf-8')).hexdigest()}"
def draft_revision(
record: dict[str, Any], attachment_tokens: list[str] | None = None,
) -> str:
"""Bind approval to the normalized source facts and review-controlled fields."""
tokens = attachment_tokens or []
if len(tokens) != len(record["attachments"]):
raise IntakeError("draft revision attachment identity is incomplete")
stable = {
"sourceRef": record["sourceRef"],
"updatedAt": record["updatedAt"],
"title": record["title"],
"actual": record["actual"],
"expected": record["expected"],
"steps": record["steps"],
"fixLogic": record["fixLogic"],
"acceptance": record["acceptance"],
"priority": record["priority"],
"attachments": [
{
**{key: attachment.get(key) for key in ("name", "type", "size")},
"tokenDigest": f"sha256:{hashlib.sha256(('ack-feishu-attachment-v1\x1f' + token).encode('utf-8')).hexdigest()}",
}
for attachment, token in zip(record["attachments"], tokens)
],
}
encoded = json.dumps(
stable, ensure_ascii=False, sort_keys=True, separators=(",", ":"),
).encode("utf-8")
return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
def fetch(config: dict[str, Any], output_dir: Path | None) -> dict[str, Any]:
profile_check(config)
prepared: list[tuple[dict[str, Any], list[tuple[dict[str, Any], str]]]] = []
batch_warnings: list[dict[str, str]] = []
total_attachments = 0
total_attachment_bytes = 0
for record_id, row in fetch_pages(config):
logical_fields = [
logical for logical in (*REQUIRED_FIELDS, *OPTIONAL_FIELDS)
if logical in config["fields"]
]
cells = dict(zip(logical_fields, row))
attachment_data = attachment_items(cells["attachments"])
total_attachments += len(attachment_data)
total_attachment_bytes += sum(metadata["size"] for metadata, _ in attachment_data)
if total_attachments > MAX_TOTAL_ATTACHMENTS:
raise IntakeError("batch exceeded the attachment count limit")
if total_attachment_bytes > MAX_TOTAL_ATTACHMENT_BYTES:
raise IntakeError("batch exceeded the attachment byte limit")
record = {"sourceRef": source_ref(config, record_id), "recordId": record_id, "updatedAt": text(cells["updatedAt"]), "title": text(cells["title"]), "actual": text(cells["actual"]), "expected": text(cells["expected"]), "steps": text(cells["stepsToReproduce"]), "fixLogic": text(cells.get("fixLogic")), "acceptance": text(cells["acceptance"]), "priority": text(cells["priority"]), "attachments": [metadata for metadata, _ in attachment_data], "warnings": []}
if not attachment_data and not any(record[field] for field in BUG_CONTENT_FIELDS):
batch_warnings.append({"recordId": record_id, "code": "blank_record_skipped"})
continue
for field in SOURCE_FACT_FIELDS:
if not record[field]:
raise IntakeError(f"record {field} must not be empty")
enrichment_fields = list(COORDINATOR_FIELDS)
if "fixLogic" in config["fields"]:
enrichment_fields.append("fixLogic")
record["enrichmentRequired"] = [
field for field in enrichment_fields if not record[field]
]
record["draftRevision"] = draft_revision(
record, [token for _, token in attachment_data],
)
prepared.append((record, attachment_data))
download_root: Path | None = None
if output_dir is not None:
try:
output_dir.mkdir(parents=True, exist_ok=True)
if not output_dir.is_dir() or output_dir.is_symlink():
raise OSError("unsafe output directory")
download_root = output_dir.resolve(strict=True)
except OSError as exc:
raise IntakeError("attachment output directory is unsafe") from exc
records = []
attachment_deadline = time.monotonic() + MAX_ATTACHMENT_BATCH_SECONDS
for record, attachment_data in prepared:
if download_root is not None:
for index, (attachment, file_token) in enumerate(attachment_data, start=1):
remaining = attachment_deadline - time.monotonic()
if remaining <= 0:
raise IntakeError("attachment batch exceeded the time limit")
attachment_dir = download_root / record["recordId"] / f"attachment-{index:02d}"
attachment["localPath"] = download(
config, record["recordId"], file_token, attachment_dir,
attachment["size"], min(60, remaining),
)
records.append(record)
return {"provider": "feishu-base", "workflow": config.get("workflow", "read-only-v1"), "profile": config["profile"], "tableId": config["tableId"], "viewId": config["viewId"], "records": records, "warnings": batch_warnings}
def review_record(
config: dict[str, Any], record_id: str, expected_source_ref: str,
expected_revision: str,
) -> dict[str, Any]:
"""Resolve one record inside the configured view and bind its reviewed version."""
if RECORD_ID.fullmatch(record_id) is None:
raise IntakeError("review record id is invalid")
if SOURCE_REF.fullmatch(expected_source_ref) is None:
raise IntakeError("expected source reference is invalid")
if DRAFT_REVISION.fullmatch(expected_revision) is None:
raise IntakeError("expected draft revision is invalid")
matching = [
record for record in fetch(config, None)["records"]
if record["recordId"] == record_id
]
if len(matching) != 1:
raise IntakeError("configured review view did not contain exactly one record")
if (
matching[0]["sourceRef"] != expected_source_ref
or matching[0]["draftRevision"] != expected_revision
):
raise IntakeError("review record changed before the requested operation")
return matching[0]
def write_draft(
config: dict[str, Any], record_id: str, expected_source_ref: str,
expected_revision: str, draft_path: Path,
) -> dict[str, Any]:
"""Overwrite only the configured fix logic and acceptance cells."""
if config.get("workflow", "read-only-v1") != "reviewed-writeback-v1":
raise IntakeError("draft writeback requires reviewed-writeback-v1 workflow")
if "fixLogic" not in config["fields"]:
raise IntakeError("bugIntake.fields.fixLogic is required for draft writeback")
review_record(config, record_id, expected_source_ref, expected_revision)
draft = load_draft(draft_path)
patch = {
config["fields"]["fixLogic"]: draft["fixLogic"],
config["fields"]["acceptance"]: "\n".join(
f"{index}. {item}" for index, item in enumerate(draft["acceptance"], start=1)
),
}
profile_check(config)
run_cli([
"base", "+record-upsert", "--profile", config["profile"],
"--base-token", config["baseToken"], "--table-id", config["tableId"],
"--record-id", record_id, "--json",
json.dumps(patch, ensure_ascii=False, separators=(",", ":")),
"--format", "json",
])
matching = [record for record in fetch(config, None)["records"] if record["recordId"] == record_id]
if len(matching) != 1 or matching[0]["sourceRef"] != expected_source_ref:
raise IntakeError("draft writeback readback did not find exactly one record")
expected_acceptance = text(patch[config["fields"]["acceptance"]])
if (
matching[0]["fixLogic"] != text(draft["fixLogic"])
or matching[0]["acceptance"] != expected_acceptance
):
raise IntakeError("draft writeback readback did not match the submitted draft")
return {
"provider": "feishu-base",
"recordId": record_id,
"written": ["fixLogic", "acceptance"],
"draftRevision": matching[0]["draftRevision"],
"ok": True,
}
def import_approved(
config: dict[str, Any], record_id: str, expected_source_ref: str,
expected_revision: str,
) -> dict[str, Any]:
"""Emit the canonical task payload for one explicitly approved draft revision."""
if config.get("workflow", "read-only-v1") != "reviewed-writeback-v1":
raise IntakeError("approved import requires reviewed-writeback-v1 workflow")
record = review_record(
config, record_id, expected_source_ref, expected_revision,
)
steps = review_items(record["steps"])
acceptance = review_items(record["acceptance"])
if not record["priority"] or not steps or not record["fixLogic"] or not acceptance:
raise IntakeError("approved record is missing prepared review fields")
task_draft: dict[str, Any] = {
"title": record["title"],
"priority": record["priority"],
"description": record["title"],
"actual": record["actual"],
"expected": record["expected"],
"stepsToReproduce": steps,
"fixLogic": record["fixLogic"],
"acceptanceCriteria": acceptance,
"source": {
"kind": "feishu-base",
"workflow": "reviewed-writeback-v1",
"ref": record["sourceRef"],
"recordId": record["recordId"],
"updatedAt": record["updatedAt"],
"approvedRevision": record["draftRevision"],
},
}
task_draft["source"]["approvedPayloadHash"] = approval_payload_hash(task_draft)
return {
"provider": "feishu-base",
"recordId": record_id,
"draftRevision": record["draftRevision"],
"taskDraft": task_draft,
"ok": True,
}
def plan_actions(board: dict[str, Any], records: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Plan idempotent Coordinator actions without mutating the task board."""
tasks = board.get("tasks")
if not isinstance(tasks, list):
raise IntakeError("task board tasks must be a list")
existing: dict[str, dict[str, Any]] = {}
for task in tasks:
if not isinstance(task, dict):
continue
source = task.get("source")
if not isinstance(source, dict) or source.get("kind") != "feishu-base":
continue
ref = source.get("ref")
task_id = task.get("id")
status = task.get("status")
updated_at = source.get("updatedAt")
if (
not isinstance(ref, str) or SOURCE_REF.fullmatch(ref) is None
or not isinstance(task_id, str) or not task_id
or not isinstance(status, str) or not status
or not isinstance(updated_at, str) or not updated_at
):
raise IntakeError("existing Feishu task source is invalid")
if ref in existing:
raise IntakeError("task board contains duplicate Feishu source references")
existing[ref] = task
actions: list[dict[str, Any]] = []
seen_records: set[str] = set()
for record in records:
ref = record.get("sourceRef")
record_id = record.get("recordId")
updated_at = record.get("updatedAt")
revision = record.get("draftRevision")
if (
not isinstance(ref, str) or SOURCE_REF.fullmatch(ref) is None
or not isinstance(record_id, str) or RECORD_ID.fullmatch(record_id) is None
or not isinstance(updated_at, str) or not updated_at
or not isinstance(revision, str) or DRAFT_REVISION.fullmatch(revision) is None
):
raise IntakeError("normalized Feishu record identity is invalid")
if ref in seen_records:
raise IntakeError("fetched records contain a duplicate source reference")
seen_records.add(ref)
task = existing.get(ref)
if task is None:
planned_action: dict[str, Any] = {"sourceRef": ref, "recordId": record_id, "draftRevision": record["draftRevision"], "action": "create"}
enrichment_required = record.get("enrichmentRequired")
if enrichment_required:
planned_action["enrichmentRequired"] = enrichment_required
actions.append(planned_action)
continue
source = task["source"]
approved_revision = source.get("approvedRevision")
if isinstance(approved_revision, str):
if approved_revision == revision:
action_name = "unchanged"
elif task["status"] == "open":
action_name = "refresh"
else:
action_name = "drift"
elif source["updatedAt"] == updated_at:
action_name = "unchanged"
elif task["status"] == "open":
action_name = "refresh"
else:
action_name = "drift"
planned_action = {
"sourceRef": ref,
"recordId": record_id,
"draftRevision": record["draftRevision"],
"taskId": task["id"],
"status": task["status"],
"action": action_name,
}
enrichment_required = record.get("enrichmentRequired")
if enrichment_required and action_name == "refresh":
planned_action["enrichmentRequired"] = enrichment_required
actions.append(planned_action)
return actions
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Read and review a configured Feishu Base bug intake")
sub = parser.add_subparsers(dest="command", required=True)
for name in ("check", "fetch", "plan", "write-draft", "import-approved"):
command = sub.add_parser(name)
command.add_argument("tasks", type=Path, help="ACK tasks.yaml or JSON board")
if name in {"fetch", "plan"}:
command.add_argument("--output-dir", type=Path, help="explicit directory for downloaded attachments")
if name in {"write-draft", "import-approved"}:
command.add_argument("--record-id", required=True, help="existing Feishu Base record id")
command.add_argument("--expected-source-ref", required=True, help="sourceRef returned by fetch")
command.add_argument("--expected-draft-revision", required=True, help="draftRevision returned by fetch")
if name == "write-draft":
command.add_argument("--input", type=Path, required=True, help="bounded JSON draft file")
args = parser.parse_args(argv)
try:
board = load_board(args.tasks)
config = config_from_board(board)
if args.command == "check":
profile_check(config)
output = {"provider": "feishu-base", "workflow": config.get("workflow", "read-only-v1"), "profile": config["profile"], "ok": True}
elif args.command == "fetch":
output = fetch(config, args.output_dir)
elif args.command == "plan":
output = fetch(config, args.output_dir)
output["actions"] = plan_actions(board, output["records"])
elif args.command == "write-draft":
output = write_draft(
config, args.record_id, args.expected_source_ref,
args.expected_draft_revision, args.input,
)
else:
output = import_approved(
config, args.record_id, args.expected_source_ref,
args.expected_draft_revision,
)
except IntakeError as exc:
sys.stderr.write(f"Feishu bug intake failed: {exc}\n")
return 1
except (OSError, subprocess.SubprocessError):
sys.stderr.write("Feishu bug intake failed: local I/O failed\n")
return 1
print(json.dumps(output, ensure_ascii=False, separators=(",", ":")))
return 0
if __name__ == "__main__":
raise SystemExit(main())