You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(archive): key own-credential answers by org or connection; sharing is an endpoint judgment
Storage and sharing are now separate dimensions. The licence still decides whether bytes are
kept; `archive.sharing` decides whose question the answer is, and the answer is confined by
the KEY it is recorded under rather than by a read-time filter:
- treg's platform key: public, as before.
- the org's own credential (API key or OAuth token, metered or not) on an `any_account`
endpoint: an org-scoped key (`org:<id>` folded into the hash); the caller reads its org key
first, then the public one, so a platform fetch still serves an own-key caller free.
- on an `own_account` endpoint: a connection-scoped key (`conn:<org>:<bound secret ids>`), only
that key is consulted - two Google accounts in one team never see each other's sites, a
reconnect starts a fresh history, and pre-existing public rows are never served.
- crossing teams requires the ENDPOINT to declare `cache.sharing: public`; the store refuses it
on a provider header, on an `own_account` endpoint, and any value but `public`. A provider's
storage licence no longer implies sharing (`judged_storable` and the `own_key_scoped` miss
are gone).
`ArchiveKey.scope` (migration 0034, still unreleased) lets the refresh worker skip private
keys; `tool_called` carries `cache_sharing` and, on a hit, `cache_scope`. Fragments: archive
("Sharing"), data-model; AGENTS.md, SECURITY.md.
| the org's own credential (API key or OAuth token, metered or not) |`org`, or `public` only where the ENDPOINT declares `cache.sharing: public`|`connection`|
96
+
97
+
Sharing is enforced by the KEY, not by a filter at read time: `scope_tags` folds `org:<id>` or
98
+
`conn:<id>:<sorted bound secret ids>` into `cache_key`, so a private history is under a hash
99
+
nobody else ever computes, its timer learns only from its own answers, and two Google accounts
100
+
in one team never see each other's sites (a reconnect is a new secret, hence a fresh history).
101
+
A caller consults its scopes most specific first — connection only; org then public (a
102
+
platform-key fetch may serve an own-key caller free); public only — and records under the first.
103
+
`ArchiveKey.scope` (`org` | `conn` | NULL = public, including every key from before the column)
104
+
lets the refresh worker skip private keys: treg's platform key cannot re-ask them. History from
105
+
before this rule (platform and billed-OAuth rows, all NULL-origin, all on public keys) is
106
+
therefore never served to an `own_account` caller and only ever served on `any_account`
107
+
endpoints, where it was a public question anyway. `cache_sharing` and, on a hit, `cache_scope`
108
+
are on `tool_called`.
109
+
110
+
`cache.sharing: public` is an ENDPOINT declaration (`_validate_cache` refuses it on a provider
111
+
header, refuses any value but `public`, and refuses it on an `own_account` endpoint): it says
112
+
this endpoint's answer is identical whoever asks — treg's own service OAuth reading public data
113
+
is the intended case — and it is judged per endpoint, never inherited from a provider's licence.
114
+
Nothing in the shipped catalog declares it yet.
91
115
92
116
## Pricing a hit (2026-09-14)
93
117
@@ -528,7 +552,7 @@ produce hypothetical hit counts or fresh-answer comparisons.
528
552
(`_buffer_response` needs the provider's reported cost), so recording adds no latency. An
529
553
own-key catalog answer is read whole only when it fits the archive's size cap (see "Own-key
530
554
answers"); larger ones stream untouched. Own-tool calls (no catalog entry) are never touched.
531
-
Who an own-key answer may serve is decided at READ time by `origin_org_id`, not at write time.
555
+
Who an own-credential answer may serve is decided by the KEY it is recorded under ("Sharing").
532
556
533
557
Gates 1+2 are `archive.policy(entry)`; gate 3 is the hook site's own context.
534
558
@@ -539,10 +563,11 @@ buffer's 8 MiB limit fail before recording and cannot populate a cache or idempo
539
563
540
564
## The cache key
541
565
542
-
`archive.cache_key(method, endpoint_id, upstream_url, body, headers)` → sha256 over the canonical
543
-
request: uppercased method, catalog endpoint id (a provider URL reshuffle starts a fresh history),
544
-
sorted query pairs, canonical-JSON body hash (raw hash for non-JSON), plus only `Accept` and
545
-
`Accept-Language` from the caller's headers. Auth/cookies/tracing/encodings never enter the key —
566
+
`archive.cache_key(method, endpoint_id, upstream_url, body, headers, scope="")` → sha256 over the
567
+
canonical request: uppercased method, catalog endpoint id (a provider URL reshuffle starts a fresh
568
+
history), sorted query pairs, canonical-JSON body hash (raw hash for non-JSON), plus only `Accept`
569
+
and `Accept-Language` from the caller's headers, plus the sharing scope when the answer is not
570
+
public (`org:<id>` / `conn:<id>:<secret ids>`, see "Sharing"; a public key hashes as it always did). Auth/cookies/tracing/encodings never enter the key —
546
571
and credentials could not anyway: injection happens after the key is taken.
547
572
548
573
## Tables (migration 0002)
@@ -551,9 +576,11 @@ and credentials could not anyway: injection happens after the key is taken.
551
576
`policy`, AIMD timer state (`ttl_s`, grow ×1.5 capped on stable refetch / shrink ×0.5 floored on
552
577
change — the learner lands in PR 5), change statistics (`change_seen`/`stable_seen`/
553
578
`last_changed_at`), legacy `volatile_paths` (retained for schema compatibility, no longer read or updated), and demand (`heat`, `last_requested_at`). Platform-scoped, no `org_id`:
554
-
one team's fetch may warm another team's hit; an own-key answer's reach is the SNAPSHOT's
555
-
`origin_org_id` (migration 0034), and `ArchiveKeyOrg` (same migration) is the per-(org, key)
556
-
"has paid for this question" mark that prices a repeat hit — see "Pricing a hit".
579
+
one team's fetch may warm another team's hit; an own-credential answer's reach is its KEY's
580
+
scope (`ArchiveKey.scope`, migration 0034, and the scope folded into the hash — see "Sharing"),
581
+
the snapshot's `origin_org_id` (same migration) is provenance, and `ArchiveKeyOrg` (same
582
+
migration) is the per-(org, key) "has paid for this question" mark that prices a repeat hit —
0 commit comments