@pentatonic-ai/ai-agent-sdk 0.9.5 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +3 -3
  2. package/bin/cli.js +1 -1
  3. package/bin/commands/config.js +1 -1
  4. package/dist/index.cjs +39 -72
  5. package/dist/index.js +36 -69
  6. package/package.json +10 -3
  7. package/packages/doctor/src/checks/local-memory.js +2 -2
  8. package/packages/memory/README.md +2 -2
  9. package/packages/memory/openclaw-plugin/README.md +2 -2
  10. package/packages/memory/openclaw-plugin/openclaw.plugin.json +1 -1
  11. package/packages/memory/package-lock.json +49 -33
  12. package/packages/memory/package.json +4 -1
  13. package/packages/memory/src/__tests__/engine.test.js +40 -5
  14. package/packages/memory/src/engine.js +38 -3
  15. package/packages/memory/src/server.js +2 -2
  16. package/packages/memory-engine-v2/.env.example +30 -0
  17. package/packages/memory-engine-v2/README.md +125 -0
  18. package/packages/memory-engine-v2/compat/Dockerfile +11 -0
  19. package/packages/memory-engine-v2/compat/requirements.txt +6 -0
  20. package/packages/memory-engine-v2/compat/server.py +1047 -0
  21. package/packages/memory-engine-v2/docker-compose.aws.yml +78 -0
  22. package/packages/memory-engine-v2/docker-compose.yml +206 -0
  23. package/packages/memory-engine-v2/extractor-async/Dockerfile +14 -0
  24. package/packages/memory-engine-v2/extractor-async/confidence.py +62 -0
  25. package/packages/memory-engine-v2/extractor-async/noise_filter.py +144 -0
  26. package/packages/memory-engine-v2/extractor-async/requirements.txt +2 -0
  27. package/packages/memory-engine-v2/extractor-async/test_confidence.py +76 -0
  28. package/packages/memory-engine-v2/extractor-async/test_noise_filter.py +177 -0
  29. package/packages/memory-engine-v2/extractor-async/worker.py +797 -0
  30. package/packages/memory-engine-v2/extractor-sync/Dockerfile +11 -0
  31. package/packages/memory-engine-v2/extractor-sync/requirements.txt +4 -0
  32. package/packages/memory-engine-v2/extractor-sync/server.py +424 -0
  33. package/packages/memory-engine-v2/org-model/migrations/001_init.sql +390 -0
  34. package/packages/memory-engine-v2/tests/e2e_smoke.py +356 -0
  35. package/packages/memory-engine-v2/tests/fixtures/generate_synthetic_corpus.py +758 -0
  36. package/packages/memory-engine/.env.example +0 -13
  37. package/packages/memory-engine/MIGRATION.md +0 -219
  38. package/packages/memory-engine/README.md +0 -145
  39. package/packages/memory-engine/bench/README.md +0 -99
  40. package/packages/memory-engine/bench/scorecards-engine/agent-coding__pentatonic-baseline__20260427-142523.json +0 -1115
  41. package/packages/memory-engine/bench/scorecards-engine/chat-recall__pentatonic-baseline__20260427-142648.json +0 -819
  42. package/packages/memory-engine/bench/scorecards-engine/circular-economy__pentatonic-baseline__20260427-142757.json +0 -1278
  43. package/packages/memory-engine/bench/scorecards-engine/customer-support__pentatonic-baseline__20260427-142900.json +0 -1018
  44. package/packages/memory-engine/bench/scorecards-engine/marketplace-ops__pentatonic-baseline__20260427-142957.json +0 -1038
  45. package/packages/memory-engine/bench/scorecards-engine/product-catalogue__pentatonic-baseline__20260427-143122.json +0 -961
  46. package/packages/memory-engine/bench/scorecards-engine-via-docker/agent-coding__pentatonic-memory__20260427-161812.json +0 -1115
  47. package/packages/memory-engine/bench/scorecards-engine-via-docker/chat-recall__pentatonic-memory__20260427-161701.json +0 -819
  48. package/packages/memory-engine/bench/scorecards-engine-via-docker/circular-economy__pentatonic-memory__20260427-161713.json +0 -1278
  49. package/packages/memory-engine/bench/scorecards-engine-via-docker/customer-support__pentatonic-memory__20260427-161723.json +0 -1018
  50. package/packages/memory-engine/bench/scorecards-engine-via-docker/marketplace-ops__pentatonic-memory__20260427-161732.json +0 -1038
  51. package/packages/memory-engine/bench/scorecards-engine-via-docker/product-catalogue__pentatonic-memory__20260427-161741.json +0 -937
  52. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/agent-coding__pentatonic-memory__20260427-184718.json +0 -1115
  53. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/chat-recall__pentatonic-memory__20260427-184614.json +0 -819
  54. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/circular-economy__pentatonic-memory__20260427-184809.json +0 -1278
  55. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/customer-support__pentatonic-memory__20260427-184854.json +0 -1018
  56. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/marketplace-ops__pentatonic-memory__20260427-184929.json +0 -1038
  57. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/product-catalogue__pentatonic-memory__20260427-185015.json +0 -961
  58. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/agent-coding__pentatonic-memory__20260427-175252.json +0 -1115
  59. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/chat-recall__pentatonic-memory__20260427-175312.json +0 -819
  60. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/circular-economy__pentatonic-memory__20260427-175335.json +0 -1278
  61. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/customer-support__pentatonic-memory__20260427-175355.json +0 -1018
  62. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/marketplace-ops__pentatonic-memory__20260427-175413.json +0 -1038
  63. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/product-catalogue__pentatonic-memory__20260427-175430.json +0 -883
  64. package/packages/memory-engine/bench/scorecards-engine-via-shim/agent-coding__pentatonic-memory__20260427-155409.json +0 -1115
  65. package/packages/memory-engine/bench/scorecards-engine-via-shim/chat-recall__pentatonic-memory__20260427-155421.json +0 -819
  66. package/packages/memory-engine/bench/scorecards-engine-via-shim/circular-economy__pentatonic-memory__20260427-155433.json +0 -1278
  67. package/packages/memory-engine/bench/scorecards-engine-via-shim/customer-support__pentatonic-memory__20260427-155443.json +0 -1018
  68. package/packages/memory-engine/bench/scorecards-engine-via-shim/marketplace-ops__pentatonic-memory__20260427-155453.json +0 -1038
  69. package/packages/memory-engine/bench/scorecards-engine-via-shim/product-catalogue__pentatonic-memory__20260427-155503.json +0 -937
  70. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory-latest__20260427-145103.json +0 -1115
  71. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory__20260427-144909.json +0 -1115
  72. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory-latest__20260427-145153.json +0 -819
  73. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory__20260427-145120.json +0 -542
  74. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory-latest__20260427-145313.json +0 -1278
  75. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory__20260427-145207.json +0 -894
  76. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory-latest__20260427-145412.json +0 -1018
  77. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory__20260427-145327.json +0 -680
  78. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory-latest__20260427-145517.json +0 -1038
  79. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory__20260427-145422.json +0 -693
  80. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory-latest__20260427-145616.json +0 -961
  81. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory__20260427-145528.json +0 -727
  82. package/packages/memory-engine/compat/Dockerfile +0 -22
  83. package/packages/memory-engine/compat/server.py +0 -1255
  84. package/packages/memory-engine/docker-compose.test.yml +0 -59
  85. package/packages/memory-engine/docker-compose.yml +0 -240
  86. package/packages/memory-engine/engine/README.md +0 -52
  87. package/packages/memory-engine/engine/l2-hybridrag-proxy.py +0 -1543
  88. package/packages/memory-engine/engine/l5-comms-layer.py +0 -663
  89. package/packages/memory-engine/engine/l6-document-store.py +0 -1018
  90. package/packages/memory-engine/engine/services/_shared/__init__.py +0 -1
  91. package/packages/memory-engine/engine/services/_shared/embed_provider.py +0 -468
  92. package/packages/memory-engine/engine/services/l2/Dockerfile +0 -50
  93. package/packages/memory-engine/engine/services/l2/init_databases.py +0 -81
  94. package/packages/memory-engine/engine/services/l2/l2-hybridrag-proxy.py +0 -2721
  95. package/packages/memory-engine/engine/services/l5/Dockerfile +0 -11
  96. package/packages/memory-engine/engine/services/l5/l5-comms-layer.py +0 -808
  97. package/packages/memory-engine/engine/services/l6/Dockerfile +0 -30
  98. package/packages/memory-engine/engine/services/l6/l6-document-store.py +0 -1221
  99. package/packages/memory-engine/engine/services/nv-embed/Dockerfile +0 -28
  100. package/packages/memory-engine/engine/services/nv-embed/server.py +0 -152
  101. package/packages/memory-engine/pme_memory/__init__.py +0 -0
  102. package/packages/memory-engine/pme_memory/__main__.py +0 -129
  103. package/packages/memory-engine/pme_memory/artifacts.py +0 -95
  104. package/packages/memory-engine/pme_memory/embed.py +0 -74
  105. package/packages/memory-engine/pme_memory/health.py +0 -36
  106. package/packages/memory-engine/pme_memory/hygiene.py +0 -159
  107. package/packages/memory-engine/pme_memory/indexer.py +0 -200
  108. package/packages/memory-engine/pme_memory/needs.py +0 -55
  109. package/packages/memory-engine/pme_memory/provenance.py +0 -80
  110. package/packages/memory-engine/pme_memory/scoring.py +0 -168
  111. package/packages/memory-engine/pme_memory/search.py +0 -52
  112. package/packages/memory-engine/pme_memory/store.py +0 -86
  113. package/packages/memory-engine/pme_memory/synthesis.py +0 -114
  114. package/packages/memory-engine/pyproject.toml +0 -65
  115. package/packages/memory-engine/scripts/kg-extractor.py +0 -557
  116. package/packages/memory-engine/scripts/kg-preflexor-v2.py +0 -738
  117. package/packages/memory-engine/scripts/wipe-legacy-l3-entities.py +0 -128
  118. package/packages/memory-engine/tests/e2e_arena.sh +0 -259
  119. package/packages/memory-engine/tests/embed_stub/Dockerfile +0 -13
  120. package/packages/memory-engine/tests/embed_stub/server.py +0 -80
  121. package/packages/memory-engine/tests/test_aggregate.py +0 -333
  122. package/packages/memory-engine/tests/test_api_contract.sh +0 -57
  123. package/packages/memory-engine/tests/test_arena_safety.py +0 -232
  124. package/packages/memory-engine/tests/test_channel_stat_reader.py +0 -437
  125. package/packages/memory-engine/tests/test_channel_stat_rollups.py +0 -308
  126. package/packages/memory-engine/tests/test_compat_nv_embed_probe.py +0 -48
  127. package/packages/memory-engine/tests/test_embed_provider.py +0 -492
  128. package/packages/memory-engine/tests/test_l2_qmd_vec_search.py +0 -280
  129. package/packages/memory-engine/tests/test_l3_arena_isolation.py +0 -412
  130. package/packages/memory-engine/tests/test_l6_module_load.py +0 -84
  131. package/packages/memory-engine/tests/test_people_list_reader.py +0 -432
