Fix sandbox git publishing and large workspace uploads (#8422)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandbox and SSH runtimes need to preserve agent work across isolated
execution environments
> - Git-backed workspaces were being copied mostly as filesystem
archives, which breaks when `.git` points outside the mounted workspace
and makes sandbox agents unable to publish their own branches
> - Large ignored dependency trees could also be swept into the sandbox
overlay, causing multi-GB transfers and max-string failures in some
sandbox clients
> - This pull request makes sandbox runtime setup use a git-backed HEAD
sync plus a small dirty/untracked overlay, and bounds sandbox file
transfers so large archives do not need one huge string
> - The benefit is that sandbox agents can commit and push from a usable
git checkout without uploading dependency trees such as `node_modules`

## Linked Issues or Issue Description

Refs #8395

No public duplicate issue or PR was found after searches for `sandbox
git workspace`, `git push sandbox`, and `node_modules sandbox upload`.

Bug report:

- What happened: sandbox-backed agent workspaces could receive a `.git`
file that pointed at host-only git state, leaving the sandbox unable to
run normal git workflows. The sandbox overlay upload could also include
ignored dependency directories, creating very large transfers.
- Expected behavior: sandbox and remote runtimes should prepare a usable
git-backed workspace, copy only the necessary workspace overlay, and
restore git history plus file changes without depending on a host-only
`.git` path.
- Steps to reproduce:
1. Run an agent in a sandbox-backed workspace whose local git checkout
is a worktree.
2. Ask the agent to complete a GitHub workflow that requires commit/push
access.
3. Observe that git operations can fail inside the sandbox, and ignored
dependency trees can be uploaded as part of the workspace overlay.
- Paperclip version or commit: reproduced against `master` before this
PR, base `7aa212296eb1`.
- Deployment mode: local dev / sandbox-backed runtime.
- Installation method: built from source.
- Agent adapters involved: local adapters using shared adapter-utils
runtime preparation.
- Database mode: not database-related.
- Access context: agent runtime.
- Local verification environment: Node.js v25.6.1, pnpm 9.15.4, macOS
arm64.
- Privacy checklist: all pasted output was reviewed for secrets, private
hostnames, local usernames, and internal instance links.

## What Changed

- Added a GitHub workflow push preflight so agent runs can detect
missing push credentials when a workflow explicitly needs GitHub
publishing.
- Added shared git workspace sync helpers for shallow HEAD import/export
and dirty/untracked overlay tracking.
- Updated sandbox managed runtime setup to use git history plus a
selected overlay instead of uploading the full local workspace for
git-backed workspaces.
- Bounded sandbox archive upload/download paths so large payloads stream
or chunk instead of materializing one oversized string.
- Excluded `.git` and ignored dependency trees from sandbox upload,
download, and restore baselines while preserving local ignored
directories during sync-back.
- Added focused tests for git workspace sync, sandbox overlay selection,
transfer chunking, and heartbeat push-preflight behavior.

## Verification

- `pnpm exec vitest run
packages/adapter-utils/src/git-workspace-sync.test.ts
packages/adapter-utils/src/sandbox-managed-runtime.test.ts
packages/adapter-utils/src/command-managed-runtime.test.ts
server/src/__tests__/heartbeat-project-env.test.ts
server/src/__tests__/heartbeat-workspace-session.test.ts`
- `pnpm --filter @paperclipai/adapter-utils typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm build`
- Public-hygiene scan of the PR diff and commit messages for internal
issue ids, local paths, private hostnames, and obvious token patterns.

## Risks

- Medium risk: this changes sandbox runtime synchronization semantics
for git-backed workspaces, especially around dirty tracked files,
untracked files, deleted paths, and ignored files.
- The main mitigation is focused test coverage for upload contents,
restore exclusions, and git round-trip behavior.
- The SSH runtime keeps the current bundle-based implementation from
`master`; this PR only aligns shared excludes and sandbox behavior with
that model.

## Model Used

OpenAI Codex, GPT-5-based coding agent, tool-enabled shell/git/GitHub
workflow, with code execution and repository inspection.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin Foley
2026-06-20 22:03:55 -07:00
committed by GitHub
parent 33353ce62b
commit 2c98c8e1e5
13 changed files with 1416 additions and 143 deletions
@@ -313,6 +313,82 @@ describe("resolveExecutionRunAdapterConfig", () => {
details: { code: "low_trust_inline_sensitive_env_denied" },
});
});
it("fails push-capability preflight when no GitHub write credential is bound at agent or project scope", async () => {
await expect(resolveExecutionRunAdapterConfig({
companyId: "company-1",
agentId: "agent-1",
issueId: "issue-1",
executionRunConfig: { env: { AGENT_ONLY: "agent-only" } },
projectEnv: { PROJECT_ONLY: "project-only" },
requiredScopedEnvBinding: {
keys: ["GH_TOKEN", "GITHUB_TOKEN"],
consumerScopes: ["agent", "project"],
reason: "push_write_credential_missing",
remediation: "GitHub PR workflow requires GH_TOKEN or GITHUB_TOKEN bound at project or agent scope.",
},
secretsSvc: {
resolveAdapterConfigForRuntime: vi.fn(),
resolveEnvBindings: vi.fn(),
} as any,
})).rejects.toMatchObject({
code: "configuration_incomplete",
message: expect.stringContaining("GitHub PR workflow requires GH_TOKEN or GITHUB_TOKEN"),
resultJson: {
configurationIncomplete: {
reason: "push_write_credential_missing",
requiredEnvKeys: ["GH_TOKEN", "GITHUB_TOKEN"],
requiredScopes: ["agent", "project"],
missingBindings: [],
},
},
});
});
it("passes push-capability preflight when a project-scoped GitHub credential is configured", async () => {
const resolveAdapterConfigForRuntime = vi.fn().mockResolvedValue({
config: { env: { AGENT_ONLY: "agent-only" } },
secretKeys: new Set<string>(),
manifest: [],
});
const resolveEnvBindings = vi.fn().mockResolvedValue({
env: { GH_TOKEN: "github-token" },
secretKeys: new Set(["GH_TOKEN"]),
manifest: [],
});
const collectMissingRuntimeBindings = vi.fn().mockResolvedValue([]);
const result = await resolveExecutionRunAdapterConfig({
companyId: "company-1",
agentId: "agent-1",
issueId: "issue-1",
projectId: "project-1",
executionRunConfig: { env: { AGENT_ONLY: "agent-only" } },
projectEnv: { GH_TOKEN: { type: "plain", value: "github-token" } },
requiredScopedEnvBinding: {
keys: ["GH_TOKEN", "GITHUB_TOKEN"],
consumerScopes: ["agent", "project"],
reason: "push_write_credential_missing",
remediation: "GitHub PR workflow requires GH_TOKEN or GITHUB_TOKEN bound at project or agent scope.",
},
secretsSvc: {
resolveAdapterConfigForRuntime,
resolveEnvBindings,
collectMissingRuntimeBindings,
} as any,
});
expect(result.resolvedConfig.env).toEqual({
AGENT_ONLY: "agent-only",
GH_TOKEN: "github-token",
});
expect(resolveEnvBindings).toHaveBeenCalledOnce();
expect(collectMissingRuntimeBindings).toHaveBeenCalledTimes(2);
expect(collectMissingRuntimeBindings.mock.calls[1]?.[2]).toMatchObject({
consumerType: "project",
consumerId: "project-1",
});
});
});
describe("extractMentionedSkillIdsFromSources", () => {
@@ -1,3 +1,8 @@
import { execFile as execFileCallback } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import { describe, expect, it } from "vitest";
import type { agents } from "@paperclipai/db";
import { sessionCodec as codexSessionCodec } from "@paperclipai/adapter-codex-local/server";
@@ -5,6 +10,7 @@ import { resolveDefaultAgentWorkspaceDir } from "../home-paths.js";
import {
applyPersistedExecutionWorkspaceConfig,
assertGitSensitiveAdapterWorkspaceValid,
assertPushCapabilityCheckoutValid,
buildRealizedExecutionWorkspaceFromPersisted,
buildExplicitResumeSessionOverride,
deriveTaskKeyWithHeartbeatFallback,
@@ -16,6 +22,7 @@ import {
prioritizeProjectWorkspaceCandidatesForRun,
parseSessionCompactionPolicy,
resolveNextSessionState,
requiresPushCapabilityPreflight,
resolveWorkspaceAfterLowTrustPreflight,
resolveRuntimeSessionParamsForWorkspace,
shouldDeferFollowupWakeForSameIssue,
@@ -29,6 +36,8 @@ import {
} from "../services/heartbeat.ts";
import type { TrustPresetResolution } from "../services/trust-preset-resolver.ts";
const execFile = promisify(execFileCallback);
function buildResolvedWorkspace(overrides: Partial<ResolvedWorkspaceForRun> = {}): ResolvedWorkspaceForRun {
return {
cwd: "/tmp/project",
@@ -105,6 +114,19 @@ function buildWorkspaceValidationInput(
};
}
async function runGit(cwd: string, args: string[]) {
await execFile("git", args, { cwd });
}
async function createGitCheckout(options: { withRemote: boolean }) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-push-preflight-"));
await runGit(root, ["init"]);
if (options.withRemote) {
await runGit(root, ["remote", "add", "origin", "https://github.com/example/repo.git"]);
}
return root;
}
async function expectWorkspaceValidationFailure(
input: WorkspaceValidationInput,
reason: string,
@@ -377,6 +399,72 @@ describe("assertGitSensitiveAdapterWorkspaceValid", () => {
});
});
describe("assertPushCapabilityCheckoutValid", () => {
it("rejects a GitHub PR workflow checkout without a configured push remote", async () => {
const cwd = await createGitCheckout({ withRemote: false });
try {
await expect(assertPushCapabilityCheckoutValid({
enabled: true,
issue: {
id: "issue-1",
identifier: "PAP-1",
},
cwd,
})).rejects.toMatchObject({
code: "workspace_validation_failed",
message: expect.stringContaining("has no configured push remote"),
resultJson: {
workspaceValidation: expect.objectContaining({
reason: "missing_git_push_remote",
issueId: "issue-1",
executionWorkspaceCwd: cwd,
}),
},
});
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
});
it("allows a GitHub PR workflow checkout when a push remote is configured", async () => {
const cwd = await createGitCheckout({ withRemote: true });
try {
await expect(assertPushCapabilityCheckoutValid({
enabled: true,
issue: {
id: "issue-1",
identifier: "PAP-1",
},
cwd,
})).resolves.toBeUndefined();
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
});
});
describe("requiresPushCapabilityPreflight", () => {
it("only enables the guard when the issue explicitly mentions the GitHub PR workflow skill", () => {
expect(requiresPushCapabilityPreflight({
adapterType: "codex_local",
issueId: "issue-1",
explicitRunScopedSkillKeys: ["paperclipai/bundled/software-development/github-pr-workflow"],
})).toBe(true);
expect(requiresPushCapabilityPreflight({
adapterType: "codex_local",
issueId: "issue-1",
explicitRunScopedSkillKeys: [],
})).toBe(false);
expect(requiresPushCapabilityPreflight({
adapterType: "cursor-cloud",
issueId: "issue-1",
explicitRunScopedSkillKeys: ["paperclipai/bundled/software-development/github-pr-workflow"],
})).toBe(false);
});
});
describe("stripHostWorkspaceProvisionForLowTrustSandbox", () => {
it("removes only the host-side provision command for sandbox-backed low-trust runs", () => {
const config = {
@@ -61,12 +61,13 @@ async function waitFor(condition: () => boolean | Promise<boolean>, timeoutMs =
throw new Error("Timed out waiting for condition");
}
async function deleteHeartbeatRunsAfterActivityLogDrains(db: Db) {
async function deleteHeartbeatRunsAndWakeupsAfterActivityLogDrains(db: Db) {
let lastError: unknown = null;
for (let attempt = 0; attempt < 10; attempt += 1) {
await db.delete(activityLog);
try {
await db.delete(heartbeatRuns);
await db.delete(agentWakeupRequests);
return;
} catch (error) {
lastError = error;
@@ -527,8 +528,7 @@ describeEmbeddedPostgres("low-trust red-team HTTP route regression suite", () =>
await db.delete(issueRelations);
await db.delete(activityLog);
await db.delete(heartbeatRunEvents);
await deleteHeartbeatRunsAfterActivityLogDrains(db);
await db.delete(agentWakeupRequests);
await deleteHeartbeatRunsAndWakeupsAfterActivityLogDrains(db);
await db.delete(issues);
await db.delete(agentRuntimeState);
await db.delete(agents);
+166 -5
View File
@@ -260,6 +260,9 @@ const WORKSPACE_VALIDATION_FAILURE_CODE = "workspace_validation_failed";
const WORKSPACE_VALIDATION_RECOVERY_CAUSE = "workspace_validation_failed";
const CONFIGURATION_INCOMPLETE_FAILURE_CODE = "configuration_incomplete";
const CONFIGURATION_INCOMPLETE_RECOVERY_CAUSE = "configuration_incomplete";
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;
// Keep this in sync with local adapters that require a git workspace before launch.
const GIT_SENSITIVE_LOCAL_ADAPTER_TYPES = new Set([
"acpx_local",
@@ -411,6 +414,34 @@ function formatMissingBindingForOperator(missing: MissingRuntimeBinding): string
return `secret ${secretLabel} not bound at ${missing.consumerType} ${missing.configPath}`;
}
function isConfiguredEnvBindingValue(binding: unknown) {
const parsed = envBindingSchema.safeParse(binding);
if (!parsed.success) return false;
const value = parsed.data;
if (typeof value === "string") return value.trim().length > 0;
if (value.type === "plain") return value.value.trim().length > 0;
return true;
}
function hasGithubPrWorkflowSkill(desiredSkills: string[]) {
return desiredSkills.some((skill) => {
const normalized = skill.trim();
return normalized === GITHUB_PR_WORKFLOW_SKILL_KEY
|| normalized === GITHUB_PR_WORKFLOW_SKILL_SLUG
|| normalized.endsWith(`/${GITHUB_PR_WORKFLOW_SKILL_SLUG}`);
});
}
export function requiresPushCapabilityPreflight(input: {
adapterType: string;
issueId: string | null | undefined;
explicitRunScopedSkillKeys: string[];
}) {
return Boolean(input.issueId)
&& GIT_SENSITIVE_LOCAL_ADAPTER_TYPES.has(input.adapterType)
&& hasGithubPrWorkflowSkill(input.explicitRunScopedSkillKeys);
}
const LOW_TRUST_SENSITIVE_ENV_KEY_RE =
/(api[-_]?key|access[-_]?token|auth(?:_?token)?|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)/i;
@@ -466,11 +497,18 @@ export async function resolveExecutionRunAdapterConfig(input: {
routineEnv?: unknown;
secretsSvc: RuntimeConfigSecretResolver;
trustPreset?: TrustPresetResolution;
requiredScopedEnvBinding?: {
keys: string[];
consumerScopes: Array<"agent" | "project">;
reason: string;
remediation: string;
};
}) {
const executionRunConfig = stripPaperclipRuntimeEnvFromAdapterConfig(input.executionRunConfig);
const environmentEnv = stripPaperclipRuntimeEnvBindings(input.environmentEnv);
const projectEnv = stripPaperclipRuntimeEnvBindings(input.projectEnv);
const routineEnv = stripPaperclipRuntimeEnvBindings(input.routineEnv);
const agentEnv = parseObject(executionRunConfig.env);
const lowTrustAllowedBindingIds = input.trustPreset?.kind === "low_trust_review"
? input.trustPreset.boundary.allowedSecretBindingIds ?? []
: undefined;
@@ -480,6 +518,31 @@ export async function resolveExecutionRunAdapterConfig(input: {
assertLowTrustEnvConfigAllowed(projectEnv, "project.env");
assertLowTrustEnvConfigAllowed(routineEnv, "routine.env");
}
const requiredScopedEnvBinding = input.requiredScopedEnvBinding ?? null;
const requiredScopedBindingsConfigured = requiredScopedEnvBinding
? requiredScopedEnvBinding.keys.some((key) => (
requiredScopedEnvBinding.consumerScopes.includes("agent")
&& isConfiguredEnvBindingValue(agentEnv[key])
) || (
requiredScopedEnvBinding.consumerScopes.includes("project")
&& isConfiguredEnvBindingValue(projectEnv?.[key])
))
: false;
if (requiredScopedEnvBinding && !requiredScopedBindingsConfigured) {
throw new ConfigurationIncompleteFailure(`configuration incomplete: ${requiredScopedEnvBinding.remediation}`, {
configurationIncomplete: {
reason: requiredScopedEnvBinding.reason,
companyId: input.companyId,
agentId: input.agentId ?? null,
issueId: input.issueId ?? null,
projectId: input.projectId ?? null,
routineId: input.routineId ?? null,
requiredEnvKeys: requiredScopedEnvBinding.keys,
requiredScopes: requiredScopedEnvBinding.consumerScopes,
missingBindings: [],
},
});
}
// Pre-dispatch binding-validation gate: detect declared secret refs that have
// no binding before resolving any secret value. Missing bindings short-circuit
// to a configuration-incomplete blocker routed to a human owner instead of a
@@ -522,6 +585,33 @@ export async function resolveExecutionRunAdapterConfig(input: {
)),
);
}
if (requiredScopedEnvBinding) {
const requiredEnvKeys = new Set(requiredScopedEnvBinding.keys);
const requiredScopes = new Set(requiredScopedEnvBinding.consumerScopes);
const requiredMissingBindings = missingBindings.filter((binding) =>
requiredScopes.has(binding.consumerType as "agent" | "project")
&& requiredEnvKeys.has(binding.envKey),
);
if (requiredMissingBindings.length > 0) {
const detail = requiredMissingBindings.map(formatMissingBindingForOperator).join("; ");
throw new ConfigurationIncompleteFailure(
`configuration incomplete: ${requiredScopedEnvBinding.remediation}; ${detail}`,
{
configurationIncomplete: {
reason: requiredScopedEnvBinding.reason,
companyId: input.companyId,
agentId: input.agentId ?? null,
issueId: input.issueId ?? null,
projectId: input.projectId ?? null,
routineId: input.routineId ?? null,
requiredEnvKeys: requiredScopedEnvBinding.keys,
requiredScopes: requiredScopedEnvBinding.consumerScopes,
missingBindings: requiredMissingBindings,
},
},
);
}
}
if (missingBindings.length > 0) {
const detail = missingBindings.map(formatMissingBindingForOperator).join("; ");
throw new ConfigurationIncompleteFailure(`configuration incomplete: ${detail}`, {
@@ -1080,6 +1170,53 @@ function sameResolvedPath(left: string | null | undefined, right: string | null
return path.resolve(leftPath) === path.resolve(rightPath);
}
async function hasGitPushRemote(cwd: string | null | undefined) {
const normalized = readNonEmptyString(cwd);
if (!normalized) return false;
const remoteNames = await execFile("git", ["remote"], { cwd: normalized })
.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 execFile("git", ["remote", "get-url", "--push", remoteName], { cwd: normalized })
.then((result) => readNonEmptyString(result.stdout))
.catch(() => null);
if (pushUrl) return true;
}
return false;
}
export async function assertPushCapabilityCheckoutValid(input: {
enabled: boolean;
issue: {
id: string;
identifier: string | null;
} | null;
cwd: string | null | undefined;
}) {
if (!input.enabled || !input.issue) return;
const cwd = readNonEmptyString(input.cwd);
if (!cwd) return;
if (await hasGitPushRemote(cwd)) return;
throw new WorkspaceValidationFailure(
`Issue ${input.issue.identifier ?? input.issue.id} requested the GitHub PR workflow, but checkout "${cwd}" has no configured push remote. Bind the run to a writable repo checkout before dispatching the agent.`,
{
workspaceValidation: {
reason: "missing_git_push_remote",
issueId: input.issue.id,
issueIdentifier: input.issue.identifier,
executionWorkspaceCwd: cwd,
requiredEnvKeys: [...PUSH_CAPABILITY_ENV_KEYS],
},
},
);
}
export async function assertGitSensitiveAdapterWorkspaceValid(input: {
adapterType: string;
agentId: string;
@@ -8631,6 +8768,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
: selectedEnvironmentId
? await environmentsSvc.getById(selectedEnvironmentId)
: null;
const runScopedMentionedSkillKeys = await resolveRunScopedMentionedSkillKeys({
db,
companyId: agent.companyId,
issueId,
});
const pushCapabilityPreflightRequired = requiresPushCapabilityPreflight({
adapterType: agent.adapterType,
issueId,
explicitRunScopedSkillKeys: runScopedMentionedSkillKeys,
});
const { resolvedConfig, secretKeys, secretManifest } = await resolveExecutionRunAdapterConfig({
companyId: agent.companyId,
agentId: agent.id,
@@ -8645,6 +8792,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
routineEnv: routineEnvContext.env,
secretsSvc,
trustPreset,
requiredScopedEnvBinding: pushCapabilityPreflightRequired
? {
keys: [...PUSH_CAPABILITY_ENV_KEYS],
consumerScopes: ["agent", "project"],
reason: "push_write_credential_missing",
remediation:
"GitHub PR workflow requires GH_TOKEN or GITHUB_TOKEN bound at project or agent scope.",
}
: undefined,
});
if (secretManifest.length > 0) {
context.paperclipSecrets = {
@@ -8653,11 +8809,6 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
} else {
delete context.paperclipSecrets;
}
const runScopedMentionedSkillKeys = await resolveRunScopedMentionedSkillKeys({
db,
companyId: agent.companyId,
issueId,
});
const effectiveResolvedConfig = applyRunScopedMentionedSkillKeys(
resolvedConfig,
runScopedMentionedSkillKeys,
@@ -9271,6 +9422,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
environmentDriver: selectedEnvironment.driver,
leaseMetadata: activeEnvironmentLease.lease.metadata,
});
await assertPushCapabilityCheckoutValid({
enabled: pushCapabilityPreflightRequired && executionTarget?.kind === "local",
issue: issueRef
? {
id: issueRef.id,
identifier: issueRef.identifier,
}
: null,
cwd: executionWorkspace.cwd,
});
const adapterEnv = Object.fromEntries(
Object.entries(parseObject(resolvedConfig.env)).filter(
(entry): entry is [string, string] => typeof entry[0] === "string" && typeof entry[1] === "string",