C3 AI Documentation Home

Overview of the C3 AI Configuration Framework

The C3 AI Configuration Framework provides a methodology for implementing configuration settings throughout the C3 AI Platform using the C3 AI Type System. The implementation of configurations often impacts user experiences through the UI of C3 AI Studio and C3 AI Applications, including default instances of environments, applications, and microservices.

In some instances, administrators, data scientists, and developers might use the C3 AI Type System to define default configurations and address specific requirements by creating and implementing configuration Types through the C3 AI VS Code extension (VSCE) or in the C3 AI Console of the environment or application.

Characteristics of the C3 AI configuration framework

The C3 AI Configuration Framework includes the following logical characteristics:

  • Configuration instances are hierarchical - The final configuration used by an instance is dependent on where the configuration is defined in relationship to that instance.

    For example, in the case of a node pool configuration for an application instance: if there is a node pool configuration defined at the cluster level, as well as the environment level, the application instance use the one defined at the environment level. See ConfigOverride enum for more information.

  • Defined configuration fields replace generic, default fields - When configurations are merged hierarchically, such as in the case of cluster to environment, fields that are defined replace those that are generic defaults. However, you can use Ann.Config#merge annotation to limit this behavior.

    See the Ann.Config Type for other ways you can override or limit default configurations for specific instances, such as setting the minimum and maximum configuration override levels per configuration Type. See Ann.Config#minOverride and Ann.Config#maxOverride for more information. Note that the Ann.Config Type applies to the field and Type level.

Configuration Type categories

The configurations possible within the context of the C3 AI Configuration Framework include the following categories:

Configuration TypeDescriptionWhen to useExample Type
SingletonOnly one instance of this Type exists and is applicable only in that context.Use to ensure there is only one instance of the configuration for an application.DefaultInstance
Sub TypeAdds fields, methods, or configurations specific to a configuration Type.Use to define multiple variations of a base Type and when variations of a Type require distinct sets of fields or methods.JupyterConfig instead of CloudConfig or KvStoreConfig instead of Singleton
UserSingletonExtends the Singleton Type to be specific to a user in the context of a particular instance.Use to define user-specific settings that remain consistent across environments or clusters.See Set a User Singleton Configuration to learn more.
IdentifiedDefines configurations for specifically identified objects configured by a globally-unique and context-specific id.Use with Types that require unique identification within the system, such as database entities and unique references.App
NamedDefines configurations for objects with a name that identifies the object by a unique label.Use with Types that can be reused across multiple parts of your application or integrated with other platform features.DatastoreConfig
ConfigAclEnabledDefines instance specific access control lists (ACL) to restrict read and write operations.Use to store configuration values and enforce ACL permissions to manage sensitive or privileged configuration settings.MetricAccessGroup
Custom configurationIncludes custom logic to define additional parameters for the configuration.Use to define a consistent way to implement configurations such as credentials, values, relationships, and dependencies the user needs to access the resource.CloudResource

Define configuration Types

Configuration Types are defined by mixing in the Config type:

Type
type SampleConfig mixes Config, Singleton {
 
  /**
   * description of the configuration
   */
  stringConfiguration: string
   
  integerConfiguration: int
 
  booleanConfiguration: boolean
}

When defining configuration Types, consider the following:

  • Ensure that only one instance of the configuration exists (for example, CloudConfig), and mix in Singleton with the configuration Type.

  • If there can be multiple instances of the configuration (for example, one instance of DatastoreConfig for each data store), the configuration Type should mix in Identified or Named.

See Config for details.

Instantiate a Type

Create an instance of a Type to work with properties and methods defined by that Type. The following table describes which methods to use to instantiate different configuration Types:

Instantiation methodWhen to useExample
.inst()Use to create a single, global instance of a Type. Use with Types that mix Singleton and DefaultInstance Types.var config = DefaultInstance.inst();
.forConfigKey()Use when you retrieve configurations identified by a specific key. Use with Types that mix Config.var config = App.forConfigKey("config_key");
.make()Use to create a new instance of a Type every time it is called, and to directly specify field values. Use with any Type.var credentials = Echo.make(message: "<message>");
.forId()Use to retrieve objects when you know the ID of the instance. Use with Types that mix Identified.var config = App.forId(id: “<id>”);
.forName()Use with Types that mix Named and are identifiable by a unique name. Use with Named Types.var config = App.forName(name: "<name>");
.builder().id().buildUse to update an object without needing to create a new instance of it every time. Use with any Type.var config = App.builder().id("<id>").build();

