Skip to content

Commit b56a731

Browse files
authored
Merge pull request #122 from l33tdawg/release/11.16.0-appv24
v11.16.0: app-v24 RBAC, projection safety, and upgrade hardening
2 parents 7fb7c53 + 61369e7 commit b56a731

130 files changed

Lines changed: 10767 additions & 592 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/v11.9-fault-gates.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
default: false
1212
type: boolean
1313
require_authorized_state_sync:
14-
description: Require the integrated exact app-v23 provider-to-pristine-full-node state-sync transfer
14+
description: Require the integrated exact app-v24 provider-to-pristine-full-node state-sync transfer
1515
required: false
1616
default: false
1717
type: boolean
@@ -28,7 +28,7 @@ on:
2828
default: false
2929
type: boolean
3030
require_authorized_state_sync:
31-
description: Require the integrated exact app-v23 provider-to-pristine-full-node state-sync phase
31+
description: Require the integrated exact app-v24 provider-to-pristine-full-node state-sync phase
3232
required: false
3333
default: false
3434
type: boolean
@@ -62,7 +62,7 @@ jobs:
6262
run: make test-cometbft-patch
6363

6464
real-comet-chaos:
65-
name: App-v23 real Comet/ABCI crash, partition, and state-sync gate
65+
name: App-v24 real Comet/ABCI crash, partition, and state-sync gate
6666
runs-on: ubuntu-latest
6767
timeout-minutes: 30
6868
# Independent of app-multiprocess: no needs.app-multiprocess.* reference
@@ -113,7 +113,7 @@ jobs:
113113
V119_STATE_SYNC_SOURCE_ID=${{ steps.source-id.outputs.value }}
114114
cache-from: type=gha,scope=v119-chaos-node
115115
cache-to: type=gha,mode=max,scope=v119-chaos-node
116-
- name: Run bounded app-v23 real process fault gate
116+
- name: Run bounded app-v24 real process fault gate
117117
env:
118118
V119_CHAOS_REBUILD: '0'
119119
V119_REQUIRE_AUTHORIZED_STATE_SYNC: ${{ inputs.require_authorized_state_sync && '1' || '0' }}

README.md

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,63 @@ The dashboard also includes agent management, domain permissions, key rotation,
5151

5252
---
5353

54+
## What's New in v11.16.0
55+
56+
**App-v24 closes the canonical terminal-hash lifecycle defect without rewriting
57+
history.** New memory submissions bind `content_hash` to the exact SHA-256 of
58+
their content, and challenge, deprecate, and other terminal transitions preserve
59+
that canonical hash. App-v24 activates at the strict height after its app-v23
60+
predecessor, so the activation block and every earlier block retain their exact
61+
historical semantics. A governed, Root-planned validator vote can re-anchor
62+
eligible historical terminal rows in bounded, atomic, idempotent batches from
63+
their unchanged canonical content. The repair changes neither content,
64+
authorship, domain ownership, nor prior blocks.
65+
66+
**Fresh first-party Mynah nodes now wait for the safe protocol floor instead of
67+
starting mute or writing through the vulnerable interval.** Direct app-v23
68+
genesis remains the authenticated bootstrap origin, but `/ready` reports
69+
`waiting_for_app_v24` until the next admitted transaction will execute under
70+
app-v24. Consensus independently rejects direct-genesis Companion memory and
71+
co-commit writes during that short governed climb, so bypassing the readiness
72+
endpoint cannot reproduce the defect. Personal nodes require app-v24 even when
73+
optional future auto-upgrades are disabled. This narrow barrier does not mute
74+
ordinary upgraded nodes: existing agents retain their app-v23 write authority
75+
while app-v24 activates.
76+
77+
**Agent recall and caller-scoped discovery work again under the new access
78+
model.** `sage_turn` now uses the shared local semantic-recall path and forwards
79+
the exact embedding provider returned by `/v1/embed`; it no longer
80+
misclassifies every turn as federated and then trips the app-v23
81+
federated-vector gate. The signed `sage_find_agent` path again searches active
82+
ordinary local agents first and then only federated contacts authorized for
83+
that caller. It is discovery metadata, not presence: an empty match does not
84+
prove that a saved exact Agent ID is unreachable, and sends always revalidate
85+
the destination.
86+
87+
**CEREBRUM RBAC now preserves the policy the operator actually approves.**
88+
Companion enrollment accepts its documented `15`/`31` profiles, valid existing
89+
federated-pipe restrictions are not silently stripped, and a newly pending
90+
mask-`30` principal becomes the documented Companion mask `15` only after
91+
approval. Encrypted and unencrypted loopback CEREBRUM use the same Root/Admin
92+
authorization boundary; an encrypted vault additionally requires its valid
93+
unlocked session. A level-2 grant is never presented as a cure for a hard
94+
capability, pending-review, profile, or ownership denial.
95+
96+
**CEREBRUM no longer mistakes a missing ordinary-memory projection for an
97+
empty brain.** App-v23 readiness now checks the complete canonical Badger
98+
inventory against the local SQL serving projection, so deletion, rollback, or
99+
partial projection loss returns `503` instead of a plausible zero-memory
100+
dashboard or empty backup. A state-sync receiver seals and node-key-signs the
101+
exact historical canonical IDs whose ordinary plaintext was intentionally not
102+
transferred; every memory committed after that baseline remains mandatory. Such a node
103+
reports `canonical_subset`, and portable full-brain export stays disabled
104+
rather than producing a partial file labeled as a backup. Pre-v11.16 receivers
105+
that do not have this exact authenticated baseline fail strict readiness and
106+
must be explicitly repaired or state-synchronized again; upgrade-time SQL state
107+
is never guessed into an omission allowlist.
108+
109+
Container: `ghcr.io/l33tdawg/sage:11.16.0`. SDK 11.16.0.
110+
54111
## What's New in v11.15.1
55112

