Tools & ACL Security

AI In A Box supports function calling (tools) that allow the LLM to retrieve data and perform actions in ServiceNow. When enabled, Tools ACL Security ensures these tool calls respect the requesting user's access controls.

What Are Tools?

Tools are functions that the LLM can call during a conversation. For example:

  • get_incident - Look up incident details by number or caller
  • search_knowledge - Search knowledge base articles
  • create_task - Create a new task record

When a user asks "What incidents has David Miller reported?", the LLM can call the get_incident tool to fetch real data instead of making up an answer.

Why ACL Security Matters

Without ACL security, tools run as the integration user (e.g., aiab_service), which typically has elevated permissions. This means:

  • Users could access data they shouldn't see
  • Field-level ACLs would be bypassed
  • Sensitive information could be exposed

With ACL security enabled, tool calls are impersonated as the requesting user, ensuring all ServiceNow access controls are enforced.

How It Works

  1. 1. User asks a question in chat
  2. 2. LLM decides to call a tool (e.g., get_incident)
  3. 3. AI In A Box server calls back to ServiceNow with the user's identity
  4. 4. ServiceNow impersonates the user before executing the tool script
  5. 5. Tool script uses GlideRecordSecure which respects ACLs
  6. 6. Only data the user can access is returned to the LLM

Setup Requirements

To enable Tools ACL Security, you need three components:

1. Service Account with Admin Role

Create a dedicated service account that will authenticate callbacks and perform impersonation:

  1. Go to User Administration > Users
  2. Create a new user (e.g., aiab_service)
  3. Set a strong password
  4. Grant the admin role
Why admin role? The GlideImpersonate API requires elevated privileges to work in REST API context. The impersonator role alone is not sufficient - testing has confirmed that only the admin role enables impersonation via REST callbacks. The service account will impersonate the requesting user before executing tool scripts, so the actual data access is governed by the requesting user's ACLs, not the service account's.

2. Callback Auth Property

Set the system property that stores the service account credentials:

ai_in_a_box.config.server.callback.auth = aiab_service:your-password

The AI In A Box server uses these credentials for Basic Authentication when calling back to ServiceNow to execute tools. ServiceNow's REST API authentication validates these credentials before any tool execution occurs.

3. Global Script Include

The AIABGlobalHelper script include must be installed in the global scope. You have two options:

Option A: Import the Update Set (Recommended)

Download and import the Global Helper update set from GitHub:

  1. Download AIABGlobalHelper Update Set from the releases page
  2. Go to System Update Sets > Retrieved Update Sets
  3. Click Import Update Set from XML
  4. Upload the XML file and click Preview
  5. Click Commit to install

Option B: Create Manually

Create a global scope script include called AIABGlobalHelper:

var AIABGlobalHelper = Class.create();
AIABGlobalHelper.prototype = {
    initialize: function() {},
    
    getUserName: function(userId) {
        var sys_user = new GlideRecord("sys_user");
        if (sys_user.get(userId)) {
            return sys_user.getValue("user_name");
        }
        return "NO USER FOUND with userId " + userId;
    },

    /**
     * Impersonate a user - callable from scoped apps
     * @param {string} userId - sys_id of user to impersonate
     * @returns {string|null} - original user sys_id if successful, null if failed
     */
    impersonate: function(userId) {
        if (!userId) {
            gs.warn("AIABGlobalHelper.impersonate: No userId provided");
            return null;
        }
        var originalUser = gs.getUserID();
        var originalUserName = this.getUserName(originalUser);
        gs.info("AIABGlobalHelper.impersonate: attempting " + userId + " from " + originalUser + "[" + originalUserName + "]");
        
        try {
            var impersonator = new GlideImpersonate();
            impersonator.impersonate(userId);
            
            var newUser = gs.getUserID();
            var newUserName = this.getUserName(newUser);
            gs.info("AIABGlobalHelper.impersonate: now running as " + newUser + "[" + newUserName + "]");
            
            if (newUser === userId) {
                gs.info("AIABGlobalHelper.impersonate: SUCCESS");
                return originalUser;
            } else {
                gs.warn("AIABGlobalHelper.impersonate: FAILED - still " + newUser);
                return null;
            }
        } catch (e) {
            gs.error("AIABGlobalHelper.impersonate: EXCEPTION - " + e);
            return null;
        }
    },

    /**
     * Revert impersonation to original user
     * @param {string} originalUserId - sys_id of original user
     */
    revert: function(originalUserId) {
        if (originalUserId) {
            try {
                var impersonator = new GlideImpersonate();
                impersonator.impersonate(originalUserId);
                gs.info("AIABGlobalHelper.revert: reverted to " + originalUserId);
            } catch (e) {
                gs.warn("AIABGlobalHelper.revert failed: " + e);
            }
        }
    },
    
    type: "AIABGlobalHelper"
};
Important: This script include MUST be in the global scope because GlideImpersonate is only available in global scope. If you have a duplicate in the scoped AI In A Box app, disable it.

