AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Decrypt a C2C encrypted payload."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from nacl.public import Box, PrivateKey, PublicKey
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
|
||||
recipient = parser.add_mutually_exclusive_group(required=True)
|
||||
recipient.add_argument("--recipient-private-key", help="Recipient private key (base64)")
|
||||
recipient.add_argument("--recipient-private-key-file", help="Path to recipient private key file or key JSON")
|
||||
|
||||
sender = parser.add_mutually_exclusive_group(required=True)
|
||||
sender.add_argument("--sender-public-key", help="Sender public key (base64)")
|
||||
sender.add_argument("--sender-public-key-file", help="Path to sender public key file or key JSON")
|
||||
|
||||
cipher = parser.add_mutually_exclusive_group(required=True)
|
||||
cipher.add_argument("--encrypted-payload", help="Encrypted payload (base64)")
|
||||
cipher.add_argument("--encrypted-file", help="Path to encrypted payload file")
|
||||
|
||||
parser.add_argument("--raw", action="store_true", help="Print raw text instead of JSON formatting")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def read_text(path: str) -> str:
|
||||
return Path(path).expanduser().read_text(encoding="utf-8").strip()
|
||||
|
||||
|
||||
def coerce_key(text: str, key_name: str) -> str:
|
||||
text = text.strip()
|
||||
if text.startswith("{"):
|
||||
return json.loads(text)[key_name]
|
||||
return text
|
||||
|
||||
|
||||
def read_key(arg_value: str | None, file_value: str | None, key_name: str) -> str:
|
||||
if arg_value:
|
||||
return arg_value.strip()
|
||||
if not file_value:
|
||||
raise ValueError(f"Missing {key_name}")
|
||||
return coerce_key(read_text(file_value), key_name)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
|
||||
recipient_private_b64 = read_key(args.recipient_private_key, args.recipient_private_key_file, "privateKey")
|
||||
sender_public_b64 = read_key(args.sender_public_key, args.sender_public_key_file, "publicKey")
|
||||
|
||||
if args.encrypted_payload:
|
||||
encrypted_b64 = args.encrypted_payload.strip()
|
||||
else:
|
||||
encrypted_b64 = read_text(args.encrypted_file)
|
||||
|
||||
recipient_private = PrivateKey(base64.b64decode(recipient_private_b64))
|
||||
sender_public = PublicKey(base64.b64decode(sender_public_b64))
|
||||
|
||||
box = Box(recipient_private, sender_public)
|
||||
plaintext = box.decrypt(base64.b64decode(encrypted_b64)).decode("utf-8")
|
||||
|
||||
if args.raw:
|
||||
print(plaintext)
|
||||
return
|
||||
|
||||
try:
|
||||
print(json.dumps(json.loads(plaintext), indent=2, ensure_ascii=False))
|
||||
except json.JSONDecodeError:
|
||||
print(plaintext)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Encrypt a JSON payload for C2C messaging."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from nacl.public import Box, PrivateKey, PublicKey
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
|
||||
sender = parser.add_mutually_exclusive_group(required=True)
|
||||
sender.add_argument("--sender-private-key", help="Sender private key (base64)")
|
||||
sender.add_argument("--sender-private-key-file", help="Path to sender private key file or key JSON")
|
||||
|
||||
recipient = parser.add_mutually_exclusive_group(required=True)
|
||||
recipient.add_argument("--recipient-public-key", help="Recipient public key (base64)")
|
||||
recipient.add_argument("--recipient-public-key-file", help="Path to recipient public key file or key JSON")
|
||||
|
||||
payload = parser.add_mutually_exclusive_group()
|
||||
payload.add_argument("--payload-json", help="Inline JSON payload string")
|
||||
payload.add_argument("--payload-file", help="Path to payload JSON file")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def read_text(path: str) -> str:
|
||||
return Path(path).expanduser().read_text(encoding="utf-8").strip()
|
||||
|
||||
|
||||
def coerce_key(text: str, key_name: str) -> str:
|
||||
text = text.strip()
|
||||
if text.startswith("{"):
|
||||
return json.loads(text)[key_name]
|
||||
return text
|
||||
|
||||
|
||||
def read_key(arg_value: str | None, file_value: str | None, key_name: str) -> str:
|
||||
if arg_value:
|
||||
return arg_value.strip()
|
||||
if not file_value:
|
||||
raise ValueError(f"Missing {key_name}")
|
||||
return coerce_key(read_text(file_value), key_name)
|
||||
|
||||
|
||||
def read_payload(args: argparse.Namespace):
|
||||
if args.payload_json:
|
||||
return json.loads(args.payload_json)
|
||||
if args.payload_file:
|
||||
return json.loads(read_text(args.payload_file))
|
||||
|
||||
stdin = sys.stdin.read().strip()
|
||||
if not stdin:
|
||||
raise ValueError("Provide --payload-json, --payload-file, or JSON on stdin")
|
||||
return json.loads(stdin)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
|
||||
sender_private_b64 = read_key(args.sender_private_key, args.sender_private_key_file, "privateKey")
|
||||
recipient_public_b64 = read_key(args.recipient_public_key, args.recipient_public_key_file, "publicKey")
|
||||
payload = read_payload(args)
|
||||
|
||||
sender_private = PrivateKey(base64.b64decode(sender_private_b64))
|
||||
recipient_public = PublicKey(base64.b64decode(recipient_public_b64))
|
||||
|
||||
box = Box(sender_private, recipient_public)
|
||||
plaintext = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
||||
encrypted = box.encrypt(plaintext)
|
||||
print(base64.b64encode(bytes(encrypted)).decode("ascii"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
387
archive/inactive-skills/clawtoclaw/scripts/event_heartbeat.py
Normal file
387
archive/inactive-skills/clawtoclaw/scripts/event_heartbeat.py
Normal file
@@ -0,0 +1,387 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run an event-focused heartbeat cycle for Claw-to-Claw check-ins.
|
||||
|
||||
Behavior:
|
||||
- Short-circuit quickly when no active local event state exists.
|
||||
- Exit after clearing expired/invalid state files.
|
||||
- When active:
|
||||
- Validate event is still live and check-in is present.
|
||||
- Read and surface intro inbox items.
|
||||
- Pull suggestions and optionally auto-propose strong matches.
|
||||
- Renew check-in when nearing expiry.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import ssl
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
DEFAULT_API_BASE = "https://www.clawtoclaw.com/api"
|
||||
DEFAULT_STATE_PATH = Path("~/.c2c/active_event.json").expanduser()
|
||||
DEFAULT_CREDENTIALS_PATH = Path("~/.c2c/credentials.json").expanduser()
|
||||
DEFAULT_RENEW_WITHIN_MINUTES = 12
|
||||
DEFAULT_RENEW_DURATION_MINUTES = 60
|
||||
REQUEST_TIMEOUT_SEC = 20
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActiveEventState:
|
||||
event_id: str
|
||||
expires_at: int
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, payload: dict[str, Any]) -> "ActiveEventState":
|
||||
return cls(
|
||||
event_id=payload["eventId"],
|
||||
expires_at=int(payload["expiresAt"]),
|
||||
)
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
return {"eventId": self.event_id, "expiresAt": self.expires_at}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--api-base",
|
||||
default=DEFAULT_API_BASE,
|
||||
help="C2C API base URL.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--state-path",
|
||||
default=str(DEFAULT_STATE_PATH),
|
||||
help="Path to active event state file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--credentials-path",
|
||||
default=str(DEFAULT_CREDENTIALS_PATH),
|
||||
help="Path to credentials JSON with apiKey.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--renew-within-minutes",
|
||||
type=int,
|
||||
default=DEFAULT_RENEW_WITHIN_MINUTES,
|
||||
help="Renew check-in when remaining time is below this threshold.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--renew-duration-minutes",
|
||||
type=int,
|
||||
default=DEFAULT_RENEW_DURATION_MINUTES,
|
||||
help="Duration (minutes) to request when renewing check-in.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--suggestions-limit",
|
||||
type=int,
|
||||
default=8,
|
||||
help="Max suggestions to retrieve from events:getSuggestions.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--propose",
|
||||
action="store_true",
|
||||
help="Auto-propose intros from strong suggestions.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--propose-threshold",
|
||||
type=int,
|
||||
default=20,
|
||||
help="Min suggestion score required to auto-propose.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-proposals",
|
||||
type=int,
|
||||
default=2,
|
||||
help="Max auto-proposals per heartbeat run.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Validate and print planned actions without mutating state or API state.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def now_ms() -> int:
|
||||
return int(datetime.now(timezone.utc).timestamp() * 1000)
|
||||
|
||||
|
||||
def read_json(path: Path) -> Any:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def write_json(path: Path, payload: dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def safe_read_state(path: Path) -> ActiveEventState | None:
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
payload = read_json(path)
|
||||
return ActiveEventState.from_json(payload)
|
||||
except Exception:
|
||||
# Corrupt state should be treated as a no-op + cleanup.
|
||||
path.unlink(missing_ok=True)
|
||||
return None
|
||||
|
||||
|
||||
def load_api_key(path: Path) -> str:
|
||||
if not path.exists():
|
||||
raise RuntimeError(f"Missing credentials file at {path}")
|
||||
|
||||
payload = read_json(path)
|
||||
if isinstance(payload, str):
|
||||
return payload.strip()
|
||||
|
||||
if isinstance(payload, dict):
|
||||
for key in ("apiKey", "key"):
|
||||
if key in payload and isinstance(payload[key], str):
|
||||
return payload[key].strip()
|
||||
|
||||
raise RuntimeError(f"Could not read apiKey from {path}")
|
||||
|
||||
|
||||
def api_request(
|
||||
api_base: str,
|
||||
api_key: str,
|
||||
path_name: str,
|
||||
args: dict[str, Any],
|
||||
endpoint: str,
|
||||
) -> Any:
|
||||
url = f"{api_base.rstrip('/')}/{endpoint}"
|
||||
payload = json.dumps({"path": path_name, "args": args, "format": "json"}).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
method="POST",
|
||||
data=payload,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=ssl.create_default_context(), timeout=REQUEST_TIMEOUT_SEC) as response:
|
||||
body = response.read().decode("utf-8")
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode("utf-8", errors="replace")
|
||||
raise RuntimeError(f"{path_name} failed: HTTP {exc.code}: {body}") from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise RuntimeError(f"{path_name} failed: {exc}") from exc
|
||||
|
||||
data = json.loads(body)
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
status = data.get("status")
|
||||
if status == "error":
|
||||
raise RuntimeError(data.get("errorMessage") or data.get("message") or "API error")
|
||||
|
||||
if "value" in data:
|
||||
return data["value"]
|
||||
return data
|
||||
|
||||
|
||||
def query_event_by_id(api_base: str, api_key: str, event_id: str) -> dict[str, Any]:
|
||||
return api_request(api_base, api_key, "events:getById", {"eventId": event_id}, "query")
|
||||
|
||||
|
||||
def list_my_intros(api_base: str, api_key: str, event_id: str) -> list[dict[str, Any]]:
|
||||
payload = {"eventId": event_id, "includeResolved": False}
|
||||
return api_request(api_base, api_key, "events:listMyIntros", payload, "query")
|
||||
|
||||
|
||||
def get_suggestions(
|
||||
api_base: str,
|
||||
api_key: str,
|
||||
event_id: str,
|
||||
limit: int,
|
||||
) -> list[dict[str, Any]]:
|
||||
return api_request(api_base, api_key, "events:getSuggestions", {"eventId": event_id, "limit": limit}, "query")
|
||||
|
||||
|
||||
def propose_intro(api_base: str, api_key: str, event_id: str, to_agent_id: str, dry_run: bool) -> dict[str, Any] | None:
|
||||
if dry_run:
|
||||
return {"dryRun": True, "status": "proposed"}
|
||||
return api_request(
|
||||
api_base,
|
||||
api_key,
|
||||
"events:proposeIntro",
|
||||
{"eventId": event_id, "toAgentId": to_agent_id},
|
||||
"mutation",
|
||||
)
|
||||
|
||||
|
||||
def renew_checkin(api_base: str, api_key: str, event_id: str, duration_minutes: int, dry_run: bool) -> dict[str, Any] | None:
|
||||
if dry_run:
|
||||
return {"dryRun": True, "status": "ok", "statusAction": "renew"}
|
||||
return api_request(
|
||||
api_base,
|
||||
api_key,
|
||||
"events:checkIn",
|
||||
{"eventId": event_id, "durationMinutes": duration_minutes},
|
||||
"mutation",
|
||||
)
|
||||
|
||||
|
||||
def clear_state(path: Path) -> None:
|
||||
path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def output(payload: dict[str, Any]) -> None:
|
||||
payload["generatedAtMs"] = now_ms()
|
||||
print(json.dumps(payload, indent=2))
|
||||
|
||||
|
||||
def run() -> None:
|
||||
args = parse_args()
|
||||
state_path = Path(args.state_path).expanduser()
|
||||
credentials_path = Path(args.credentials_path).expanduser()
|
||||
|
||||
state = safe_read_state(state_path)
|
||||
if not state:
|
||||
output({"status": "HEARTBEAT_OK", "reason": "no_active_event_state"})
|
||||
return
|
||||
|
||||
if state.expires_at <= now_ms():
|
||||
clear_state(state_path)
|
||||
output({
|
||||
"status": "HEARTBEAT_OK",
|
||||
"reason": "active_event_expired",
|
||||
"state": state.to_json(),
|
||||
"action": "cleared_state",
|
||||
})
|
||||
return
|
||||
|
||||
try:
|
||||
api_key = load_api_key(credentials_path)
|
||||
except Exception as exc:
|
||||
output({"status": "HEARTBEAT_ERROR", "reason": "missing_credentials", "error": str(exc)})
|
||||
return
|
||||
|
||||
try:
|
||||
event_data = query_event_by_id(args.api_base, api_key, state.event_id)
|
||||
except Exception as exc:
|
||||
output({
|
||||
"status": "HEARTBEAT_ERROR",
|
||||
"reason": "get_event_failed",
|
||||
"error": str(exc),
|
||||
"state": state.to_json(),
|
||||
})
|
||||
return
|
||||
|
||||
event_status = event_data.get("status")
|
||||
my_checkin = event_data.get("myCheckin")
|
||||
if event_status != "live" or not my_checkin:
|
||||
clear_state(state_path)
|
||||
output({
|
||||
"status": "HEARTBEAT_OK",
|
||||
"reason": "inactive_event_or_checkin",
|
||||
"eventStatus": event_status,
|
||||
"state": state.to_json(),
|
||||
"action": "cleared_state",
|
||||
})
|
||||
return
|
||||
|
||||
active_expires_ms = int(my_checkin.get("expiresAt", 0))
|
||||
write_json(state_path, {"eventId": state.event_id, "expiresAt": active_expires_ms, "checkedInAt": event_data.get("checkedInAt", now_ms())})
|
||||
|
||||
try:
|
||||
intro_inbox = list_my_intros(args.api_base, api_key, state.event_id)
|
||||
except Exception as exc:
|
||||
intro_inbox = [{"error": str(exc)}]
|
||||
intro_actions = [
|
||||
item
|
||||
for item in (intro_inbox or [])
|
||||
if isinstance(item, dict) and (item.get("canRespond") or item.get("canApprove"))
|
||||
]
|
||||
|
||||
try:
|
||||
suggestions = get_suggestions(
|
||||
args.api_base,
|
||||
api_key,
|
||||
state.event_id,
|
||||
args.suggestions_limit,
|
||||
)
|
||||
except Exception as exc:
|
||||
suggestions = [{"error": str(exc)}]
|
||||
suggested_ids: list[str] = []
|
||||
proposals: list[dict[str, Any]] = []
|
||||
proposals_made = 0
|
||||
|
||||
if args.propose:
|
||||
for candidate in suggestions or []:
|
||||
if proposals_made >= args.max_proposals:
|
||||
break
|
||||
if not isinstance(candidate, dict):
|
||||
continue
|
||||
score = int(candidate.get("score", 0))
|
||||
to_agent = candidate.get("toAgent") or {}
|
||||
candidate_id = to_agent.get("agentId")
|
||||
if not candidate_id or score < args.propose_threshold:
|
||||
continue
|
||||
try:
|
||||
proposals_made += 1
|
||||
proposals.append(propose_intro(args.api_base, api_key, state.event_id, candidate_id, args.dry_run))
|
||||
suggested_ids.append(candidate_id)
|
||||
except Exception as exc:
|
||||
proposals.append({"error": str(exc), "toAgentId": candidate_id})
|
||||
|
||||
renewal = None
|
||||
remaining_ms = active_expires_ms - now_ms()
|
||||
if remaining_ms <= args.renew_within_minutes * 60_000:
|
||||
try:
|
||||
renewal = renew_checkin(
|
||||
args.api_base,
|
||||
api_key,
|
||||
state.event_id,
|
||||
args.renew_duration_minutes,
|
||||
args.dry_run,
|
||||
)
|
||||
except Exception as exc:
|
||||
output({
|
||||
"status": "HEARTBEAT_ERROR",
|
||||
"reason": "checkin_renewal_failed",
|
||||
"error": str(exc),
|
||||
"event": {"eventId": state.event_id, "status": event_status},
|
||||
})
|
||||
return
|
||||
|
||||
if not args.dry_run and isinstance(renewal, dict) and renewal.get("expiresAt"):
|
||||
write_json(state_path, {"eventId": state.event_id, "expiresAt": int(renewal["expiresAt"])})
|
||||
|
||||
output({
|
||||
"status": "HEARTBEAT_OK",
|
||||
"event": {
|
||||
"eventId": state.event_id,
|
||||
"status": event_status,
|
||||
"name": event_data.get("name"),
|
||||
"myCheckin": {
|
||||
"expiresAt": active_expires_ms,
|
||||
"introEnabled": my_checkin.get("introEnabled", False),
|
||||
},
|
||||
},
|
||||
"introActions": intro_actions,
|
||||
"suggestionActions": {
|
||||
"checked": len(suggestions or []),
|
||||
"proposed": proposals_made,
|
||||
"proposedToAgents": suggested_ids,
|
||||
"results": proposals,
|
||||
"proposalThreshold": args.propose_threshold,
|
||||
"proposedEnabled": args.propose,
|
||||
},
|
||||
"renewal": renewal,
|
||||
"state": state.to_json(),
|
||||
"dryRun": args.dry_run,
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
131
archive/inactive-skills/clawtoclaw/scripts/event_state.py
Normal file
131
archive/inactive-skills/clawtoclaw/scripts/event_state.py
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Manage C2C active event state for heartbeat workflows."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
DEFAULT_STATE_PATH = Path("~/.c2c/active_event.json").expanduser()
|
||||
|
||||
|
||||
@dataclass
|
||||
class EventState:
|
||||
event_id: str
|
||||
expires_at: int
|
||||
checked_in_at: str
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "EventState":
|
||||
return cls(
|
||||
event_id=data["eventId"],
|
||||
expires_at=int(data["expiresAt"]),
|
||||
checked_in_at=data.get("checkedInAt")
|
||||
or datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"eventId": self.event_id,
|
||||
"expiresAt": self.expires_at,
|
||||
"checkedInAt": self.checked_in_at,
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--path", default=str(DEFAULT_STATE_PATH), help="State file path")
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
set_cmd = subparsers.add_parser("set", help="Persist active event state")
|
||||
set_cmd.add_argument("--event-id", required=True, help="Event ID")
|
||||
set_cmd.add_argument("--expires-at", required=True, type=int, help="Epoch millis expiration")
|
||||
set_cmd.add_argument("--checked-in-at", help="ISO timestamp, defaults to now")
|
||||
|
||||
status_cmd = subparsers.add_parser("status", help="Read active event state")
|
||||
status_cmd.add_argument("--now-ms", type=int, help="Override current epoch millis")
|
||||
status_cmd.add_argument("--clear-expired", action="store_true", help="Delete file when expired")
|
||||
|
||||
subparsers.add_parser("clear", help="Delete active event state")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def get_now_ms(now_ms: int | None = None) -> int:
|
||||
if now_ms is not None:
|
||||
return now_ms
|
||||
return int(datetime.now(timezone.utc).timestamp() * 1000)
|
||||
|
||||
|
||||
def read_state(path: Path) -> EventState:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
return EventState.from_dict(data)
|
||||
|
||||
|
||||
def write_state(path: Path, state: EventState) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(state.to_dict(), indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def print_json(data: dict[str, Any]) -> None:
|
||||
print(json.dumps(data, indent=2))
|
||||
|
||||
|
||||
def run_set(args: argparse.Namespace, path: Path) -> None:
|
||||
checked_in_at = args.checked_in_at or datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
state = EventState(event_id=args.event_id, expires_at=args.expires_at, checked_in_at=checked_in_at)
|
||||
write_state(path, state)
|
||||
print_json({"status": "ok", "path": str(path), "state": state.to_dict()})
|
||||
|
||||
|
||||
def run_status(args: argparse.Namespace, path: Path) -> None:
|
||||
if not path.exists():
|
||||
print_json({"active": False, "reason": "missing_state_file", "path": str(path)})
|
||||
return
|
||||
|
||||
state = read_state(path)
|
||||
now_ms = get_now_ms(args.now_ms)
|
||||
expired = state.expires_at <= now_ms
|
||||
|
||||
if expired and args.clear_expired:
|
||||
path.unlink(missing_ok=True)
|
||||
|
||||
print_json(
|
||||
{
|
||||
"active": not expired,
|
||||
"expired": expired,
|
||||
"path": str(path),
|
||||
"nowMs": now_ms,
|
||||
"state": state.to_dict(),
|
||||
"action": "cleared" if expired and args.clear_expired else "none",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def run_clear(path: Path) -> None:
|
||||
existed = path.exists()
|
||||
path.unlink(missing_ok=True)
|
||||
print_json({"status": "ok", "path": str(path), "deleted": existed})
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
path = Path(args.path).expanduser()
|
||||
|
||||
if args.command == "set":
|
||||
run_set(args, path)
|
||||
elif args.command == "status":
|
||||
run_status(args, path)
|
||||
elif args.command == "clear":
|
||||
run_clear(path)
|
||||
else:
|
||||
raise ValueError(f"Unsupported command: {args.command}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate an X25519 keypair for C2C encrypted messaging."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from nacl.public import PrivateKey
|
||||
|
||||
|
||||
def utc_now_iso() -> str:
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--agent-id", help="Write to ~/.c2c/keys/<agent-id>.json when set")
|
||||
parser.add_argument("--output", help="Explicit output path for key JSON")
|
||||
parser.add_argument("--stdout-only", action="store_true", help="Do not write a file")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def resolve_output_path(args: argparse.Namespace) -> Path | None:
|
||||
if args.stdout_only:
|
||||
return None
|
||||
if args.output:
|
||||
return Path(args.output).expanduser().resolve()
|
||||
if args.agent_id:
|
||||
return Path("~/.c2c/keys").expanduser() / f"{args.agent_id}.json"
|
||||
return None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
|
||||
private_key = PrivateKey.generate()
|
||||
private_bytes = bytes(private_key)
|
||||
public_bytes = bytes(private_key.public_key)
|
||||
payload = {
|
||||
"algorithm": "x25519",
|
||||
"createdAt": utc_now_iso(),
|
||||
"privateKey": base64.b64encode(private_bytes).decode("ascii"),
|
||||
"publicKey": base64.b64encode(public_bytes).decode("ascii"),
|
||||
}
|
||||
|
||||
output = json.dumps(payload, indent=2)
|
||||
print(output)
|
||||
|
||||
output_path = resolve_output_path(args)
|
||||
if not output_path:
|
||||
return
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(output + "\n", encoding="utf-8")
|
||||
os.chmod(output_path, 0o600)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user