oficina / memoria Public

Clone
git clone https://amadan.net/oficina/memoria

Plain git — no account needed to clone.

Download

Download this file

1package memoria
2
3import (
4 "encoding/base64"
5 "errors"
6 "fmt"
7
8 "amadan.net/oficina/home/connect"
9
10 "amadan.net/oficina/memoria/internal/memoria/seal"
11)
12
13// Config is everything memoria needs from platform config.
14//
15// This is a type with a constructor, rather than a run of os.Getenv calls
16// in main, for one reason: the refusals below are a safety property and a
17// safety property needs a test. An earlier version of this validation
18// lived inline in main.go and was "checked" by grepping the source for
19// os.Exit near each variable name — which passed even when the refusal
20// was deleted, because a neighbouring error path supplied the os.Exit the
21// grep was looking for. A guard that cannot fail is not a guard.
22type Config struct {
23 Origin string
24 InstanceKey string
25 Owners []string
26 Sealer *seal.Sealer
27 Home *connect.DecodedCredentials
28 // AllowLoggedMail permits booting with no SMTP, which makes
29 // rastrillo substitute mail.Logged — and mail.Logged writes the
30 // magic link, a live credential, into the process log at Warn.
31 // `carlos logs` is platform-held storage we cannot grep, so that is
32 // a credential we cannot retract. Opt in explicitly for local dev.
33 AllowLoggedMail bool
34
35 // AI is the team owner's opt-in to the model layer (decision 4). With
36 // it off, nothing is ever sent to a provider and §8.9 does nothing.
37 AI bool
38 // TensorXKey is the provider credential. Required when AI is on,
39 // ignored when it is off. Never logged, never in an error.
40 TensorXKey string
41}
42
43// ErrMissing is returned when required platform config is absent.
44var ErrMissing = errors.New("memoria: required configuration is missing")
45
46// LoadConfig reads and validates configuration. getenv is a parameter so
47// a test can supply an environment without mutating the process's.
48//
49// Every failure here is fatal by design. There is deliberately no
50// generated-fallback DEK: a fallback would seal this boot's corpus under
51// a key the next boot cannot derive, losing the data quietly instead of
52// refusing loudly. And there is no default owner list, because an empty
53// admission gate on a public hostname admits anyone who can receive mail.
54func LoadConfig(getenv func(string) string) (Config, error) {
55 var c Config
56
57 dekRaw := getenv("MEMORIA_DEK")
58 if dekRaw == "" {
59 return c, fmt.Errorf("%w: MEMORIA_DEK", ErrMissing)
60 }
61 dek, err := base64.StdEncoding.DecodeString(dekRaw)
62 if err != nil {
63 return c, fmt.Errorf("memoria: MEMORIA_DEK is not valid base64: %w", err)
64 }
65 sealer, err := seal.New(dek)
66 if err != nil {
67 return c, fmt.Errorf("memoria: MEMORIA_DEK rejected: %w", err)
68 }
69 c.Sealer = sealer
70
71 if c.InstanceKey = getenv("MEMORIA_INSTANCE_KEY"); c.InstanceKey == "" {
72 return c, fmt.Errorf("%w: MEMORIA_INSTANCE_KEY", ErrMissing)
73 }
74
75 if c.Owners = ParseOwners(getenv("MEMORIA_OWNERS")); len(c.Owners) == 0 {
76 return c, fmt.Errorf("%w: MEMORIA_OWNERS (no admission gate)", ErrMissing)
77 }
78
79 // Refuse to log credentials by default.
80 //
81 // The prefix is CARLOS, not MEMORIA: rastrillo's mail.FromEnv takes
82 // the prefix as an argument and the suite passes "CARLOS", so the
83 // platform config is CARLOS_SMTP_*. An earlier version of this check
84 // read MEMORIA_SMTP_HOST, which no deployment sets — so it would have
85 // refused to boot on a correctly configured instance, and the obvious
86 // fix under deadline would have been to delete the check.
87 //
88 // Both HOST and FROM, because mail.FromEnv falls back to the logging
89 // sender unless it has both. Checking only HOST would let a
90 // half-configured instance log links while this function reported
91 // everything fine.
92 c.AllowLoggedMail = getenv("MEMORIA_ALLOW_LOGGED_MAIL") == "1"
93 if !c.AllowLoggedMail {
94 if getenv("CARLOS_SMTP_HOST") == "" || getenv("CARLOS_SMTP_FROM") == "" {
95 return c, fmt.Errorf("%w: CARLOS_SMTP_HOST and CARLOS_SMTP_FROM (or MEMORIA_ALLOW_LOGGED_MAIL=1 to log sign-in links instead, which puts a live credential in a log nobody can delete)", ErrMissing)
96 }
97 }
98
99 // The model layer: off unless the owner says "on", in exactly those
100 // words (decision 4 — opt-in per team).
101 //
102 // **Only "on" and "" are accepted.** A typo — "yes", "true", "On" —
103 // is a refusal, not a silent off: an owner who meant to switch AI on
104 // and got nothing would be told nothing, and an owner who meant to
105 // switch it OFF and wrote "false" must not be read as meaning on.
106 // Refusing both is the only reading that cannot be wrong.
107 //
108 // And a key with AI off does NOT turn AI on. Presence of a
109 // credential is not consent; the switch is.
110 switch ai := getenv("MEMORIA_AI"); ai {
111 case "":
112 case "on":
113 c.AI = true
114 // MEMORIA_TENSORX_KEY, because that is the name the credential was
115 // already given on the platform (set by hand for the 2026-09-05
116 // live-key checks). This code first read MEMORIA_TENSORX_API_KEY,
117 // which would have meant a second copy of the key under a new name —
118 // and a platform secret cannot be read back to make one.
119 if c.TensorXKey = getenv("MEMORIA_TENSORX_KEY"); c.TensorXKey == "" {
120 return c, fmt.Errorf("%w: MEMORIA_TENSORX_KEY (MEMORIA_AI=on names a provider but gives no credential for it)", ErrMissing)
121 }
122 default:
123 return c, fmt.Errorf("memoria: MEMORIA_AI must be \"on\" or unset; refusing to guess what %q means", ai)
124 }
125
126 // Origin is the only one with a default, and only because the
127 // default is a loopback address that cannot be reached from
128 // anywhere else. It decides the CSRF yardstick and the cookie
129 // attributes, so a silent production default would be a real bug —
130 // hence the caller logs a warning when it is used.
131 if c.Origin = getenv("MEMORIA_ORIGIN"); c.Origin == "" {
132 c.Origin = "http://localhost:8080"
133 }
134 home := connect.Credentials{Issuer: getenv("MEMORIA_HOME_ISSUER"), ConsumerID: getenv("MEMORIA_HOME_CONSUMER_ID"), KeysBase64: getenv("MEMORIA_HOME_KEYS_BASE64"), DEKBase64: getenv("MEMORIA_HOME_DEK_BASE64")}
135 if home.Issuer != "" || home.ConsumerID != "" || home.KeysBase64 != "" || home.DEKBase64 != "" {
136 // The hub directory "All apps" links to: unprefixed, the name and
137 // value Home itself reads, as connect.CredentialsFromEnv does. It
138 // is optional and enables nothing, so it does not count toward
139 // "Home is configured"; DecodeCredentials validates it.
140 home.Directory = getenv("HOME_DIRECTORY_ORIGIN")
141 c.Home, err = connect.DecodeCredentials(&home, c.Origin)
142 if err != nil {
143 return c, err
144 }
145 }
146 return c, nil
147}
148
149// UsedDefaultOrigin reports whether the origin was defaulted, so main can
150// say so out loud.
151func (c Config) UsedDefaultOrigin() bool { return c.Origin == "http://localhost:8080" }
152