Managed secrets
Managed secrets keep outbound credentials out of C# source, function arguments,
ordinary records, results, and run history. An owner or administrator writes a
string value under a stable workspace key. Trusted C# business logic resolves
that key through ISecretService only while it runs.
What write-only means
Section titled “What write-only means”The value is accepted only when a secret is created or rotated. List, detail, audit, error, export, and UI responses contain metadata only. There is no reveal, copy-existing-value, rename, or restore-after-retirement operation.
Keys are immutable, workspace-unique, and case-insensitive. Moltaro does not trim or otherwise transform a key or value. A key with leading or trailing whitespace or a null character is rejected, and a value is stored exactly as submitted.
Manage secrets in the Web Application
Section titled “Manage secrets in the Web Application”Workspace owners and administrators can complete the full lifecycle without handling raw API requests:
- Open Administration > Secrets.
- Choose Create secret, enter the immutable lookup key, and enter the initial value.
- Select a row to open its metadata and key-local Audit Trail history.
- Use Rotate value to replace the write-only value, or disable and re-enable the key without changing its value version.
- Use Retire only when the key must never be restored or reused. Retirement removes the ciphertext from the live secret row.
Retirement is terminal inside Moltaro, but it does not rewrite existing database backups, PostgreSQL WAL archives, or infrastructure snapshots. Those may retain older encrypted bytes until the operator’s normal retention and disposal policy expires them. Protect and retire the persistent Data Protection key ring under the same operational policy.
The create and rotate dialogs accept a value but never preload, reveal, or copy the current value. Closing and reopening a dialog always starts with an empty value field. The list, mobile cards, detail drawer, and history show metadata only. A Configurator or ordinary user cannot see the navigation item and is blocked from the direct route.
Use the local history to confirm who changed a key and when. Resolution events from Jobs, Commands, Actions, triggers, validations, before-save mutations, and HTTP endpoints appear in the same history after their invocation audit is flushed.
Automate the owner and administrator lifecycle
Section titled “Automate the owner and administrator lifecycle”Only workspace owners and administrators can use the managed-secret
Configuration API. A Configurator role is not sufficient. All paths below are
relative to the configured installation’s workspace API base URL, never
moltaro.com.
| Operation | Request | Result |
|---|---|---|
| List metadata | GET /api/workspace/admin/secrets | Keys, lifecycle state, version, timestamps, actors, and RowVersion |
| Read metadata | GET /api/workspace/admin/secrets/{id} | One metadata record; never its value |
| Create | POST /api/workspace/admin/secrets with Key and Value | Enabled secret at value version 1 |
| Rotate | POST /api/workspace/admin/secrets/{id}/rotate with Value and RowVersion | Replaces the value and increments its value version |
| Disable | POST /api/workspace/admin/secrets/{id}/disable with RowVersion | Prevents resolution without changing the value version |
| Re-enable | POST /api/workspace/admin/secrets/{id}/enable with RowVersion | Makes the current value resolvable again |
| Retire | POST /api/workspace/admin/secrets/{id}/retire with RowVersion | Terminal state; increments the version and removes ciphertext from the live row |
Keys are limited to 128 characters. Create and rotate require a non-empty value whose UTF-8 representation is no larger than 64 KiB; values over that limit are rejected before protection or storage.
Use the latest RowVersion returned by a read or mutation. HTTP 409 means
another actor changed the record: read it again, decide whether the requested
transition is still appropriate, and never resend a stale token blindly.
Separate keys rotate independently. Moltaro does not provide an atomic multi-key rotation. Design integrations so one credential can change at a time, or let the external service accept an overlap during rotation.
Audit and history
Section titled “Audit and history”Create, rotate, disable, enable, retire, and runtime resolution events are
written to the global Audit Trail with object type ManagedSecret. The same
append-only events form a key’s local history; there is no separate mutable
history store.
GET /api/workspace/admin/audit/events?objectType=ManagedSecret&objectId={secretId}&page=1&pageSize=50Audit data contains the key, version, invocation identity, and stable outcome where applicable. It never contains the value, ciphertext, a value-derived hint, or a derived bearer token.
Resolve a secret from trusted C#
Section titled “Resolve a secret from trusted C#”Inject Moltaro.Package.NET.Functions.ISecretService into a function
constructor. GetRequired is appropriate when the integration cannot run
without the setting. TryGet is for genuinely optional configuration.
The following schedule-compatible Job obtains short-lived authorization from an example external service. The URLs are deliberately non-production. The example sends credentials only to their intended token endpoint, keeps the derived token in memory for one request, and returns only non-secret status.
using System.Net.Http.Headers;using System.Net.Http.Json;using System.Text.Json.Nodes;using Moltaro.Package.NET;using Moltaro.Package.NET.Functions;
namespace Moltaro.Operational.Functions;
[MoltaroFunction( "docs.examples.managedSecretsPartnerImport", Key = "docs.managedSecrets.partnerImport", Name = "Partner import", Description = "Authenticates with managed credentials and runs a partner import.")]public sealed class ManagedSecretsPartnerImportJob(ISecretService secrets) : GlobalFunction{ public override async Task<FunctionResult> OnRunAsync( CancellationToken cancellationToken) { var clientId = secrets.GetRequired("integrations.partner.client-id"); var clientSecret = secrets.GetRequired("integrations.partner.client-secret");
var tokenFields = new Dictionary<string, string> { ["grant_type"] = "client_credentials", ["client_id"] = clientId, ["client_secret"] = clientSecret };
if (secrets.TryGet("integrations.partner.scope", out var scope)) { tokenFields["scope"] = scope; }
using var client = new HttpClient(); using var tokenRequest = new HttpRequestMessage( HttpMethod.Post, "https://api.example.invalid/oauth/token") { Content = new FormUrlEncodedContent(tokenFields) }; using var tokenResponse = await client.SendAsync(tokenRequest, cancellationToken);
if (!tokenResponse.IsSuccessStatusCode) { return FunctionResult.Failed("Partner authentication failed."); }
var tokenPayload = await tokenResponse.Content .ReadFromJsonAsync<JsonObject>(cancellationToken); var accessToken = tokenPayload?["access_token"]?.GetValue<string>(); if (string.IsNullOrWhiteSpace(accessToken)) { return FunctionResult.Failed("Partner authentication returned no token."); }
using var importRequest = new HttpRequestMessage( HttpMethod.Post, "https://api.example.invalid/import"); importRequest.Headers.Authorization = new AuthenticationHeaderValue( "Bearer", accessToken); using var importResponse = await client.SendAsync(importRequest, cancellationToken);
return importResponse.IsSuccessStatusCode ? FunctionResult.Ok("Partner import completed.") : FunctionResult.Failed("Partner import failed."); }}Do not include the credentials or accessToken in DevLog data, exception text,
function results, correlation ids, request diagnostics, audit data, or ordinary
records. Avoid returning an external response body when it might echo request
credentials or tokens. Prefer short-lived access tokens with the narrowest
external scope and discard them when the outbound request completes.
Availability and stable codes
Section titled “Availability and stable codes”GetRequired throws SecretUnavailableException for a valid but unavailable
key. Branch on its Code, never its message:
| State | SecretUnavailableCodes value | Stable code |
|---|---|---|
| No key exists | Missing | moltaro.secrets.missing |
| Key is disabled | Disabled | moltaro.secrets.disabled |
| Key is retired | Retired | moltaro.secrets.retired |
TryGet returns false and sets its output to null for all three states.
Both methods throw ArgumentException for null, empty, whitespace-padded, or
null-character-containing keys because those are programming errors, not
lifecycle states.
Missing usually means provisioning or spelling is wrong. Disabled and retired are deliberate administrative states. Do not loop or silently fall back to a credential from source. Let the invocation fail safely, have an owner or administrator resolve the configuration, and retry in a new invocation.
Invocation snapshots and rotation
Section titled “Invocation snapshots and rotation”Moltaro loads one immutable managed-secret snapshot before constructing a
function. Every lookup in that invocation uses the same snapshot and performs
only synchronous in-memory work. Do not use .Result, .Wait(), or invent an
asynchronous secret API.
An invocation already in progress may continue with the old value after a rotation or lifecycle change. A later invocation gets a fresh snapshot. This keeps one run internally consistent and makes the retry boundary explicit.
Guidance for coding agents
Section titled “Guidance for coding agents”Managed-secret adoption is a handoff between people, an authoring agent, and the runtime. Keep those responsibilities separate:
| Responsibility | Owner |
|---|---|
| Choose the stable key names and enter or rotate values | Workspace owner or administrator |
| Reference the supplied key names and author trusted C# | Developer or explicitly authorized coding agent |
| Load one immutable snapshot and resolve values in memory | Moltaro runtime |
| Approve rollout, external credential rotation, and scheduling | The integration’s operator |
An agent can complete the C# integration without ever receiving a secret value:
- Start at public
/llms.txt, then read the installation-generatedAGENTS.mdorCLAUDE.mdsupplied by the workspace owner. - Fetch the installation’s Configuration OpenAPI document and Net Operation
Project
developer-surface. Confirm thatMoltaro.Package.NET.Functions.ISecretServiceis in the supported service catalog. - Ask the owner for the exact stable key names only. If a required key has not been provisioned, report that fact; do not ask for its value.
- Inject
ISecretServiceinto the function constructor. PreferGetRequiredfor required integration configuration and useTryGetonly when absence is an intended optional state. - Create a source revision through the Configuration API, run Check, then run Build. Use the resulting published artifact for the intended Job, Command, Action, TriggerHandler, Validation, BeforeSaveMutation, or HttpEndpoint flow.
- Prove success with a non-secret result such as
Matched, a boolean, or an ordinary marker field. Never echo a credential or derived token for testing. - Ask the owner or administrator to inspect the key-local Audit Trail and the relevant function run or job before rollout.
- Hand provisioning, rotation, external-system changes, and schedule enablement back to the owner or integration operator. They are not implicit coding-agent actions.
An agent may enumerate, create, rotate, disable, enable, or retire secrets only when the assigned task explicitly authorizes that owner/admin operation. Never ask the user to paste a secret into a prompt, source file, function argument, or test assertion. Values must travel through an appropriate secret channel directly to Administration > Secrets, or to the write-only API when the task explicitly authorizes owner/admin automation. Keep keys stable across source revisions and make function output prove only a non-secret business outcome.
Continue with the AI agent development quickstart, the .NET SDK guide, and the installation’s Configuration API reference.