From 0e0ee695e630fc5559e2e778cc729818a30732e5 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Mon, 27 Apr 2026 11:50:05 +0800 Subject: [PATCH] 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 --- hermes_web_cli/crud_ops.py | 42 +++--- hermes_web_cli/db_tables.py | 18 +-- hermes_web_cli/init.py | 101 ++++++++------ init/data.json | 6 +- json/hermes_services.json | 3 +- models/hermes_services.json | 18 +++ test_orgid_refactor.py | 191 +++++++++++++++++++++++++++ wwwroot/services/list/index.dspy | 6 +- wwwroot/services/remove/index.dspy | 8 +- wwwroot/services/test/index.dspy | 7 +- wwwroot/sessions/create_session.dspy | 9 +- 11 files changed, 318 insertions(+), 91 deletions(-) create mode 100644 test_orgid_refactor.py diff --git a/hermes_web_cli/crud_ops.py b/hermes_web_cli/crud_ops.py index 9bb5970..c0eef4a 100644 --- a/hermes_web_cli/crud_ops.py +++ b/hermes_web_cli/crud_ops.py @@ -13,43 +13,43 @@ SERVICES_CRUD = { "operations": { "create": { "name": "create_service_record", - "description": "Create a new service record for the current user", - "parameters": ["user_id", "name", "service_url", "description", "apikey"], + "description": "Create a new service record for the current organization", + "parameters": ["orgid", "name", "service_url", "description", "apikey"], "sql_template": """ - INSERT INTO services (id, user_id, name, service_url, description, apikey, status, created_at, updated_at) - VALUES (${id}$, ${user_id}$, ${name}$, ${service_url}$, ${description}$, ${apikey}$, ${status}$, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + INSERT INTO services (id, orgid, name, service_url, description, apikey, status, created_at, updated_at) + VALUES (${id}$, ${orgid}$, ${name}$, ${service_url}$, ${description}$, ${apikey}$, ${status}$, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) """, "return_fields": ["id"] }, "read_all": { - "name": "get_all_services_for_user", - "description": "Get all services for the current user", - "parameters": ["user_id"], + "name": "get_all_services_for_org", + "description": "Get all services for the current organization", + "parameters": ["orgid"], "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 FROM services - WHERE user_id = ${user_id}$ + WHERE orgid = ${orgid}$ 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": { - "name": "get_service_by_id_and_user", - "description": "Get a specific service by ID for the current user", - "parameters": ["service_id", "user_id"], + "name": "get_service_by_id_and_org", + "description": "Get a specific service by ID for the current organization", + "parameters": ["service_id", "orgid"], "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 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": { "name": "update_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": """ UPDATE services SET name = ${name}$, @@ -58,17 +58,17 @@ SERVICES_CRUD = { apikey = ${apikey}$, status = ${status}$, updated_at = CURRENT_TIMESTAMP - WHERE id = ${service_id}$ AND user_id = ${user_id}$ + WHERE id = ${service_id}$ AND orgid = ${orgid}$ """, "return_fields": [] }, "delete": { "name": "delete_service_record", - "description": "Delete a service record for the current user", - "parameters": ["service_id", "user_id"], + "description": "Delete a service record for the current organization", + "parameters": ["service_id", "orgid"], "sql_template": """ DELETE FROM services - WHERE id = ${service_id}$ AND user_id = ${user_id}$ + WHERE id = ${service_id}$ AND orgid = ${orgid}$ """, "return_fields": [] } diff --git a/hermes_web_cli/db_tables.py b/hermes_web_cli/db_tables.py index 359ef41..47339b7 100644 --- a/hermes_web_cli/db_tables.py +++ b/hermes_web_cli/db_tables.py @@ -7,7 +7,7 @@ Follows database-table-definition-spec: summary/fields/indexes/codes four-sectio SERVICES_TABLE = { "summary": { "name": "services", - "description": "Stores Hermes service configurations for each user", + "description": "Stores Hermes service configurations for each organization", "module": "hermes-web-cli" }, "fields": [ @@ -19,10 +19,10 @@ SERVICES_TABLE = { "description": "Unique service identifier (UUID)" }, { - "name": "user_id", + "name": "orgid", "type": "varchar(64)", "nullable": False, - "description": "Owner user ID for multi-user isolation" + "description": "Organization ID that owns this service" }, { "name": "name", @@ -73,10 +73,10 @@ SERVICES_TABLE = { ], "indexes": [ { - "name": "idx_services_user_id", - "fields": ["user_id"], + "name": "idx_services_orgid", + "fields": ["orgid"], "unique": False, - "description": "Index for user-based queries" + "description": "Index for org-based queries" }, { "name": "idx_services_status", @@ -85,10 +85,10 @@ SERVICES_TABLE = { "description": "Index for status-based queries" }, { - "name": "idx_services_user_status", - "fields": ["user_id", "status"], + "name": "idx_services_orgid_status", + "fields": ["orgid", "status"], "unique": False, - "description": "Composite index for user and status queries" + "description": "Composite index for org and status queries" } ], "codes": [] diff --git a/hermes_web_cli/init.py b/hermes_web_cli/init.py index 8e58d6f..3b398a7 100644 --- a/hermes_web_cli/init.py +++ b/hermes_web_cli/init.py @@ -10,13 +10,14 @@ implement these endpoints by calling the functions provided in this module. """ import json +import uuid import asyncio import aiohttp from typing import Dict, List, Optional, Tuple from datetime import datetime # 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 from .db_tables import TABLE_DEFINITIONS @@ -54,23 +55,23 @@ def load_hermes_web_cli(): return True # Database operations using sqlor-database-module -async def get_all_services(user_id: str) -> List[Dict]: - """Get all registered Hermes services for the specified user from database. +async def get_all_services(orgid: str) -> List[Dict]: + """Get all registered Hermes services for the specified organization from database. Args: - user_id: The ID of the user whose services to retrieve + orgid: The ID of the organization whose services to retrieve Returns: - List of service dictionaries belonging to the specified user + List of service dictionaries belonging to the specified organization """ try: - # Query services table with user_id filter using sqlor-database-module + # Query services table with orgid filter using sqlor-database-module db = DBPools() env = ServerEnv() dbname = env.get_module_dbname() async with db.sqlorContext(dbname) as sor: 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 result = [] @@ -88,13 +89,13 @@ async def get_all_services(user_id: str) -> List[Dict]: print(f"Error getting services: {str(e)}") return [] -async def create_service(name: str, url: str, user_id: str, description: str = "", apikey: str = "") -> str: - """Create a new Hermes service registration for the specified user. +async def create_service(name: str, url: str, orgid: str, description: str = "", apikey: str = "") -> str: + """Create a new Hermes service registration for the specified organization. Args: name: Service name 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) 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'] await sor.sqlExe(sql_template, { 'id': service_id, - 'user_id': user_id, + 'orgid': orgid, 'name': name, 'service_url': url, '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)}") raise -async def delete_service(service_id: str, user_id: str) -> bool: - """Delete a Hermes service registration (only if owned by specified user). +async def delete_service(service_id: str, orgid: str) -> bool: + """Delete a Hermes service registration (only if owned by specified organization). Args: 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: True if deleted successfully, False otherwise """ try: - # Verify service belongs to current user before deletion - service = await get_service_by_id(service_id, user_id) + # Verify service belongs to current org before deletion + service = await get_service_by_id(service_id, orgid) if not service: return False - if service.get("user_id") != user_id: - print(f"Permission denied: Service {service_id} does not belong to user {user_id}") + if service.get("orgid") != orgid: + print(f"Permission denied: Service {service_id} does not belong to org {orgid}") return False # 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'] await sor.sqlExe(sql_template, { 'service_id': service_id, - 'user_id': user_id + 'orgid': orgid }) # 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: await sor.sqlExe(""" DELETE FROM sessions - WHERE service_id = ${service_id}$ AND user_id = ${user_id}$ + WHERE service_id = ${service_id}$ """, { - 'service_id': service_id, - 'user_id': user_id + 'service_id': service_id }) return True @@ -178,18 +178,18 @@ async def delete_service(service_id: str, user_id: str) -> bool: print(f"Error deleting service: {str(e)}") return False -async def get_service_by_id(service_id: str, user_id: str) -> Optional[Dict]: - """Get service configuration by ID (only if owned by specified user). +async def get_service_by_id(service_id: str, orgid: str) -> Optional[Dict]: + """Get service configuration by ID (only if owned by specified organization). Args: 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: - Service dictionary if found and owned by user, None otherwise + Service dictionary if found and owned by org, None otherwise """ try: - # Query database directly with user_id filter for security + # Query database directly with orgid filter for security db = DBPools() env = ServerEnv() 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'] recs = await sor.sqlExe(sql_template, { 'service_id': service_id, - 'user_id': user_id + 'orgid': orgid }) if len(recs) > 0: @@ -215,11 +215,12 @@ async def get_service_by_id(service_id: str, user_id: str) -> Optional[Dict]: return None # 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. Args: service_id: The ID of the service to test + orgid: The ID of the organization (optional, for org-scoped lookup) Returns: Tuple[bool, str]: (is_connected, status_message) @@ -230,11 +231,21 @@ async def test_service_connection(service_id: str) -> Tuple[bool, str]: env = ServerEnv() dbname = env.get_module_dbname() async with db.sqlorContext(dbname) as sor: - sql_template = SERVICES_CRUD['operations']['read_by_id']['sql_template'] - recs = await sor.sqlExe(sql_template, { - 'service_id': service_id, - 'user_id': '' - }) + if orgid: + sql_template = SERVICES_CRUD['operations']['read_by_id']['sql_template'] + recs = await sor.sqlExe(sql_template, { + '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: return False, "Service not found" 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)}" # 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.""" try: - # Get service configuration (verify it belongs to current user) - service = await get_service_by_id(service_id, user_id) + # Get service configuration (verify it belongs to current org) + service = await get_service_by_id(service_id, orgid) if not service: 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)}") 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). Args: @@ -331,6 +342,7 @@ async def send_message_to_service(service_id: str, session_id: str, message: str session_id: The session ID message: The message to send user_id: The ID of the user sending the message + orgid: The ID of the organization Returns: 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: 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: - 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"] 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}") 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). Args: session_id: The session ID user_id: The ID of the user requesting messages + orgid: The ID of the organization Returns: List of message dictionaries """ try: # 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: print(f"Session {session_id} not found or access denied for user {user_id}") return [] - # Get the associated service - service = await get_service_by_id(session['service_id'], user_id) + # Get the associated service (verify org access) + service = await get_service_by_id(session['service_id'], orgid) if not service: print(f"Service for session {session_id} not found or access denied") return [] diff --git a/init/data.json b/init/data.json index 5cbf25e..0131342 100644 --- a/init/data.json +++ b/init/data.json @@ -2,12 +2,12 @@ "hermes_services": [ { "id": "00000000-0000-0000-0000-000000000001", - "user_id": null, + "orgid": "", "name": "Local Hermes Service", "service_url": "http://localhost:9120", - "api_key": null, + "apikey": "", "description": "Default local Hermes service instance", "status": "active" } ] -} \ No newline at end of file +} diff --git a/json/hermes_services.json b/json/hermes_services.json index 15a18b7..5721118 100644 --- a/json/hermes_services.json +++ b/json/hermes_services.json @@ -3,7 +3,8 @@ "title": "Hermes Services", "params": { "sortby": ["created_at desc"], - "confidential_fields": [], + "logined_userorgid": "orgid", + "confidential_fields": ["apikey"], "browserfields": { "exclouded": ["id", "service_url", "created_at", "updated_at"], "alters": { diff --git a/models/hermes_services.json b/models/hermes_services.json index 77d71b8..f9e0478 100644 --- a/models/hermes_services.json +++ b/models/hermes_services.json @@ -16,6 +16,14 @@ "nullable": "no", "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", "title": "Service Name", @@ -72,6 +80,11 @@ } ], "indexes": [ + { + "name": "idx_hermes_services_orgid", + "idxtype": "index", + "idxfields": ["orgid"] + }, { "name": "idx_hermes_services_name", "idxtype": "index", @@ -81,6 +94,11 @@ "name": "idx_hermes_services_status", "idxtype": "index", "idxfields": ["status"] + }, + { + "name": "idx_hermes_services_orgid_status", + "idxtype": "index", + "idxfields": ["orgid", "status"] } ], "codes": [] diff --git a/test_orgid_refactor.py b/test_orgid_refactor.py new file mode 100644 index 0000000..6de8710 --- /dev/null +++ b/test_orgid_refactor.py @@ -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) diff --git a/wwwroot/services/list/index.dspy b/wwwroot/services/list/index.dspy index 8a6850f..31fe9b8 100644 --- a/wwwroot/services/list/index.dspy +++ b/wwwroot/services/list/index.dspy @@ -2,11 +2,11 @@ # This .dspy file uses functions provided by load_hermes_web_cli() try: - # Get current user ID using ahserver's built-in get_user() function - user_id = await get_user() + # Get current user's org ID using ahserver's built-in get_userorgid() function + orgid = await get_userorgid() # 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 result = [] diff --git a/wwwroot/services/remove/index.dspy b/wwwroot/services/remove/index.dspy index 25b27e6..6c68fa2 100644 --- a/wwwroot/services/remove/index.dspy +++ b/wwwroot/services/remove/index.dspy @@ -6,15 +6,15 @@ try: if not service_id: return {"error": "Service ID is required"} - # Get current user ID using ahserver's built-in get_user() function - user_id = await get_user() + # Get current user's org ID using ahserver's built-in get_userorgid() function + orgid = await get_userorgid() # 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: return {"success": True, "message": "Service removed successfully"} else: return {"error": "Failed to remove service"} except Exception as e: - return {"error": str(e)} \ No newline at end of file + return {"error": str(e)} diff --git a/wwwroot/services/test/index.dspy b/wwwroot/services/test/index.dspy index 00ea9cc..37505b4 100644 --- a/wwwroot/services/test/index.dspy +++ b/wwwroot/services/test/index.dspy @@ -6,10 +6,13 @@ try: if not service_id: 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 - 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} except Exception as e: - return {"error": str(e)} \ No newline at end of file + return {"error": str(e)} diff --git a/wwwroot/sessions/create_session.dspy b/wwwroot/sessions/create_session.dspy index dff90ab..97e0438 100644 --- a/wwwroot/sessions/create_session.dspy +++ b/wwwroot/sessions/create_session.dspy @@ -17,12 +17,13 @@ try: # Redirect back to form with error 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() + orgid = await get_userorgid() # Use the function provided by the hermes-web-cli module - # This will call the remote hermes-service API with user_id support - session_result = await create_session(service_id, user_id, "") + # Service lookup uses orgid, session ownership uses user_id + session_result = await create_session(service_id, user_id, orgid, "") if session_result: # Redirect to the new session's user interaction page @@ -34,4 +35,4 @@ try: except Exception as e: # Log the error and redirect back with error message print(f"Error creating session: {str(e)}") - return {"redirect": f"/hermes-web-cli/new_session.ui?error=Internal+error:+{str(e)}"} \ No newline at end of file + return {"redirect": f"/hermes-web-cli/new_session.ui?error=Internal+error:+{str(e)}"}