mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
The merged branch moved every role-local script out of files/ into a folder named for its language (files/python/, files/php/, files/shell/), but the moves lost their staging and never landed. HEAD still carried the old paths while the worktree had the new ones. Move the 24 remaining scripts and update the six places that resolve them: the mcp-adapter instance/isolation/probe tasks, the litellm key task, the adapter README and the endpoint-path lint. Every changed line in those files is a path. Also drop roles/sys-svc-compose/files/swarm/stack_ready.sh, a stale duplicate of the copy already tracked under files/shell/swarm/. The added comments are the branch's own `nocheck: mirrored-unit-test` directives, which state why a script whose measurement only exists inside its container cannot have a mirrored unit test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
115 lines
3.8 KiB
Python
115 lines
3.8 KiB
Python
# nocheck: mirrored-unit-test - the contract is the shape n8n 1.95.3 accepts on
|
|
# /rest/credentials behind a session cookie; every helper is one call against the live
|
|
# instance and a stubbed opener would assert this file's own payload back at it
|
|
"""Provision the deployment-managed OpenAI credential of n8n 1.95.3.
|
|
|
|
Prints ``CHANGED``/``OK``.
|
|
|
|
Environment:
|
|
N8N_BASE: origin of the n8n API.
|
|
N8N_OWNER_EMAIL: owner account email.
|
|
N8N_OWNER_PASSWORD: owner account password.
|
|
N8N_AI_CREDENTIAL: deterministic name of the managed credential.
|
|
N8N_AI_BASE_URL: OpenAI-compatible base URL of the gateway.
|
|
N8N_AI_API_KEY: gateway virtual key of this role.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
BASE = os.environ.get("N8N_BASE", "").rstrip("/")
|
|
EMAIL = os.environ.get("N8N_OWNER_EMAIL", "")
|
|
PASSWORD = os.environ.get("N8N_OWNER_PASSWORD", "")
|
|
CREDENTIAL_NAME = os.environ.get("N8N_AI_CREDENTIAL", "")
|
|
AI_BASE_URL = os.environ.get("N8N_AI_BASE_URL", "")
|
|
AI_API_KEY = os.environ.get("N8N_AI_API_KEY", "")
|
|
|
|
REST = "/rest"
|
|
CREDENTIAL_TYPE = "openAiApi"
|
|
|
|
SESSION = {"cookie": ""}
|
|
|
|
|
|
def call(path, method="GET", payload=None):
|
|
"""Return ``(status, body)`` of one n8n internal API call.
|
|
|
|
Args:
|
|
path: API path below the n8n origin.
|
|
method: HTTP method.
|
|
payload: JSON-serialisable request body, or None.
|
|
"""
|
|
data = json.dumps(payload).encode() if payload is not None else None
|
|
request = urllib.request.Request( # noqa: S310 - fixed http:// base from N8N_BASE, no user-supplied scheme
|
|
f"{BASE}{path}", data=data, method=method
|
|
)
|
|
request.add_header("Content-Type", "application/json")
|
|
if SESSION["cookie"]:
|
|
request.add_header("Cookie", SESSION["cookie"])
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=60) as response: # noqa: S310 - fixed http:// base from N8N_BASE
|
|
for header in response.headers.get_all("Set-Cookie") or []:
|
|
if header.startswith("n8n-auth="):
|
|
SESSION["cookie"] = header.split(";", 1)[0]
|
|
return response.status, json.loads(response.read() or b"null")
|
|
except urllib.error.HTTPError as error:
|
|
return error.code, error.read().decode(errors="replace")
|
|
|
|
|
|
def login():
|
|
status, body = call(
|
|
f"{REST}/login",
|
|
method="POST",
|
|
payload={"emailOrLdapLoginId": EMAIL, "password": PASSWORD},
|
|
)
|
|
if status != 200:
|
|
sys.exit(f"FAILED logging in as {EMAIL}: {status} {body}")
|
|
|
|
|
|
def payload():
|
|
return {
|
|
"name": CREDENTIAL_NAME,
|
|
"type": CREDENTIAL_TYPE,
|
|
"data": {"apiKey": AI_API_KEY, "url": AI_BASE_URL},
|
|
}
|
|
|
|
|
|
def upsert():
|
|
"""Create or overwrite the managed gateway credential."""
|
|
status, body = call(f"{REST}/credentials")
|
|
if status != 200:
|
|
sys.exit(f"FAILED listing credentials: {status} {body}")
|
|
|
|
existing = (body or {}).get("data") if isinstance(body, dict) else body
|
|
matches = [item for item in existing or [] if item.get("name") == CREDENTIAL_NAME]
|
|
if len(matches) > 1:
|
|
sys.exit(f"FAILED: {len(matches)} credentials named {CREDENTIAL_NAME}")
|
|
|
|
if not matches:
|
|
status, body = call(f"{REST}/credentials", method="POST", payload=payload())
|
|
if status not in (200, 201):
|
|
sys.exit(f"FAILED creating {CREDENTIAL_NAME}: {status} {body}")
|
|
return True
|
|
|
|
status, body = call(
|
|
f"{REST}/credentials/{matches[0]['id']}", method="PATCH", payload=payload()
|
|
)
|
|
if status != 200:
|
|
sys.exit(f"FAILED updating {CREDENTIAL_NAME}: {status} {body}")
|
|
return False
|
|
|
|
|
|
def main():
|
|
if not AI_API_KEY:
|
|
sys.exit("FAILED: no gateway virtual key configured")
|
|
login()
|
|
print("CHANGED" if upsert() else "OK")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|