mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
HIP-0106's plugin contract, in the framework rather than in 116 repositories. Declaration projects the LIVE ROUTER — every method+pattern, complete by construction, so a route a plugin serves and does not publish is still declared. That is the analytics-ingestion outage class: the published paths were listed, the four ingestion doors were not, and every product beacon in the fleet answered 405. Declaration/Route/(*App).Declaration what a host discovers /.well-known/zip/plugin.json the same value off a running child (*App).Described `<bin> openapi <f>` / `<bin> declare <f>` Tenant the tenancy read, with the real rule Delegate a continuation for another org Ask dial a peer by name, invoke one op (*App).Peer peer ops on their own app, own socket (*App).Ops /healthz + /readyz + /metrics Config.Eager the one fact a router cannot show App.claim a second claim of a prefix is an ERROR zip/runtime -> zip/js a language runtime is an adaptor Four things the reference plugin falsified while being built: - Describe was already taken. zip.Describe(methodPath, Doc) is the doc registry generated code calls. The projection writer is (*App).Described, one call, because argv is read in one place and a main cannot forget to forward it. - zip's control plane is NOT all under /.well-known/. /docs and the MCP path are not, so a prefix filter let a child declare them — paths outside /v1/ that every plugin serves and whose first claimant takes the HOST's document and agent door. App.control now registers and records in one call, so leaking one is impossible rather than unlikely. - OPS_PORT appeared in no Go file in the fleet. HIP-0119 named the paths and nobody implemented them, so conformance meant 116 repos hand-rolling three routes and a listener. It is one sibling app now, brought up by Listen. - There is deliberately NO default ops port. A default made every second app in a process die on "address already in use" — a plugin has two apps in it and a test process has many. The deployment states OPS_PORT; a host states none on a child, so a child correctly owns none. Load's duplicate claim used to return nil while the first registration kept answering: the losing plugin's whole surface served another plugin's 404 and nothing said so. It is now an error naming both claimants, all-or-nothing across a plugin's prefixes, taken before anything is spawned. a.ops was the op registry and is now a.registry — the ONE registry the OpenAPI document, the MCP tool, the CLI command and the plane op are all projected from. a.ops is the ops sibling. Co-authored-by: Hanzo Dev <[email protected]>
72 lines
3.0 KiB
Go
72 lines
3.0 KiB
Go
package zip
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
)
|
|
|
|
// The tenancy read, once, for every transport.
|
|
//
|
|
// "CallerOf(ctx).Org != empty" is NOT the check, and the reason is the whole of
|
|
// why this lives in zip rather than in each callee. X-Org-Id on its own is a
|
|
// client-settable header: a caller reaching a service directly — in-cluster, or
|
|
// on a public host whose edge has not yet been locked down — can present an org
|
|
// with NO validated user beside it. "The org is non-empty" is then a statement
|
|
// the caller made about itself, and the value is retained past the request as a
|
|
// store key and a ledger key.
|
|
//
|
|
// Where the principal COMES FROM is IAM's (HIP-0134): it is established once,
|
|
// nothing re-derives it, and a call carries it as delegation rather than
|
|
// assertion. Checking that one ARRIVED and is bounded before using it as a key
|
|
// is the callee's, and it is not a second implementation of IAM.
|
|
|
|
// MaxOrgLen bounds the org key. The org is the validated IAM owner claim — a
|
|
// short DNS-ish label — so anything longer is malformed or hostile and is
|
|
// refused before it can become a storage key or a namespace.
|
|
const MaxOrgLen = 128
|
|
|
|
// Tenant is the tenant a call may act for, or ("", false).
|
|
//
|
|
// It returns false — not an empty string a caller might use anyway — on each of
|
|
// three conditions, all of which are the same defect at different depths:
|
|
//
|
|
// no validated User claim the org that rode along is the caller's own assertion
|
|
// an empty Org there is no tenant to act for
|
|
// an Org over MaxOrgLen malformed or hostile, and it is about to be a key
|
|
//
|
|
// The org is returned VERBATIM apart from surrounding space: never lower-cased,
|
|
// never truncated. Folding collapses distinct owners ("acme", "ACME", a 32-char
|
|
// prefix) into one bucket, which is itself a cross-tenant break.
|
|
//
|
|
// A handler that touches per-tenant state calls this and refuses !ok. A handler
|
|
// that re-derives the rule is the sixth copy this function exists to delete.
|
|
func Tenant(ctx context.Context) (string, bool) {
|
|
c := CallerOf(ctx)
|
|
if strings.TrimSpace(c.User) == "" {
|
|
return "", false
|
|
}
|
|
org := strings.TrimSpace(c.Org)
|
|
if org == "" || len(org) > MaxOrgLen {
|
|
return "", false
|
|
}
|
|
return strings.Clone(org), true
|
|
}
|
|
|
|
// Delegate is the context a background continuation runs under: the in-flight
|
|
// request's caller, with the org replaced.
|
|
//
|
|
// It is for the one case a request handler legitimately acts for another
|
|
// tenant — a platform operator's fan-out, a cross-org reconciliation — and it
|
|
// keeps the ACTING user, so the audit trail still names a person. Passing an
|
|
// org through an argument instead is the thing §6 forbids: an org in the
|
|
// argument is an org the caller chose.
|
|
//
|
|
// The returned context carries no request, so it outlives the handler: both
|
|
// transports reuse the underlying request for the next call on the connection,
|
|
// and a goroutine holding one reads another caller's headers.
|
|
func Delegate(c *Ctx, org string) context.Context {
|
|
as := CallerOf(c.Forward())
|
|
as.Org = strings.TrimSpace(org)
|
|
return WithCaller(context.Background(), as)
|
|
}
|