@@ -1,128 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Wipe pre-arena-scoping :Entity nodes from the L3 Neo4j graph.
3
-
4
- Run this AFTER the engine has been deployed with the arena-scoped
5
- writer paths, not before. Sequence:
6
-
7
- 1. Deploy l2-hybridrag-proxy with arena-scoped MERGE patterns.
8
- 2. Verify new ingest is creating arena-tagged entities (run
9
- --dry-run on this script first; it reports legacy vs new).
10
- 3. Run this script with --confirm to wipe legacy entities.
11
- 4. Future ingest re-extracts entities from existing :Chunk nodes
12
- on-demand (search-side touches them; new stores recreate them
13
- from scratch under the right arena).
14
-
15
- Why wipe vs. backfill: pre-fix entities collapsed cross-tenant by name,
16
- so their MENTIONS edges connect to chunks across multiple arenas.
17
- Splitting them by mentions is doable but error-prone (edge cases for
18
- many-arena entities, orphans, no-mention entities). The Hebbian weights
19
- on those edges were also corrupted by cross-tenant traffic, so they
20
- weren't worth saving. Chunks are preserved either way — re-extraction
21
- is cheap.
22
-
23
- Usage:
24
- # report counts only
25
- python wipe-legacy-l3-entities.py --neo4j-uri bolt://localhost:7687
26
-
27
- # actually wipe
28
- python wipe-legacy-l3-entities.py --neo4j-uri bolt://localhost:7687 --confirm
29
-
30
- The script is idempotent — running it twice on a clean graph deletes
31
- zero rows.
32
- """
33
- from __future__ import annotations
34
-
35
- import argparse
36
- import os
37
- import sys
38
-
39
- try:
40
- from neo4j import GraphDatabase
41
- except ImportError:
42
- print("ERROR: neo4j driver not installed. `pip install neo4j` first.", file=sys.stderr)
43
- sys.exit(1)
44
-
45
-
46
- def main() -> int:
47
- parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
48
- parser.add_argument(
49
- "--neo4j-uri",
50
- default=os.environ.get("NEO4J_URI", "bolt://localhost:7687"),
51
- help="Neo4j bolt URI (default: NEO4J_URI env or bolt://localhost:7687)",
52
- )
53
- parser.add_argument(
54
- "--neo4j-user",
55
- default=os.environ.get("NEO4J_USER", "neo4j"),
56
- )
57
- parser.add_argument(
58
- "--neo4j-password",
59
- default=os.environ.get("NEO4J_PASSWORD"),
60
- )
61
- parser.add_argument(
62
- "--confirm",
63
- action="store_true",
64
- help="Actually delete. Without this flag, runs in dry-run mode "
65
- "(reports counts only).",
66
- )
67
- args = parser.parse_args()
68
-
69
- if not args.neo4j_password:
70
- print("ERROR: --neo4j-password or NEO4J_PASSWORD env required", file=sys.stderr)
71
- return 2
72
-
73
- driver = GraphDatabase.driver(
74
- args.neo4j_uri, auth=(args.neo4j_user, args.neo4j_password),
75
- )
76
- try:
77
- with driver.session() as session:
78
- # Count legacy vs arena-scoped entities so the operator can
79
- # eyeball whether the new writer path has actually started
80
- # producing arena-scoped rows before deleting anything.
81
- legacy = session.run(
82
- "MATCH (e:Entity) WHERE e.arena IS NULL RETURN count(e) AS n"
83
- ).single()["n"]
84
- scoped = session.run(
85
- "MATCH (e:Entity) WHERE e.arena IS NOT NULL RETURN count(e) AS n"
86
- ).single()["n"]
87
- print(f"L3 Entity nodes: {legacy} legacy (no arena) / {scoped} arena-scoped")
88
-
89
- # Same breakdown for chunks — they should already all be
90
- # arena-tagged but worth verifying before/after.
91
- chunk_legacy = session.run(
92
- "MATCH (c:Chunk) WHERE c.arena IS NULL RETURN count(c) AS n"
93
- ).single()["n"]
94
- chunk_scoped = session.run(
95
- "MATCH (c:Chunk) WHERE c.arena IS NOT NULL RETURN count(c) AS n"
96
- ).single()["n"]
97
- print(f"L3 Chunk nodes: {chunk_legacy} legacy (no arena) / {chunk_scoped} arena-scoped")
98
-
99
- if not args.confirm:
100
- print("\nDry run — pass --confirm to actually delete legacy entities.")
101
- return 0
102
-
103
- if legacy == 0:
104
- print("\nNothing to do — all entities already arena-scoped.")
105
- return 0
106
-
107
- print(f"\nWiping {legacy} legacy entities…")
108
- result = session.run(
109
- "MATCH (e:Entity) WHERE e.arena IS NULL DETACH DELETE e RETURN count(e) AS n"
110
- ).single()
111
- deleted = result["n"]
112
- print(f"Deleted {deleted} legacy entities.")
113
-
114
- # Drop the old single-key entity index now that the data
115
- # is gone — safe to do because the new writer path uses
116
- # the compound (arena, name) index.
117
- try:
118
- session.run("DROP INDEX entity_name IF EXISTS")
119
- print("Dropped legacy `entity_name` index.")
120
- except Exception as e: # noqa: BLE001
121
- print(f"Note: could not drop entity_name index ({e}); ok if absent.")
122
- return 0
123
- finally:
124
- driver.close()
125
-
126
-
127
- if __name__ == "__main__":
128
- sys.exit(main())
@@ -1,259 +0,0 @@
1
- #!/usr/bin/env bash
2
- # e2e_arena.sh — multi-tenant store/retrieve smoke test against a live
3
- # memory-engine stack. Exercises /store, arena-scoped /search, and
4
- # /forget end-to-end across L0/L4/L5/L6 + the compat shim.
5
- #
6
- # Run after `docker compose -f docker-compose.yml -f docker-compose.test.yml \
7
- # up -d --wait l3 l4 l5 l6 l2 compat embed-stub`.
8
- set -eu
9
-
10
- BASE="${BASE:-http://localhost:8099}"
11
- WAIT_HEALTH_SECS="${WAIT_HEALTH_SECS:-180}"
12
- PASS=0
13
- FAIL=0
14
-
15
- ok() { echo " ✅ $1"; PASS=$((PASS+1)); }
16
- fail() { echo " ❌ $1"; FAIL=$((FAIL+1)); }
17
-
18
- # ---------------------------------------------------------------------------
19
- # Wait for the compat shim to come up. Its /health aggregates layer
20
- # health; we accept "ok" or "degraded" (l3 cosmetic 404 is known and
21
- # doesn't block functional paths).
22
- # ---------------------------------------------------------------------------
23
-
24
- echo "=== waiting for $BASE/health (up to ${WAIT_HEALTH_SECS}s) ==="
25
- deadline=$(( $(date +%s) + WAIT_HEALTH_SECS ))
26
- while :; do
27
- if H=$(curl -sf --max-time 5 "$BASE/health"); then
28
- s=$(echo "$H" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("status",""))')
29
- if [ "$s" = "ok" ] || [ "$s" = "degraded" ]; then
30
- echo " health: $s"
31
- break
32
- fi
33
- fi
34
- if [ "$(date +%s)" -ge "$deadline" ]; then
35
- echo " ❌ engine never became healthy"
36
- exit 1
37
- fi
38
- sleep 3
39
- done
40
-
41
- # ---------------------------------------------------------------------------
42
- # /store — two arenas, two distinct documents per arena.
43
- # ---------------------------------------------------------------------------
44
-
45
- echo ""
46
- echo "=== /store ==="
47
- post() {
48
- curl -sf -X POST "$BASE/store" \
49
- -H "Content-Type: application/json" \
50
- -d "$1"
51
- }
52
-
53
- R1=$(post '{"content":"Alpha team owns project Atlas","metadata":{"arena":"e2e-tenant-a","kind":"note","probe":"e2e-arena"}}')
54
- R2=$(post '{"content":"Alpha team owns project Borealis","metadata":{"arena":"e2e-tenant-b","kind":"note","probe":"e2e-arena"}}')
55
- R3=$(post '{"content":"Bravo team owns project Cobalt","metadata":{"arena":"e2e-tenant-a","kind":"note","probe":"e2e-arena"}}')
56
- R4=$(post '{"content":"Bravo team owns project Diamond","metadata":{"arena":"e2e-tenant-b","kind":"note","probe":"e2e-arena"}}')
57
-
58
- [ -n "$R1" ] && [ -n "$R2" ] && [ -n "$R3" ] && [ -n "$R4" ] \
59
- && ok "stored 4 docs across 2 arenas" \
60
- || fail "store"
61
-
62
- # Indexing is async on some layers — give the stack a brief settle.
63
- sleep 4
64
-
65
- # ---------------------------------------------------------------------------
66
- # /search — arena scoping. tenant-a should never see Borealis/Diamond,
67
- # tenant-b should never see Atlas/Cobalt.
68
- # ---------------------------------------------------------------------------
69
-
70
- echo ""
71
- echo "=== /search arena=e2e-tenant-a ==="
72
- SA=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
73
- -d '{"query":"team project","limit":20,"arena":"e2e-tenant-a"}')
74
- echo " hits: $(echo "$SA" | python3 -c 'import json,sys; print(len(json.load(sys.stdin).get("results",[])))')"
75
-
76
- leak_a=$(echo "$SA" | python3 -c '
77
- import json,sys
78
- data=json.load(sys.stdin).get("results",[])
79
- bad=[r for r in data if "Borealis" in r.get("content","") or "Diamond" in r.get("content","")]
80
- print(len(bad))')
81
- [ "$leak_a" = "0" ] && ok "tenant-a: no Borealis/Diamond leakage" \
82
- || fail "tenant-a leaked $leak_a tenant-b docs"
83
-
84
- found_atlas=$(echo "$SA" | python3 -c '
85
- import json,sys
86
- data=json.load(sys.stdin).get("results",[])
87
- print("yes" if any("Atlas" in r.get("content","") for r in data) else "no")')
88
- [ "$found_atlas" = "yes" ] && ok "tenant-a: Atlas recovered" \
89
- || fail "tenant-a missing Atlas"
90
-
91
- echo ""
92
- echo "=== /search arena=e2e-tenant-b ==="
93
- SB=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
94
- -d '{"query":"team project","limit":20,"arena":"e2e-tenant-b"}')
95
- echo " hits: $(echo "$SB" | python3 -c 'import json,sys; print(len(json.load(sys.stdin).get("results",[])))')"
96
-
97
- leak_b=$(echo "$SB" | python3 -c '
98
- import json,sys
99
- data=json.load(sys.stdin).get("results",[])
100
- bad=[r for r in data if "Atlas" in r.get("content","") or "Cobalt" in r.get("content","")]
101
- print(len(bad))')
102
- [ "$leak_b" = "0" ] && ok "tenant-b: no Atlas/Cobalt leakage" \
103
- || fail "tenant-b leaked $leak_b tenant-a docs"
104
-
105
- found_borealis=$(echo "$SB" | python3 -c '
106
- import json,sys
107
- data=json.load(sys.stdin).get("results",[])
108
- print("yes" if any("Borealis" in r.get("content","") for r in data) else "no")')
109
- [ "$found_borealis" = "yes" ] && ok "tenant-b: Borealis recovered" \
110
- || fail "tenant-b missing Borealis"
111
-
112
- # ---------------------------------------------------------------------------
113
- # /search with metadata_filter — arena+probe combo should still scope.
114
- # ---------------------------------------------------------------------------
115
-
116
- echo ""
117
- echo "=== /search metadata_filter probe=e2e-arena ==="
118
- SF=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
119
- -d '{"query":"team","limit":20,"arena":"e2e-tenant-a","metadata_filter":{"probe":"e2e-arena"}}')
120
- all_match=$(echo "$SF" | python3 -c '
121
- import json,sys
122
- data=json.load(sys.stdin).get("results",[])
123
- ok=all(r.get("metadata",{}).get("probe")=="e2e-arena" and r.get("metadata",{}).get("arena")=="e2e-tenant-a" for r in data)
124
- print("yes" if ok and data else "no")')
125
- [ "$all_match" = "yes" ] && ok "metadata_filter scopes to probe + arena" \
126
- || fail "metadata_filter let other rows through"
127
-
128
- # ---------------------------------------------------------------------------
129
- # User-scope vs tenant-wide arenas — proves the multi-arena search model.
130
- #
131
- # tenant-wide row arena=acme (visible to every user in acme)
132
- # user-A's row arena=acme:user-a (only user-A retrieves it)
133
- # user-B's row arena=acme:user-b (only user-B retrieves it)
134
- #
135
- # A user-scoped search sends arenas=[acme, acme:userX] so the user sees
136
- # tenant-wide AND own user-scope, but never another user's user-scope.
137
- # ---------------------------------------------------------------------------
138
-
139
- echo ""
140
- echo "=== user-scope vs tenant-wide ==="
141
- post '{"content":"acme tenant-wide rules of engagement","metadata":{"arena":"acme","probe":"e2e-arena"}}' >/dev/null
142
- post '{"content":"alice private note about Project Mercury","metadata":{"arena":"acme:alice","probe":"e2e-arena"}}' >/dev/null
143
- post '{"content":"bob private note about Project Saturn","metadata":{"arena":"acme:bob","probe":"e2e-arena"}}' >/dev/null
144
- sleep 3
145
-
146
- # Search as alice: arenas=[acme, acme:alice] — should see tenant-wide + own
147
- SAlice=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
148
- -d '{"query":"Project rules note","limit":20,"arenas":["acme","acme:alice"]}')
149
-
150
- alice_sees_tenant=$(echo "$SAlice" | python3 -c '
151
- import json,sys
152
- data=json.load(sys.stdin).get("results",[])
153
- print("yes" if any("tenant-wide" in r.get("content","") for r in data) else "no")')
154
- alice_sees_own=$(echo "$SAlice" | python3 -c '
155
- import json,sys
156
- data=json.load(sys.stdin).get("results",[])
157
- print("yes" if any("Mercury" in r.get("content","") for r in data) else "no")')
158
- alice_leak_bob=$(echo "$SAlice" | python3 -c '
159
- import json,sys
160
- data=json.load(sys.stdin).get("results",[])
161
- print(sum(1 for r in data if "Saturn" in r.get("content","")))')
162
-
163
- [ "$alice_sees_tenant" = "yes" ] && ok "alice: tenant-wide visible" \
164
- || fail "alice: missing tenant-wide row"
165
- [ "$alice_sees_own" = "yes" ] && ok "alice: own user-scope visible" \
166
- || fail "alice: missing own user-scope row"
167
- [ "$alice_leak_bob" = "0" ] && ok "alice: no leakage of bob's user-scope" \
168
- || fail "alice leaked $alice_leak_bob bob rows (cross-user!)"
169
-
170
- # Search as bob: arenas=[acme, acme:bob] — should see tenant-wide + own
171
- SBob=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
172
- -d '{"query":"Project rules note","limit":20,"arenas":["acme","acme:bob"]}')
173
-
174
- bob_sees_own=$(echo "$SBob" | python3 -c '
175
- import json,sys
176
- data=json.load(sys.stdin).get("results",[])
177
- print("yes" if any("Saturn" in r.get("content","") for r in data) else "no")')
178
- bob_leak_alice=$(echo "$SBob" | python3 -c '
179
- import json,sys
180
- data=json.load(sys.stdin).get("results",[])
181
- print(sum(1 for r in data if "Mercury" in r.get("content","")))')
182
-
183
- [ "$bob_sees_own" = "yes" ] && ok "bob: own user-scope visible" \
184
- || fail "bob: missing own user-scope row"
185
- [ "$bob_leak_alice" = "0" ] && ok "bob: no leakage of alice's user-scope" \
186
- || fail "bob leaked $bob_leak_alice alice rows (cross-user!)"
187
-
188
- # ---------------------------------------------------------------------------
189
- # Same content across two arenas — proves the arena-aware id derivation.
190
- # Pre-v0.7.8, identical content collapsed to one row in L4/L5/L6 because
191
- # the id was sha1(content); the second tenant's write overwrote the first.
192
- # ---------------------------------------------------------------------------
193
-
194
- echo ""
195
- echo "=== same-content-across-arenas ==="
196
- post '{"content":"shared phrase about Eclipse","metadata":{"arena":"e2e-tenant-x","probe":"e2e-arena"}}' >/dev/null
197
- post '{"content":"shared phrase about Eclipse","metadata":{"arena":"e2e-tenant-y","probe":"e2e-arena"}}' >/dev/null
198
- sleep 3
199
-
200
- SX=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
201
- -d '{"query":"Eclipse","limit":10,"arena":"e2e-tenant-x"}')
202
- SY=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
203
- -d '{"query":"Eclipse","limit":10,"arena":"e2e-tenant-y"}')
204
-
205
- x_has_phrase=$(echo "$SX" | python3 -c '
206
- import json,sys
207
- data=json.load(sys.stdin).get("results",[])
208
- print("yes" if any("Eclipse" in r.get("content","") for r in data) else "no")')
209
- y_has_phrase=$(echo "$SY" | python3 -c '
210
- import json,sys
211
- data=json.load(sys.stdin).get("results",[])
212
- print("yes" if any("Eclipse" in r.get("content","") for r in data) else "no")')
213
-
214
- [ "$x_has_phrase" = "yes" ] && ok "tenant-x: shared phrase preserved" \
215
- || fail "tenant-x lost the shared phrase (id collision?)"
216
- [ "$y_has_phrase" = "yes" ] && ok "tenant-y: shared phrase preserved" \
217
- || fail "tenant-y lost the shared phrase (id collision?)"
218
-
219
- # ---------------------------------------------------------------------------
220
- # /forget — tenant-scoped delete by arena.
221
- #
222
- # Pre-arena, /forget would unconditionally trigger a global wipe of
223
- # L0+L4+L3 inside the shim, regardless of what filters were passed. The
224
- # smoke test happily asserted "deleted >= 1" because the shim returned
225
- # the count of every row globally, even rows the caller didn't ask to
226
- # remove. That was the right thing for a bench reset, the wrong thing
227
- # for any real tenant operation.
228
- #
229
- # Now /forget honours the arena in the metadata filter:
230
- # - {metadata_contains: {arena: <tenant>}} → tenant-scoped wipe
231
- # - {metadata_contains: {<other>: <val>}} → L6 targeted only
232
- # - {} (empty body) → bench reset (gated
233
- # by GLOBAL_WIPE in the
234
- # internal endpoint)
235
- #
236
- # We assert here that wiping arena=e2e-tenant-a actually removes ≥1 row,
237
- # AND that arena=e2e-tenant-b's rows survived — proves real tenant
238
- # isolation rather than just "shim returned non-zero".
239
- # ---------------------------------------------------------------------------
240
-
241
- echo ""
242
- echo "=== /forget arena=e2e-tenant-a ==="
243
- F=$(curl -sf -X POST "$BASE/forget" -H "Content-Type: application/json" \
244
- -d '{"metadata_contains":{"arena":"e2e-tenant-a"}}')
245
- deleted=$(echo "$F" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("deleted",0))')
246
- echo " deleted: $deleted"
247
- [ "$deleted" -ge "1" ] && ok "/forget removed tenant-a rows" || fail "/forget tenant-a wipe"
248
-
249
- # tenant-b should still have its rows.
250
- B=$(curl -sf -X POST "$BASE/search" -H "Content-Type: application/json" \
251
- -d '{"arena":"e2e-tenant-b","query":"Borealis","limit":4}')
252
- b_hits=$(echo "$B" | python3 -c 'import json,sys; print(len(json.load(sys.stdin).get("results",[])))')
253
- [ "$b_hits" -ge "1" ] && ok "/forget left tenant-b intact" || fail "/forget leaked into tenant-b"
254
-
255
- echo ""
256
- echo "=== Result ==="
257
- echo " PASS: $PASS"
258
- echo " FAIL: $FAIL"
259
- exit $FAIL
@@ -1,13 +0,0 @@
1
- FROM python:3.12-slim
2
-
3
- WORKDIR /app
4
-
5
- RUN pip install --no-cache-dir fastapi "uvicorn[standard]" pydantic
6
-
7
- COPY server.py /app/server.py
8
-
9
- ENV EMBED_DIM=4096
10
-
11
- EXPOSE 8041
12
-
13
- CMD ["python", "-m", "uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8041"]
@@ -1,80 +0,0 @@
1
- """Deterministic embedding stub for hermetic CI runs.
2
-
3
- Returns a fixed-dim vector per input string, derived from a hash so the
4
- same text always maps to the same vector. Cosine similarity between two
5
- embeddings equals 1.0 only for identical input strings, and decreases
6
- roughly with edit distance — enough to exercise the engine's vector
7
- search paths in CI without an actual embedding model.
8
-
9
- Speaks both shapes the engine uses:
10
- POST /v1/embeddings { input, model } -> { data:[{embedding:[...] }] }
11
- POST /v1/embed { input, model } -> { embeddings:[[...]] }
12
-
13
- Run:
14
- EMBED_DIM=4096 uvicorn server:app --host 0.0.0.0 --port 8041
15
- """
16
- from __future__ import annotations
17
-
18
- import hashlib
19
- import math
20
- import os
21
- from typing import Any
22
-
23
- from fastapi import FastAPI
24
- from pydantic import BaseModel
25
-
26
- EMBED_DIM = int(os.environ.get("EMBED_DIM", "4096"))
27
-
28
- app = FastAPI(title="embed-stub")
29
-
30
-
31
- class EmbedRequest(BaseModel):
32
- input: Any
33
- model: str | None = None
34
-
35
-
36
- def _vector_for(text: str) -> list[float]:
37
- """Deterministic vector: hash the text, expand to EMBED_DIM, L2-normalise."""
38
- text = text or ""
39
- seed = hashlib.sha256(text.encode("utf-8")).digest()
40
- raw: list[int] = []
41
- counter = 0
42
- while len(raw) < EMBED_DIM:
43
- chunk = hashlib.sha256(seed + counter.to_bytes(4, "big")).digest()
44
- raw.extend(chunk)
45
- counter += 1
46
- floats = [(b - 127.5) / 127.5 for b in raw[:EMBED_DIM]]
47
- norm = math.sqrt(sum(x * x for x in floats)) or 1.0
48
- return [x / norm for x in floats]
49
-
50
-
51
- def _normalise_inputs(inp: Any) -> list[str]:
52
- if isinstance(inp, str):
53
- return [inp]
54
- if isinstance(inp, list):
55
- return [str(x) for x in inp]
56
- return [str(inp)]
57
-
58
-
59
- @app.get("/health")
60
- def health() -> dict:
61
- return {"status": "ok", "dim": EMBED_DIM}
62
-
63
-
64
- @app.post("/v1/embeddings")
65
- def openai_embeddings(req: EmbedRequest) -> dict:
66
- texts = _normalise_inputs(req.input)
67
- return {
68
- "object": "list",
69
- "data": [
70
- {"object": "embedding", "index": i, "embedding": _vector_for(t)}
71
- for i, t in enumerate(texts)
72
- ],
73
- "model": req.model or "embed-stub",
74
- }
75
-
76
-
77
- @app.post("/v1/embed")
78
- def lambda_gateway_embed(req: EmbedRequest) -> dict:
79
- texts = _normalise_inputs(req.input)
80
- return {"embeddings": [_vector_for(t) for t in texts]}