mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
Add ambient host credential push preflight
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
fb4d54e46d
commit
9fe17a8c6a
3 files changed
+349
-4
No files matched your search
@@ -442,6 +442,168 @@ describe("resolveExecutionRunAdapterConfig", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("passes with an ambient gh credential without injecting a token into the run env", async () => {
|
||||
const probe = vi.fn().mockResolvedValue("gh_cli");
|
||||
const getByNameInsensitive = vi.fn();
|
||||
const resolveInjectedEnvBindingForRuntime = vi.fn();
|
||||
|
||||
const result = await resolveExecutionRunAdapterConfig({
|
||||
companyId: "company-1",
|
||||
agentId: "agent-1",
|
||||
issueId: "issue-1",
|
||||
executionRunConfig: { env: { AGENT_ONLY: "agent-only" } },
|
||||
projectEnv: null,
|
||||
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",
|
||||
ambientHostCredential: {
|
||||
enabled: true,
|
||||
checkoutCwd: "/checkout",
|
||||
probe,
|
||||
},
|
||||
},
|
||||
secretsSvc: {
|
||||
resolveAdapterConfigForRuntime: vi.fn().mockResolvedValue({
|
||||
config: { env: { AGENT_ONLY: "agent-only" } },
|
||||
secretKeys: new Set<string>(),
|
||||
manifest: [],
|
||||
}),
|
||||
resolveEnvBindings: vi.fn(),
|
||||
resolveInjectedEnvBindingForRuntime,
|
||||
getByNameInsensitive,
|
||||
collectMissingRuntimeBindings: vi.fn().mockResolvedValue([]),
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(probe).toHaveBeenCalledWith("/checkout");
|
||||
expect(result.resolvedConfig.env).toEqual({ AGENT_ONLY: "agent-only" });
|
||||
expect(result.secretKeys).toEqual(new Set());
|
||||
expect(getByNameInsensitive).not.toHaveBeenCalled();
|
||||
expect(resolveInjectedEnvBindingForRuntime).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["timeout", Object.assign(new Error("timed out"), { code: "ETIMEDOUT" })],
|
||||
["non-zero exit", Object.assign(new Error("exited 1"), { code: 1 })],
|
||||
])("falls through to the company-secret tier after an ambient gh probe %s", async (_case, probeError) => {
|
||||
const probe = vi.fn().mockRejectedValue(probeError);
|
||||
const resolveInjectedEnvBindingForRuntime = vi.fn().mockResolvedValue({
|
||||
env: { GH_TOKEN: "resolved-token" },
|
||||
secretKeys: new Set(["GH_TOKEN"]),
|
||||
manifest: [],
|
||||
});
|
||||
|
||||
const result = await resolveExecutionRunAdapterConfig({
|
||||
companyId: "company-1",
|
||||
agentId: "agent-1",
|
||||
issueId: "issue-1",
|
||||
executionRunConfig: { env: {} },
|
||||
projectEnv: null,
|
||||
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",
|
||||
ambientHostCredential: { enabled: true, checkoutCwd: "/checkout", probe },
|
||||
},
|
||||
secretsSvc: {
|
||||
resolveAdapterConfigForRuntime: vi.fn().mockResolvedValue({
|
||||
config: { env: {} },
|
||||
secretKeys: new Set<string>(),
|
||||
manifest: [],
|
||||
}),
|
||||
resolveEnvBindings: vi.fn(),
|
||||
resolveInjectedEnvBindingForRuntime,
|
||||
getByNameInsensitive: vi.fn().mockResolvedValue({ id: "secret-1", name: "GH_TOKEN" }),
|
||||
collectMissingRuntimeBindings: vi.fn().mockResolvedValue([]),
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(probe).toHaveBeenCalledOnce();
|
||||
expect(result.resolvedConfig.env).toEqual({ GH_TOKEN: "resolved-token" });
|
||||
expect(resolveInjectedEnvBindingForRuntime).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("passes with an SSH-style push remote and no injected token", async () => {
|
||||
const probe = vi.fn().mockResolvedValue("ssh_push_remote");
|
||||
const resolveInjectedEnvBindingForRuntime = vi.fn();
|
||||
const result = await resolveExecutionRunAdapterConfig({
|
||||
companyId: "company-1",
|
||||
agentId: "agent-1",
|
||||
issueId: "issue-1",
|
||||
executionRunConfig: { env: {} },
|
||||
projectEnv: null,
|
||||
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",
|
||||
ambientHostCredential: { enabled: true, checkoutCwd: "/checkout", probe },
|
||||
},
|
||||
secretsSvc: {
|
||||
resolveAdapterConfigForRuntime: vi.fn().mockResolvedValue({
|
||||
config: { env: {} },
|
||||
secretKeys: new Set<string>(),
|
||||
manifest: [],
|
||||
}),
|
||||
resolveEnvBindings: vi.fn(),
|
||||
resolveInjectedEnvBindingForRuntime,
|
||||
getByNameInsensitive: vi.fn(),
|
||||
collectMissingRuntimeBindings: vi.fn().mockResolvedValue([]),
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(result.resolvedConfig.env).toEqual({});
|
||||
expect(resolveInjectedEnvBindingForRuntime).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips the ambient tier for a remote execution target", async () => {
|
||||
const probe = vi.fn();
|
||||
const resolveInjectedEnvBindingForRuntime = vi.fn().mockResolvedValue({
|
||||
env: { GH_TOKEN: "resolved-token" },
|
||||
secretKeys: new Set(["GH_TOKEN"]),
|
||||
manifest: [],
|
||||
});
|
||||
const result = await resolveExecutionRunAdapterConfig({
|
||||
companyId: "company-1",
|
||||
agentId: "agent-1",
|
||||
issueId: "issue-1",
|
||||
executionRunConfig: { env: {} },
|
||||
projectEnv: null,
|
||||
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",
|
||||
ambientHostCredential: { enabled: false, checkoutCwd: "/checkout", probe },
|
||||
},
|
||||
secretsSvc: {
|
||||
resolveAdapterConfigForRuntime: vi.fn().mockResolvedValue({
|
||||
config: { env: {} },
|
||||
secretKeys: new Set<string>(),
|
||||
manifest: [],
|
||||
}),
|
||||
resolveEnvBindings: vi.fn(),
|
||||
resolveInjectedEnvBindingForRuntime,
|
||||
getByNameInsensitive: vi.fn().mockResolvedValue({ id: "secret-1", name: "GH_TOKEN" }),
|
||||
collectMissingRuntimeBindings: vi.fn().mockResolvedValue([]),
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(probe).not.toHaveBeenCalled();
|
||||
expect(result.resolvedConfig.env).toEqual({ GH_TOKEN: "resolved-token" });
|
||||
});
|
||||
|
||||
it("does not accept a lowercase GitHub credential binding key", async () => {
|
||||
await expect(resolveExecutionRunAdapterConfig({
|
||||
companyId: "company-1",
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
mergeExecutionWorkspaceMetadataForPersistence,
|
||||
mergeCoalescedContextSnapshot,
|
||||
preflightLowTrustWorkspaceIsolation,
|
||||
probeAmbientHostPushCredential,
|
||||
prioritizeProjectWorkspaceCandidatesForRun,
|
||||
parseSessionCompactionPolicy,
|
||||
provisionExecutionWorkspaceForFreshnessDecision,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
resolveWorkspaceAfterLowTrustPreflight,
|
||||
resolveRuntimeSessionParamsForWorkspace,
|
||||
shouldDeferFollowupWakeForSameIssue,
|
||||
shouldProbeAmbientHostPushCredential,
|
||||
stripHostWorkspaceProvisionForLowTrustSandbox,
|
||||
stripWorkspaceRuntimeFromExecutionRunConfig,
|
||||
shouldResetTaskSessionForModelChange,
|
||||
@@ -972,6 +974,84 @@ describe("assertPushCapabilityCheckoutValid", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("probeAmbientHostPushCredential", () => {
|
||||
it("uses the host gh auth store without passing an injected environment", async () => {
|
||||
const execFileMock = vi.fn().mockResolvedValue({ stdout: "gho_ambient\n", stderr: "" });
|
||||
|
||||
await expect(probeAmbientHostPushCredential("/checkout", {
|
||||
execFile: execFileMock as unknown as typeof execFile,
|
||||
})).resolves.toBe("gh_cli");
|
||||
|
||||
expect(execFileMock).toHaveBeenCalledOnce();
|
||||
expect(execFileMock).toHaveBeenCalledWith(
|
||||
"gh",
|
||||
["auth", "token", "--hostname", "github.com"],
|
||||
expect.objectContaining({ timeout: 2_000 }),
|
||||
);
|
||||
expect(execFileMock.mock.calls[0]?.[2]).not.toHaveProperty("env");
|
||||
});
|
||||
|
||||
it.each([
|
||||
"ssh://git@github.com/example/repo.git",
|
||||
"git@github.com:example/repo.git",
|
||||
])("accepts SSH push remote %s after the gh probe misses", async (pushUrl) => {
|
||||
const execFileMock = vi.fn(async (command: string, args: string[]) => {
|
||||
if (command === "gh") throw Object.assign(new Error("not logged in"), { code: 1 });
|
||||
if (args[0] === "remote" && args.length === 1) return { stdout: "origin\n", stderr: "" };
|
||||
return { stdout: `${pushUrl}\n`, stderr: "" };
|
||||
});
|
||||
|
||||
await expect(probeAmbientHostPushCredential("/checkout", {
|
||||
execFile: execFileMock as unknown as typeof execFile,
|
||||
})).resolves.toBe("ssh_push_remote");
|
||||
expect(execFileMock.mock.calls[0]?.[0]).toBe("gh");
|
||||
expect(execFileMock.mock.calls[2]?.[1]).toEqual(["remote", "get-url", "--push", "origin"]);
|
||||
});
|
||||
|
||||
it("treats gh and git probe failures as a tier miss", async () => {
|
||||
const execFileMock = vi.fn().mockRejectedValue(Object.assign(new Error("timed out"), {
|
||||
code: "ETIMEDOUT",
|
||||
}));
|
||||
|
||||
await expect(probeAmbientHostPushCredential("/checkout", {
|
||||
execFile: execFileMock as unknown as typeof execFile,
|
||||
})).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldProbeAmbientHostPushCredential", () => {
|
||||
it.each(["ssh", "sandbox", "plugin"])("skips the ambient host tier for %s execution", (driver) => {
|
||||
expect(shouldProbeAmbientHostPushCredential({
|
||||
pushCapabilityPreflightRequired: true,
|
||||
environmentDriver: driver,
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
it("enables the ambient host tier for a standard local execution target", () => {
|
||||
expect(shouldProbeAmbientHostPushCredential({
|
||||
pushCapabilityPreflightRequired: true,
|
||||
environmentDriver: "local",
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it("skips the host tier when low-trust preflight will redirect local selection to a sandbox", () => {
|
||||
expect(shouldProbeAmbientHostPushCredential({
|
||||
pushCapabilityPreflightRequired: true,
|
||||
environmentDriver: "local",
|
||||
trustPreset: {
|
||||
kind: "low_trust_review",
|
||||
preset: "low_trust_review",
|
||||
boundary: {
|
||||
mode: "low_trust_review",
|
||||
companyId: "company-1",
|
||||
issueIds: ["issue-1"],
|
||||
},
|
||||
sourcePresets: {},
|
||||
},
|
||||
})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requiresPushCapabilityPreflight", () => {
|
||||
it("only enables the guard when the issue explicitly mentions the GitHub PR workflow skill", () => {
|
||||
expect(requiresPushCapabilityPreflight({
|
||||
|
||||
@@ -463,6 +463,7 @@ const EXECUTION_REVIEW_PARTICIPANT_RECOVERY_CAUSE = "execution_review_participan
|
||||
const GITHUB_PR_WORKFLOW_SKILL_KEY = "paperclipai/bundled/software-development/github-pr-workflow";
|
||||
const GITHUB_PR_WORKFLOW_SKILL_SLUG = "github-pr-workflow";
|
||||
const PUSH_CAPABILITY_ENV_KEYS = ["GH_TOKEN", "GITHUB_TOKEN"] as const;
|
||||
const AMBIENT_GITHUB_CREDENTIAL_PROBE_TIMEOUT_MS = 2_000;
|
||||
// Keep this in sync with local adapters that require a git workspace before launch.
|
||||
const GIT_SENSITIVE_LOCAL_ADAPTER_TYPES = new Set([
|
||||
"claude_local",
|
||||
@@ -841,6 +842,72 @@ function hasGithubPrWorkflowSkill(desiredSkills: string[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export type AmbientHostPushCredential = "gh_cli" | "ssh_push_remote";
|
||||
|
||||
/**
|
||||
* Probe credentials that are already available to a local execution host without
|
||||
* copying them into the dispatched run. In particular, do not pass the resolved
|
||||
* run env to `gh`: an injected GH_TOKEN would override a working hosts.yml login.
|
||||
*/
|
||||
export async function probeAmbientHostPushCredential(
|
||||
cwd: string | null | undefined,
|
||||
deps: { execFile: typeof execFile } = { execFile },
|
||||
): Promise<AmbientHostPushCredential | null> {
|
||||
try {
|
||||
const result = await deps.execFile(
|
||||
"gh",
|
||||
["auth", "token", "--hostname", "github.com"],
|
||||
{ timeout: AMBIENT_GITHUB_CREDENTIAL_PROBE_TIMEOUT_MS, windowsHide: true },
|
||||
);
|
||||
if (readNonEmptyString(result.stdout)) return "gh_cli";
|
||||
} catch {
|
||||
// A missing CLI, timeout, non-zero exit, or unreadable auth state is a tier miss.
|
||||
}
|
||||
|
||||
const normalizedCwd = readNonEmptyString(cwd);
|
||||
if (!normalizedCwd) return null;
|
||||
const remoteNames = await deps.execFile(
|
||||
"git",
|
||||
["remote"],
|
||||
{
|
||||
cwd: normalizedCwd,
|
||||
timeout: AMBIENT_GITHUB_CREDENTIAL_PROBE_TIMEOUT_MS,
|
||||
windowsHide: true,
|
||||
},
|
||||
).then((result) =>
|
||||
result.stdout
|
||||
.split(/\r?\n/)
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0),
|
||||
).catch(() => []);
|
||||
|
||||
for (const remoteName of remoteNames) {
|
||||
const pushUrl = await deps.execFile(
|
||||
"git",
|
||||
["remote", "get-url", "--push", remoteName],
|
||||
{
|
||||
cwd: normalizedCwd,
|
||||
timeout: AMBIENT_GITHUB_CREDENTIAL_PROBE_TIMEOUT_MS,
|
||||
windowsHide: true,
|
||||
},
|
||||
).then((result) => readNonEmptyString(result.stdout)).catch(() => null);
|
||||
if (pushUrl && (/^ssh:\/\//i.test(pushUrl) || /^[^\s/@:]+@[^\s/:]+:.+/.test(pushUrl))) {
|
||||
return "ssh_push_remote";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function shouldProbeAmbientHostPushCredential(input: {
|
||||
pushCapabilityPreflightRequired: boolean;
|
||||
environmentDriver: string | null | undefined;
|
||||
trustPreset?: TrustPresetResolution;
|
||||
}) {
|
||||
return input.pushCapabilityPreflightRequired
|
||||
&& input.trustPreset?.kind !== "low_trust_review"
|
||||
&& !isRemoteExecutionEnvironmentDriver(input.environmentDriver);
|
||||
}
|
||||
|
||||
export function requiresPushCapabilityPreflight(input: {
|
||||
adapterType: string;
|
||||
issueId: string | null | undefined;
|
||||
@@ -922,6 +989,11 @@ export async function resolveExecutionRunAdapterConfig(input: {
|
||||
remediation: string;
|
||||
fallbackSecretNames?: readonly string[];
|
||||
fallbackInjectionEnvKey?: string;
|
||||
ambientHostCredential?: {
|
||||
enabled: boolean;
|
||||
checkoutCwd?: string | null;
|
||||
probe?: typeof probeAmbientHostPushCredential;
|
||||
};
|
||||
};
|
||||
}) {
|
||||
const executionRunConfig = stripForbiddenEnvFromAdapterConfig(input.executionRunConfig);
|
||||
@@ -954,6 +1026,13 @@ export async function resolveExecutionRunAdapterConfig(input: {
|
||||
&& isConfiguredEnvBindingValue(routineEnv?.[key])
|
||||
))
|
||||
: false;
|
||||
const ambientHostCredential = requiredScopedEnvBinding
|
||||
&& !requiredScopedBindingsConfigured
|
||||
&& requiredScopedEnvBinding.ambientHostCredential?.enabled
|
||||
? await (
|
||||
requiredScopedEnvBinding.ambientHostCredential.probe ?? probeAmbientHostPushCredential
|
||||
)(requiredScopedEnvBinding.ambientHostCredential.checkoutCwd).catch(() => null)
|
||||
: null;
|
||||
let fallbackSecret: Awaited<ReturnType<RuntimeConfigSecretResolver["getByNameInsensitive"]>> | null = null;
|
||||
let fallbackAuthorization: {
|
||||
bindingId: string;
|
||||
@@ -964,6 +1043,7 @@ export async function resolveExecutionRunAdapterConfig(input: {
|
||||
if (
|
||||
requiredScopedEnvBinding
|
||||
&& !requiredScopedBindingsConfigured
|
||||
&& !ambientHostCredential
|
||||
&& requiredScopedEnvBinding.fallbackSecretNames?.length
|
||||
&& typeof input.secretsSvc.getByNameInsensitive === "function"
|
||||
) {
|
||||
@@ -999,7 +1079,12 @@ export async function resolveExecutionRunAdapterConfig(input: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (requiredScopedEnvBinding && !requiredScopedBindingsConfigured && !fallbackSecret) {
|
||||
if (
|
||||
requiredScopedEnvBinding
|
||||
&& !requiredScopedBindingsConfigured
|
||||
&& !ambientHostCredential
|
||||
&& !fallbackSecret
|
||||
) {
|
||||
throw new ConfigurationIncompleteFailure(`configuration incomplete: ${requiredScopedEnvBinding.remediation}`, {
|
||||
configurationIncomplete: {
|
||||
reason: requiredScopedEnvBinding.reason,
|
||||
@@ -1302,13 +1387,19 @@ export async function resolveExecutionRunAdapterConfig(input: {
|
||||
}
|
||||
}
|
||||
if (requiredScopedEnvBinding) {
|
||||
const tier = requiredScopedBindingsConfigured
|
||||
? "T1_env_binding"
|
||||
: ambientHostCredential
|
||||
? "T2_ambient_host_credential"
|
||||
: "T3_well_known_company_secret";
|
||||
logger.info(
|
||||
{
|
||||
companyId: input.companyId,
|
||||
agentId: input.agentId ?? null,
|
||||
issueId: input.issueId ?? null,
|
||||
heartbeatRunId: input.heartbeatRunId ?? null,
|
||||
tier: fallbackSecret ? "T3_well_known_company_secret" : "T1_env_binding",
|
||||
tier,
|
||||
ambientCredentialKind: ambientHostCredential,
|
||||
selectedSecretName: fallbackSecret?.name ?? null,
|
||||
},
|
||||
"Push-capability preflight satisfied",
|
||||
@@ -14677,6 +14768,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
issueId,
|
||||
explicitRunScopedSkillKeys: runScopedMentionedSkillKeys,
|
||||
});
|
||||
const ambientHostPushCredentialEnabled = shouldProbeAmbientHostPushCredential({
|
||||
pushCapabilityPreflightRequired,
|
||||
environmentDriver: selectedEnvironmentForConfig?.driver ?? null,
|
||||
trustPreset,
|
||||
});
|
||||
const { resolvedConfig, secretKeys, secretManifest } = await resolveExecutionRunAdapterConfig({
|
||||
companyId: agent.companyId,
|
||||
agentId: agent.id,
|
||||
@@ -14699,10 +14795,17 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
keys: [...PUSH_CAPABILITY_ENV_KEYS],
|
||||
consumerScopes: ["agent", "project", "environment", "routine"],
|
||||
reason: "push_write_credential_missing",
|
||||
remediation:
|
||||
"GitHub PR workflow found no GH_TOKEN/GITHUB_TOKEN binding at agent, project, environment, or routine scope and no company secret named GITHUB_TOKEN, GH_TOKEN, or PAPERCLIP_GITHUB_TOKEN. Bind GH_TOKEN or GITHUB_TOKEN at one of those scopes, or configure one of the well-known company secrets.",
|
||||
remediation: ambientHostPushCredentialEnabled
|
||||
? "GitHub PR workflow found no GH_TOKEN/GITHUB_TOKEN binding at agent, project, environment, or routine scope, no ambient gh/ssh credential on host, and no company secret named GITHUB_TOKEN, GH_TOKEN, or PAPERCLIP_GITHUB_TOKEN. Bind GH_TOKEN or GITHUB_TOKEN at one of those scopes, authenticate gh or configure an SSH push remote on the local host, or configure one of the well-known company secrets."
|
||||
: "GitHub PR workflow found no GH_TOKEN/GITHUB_TOKEN binding at agent, project, environment, or routine scope and no company secret named GITHUB_TOKEN, GH_TOKEN, or PAPERCLIP_GITHUB_TOKEN. Bind GH_TOKEN or GITHUB_TOKEN at one of those scopes, or configure one of the well-known company secrets.",
|
||||
fallbackSecretNames: DEFAULT_GITHUB_TOKEN_SECRET_NAMES,
|
||||
fallbackInjectionEnvKey: "GH_TOKEN",
|
||||
ambientHostCredential: {
|
||||
enabled: ambientHostPushCredentialEnabled,
|
||||
checkoutCwd:
|
||||
reusableExistingExecutionWorkspace?.cwd
|
||||
?? executionRunConfig.cwd as string | null | undefined,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user