56113
**Emergency CEREBRUM rendering recovery.** v11.15.0 shipped one malformed
@@ -915,7 +972,7 @@ docker run -d --name sage \
915972
ghcr.io/l33tdawg/sage:latest
916973
```
917974

918-
Pin a specific version with `ghcr.io/l33tdawg/sage:11.15.0`.
975+
Pin a specific version with `ghcr.io/l33tdawg/sage:11.16.0`.
919976

920977
The SAGE server stays in that container. To give a local MCP client a stdio
921978
bridge, start a second process **inside the same running container**:

api/openapi.yaml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
openapi: 3.1.0
22
info:
33
title: (S)AGE API
4-
version: 11.15.1
4+
version: 11.16.0
55
description: |-
66
(Sovereign) Agent Governed Experience — network REST API for memory
77
submission, validation, and query.
@@ -2814,13 +2814,17 @@ paths:
28142814
/v1/agents:
28152815
get:
28162816
operationId: listRegisteredAgents
2817-
summary: List all registered agents. No auth required.
2817+
summary: List agents visible to the authenticated caller.
2818+
description: >-
2819+
Requires an Ed25519-signed request. After app-v23 activation the
2820+
response contains only active ordinary agents within the caller's
2821+
enforced discovery boundary; pending, inactive, Root, and inconsistent
2822+
identities are omitted.
28182823
tags:
28192824
- Agent
2820-
security: []
28212825
responses:
28222826
"200":
2823-
description: List of agents.
2827+
description: Caller-scoped list of active ordinary agents.
28242828
content:
28252829
application/json:
28262830
schema:
@@ -2832,6 +2836,10 @@ paths:
28322836
$ref: "#/components/schemas/RegisteredAgent"
28332837
total:
28342838
type: integer
2839+
"401":
2840+
$ref: "#/components/responses/Unauthorized"
2841+
"403":
2842+
$ref: "#/components/responses/Forbidden"
28352843
"500":
28362844
$ref: "#/components/responses/InternalError"
28372845

@@ -4484,7 +4492,7 @@ paths:
44844492
description: >-
44854493
This is local queue/workflow state, not proof of remote delivery or
44864494
reading. Sender-queryable successful-delivery and receiver claim/read
4487-
receipts are explicitly deferred to v11.16.
4495+
receipts are explicitly deferred beyond v11.16.
44884496
tags:
44894497
- Pipeline
44904498
parameters:

api/rest/agent_handler.go

Lines changed: 120 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -811,8 +811,10 @@ func (s *Server) handleGetRegisteredAgent(w http.ResponseWriter, r *http.Request
811811
writeJSON(w, http.StatusOK, sanitizeAgentForRead(agent, privileged))
812812
}
813813

814-
// handleListRegisteredAgents handles GET /v1/agents.
815-
// Lists all registered agents from offchain store.
814+
// handleListRegisteredAgents handles signed GET /v1/agents. After app-v23 it
815+
// returns only active ordinary canonical enrollments; the unsigned full-roster
816+
// oracle was removed because it bypassed caller-scoped recipient discovery and
817+
// exposed local RBAC/network topology.
816818
func (s *Server) handleListRegisteredAgents(w http.ResponseWriter, r *http.Request) {
817819
if s.agentStore == nil {
818820
writeProblem(w, http.StatusServiceUnavailable, "Agent store unavailable", "Agent store not configured.")
@@ -821,20 +823,31 @@ func (s *Server) handleListRegisteredAgents(w http.ResponseWriter, r *http.Reque
821823

822824
agents, err := s.agentStore.ListAgents(r.Context())
823825
if err != nil {
824-
writeProblem(w, http.StatusInternalServerError, "List error", err.Error())
826+
writeProblem(w, http.StatusInternalServerError, "List error", "The agent roster could not be read.")
825827
return
826828
}
827829
if agents == nil {
828830
agents = make([]*store.AgentEntry, 0)
829831
}
830832

831-
// /v1/agents is unauthenticated — never expose claim_token (a one-time
832-
// credential exchangeable for the agent key seed) or per-agent ACL topology.
833+
callerID := middleware.ContextAgentID(r.Context())
834+
privileged := s.callerIsOperatorOrAdmin(r.Context(), callerID)
833835
sanitized := make([]*store.AgentEntry, 0, len(agents))
834836
for _, a := range agents {
835837
if a == nil {
836838
continue
837839
}
840+
if s.isPostV23ForNextTx() {
841+
active, activeErr := s.appV23ActiveOrdinaryAgent(a.AgentID)
842+
if activeErr != nil {
843+
writeProblem(w, http.StatusServiceUnavailable, "Access control unavailable",
844+
"Current local enrollment state is unavailable.")
845+
return
846+
}
847+
if !active {
848+
continue
849+
}
850+
}
838851
isRoot, rootErr := s.appV23IsRootIdentity(a.AgentID)
839852
if rootErr != nil {
840853
writeProblem(w, http.StatusServiceUnavailable, "Access control unavailable",
@@ -845,7 +858,10 @@ func (s *Server) handleListRegisteredAgents(w http.ResponseWriter, r *http.Reque
845858
continue
846859
}
847860
s.overlayOnChainAgentPolicyForRead(a)
848-
sanitized = append(sanitized, sanitizeAgentForRead(a, false))
861+
sanitized = append(sanitized, sanitizeAgentForRead(
862+
a,
863+
privileged || callerID == a.AgentID,
864+
))
849865
}
850866

851867
writeJSON(w, http.StatusOK, map[string]any{
@@ -858,6 +874,47 @@ type agentNameFinder interface {
858874
FindAgentsByName(ctx context.Context, name string, limit int) ([]*store.AgentEntry, error)
859875
}
860876

877+
type agentNamePageFinder interface {
878+
FindAgentsByNamePage(ctx context.Context, name string, limit, offset int) ([]*store.AgentEntry, error)
879+
}
880+
881+
type agentLookupResult struct {
882+
*store.AgentEntry
883+
MatchKind string `json:"match_kind"`
884+
}
885+
886+
// equalAgentLookupField applies the lookup endpoint's documented comparison:
887+
// ASCII letters are case-insensitive while every non-ASCII byte retains its
888+
// registered casing. strings.EqualFold is deliberately too broad here.
889+
func equalAgentLookupField(left, right string) bool {
890+
if len(left) != len(right) {
891+
return false
892+
}
893+
for i := range len(left) {
894+
l, r := left[i], right[i]
895+
if l >= 'A' && l <= 'Z' {
896+
l += 'a' - 'A'
897+
}
898+
if r >= 'A' && r <= 'Z' {
899+
r += 'a' - 'A'
900+
}
901+
if l != r {
902+
return false
903+
}
904+
}
905+
return true
906+
}
907+
908+
func agentLookupMatchKind(query string, agent *store.AgentEntry) string {
909+
if agent != nil &&
910+
(equalAgentLookupField(query, agent.Name) ||
911+
equalAgentLookupField(query, agent.RegisteredName) ||
912+
equalAgentLookupField(query, agent.Provider)) {
913+
return "exact"
914+
}
915+
return "substring"
916+
}
917+
861918
// handleFindRegisteredAgents is the signed, bounded companion to the public
862919
// roster endpoint. MCP recipient discovery must not fetch ListAgents merely to
863920
// return at most 20 matches: that full endpoint computes every agent's derived
@@ -886,25 +943,69 @@ func (s *Server) handleFindRegisteredAgents(w http.ResponseWriter, r *http.Reque
886943
}
887944
limit = parsed
888945
}
889-
agents, err := finder.FindAgentsByName(r.Context(), name, limit)
890-
if err != nil {
891-
writeProblem(w, http.StatusInternalServerError, "Lookup error", err.Error())
892-
return
893-
}
894-
sanitized := make([]*store.AgentEntry, 0, len(agents))
895-
for _, agent := range agents {
896-
if agent != nil {
897-
isRoot, rootErr := s.appV23IsRootIdentity(agent.AgentID)
898-
if rootErr != nil {
946+
// SQL status is only a discovery projection after app-v23; canonical active
947+
// enrollment lives in Badger. Page the bounded SQL candidates until the
948+
// requested number of canonical recipients is found or the query is
949+
// exhausted. Applying the public limit before this filter lets 20 pending
950+
// self-registrations hide every later active match.
951+
const (
952+
candidatePageSize = 20
953+
maxCandidatePages = 256
954+
)
955+
pager, paged := s.agentStore.(agentNamePageFinder)
956+
sanitized := make([]agentLookupResult, 0, limit)
957+
seen := make(map[string]struct{}, limit)
958+
for offset := 0; len(sanitized) < limit; {
959+
if paged && offset/candidatePageSize >= maxCandidatePages {
960+
writeProblem(w, http.StatusServiceUnavailable, "Lookup incomplete",
961+
"The bounded agent candidate scan was exhausted; narrow the name query.")
962+
return
963+
}
964+
var agents []*store.AgentEntry
965+
var err error
966+
if paged {
967+
agents, err = pager.FindAgentsByNamePage(
968+
r.Context(), name, candidatePageSize, offset,
969+
)
970+
} else {
971+
// Compatibility for tests and third-party stores that implement the
972+
// original bounded finder but not the paged extension.
973+
agents, err = finder.FindAgentsByName(r.Context(), name, limit)
974+
}
975+
if err != nil {
976+
writeProblem(w, http.StatusInternalServerError, "Lookup error",
977+
"The agent directory could not be searched.")
978+
return
979+
}
980+
for _, agent := range agents {
981+
if agent == nil {
982+
continue
983+
}
984+
if _, duplicate := seen[agent.AgentID]; duplicate {
985+
continue
986+
}
987+
seen[agent.AgentID] = struct{}{}
988+
active, activeErr := s.appV23ActiveOrdinaryAgent(agent.AgentID)
989+
if activeErr != nil {
899990
writeProblem(w, http.StatusServiceUnavailable, "Access control unavailable",
900-
"Current CEREBRUM Root state is unavailable.")
991+
"Current local enrollment state is unavailable.")
901992
return
902993
}
903-
if isRoot {
994+
if !active {
904995
continue
905996
}
906-
sanitized = append(sanitized, sanitizeAgentForRead(agent, false))
997+
sanitized = append(sanitized, agentLookupResult{
998+
AgentEntry: sanitizeAgentForRead(agent, false),
999+
MatchKind: agentLookupMatchKind(name, agent),
1000+
})
1001+
if len(sanitized) == limit {
1002+
break
1003+
}
1004+
}
1005+
if !paged || len(agents) < candidatePageSize {
1006+
break
9071007
}
1008+
offset += len(agents)
9081009
}
9091010
writeJSON(w, http.StatusOK, map[string]any{"agents": sanitized, "total": len(sanitized)})
9101011
}

api/rest/appv22_agent_capabilities_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ func TestPostgresAgentBackendImplementsRESTNameLookup(t *testing.T) {
5454
var backend store.AgentStore = (*store.PostgresStore)(nil)
5555
_, ok := backend.(agentNameFinder)
5656
assert.True(t, ok, "amid's Postgres backend must implement bounded /v1/agents/lookup")
57+
_, paged := backend.(agentNamePageFinder)
58+
assert.True(t, paged, "amid's Postgres backend must page lookup candidates before canonical filtering")
5759
}
5860

5961
func TestAppV22AgentReadEndpointsOverlayConsensusCapabilities(t *testing.T) {

api/rest/appv23_access_groups_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ func TestAppV23RootHandoverPreservesDomainAuthorityAndRecallsOldAndNewAuthorship
270270
require.Equal(t, newRootID, authors["after-handover"])
271271
}
272272

273-
func TestAppV23RecallFailsClosedOnUnreadableClassificationAcrossAllModes(t *testing.T) {
273+
func TestAppV23RecallOmitsUnreadableClassificationAcrossAllBroadModes(t *testing.T) {
274274
srv, badger, memberID, ownerID, _ := setupAppV23RESTAccess(t)
275275
memStore := srv.store.(*rbacMockMemoryStore)
276276
seedMemory(t, memStore, "corrupt-classification", ownerID, "owner.home", "needle classified memory")
@@ -303,8 +303,8 @@ func TestAppV23RecallFailsClosedOnUnreadableClassificationAcrossAllModes(t *test
303303
req = req.WithContext(middleware.WithAgentID(req.Context(), memberID))
304304
recorder := httptest.NewRecorder()
305305
tc.handler.ServeHTTP(recorder, req)
306-
require.Equal(t, http.StatusServiceUnavailable, recorder.Code, recorder.Body.String())
307-
require.Contains(t, recorder.Body.String(), "Memory classification state is unavailable")
306+
require.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())
307+
require.Contains(t, recorder.Body.String(), `"results":[]`)
308308
require.NotContains(t, recorder.Body.String(), "needle classified memory")
309309
})
310310
}

0 commit comments

Comments
 (0)