0e6ecae26d
Rename all acdl-* AWS resources → nova-* across terraform (DynamoDB, Secrets Manager, Lambda, SNS, SG, KMS alias, ECS, ECR, IAM user/policy, state bucket, ALB, VPC/subnet names). Lambda default table names → nova-* (D-111). State bucket backend → nova-tfstate (-migrate-state documented). New docs/NOVA_AWS_MIGRATION.md runbook (staged migration + rollback). New scripts/migrate_dynamodb_data.py (scan+copy, dry-run default). acdl-deploy- → nova-deploy- role ARN in deploy workflows. Test fixtures updated; terraform validate + pytest + run_ci.sh PASS. ---ci--- project: acdl phase: 4 milestone: v1.15 status: execute ---/ci---
71 lines
2.5 KiB
Python
71 lines
2.5 KiB
Python
"""Nova Outbox Writer — write an evidence event to the DynamoDB outbox.
|
|
|
|
ARCHITECTURE.md §9: DynamoDB outbox, RPO=0 (synchronous write before
|
|
ack). The event is hash-chained (SHA-256 over canonical JSON); the first
|
|
event has prev_event_hash="GENESIS". D-P10-3: the spike writes ONE
|
|
CONFIDENCE_COMPUTED event.
|
|
|
|
The outbox table (Phase 08): nova-outbox, PAY_PER_REQUEST, PK contractId,
|
|
SK eventType#eventTs, TTL expire_at = now + 365d (D-044).
|
|
|
|
CLI: outbox_writer.py <event.json> (uses AWS creds from env)
|
|
"""
|
|
|
|
import datetime
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
import boto3
|
|
|
|
|
|
OUTBOX_TABLE = "nova-outbox"
|
|
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
|
|
|
|
|
def _canonical_hash(event):
|
|
"""SHA-256 over canonical JSON (sort_keys, compact separators)."""
|
|
canonical = json.dumps(event, sort_keys=True, separators=(",", ":"))
|
|
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def write_event(event, outbox_table=OUTBOX_TABLE, region=REGION):
|
|
"""Write an evidence event to the DynamoDB outbox. Returns the item dict."""
|
|
contract_id = event["contractId"]
|
|
event_type = event.get("eventType", "CONFIDENCE_COMPUTED")
|
|
event_ts = event.get("ts") or datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
sk = f"{event_type}#{event_ts}"
|
|
|
|
# Chain: first event = GENESIS (D-P10-3 spike writes one event).
|
|
prev_hash = event.get("prev_event_hash", "GENESIS")
|
|
event_hash = _canonical_hash(event)
|
|
|
|
item = {
|
|
"contractId": {"S": contract_id},
|
|
"eventType#eventTs": {"S": sk},
|
|
"payload": {"S": json.dumps(event, sort_keys=True)},
|
|
"prev_event_hash": {"S": prev_hash},
|
|
"hash": {"S": event_hash},
|
|
"environment": {"S": str(event.get("environment", ""))},
|
|
"stack": {"S": str(event.get("stack", ""))},
|
|
"score": {"N": str(event.get("score", 0))},
|
|
"band": {"S": str(event.get("band", ""))},
|
|
"expire_at": {"N": str(int((datetime.datetime.now(datetime.timezone.utc) +
|
|
datetime.timedelta(days=365)).timestamp()))},
|
|
}
|
|
|
|
session = boto3.Session(region_name=region)
|
|
dyn = session.client("dynamodb")
|
|
dyn.put_item(TableName=outbox_table, Item=item)
|
|
return item
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) != 2:
|
|
print("usage: outbox_writer.py <event.json>", file=sys.stderr)
|
|
sys.exit(2)
|
|
with open(sys.argv[1], "r") as fh:
|
|
event = json.load(fh)
|
|
item = write_event(event)
|
|
print(json.dumps({k: list(v.values())[0] for k, v in item.items()}, indent=2)) |