Add producer-owned Go approval fact validation and canonical vectors
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
parent
eece796b8b
commit
3485415b82
14 changed files with 5655 additions and 0 deletions
55
adapters/go/approvalfact/README.md
Normal file
55
adapters/go/approvalfact/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Approval fact data adapter
|
||||
|
||||
Provider-owned pure Go module for Access's Approval B2 source reader. No network,
|
||||
credentials, subprocess, policy evaluator, server or consume mutation exists here.
|
||||
Python `approval_engine.binding` remains the authoritative native format.
|
||||
|
||||
API:
|
||||
|
||||
- `CanonicalBinding(rawJSON)` returns native five-field canonical UTF-8 bytes
|
||||
and sha256 digest; extra binding metadata is excluded.
|
||||
- `ValidateClaim(rawClaim, Expected, now)` validates one exact human/PDP-path
|
||||
claim against independently owner-derived `Expected.NativeBindingJSON`, exact
|
||||
approval ID and actual Access approval-free PDP digest.
|
||||
- `MatchObserved(submitted, observed)` requires two validated facts at the same
|
||||
captured instant and identical nonfreshness authority facts. No request is
|
||||
rewritten. `Fact.RawJSON()` returns a detached original byte copy; deadlines
|
||||
preserve each input's own freshness and approval expiry.
|
||||
|
||||
`Fact` means validated data, never authenticated provider origin. Access must
|
||||
fetch provider bytes through its independently admitted fixed origin/identity,
|
||||
then wrap this data in its own unforgeable source handle. A caller-supplied claim
|
||||
cannot become source truth merely by passing this helper. Access remains the
|
||||
single evaluator and must bound decision/operation/caller lifetimes separately.
|
||||
PEP consume CAS and live one-shot journal remain required before any effect.
|
||||
|
||||
Expected native act bytes must preserve the actual owner/compiler/normalization
|
||||
representation. Marshaling float64-decoded CheckRequest maps loses 1.0/1 and
|
||||
large integers. Raw submitted resource is sufficient only when Access proves
|
||||
the active registry cannot override that target and applies its exact existing
|
||||
normalization. Otherwise authoritative raw enrichment/compiler evidence is
|
||||
needed. This helper supplies no fallback or invented restricted numeric domain.
|
||||
|
||||
The strict parser retains integer/floating lexical types, arbitrary integer
|
||||
magnitudes, finite IEEE-754 float semantics and valid Unicode scalar values.
|
||||
Canonical float/string formatting follows Python source, including negative
|
||||
floating zero, exponent switching/padding, HTML and U+2028/U+2029. Duplicate
|
||||
object keys, nonfinite/overflow numbers, invalid UTF-8 and lone surrogates refuse.
|
||||
Inputs are bounded to 1 MiB and 128 container levels. Timestamp precision is at
|
||||
most nanoseconds with strict zoned RFC3339; current producer emits whole seconds.
|
||||
These explicit parser/resource limits fail closed, never normalize authority.
|
||||
|
||||
`go test -race ./...` checks the portable producer vectors, typed fact pairing,
|
||||
strict JSON/Unicode and exact deadline semantics. From the owner repo root,
|
||||
`python3 scripts/check_go_approvalfact.py` runs a deterministic 14,114-case
|
||||
development-only differential corpus against the actual Python producer,
|
||||
including random finite IEEE bit patterns, exponent boundaries/subnormals and
|
||||
Unicode. It uses a test-only local oracle file; production imports no oracle or
|
||||
Python runtime. Sample conformance is evidence, not a formal proof over all JSON.
|
||||
|
||||
The module pins Go 1.26.7 to match the existing Access consumer toolchain.
|
||||
`testdata/owner-vectors.json` is an unchanged portable copy of the Approval owner
|
||||
fixture, SHA256 `dfb0a8810cd91847d7af6a7af6a51607a67a66fad7b86fc57365308c36bd9323`.
|
||||
Transport cases requiring HTTP remain the Access adapter's tests; this module
|
||||
checks their duplicate/nonfinite bodies only. Import/deployment/native identity
|
||||
admission and positive policy gates require separate owner evidence.
|
||||
295
adapters/go/approvalfact/canonical.go
Normal file
295
adapters/go/approvalfact/canonical.go
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
// Package approvalfact provides data-only Approval claim correspondence.
|
||||
// It neither authenticates a provider nor decides permission or consumes objects.
|
||||
package approvalfact
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const MaxJSONBytes = 1 << 20
|
||||
|
||||
var errJSON = errors.New("invalid Approval JSON")
|
||||
|
||||
// DecodeJSON retains numeric lexical types, refuses duplicates and invalid
|
||||
// Unicode, and bounds work. A Go map previously decoded into float64 cannot
|
||||
// recover the producer's integer-versus-floating identity; pass source bytes.
|
||||
func DecodeJSON(raw []byte) (any, error) {
|
||||
if len(raw) > MaxJSONBytes || !utf8.Valid(raw) || !validSurrogates(raw) {
|
||||
return nil, errJSON
|
||||
}
|
||||
d := json.NewDecoder(bytes.NewReader(raw))
|
||||
d.UseNumber()
|
||||
var read func(int) (any, error)
|
||||
read = func(depth int) (any, error) {
|
||||
if depth > 128 {
|
||||
return nil, errJSON
|
||||
}
|
||||
t, e := d.Token()
|
||||
if e != nil {
|
||||
return nil, errJSON
|
||||
}
|
||||
switch x := t.(type) {
|
||||
case json.Delim:
|
||||
if x == '{' {
|
||||
m := map[string]any{}
|
||||
for d.More() {
|
||||
k, e := d.Token()
|
||||
if e != nil {
|
||||
return nil, errJSON
|
||||
}
|
||||
s, ok := k.(string)
|
||||
if !ok {
|
||||
return nil, errJSON
|
||||
}
|
||||
if _, ok = m[s]; ok {
|
||||
return nil, errJSON
|
||||
}
|
||||
v, e := read(depth + 1)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
m[s] = v
|
||||
}
|
||||
end, e := d.Token()
|
||||
if e != nil || end != json.Delim('}') {
|
||||
return nil, errJSON
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
if x == '[' {
|
||||
a := []any{}
|
||||
for d.More() {
|
||||
v, e := read(depth + 1)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
a = append(a, v)
|
||||
}
|
||||
end, e := d.Token()
|
||||
if e != nil || end != json.Delim(']') {
|
||||
return nil, errJSON
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
return nil, errJSON
|
||||
case json.Number:
|
||||
if _, e := pythonNumber(x); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return x, nil
|
||||
case string, bool, nil:
|
||||
return x, nil
|
||||
default:
|
||||
return nil, errJSON
|
||||
}
|
||||
}
|
||||
v, e := read(0)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if _, e = d.Token(); e != io.EOF {
|
||||
return nil, errJSON
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// encoding/json replaces lone UTF-16 surrogates, whereas the Python producer
|
||||
// cannot encode them as UTF-8. Reject before the decoder can erase that fact.
|
||||
func validSurrogates(raw []byte) bool {
|
||||
in := false
|
||||
for i := 0; i < len(raw); i++ {
|
||||
if raw[i] == '"' {
|
||||
in = !in
|
||||
continue
|
||||
}
|
||||
if !in || raw[i] != '\\' {
|
||||
continue
|
||||
}
|
||||
i++
|
||||
if i >= len(raw) {
|
||||
return false
|
||||
}
|
||||
if raw[i] != 'u' {
|
||||
continue
|
||||
}
|
||||
if i+4 >= len(raw) {
|
||||
return false
|
||||
}
|
||||
n, e := strconv.ParseUint(string(raw[i+1:i+5]), 16, 16)
|
||||
if e != nil {
|
||||
return false
|
||||
}
|
||||
i += 4
|
||||
if n >= 0xdc00 && n <= 0xdfff {
|
||||
return false
|
||||
}
|
||||
if n < 0xd800 || n > 0xdbff {
|
||||
continue
|
||||
}
|
||||
if i+6 >= len(raw) || raw[i+1] != '\\' || raw[i+2] != 'u' {
|
||||
return false
|
||||
}
|
||||
low, e := strconv.ParseUint(string(raw[i+3:i+7]), 16, 16)
|
||||
if e != nil || low < 0xdc00 || low > 0xdfff {
|
||||
return false
|
||||
}
|
||||
i += 6
|
||||
}
|
||||
return !in
|
||||
}
|
||||
|
||||
func pythonNumber(n json.Number) (string, error) {
|
||||
s := string(n)
|
||||
if !strings.ContainsAny(s, ".eE") {
|
||||
i, ok := new(big.Int).SetString(s, 10)
|
||||
if !ok {
|
||||
return "", errJSON
|
||||
}
|
||||
return i.String(), nil
|
||||
}
|
||||
f, e := strconv.ParseFloat(s, 64)
|
||||
if e != nil || math.IsInf(f, 0) || math.IsNaN(f) {
|
||||
return "", errJSON
|
||||
}
|
||||
// Python repr uses shortest round-trip digits, fixed form for exponents
|
||||
// -4..15, a mandatory .0 for integral floats, and two exponent digits.
|
||||
sci := strconv.FormatFloat(f, 'e', -1, 64)
|
||||
at := strings.IndexByte(sci, 'e')
|
||||
exp, _ := strconv.Atoi(sci[at+1:])
|
||||
if exp >= -4 && exp < 16 {
|
||||
s = strconv.FormatFloat(f, 'f', -1, 64)
|
||||
if !strings.Contains(s, ".") {
|
||||
s += ".0"
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
sign := "+"
|
||||
if exp < 0 {
|
||||
sign = "-"
|
||||
exp = -exp
|
||||
}
|
||||
digits := strconv.Itoa(exp)
|
||||
if len(digits) < 2 {
|
||||
digits = "0" + digits
|
||||
}
|
||||
return sci[:at] + "e" + sign + digits, nil
|
||||
}
|
||||
|
||||
func quote(s string) string {
|
||||
var b strings.Builder
|
||||
b.WriteByte('"')
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"', '\\':
|
||||
b.WriteByte('\\')
|
||||
b.WriteRune(r)
|
||||
case '\b':
|
||||
b.WriteString(`\b`)
|
||||
case '\f':
|
||||
b.WriteString(`\f`)
|
||||
case '\n':
|
||||
b.WriteString(`\n`)
|
||||
case '\r':
|
||||
b.WriteString(`\r`)
|
||||
case '\t':
|
||||
b.WriteString(`\t`)
|
||||
default:
|
||||
if r < 32 {
|
||||
b.WriteString(`\u00`)
|
||||
const h = "0123456789abcdef"
|
||||
b.WriteByte(h[byte(r)>>4])
|
||||
b.WriteByte(h[byte(r)&15])
|
||||
} else {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
b.WriteByte('"')
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func canonical(v any) (string, error) {
|
||||
switch x := v.(type) {
|
||||
case nil:
|
||||
return "null", nil
|
||||
case bool:
|
||||
if x {
|
||||
return "true", nil
|
||||
}
|
||||
return "false", nil
|
||||
case string:
|
||||
return quote(x), nil
|
||||
case json.Number:
|
||||
return pythonNumber(x)
|
||||
case []any:
|
||||
a := make([]string, len(x))
|
||||
for i, v := range x {
|
||||
s, e := canonical(v)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
a[i] = s
|
||||
}
|
||||
return "[" + strings.Join(a, ",") + "]", nil
|
||||
case map[string]any:
|
||||
keys := make([]string, 0, len(x))
|
||||
for k := range x {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
a := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
s, e := canonical(x[k])
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
a = append(a, quote(k)+":"+s)
|
||||
}
|
||||
return "{" + strings.Join(a, ",") + "}", nil
|
||||
default:
|
||||
return "", errJSON
|
||||
}
|
||||
}
|
||||
|
||||
// CanonicalBinding reproduces approval_engine.binding.binding_digest over the
|
||||
// five native act fields. Extra binding metadata is excluded by that contract.
|
||||
func CanonicalBinding(raw []byte) ([]byte, string, error) {
|
||||
v, e := DecodeJSON(raw)
|
||||
if e != nil {
|
||||
return nil, "", e
|
||||
}
|
||||
m, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
return nil, "", errJSON
|
||||
}
|
||||
act := map[string]any{}
|
||||
for _, k := range []string{"action", "actor", "principal", "purpose"} {
|
||||
s, ok := m[k].(string)
|
||||
if !ok || s == "" {
|
||||
return nil, "", errJSON
|
||||
}
|
||||
act[k] = s
|
||||
}
|
||||
target, ok := m["target"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, "", errJSON
|
||||
}
|
||||
act["target"] = target
|
||||
s, e := canonical(act)
|
||||
if e != nil {
|
||||
return nil, "", e
|
||||
}
|
||||
sum := sha256.Sum256([]byte(s))
|
||||
return []byte(s), "sha256:" + hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
170
adapters/go/approvalfact/claim.go
Normal file
170
adapters/go/approvalfact/claim.go
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
package approvalfact
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var errClaim = errors.New("Approval fact does not match required claim")
|
||||
var digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
||||
var uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
|
||||
var timePattern = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$`)
|
||||
|
||||
// Expected contains independently owner-derived native act bytes and the actual
|
||||
// Access approval-free digest. Marshaling an already float64-decoded Go request
|
||||
// is not an authoritative reconstruction of the native act.
|
||||
type Expected struct {
|
||||
ApprovalID string
|
||||
NativeBindingJSON []byte
|
||||
PDPDigest string
|
||||
}
|
||||
|
||||
// Fact is validated data shape/correspondence, NOT authenticated provider origin
|
||||
// or execution authority. Access must wrap it only after its own source admission.
|
||||
type Fact struct {
|
||||
raw []byte
|
||||
stable string
|
||||
digest string
|
||||
observed, timeLimit, validatedAt time.Time
|
||||
}
|
||||
|
||||
func (f Fact) RawJSON() []byte { return append([]byte(nil), f.raw...) }
|
||||
func (f Fact) ObservedAt() time.Time { return f.observed }
|
||||
func (f Fact) Deadline() time.Time { return f.timeLimit }
|
||||
func (f Fact) NativeDigest() string { return f.digest }
|
||||
|
||||
func members(m map[string]any, required, optional []string) bool {
|
||||
allowed := map[string]bool{}
|
||||
for _, k := range required {
|
||||
allowed[k] = true
|
||||
if _, ok := m[k]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, k := range optional {
|
||||
allowed[k] = true
|
||||
}
|
||||
for k := range m {
|
||||
if !allowed[k] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
func object(v any) (map[string]any, bool) { m, ok := v.(map[string]any); return m, ok }
|
||||
func stamp(v any) (time.Time, error) {
|
||||
s, ok := v.(string)
|
||||
if !ok || !timePattern.MatchString(s) {
|
||||
return time.Time{}, errClaim
|
||||
}
|
||||
t, e := time.Parse(time.RFC3339Nano, s)
|
||||
if e != nil {
|
||||
return time.Time{}, errClaim
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// ValidateClaim checks one original or fetched schema-0.1 human/PDP-path claim
|
||||
// at a caller-captured instant, using the existing Approval producer semantics.
|
||||
// It has no I/O, policy evaluator, credentials, cache, or consume mutation.
|
||||
func ValidateClaim(raw []byte, expected Expected, now time.Time) (Fact, error) {
|
||||
if now.IsZero() || !uuidPattern.MatchString(expected.ApprovalID) || !digestPattern.MatchString(expected.PDPDigest) {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
_, want, e := CanonicalBinding(expected.NativeBindingJSON)
|
||||
if e != nil {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
v, e := DecodeJSON(raw)
|
||||
if e != nil {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
m, ok := object(v)
|
||||
if !ok || !members(m, []string{"schema_version", "kind", "issuer", "approval_id", "state", "valid_now", "consumed", "binding", "freshness", "validity", "reason_code"}, []string{"yields_to"}) {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
if y, ok := m["yields_to"]; ok {
|
||||
if _, ok = y.(string); !ok {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
}
|
||||
if m["schema_version"] != "0.1" || m["kind"] != "approval-claim" || m["issuer"] != "approval-engine" || m["approval_id"] != expected.ApprovalID || m["state"] != "valid" || m["valid_now"] != true || m["consumed"] != false || m["reason_code"] != "ok" {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
b, ok := object(m["binding"])
|
||||
if !ok || !members(b, []string{"action", "actor", "principal", "purpose", "target", "digest", "pdp_digest", "pdp_path", "human_control"}, nil) {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
if b["digest"] != want || b["pdp_digest"] != expected.PDPDigest || b["pdp_path"] != true || b["human_control"] != true {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
text, e := canonical(b)
|
||||
if e != nil {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
_, got, e := CanonicalBinding([]byte(text))
|
||||
if e != nil || got != want {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
f, ok := object(m["freshness"])
|
||||
if !ok || !members(f, []string{"observed_at", "ttl_seconds", "not_after"}, nil) {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
observed, e := stamp(f["observed_at"])
|
||||
if e != nil {
|
||||
return Fact{}, e
|
||||
}
|
||||
notAfter, e := stamp(f["not_after"])
|
||||
if e != nil {
|
||||
return Fact{}, e
|
||||
}
|
||||
ttl, ok := f["ttl_seconds"].(json.Number)
|
||||
if !ok {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
seconds, e := strconv.ParseInt(string(ttl), 10, 64)
|
||||
if e != nil || seconds < 1 {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
// Bound arithmetic to time.Duration's representable seconds, not a new
|
||||
// semantic freshness grant or configured TTL. Unsupported extremes refuse.
|
||||
if seconds > int64((1<<63-1)/int64(time.Second)) || !observed.Add(time.Duration(seconds)*time.Second).Equal(notAfter) || now.Before(observed) || !now.Before(notAfter) {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
validity, ok := object(m["validity"])
|
||||
if !ok || !members(validity, []string{"not_before", "expires_at"}, nil) {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
before, e := stamp(validity["not_before"])
|
||||
if e != nil {
|
||||
return Fact{}, e
|
||||
}
|
||||
expires, e := stamp(validity["expires_at"])
|
||||
if e != nil || now.Before(before) || !now.Before(expires) {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
limit := notAfter
|
||||
if expires.Before(limit) {
|
||||
limit = expires
|
||||
}
|
||||
delete(m, "freshness")
|
||||
delete(m, "yields_to")
|
||||
stable, e := canonical(m)
|
||||
if e != nil {
|
||||
return Fact{}, errClaim
|
||||
}
|
||||
return Fact{raw: append([]byte(nil), raw...), stable: stable, digest: want, observed: observed, timeLimit: limit, validatedAt: now}, nil
|
||||
}
|
||||
|
||||
// MatchObserved requires independently validated original/fetched facts at the
|
||||
// same captured instant and exact nonfreshness authority correspondence. It
|
||||
// never rewrites the original request or extends its original deadline.
|
||||
func MatchObserved(submitted, observed Fact) error {
|
||||
if submitted.stable == "" || observed.stable == "" || !submitted.validatedAt.Equal(observed.validatedAt) || submitted.stable != observed.stable {
|
||||
return errClaim
|
||||
}
|
||||
return nil
|
||||
}
|
||||
144
adapters/go/approvalfact/claim_test.go
Normal file
144
adapters/go/approvalfact/claim_test.go
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package approvalfact
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type vectors struct {
|
||||
Native []struct {
|
||||
Binding json.RawMessage `json:"binding"`
|
||||
Canonical string `json:"canonical_binding_utf8"`
|
||||
Digest string `json:"native_digest"`
|
||||
} `json:"native_binding_vectors"`
|
||||
Claims []struct {
|
||||
Name string `json:"name"`
|
||||
Now time.Time `json:"validation_time"`
|
||||
Submitted json.RawMessage `json:"submitted_claim"`
|
||||
Observed json.RawMessage `json:"observed_claim"`
|
||||
Binding json.RawMessage `json:"expected_binding"`
|
||||
PDP string `json:"expected_pdp_digest"`
|
||||
Accept bool `json:"expected_accept"`
|
||||
} `json:"claim_cases"`
|
||||
Transport []struct {
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body_utf8"`
|
||||
} `json:"transport_cases"`
|
||||
}
|
||||
|
||||
func load(t *testing.T) vectors {
|
||||
t.Helper()
|
||||
raw, e := os.ReadFile("testdata/owner-vectors.json")
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
var v vectors
|
||||
if e = json.Unmarshal(raw, &v); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func TestProducerCanonicalVectors(t *testing.T) {
|
||||
for _, v := range load(t).Native {
|
||||
t.Run(v.Digest, func(t *testing.T) {
|
||||
raw, d, e := CanonicalBinding(v.Binding)
|
||||
if e != nil || string(raw) != v.Canonical || d != v.Digest {
|
||||
t.Fatalf("producer parity: %q %s %v, want %q %s", raw, d, e, v.Canonical, v.Digest)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProducerClaimVectors(t *testing.T) {
|
||||
for _, v := range load(t).Claims {
|
||||
t.Run(v.Name, func(t *testing.T) {
|
||||
var id struct {
|
||||
ID string `json:"approval_id"`
|
||||
}
|
||||
if e := json.Unmarshal(v.Submitted, &id); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
expected := Expected{id.ID, v.Binding, v.PDP}
|
||||
old, e1 := ValidateClaim(v.Submitted, expected, v.Now)
|
||||
fresh, e2 := ValidateClaim(v.Observed, expected, v.Now)
|
||||
accepted := e1 == nil && e2 == nil && MatchObserved(old, fresh) == nil
|
||||
if accepted != v.Accept {
|
||||
t.Fatalf("accepted=%v want=%v original=%v observed=%v", accepted, v.Accept, e1, e2)
|
||||
}
|
||||
if accepted {
|
||||
raw := old.RawJSON()
|
||||
if !bytes.Equal(raw, v.Submitted) {
|
||||
t.Fatal("original bytes changed")
|
||||
}
|
||||
raw[0] = 'x'
|
||||
if old.RawJSON()[0] != '{' {
|
||||
t.Fatal("mutable fact storage")
|
||||
}
|
||||
if !old.Deadline().Before(fresh.Deadline()) {
|
||||
t.Fatal("refresh must not extend original deadline")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictJSONRefusals(t *testing.T) {
|
||||
for _, raw := range []string{`{"a":1,"a":2}`, `{"a":{"x":1,"x":2}}`, `{"a":NaN}`, `{"a":1e999}`, `{"a":"\ud800"}`, `{"a":"\udfff"}`, `{"a":"\ud800x"}`, `{"a":1} {}`, `{"a":"` + string([]byte{0xff}) + `"}`, strings.Repeat("[", 130) + strings.Repeat("]", 130), strings.Repeat(" ", MaxJSONBytes+1)} {
|
||||
if _, e := DecodeJSON([]byte(raw)); e == nil {
|
||||
t.Fatalf("accepted malformed JSON %.80q", raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnicodeAndControlEncoding(t *testing.T) {
|
||||
for _, raw := range []string{`{"v":"\ud83d\ude00"}`, `{"v":"escaped \\\" quote"}`, `{"v":"\u0000\b\f\n\r\t"}`} {
|
||||
if _, e := DecodeJSON([]byte(raw)); e != nil {
|
||||
t.Fatalf("refused valid Unicode %q", raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenDuplicateAndNonfiniteBodies(t *testing.T) {
|
||||
for _, v := range load(t).Transport {
|
||||
if v.Name == "duplicate_valid_now" || v.Name == "nonfinite_json" {
|
||||
if _, e := DecodeJSON([]byte(v.Body)); e == nil {
|
||||
t.Fatal(v.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidExpectedAndZeroFacts(t *testing.T) {
|
||||
v := load(t).Claims[0]
|
||||
if _, e := ValidateClaim(v.Submitted, Expected{}, v.Now); e == nil {
|
||||
t.Fatal("missing expected act accepted")
|
||||
}
|
||||
if MatchObserved(Fact{}, Fact{}) == nil {
|
||||
t.Fatal("zero facts accepted")
|
||||
}
|
||||
expected := Expected{"11111111-1111-4111-8111-111111111111", v.Binding, v.PDP}
|
||||
a, e := ValidateClaim(v.Submitted, expected, v.Now)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
b, e := ValidateClaim(v.Observed, expected, v.Now.Add(time.Second))
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if MatchObserved(a, b) == nil {
|
||||
t.Fatal("different validation instants accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimestampPrecisionAndOffsetRefusals(t *testing.T) {
|
||||
for _, value := range []string{"2026-08-29T12:00:00,1Z", "2026-08-29T12:00:00+24:00", "2026-08-29T12:00:00+00:60", "2026-08-29T12:00:00.0000000001Z", "2026-08-29T12:00:00"} {
|
||||
if _, err := stamp(value); err == nil {
|
||||
t.Fatalf("accepted unsupported timestamp %q", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
3
adapters/go/approvalfact/go.mod
Normal file
3
adapters/go/approvalfact/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/netkingdom/approval-engine/adapters/go/approvalfact
|
||||
|
||||
go 1.26.7
|
||||
42
adapters/go/approvalfact/oracle_test.go
Normal file
42
adapters/go/approvalfact/oracle_test.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package approvalfact
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// This test-only input is produced by scripts/check_go_approvalfact.py using
|
||||
// the actual Python owner primitive. Production has no subprocess or oracle.
|
||||
func TestPythonDifferential(t *testing.T) {
|
||||
path := os.Getenv("APPROVAL_TEST_ORACLE")
|
||||
if path == "" {
|
||||
t.Skip("run scripts/check_go_approvalfact.py for Python differential corpus")
|
||||
}
|
||||
f, e := os.Open(path)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
defer f.Close()
|
||||
s := bufio.NewScanner(f)
|
||||
count := 0
|
||||
for s.Scan() {
|
||||
var v struct{ Raw, Canonical, Digest string }
|
||||
if e = json.Unmarshal(s.Bytes(), &v); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
raw, d, e := CanonicalBinding([]byte(v.Raw))
|
||||
if e != nil || string(raw) != v.Canonical || d != v.Digest {
|
||||
t.Fatalf("Python differential row %d: got %q %s %v want %q %s", count, raw, d, e, v.Canonical, v.Digest)
|
||||
}
|
||||
count++
|
||||
}
|
||||
if e = s.Err(); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if count < 10000 {
|
||||
t.Fatalf("incomplete differential corpus: %d", count)
|
||||
}
|
||||
t.Logf("matched %d Python source canonical bytes and digests", count)
|
||||
}
|
||||
2305
adapters/go/approvalfact/testdata/owner-vectors.json
vendored
Normal file
2305
adapters/go/approvalfact/testdata/owner-vectors.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
137
docs/access-claim-fact-reader.md
Normal file
137
docs/access-claim-fact-reader.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Thin Access Approval fact reader — owner source contract proposal
|
||||
|
||||
Owner scope: Approval Engine / PIP. Work: APPROVAL-WP-0005-T01, selected source
|
||||
subset of APPROVAL-WP-0002-T01/T05 under CUST-WP-0080. This interprets the existing
|
||||
claim schema 0.1, not a new API/schema/signature or policy grant. Access's B2
|
||||
implementation stays DENY until its separately reviewed complete successor.
|
||||
|
||||
## Authoritative source and guarantee
|
||||
|
||||
Only server-installed, independently admitted provider origin and transport may
|
||||
GET `/v1/approvals/{id}/claim`, with least-scope `approval:read` credential and
|
||||
exact admitted store tenant. No caller URL/opener/key/verified flag, redirects,
|
||||
environment proxy, caller JSON or public same-channel key bootstrap can supply
|
||||
this provenance. Owner transport must enforce total deadline/size, exact 200
|
||||
application/json UTF-8, no encoding, duplicate-key/nonfinite/malformed JSON
|
||||
refusal, no retry and bounded errors without bearer/body leakage. Provider
|
||||
unavailability refuses this source join. Never invoke a credential-carrying
|
||||
Python subprocess as the Go adapter; reuse Go's existing transport boundary.
|
||||
|
||||
At the provider observation, `valid_now: true` AND `binding.human_control: true`
|
||||
means the configured required count of distinct entries is met and every entry
|
||||
has verified `principal_type=human`. `/entries` obtains identities/types only from
|
||||
its authenticator, rejects non-human binds for this declaration, and persists
|
||||
UNIQUE(approval_id, subject_id); `_evaluate` repeats the threshold/human check.
|
||||
It does NOT mean required_count is two, requester/actor/principal is human,
|
||||
directory-derived tenant membership, a particular MFA/assurance level, policy
|
||||
eligibility, uncompromised source, native issuer admission, or continuing
|
||||
validity after the observation. Consumer trusts this owner evaluation instead
|
||||
of fetching identities/counting approvers or revalidating human JWTs. Native
|
||||
issuer/profile acceptance is still T01, not established by synthetic vectors.
|
||||
|
||||
## Minimal typed fact validation
|
||||
|
||||
The submitted claim and fetched observation must each satisfy existing schema
|
||||
0.1 with exact JSON types; `human_control` must be present and exactly true on
|
||||
this lane despite being historically optional in the generic schema. Require
|
||||
schema_version=0.1, kind=approval-claim, issuer=approval-engine, exact expected
|
||||
approval UUID, state=valid, valid_now=true, consumed=false, reason_code=ok,
|
||||
pdp_path=true and non-null lowercase sha256 pdp_digest. Reject decision-shaped
|
||||
keys and unknown schema members (except optional informational yields_to).
|
||||
The signed/requesting service must never become a human approver by inference.
|
||||
|
||||
At one captured UTC validation instant now, require zoned RFC3339 timestamps,
|
||||
positive integral ttl_seconds, observed_at<=now<not_after and
|
||||
not_after=observed_at+ttl_seconds. Require not_before<=now<expires_at. Do not
|
||||
invent skew or extend TTL; a separately admitted clock policy is needed to
|
||||
relax the zero-skew default. The provider default TTL is 30 seconds but source
|
||||
configuration may differ; an Access-owned maximum may only shorten it.
|
||||
|
||||
Compare exact five-field act against the owner-derived normalized proposed act,
|
||||
including all nested target values; no action vocabulary translation is owned
|
||||
here. Recompute native binding.digest from those five fields and require exact
|
||||
served and expected digest correspondence. Match pdp_digest to Access's actual
|
||||
ApprovalBindingDigest over the exact normalized original request (excludes only
|
||||
top-level context.approval). Preserve execution_contract and all other context.
|
||||
Never substitute full request_digest, native act digest or a guessed Go digest.
|
||||
The PDP digest proves correspondence, not decision authenticity or approval
|
||||
source authenticity. Registry/normalization changes must fail or reissue.
|
||||
|
||||
## Original request is immutable; fresh observation stays out-of-band
|
||||
|
||||
The original submitted context.approval must remain byte/value-equivalent under
|
||||
Access's existing canonical request rules. Do not replace it, update freshness,
|
||||
change binding/declarations, or manufacture an evaluated refreshed request.
|
||||
Validate its own original freshness/validity too; a newly fetched observation
|
||||
cannot rescue a stale original claim. Require all non-freshness authority facts
|
||||
to match between the submitted and fetched claims: schema/kind/issuer/id,
|
||||
state/valid_now/consumed/reason_code, binding and validity. Optional yields_to
|
||||
is informational only. Each native binding is independently recomputed.
|
||||
Freshness is validated separately on both observations; a later observation
|
||||
may have a later freshness window without changing the signed request.
|
||||
|
||||
Return a package-private/server-created typed observation handle only after
|
||||
these checks. Access may bind its source identity/revision, observed_at,
|
||||
not_after, fetched-claim content digest and validation time into owner-produced
|
||||
signed provenance separately from unchanged request_digest and
|
||||
approval_binding_digest. This does not require changing the Approval schema;
|
||||
Access owns the minimal existing provenance representation and signer contract.
|
||||
An HTTP caller cannot instantiate this handle or set its verified state.
|
||||
|
||||
Suggested Go boundary, for Access to implement in its existing process:
|
||||
`ApprovalFactSource.Observe(ctx, approvalID) (Observation, error)` reads only
|
||||
provider bytes plus source provenance; a separate package-private validator
|
||||
joins Observation with the immutable normalized request, submitted claim and
|
||||
captured validation time. The handle retains detached raw provider bytes and
|
||||
strict parsed numbers (json.Number), not caller-installable validation flags.
|
||||
Neither this reader nor its validator evaluates policy. Access's existing
|
||||
single policy evaluator consumes the verified factual handle without rewriting
|
||||
the request. Callers may not supply a ready-made Observation on the HTTP body.
|
||||
|
||||
Effective input deadline is the minimum of ORIGINAL submitted freshness,
|
||||
fetched freshness, approval expiry, authenticated caller expiry and operation/
|
||||
decision deadlines. No equality at an expiry boundary is accepted. Refetching
|
||||
does not renew the original full request. Revocation/use may race a read; the
|
||||
PEP still performs one original exact consume CAS before effect and retains
|
||||
the same live Platform journal admission, deadlines and unknown-outcome rules.
|
||||
Access never consumes or executes. Historical confirm_consumption cannot
|
||||
substitute for this fresh predecision source validation.
|
||||
|
||||
## Go canonicalization and golden vectors
|
||||
|
||||
`examples/access-claim-reader-vectors.json` contains producer-generated native
|
||||
canonical UTF-8 bytes/digests and source lifecycle observations, plus explicitly
|
||||
mutated refusal fixtures. Python dict equality and Go decoding numbers into
|
||||
float64 lose authority distinctions: True/1, 1/1.0 and integers above 2^53 differ.
|
||||
Go default json.Marshal additionally escapes HTML/U+2028 differently. Retain
|
||||
number tokens with UseNumber and implement/test compatibility with Approval's
|
||||
existing Python json.dumps(sort_keys=True,separators=(',', ':'),ensure_ascii=False)
|
||||
format; do not silently normalize types or hash whole binding/freshness.
|
||||
UseNumber preserves number tokens but is NOT a canonicalizer: producer parsing
|
||||
of `1e-7` emits `1e-07`, `1e20` emits `1e+20`, `1.0` remains distinct from `1`,
|
||||
and negative floating zero emits `-0.0`. A signed typed Go envelope alone does
|
||||
not preserve or authenticate Python's original numeric representation.
|
||||
|
||||
These finite vectors are a minimum, not proof of a complete Python float/string
|
||||
port. A thin pilot may fail closed on unsupported numeric/string domains only
|
||||
if its actual owner-compiled operation contract excludes those values and Access
|
||||
declares/tests that boundary. It may not silently change the global Approval
|
||||
algorithm or accept a digest without checking the proposed act. Required vectors
|
||||
cover bool/int/float distinction, large integer, Unicode/HTML/U+2028, reordered
|
||||
keys, nested containers, exponent formatting and negative floating zero. These
|
||||
are authoritative producer vectors for the domains shown, not arbitrary new
|
||||
restrictions on current pilot metadata. No currently authoritative narrow numeric
|
||||
restriction was established by this review, so it cannot be presumed. If a
|
||||
small shared producer-owned Go canonicalization helper becomes necessary, scope
|
||||
it explicitly to the actual consumer need and test against these producer bytes;
|
||||
do not invent a second unverified serialization algorithm in the adapter. This
|
||||
initial review provided vectors only. Subsequent scoped WP0005-T03 supplies
|
||||
`adapters/go/approvalfact`, a producer-owned pure data implementation agreed with
|
||||
Access; it adds no credential transport/runtime. The Go consumer owner must
|
||||
return exact accepted/refused vector results and transport negatives
|
||||
(redirect/proxy/duplicate/size/deadline).
|
||||
|
||||
The superseded service-http-join-proposal-r1 B3 refreshed-evaluation wording is
|
||||
changes-requested for this immutable-context path. B2's source reader can proceed
|
||||
against this proposal after Access's own design/conformance review. No foreign
|
||||
owner assent, policy activation, live credential or native acceptance is implied.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"reviewer": "approval-engine-owner (agent approval-engine, codex)",
|
||||
"decision": "changes_requested",
|
||||
"comment": "INTENT Purpose and exact binding/claim boundary: B2 can reuse existing GET claim schema0.1 and verified human-control predicate without republishing approver identities. Superseded B3 refreshed evaluated-request/context design cannot satisfy the exact original strict handoff; retain original context/full and approval-free digests and verify fresh provider observation out-of-band. Work APPROVAL-WP-0005-T01; source base eece796b8bbee19551f6aa07ad389cbf83cabcdb. No policy activation, issuer admission, foreign owner assent or native acceptance.",
|
||||
"reviewed_artifact": "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/access-native-source/service-http-join-proposal-r1.json",
|
||||
"reviewed_sha256": "e0f8f12c48faf2331cf366591028577beffe617250d7005dcaf2a8b7396535fc",
|
||||
"owner_contract": "docs/access-claim-fact-reader.md",
|
||||
"owner_contract_sha256": "3dfb7987a4d9fe461b917cfa5bae7beef61bc4d9a4561fdded3e90128f5966de",
|
||||
"required_returns": [
|
||||
"Access own B2 contract/conformance review and exact native canonicalization parity; original submitted context/digests retained",
|
||||
"Owner-admitted read identity/store tenant and trusted provider transport separate from source fixture tests",
|
||||
"Actual runtime source integration and original consumer/journal/cas path before any policy eligibility successor"
|
||||
],
|
||||
"native_accepted": false
|
||||
}
|
||||
2305
examples/access-claim-reader-vectors.json
Normal file
2305
examples/access-claim-reader-vectors.json
Normal file
File diff suppressed because it is too large
Load diff
53
scripts/check_go_approvalfact.py
Normal file
53
scripts/check_go_approvalfact.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""Development-only Go parity check against the actual Python producer."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from approval_engine.binding import binding_digest, canonical_binding, canonical_json
|
||||
|
||||
|
||||
def main():
|
||||
rng = random.Random(20261016)
|
||||
values = [0.0, -0.0, 5e-324, -5e-324, sys.float_info.max,
|
||||
sys.float_info.min, 1.0, 1e-4, 1e-5, 1e15, 1e16, 1e20, 1e21]
|
||||
for _ in range(12000):
|
||||
value = struct.unpack(">d", rng.getrandbits(64).to_bytes(8, "big"))[0]
|
||||
if math.isfinite(value):
|
||||
values.append(value)
|
||||
for power in range(-323, 309):
|
||||
value = float("1e" + str(power))
|
||||
values.extend([value, math.nextafter(value, 0.0), math.nextafter(value, math.inf)])
|
||||
targets = [{"value": value} for value in values if math.isfinite(value)]
|
||||
targets += [{"value": n} for n in [0, -1, 1, 2**53+1, 10**100, -(10**100)]]
|
||||
targets += [json.loads('{"value":' + token + '}') for token in
|
||||
["-0", "-0.0", "1E0", "1.000", "100e-2", "1e-07", "1e+20"]]
|
||||
targets += [{"text": "".join(chr(i) for i in range(32)) + '<>&é雪😀\u2028\u2029'}]
|
||||
for _ in range(200):
|
||||
points = [rng.randrange(0x110000) for _ in range(20)]
|
||||
targets.append({"text": "".join(chr(c) for c in points if not 0xD800 <= c <= 0xDFFF)})
|
||||
with tempfile.TemporaryDirectory(prefix="approval-go-oracle-") as tmp:
|
||||
path = Path(tmp) / "oracle.jsonl"
|
||||
with path.open("w") as stream:
|
||||
for target in targets:
|
||||
act = dict(action="execute-ccr", actor="service:secrets-engine",
|
||||
principal="principal", purpose="differential", target=target)
|
||||
row = {"Raw": json.dumps(act, ensure_ascii=False, allow_nan=False),
|
||||
"Canonical": canonical_json(canonical_binding(act)).decode(),
|
||||
"Digest": binding_digest(act)}
|
||||
stream.write(json.dumps(row, ensure_ascii=True, allow_nan=False) + "\n")
|
||||
env = dict(os.environ, APPROVAL_TEST_ORACLE=str(path))
|
||||
subprocess.run(["go", "test", "-count=1", "-run", "TestPythonDifferential", "-v", "./..."],
|
||||
cwd=ROOT / "adapters/go/approvalfact", env=env, check=True)
|
||||
print(f"Python producer corpus: {len(targets)} cases")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
42
tests/test_access_claim_vectors.py
Normal file
42
tests/test_access_claim_vectors.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"""Producer facts/canonical vectors only; not a Go consumer acceptance test."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import jsonschema
|
||||
|
||||
from approval_engine.binding import binding_digest, canonical_binding, canonical_json
|
||||
from approval_engine.store import Engine
|
||||
from tests.conftest import FROZEN, binding, validity
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
VECTORS = json.loads((ROOT / "examples/access-claim-reader-vectors.json").read_text())
|
||||
|
||||
|
||||
def test_native_vectors_match_actual_producer_bytes_and_digest():
|
||||
for case in VECTORS["native_binding_vectors"]:
|
||||
act = binding(target=json.loads(case["input_target_json"]))
|
||||
assert act == case["binding"]
|
||||
assert canonical_json(canonical_binding(act)).decode() == case["canonical_binding_utf8"]
|
||||
assert binding_digest(act) == case["native_digest"]
|
||||
digests = [v["native_digest"] for v in VECTORS["native_binding_vectors"]]
|
||||
assert len(set(digests)) == len(digests)
|
||||
|
||||
|
||||
def test_positive_claim_vector_matches_actual_provider_and_schema():
|
||||
case = VECTORS["claim_cases"][0]
|
||||
engine = Engine(clock=lambda: FROZEN)
|
||||
obj = engine.create(binding(), validity(), approval_id=case["submitted_claim"]["approval_id"],
|
||||
required_count=2, pdp_digest=case["expected_pdp_digest"],
|
||||
pdp_path=True, human_control=True)
|
||||
engine.add_entry(obj.id, "human:one", principal_type="human")
|
||||
assert engine.claim(obj.id)["valid_now"] is False
|
||||
engine.add_entry(obj.id, "human:two", principal_type="human")
|
||||
assert engine.claim(obj.id) == case["submitted_claim"]
|
||||
from datetime import timedelta
|
||||
engine.clock = lambda: FROZEN + timedelta(seconds=5)
|
||||
assert engine.claim(obj.id) == case["observed_claim"]
|
||||
assert case["submitted_claim"]["freshness"] != case["observed_claim"]["freshness"]
|
||||
schema = json.loads((ROOT / "schemas/approval_claim.schema.json").read_text())
|
||||
jsonschema.Draft202012Validator(schema, format_checker=jsonschema.FormatChecker()).validate(
|
||||
case["observed_claim"])
|
||||
engine.close()
|
||||
|
|
@ -26,6 +26,12 @@ state_hub_workstream_id: "4fa25ad5-f5d0-5592-aa59-085f8ee3edaf"
|
|||
|
||||
# APPROVAL-WP-0002 — Production readiness and consumer adoption
|
||||
|
||||
2026-10-16 additional scoped demand before Access B2 implementation:
|
||||
CUST-WP-0080 selects T01/T05's authoritative claim/human-control/freshness and
|
||||
exact-source consumer contract review and producer fixtures only, tracked in
|
||||
APPROVAL-WP-0005. Existing residual/native statuses and authority gates remain;
|
||||
this does not activate other residual work, a consumer policy or live transport.
|
||||
|
||||
Move the completed first-cut engine spine into an authenticated, durable,
|
||||
observable production service and prove one PEP integration end to end. This is
|
||||
the residual production scope deliberately excluded from APPROVAL-WP-0001.
|
||||
|
|
|
|||
83
workplans/APPROVAL-WP-0005-access-claim-fact-contract.md
Normal file
83
workplans/APPROVAL-WP-0005-access-claim-fact-contract.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
id: APPROVAL-WP-0005
|
||||
type: workplan
|
||||
title: "Approval fact contract and reusable Go data adapter"
|
||||
domain: infotech
|
||||
repo: approval-engine
|
||||
status: active
|
||||
flavor: implementation
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-10-16"
|
||||
updated: "2026-10-16"
|
||||
depends_on: []
|
||||
---
|
||||
|
||||
CUST-WP-0080 explicitly selects the necessary Approval-WP-0002-T01/T05 source
|
||||
contract subset for the Access service HTTP B2 join. Initial scope is owner
|
||||
review and producer fixtures; subsequent explicit CUST-WP-0080 demand selects
|
||||
T03's pure reusable data adapter, agreed with Access before implementation.
|
||||
No broad residual activation, credential transport runtime, trust admission,
|
||||
policy eligibility or live action is included.
|
||||
|
||||
## Review authoritative claim guarantees and publish thin consumer vectors
|
||||
|
||||
```task
|
||||
id: APPROVAL-WP-0005-T01
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
INTENT basis: factual exact-bound approval claims supplied to Access, never a
|
||||
second PDP. Source contract documents verified human-control semantics and their
|
||||
issuer admission limit, both original/subsequent observation bounds, exact
|
||||
native/PDP correspondence and immutable original submitted context. Producer
|
||||
vectors cover current lifecycle and canonical types. The superseded proposal's
|
||||
request-refresh design requires change; fresh facts are verified out-of-band.
|
||||
|
||||
## Independently integrate owner contract and coordinate Access reader conformance
|
||||
|
||||
```task
|
||||
id: APPROVAL-WP-0005-T02
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
Coordinator reviews/integrates these source records. Access owns the thin Go
|
||||
reader/normalization and conformance implementation under FLEX-WP-0024-T06;
|
||||
Secrets owns strict immutable handoff and Platform owns provider origin/custody
|
||||
and final client pin. Native issuer/profile and served-PEP obligations remain
|
||||
live under APPROVAL-WP-0002-T01/T05, with audit under T04. No new native residual
|
||||
is created, promoted or marked complete by these source-only fixtures.
|
||||
|
||||
## Supply producer-owned reusable Go claim and native binding data adapter
|
||||
|
||||
```task
|
||||
id: APPROVAL-WP-0005-T03
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Explicit bounded implementation demand after Access confirmed the exact module
|
||||
and raw-JSON API: adapters/go/approvalfact contains the existing Python-native
|
||||
five-field canonicalization port and strict human/PDP-path typed claim validation
|
||||
plus original/fresh fact matching. It is pure data code, not a source transport,
|
||||
credential runtime, policy evaluator or consume service. Fact means validated
|
||||
shape/correspondence, never authenticated provider origin.
|
||||
|
||||
Fourteen pinned native vectors and 24 claim cases pass in Go, with strict
|
||||
duplicate/Unicode/type/expiry defenses and detached original bytes. A 14,114-case
|
||||
deterministic differential corpus matches actual Python producer canonical bytes
|
||||
and digests; Go race tests pass. This is source sample evidence, not exhaustive
|
||||
proof or native acceptance. An initial module toolchain selection failure and
|
||||
shared cache sandbox refusal are retained in preparation evidence; final module
|
||||
uses Access's existing Go1.26.7 and completed tests.
|
||||
|
||||
Access explicitly retains the expected-native-raw-source correspondence gate:
|
||||
its current decoded float64 maps cannot recover Python float/int identity.
|
||||
The current pilot's raw resource may be used only with proof of no active target
|
||||
registry override plus exact existing tenant normalization. Other enrichment
|
||||
needs authoritative raw source/compiler representation. No numeric restriction,
|
||||
digest substitution, foreign review, runtime enable or live proof is invented.
|
||||
T02 still owns independent source integration/handoff; existing T01/T05 native
|
||||
residual obligations and actual Access conformance/integration remain unchanged.
|
||||
Loading…
Add table
Add a link
Reference in a new issue