Skip to content

Commit 3c5727d

Browse files
committed
feat(vault): support credential networking and injection policies
1 parent 941bc00 commit 3c5727d

15 files changed

Lines changed: 343 additions & 25 deletions

File tree

docs/guides/use-mcp-and-vaults.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ vaults:
2424
secret_value: ${DB_TOKEN}
2525
networking:
2626
type: limited
27+
allowed_hosts: ["api.example.com"]
2728
```
2829
2930
| Credential type | Key fields |
@@ -33,6 +34,34 @@ vaults:
3334

3435
Secrets are referenced with `${VAR_NAME}` and loaded from `.env`; never inline a real token.
3536

37+
### Bailian credential injection
38+
39+
Bailian supports `environment_variable` credentials. Declare the outbound hosts and
40+
request locations where the secret may be injected:
41+
42+
```yaml
43+
vaults:
44+
api-credentials:
45+
provider: bailian
46+
display_name: "API Credentials"
47+
credentials:
48+
- name: api-token
49+
type: environment_variable
50+
secret_name: API_TOKEN
51+
secret_value: ${API_TOKEN}
52+
networking:
53+
allowed_hosts: ["api.example.com", "*.example.org"]
54+
injection_location:
55+
header: true
56+
body: false
57+
```
58+
59+
The API receives both objects under `auth`. You do not need to send `networking.type`.
60+
Use `["*"]` to explicitly allow all hosts. Existing declarations with no networking
61+
policy or with `type: unrestricted` retain that scope; an omitted injection location
62+
defaults to headers enabled and body disabled. Legacy `type: limited` requires an
63+
explicit `allowed_hosts` list.
64+
3665
## Attach MCP servers to an agent
3766

3867
Reference a URL-based MCP server and bind the vault that holds its token:

docs/reference/configuration.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,18 @@ vaults:
222222
| `type` | `"environment_variable"` | yes | |
223223
| `secret_name` | string | yes | Secret name. |
224224
| `secret_value` | string | yes | Secret value (string or number, coerced). |
225-
| `networking.type` | `"unrestricted"` \| `"limited"` | no | |
225+
| `networking.type` | `"unrestricted"` \| `"limited"` | no | Legacy policy type. Bailian requests use `allowed_hosts` instead. |
226+
| `networking.allowed_hosts` | string[] | no | Bailian credential injection host allow-list, e.g. `["api.example.com", "*.example.org"]`; `["*"]` allows all hosts. |
227+
| `injection_location.header` | boolean | no | Bailian: allow secret replacement in request headers. |
228+
| `injection_location.body` | boolean | no | Bailian: allow secret replacement in request bodies. |
229+
230+
For Bailian, these fields are nested under `auth` in credential create/update requests.
231+
When omitted, creation uses `networking: { allowed_hosts: ["*"] }` and
232+
`injection_location: { header: true, body: false }`. Legacy `networking.type: unrestricted`
233+
maps to `["*"]`; `limited` must include `allowed_hosts` and is never widened implicitly.
234+
Explicit host lists and injection booleans are preserved, including through sync/export.
235+
Credential retry adoption compares host sets and injection policy as well as the existing
236+
identity and metadata fields; a different policy is not treated as an exact match.
226237

227238
## Memory store
228239

packages/playbooks/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ export interface VaultCredentialStructure {
7373
name: string;
7474
type: "environment_variable";
7575
secret_name: string;
76-
networking?: { type: "unrestricted" | "limited" };
76+
networking?: { type?: "unrestricted" | "limited"; allowed_hosts?: string[] };
77+
injection_location?: { header?: boolean; body?: boolean };
7778
}
7879

7980
export interface VaultProfile {

packages/sdk/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export {
2525

2626
export type {
2727
CredentialDecl,
28+
CredentialInjectionLocation,
29+
CredentialNetworking,
2830
DeploymentDecl,
2931
EnvironmentDecl,
3032
ProjectConfig,

packages/sdk/src/internal/core/validate-config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,20 @@ export function collectProviderCapabilities(
157157
}
158158
const caps = def.capabilities;
159159

160+
for (const [name, vault] of Object.entries(config.vaults ?? {})) {
161+
if (vault.provider && vault.provider !== providerName) continue;
162+
if (
163+
providerName !== "bailian" &&
164+
vault.credentials.some((credential) => credential.injection_location !== undefined)
165+
) {
166+
diagnostics.error(
167+
`${providerName}.vault.injection_location.unsupported`,
168+
`vault.${name}: provider '${providerName}' does not support credential injection_location; pin this vault to bailian.`,
169+
{ type: "vault", name, provider: providerName },
170+
);
171+
}
172+
}
173+
160174
for (const [name, environment] of Object.entries(config.environments ?? {})) {
161175
if (environment.provider && environment.provider !== providerName) continue;
162176
if (environment.environment_id) continue;

packages/sdk/src/internal/core/vault-credential-runtime.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,28 @@ function resolveVaultProvider(
206206
function credentialMatches(remote: VaultCredentialInfo, desired: CredentialDecl): boolean {
207207
if (remote.auth_type !== desired.type || !metadataMatches(remote.metadata, desired.metadata)) return false;
208208
if (desired.type === "static_bearer") return remote.mcp_server_url === desired.mcp_server_url;
209+
const desiredHosts = credentialHosts(desired.networking);
210+
const remoteHosts = credentialHosts(remote.networking ?? { type: remote.networking_type });
211+
const desiredLocation = desired.injection_location;
212+
const remoteLocation = remote.injection_location;
213+
// An omitted response cannot prove that an explicitly requested injection policy matches.
214+
if (desiredLocation && !remoteLocation) return false;
209215
return (
210216
remote.secret_name === desired.secret_name &&
211-
(remote.networking_type ?? "unrestricted") === (desired.networking?.type ?? "unrestricted")
217+
desiredHosts !== undefined &&
218+
remoteHosts !== undefined &&
219+
desiredHosts.length === remoteHosts.length &&
220+
desiredHosts.every((host) => remoteHosts.includes(host)) &&
221+
(desiredLocation?.header ?? true) === (remoteLocation?.header ?? true) &&
222+
(desiredLocation?.body ?? false) === (remoteLocation?.body ?? false)
212223
);
213224
}
214225

226+
function credentialHosts(networking: { type?: string; allowed_hosts?: string[] } | undefined): string[] | undefined {
227+
if (networking?.allowed_hosts !== undefined) return [...new Set(networking.allowed_hosts)];
228+
return networking?.type === "limited" ? undefined : ["*"];
229+
}
230+
215231
function metadataMatches(
216232
remote: Record<string, string> | undefined,
217233
desired: Record<string, string> | undefined,

packages/sdk/src/internal/parser/schema.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,18 @@ const environmentVariableCredentialSchema = z.object({
6161
metadata: z.record(z.string(), z.string()).optional(),
6262
secret_name: z.string(),
6363
secret_value: coerceString,
64-
networking: z.object({ type: z.enum(["unrestricted", "limited"]) }).optional(),
64+
networking: z
65+
.object({
66+
type: z.enum(["unrestricted", "limited"]).optional(),
67+
allowed_hosts: z.array(z.string()).optional(),
68+
})
69+
.optional(),
70+
injection_location: z
71+
.object({
72+
header: z.boolean().optional(),
73+
body: z.boolean().optional(),
74+
})
75+
.optional(),
6576
});
6677

6778
const credentialSchema = z.discriminatedUnion("type", [

packages/sdk/src/internal/providers/bailian/adapter.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,8 @@ export class BailianAdapter implements ProviderAdapter {
639639
secret_name: auth.secret_name as string | undefined,
640640
mcp_server_url: auth.mcp_server_url as string | undefined,
641641
networking_type: networking?.type as string | undefined,
642+
networking: networking as CredentialDecl["networking"],
643+
injection_location: auth.injection_location as CredentialDecl["injection_location"],
642644
metadata: metadata
643645
? Object.fromEntries(
644646
Object.entries(metadata).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
@@ -655,7 +657,17 @@ export class BailianAdapter implements ProviderAdapter {
655657
async updateCredential(
656658
vaultId: string,
657659
credentialId: string,
658-
patch: { display_name?: string; metadata?: Record<string, string> },
660+
patch: {
661+
display_name?: string;
662+
metadata?: Record<string, string>;
663+
auth?: {
664+
type: "environment_variable";
665+
secret_name?: string;
666+
secret_value?: string;
667+
networking?: { allowed_hosts: string[] };
668+
injection_location?: CredentialDecl["injection_location"];
669+
};
670+
},
659671
): Promise<RemoteResource> {
660672
const res = (await this.client.post(`/vaults/${vaultId}/credentials/${credentialId}`, patch)) as Record<
661673
string,

packages/sdk/src/internal/providers/bailian/mapper.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,16 @@ export function mapCredential(cred: CredentialDecl): unknown {
5959
`credential '${cred.name}': Bailian only supports credential type 'environment_variable', but '${cred.type}' was declared.`,
6060
);
6161
}
62+
if (cred.networking?.type === "limited" && cred.networking.allowed_hosts === undefined) {
63+
throw new UserError(`credential '${cred.name}': Bailian limited networking requires networking.allowed_hosts.`);
64+
}
6265
const body: Record<string, unknown> = {
6366
auth: {
6467
type: "environment_variable",
6568
secret_name: cred.secret_name,
6669
secret_value: cred.secret_value,
67-
networking: cred.networking ?? { type: "unrestricted" },
70+
networking: { allowed_hosts: cred.networking?.allowed_hosts ?? ["*"] },
71+
injection_location: cred.injection_location ?? { header: true, body: false },
6872
},
6973
display_name: cred.name,
7074
};
@@ -95,14 +99,16 @@ export function credToDecl(raw: Record<string, unknown>, vaultName: string): Cre
9599
}
96100

97101
// Default to environment_variable (Bailian's accepted credential type).
98-
const networking = auth.networking as { type: "unrestricted" | "limited" } | undefined;
102+
const networking = auth.networking as CredentialDecl["networking"];
103+
const injectionLocation = auth.injection_location as CredentialDecl["injection_location"];
99104
return {
100105
name,
101106
type: "environment_variable",
102107
metadata: stripAgentsMetadata(raw.metadata),
103108
secret_name: (auth.secret_name as string) ?? name,
104109
secret_value: placeholder,
105110
networking: networking ?? { type: "unrestricted" },
111+
...(injectionLocation ? { injection_location: injectionLocation } : {}),
106112
};
107113
}
108114

packages/sdk/src/internal/types/config.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,17 @@ export interface VaultDecl {
100100

101101
export type CredentialType = "static_bearer" | "environment_variable";
102102

103+
export interface CredentialNetworking {
104+
/** Legacy policy type; Bailian requests use allowed_hosts instead. */
105+
type?: "unrestricted" | "limited";
106+
allowed_hosts?: string[];
107+
}
108+
109+
export interface CredentialInjectionLocation {
110+
header?: boolean;
111+
body?: boolean;
112+
}
113+
103114
export interface CredentialDecl {
104115
name: string;
105116
type: CredentialType;
@@ -111,7 +122,8 @@ export interface CredentialDecl {
111122
// environment_variable
112123
secret_name?: string;
113124
secret_value?: string;
114-
networking?: { type: "unrestricted" | "limited" };
125+
networking?: CredentialNetworking;
126+
injection_location?: CredentialInjectionLocation;
115127
}
116128

117129
// --- Memory Store ---

0 commit comments

Comments
 (0)