Child configurations

Child configurations link parent and child Types, so you can reference a parent configuration from a child configuration.

A configuration can have several child configurations. For example:

Type
type SampleConfig mixes Config, Singleton {
 
  child1: SubConfig1

  child2: SubConfig2
}

The child configurations need to mix in the generic ConfigChild Type with the parent configuration type as the binding:

Type
type SubConfig1 mixes ConfigChild<SampleConfig> {
 
  boolConfig: boolean
}

In this example, SubConfig1 inherits configurations from SampleConfig, and you can reference configurations from SampleConfig using SubConfig1.

Configurations with Secret Values

A configuration can have both values that are considered secrets, and values that are not considered secrets. Secret values are not only stored in a separate physical location than non-secret values, but also may be encrypted if VaultEncrypter#isEnabled is enabled, or if HashiCorpVault is enabled.

Secret configurations should be used for configurations whose values need to be kept hidden, such as passwords, access tokens, etc.

Config values are marked as a secret by adding the Ann.Config#secret annotation. The below example adds a field password to the SampleConfig configuration we have been using:

Type
type SampleConfig mixes Config {
  @config(secret=true)
  password: string
  
  child1: SubConfig1

  child2: SubConfig2
}

All the fields in a configuration may also be marked as a secret by adding the annotation at the type level:

Type
@config(secret=true)
type MyCredentials mixes Config {
  accessToken: string
  secretToken: string
}

In the above example, both accessToken and secretToken fields will be treated as a secret.

Examples of how to use configuration Types

The following sections use the Singleton sample configuration Type SampleConfig from the examples above as a starting point.

Set the configuration value

The configuration could be set in the C3 AI console directly. See the code snippets below for examples of values that can be set.

JavaScript
// Set the boolean configuration to be true
SampleConfig.setConfigValue('booleanConfiguration', true)

// Set the integer configuration to be a number
SampleConfig.setConfigValue('integerConfiguration', 10)

NOTE: Singleton configs should use .inst() (not .make()) as with all singletons. The examples above take advantage of the fact that SampleConfig is a singleton to call the methods directly on the Type, which implicitly calls inst().

Set the secret configuration value

If the field is designed as a secret, instead of using setConfigValue, setSecretValue should be used.

JavaScript
// Set the string configuration to be the password
SampleConfig.setSecretValue('password', "my_password")

Use ConfigOverride

setConfigValue and setSecretValue has the override argument, which takes in ConfigOverride enum Type to set the override level for the configuration.

ConfigOverride has the following enum values that represent the different override levels in the configuration override hierarchy:

  • USER
  • APP
  • ENV
  • CLUSTER
  • ROOT

This hierarchy indicates configuration is implemented in relationship to the instance. For example, if a configuration is set at the cluster level, setting a ConfigOverride at the environment level means the instance will use the environment configuration instead. Additionally, this would mean any application in that environment would also use the configuration set at the environment level, unless the specific application has a configuration override set within that application. In that case, the specific application would use the ConfigOverride set within the application instead of the one set at the environment level.

Examples of ConfigOverride values

Run commands such as the following in the C3 AI console of the environment or application you desire to enact the override.

JavaScript
// Set the integer configuration on the environment override level
SampleConfig.setConfigValue('integerConfiguration', 10, ConfigOverride.ENV)

// Now across all apps in the current environment, the config value for integerConfiguration will be set to 10

// Set the integer configuration on the application override level
SampleConfig.setConfigValue('integerConfiguration', 100, ConfigOverride.APP)

// Now in the current app, the config value for integerConfiguration has been set to 100.
// In other apps of the same environment, the config values for integerConfiguration are still 10.

// The same can be done for secret values
SampleConfig.setSecretValue('password', 'my_password', ConfigOverride.ENV)

// Now for all apps in the environment, the secre value for password will be set to 'my_password'

Set default configuration values

Define default configuration values for the configuration Types by using default values in the Type definition. See the following example code snippet.

Type
type SampleConfig mixes Config, Singleton {
 
  stringConfiguration: string = "'CONSTANT_STRING'"
   
  integerConfiguration: int = "1000"
 
  booleanConfiguration: boolean = "true"
  
  password: string = "default_password"
}

When the instance is started, the configuration value is set automatically for the configuration fields with the default values.

