Draft the inactive user-engine caller policy.
Some checks failed
Transactional mail acceptance / mail (push) Failing after 33s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

The draft names the live registration binding and quotas, and records that current invitation envelopes are still denied. Ledger tasks stay waiting on pair custody.

Assistant: grok
Assistant-Session: 01a11cdb-f9fe-7011-947c-936e72007e54
This commit is contained in:
tegwick 2026-10-08 21:28:07 +02:00
parent 48823c8182
commit a60a729421
5 changed files with 199 additions and 0 deletions

View file

@ -0,0 +1,29 @@
{
"schema_version": 1,
"callers": [
{
"caller_id": "user-engine",
"source": "user-engine",
"token_file": "/var/run/secrets/email-connect-callers/user-engine",
"bindings": [
["tenant:coulomb", "coulomb-social"]
],
"operations": [
"send",
"delivery-status",
"status",
"verify",
"consume",
"cancel"
],
"templates": [
"family_member.invited",
"family_invitation.resent"
],
"rate_per_minute": 10,
"burst": 5,
"max_payload_bytes": 8192,
"adopt_legacy_verifications": true
}
]
}

View file

@ -0,0 +1,42 @@
# User-engine caller policy draft — 2026-10-08
This file proposes the only caller that exists today. It is not mounted,
`EMAIL_CONNECT_CALLERS_FILE` is not set, and no token file was created.
EMAIL-WP-0008-T03 stays waiting until the bindings, quotas and token custody
are approved.
Source: `config/callers.user-engine.draft.json`.
## What the live caller does
user-engine sends invitation mail to `/v1/send` with source `user-engine` and
templates `family_member.invited` or `family_invitation.resent`. It reads
`/v1/status` and `/v1/delivery-status`. Registration uses
`/v1/registration-verifications`, consume and cancel. The live registration
allow-list is tenant `tenant:coulomb` and client `coulomb-social`
(`USER_ENGINE_REGISTRATION_TENANTS` / `USER_ENGINE_REGISTRATION_CLIENTS` in
`net-kingdom/sso-mfa/k8s/user-engine/runtime.yaml`). Mail and verification
share the current `user-engine-delivery` `mail-token`. Suppression is not a
user-engine operation and is omitted.
## Proposed limits
`rate_per_minute` 10 matches the live registration limit of 10 requests per
60 seconds. Burst 5 and an 8 KiB payload cap are below the receiver ceilings
(6000/minute, 600 burst, 128 KiB). They are proposals, not an approved quota.
`adopt_legacy_verifications` is proposed so this original caller can still
consume verification rows written before caller ids existed.
## Gap that blocks activation
Current invitation envelopes carry `tenant` and do not carry `client_id`.
Scoped send accepts a binding only when both fields are present and admitted,
or when both are absent and `legacy_invitation_binding` is set. A tenant
without a client id is denied. Family invitations also use tenants other than
`tenant:coulomb`. This draft therefore does not claim to carry current
invitation traffic. Activation needs a user-engine change that sends an
admitted tenant/client pair, or a separate reviewed contract change.
The token path in the draft does not exist. Selecting this file on the
running receiver would fail startup before serving traffic. Do not copy the
shared ingest token into that path as part of this draft.

View file

