Skip to content

Latest commit

 

History

History
705 lines (559 loc) · 29.2 KB

File metadata and controls

705 lines (559 loc) · 29.2 KB

gas-fakes logo A Developer's Guide to the gas-fakes CLI

Introduction

The gas-fakes library is a powerful tool for safely executing Google Apps Script in a local Node.js environment, functioning as a sandboxed runtime. This is particularly beneficial when running Google Apps Script generated by AI, as it mitigates the security risks associated with the broad permissions that scripts often require. gas-fakes enhances security by emulating the Apps Script environment through the translation of GAS service calls into their equivalent, underlying Google API requests, which allows for more granular and file-specific permission controls.

Until now, gas-fakes has primarily been used as a library within Node.js scripts. Recognizing the potential for broader application and improved developer experience, a command-line interface (CLI) tool has been created. This CLI tool simplifies the use of gas-fakes and can also be used as an MCP (Model Context Protocol) server. This functionality is motivated by the idea that it will enhance its application, especially for tasks like conversational automation of Google Workspace when integrated with tools like the Gemini CLI.

Getting Started

See the Getting Started for more information on getting started with authentication.

1. Installation

To install the gas-fakes CLI tool, run the following command in your terminal:

npm install -g @mcpher/gas-fakes

2. Authentication

gas-fakes supports multiple authentication backends, including Google Workspace and Infomaniak KSuite.

Setup with the gas-fakes Command-Line Tool

You can use the gas-fakes command-line interface (CLI) to assist with the setup.

First, create a .env file to store your project configuration. The init command will prompt you to select the backends you wish to use (Google, KSuite, or both).

To initialize Google with Domain Wide Delegation (default):

gas-fakes init

To initialize Google with Application Default Credentials (ADC):

gas-fakes init --auth-type adc

To initialize multiple backends at once (e.g., Google, KSuite, and MS Graph):

gas-fakes init -b google -b ksuite -b msgraph

Next, authorize the tool. This command will guide you through the process of logging into your accounts. By default, it authenticates Google.

gas-fakes auth

To authenticate a specific backend like KSuite or MS Graph:

gas-fakes auth --backend ksuite
# or
gas-fakes auth --backend msgraph

If you need to enable the required Google APIs for your project, you can do so with the following command:

gas-fakes enableAPIs --help

Note on Scopes: Starting with v2.1.0, gas-fakes automatically discovers required scopes by reading your appsscript.json file, simplifying the setup for both ADC and DWD. Note on .env: Your active platforms and configuration are stored in the .env file. gasfakes.json is no longer used as of v2.2.0.

Basic Usage

Displaying Help

To see the available commands and options, run gas-fakes with no arguments:

gas-fakes --help

This will display the following help message:

$ gas-fakes --help
Usage: gas-fakes [options] [command]

Execute a Google Apps Script file or string.

Options:
  --at,--auth-type <string>                 The authentication type to use. ("adc" or "dwd", default: "dwd")
  -v, --version                             Display the current version of gas-fakes
  -f, --filename <string>                   Path to the Google Apps Script file.
  -s, --script <string>                     A string containing the Google Apps Script.
  -e, --env <path>                          Path to a custom .env file. (default: "./.env")
  -x, --sandbox                             Run the script in a basic sandbox.

> **Note on .env files:** `gas-fakes` also supports the native Node.js `--env-file` flag (available in Node.js 20.6.0+). If you run `node --env-file=.env gas-fakes ...`, the CLI will respect those variables and skip loading its default `./.env` file. Explicitly providing `-e` will still take precedence.
  -w, --whitelistRead <string>              Comma-separated file IDs for read-only access (enables sandbox).
  --ww, --whitelistReadWrite <string>       Comma-separated file IDs for read/write access (enables sandbox).
  --wt, --whitelistReadWriteTrash <string>  Comma-separated file IDs for read/write/trash access (enables sandbox).
  -j, --json <string>                       JSON string for advanced sandbox configuration (overrides whitelist flags).
  -d, --display                             Display the generated script before execution. (default: false)
  -a, --args <string>                       Arguments for the function of Google Apps Script. Provide it as a JSON string. The name of the argument is "args" as
                                            a fixed name. For example, when the function of GAS is `function sample(args) { script }`, you can provide the
                                            arguments like `-a '{"key": "value"}'`. (default: null)
  -l, --libraries <string...>               Libraries. You can run the Google Apps Script with libraries. When you use 2 libraries "Lib1" and "Lib" which are
                                            the identifiers of library, provide '--libraries "Lib1@{filename}" --libraries "Lib2@{file URL}"'. (default: null)
  -h, --help                                display help for command

Commands:
  init [options]                            Initializes the configuration by creating or updating the .env file.
  auth                                      Runs the Google Cloud authentication and authorization flow.
  enableAPIs [options]                      Enables or disables required Google Cloud APIs for the project.
  mcp [options]                             Launch gas-fakes as an MCP server.

