novo agents

Secrets

Secrets store opaque customer secret material. Novo encrypts native secrets at rest and never returns the secret value after create or rotate.

V1 supports source: 'novo' only. The source field is reserved so future enterprise resolvers can point at customer-owned systems such as Vault, AWS Secrets Manager, or 1Password without changing workspace or MCP bindings.

Methods

| Method | Route | |---|---| | novo.secrets.create({...}) | POST /v1/secrets | | novo.secrets.list({ limit?, cursor?, name?, metadata? }) | GET /v1/secrets | | novo.secrets.get(id) | GET /v1/secrets/:id | | novo.secrets.update(id, {...}) | PATCH /v1/secrets/:id | | novo.secrets.delete(id) | DELETE /v1/secrets/:id | | novo.secrets.rotate(id, { value }) | POST /v1/secrets/:id/rotate |

Type signature

type Secret = {
  id: string;
  object: 'secret';
  name: string;
  source: 'novo' | 'external';
  contentType: string | null;
  metadata: Metadata;
  configVersion: number;
  createdAt: string;
  updatedAt: string;
};

type SecretsNamespace = {
  create(input: {
    name: string;
    value: string;
    source?: 'novo';
    contentType?: string | null;
    metadata?: Metadata;
  }): Promise<Secret>;
  list(input?: {
    limit?: number;
    cursor?: string;
    name?: string;
    metadata?: Metadata;
  }): Promise<Page<Secret>>;
  get(secretId: string): Promise<Secret>;
  update(secretId: string, input: {
    name?: string;
    contentType?: string | null;
    metadata?: MetadataPatch;
  }): Promise<Secret>;
  delete(secretId: string): Promise<{ id: string; object: 'secret'; deleted: true }>;
  rotate(secretId: string, input: { value: string }): Promise<Secret>;
};

Create and rotate

const stripeKey = await novo.secrets.create({
  name: 'stripe-api-key',
  value: process.env.STRIPE_API_KEY!,
  contentType: 'text/plain',
  metadata: { owner: 'payments' },
});

await novo.secrets.rotate(stripeKey.id, {
  value: process.env.STRIPE_API_KEY_NEXT!,
});

value is write-only. Responses include id, config metadata, and timestamps, but never the secret value.

List and reconcile

list returns a full Page<Secret> (hasMore, nextCursor, limit). name is an exact match, and metadata is exact key/value containment over the flat metadata object. Use those filters to adopt existing resources after 409 name_conflict or to sweep tenant-owned resources by metadata.

How secrets are used

A Secret by itself does not expose anything to an agent. Access is granted through a destination-specific projection:

  • Use a Secret context with workspace_env to inject a secret as a managed workspace environment variable.
  • Use Tool server auth: { type: 'bearer', secretId } to authenticate Novo-to-MCP traffic.
  • Use HTTP connection auth: { type: 'bearer', secretId } to authenticate Novo-to-API traffic.

The same Secret can be used in multiple places only when you attach it to each destination explicitly.

Limits and responsibility

Secret values are opaque UTF-8 strings up to 64 KiB. contentType is advisory and does not affect validation. Secret names do not need to be valid environment variable names.

Novo provides encrypted storage, org isolation, projection validation, runtime redaction, and destination boundaries. You are responsible for least privilege, rotation policy, and limiting each secret's blast radius.

Errors

  • 404 secret_not_found: bad ID.
  • 400 invalid_request_error: empty value, value over 64 KiB, unsupported source, or invalid metadata.
  • 409 name_conflict: another Secret in the organization already uses that name.
  • 409 resource_in_use: delete attempted while a tool server, HTTP connection, or secret context still references the Secret. The error includes details.inUseBy.