feat: add orgid field to hermes_services for organization-scoped service isolation

- Add orgid field (str32, not nullable) to hermes_services table
- Replace user_id with orgid in all service CRUD operations (SQL + functions)
- Update function signatures: get_all_services, create_service, delete_service,
  get_service_by_id, test_service_connection, create_session,
  send_message_to_service, get_session_messages all use orgid
- Add orgid indexes: idx_hermes_services_orgid, idx_hermes_services_orgid_status
- Add logined_userorgid filtering to CRUD definition for automatic framework-level isolation
- Update all .dspy files to use get_userorgid() for org-scoped service queries
- Update init/data.json and db_tables.py to reflect orgid field
This commit is contained in:
yumoqing 2026-04-27 11:50:05 +08:00
parent 4df2f72758
commit 0e0ee695e6
11 changed files with 318 additions and 91 deletions

View File

@ -13,43 +13,43 @@ SERVICES_CRUD = {
"operations": { "operations": {
"create": { "create": {
"name": "create_service_record", "name": "create_service_record",
"description": "Create a new service record for the current user", "description": "Create a new service record for the current organization",
"parameters": ["user_id", "name", "service_url", "description", "apikey"], "parameters": ["orgid", "name", "service_url", "description", "apikey"],
"sql_template": """ "sql_template": """
INSERT INTO services (id, user_id, name, service_url, description, apikey, status, created_at, updated_at) INSERT INTO services (id, orgid, name, service_url, description, apikey, status, created_at, updated_at)
VALUES (${id}$, ${user_id}$, ${name}$, ${service_url}$, ${description}$, ${apikey}$, ${status}$, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) VALUES (${id}$, ${orgid}$, ${name}$, ${service_url}$, ${description}$, ${apikey}$, ${status}$, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", """,
"return_fields": ["id"] "return_fields": ["id"]
}, },
"read_all": { "read_all": {
"name": "get_all_services_for_user", "name": "get_all_services_for_org",
"description": "Get all services for the current user", "description": "Get all services for the current organization",
"parameters": ["user_id"], "parameters": ["orgid"],
"sql_template": """ "sql_template": """
SELECT id, user_id, name, service_url, description, apikey, status, SELECT id, orgid, name, service_url, description, apikey, status,
created_at, updated_at created_at, updated_at
FROM services FROM services
WHERE user_id = ${user_id}$ WHERE orgid = ${orgid}$
ORDER BY created_at DESC ORDER BY created_at DESC
""", """,
"return_fields": ["id", "user_id", "name", "service_url", "description", "apikey", "status", "created_at", "updated_at"] "return_fields": ["id", "orgid", "name", "service_url", "description", "apikey", "status", "created_at", "updated_at"]
}, },
"read_by_id": { "read_by_id": {
"name": "get_service_by_id_and_user", "name": "get_service_by_id_and_org",
"description": "Get a specific service by ID for the current user", "description": "Get a specific service by ID for the current organization",
"parameters": ["service_id", "user_id"], "parameters": ["service_id", "orgid"],
"sql_template": """ "sql_template": """
SELECT id, user_id, name, service_url, description, apikey, status, SELECT id, orgid, name, service_url, description, apikey, status,
created_at, updated_at created_at, updated_at
FROM services FROM services
WHERE id = ${service_id}$ AND user_id = ${user_id}$ WHERE id = ${service_id}$ AND orgid = ${orgid}$
""", """,
"return_fields": ["id", "user_id", "name", "service_url", "description", "apikey", "status", "created_at", "updated_at"] "return_fields": ["id", "orgid", "name", "service_url", "description", "apikey", "status", "created_at", "updated_at"]
}, },
"update": { "update": {
"name": "update_service_record", "name": "update_service_record",
"description": "Update an existing service record", "description": "Update an existing service record",
"parameters": ["service_id", "user_id", "name", "service_url", "description", "apikey", "status"], "parameters": ["service_id", "orgid", "name", "service_url", "description", "apikey", "status"],
"sql_template": """ "sql_template": """
UPDATE services UPDATE services
SET name = ${name}$, SET name = ${name}$,
@ -58,17 +58,17 @@ SERVICES_CRUD = {
apikey = ${apikey}$, apikey = ${apikey}$,
status = ${status}$, status = ${status}$,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = ${service_id}$ AND user_id = ${user_id}$ WHERE id = ${service_id}$ AND orgid = ${orgid}$
""", """,
"return_fields": [] "return_fields": []
}, },
"delete": { "delete": {
"name": "delete_service_record", "name": "delete_service_record",
"description": "Delete a service record for the current user", "description": "Delete a service record for the current organization",
"parameters": ["service_id", "user_id"], "parameters": ["service_id", "orgid"],
"sql_template": """ "sql_template": """
DELETE FROM services DELETE FROM services
WHERE id = ${service_id}$ AND user_id = ${user_id}$ WHERE id = ${service_id}$ AND orgid = ${orgid}$
""", """,
"return_fields": [] "return_fields": []
} }

View File

@ -7,7 +7,7 @@ Follows database-table-definition-spec: summary/fields/indexes/codes four-sectio
SERVICES_TABLE = { SERVICES_TABLE = {
"summary": { "summary": {
"name": "services", "name": "services",
"description": "Stores Hermes service configurations for each user", "description": "Stores Hermes service configurations for each organization",
"module": "hermes-web-cli" "module": "hermes-web-cli"
}, },
"fields": [ "fields": [
@ -19,10 +19,10 @@ SERVICES_TABLE = {
"description": "Unique service identifier (UUID)" "description": "Unique service identifier (UUID)"
}, },
{ {
"name": "user_id", "name": "orgid",
"type": "varchar(64)", "type": "varchar(64)",
"nullable": False, "nullable": False,
"description": "Owner user ID for multi-user isolation" "description": "Organization ID that owns this service"
}, },
{ {
"name": "name", "name": "name",
@ -73,10 +73,10 @@ SERVICES_TABLE = {
], ],
"indexes": [ "indexes": [
{ {
"name": "idx_services_user_id", "name": "idx_services_orgid",
"fields": ["user_id"], "fields": ["orgid"],
"unique": False, "unique": False,
"description": "Index for user-based queries" "description": "Index for org-based queries"
}, },
{ {
"name": "idx_services_status", "name": "idx_services_status",
@ -85,10 +85,10 @@ SERVICES_TABLE = {
"description": "Index for status-based queries" "description": "Index for status-based queries"
}, },
{ {
"name": "idx_services_user_status", "name": "idx_services_orgid_status",
"fields": ["user_id", "status"], "fields": ["orgid", "status"],
"unique": False, "unique": False,
"description": "Composite index for user and status queries" "description": "Composite index for org and status queries"
} }
], ],
"codes": [] "codes": []

View File

@ -10,13 +10,14 @@ implement these endpoints by calling the functions provided in this module.
""" """
import json import json
import uuid
import asyncio import asyncio
import aiohttp import aiohttp
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from datetime import datetime from datetime import datetime
# Import sqlor database module # Import sqlor database module
from sqlor.dbpools import get_sor_context from sqlor.dbpools import get_sor_context, DBPools
# Import database table definitions and CRUD operations # Import database table definitions and CRUD operations
from .db_tables import TABLE_DEFINITIONS from .db_tables import TABLE_DEFINITIONS
@ -54,23 +55,23 @@ def load_hermes_web_cli():
return True return True
# Database operations using sqlor-database-module # Database operations using sqlor-database-module
async def get_all_services(user_id: str) -> List[Dict]: async def get_all_services(orgid: str) -> List[Dict]:
"""Get all registered Hermes services for the specified user from database. """Get all registered Hermes services for the specified organization from database.
Args: Args:
user_id: The ID of the user whose services to retrieve orgid: The ID of the organization whose services to retrieve
Returns: Returns:
List of service dictionaries belonging to the specified user List of service dictionaries belonging to the specified organization
""" """
try: try:
# Query services table with user_id filter using sqlor-database-module # Query services table with orgid filter using sqlor-database-module
db = DBPools() db = DBPools()
env = ServerEnv() env = ServerEnv()
dbname = env.get_module_dbname() dbname = env.get_module_dbname()
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
sql_template = SERVICES_CRUD['operations']['read_all']['sql_template'] sql_template = SERVICES_CRUD['operations']['read_all']['sql_template']
recs = await sor.sqlExe(sql_template, {'user_id': user_id}) recs = await sor.sqlExe(sql_template, {'orgid': orgid})
# Convert datetime objects to ISO format strings for JSON serialization # Convert datetime objects to ISO format strings for JSON serialization
result = [] result = []
@ -88,13 +89,13 @@ async def get_all_services(user_id: str) -> List[Dict]:
print(f"Error getting services: {str(e)}") print(f"Error getting services: {str(e)}")
return [] return []
async def create_service(name: str, url: str, user_id: str, description: str = "", apikey: str = "") -> str: async def create_service(name: str, url: str, orgid: str, description: str = "", apikey: str = "") -> str:
"""Create a new Hermes service registration for the specified user. """Create a new Hermes service registration for the specified organization.
Args: Args:
name: Service name name: Service name
url: Service URL url: Service URL
user_id: The ID of the user creating the service orgid: The ID of the organization creating the service
description: Service description (optional) description: Service description (optional)
apikey: API key for the service (optional) apikey: API key for the service (optional)
@ -116,7 +117,7 @@ async def create_service(name: str, url: str, user_id: str, description: str = "
sql_template = SERVICES_CRUD['operations']['create']['sql_template'] sql_template = SERVICES_CRUD['operations']['create']['sql_template']
await sor.sqlExe(sql_template, { await sor.sqlExe(sql_template, {
'id': service_id, 'id': service_id,
'user_id': user_id, 'orgid': orgid,
'name': name, 'name': name,
'service_url': url, 'service_url': url,
'description': description, 'description': description,
@ -130,24 +131,24 @@ async def create_service(name: str, url: str, user_id: str, description: str = "
print(f"Error creating service: {str(e)}") print(f"Error creating service: {str(e)}")
raise raise
async def delete_service(service_id: str, user_id: str) -> bool: async def delete_service(service_id: str, orgid: str) -> bool:
"""Delete a Hermes service registration (only if owned by specified user). """Delete a Hermes service registration (only if owned by specified organization).
Args: Args:
service_id: The ID of the service to delete service_id: The ID of the service to delete
user_id: The ID of the user attempting deletion orgid: The ID of the organization attempting deletion
Returns: Returns:
True if deleted successfully, False otherwise True if deleted successfully, False otherwise
""" """
try: try:
# Verify service belongs to current user before deletion # Verify service belongs to current org before deletion
service = await get_service_by_id(service_id, user_id) service = await get_service_by_id(service_id, orgid)
if not service: if not service:
return False return False
if service.get("user_id") != user_id: if service.get("orgid") != orgid:
print(f"Permission denied: Service {service_id} does not belong to user {user_id}") print(f"Permission denied: Service {service_id} does not belong to org {orgid}")
return False return False
# Delete from database using sqlor-database-module # Delete from database using sqlor-database-module
@ -158,7 +159,7 @@ async def delete_service(service_id: str, user_id: str) -> bool:
sql_template = SERVICES_CRUD['operations']['delete']['sql_template'] sql_template = SERVICES_CRUD['operations']['delete']['sql_template']
await sor.sqlExe(sql_template, { await sor.sqlExe(sql_template, {
'service_id': service_id, 'service_id': service_id,
'user_id': user_id 'orgid': orgid
}) })
# Also delete associated sessions # Also delete associated sessions
@ -167,10 +168,9 @@ async def delete_service(service_id: str, user_id: str) -> bool:
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
await sor.sqlExe(""" await sor.sqlExe("""
DELETE FROM sessions DELETE FROM sessions
WHERE service_id = ${service_id}$ AND user_id = ${user_id}$ WHERE service_id = ${service_id}$
""", { """, {
'service_id': service_id, 'service_id': service_id
'user_id': user_id
}) })
return True return True
@ -178,18 +178,18 @@ async def delete_service(service_id: str, user_id: str) -> bool:
print(f"Error deleting service: {str(e)}") print(f"Error deleting service: {str(e)}")
return False return False
async def get_service_by_id(service_id: str, user_id: str) -> Optional[Dict]: async def get_service_by_id(service_id: str, orgid: str) -> Optional[Dict]:
"""Get service configuration by ID (only if owned by specified user). """Get service configuration by ID (only if owned by specified organization).
Args: Args:
service_id: The ID of the service to retrieve service_id: The ID of the service to retrieve
user_id: The ID of the user requesting the service orgid: The ID of the organization requesting the service
Returns: Returns:
Service dictionary if found and owned by user, None otherwise Service dictionary if found and owned by org, None otherwise
""" """
try: try:
# Query database directly with user_id filter for security # Query database directly with orgid filter for security
db = DBPools() db = DBPools()
env = ServerEnv() env = ServerEnv()
dbname = env.get_module_dbname() dbname = env.get_module_dbname()
@ -197,7 +197,7 @@ async def get_service_by_id(service_id: str, user_id: str) -> Optional[Dict]:
sql_template = SERVICES_CRUD['operations']['read_by_id']['sql_template'] sql_template = SERVICES_CRUD['operations']['read_by_id']['sql_template']
recs = await sor.sqlExe(sql_template, { recs = await sor.sqlExe(sql_template, {
'service_id': service_id, 'service_id': service_id,
'user_id': user_id 'orgid': orgid
}) })
if len(recs) > 0: if len(recs) > 0:
@ -215,11 +215,12 @@ async def get_service_by_id(service_id: str, user_id: str) -> Optional[Dict]:
return None return None
# Service connection testing # Service connection testing
async def test_service_connection(service_id: str) -> Tuple[bool, str]: async def test_service_connection(service_id: str, orgid: str = "") -> Tuple[bool, str]:
"""Test connection to a Hermes service endpoint. """Test connection to a Hermes service endpoint.
Args: Args:
service_id: The ID of the service to test service_id: The ID of the service to test
orgid: The ID of the organization (optional, for org-scoped lookup)
Returns: Returns:
Tuple[bool, str]: (is_connected, status_message) Tuple[bool, str]: (is_connected, status_message)
@ -230,11 +231,21 @@ async def test_service_connection(service_id: str) -> Tuple[bool, str]:
env = ServerEnv() env = ServerEnv()
dbname = env.get_module_dbname() dbname = env.get_module_dbname()
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
sql_template = SERVICES_CRUD['operations']['read_by_id']['sql_template'] if orgid:
recs = await sor.sqlExe(sql_template, { sql_template = SERVICES_CRUD['operations']['read_by_id']['sql_template']
'service_id': service_id, recs = await sor.sqlExe(sql_template, {
'user_id': '' 'service_id': service_id,
}) 'orgid': orgid
})
else:
recs = await sor.sqlExe("""
SELECT id, orgid, name, service_url, description, apikey, status,
created_at, updated_at
FROM services
WHERE id = ${service_id}$
""", {
'service_id': service_id
})
if not recs: if not recs:
return False, "Service not found" return False, "Service not found"
service = dict(recs[0]) service = dict(recs[0])
@ -263,11 +274,11 @@ async def test_service_connection(service_id: str) -> Tuple[bool, str]:
return False, f"Error: {str(e)}" return False, f"Error: {str(e)}"
# Session management # Session management
async def create_session(service_id: str, user_id: str, user_message: str = "") -> str: async def create_session(service_id: str, user_id: str, orgid: str, user_message: str = "") -> str:
"""Create a new session with a Hermes service.""" """Create a new session with a Hermes service."""
try: try:
# Get service configuration (verify it belongs to current user) # Get service configuration (verify it belongs to current org)
service = await get_service_by_id(service_id, user_id) service = await get_service_by_id(service_id, orgid)
if not service: if not service:
raise ValueError(f"Service {service_id} not found or access denied") raise ValueError(f"Service {service_id} not found or access denied")
@ -323,7 +334,7 @@ async def create_session(service_id: str, user_id: str, user_message: str = "")
print(f"Error creating session: {str(e)}") print(f"Error creating session: {str(e)}")
raise raise
async def send_message_to_service(service_id: str, session_id: str, message: str, user_id: str) -> Dict: async def send_message_to_service(service_id: str, session_id: str, message: str, user_id: str, orgid: str) -> Dict:
"""Send a message to a Hermes service and get response (only if session owned by specified user). """Send a message to a Hermes service and get response (only if session owned by specified user).
Args: Args:
@ -331,6 +342,7 @@ async def send_message_to_service(service_id: str, session_id: str, message: str
session_id: The session ID session_id: The session ID
message: The message to send message: The message to send
user_id: The ID of the user sending the message user_id: The ID of the user sending the message
orgid: The ID of the organization
Returns: Returns:
Response from the service Response from the service
@ -341,9 +353,9 @@ async def send_message_to_service(service_id: str, session_id: str, message: str
if not session: if not session:
raise ValueError(f"Session {session_id} not found or access denied for user {user_id}") raise ValueError(f"Session {session_id} not found or access denied for user {user_id}")
service = await get_service_by_id(session['service_id'], user_id) service = await get_service_by_id(session['service_id'], orgid)
if not service: if not service:
raise ValueError(f"Service for session {session_id} not found or access denied for user {user_id}") raise ValueError(f"Service for session {session_id} not found or access denied for org {orgid}")
service_url = service["service_url"] service_url = service["service_url"]
apikey = service.get("apikey", "") apikey = service.get("apikey", "")
@ -373,25 +385,26 @@ async def send_message_to_service(service_id: str, session_id: str, message: str
print(f"Error sending message: {e}") print(f"Error sending message: {e}")
raise raise
async def get_session_messages(session_id: str, user_id: str) -> List[Dict]: async def get_session_messages(session_id: str, user_id: str, orgid: str) -> List[Dict]:
"""Get all messages for a session (only if session owned by specified user). """Get all messages for a session (only if session owned by specified user).
Args: Args:
session_id: The session ID session_id: The session ID
user_id: The ID of the user requesting messages user_id: The ID of the user requesting messages
orgid: The ID of the organization
Returns: Returns:
List of message dictionaries List of message dictionaries
""" """
try: try:
# Verify session belongs to current user before getting messages # Verify session belongs to current user before getting messages
session = await get_session_by_id(session_id) session = await get_session_by_id(session_id, user_id)
if not session: if not session:
print(f"Session {session_id} not found or access denied for user {user_id}") print(f"Session {session_id} not found or access denied for user {user_id}")
return [] return []
# Get the associated service # Get the associated service (verify org access)
service = await get_service_by_id(session['service_id'], user_id) service = await get_service_by_id(session['service_id'], orgid)
if not service: if not service:
print(f"Service for session {session_id} not found or access denied") print(f"Service for session {session_id} not found or access denied")
return [] return []

View File

@ -2,12 +2,12 @@
"hermes_services": [ "hermes_services": [
{ {
"id": "00000000-0000-0000-0000-000000000001", "id": "00000000-0000-0000-0000-000000000001",
"user_id": null, "orgid": "",
"name": "Local Hermes Service", "name": "Local Hermes Service",
"service_url": "http://localhost:9120", "service_url": "http://localhost:9120",
"api_key": null, "apikey": "",
"description": "Default local Hermes service instance", "description": "Default local Hermes service instance",
"status": "active" "status": "active"
} }
] ]
} }

View File

@ -3,7 +3,8 @@
"title": "Hermes Services", "title": "Hermes Services",
"params": { "params": {
"sortby": ["created_at desc"], "sortby": ["created_at desc"],
"confidential_fields": [], "logined_userorgid": "orgid",
"confidential_fields": ["apikey"],
"browserfields": { "browserfields": {
"exclouded": ["id", "service_url", "created_at", "updated_at"], "exclouded": ["id", "service_url", "created_at", "updated_at"],
"alters": { "alters": {

View File

@ -16,6 +16,14 @@
"nullable": "no", "nullable": "no",
"comments": "Primary key - UUID format" "comments": "Primary key - UUID format"
}, },
{
"name": "orgid",
"title": "Organization ID",
"type": "str",
"length": 32,
"nullable": "no",
"comments": "Organization ID that owns this service"
},
{ {
"name": "name", "name": "name",
"title": "Service Name", "title": "Service Name",
@ -72,6 +80,11 @@
} }
], ],
"indexes": [ "indexes": [
{
"name": "idx_hermes_services_orgid",
"idxtype": "index",
"idxfields": ["orgid"]
},
{ {
"name": "idx_hermes_services_name", "name": "idx_hermes_services_name",
"idxtype": "index", "idxtype": "index",
@ -81,6 +94,11 @@
"name": "idx_hermes_services_status", "name": "idx_hermes_services_status",
"idxtype": "index", "idxtype": "index",
"idxfields": ["status"] "idxfields": ["status"]
},
{
"name": "idx_hermes_services_orgid_status",
"idxtype": "index",
"idxfields": ["orgid", "status"]
} }
], ],
"codes": [] "codes": []

191
test_orgid_refactor.py Normal file
View File

@ -0,0 +1,191 @@
#!/usr/bin/env python3
"""
Test script to verify orgid refactoring of hermes_services table.
Validates: function signatures, SQL templates, JSON definitions, .dspy files.
"""
import sys
import os
import json
import inspect
MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
def test_table_definition():
"""Verify models/hermes_services.json has orgid field and correct indexes."""
path = os.path.join(MODULE_DIR, 'models', 'hermes_services.json')
with open(path) as f:
data = json.load(f)
fields = {f['name']: f for f in data['fields']}
# orgid field exists with correct properties
assert 'orgid' in fields, "orgid field missing from table definition"
assert fields['orgid']['type'] == 'str', "orgid type should be str"
assert fields['orgid']['length'] == 32, "orgid length should be 32"
assert fields['orgid']['nullable'] == 'no', "orgid should be not nullable"
# user_id should NOT exist
assert 'user_id' not in fields, "user_id field should be removed"
# orgid indexes exist
index_names = [i['name'] for i in data['indexes']]
assert 'idx_hermes_services_orgid' in index_names, "missing idx_hermes_services_orgid"
assert 'idx_hermes_services_orgid_status' in index_names, "missing idx_hermes_services_orgid_status"
# old user indexes removed
assert 'idx_hermes_services_user_id' not in index_names, "old user index should be removed"
print(" table definition OK")
def test_crud_definition():
"""Verify json/hermes_services.json has logined_userorgid param."""
path = os.path.join(MODULE_DIR, 'json', 'hermes_services.json')
with open(path) as f:
data = json.load(f)
params = data['params']
assert params.get('logined_userorgid') == 'orgid', "logined_userorgid should be 'orgid'"
assert 'apikey' in params.get('confidential_fields', []), "apikey should be confidential"
print(" CRUD definition OK")
def test_db_tables():
"""Verify db_tables.py SERVICES_TABLE uses orgid."""
path = os.path.join(MODULE_DIR, 'hermes_web_cli', 'db_tables.py')
with open(path) as f:
content = f.read()
assert '"orgid"' in content, "db_tables.py should contain orgid"
assert 'idx_services_orgid' in content, "should have idx_services_orgid index"
assert 'idx_services_orgid_status' in content, "should have idx_services_orgid_status index"
# old user_id references in SERVICES_TABLE should be gone
svc_start = content.index('SERVICES_TABLE = {')
svc_end = content.index('# Sessions table', svc_start)
svc_section = content[svc_start:svc_end]
assert '"user_id"' not in svc_section, "SERVICES_TABLE should not have user_id"
print(" db_tables.py OK")
def test_crud_ops():
"""Verify crud_ops.py SERVICES_CRUD SQL uses orgid."""
path = os.path.join(MODULE_DIR, 'hermes_web_cli', 'crud_ops.py')
with open(path) as f:
content = f.read()
svc_start = content.index('SERVICES_CRUD = {')
svc_end = content.index('# Sessions CRUD', svc_start)
svc_section = content[svc_start:svc_end]
assert '${orgid}$' in svc_section, "SQL should use ${orgid}$ parameter"
assert '${user_id}$' not in svc_section, "SQL should not use ${user_id}$ parameter"
assert 'orgid = ${orgid}$' in svc_section or 'AND orgid = ${orgid}$' in svc_section, "WHERE should filter by orgid"
print(" crud_ops.py OK")
def test_init_signatures():
"""Verify init.py function signatures use orgid."""
path = os.path.join(MODULE_DIR, 'hermes_web_cli', 'init.py')
with open(path) as f:
content = f.read()
# Check function signatures
assert 'async def get_all_services(orgid: str)' in content, "get_all_services should take orgid"
assert 'async def create_service(name: str, url: str, orgid: str' in content, "create_service should take orgid"
assert 'async def delete_service(service_id: str, orgid: str)' in content, "delete_service should take orgid"
assert 'async def get_service_by_id(service_id: str, orgid: str)' in content, "get_service_by_id should take orgid"
assert 'test_service_connection(service_id: str, orgid: str = "")' in content, "test_service_connection should take optional orgid"
assert 'async def create_session(service_id: str, user_id: str, orgid: str' in content, "create_session should take orgid"
assert 'send_message_to_service(service_id: str, session_id: str, message: str, user_id: str, orgid: str)' in content, "send_message_to_service should take orgid"
assert 'async def get_session_messages(session_id: str, user_id: str, orgid: str)' in content, "get_session_messages should take orgid"
# Verify SQL calls use orgid param
assert "'orgid': orgid" in content, "SQL params should pass orgid"
assert "'orgid': user_id" not in content, "should not pass user_id as orgid"
print(" init.py signatures OK")
def test_dspy_files():
"""Verify .dspy files use get_userorgid()."""
files_to_check = [
'wwwroot/services/list/index.dspy',
'wwwroot/services/test/index.dspy',
'wwwroot/services/remove/index.dspy',
'wwwroot/sessions/create_session.dspy',
]
for fpath in files_to_check:
full_path = os.path.join(MODULE_DIR, fpath)
with open(full_path) as f:
content = f.read()
assert 'get_userorgid()' in content, f"{fpath} should call get_userorgid()"
# services/list should pass orgid to get_all_services
with open(os.path.join(MODULE_DIR, 'wwwroot/services/list/index.dspy')) as f:
content = f.read()
assert 'get_all_services(orgid)' in content, "services/list should pass orgid to get_all_services"
# services/remove should pass orgid to delete_service
with open(os.path.join(MODULE_DIR, 'wwwroot/services/remove/index.dspy')) as f:
content = f.read()
assert 'delete_service(service_id, orgid)' in content, "services/remove should pass orgid to delete_service"
# create_session should pass both user_id and orgid
with open(os.path.join(MODULE_DIR, 'wwwroot/sessions/create_session.dspy')) as f:
content = f.read()
assert 'create_session(service_id, user_id, orgid' in content, "create_session should pass both user_id and orgid"
print(" .dspy files OK")
def test_init_data():
"""Verify init/data.json uses orgid field."""
path = os.path.join(MODULE_DIR, 'init', 'data.json')
with open(path) as f:
data = json.load(f)
for svc in data.get('hermes_services', []):
assert 'orgid' in svc, "init data should have orgid field"
assert 'user_id' not in svc, "init data should not have user_id field"
print(" init/data.json OK")
def test_syntax():
"""Verify all Python files compile."""
import py_compile
for fpath in ['hermes_web_cli/init.py', 'hermes_web_cli/crud_ops.py', 'hermes_web_cli/db_tables.py']:
full_path = os.path.join(MODULE_DIR, fpath)
try:
py_compile.compile(full_path, doraise=True)
except py_compile.PyCompileError as e:
assert False, f"Syntax error in {fpath}: {e}"
print(" Python syntax OK")
if __name__ == "__main__":
tests = [
("Syntax check", test_syntax),
("Table definition", test_table_definition),
("CRUD definition", test_crud_definition),
("db_tables.py", test_db_tables),
("crud_ops.py", test_crud_ops),
("init.py signatures", test_init_signatures),
(".dspy files", test_dspy_files),
("init/data.json", test_init_data),
]
passed = 0
failed = 0
for name, test_fn in tests:
try:
test_fn()
passed += 1
except AssertionError as e:
print(f" FAILED: {e}")
failed += 1
except Exception as e:
print(f" ERROR: {e}")
failed += 1
print(f"\n{'='*50}")
print(f"Results: {passed} passed, {failed} failed out of {len(tests)} tests")
if failed > 0:
print("FAILED - do not commit")
sys.exit(1)
else:
print("ALL TESTS PASSED")
sys.exit(0)

View File

@ -2,11 +2,11 @@
# This .dspy file uses functions provided by load_hermes_web_cli() # This .dspy file uses functions provided by load_hermes_web_cli()
try: try:
# Get current user ID using ahserver's built-in get_user() function # Get current user's org ID using ahserver's built-in get_userorgid() function
user_id = await get_user() orgid = await get_userorgid()
# Use the function provided by the hermes-web-cli module # Use the function provided by the hermes-web-cli module
services = await get_all_services(user_id) services = await get_all_services(orgid)
# Format services for UI display # Format services for UI display
result = [] result = []

View File

@ -6,15 +6,15 @@ try:
if not service_id: if not service_id:
return {"error": "Service ID is required"} return {"error": "Service ID is required"}
# Get current user ID using ahserver's built-in get_user() function # Get current user's org ID using ahserver's built-in get_userorgid() function
user_id = await get_user() orgid = await get_userorgid()
# Call the function provided by the hermes-web-cli module # Call the function provided by the hermes-web-cli module
success = await delete_service(service_id, user_id) success = await delete_service(service_id, orgid)
if success: if success:
return {"success": True, "message": "Service removed successfully"} return {"success": True, "message": "Service removed successfully"}
else: else:
return {"error": "Failed to remove service"} return {"error": "Failed to remove service"}
except Exception as e: except Exception as e:
return {"error": str(e)} return {"error": str(e)}

View File

@ -6,10 +6,13 @@ try:
if not service_id: if not service_id:
return {"error": "Service ID is required"} return {"error": "Service ID is required"}
# Get current user's org ID
orgid = await get_userorgid()
# Call the function provided by the hermes-web-cli module # Call the function provided by the hermes-web-cli module
is_connected, status_msg = await test_service_connection(service_id) is_connected, status_msg = await test_service_connection(service_id, orgid)
return {"status": status_msg} return {"status": status_msg}
except Exception as e: except Exception as e:
return {"error": str(e)} return {"error": str(e)}

View File

@ -17,12 +17,13 @@ try:
# Redirect back to form with error # Redirect back to form with error
return {"redirect": "/hermes-web-cli/new_session.ui?error=Service+ID+is+required"} return {"redirect": "/hermes-web-cli/new_session.ui?error=Service+ID+is+required"}
# Get current user ID using ahserver's built-in get_user() function # Get current user ID and org ID
user_id = await get_user() user_id = await get_user()
orgid = await get_userorgid()
# Use the function provided by the hermes-web-cli module # Use the function provided by the hermes-web-cli module
# This will call the remote hermes-service API with user_id support # Service lookup uses orgid, session ownership uses user_id
session_result = await create_session(service_id, user_id, "") session_result = await create_session(service_id, user_id, orgid, "")
if session_result: if session_result:
# Redirect to the new session's user interaction page # Redirect to the new session's user interaction page
@ -34,4 +35,4 @@ try:
except Exception as e: except Exception as e:
# Log the error and redirect back with error message # Log the error and redirect back with error message
print(f"Error creating session: {str(e)}") print(f"Error creating session: {str(e)}")
return {"redirect": f"/hermes-web-cli/new_session.ui?error=Internal+error:+{str(e)}"} return {"redirect": f"/hermes-web-cli/new_session.ui?error=Internal+error:+{str(e)}"}