NOTE: The default configuration value in the dependent packages can also be overwritten by remixing the Type. See the following example code snippet.

Type
remix type SampleConfig {
 
  stringConfiguration: "'OVERWRITTEN_CONSTANT_STRING'"
}

Get the configuration value

To get the configuration values of the instance, run the following code snippet.

JavaScript
SampleConfig.getConfig()

This returns { stringConfiguration: "CONSTANT_STRING", integerConfiguration: 1000, booleanConfiguration: true}.

You can also obtain each individual configuration field value by using the configValue() method. See the following example code snippet.

JavaScript
SampleConfig.configValue('stringConfiguration')

This returns "CONSTANT_STRING".

To get the secret configuration values of the instance, run the following code snippet.

JavaScript
SampleConfig.getSecret()

This returns {password: "default_password"}. You can also obtain individual secret configuration field values by using the secretValue() method.

JavaScript
SampleConfig.secretValue('password')

Understanding the path parameter

The path parameter is a string that specifies the location of a configuration field within a hierarchical structure. For example, the path parameter allows Config#clearConfigValue and Config#clearSecretValue to identify and clear specific fields in a nested configuration object. The path is interpreted in a dot-separated format to navigate through the nested configuration fields.

Example usage with path

Suppose you have the following configuration object:

JSON
{
  "database": {
    "host": "localhost",
    "port": 5432
  },
  "logging": {
    "level": "DEBUG",
    "format": "json"
  }
}
  • To clear the host field in the database configuration, you would call:
JavaScript
// Using StudioClusterConfig.inst() as an exmaple
StudioClusterConfig.inst().clearConfigValue("database.host", ConfigOverride.APP);

After clearing the host field within the database configuration, the updated object looks like this:

JSON
{
  "database": {
    "port": 5432
  },
  "logging": {
    "level": "DEBUG",
    "format": "json"
  }
}
  • To clear the database field in the configuration, you could call:
JavaScript
StudioClusterConfig.inst().clearConfigValue("database", ConfigOverride.APP);

After clearing the database field in the configuration, the entire database field is removed from the configuration:

JSON
{
  "logging": {
    "level": "DEBUG",
    "format": "json"
  }
}
  • To clear the level field in the logging configuration, you would call:
JavaScript
StudioClusterConfig.inst().clearConfigValue("logging.level", ConfigOverride.APP);

After clearing the level configuration, the entire level field and its nested fields are cleared:

JSON
{
  "logging": {
    "format": "json"
  }
}

The same functionality applies when using Config#clearSecretValue to clear fields marked as secrets.

Note: The override parameter allows the method to clear configuration values at different levels of precedence, such as USER, APP, CLUSTER, ENV, or ROOT. This enables flexible configuration management across different scopes.

Access Control List enabled configurations

Configurations in the C3 Agentic AI Platform can leverage an access control list (ACL) to manage permission settings for each configuration object or instance, rather than for the entire configuration subtype. The ACL is stored directly on the configuration instance and is persisted to the filesystem. Use the ConfigAclEnabled Type, parameterized with the configuration Type you are working with, to define ACL-enabled configurations.

To define a ConfigAclEnabled Type, mix in the ConfigAclEnabled Type parameterized with the Type you are creating. You can use the example Type below for reference in this topic:

Type
type TestConfigAclEnabled mixes ConfigAclEnabled<TestConfigAclEnabled> {
  value: int = 0
  @config(secret=true)
  secret: int = 0
}

Enable the ACL

You can create a ConfigAclEnabled object with a default ACL (only the creator is in the ACL) or a preset ACL. In both cases the creator of the object exists in the ACL with read, write, and modify ACL permissions.

  • To define it with a default ACL, where only the creator has read, write, and modify permissions.

For example:

Type
type TestConfigAclEnabled mixes ConfigAclEnabled<TestConfigAclEnabled> {
  value: int = 0
  @config(secret=true)
  secret: int = 0
}
JavaScript
TestConfigAclEnabled.make().setConfig();
  • Or use a preset ACL with predefined permissions for other users:

    JavaScript
    var acl = AclEntry.arry([<list of User objects>]);
    TestConfigAclEnabled.make().setConfigValue("acl", acl);

An ACL-enabled configuration ensures that only users included in the ACL can access or modify configuration values, secret values, and the ACL itself. Each user’s permissions can be customized, allowing them to read, write, or modify ACLs based on their assigned privileges. For example:

  • A user with only read access can retrieve configuration values but cannot modify or update them.

    JavaScript
    testConfigAclEnabled.configValue("value");
  • Users with write access can update configuration values, and those with modify ACL permissions can also alter the ACL.

