Fail closed on empty GitHub token bindings

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-19 16:45:55 +00:00
1 parent 95a53b52e2
commit 014390c9c0
2 files changed
+75 -7

No files matched your search

@@ -1026,6 +1026,55 @@ describe("resolveExecutionRunAdapterConfig", () => {
expect(resolveInjectedEnvBindingForRuntime).not.toHaveBeenCalled();
});
it.each([
["agent", "empty string", { executionRunConfig: { env: { GH_TOKEN: "" } } }],
["agent", "empty plain binding", { executionRunConfig: { env: { GITHUB_TOKEN: { type: "plain", value: "" } } } }],
["project", "empty string", { projectEnv: { GH_TOKEN: "" } }],
["project", "empty plain binding", { projectEnv: { GITHUB_TOKEN: { type: "plain", value: "" } } }],
["environment", "empty string", { environmentEnv: { GH_TOKEN: "" } }],
["environment", "empty plain binding", { environmentEnv: { GITHUB_TOKEN: { type: "plain", value: "" } } }],
["routine", "empty string", { routineEnv: { GH_TOKEN: "" } }],
["routine", "empty plain binding", { routineEnv: { GITHUB_TOKEN: { type: "plain", value: "" } } }],
])("fails closed for an explicit %s-scoped %s instead of injecting a company secret", async (_scope, _binding, scopeConfig) => {
const getByNameInsensitive = vi.fn().mockResolvedValue({ id: "secret-1", name: "GH_TOKEN" });
const resolveInjectedEnvBindingForRuntime = vi.fn();
await expect(resolveExecutionRunAdapterConfig({
companyId: "company-1",
agentId: "agent-1",
issueId: "issue-1",
projectId: "project-1",
environmentId: "environment-1",
routineId: "routine-1",
executionRunConfig: { env: {} },
projectEnv: null,
...scopeConfig,
requiredScopedEnvBinding: {
keys: ["GH_TOKEN", "GITHUB_TOKEN"],
consumerScopes: ["agent", "project", "environment", "routine"],
reason: "push_write_credential_missing",
remediation: "No GitHub credential found.",
fallbackSecretNames: ["GITHUB_TOKEN", "GH_TOKEN", "PAPERCLIP_GITHUB_TOKEN"],
fallbackInjectionEnvKey: "GH_TOKEN",
},
secretsSvc: {
resolveAdapterConfigForRuntime: vi.fn(),
resolveEnvBindings: vi.fn(),
resolveInjectedEnvBindingForRuntime,
getByNameInsensitive,
} as any,
})).rejects.toMatchObject({
code: "configuration_incomplete",
resultJson: {
configurationIncomplete: {
reason: "push_write_credential_missing",
},
},
});
expect(getByNameInsensitive).not.toHaveBeenCalled();
expect(resolveInjectedEnvBindingForRuntime).not.toHaveBeenCalled();
});
it("skips well-known-secret injection when a supported-scope binding exists", async () => {
const getByNameInsensitive = vi.fn();
const resolveInjectedEnvBindingForRuntime = vi.fn();
+26 -7
View File
@@ -824,7 +824,11 @@ function formatMissingBindingForOperator(missing: MissingRuntimeBinding): string
return `secret ${secretLabel} not bound at ${missing.consumerType} ${missing.configPath}`;
}
function isConfiguredEnvBindingValue(binding: unknown) {
function hasOwnEnvBinding(env: Record<string, unknown> | null, key: string) {
return env !== null && Object.prototype.hasOwnProperty.call(env, key);
}
function isUsableEnvBindingValue(binding: unknown) {
const parsed = envBindingSchema.safeParse(binding);
if (!parsed.success) return false;
const value = parsed.data;
@@ -1011,23 +1015,38 @@ export async function resolveExecutionRunAdapterConfig(input: {
assertLowTrustEnvConfigAllowed(routineEnv, "routine.env");
}
const requiredScopedEnvBinding = input.requiredScopedEnvBinding ?? null;
const requiredScopedBindingPresent = requiredScopedEnvBinding
? requiredScopedEnvBinding.keys.some((key) => (
requiredScopedEnvBinding.consumerScopes.includes("agent")
&& hasOwnEnvBinding(agentEnv, key)
) || (
requiredScopedEnvBinding.consumerScopes.includes("project")
&& hasOwnEnvBinding(projectEnv, key)
) || (
requiredScopedEnvBinding.consumerScopes.includes("environment")
&& hasOwnEnvBinding(environmentEnv, key)
) || (
requiredScopedEnvBinding.consumerScopes.includes("routine")
&& hasOwnEnvBinding(routineEnv, key)
))
: false;
const requiredScopedBindingsConfigured = requiredScopedEnvBinding
? requiredScopedEnvBinding.keys.some((key) => (
requiredScopedEnvBinding.consumerScopes.includes("agent")
&& isConfiguredEnvBindingValue(agentEnv[key])
&& isUsableEnvBindingValue(agentEnv[key])
) || (
requiredScopedEnvBinding.consumerScopes.includes("project")
&& isConfiguredEnvBindingValue(projectEnv?.[key])
&& isUsableEnvBindingValue(projectEnv?.[key])
) || (
requiredScopedEnvBinding.consumerScopes.includes("environment")
&& isConfiguredEnvBindingValue(environmentEnv?.[key])
&& isUsableEnvBindingValue(environmentEnv?.[key])
) || (
requiredScopedEnvBinding.consumerScopes.includes("routine")
&& isConfiguredEnvBindingValue(routineEnv?.[key])
&& isUsableEnvBindingValue(routineEnv?.[key])
))
: false;
const ambientHostCredential = requiredScopedEnvBinding
&& !requiredScopedBindingsConfigured
&& !requiredScopedBindingPresent
&& requiredScopedEnvBinding.ambientHostCredential?.enabled
? await (
requiredScopedEnvBinding.ambientHostCredential.probe ?? probeAmbientHostPushCredential
@@ -1042,7 +1061,7 @@ export async function resolveExecutionRunAdapterConfig(input: {
} | null = null;
if (
requiredScopedEnvBinding
&& !requiredScopedBindingsConfigured
&& !requiredScopedBindingPresent
&& !ambientHostCredential
&& requiredScopedEnvBinding.fallbackSecretNames?.length
&& typeof input.secretsSvc.getByNameInsensitive === "function"