feat(forge): adapter seam + Gitea implementation — optional read access to the operator's forge (#2689)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 55s
CI & Build / Build & push image (push) Successful in 36s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 55s
CI & Build / Build & push image (push) Successful in 36s
Step 4 of milestone 288 (decision #2686). services/forge.py defines the contract steps 5-7 consume — read_file (content + last_commit_sha, the provenance stamp), default_branch, resolve_repo, check — with GiteaForge as the first implementation over the REST contents/repo/version/user endpoints. Repo identity reuses normalize_repo_key: the host segment selects whether this forge serves a recorded repo, the remainder is the API path, so no new identity scheme exists. Read-only by construction; errors never carry the token; first outbound-HTTP timeout convention (5s total, no retries — the consumer's fallback is the retry policy). OPTIONAL per instance (rule #115): get_forge() returns None when unconfigured and every consumer treats None as today's behavior. Config lives in admin settings (Settings → Config → Git Forge: kind/base URL/token, save + test-connection probe reporting version + identity), with FORGE_* env / Docker-secret fallbacks; DB wins so a UI edit can't silently lose to an env var. Token treatment follows the smtp_password convention (masked on read, mask-sentinel skipped on write, absent from audit details) — and wiring it surfaced that the generic GET/PUT /api/settings dump bypassed that masking for the owning admin's raw KV rows, so secret keys are now masked there too (fixes the same exposure for smtp_password). Contract tests run against httpx.MockTransport as the fake forge — the reference behaviors the GitHub adapter (step 8) must reproduce — plus the off-by-default gate, partial-config-is-off, env-vs-DB precedence, and route/mask structural checks. Also: the step-2 definition detector learned to skip dunders after flagging __init__ as 'already defined in 4 files' on this step's own build — guaranteed noise for a hint that must stay trustworthy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,13 +16,27 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
settings_bp = Blueprint("settings", __name__, url_prefix="/api/settings")
|
||||
|
||||
# Keys whose values are credentials. The admin endpoints that own them mask on
|
||||
# read and skip the mask on write; this generic KV surface has to apply the
|
||||
# same treatment, or it silently un-masks what those endpoints masked — the
|
||||
# rows live on the admin's own user_id, so the plain GET returned them raw.
|
||||
_SECRET_KEYS = frozenset({"smtp_password", "forge_token"})
|
||||
_SECRET_MASK = "********"
|
||||
|
||||
|
||||
def _masked(settings: dict) -> dict:
|
||||
return {
|
||||
k: (_SECRET_MASK if k in _SECRET_KEYS and v else v)
|
||||
for k, v in settings.items()
|
||||
}
|
||||
|
||||
|
||||
@settings_bp.route("", methods=["GET"])
|
||||
@login_required
|
||||
async def get_settings_route():
|
||||
uid = get_current_user_id()
|
||||
settings = await get_all_settings(uid)
|
||||
return jsonify(settings)
|
||||
return jsonify(_masked(settings))
|
||||
|
||||
|
||||
@settings_bp.route("", methods=["PUT"])
|
||||
@@ -36,6 +50,10 @@ async def update_settings_route():
|
||||
to_save = {}
|
||||
for k, v in data.items():
|
||||
str_v = str(v)
|
||||
# A masked secret round-tripping through a client is "unchanged", not
|
||||
# a request to store the mask over the real credential.
|
||||
if k in _SECRET_KEYS and str_v == _SECRET_MASK:
|
||||
continue
|
||||
if not str_v:
|
||||
await delete_setting(uid, k)
|
||||
else:
|
||||
@@ -45,7 +63,7 @@ async def update_settings_route():
|
||||
await set_settings_batch(uid, to_save)
|
||||
|
||||
settings = await get_all_settings(uid)
|
||||
return jsonify(settings)
|
||||
return jsonify(_masked(settings))
|
||||
|
||||
|
||||
@settings_bp.route("/search", methods=["GET"])
|
||||
|
||||
Reference in New Issue
Block a user