Draft the inactive user-engine caller policy.
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:
parent
48823c8182
commit
a60a729421
5 changed files with 199 additions and 0 deletions
29
config/callers.user-engine.draft.json
Normal file
29
config/callers.user-engine.draft.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
42
docs/caller-policy-draft-2026-10-08.md
Normal file
42
docs/caller-policy-draft-2026-10-08.md
Normal 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.
|
||||
107
tests/test_caller_policy_draft.py
Normal file
107
tests/test_caller_policy_draft.py
Normal 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"
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue