Data-egress matrix
Source of truth: scripts/guardrails/egress-matrix.json.
One owned corpus × provider × purpose policy that every egress-enforcing
surface reads, replacing per-tool hard-coded policy. It exists because four
ad-hoc egress decisions (ollama-only → DeepSeek override → GLM-Clippings
exception → Alibaba embeddings) accumulated across four documents with no
single owner, producing a live contradiction (the graphify fence as
specced denied Alibaba for luna content, while the embedding pilot sent luna
content to Alibaba for embeddings). (That contradiction is now moot: DeepSeek
and Alibaba were de-listed entirely — see the provider-policy note in
Semantics.)
Semantics
- Evaluation: first-match-wins over
rules,*wildcards, thendefault(which isdeny— fail-closed). - Verdicts:
allow·allow+log(must append a ledger line) ·conditional(allow only while the rule’sconditionholds) ·deny·pending-operator(evaluates as deny; a named operator decision flips the cell via a one-line PR on the JSON). - Reference implementation: the
evaluate()function inscripts/guardrails/test-egress-matrix.mjs— consumers in other languages copy those semantics. - Load-bearing nuance: embedding, rerank, and vision-embedding send FULL content (text or images) to the provider. An “embedding lane” is content egress, not metadata egress, and is gated accordingly.
- enrichment is content egress like extraction — full note
bodies leave the machine, plus the vault-wide top-200 tag vocabulary (tag
names only, derived from all vault markdown) sent with each request as the
allowed-tags list. Its
luna-personal × deepseekcell was ratified 2026-07-10 then REVERSED 2026-07-22: nowdeny— DeepSeek de-listed (bad results). The GLM enrichment cell that replaced it (luna-personal × zai-glm × enrichment) was itself REVERSED 2026-08-29: nowdeny— see the GLM de-listing below. No sanctioned CN enrichment lane remains;enrich-chat-notes.pyships--provider claude. luna-personal × zai-glm × extractionwas ratified 2026-07-17 (operator) asallow+log, and REVERSED 2026-08-29: nowdeny— the GLM lane was dropped (below). The ratification reasoning is kept here because the matrix records why a vendor was ever permitted, not only its current verdict; read the rest of this bullet as history. This WIDENS the permitted provider set — full luna non-Clippings note bodies may now leave the machine to a second, additional vendor (Z.ai) that could not receive them before. What it does not change is the corpus, purpose, region (CN), or content scope: those are identical to the 2026-07-05 DeepSeek extraction override it sits beside, so the kind of exposure is unchanged even though the number of vendors holding it goes from one to two. Both facts matter to a privacy review; neither cancels the other. Ratified because a measured A/B on an identical corpus put glm-5.2 ahead of the incumbent on every axis (link recall 63–79% vs DeepSeek’s 26%, file coverage 100% vs 95%) at flat-rate Coding-Plan cost, against $3.14 per full-vault DeepSeek run — the vault was paying for the weaker backend purely because the matrix had no GLM cell. Scope is deliberately narrow:luna-personal × zai-glm × inferencestayed deny — extraction only, and it still denies by default (the GLM de-listing added explicit deny rows only for the four cells that were open, it did not invent new rows).salus × * × *stays deny, hard — no override can flip it. The one carve-out is the explicitsalus × openai-codex × inferenceallow row ahead of it (operator ruling 2026-10-08).- Not a default.
refresh-graph-map.shkeepsBACKEND=claude-cli(the claude-only adopter story). GLM is a per-run opt-in for an operator who has a Coding Plan; an adopter without one is never expected to have it.
- Provider policy — DeepSeek + Alibaba de-listed (2026-07-22).
The sanctioned provider set for vault/handover egress is GLM (zai-glm) +
Claude (anthropic) + Codex (openai-codex). DeepSeek and Alibaba were dropped
(bad results, operator judgment): every DeepSeek vault/handover extraction and
enrichment cell (the 2026-07-05 extraction override, enrichment; the
station opt-in inference cells behind
HIMMEL_DEEPSEEK_INFERENCE_OK=1stay conditional) and every Alibaba cell (the five embedding/rerank pilot cells + the brief-scopedhandover-state × alibaba × inferencecell) is now explicitdeny. Both providers stay allow onhimmel-codevia its* ×wildcard — the de-listing is about private-content egress, not public code, sohard:trueis NOT used (that stays reserved for salus + gemini-keys-unset). The reversal is legible (each cell’swhyrecords it) and reversible (a future operator decision could re-add a cell). Salus PHI egress is untouched. - Provider policy — GLM (zai-glm) de-listed (2026-08-29).
The GLM lane was resolved as DROP (Coding Plan auto-renew
cancelled 2026-08-12; the plan lapsed 2026-08-17) but its reversal branch
never landed, leaving four cells live for 12 days. They are now explicit
denyon the same pattern as the DeepSeek/Alibaba de-listing:luna-personal × zai-glm × extraction,luna-personal × zai-glm × enrichment,luna-clippings × zai-glm × extraction(the narrow G2.3 ladder exception) andhandover-state × zai-glm × inference(the brief-scoped worker cell). (At the time, Kimi (moonshot) was adopted as the replacement CN extraction lane — see the kimi/moonshot retirement bullet below for why that no longer holds.) Resist the urge to compress what remains into one provider list: the matrix is keyed by corpus x provider x purpose, and after this reversal no provider is broadly authorized across both corpora. Cell by cell, what stayed open at the time was:luna-personalandluna-clippingsextraction —moonshot(allow+log, since retired) plusanthropic/local-ollama; enrichment on either corpus —anthropic/local-ollamaonly, no CN lane at all;handover-stateinference —openai-codex(conditional, brief-scoped) andopenrouterplusanthropic/local-ollama. Kimi held NO handover-state cell and no enrichment cell; Codex holds NO vault cell. This was a record-integrity fix, not an incident: nothing routed to GLM — no GLM lane appears inscripts/lanes/resolve.mjs— but a liveallow+logcell is exactly what a later worker would cite as pre-existing authorization. As with DeepSeek/Alibaba the rows aredeny, not deleted (a deleted row falls through todefault: denyand loses the history), nothard, andhimmel-code × zai-glmstays allow via the wildcard — public code, not private content, sorefresh-graph-map.sh’s--backend glmremap still legitimately serves it. Consumer sweep:graphify-fence.shloses theluna-clippings × zai-glmconditional branch and itsGRAPHIFY_CLIPPINGS_GLM_OKopt-in (the cell it opened is now deny, so the flag opened nothing);enrich-chat-notes.pykeeps itsglmprovider entry but its egress gate now refuses it, exactly as it already refusesdeepseek. - Provider policy — kimi/moonshot retired (operator ruling
2026-08-24/2026-09-15: there is no kimi backend). Unlike the
DeepSeek/Alibaba/GLM de-listings above, the
moonshotprovider entry and its twoallow+logrows (luna-personal × moonshot × extraction,luna-clippings × moonshot × extraction) are DELETED outright, not reversed to explicit deny — the operator retired the backend itself, not just its egress authorization, and kimi was never de-listed for bad results the way DeepSeek/Alibaba/GLM were, so there is no “why a vendor was ever permitted” history worth preserving as a live row.moonshotnow falls through todefault: denyexactly like any other unclassified provider. No sanctioned CN/cloud extraction lane remains forluna-personalorluna-clippings—anthropic(the operating substrate) is the only live allow cell either corpus has for extraction, and it is a plainallow, notallow+log.graphmap-cadence.sh’s weekly semantic refresh reverts toBACKEND="claude-cli"(its earlier default), drawing the operator’s own interactive 5h/weekly subscription usage budget — gated bybank-preflight.shinsiderefresh-graph-map.sh, not by this matrix.graphify-fence.shdrops_map_kimi_endpointand itskimi -> moonshotmapping entirely;--backend kiminow hits the same undeclared-literal-provider fail-closed path any other unrecognized backend name does.
Corpus resolution (shared primitives, not new ones)
Corpus membership resolves through the SAME primitives the live guards
already use: .salus root markers and the
~/.config/claude-glm/{phi-roots,egress-denylist} list files
(scripts/telegram/phi-egress-guard.ts — renamed from glm-guard.ts — and scripts/hermes/assets/parity_guard.py),
plus vault/state roots from env or config — never hardcoded absolute paths.
The matrix defines policy; membership resolution stays with the guards.
List files are read line by line with surrounding whitespace (and a CRLF \r)
trimmed and whole-line # comments skipped — in graphify-fence.sh
(_under_any_list), its graph-refresh.sh twin, and refresh-graph-map.sh’s
_corpus_is_salus_root alike. A line that trims to nothing is
skipped, never compared (an empty root would prefix-match every path).
Staged-copy corpus declaration (.graphify-corpus)
The 621/622 plan runs extraction on scratchpad copies of corpus content,
never live vaults — but a copy classifies as nothing, so an undeclared copy is
denied unconditionally. A .graphify-corpus marker file at or above the target
declares the copy’s origin corpus: its first line, trimmed, must be one of
salus · luna-personal · luna-clippings · handover-state · himmel-code
(anything else, or an unreadable marker, is a fail-closed deny). Precedence
is strict: .salus/PHI roots and the real luna/handover/himmel roots BOTH beat
the marker (a marker inside a real vault can never relax classification); the
marker is consulted only for otherwise-unclassifiable paths. Because a copy
is origin-blind, mis-declaration is possible and accepted — the load-bearing PHI
guard stays parity_guard / the file-tool fence, not this command-text guard.
As a backstop, any marker-derived invocation always appends a ledger line
on every allow-family verdict (allow / allow+log / conditional — a deny
already blocks the egress), carrying "declared":true, even on a plain allow
cell, so a mis-declared marker cannot also dodge the audit trail. The marker is
consulted only when a luna root is configured (LUNA_VAULT /
LUNA_VAULT_PATH non-empty): the real-root-beats-marker guarantee depends on
the fence seeing the real roots, so with no luna root the marker is inert and
staged copies stay unclassifiable → deny. graphify-fence.sh also normalizes
MSYS-form paths (/c/… → c:/…) for its lexical root comparisons (the
filesystem marker walks use the original path form) so Windows Git-Bash
candidates and the Bash tool’s always-MSYS $PWD match the drive-lettered
corpus roots.
.graphify-backend file-declared backend
graphify update (and other LLM-free subcommands) take no --backend on the
CLI, so a non-himmel corpus needs a declared provider from elsewhere to
satisfy the fence’s no-backend policy. GRAPHIFY_DECLARED_BACKEND
covers this, but it is launching-shell-only by design — a per-call VAR=x
prefix or an in-session export never reaches the PreToolUse hook process —
so an in-session or headless graphify update run could never satisfy it
without a session restart. A .graphify-backend file in the exact same
directory as the .graphify-corpus marker (not any other ancestor
directory) is a second, per-run way to supply the same declaration:
single-line, trimmed, non-empty, safe-charset ([A-Za-z0-9._-]+) backend
name; empty, multiline, unreadable, or invalid content is a fail-closed
deny, same as an invalid .graphify-corpus marker. Precedence:
GRAPHIFY_DECLARED_BACKEND wins when set; the file is consulted only when the
env var is unset. Scoped identically to the env var (LLM-free subcommand +
non-himmel corpus only). The file’s value flows through the same matrix eval
as a real --backend token — not a bypass — and any declared-backend
substitution (env or file) always leaves a ledger line on every
allow-family verdict — even a plain allow matrix cell on a real
(non-marker) root — carrying a declared_backend_source field ("env" or
"file") so audits can tell the two paths apart and no declaration-reached
run goes unrecorded. All-dirs-must-agree:
when one invocation carries multiple marker-derived targets, every
.graphify-corpus marker directory is consulted (not only the
rank-winning target’s — equal-rank argument ordering must not pick which
declaration gets read): each dir must carry a valid .graphify-backend and
all files must declare the same value; a missing, unreadable, or invalid
file in any dir, or two differing values, is a fail-closed deny
regardless of argument order. Staged-only: file declarations are honored
only when every classified target in the invocation is a marker-declared
staged copy — any target that classifies via a real configured root disables
the file path outright (a staged copy’s declaration must not vouch for a
real vault path listed beside it); mixed staged+real invocations need
--backend or the launching-shell env var.
Direct-eval mode (--eval)
The fence is wired only as a PreToolUse hook, which never sees graphify calls a
script makes itself (cron/schtasks). Non-hook callers — currently
scripts/graphify/refresh-graph-map.sh, before both of its graphify
dispatches — run the same evaluation directly instead of keeping a private
copy of the provider map, matrix eval and ledger:
graphify-fence.sh --eval <corpus> <backend> <abs-target> <tool-label>Exit 0 = allow (ledger written where the verdict requires it); exit 2 = deny, reason on stderr. The verdict is exactly what an agent-typed invocation of the same corpus × provider gets from the matrix.
- Validation (fail-closed, exit 2):
<corpus>must be one of the five classes (salus,luna-personal,luna-clippings,handover-state,himmel-code);<backend>and<tool-label>must match[A-Za-z0-9._-]+(they land in the ledger JSON unescaped, so the charset is the escaping);<abs-target>must be an absolute path. Any operand count other than four after--evalexits 2 — a malformed direct call is never let fall through to hook-mode parsing, where a bare--evalis not a graphify clause and would exit 0. - Class tightening:
<corpus>is the caller’s asserted class, a declaration. The fence also classifies<abs-target>by path (.salusmarkers, the PHI root lists, configured real roots) and the more restrictive of the two wins — classification can only tighten the asserted class, never relax it. - Ledger: allow-family lines go to the same ledger file as hook mode, with
tool=<tool-label>,"declared":true(the asserted class is a declaration, like a.graphify-corpusmarker) and"purpose":"extraction". Hook-mode lines keeptool=graphifyand carry nopurposefield.
This section documents the contract only. It deliberately restates no matrix
verdicts — scripts/guardrails/egress-matrix.json is authoritative.
Salus ollama-cloud opt-in and its trust root
The one persisted opt-in that lets the fence send a salus (PHI) corpus to
ollama-cloud is graphify.salus_ollama_cloud_ok in $HOME/.himmel/config.json.
Granting it is an operator call. Defense in depth around it:
- Symlink refusal. The fence ignores the opt-in (treats it as not granted)
when
$HOME/.himmelor$HOME/.himmel/config.jsonis a symlink — a link to a file elsewhere that holdstrueis not an operator grant. - Write guard (soft block (speed bump)).
scripts/hooks/block-himmel-config-writes.shdenies tool-driven writes to that file (file tools, and Bash commands that name it other than plain reads). It is text matching, not a sandbox; its header lists what it cannot see. It is not wired yet — wiring it into.claude/settings.json,.codex/hooks.jsonandwire-hook-bash.mjsis a planned follow-up, so until that lands the symlink refusal is the only live layer. $HOMEis the trust root, in hook mode and in--evalmode alike. The fence reads the caller’s$HOMEand does not pin the passwd-database home; a caller that controls its own$HOMEcontrols the opt-in. That is by decision, pinned by a test row, not an oversight.- Loopback means exactly
localhost,127.0.0.1and[::1]. Other127.xaddresses and0.0.0.0are not loopback for the fence and are refused fail-closed; widening the set is a matrix-review change, not a config one. - Cloud-model detection is a name heuristic: surrounding whitespace is
trimmed and a
:latestsuffix ignored before testing*-cloud/*:cloud. A local alias of a cloud model (made withollama cp) is not detected — the upgrade path is asking the daemon (ollama show) for the remote host.
Consumers
| Surface | What it reads |
|---|---|
scripts/guardrails/graphify-fence.sh | maps graphify --backend → provider, target path → corpus, purpose = extraction; verdict via scripts/guardrails/egress-matrix-eval.mjs (the reference semantics as a CLI); wired as the narrow block-graphify-egress PreToolUse hook |
parity_guard.py PHI/egress fence | the salus row (hard deny) — already enforced; the matrix documents the policy it implements |
phi-egress-guard.ts (renamed from glm-guard.ts) | same salus/denylist row |
scripts/hermes/egress-gate.sh — the Hermes dispatch sanctioned script (invoke.sh, which dispatch-trusted.sh execs) | target = the --prompt-file path → corpus (salus / luna-personal / luna-clippings / handover-state), --provider → provider (required for a gated corpus), purpose = inference; verdict via egress-matrix-eval.mjs. Refuses with rc 4 before hermes spawns; conditional is honored only for handover-state × openai-codex (brief-scoped: exactly ONE regular file per dispatch — a directory / non-regular path, or a path under the handover root that resolves outside it, is refused), every other conditional cell fails closed. Permitted gated dispatches append ~/.himmel/hermes-egress.jsonl (resolved path + byte size + sha256 of the dispatched snapshot — the “chokepoint audit is the ledger”). invoke.sh dispatches an immutable private mode-0600 snapshot the gate copied into a private directory it creates itself (mktemp -d, mode 0700, holding a 0600 file written exclusively under umask 077; the caller passes no destination and removes the directory on every exit path, so the gated bytes never sit in a caller-chosen or umask-perm file and the prompt is never opened for writing; the file identity and the resolved path are re-checked around the copy; a change detected there, or any copy/stat error, refuses), never the gated path again — so a swap or symlink retarget after the copy cannot change what hermes reads. Residual window a shell cannot close (the ponytail: note in the gate): a same-inode in-place edit before the copy, a swap undone before the post-copy checks, or a plain-file replacement landing before the first identity read is not detected. Public-code prompts are not gated, so qwen/alibaba stays a valid --model for himmel-code work. Limit: text/stdin prompts carry no path and are not classified |
marketplace/plugins/himmel-ops/hooks/block-agent-native-egress.sh | the salus × builder-io row (hard deny): refuses an mcp__*agent-native* call (the hosted Builder.io MCP that builder-visual@himmel registers) when the session cwd, its git toplevel or remotes, a .salus marker or a phi-roots/egress-denylist root marks salus, or the tool payload names salus. Plugin-shipped so it also fires in a salus session; it does not read the matrix at runtime, and test-egress-matrix.mjs pins the row |
| embedding/rerank pilot client | the alibaba × embedding/rerank/vision-embedding cells — all now explicit deny (Alibaba de-listed; the pilot is not being pursued) |
scripts/eval/worker-digest/failure_router.py | the worker-failure-metadata × jira × ticketing row (conditional): the router files and comments only a summary and body its own closed-alphabet check (check-body) passes, through the Jira CLI by absolute path, and logs one decision line per send. It does not read the matrix at runtime; test-egress-matrix.mjs 5d pins the conditional row |
| Atlassian MCP catalog capture (a one-off station script, not a shipped consumer) | the mcp-tool-catalog × atlassian-mcp × catalog-fetch row (allow, operator ruling 2026-10-08): an auth-only initialize + tools/list call to https://mcp.atlassian.com/v1/mcp, no ticket, vault or corpus content. Every tools/call, other purpose and other corpus stays default-deny; test-egress-matrix.mjs 6 pins the scope |
Reviewed outside the matrix: the research path’s Reddit lane
scripts/web/research/research_path.py (HIMMEL-5862) fetches two hosts,
www.reddit.com (Scrapling, keyless) and arctic-shift.photon-reddit.com
(plain GET, keyless). They carry no row here: the matrix keys corpus content
leaving the machine, and this lane sends none. Boundary: the only text sent
is the research query the operator typed, URL-encoded into a search URL, plus
public thread ids and permalinks read back from Reddit’s own results. Text taken
from a vault or corpus (luna, salus, handover state) is never sent. The code
enforces consistency, not provenance: reddit_rung() is built with one query
and raises on any other, the two fetchers refuse any URL outside their own
https host, and the browser may load Reddit-owned hosts only (a final URL off
www.reddit.com is refused). That the bound query is the operator’s own is the
caller’s contract: a caller that feeds the path from notes must still pass the
operator’s typed query. Widening the lane to a third host or a derived query is a matrix
review, not a code edit.
Invariants (enforced by the test)
defaultisdeny; salus × any-cloud × anything is a hard deny with no recordable override, exceptsalus × openai-codex × inference(an explicit allow row ahead of the wildcard, operator ruling 2026-10-08, which also allowsluna-personalandluna-clippingsfor codex inference only); google-gemini is denied everywhere (keys stay unset); there are now zeropending-operatorcells (the five Alibaba embedding/rerank cells were demoted to explicitdeny); DeepSeek + Alibaba are de-listed for vault/handover egress (explicitdeny), and stayallowonhimmel-codeonly; no bulk pipelines over handover-state.jina(EU, Berlin; Elastic-owned) is allowed forembeddingandrerankonly, onluna-personal,luna-clippingsandhimmel-code, via explicit rows (HIMMEL-5673, operator ruling 2026-10-14; Jina legal 10.4: no training on customer request data), and, since HIMMEL-5683, on every other qmd corpus except salus:operator-repos(ggs-local, ggs-bridge, agent-command-chain, station-ops, nvidia-steam-shader-cache),jira-mirror,roadmap-planandskill-index. Every other purpose and corpus stays denied (default-deny, plus an explicithimmel-code × jina × *deny ahead of the himmel-code wildcard allow), andsalus × jinais refused by the hard salus wildcard (the test pins that it resolves through that row). This only permits: the live qmd use stays off until the fork backend is installed.jinais distinct fromjina-reader(US, keyless URL scrape).openrouter(US) is the remote qmd embedder and reranker (HIMMEL-5756, operator ruling 2026-10-15, HIMMEL-5712 A/B winnerqwen3-embedding-8bpluscohere/rerank-4-fast):embeddingandrerankonly, via explicit rows, on every non-salus qmd corpus (himmel-code,luna-personal,luna-clippings,operator-repos,jira-mirror,roadmap-plan,skill-index). Every other purpose keeps its prior verdict. Query expansion (qmd’sgeneraterole, HIMMEL-6125, operator ruling 2026-10-17) is its own purpose,query-expansion, so theinferencecells (himmel-code,handover-state) are not widened: the same seven corpora get explicit allow rows and the raw query is the content sent.salus × openrouter × embedding|rerank|query-expansionare explicit HARD denies placed ahead of every openrouter allow and of the non-hardsalus × openrouter × *cell; the test pins both the verdict and the row order. Salus stays on its own local-only qmd index and daemon. The qmd gate maps the hostopenrouter.ai(exact) to provideropenrouter, accepts only the pinned models (openai:qwen/qwen3-embedding-8bembed,openai:cohere/rerank-4-fastrerank,openai:google/gemini-2.5-flash-litegenerate; the matrix is per provider, so the consumer pins the model), refuses an unpinned model in any role, an unrecognised host andmodels.openai_api_keyin the yaml; a missing or unparseable base URL is refused like an unrecognised one. The gate also refuses by path: a collection whose path overlaps a PHI root (graphify fence lists, a sibling salus config’s paths, a.salusvault) is PHI whatever it is called. The gate also refuses a collection that exists only in the index database, a directory holding a.salusvault below it, an unreadable claude-glm config dir, a base URL carrying credentials, and a salus index without explicit localembedandrerankmodels; refusal text never echoes a URL’s userinfo or an unparsed config line. The key lives only in the daemon environment (QMD_OPENAI_API_KEY);QMD_GATE_REQUIRE_KEY=1(set by the launcher) refuses a remote role with no key.qmd_cmd(scripts/lib/qmd-bin.sh) runs the gate before any himmel-launched qmd process when the config names a remote model. A hand-typedqmdon PATH bypasses it: salus is protected by living in its own local-only index. Windows launchers are out of scope (Windows is dropped).- qmd enforcement (HIMMEL-5683). qmd’s model config is per index, not per
collection, so salus lives in its own index (
qmd --index salus, ownsalus.yml/db/daemon port, local models).scripts/qmd/qmd-jina-gate.mjsevaluates every remote model role (anopenai:URI to a non-loopback endpoint) against every collection in an index throughegress-matrix-eval.mjs, usingscripts/qmd/qmd-collection-corpus.json(collection to corpora). A collection with no mapping is denied. The qmd plugin’sensure-qmd-daemon.shruns the gate before it starts a daemon whose config names a remote model and refuses (exit 1) on a denial, a missing gate or a missing node. The gate also reads the index database (sqlite3read-only, active documents only): qmd embeds every active document whatever the config lists, so a collection dropped fromindex.ymlbut still active in the database counts, and an unreadable database fails closed. A role is remote when the config OR the environment names a remote model. The config writer (scripts/luna/qmd-embed-model.sh, before it writes or swaps) and the reindex runner (scripts/luna/qmd-reindex.sh, exit 10, beforeqmd update; the cadence runs it) call the same gate. Removing salus from a live index meansqmd collection remove salusand a zero count fromSELECT count(*) FROM documents WHERE collection='salus' AND active=1before any model change. A new qmd collection must be added to the map, and to the matrix if it needs a corpus, before it can join a Jina index. - Run:
node scripts/guardrails/test-egress-matrix.mjs.
Changing the matrix
Every change is a PR through the normal CR flow — the matrix is
enforcement-path configuration. In particular the planned self-evolving
loop is structurally denied writes here: a widened cell
must always be a human-merged diff. Flipping a pending-operator cell:
change its verdict (typically to allow+log), delete decision_needed,
note the operator decision + date in why, update the test’s expectations,
and cite the ratifying ticket in the commit.
Open operator decisions carried in the matrix
- (none) — the former Alibaba embedding/rerank decision (five
pending-operatorcells) was closed: Alibaba de-listed for bad results, so those cells are now explicitdenyand the pilot is not being pursued. Re-opening it would be a fresh operator ratification (a one-line PR flipping the relevant cells, per Changing the matrix above).