It is crucial to carefully manage ACL-enabled configurations. Incorrect handling of permissions can lock out users, leading to orphaned configurations. Always ensure at least one user retains full control over the ACL.

The code snippet below is an example of bootstrapping the object with a given ACL in the C3 AI Console:

JavaScript
// Create / get users to add to the ACL
var ownerUser = User.make({
  id: "<user ID>", 
  name: "<user name>"
});
var ownerUserEntry = AclEntry.make({
    member: ownerUser,
    canUpdate: true,
    canModifyAcl: true,
    canRemove: true
});
var uf = UserFields.builder().email("testoktauser123").familyName("Okta").givenName("Test").build();
var sharedUser = IdentityProvider.createUser(uf);
IdentityProvider.setPassword(sharedUser, "Password1");
IdentityProvider.addUserToGroup(sharedUser, "C3.Developer");

// Create an AclEntry for each User
var sharedUserEntry = AclEntry.make({
    member: sharedUser,
    canUpdate: true
});

// Make the ConfigAclEnabledObject with a preset ACL
var testConfigAclEnabled = TestConfigAclEnabled.make({
    acl: [ownerUserEntry, sharedUserEntry]
});

testConfigAclEnabled.setConfig();

Note: If a ConfigAclEnabled object's Config file does not exist, any non-bootstrapping actions (setConfig() and setConfigValue("acl", acl)) fail.

Permissions

Permissions to config values, secret values, and the ACL for read and write operations are mapped out below:

Read Config valuesRead ACLRead Secret valuesWrite Config valuesWrite Secret valuesModify ACL
Not in ACLNoNoNoNoNoNo
Read (given to all members in ACL)YesYesYesNoNoNo
Write (can update)YesYesYesYesYesNo
Write and modify ACLYesYesYesYesYesYes

Trying to perform an action with invalid permissions results in an exception being thrown with the appropriate error message.

For example:

JavaScript
// Assume the user only has 'read' permissions
testConfigAclEnabled.setConfig();
// Produces the below error
// POST http://localhost:8080/api/1/c3/c3/TestConfigAclEnabled?action=secretValue 500 (Server Error)
// TestConfigAclEnabled.secretValue: This user does not have write access.

Note: The config framework saves the acl field from AclEnabled as a regular config value, making it visible to anyone who has read permissions by default.

Reading and writing values

Reading config values can be done with commonly used Config read actions, for example:

JavaScript
testConfigAclEnabled.configValue('value')
// and
testConfigAclEnabled.getConfig().

Reading and writing config and secret values requires the acl field to match the current acl field that currently exists in the config file. Writing to a ConfigAclEnabled object with setConfig() should be done with caution if the caller has write + modifyAcl permissions, as this can result in overwriting the ACL mistakenly. To ensure that the acl field matches the acl in the config file, write values by first getting the config, then writing back with the desired values overwritten:

JavaScript
// Writing values
TestConfigAclEnabled.make().getConfig().setConfigValue("value", 21);
// Reading secret values
TestConfigAclEnabled.make().getConfig().secretValue("secret");

Granting and revoking permissions

To grant and revoke permissions, update the acl field in the ConfigAclEnabled object. Some common ways to set this field follow:

JavaScript
TestConfigAclEnabled.make({
    acl: [ownerUserEntry, sharedUserEntry]
}).setConfig();

Note: It is possible to lock yourself (and another user) out of a ConfigAclEnabled object, resulting in an orphaned object. Be cautious not to clear the acl field. You should keep at least one user in the acl with write and modify ACL permissions to manage the acl and prevent orphaned objects from being produced.

Best practice considerations

Consider the following when using and implementing configs:

  • Updating a singleton config requires you to refresh your browser.

  • The function clearConfigAndSecretAllOverrides on the Config Type does not clear seeded configs and defaults.

  • It is recommended to run App.emptyAllCaches() after any config is updated in multi-node environments (MNE).

    NOTE: If you want to limit the solution to a specific cache, run the following:

    JavaScript
      C3.app().nodes().each(n => n.callJson('Lambda', 'call', Lambda.fromJsFunc(() => { MyConfigType.clearCache() }).toJson()));

See also

Was this page helpful?