Writing ACL-Safe Tool Scripts

When creating tools, follow these best practices to ensure ACLs are respected:

Use GlideRecordSecure

Always use GlideRecordSecure instead of GlideRecord:

(function(args, userId) {
    // GOOD: Uses GlideRecordSecure which respects ACLs
    var gr = new GlideRecordSecure('incident');
    if (gr.get('number', args.number)) {
        return {
            number: gr.getValue('number'),
            short_description: gr.getValue('short_description'),
            state: gr.getDisplayValue('state')
        };
    }
    return { error: 'Incident not found' };
})(args, userId);

Check Field-Level Access

For sensitive fields, explicitly check read access:

(function(args, userId) {
    var gr = new GlideRecordSecure('incident');
    if (gr.get('number', args.number)) {
        var result = {
            number: gr.getValue('number'),
            short_description: gr.getValue('short_description')
        };
        
        // Only include caller_id if user can read it
        if (gr.caller_id.canRead()) {
            result.caller_id = gr.getDisplayValue('caller_id');
        }
        
        return result;
    }
    return { error: 'Incident not found' };
})(args, userId);

Avoid ACL Bypasses

Never use:

  • gr.setWorkflow(false) to bypass business rules
  • GlideRecord directly (use GlideRecordSecure for ACL enforcement)

Troubleshooting

"Impersonation failed" or "Still running as [service account]"

This is the most common issue. The GlideImpersonate API requires the admin role to work in REST API context:

  • Verify the service account has the admin role (not just impersonator)
  • Check the system logs for "AIABGlobalHelper.impersonate: FAILED" messages
  • Confirm the user you're impersonating exists and is active
Key Finding: The impersonator role is NOT sufficient for GlideImpersonate to work in REST API context. You need the admin role on the service account.

Tool Returns 401 Unauthorized

  • Check ai_in_a_box.config.server.callback.auth contains valid credentials
  • Verify the service account is active and not locked out
  • Confirm the password is correct (format: username:password)

Tool Returns No Data (But Should)

  • The user may not have access to the record (ACL working correctly!)
  • Check the user's roles and group memberships
  • Review the table and field-level ACLs

"AIABGlobalHelper not found"

  • Ensure the script include is in global scope (not the AI In A Box scoped app)
  • If you have duplicates, disable the one in the scoped app
  • Verify the name is exactly AIABGlobalHelper
  • Check that the script include is active

Duplicate AIABGlobalHelper Script Includes

If you have AIABGlobalHelper in both global scope and the AI In A Box scoped app:

  1. Keep the global scope version active
  2. Disable the scoped app version
  3. ServiceNow's scope resolution may pick the wrong one if both are active

Example: get_incident Tool

Here's a complete example of an ACL-safe tool with multiple search options:

Parameters

{
  "type": "object",
  "properties": {
    "number": {
      "type": "string",
      "description": "Incident number like INC0000001"
    },
    "caller": {
      "type": "string",
      "description": "Name of the person who reported the incident"
    },
    "assigned_to": {
      "type": "string",
      "description": "Name of the person assigned to the incident"
    },
    "assignment_group": {
      "type": "string",
      "description": "Name of the group assigned to the incident"
    },
    "state": {
      "type": "string",
      "enum": ["new", "in_progress", "on_hold", "resolved", "closed"],
      "description": "Incident state"
    },
    "priority": {
      "type": "string",
      "enum": ["1", "2", "3", "4", "5"],
      "description": "Priority level (1=Critical, 5=Planning)"
    },
    "opened_today": {
      "type": "boolean",
      "description": "If true, only return incidents opened today"
    },
    "short_description_contains": {
      "type": "string",
      "description": "Search for text in the short description"
    },
    "limit": {
      "type": "integer",
      "description": "Max number of incidents to return (default 5, max 10)"
    }
  },
  "required": []
}

Script

(function(args, userId) {
    // Impersonation is handled by the Execute Tool endpoint via AIABGlobalHelper
    // Use GlideRecordSecure to respect ACLs of the impersonated user
    
    var incidentRecord = new GlideRecordSecure("incident");

    if (args.number) {
        incidentRecord.addQuery("number", args.number);
    }
    if (args.caller) {
        incidentRecord.addQuery("caller_id.name", "LIKE", args.caller);
    }
    if (args.assigned_to) {
        incidentRecord.addQuery("assigned_to.name", "LIKE", args.assigned_to);
    }
    if (args.assignment_group) {
        incidentRecord.addQuery("assignment_group", "LIKE", args.assignment_group);
    }
    if (args.state) {
        var stateMap = { 
            "new": 1,
            "in_progress": 2,
            "on_hold": 3,
            "resolved": 6,
            "closed": 7 
        };
        if (stateMap[args.state.toLowerCase()]) {
            incidentRecord.addQuery("state", stateMap[args.state.toLowerCase()]);
        }
    }
    if (args.priority) {
        incidentRecord.addQuery("priority", args.priority);
    }
    if (args.opened_today === true) {
        incidentRecord.addQuery("opened_at", ">=", gs.beginningOfToday());
    }
    if (args.short_description_contains) {
        incidentRecord.addQuery("short_description", "CONTAINS", args.short_description_contains);
    }

    incidentRecord.orderByDesc("opened_at");
    incidentRecord.setLimit(Math.min(args.limit || 5, 10));
    incidentRecord.query();

    var incidents = [];
    while (incidentRecord.next()) {
        var incident = {
            number: "" + incidentRecord.number,
            short_description: "" + incidentRecord.short_description,
            state: incidentRecord.getDisplayValue("state"),
            priority: incidentRecord.getDisplayValue("priority"),
            assigned_to: incidentRecord.getDisplayValue("assigned_to"),
            opened_at: "" + incidentRecord.opened_at
        };
        // Only include caller_id if user can read it
        if (incidentRecord.caller_id.canRead()) {
            incident.caller_id = incidentRecord.getDisplayValue("caller_id");
        }
        incidents.push(incident);
    }

    if (incidents.length === 0) {
        return { found: false, count: 0, message: "No incidents found" };
    }
    
    // If searching by number and found exactly one, return full details
    if (args.number && incidents.length === 1) {
        incidentRecord.get("number", args.number);
        var singleIncident = {
            number: "" + incidentRecord.number,
            short_description: "" + incidentRecord.short_description,
            description: "" + incidentRecord.description,
            state: incidentRecord.getDisplayValue("state"),
            priority: incidentRecord.getDisplayValue("priority"),
            assigned_to: incidentRecord.getDisplayValue("assigned_to"),
            opened_at: "" + incidentRecord.opened_at
        };
        // Only include caller_id if user can read it
        if (incidentRecord.caller_id.canRead()) {
            singleIncident.caller_id = incidentRecord.getDisplayValue("caller_id");
        }
        return { found: true, incident: singleIncident };
    }
    
    return { found: true, count: incidents.length, incidents: incidents };
})(args, userId);

Security Considerations

Service Account Security

The service account has admin role, but this is mitigated by:

  • The account only performs impersonation, then runs as the requesting user
  • Actual data access is governed by the impersonated user's ACLs
  • All tool executions are logged in the u_ai_inference table
  • Credential storage is encrypted in ServiceNow properties

Best Practices

  • Use a dedicated service account (not a real person's account)
  • Set a strong, unique password
  • Rotate the password periodically
  • Monitor the u_ai_inference table for unusual activity
  • Review tool scripts for ACL compliance before deployment

Audit Trail

All tool executions are logged in the u_ai_inference table, including:

  • The requesting user (u_requested_by)
  • Tools called and their arguments (u_tool_calls)
  • Results returned to the LLM

Support

Need help setting up Tools ACL Security?