Running a Script from a File

You can execute a script from a local file using the -f or --filename option.

Example:

  1. Create a file named sample1.js with the following content:

    const rootFolder = DriveApp.getRootFolder();
    const rootFolderName = rootFolder.getName();
    console.log(rootFolderName);
  2. Run the following command:

    gas-fakes -f sample1.js

Running a Script as a String

For shorter scripts, you can pass the code directly as a string using the -s or --script option.

Example:

gas-fakes -s "const rootFolder = DriveApp.getRootFolder(); const rootFolderName = rootFolder.getName(); console.log(rootFolderName)"

This will produce the same output as the file-based example.

Multi-Backend Support (KSuite)

Starting with version 2.1.0, gas-fakes supports Infomaniak KSuite (kDrive) alongside Google Workspace. This allows you to run standard Google Apps Script code against non-Google platforms by simply switching the "platform" in your script.

Switching Platforms

You can steer gas-fakes to use a specific backend by setting the ScriptApp.__platform property.

  • ScriptApp.__platform = 'google' (Default): Targets Google APIs.
  • ScriptApp.__platform = 'ksuite': Targets Infomaniak KSuite APIs.

Example:

// Check auth status
if (ScriptApp.__isPlatformAuthed('google')) {
  console.log('Google is authorized');
}
console.log('Authorized platforms:', ScriptApp.__platforms);

// Switch to KSuite
ScriptApp.__platform = 'ksuite';

// This standard GAS code now runs against Infomaniak kDrive!
const root = DriveApp.getRootFolder();
console.log("KSuite Root Folder:", root.getName()); 

const folder = root.createFolder("KSuite-Test");
folder.createFile("hello.txt", "This was created on kDrive via standard GAS code.");

Setup for KSuite

  1. Initialize the KSuite backend:
    gas-fakes init -b ksuite
  2. Add your Infomaniak API token to your .env file:
    KSUITE_TOKEN=your_infomaniak_api_token
  3. Ensure your GF_PLATFORM_AUTH includes ksuite.

For more details, see the KSuite POC documentation.

Sandbox Security

A key feature of gas-fakes is its ability to run scripts in a sandbox, which is a restricted environment that prevents the script from accessing unauthorized resources. This is especially important for running code from untrusted sources.

Enabling the Sandbox

To enable the sandbox, use the -x or --sandbox option.

Example:

gas-fakes -x -f sample1.js

When you run this command, you will see a more verbose output that details the sandbox setup process:

$ gas-fakes -x -f sample1.js
[Worker] ...importing Drive API
[Worker] ...importing Sheets API
[Worker] ...importing Slides API
[Worker] ...using env file in /workspace/.env
[Worker] ...cache will be in /tmp/gas-fakes/cache
[Worker] ...properties will be in /tmp/gas-fakes/properties
[Worker] ...initializing auth and discovering project ID
[Worker] ...discovered and set projectId to for-testing
[Worker] Creating new Drive API client
MyDrive
...trashed 0 sandboxed files
...terminating worker thread

Whitelisting Files in the Sandbox

By default, a sandboxed script cannot access any of your files. To grant access to specific files, you can use the -w or --whitelist option, providing a comma-separated list of file IDs.

Example:

  1. Create a file named sample2.js with the following content. Replace ### with a file ID from your Google Drive.

    const fileId = "###";
    const file = DriveApp.getFileById(fileId);
    const name = file.getName();
    console.log(name);
  2. If you run this in a sandbox without whitelisting, you will get an error:

    gas-fakes -x -f sample2.js

    Output:

    Error: Access to file ### is denied by sandbox rules
    
  3. To grant access, add the file ID to the whitelist:

    gas-fakes -x -f sample2.js -w "###"

    This time, the script will execute successfully and print the name of the file.

Advanced Sandbox Control

For more fine-grained control over the sandbox, you can use the -j or --json option to provide a JSON object that specifies permissions.

Example:

Using the same sample2.js file, you can define specific permissions for items and services:

gas-fakes -x -f sample2.js -j '{"whitelistItems":[{"itemId":"###"}],"whitelistServices":[{"className":"DriveApp","methodNames":["getFileById"]}]}'

In this example, the sandbox only allows access to the specified file ID (itemId) and only permits the use of the getFileById method from the DriveApp service.

JSON Schema for Sandbox Configuration

