novo agents

Secret contexts

Secret contexts group projections that expose one or more Secrets to a destination.

V1 projections:

  • workspace_env injects a Secret into managed workspace commands and declared services as an environment variable.
  • egress_header attaches a Secret as one named header only on outbound requests to one HTTPS domain; the value never enters the guest environment.
  • mcp_bearer reserves the projection type for MCP bearer use. Tool servers attach bearer auth directly through auth.secretId; MCP bearer values are not exposed to the workspace unless separately projected as workspace_env.

Methods

| Method | Route | |---|---| | novo.secretContexts.create({...}) | POST /v1/secret-contexts | | novo.secretContexts.list({ limit?, cursor?, name?, metadata? }) | GET /v1/secret-contexts | | novo.secretContexts.get(id) | GET /v1/secret-contexts/:id | | novo.secretContexts.update(id, {...}) | PATCH /v1/secret-contexts/:id | | novo.secretContexts.delete(id) | DELETE /v1/secret-contexts/:id | | novo.secretContexts.attachBinding(id, binding) | POST /v1/secret-contexts/:id/bindings/attach | | novo.secretContexts.detachBinding(id, binding) | POST /v1/secret-contexts/:id/bindings/detach | | novo.secretContexts.test(id) | POST /v1/secret-contexts/:id/test |

Type signature

type SecretProjection =
  | { type: 'workspace_env'; name: string }
  | { type: 'egress_header'; domain: string; header: string }
  | { type: 'mcp_bearer' };

type SecretContextBinding = {
  secretId: string;
  projection: SecretProjection;
};

type SecretContext = {
  id: string;
  object: 'secret_context';
  name: string;
  bindings: SecretContextBinding[];
  metadata: Metadata;
  configVersion: number;
  createdAt: string;
  updatedAt: string;
};

type SecretContextsNamespace = {
  create(input: {
    name: string;
    bindings: SecretContextBinding[];
    metadata?: Metadata;
  }): Promise<SecretContext>;
  list(input?: {
    limit?: number;
    cursor?: string;
    name?: string;
    metadata?: Metadata;
  }): Promise<Page<SecretContext>>;
  get(secretContextId: string): Promise<SecretContext>;
  update(secretContextId: string, input: {
    name?: string;
    bindings?: SecretContextBinding[];
    metadata?: MetadataPatch;
  }): Promise<SecretContext>;
  delete(secretContextId: string): Promise<{ id: string; object: 'secret_context'; deleted: true }>;
  attachBinding(secretContextId: string, binding: SecretContextBinding): Promise<SecretContext>;
  detachBinding(secretContextId: string, binding: SecretContextBinding): Promise<SecretContext>;
  test(secretContextId: string): Promise<{
    ok: true;
    bindingCount: number;
    workspaceEnv: { count: number; names: string[]; totalBytes: number; maxCount: number; maxBytes: number };
    mcpBearerCount: number;
    egressHeaderCount: number;
  }>;
};

Workspace env projection

const stripeKey = await novo.secrets.create({
  name: 'stripe-api-key',
  value: process.env.STRIPE_API_KEY!,
});

const paymentsEnv = await novo.secretContexts.create({
  name: 'payments-env',
  bindings: [
    {
      secretId: stripeKey.id,
      projection: { type: 'workspace_env', name: 'STRIPE_API_KEY' },
    },
  ],
});

await novo.agents.create({
  context: 'You maintain the payments service.',
  band: 'core',
  workspace: {
    type: 'managed',
    secretContexts: [paymentsEnv.id],
    services: [{ name: 'web', command: 'pnpm dev', port: 3000 }],
  },
});

Novo injects workspace_env projections into managed workspace shell commands and declared services. It does not create .env.local, credential files, or shell profile files by default.

Egress header projection

Use egress_header when a managed workspace must call one external HTTPS API but must never read the credential itself:

await novo.secretContexts.create({
  name: 'payments-egress',
  bindings: [
    {
      secretId: stripeKey.id,
      projection: {
        type: 'egress_header',
        domain: 'api.stripe.com',
        header: 'Authorization',
      },
    },
  ],
});

Novo's managed network policy injects the header only for that exact domain. The plaintext is absent from command and service environments. The destination is HTTPS-only, and redirects do not broaden it.

Boundaries

Secret contexts apply only to managed workspaces. Remote workspaces are customer-owned and receive their secrets through your remote adapter contract.

In the console, register secret contexts (and the reusable Secrets they bind) from the Adapters screen, then attach them from the agent editor under workspace.secretContexts; the SDK/API field is the same workspace.secretContexts array shown above.

Workspace environment projection names must match [A-Za-z_][A-Za-z0-9_]*. Secret names and values are generic and are not validated as environment variables.

When multiple attached contexts project to the same environment variable name, the run is rejected. Managed workspace secret env is capped at 100 variables and 256 KiB total value bytes.

Contexts may have zero bindings. Reconcilers can use attachBinding and detachBinding for idempotent single-binding edits instead of racing full-array update calls. test(id) validates the current bindings and reports workspace-env names and byte totals without returning secret values.

list returns a full Page<SecretContext> and accepts exact name plus flat metadata containment filters for adoption and orphan sweeps.

Errors

  • 404 secret_context_not_found: bad ID.
  • 400 invalid_request_error: invalid projection shape, invalid env var name, missing or wrong-org Secret ID, duplicate env projection, or too many bindings.
  • 409 name_conflict: another Secret context in the organization already uses that name.
  • 409 resource_in_use: delete attempted while active agents still attach this context. The error includes details.inUseByAgentIds.