@ -0,0 +1,107 @@
"""The user-engine draft parses and matches today's call set. It is not deployed."""
import io
import json
from pathlib import Path
from email_connect.callers import ScopedTransactionalApplication, load_callers
from email_connect.transactional import SQLiteDeliveryStore
ROOT = Path(__file__).resolve().parents[1]
DRAFT = ROOT / "config" / "callers.user-engine.draft.json"
TOKEN = "synthetic-user-engine-token"
class Provider:
def __init__(self):
self.calls = []
def send(self, *args):
self.calls.append(args)
return "synthetic-reference"
def check(self):
return None
def load_draft(tmp_path):
token = tmp_path / "token"
token.write_text(TOKEN + "\n")
document = json.loads(DRAFT.read_text(encoding="utf-8"))
document["callers"][0]["token_file"] = str(token)
path = tmp_path / "policy.json"
path.write_text(json.dumps(document))
return load_callers(path)
def invoke(app, payload, path):
raw = json.dumps(payload).encode()
response = {}
body = b"".join(
app(
{
"PATH_INFO": path,
"REQUEST_METHOD": "POST",
"CONTENT_LENGTH": str(len(raw)),
"wsgi.input": io.BytesIO(raw),
"HTTP_AUTHORIZATION": "Bearer " + TOKEN,
"HTTP_IDEMPOTENCY_KEY": payload.get("id", ""),
},
lambda status, headers: response.update(status=int(status.split()[0])),
)
)
return response["status"], json.loads(body)
def test_draft_admits_only_the_live_registration_pair(tmp_path):
policies = load_draft(tmp_path)
assert len(policies) == 1
policy = policies[0]
assert policy.caller_id == "user-engine"
assert policy.source == "user-engine"
assert policy.bindings == frozenset({("tenant:coulomb", "coulomb-social")})
assert policy.operations == frozenset(
{"send", "delivery-status", "status", "verify", "consume", "cancel"}
)
assert "suppress" not in policy.operations
assert policy.templates == frozenset({"family_member.invited", "family_invitation.resent"})
assert (policy.rate_per_minute, policy.burst, policy.max_payload_bytes) == (10, 5, 8192)
assert policy.adopt_legacy_verifications is True
assert policy.legacy_invitation_binding is None
assert policy.token == TOKEN
assert TOKEN not in DRAFT.read_text(encoding="utf-8")
assert "token" not in json.loads(DRAFT.read_text(encoding="utf-8"))["callers"][0]
def test_current_invitation_shape_is_denied_and_registration_pair_is_allowed(tmp_path):
app = ScopedTransactionalApplication(
SQLiteDeliveryStore(str(tmp_path / "ledger.db")),
Provider(),
"https://issuer.example.test",
load_draft(tmp_path),
)
invitation = {
"id": "evt-1",
"type": "family_member.invited",
"source": "user-engine",
"tenant": "tenant:family",
"data": {"primary_email": "member@example.test", "invitation_id": "invite-1"},
}
status, body = invoke(app, invitation, "/v1/send")
assert status == 400 and body["error"] == "caller_scope_denied"
assert app.apps["user-engine"].provider.calls == []
verification = {
"registration_id": "reg-1",
"normalized_email": "member@example.test",
"preferred_username": "member",
"client_id": "coulomb-social",
"tenant": "tenant:coulomb",
"correlation_id": "trace-1",
}
status, body = invoke(app, verification, "/v1/registration-verifications")
assert status == 202 and body["accepted"] is True
assert len(app.apps["user-engine"].provider.calls) == 1
status, body = invoke(app, {"recipient": "member@example.test"}, "/v1/suppressions")
assert status == 403 and body["error"] == "operation_not_allowed"

View file

@ -123,3 +123,12 @@ with every existing row preserved, UID10001/network-none and no send. See
docs/caller-deployment-2026-10-08.md. This is image/schema alignment only: T03/T04
archive custody, first admitted offsite run/retrieval/application recovery and
monitoring stay waiting. CSOC-WP-0015-T03 is the product acceptance follow-up.
## Pair-custody preview — 2026-10-08
Founder asked to start platform custody and stop before keys. railiance-platform
`scripts/application_backup_pair_custody.py` previews CCR-2026-0037: one future
CAS=0 write of both AWS fields, writer policy `backup-pair-writer` not admitted,
no OpenBao call and no Scaleway key. The delivery reader remains unable to
create the pair. T03/T04 stay wait. No receiver pause, upload, primary.env or
timer. Review: railiance-platform/docs/credential-lane-designs/application-backup-pair-custody-2026-10-08.md.

View file

@ -102,3 +102,15 @@ CSOC-WP-0015; EMAIL-WP-0008-T03, EMAIL-WP-0007 and RPF-WP-0053 retain their
canonical runtime/custody tasks. Existing personal recovery acceptance and the
receiver revision7 deployment remain evidenced. No pending owner requirement
is waived or marked satisfied by the original workplans' administrative closure.
## User-engine policy draft — 2026-10-08
Founder asked for a user-engine-only draft and no activation. Draft:
config/callers.user-engine.draft.json. Review: docs/caller-policy-draft-2026-10-08.md.
It names caller/source user-engine, binding tenant:coulomb/coulomb-social,
send, delivery-status, status, verify, consume and cancel, both family
templates, 10/minute, burst 5, 8192-byte cap, and legacy verification adoption.
No suppress operation and no token value. Current invitation envelopes omit
client_id, so this draft does not admit them. The token path is not created
and EMAIL_CONNECT_CALLERS_FILE is not set. T03 stays wait for approval of
these bindings, quotas and separate token custody.