fix: Z39 isolate module data by slug
Keep agent-bound modules in per-app schemas and prevent stale frontend routes from displaying another module's blueprint or rows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package blueprint
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
@@ -48,12 +49,12 @@ type Storage struct {
|
||||
}
|
||||
|
||||
type Entity struct {
|
||||
Name string `json:"name"`
|
||||
Table string `json:"table"`
|
||||
Label string `json:"label"`
|
||||
PrimaryKey string `json:"primary_key"`
|
||||
Fields []Field `json:"fields"`
|
||||
Indexes []Index `json:"indexes,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Table string `json:"table"`
|
||||
Label string `json:"label"`
|
||||
PrimaryKey string `json:"primary_key"`
|
||||
Fields []Field `json:"fields"`
|
||||
Indexes []Index `json:"indexes,omitempty"`
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
@@ -271,10 +272,7 @@ func (bp *Blueprint) AssignSchemaName(tenantID int64) string {
|
||||
bp.Storage.SchemaName = "public"
|
||||
return "public"
|
||||
}
|
||||
name := fmt.Sprintf("app_t%d_%s", tenantID, bp.Meta.Slug)
|
||||
if len(name) > 48 {
|
||||
name = name[:48]
|
||||
}
|
||||
name := scopedIdent(fmt.Sprintf("app_t%d_%s", tenantID, bp.Meta.Slug))
|
||||
bp.Storage.SchemaName = name
|
||||
return name
|
||||
}
|
||||
@@ -284,11 +282,30 @@ func (bp *Blueprint) AssignDatabaseName(tenantID int64) string {
|
||||
if bp.Storage.Mode != "database_per_app" {
|
||||
return ""
|
||||
}
|
||||
name := fmt.Sprintf("appdb_t%d_%s", tenantID, bp.Meta.Slug)
|
||||
if len(name) > 48 {
|
||||
name = name[:48]
|
||||
return scopedIdent(fmt.Sprintf("appdb_t%d_%s", tenantID, bp.Meta.Slug))
|
||||
}
|
||||
|
||||
// scopedIdent 保留可读前缀,并为超长标识符附加内容哈希,避免不同 slug 截断后碰撞。
|
||||
func scopedIdent(raw string) string {
|
||||
if len(raw) <= 48 {
|
||||
return raw
|
||||
}
|
||||
return name
|
||||
sum := sha256.Sum256([]byte(raw))
|
||||
suffix := fmt.Sprintf("_%x", sum[:6])
|
||||
return raw[:48-len(suffix)] + suffix
|
||||
}
|
||||
|
||||
// Revision 返回蓝图内容的稳定短哈希,用于发布回执、缓存隔离与诊断日志。
|
||||
func (bp *Blueprint) Revision() string {
|
||||
if bp == nil {
|
||||
return ""
|
||||
}
|
||||
raw, err := json.Marshal(bp)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
sum := sha256.Sum256(raw)
|
||||
return fmt.Sprintf("%x", sum[:8])
|
||||
}
|
||||
|
||||
func checkIdent(field, v string) error {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package blueprint
|
||||
package blueprint
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -18,6 +18,32 @@ func TestNormalizeIdent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignSchemaNameKeepsLongSlugsDistinct(t *testing.T) {
|
||||
prefix := "module_with_a_very_long_shared_slug_prefix_that_used_to_collide_"
|
||||
a := &Blueprint{Meta: Meta{Slug: prefix + "a"}, Storage: Storage{Mode: "schema_per_app"}}
|
||||
b := &Blueprint{Meta: Meta{Slug: prefix + "b"}, Storage: Storage{Mode: "schema_per_app"}}
|
||||
|
||||
gotA := a.AssignSchemaName(12)
|
||||
gotB := b.AssignSchemaName(12)
|
||||
if gotA == gotB {
|
||||
t.Fatalf("different slugs resolved to the same schema: %q", gotA)
|
||||
}
|
||||
if len(gotA) > 48 || len(gotB) > 48 {
|
||||
t.Fatalf("schema names exceed identifier limit: %q / %q", gotA, gotB)
|
||||
}
|
||||
if gotA != a.AssignSchemaName(12) {
|
||||
t.Fatalf("schema name is not deterministic: %q", gotA)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevisionChangesWithSlug(t *testing.T) {
|
||||
a := &Blueprint{Version: "1.0", Meta: Meta{Slug: "whm1"}}
|
||||
b := &Blueprint{Version: "1.0", Meta: Meta{Slug: "whm11"}}
|
||||
if a.Revision() == "" || a.Revision() == b.Revision() {
|
||||
t.Fatalf("revision must include blueprint slug: %q / %q", a.Revision(), b.Revision())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFields(t *testing.T) {
|
||||
bp := &Blueprint{
|
||||
Version: "1.0",
|
||||
|
||||
Reference in New Issue
Block a user