The JSON object used with the -j option follows this schema:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Whitelist and Blacklist Configuration",
  "description": "A configuration for whitelisting and blacklisting items and services.",
  "type": "object",
  "properties": {
    "whitelistItems": {
      "description": "A list of items to be whitelisted.",
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "itemId": {
            "description": "The file ID and folder ID of the file on Google Drive.",
            "type": "string"
          },
          "read": {
            "description": "Read permission for the item. Default is true.",
            "type": "boolean"
          },
          "write": {
            "description": "Write permission for the item. Default is false.",
            "type": "boolean"
          },
          "trash": {
            "description": "Trash permission for the item. Default is false.",
            "type": "boolean"
          }
        },
        "required": ["itemId"]
      }
    },
    "gmailSandbox": {
      "description": "Configuration for Gmail sandbox settings.",
      "type": "object",
      "properties": {
        "emailWhitelist": {
          "description": "List of email addresses allowed to receive emails. Emails sent to addresses not in this list (or allowed by session rules) will throw an error.",
          "type": "array",
          "items": {
            "description": "List of email addresses allowed to receive emails. Emails sent to addresses not in this list (or allowed by session rules) will throw an error.",
            "type": "string"
          }
        },
        "usageLimit": {
          "description": "Limits for operations. Can be a number (implies total limit for all operations combined) or an object { read?: number, write?: number, trash?: number, send?: number }.",
          "type": ["number", "object"]
        },
        "labelWhitelist": {
          "description": "Configuration for allowed labels, specifying name and permissions (read, write, delete, send).",
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": {
                "description": "Label name",
                "type": "string"
              },
              "read": {
                "description": "read",
                "type": "boolean"
              },
              "write": {
                "description": "write",
                "type": "boolean"
              },
              "delete": {
                "description": "delete",
                "type": "boolean"
              },
              "send": {
                "description": "send",
                "type": "boolean"
              }
            }
          }
        },
        "cleanup": {
          "description": "Controls whether Gmail artifacts (labels, threads) created in the session are trashed on cleanup. Defaults to global setting if not set.",
          "type": "boolean"
        }
      }
    },
    "calendarSandbox": {
      "description": "Configuration for Calendar sandbox settings.",
      "type": "object",
      "properties": {
        "calendarWhitelist": {
          "description": "Configuration for allowed calendars, specifying name and permissions (read, write, delete).",
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": {
                "description": "Calendar name",
                "type": "string"
              },
              "read": {
                "description": "read",
                "type": "boolean"
              },
              "write": {
                "description": "write",
                "type": "boolean"
              },
              "delete": {
                "description": "delete",
                "type": "boolean"
              }
            },
            "required": ["name"]
          }
        },
        "usageLimit": {
          "description": "Limits for operations. Can be a number (implies total limit for all operations combined) or an object { read?: number, write?: number, trash?: number }.",
          "type": ["number", "object"]
        },
        "cleanup": {
          "description": "Controls whether calendars created in the session are deleted on cleanup. Defaults to global setting if not set.",
          "type": "boolean"
        }
      }
    },
    "whitelistServices": {
      "description": "A list of services to be whitelisted.",
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "className": {
            "description": "The name of the class of the service.",
            "type": "string"
          },
          "methodNames": {
            "description": "A list of method names for the class to be whitelisted.",
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": ["className"]
      }
    },
    "blacklistServices": {
      "description": "A list of services to be blacklisted.",
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  },
  "required": ["whitelistItems"]
}

Provide arguments to GAS function

You might have a situation that you want to run the GAS function by providing arguments. In that case, you can use as follows.

gas-fakes -s "function sample(obj) {console.log(obj);} sample(args);" -a '{"key": "value"}'

By this, the following result is returned.

$ gas-fakes -s "function sample(obj) {console.log(obj);} sample(args);" -a '{"key": "value"}'
...using env file in .env
[Worker] ...importing Drive API
[Worker] ...importing Sheets API
[Worker] ...importing Slides API
[Worker] ...cache will be in /tmp/gas-fakes/cache
[Worker] ...properties will be in /tmp/gas-fakes/properties
[Worker] ...initializing auth and discovering project ID
[Worker] ...discovered and set projectId to for-testing
[Worker] Creating new Drive API client
{"key": "value"}
...terminating worker thread

Get response value

This simple example demonstrates how to use a local value in a Google Apps Script and receive a value in return.

gas-fakes -a '{"key":"sample"}' -s 'const res = "The provided argument is " + args.key; return JSON.stringify({output: res});' | grep '^{"output":' | jq -r '.output'

When this command is run, the following result is returned. You can see that the provided argument is used in the Google Apps Script to return the value.

The provided argument is sample

Using GAS libraries

gas-fakes CLI can run Google Apps Script using libraries as follows.

1. Create a library file

Create a sample library file named sampleLib.js by copying and pasting the following script.

function function1() {
  return "function1";
}

var value1 = "value1";

2. Create a main script

Create a sample script named sample.js that utilizes the library. In this example, the library identifier is defined as LIB.

const res1 = LIB.function1();
const res2 = LIB.value1;
console.log({ res1, res2 });

3. Run the script

Execute the following command to link the library file to the identifier LIB and run the script:

gas-fakes -f sample.js -l LIB@sampleLib.js

LIB@sampleLib.js: LIB and sampleLib.js are the identifier and the file of library, respectively. At gas-fakes CLI, the file, the hyperlink, and the library key (File ID) of Google Apps Script library can be used.

Output:

$ gas-fakes -f sample.js -l LIB@sampleLib.js
...using env file in /workspace/.env
[Worker] ...importing Drive API
[Worker] ...importing Sheets API
[Worker] ...importing Slides API
[Worker] ...cache will be in /tmp/gas-fakes/cache
[Worker] ...properties will be in /tmp/gas-fakes/properties
[Worker] ...initializing auth and discovering project ID
[Worker] ...discovered and set projectId to for-testing
...gas-fakes version 1.2.##
{ res1: 'function1', res2: 'value1' }
...terminating worker thread

The output confirms that the function and variable from the library were correctly retrieved.

Using non default locations

Let's look at this example, where I'm setting a value in a property store unique to the folder Im running from, and using a local .env file, if there is one. Cache stores work in exactly the same way as the property store examples below.

$ gas-fakes -s "PropertiesService.getScriptProperties().setProperty('hello','world')"

I'll see a message like this - in this case i have no .env file in this folder

...using env file in /Users/brucemcpherson/Documents/repos/gas-fakes/t/.env
...etc..
.....PROPERTY store service is using store type FILE as backend

Now i can retrieve that value with

$ gas-fakes -s "console.log(PropertiesService.getScriptProperties().getProperty('hello'))"
...
...PROPERTY store service is using store type FILE as backend
world

Using a different env

Let's say that instead of using the default store type, I want to use redis (see - sharing cache and properties between gas-fakes and live apps script), whose details are already defined in some other .env file, I can do this:

$ gas-fakes -s "PropertiesService.getScriptProperties().setProperty('hello','world of redis')" -e ../.env
...PROPERTY store service is using store type UPSTASH as backend

$ gas-fakes -s "console.log(PropertiesService.getScriptProperties().getProperty('hello'))" -e ../.env
...PROPERTY store service is using store type UPSTASH as backend
world of redis

ScriptId and Store Partitioning

A stable scriptId is required to partition property and cache stores. During gas-fakes init, the utility will attempt to discover your scriptId from .clasp.json. If no ID is found in your configuration or in .clasp.json, a random UUID is generated and saved to your .env file as GF_SCRIPT_ID. This ensures that your local stores remain consistent across different sessions.

If you want to share stores between different folders, you need to ensure that the GF_SCRIPT_ID in your .env has the same value. This is especially important if you plan to share property and cache stores with live Apps Script (yes you can!). In this case, you set the GF_SCRIPT_ID in your .env to match the scriptId of the live apps script you want to share these stores with.

$ gas-fakes -s "PropertiesService.getScriptProperties().setProperty('hello','to live apps script')" -e ../.env
....PROPERTY store service is using store type UPSTASH as backend

$ gas-fakes -s "console.log(PropertiesService.getScriptProperties().getProperty('hello'))" -e ../.env
...PROPERTY store service is using store type UPSTASH as backend
to live apps script

You can find more info about how to read or update these values directly in live apps script using the Dropin replacement for the property and cache stores in sharing cache and properties between gas-fakes and live apps script

MCP

This CLI tool can also be used as the MCP server. Please add the MCP server to settings.json as follows.

"mcpServers": {
  "gas-fakes": {
    "command": "gas-fakes",
    "args": ["mcp"]
  }
}

When this is used for the Gemini CLI, the following result is obtained.

> /mcp

Configured MCP servers:

🟢 gas-fakes - Ready (2 tools)
  Tools:
  - create-new-tools
  - run-gas-by-gas-fakes

When you want to load your custom tools to gas-fakes MCP, please use the following setting.

"mcpServers": {
  "gas-fakes": {
    "command": "gas-fakes",
    "args": [
      "mcp",
      "--tools",
      "tools.js" // <--- A script file including custom tools.
    ]
  }
}

The simple sample script for tools.js is as follows. This tool searches files on Google Drive. The script is Google Apps Script. When this tool is used as the above, a tool searchGoogleDriveFiles will be added to the MCP server.

import { z } from "zod";

const tools = [
  {
    name: "searchGoogleDriveFiles",
    schema: {
      description: "Use this to search files by a filename on Google Drive.",
      inputSchema: {
        filename: z.string().describe("Filename of the search file."),
      },
    },
    func: (object = {}) => {
      const { filename } = object;
      const files = DriveApp.getFilesByName(filename);
      const ar = [];
      while (files.hasNext()) {
        const file = files.next();
        ar.push({ filename: file.getName(), fileId: file.getId() });
      }
      return ar;
    },
  },
];

gas-fakes logo Further Reading

Watch the video

Watch the video

Read more docs