Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions dev/local/env-sync/plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,39 @@ function computeCloudAgentNextPlan(root: string) {
return plan;
}

const gitTokenServiceDir = 'services/git-token-service';
const annotatedCapabilitySecretBinding = `{
"binding": "SCM_SESSION_CAPABILITY_ENCRYPTION_KEY",
"store_id": "store-id",
// @dev-generate base64 32
"secret_name": "SCM_SESSION_CAPABILITY_ENCRYPTION_KEY_DEV"
}`;

function createGitTokenServiceRepo(options: {
envLocal?: string;
devSecretBinding?: string;
rootSecretBinding?: string;
}): TestRepo {
const rootSecrets = options.rootSecretBinding
? `"secrets_store_secrets": [${options.rootSecretBinding}],`
: '';
return createRepo({
'.env.local': options.envLocal ?? '',
[`${gitTokenServiceDir}/package.json`]: JSON.stringify({
scripts: { dev: 'wrangler dev --env dev' },
}),
[`${gitTokenServiceDir}/.dev.vars.example`]: '',
[`${gitTokenServiceDir}/wrangler.jsonc`]: `{
${rootSecrets}
"env": {
"dev": {
"secrets_store_secrets": [${options.devSecretBinding ?? annotatedCapabilitySecretBinding}]
}
}
}`,
});
}

function withFakePnpm(output: string, fn: () => void): void {
const binDir = fs.mkdtempSync(path.join(os.tmpdir(), 'env-sync-bin-'));
const oldPath = process.env.PATH;
Expand Down Expand Up @@ -261,6 +294,132 @@ test('writes example defaults to .dev.vars when they override wrangler vars', ()
}
});

test('generates an annotated missing secret directly into the local Secrets Store', () => {
const repo = createGitTokenServiceRepo({});
try {
withFakePnpm('', () => {
const plan = computePlan(repo.root, new Set(['cloudflare-git-token-service']));
assert.equal(plan.missingEnvLocal, false);
assert.deepEqual(plan.devVarsChanges, []);
assert.deepEqual(plan.envLocalAutoCreates, []);
assert.deepEqual(plan.secretStoreWarnings, []);
assert.equal(plan.secretStoreAutoCreates.length, 1);
const [create] = plan.secretStoreAutoCreates;
assert.ok(create);
assert.equal(create.binding.secret_name, 'SCM_SESSION_CAPABILITY_ENCRYPTION_KEY_DEV');
assert.equal(create.sourceKey, '@dev-generate base64 32');
assert.equal(Buffer.from(create.value, 'base64').length, 32);
});
} finally {
repo.cleanup();
}
});

test('generates an annotated secret instead of copying a plaintext env source', () => {
const repo = createGitTokenServiceRepo({
envLocal: 'SCM_SESSION_CAPABILITY_ENCRYPTION_KEY=plaintext-source\n',
});
try {
withFakePnpm('', () => {
const plan = computePlan(repo.root, new Set(['cloudflare-git-token-service']));
const [create] = plan.secretStoreAutoCreates;
assert.ok(create);
assert.equal(create.sourceKey, '@dev-generate base64 32');
assert.notEqual(create.value, 'plaintext-source');
assert.equal(Buffer.from(create.value, 'base64').length, 32);
});
} finally {
repo.cleanup();
}
});

test('applies generation only to the annotated Secrets Store binding', () => {
const repo = createGitTokenServiceRepo({
rootSecretBinding: `{
"binding": "SHARED_SECRET",
"store_id": "shared-store",
// @dev-generate base64 32
"secret_name": "SHARED_SECRET"
}`,
devSecretBinding: `{
"binding": "SHARED_SECRET",
"store_id": "shared-store",
"secret_name": "SHARED_SECRET"
}`,
});
try {
withFakePnpm('', () => {
const plan = computePlan(repo.root, new Set(['cloudflare-git-token-service']));
assert.deepEqual(plan.secretStoreAutoCreates, []);
assert.deepEqual(plan.secretStoreWarnings, [
{
workerDir: 'services/git-token-service',
bindings: [
{
binding: 'SHARED_SECRET',
store_id: 'shared-store',
secret_name: 'SHARED_SECRET',
},
],
},
]);
});
} finally {
repo.cleanup();
}
});

test('rejects malformed Secrets Store generation annotations', () => {
const repo = createGitTokenServiceRepo({
devSecretBinding: `{
"binding": "SCM_SESSION_CAPABILITY_ENCRYPTION_KEY",
"store_id": "store-id",
// @dev-generate base64 nope
"secret_name": "SCM_SESSION_CAPABILITY_ENCRYPTION_KEY_DEV"
}`,
});
try {
assert.throws(
() => computePlan(repo.root, new Set(['cloudflare-git-token-service'])),
/Invalid @dev-generate directive/
);
} finally {
repo.cleanup();
}
});

test('rejects reserved generated-secret metadata in source Wrangler config', () => {
const repo = createGitTokenServiceRepo({
devSecretBinding: `{
"binding": "SCM_SESSION_CAPABILITY_ENCRYPTION_KEY",
"store_id": "store-id",
"__kilo\\u005fdev_generated_base64_bytes_0": 4096,
"secret_name": "SCM_SESSION_CAPABILITY_ENCRYPTION_KEY_DEV"
}`,
});
try {
assert.throws(
() => computePlan(repo.root, new Set(['cloudflare-git-token-service'])),
/reserved for generated-secret metadata/
);
} finally {
repo.cleanup();
}
});

test('preserves an existing annotated local Secrets Store secret', () => {
const repo = createGitTokenServiceRepo({});
try {
withFakePnpm('SCM_SESSION_CAPABILITY_ENCRYPTION_KEY_DEV\n', () => {
const plan = computePlan(repo.root, new Set(['cloudflare-git-token-service']));
assert.deepEqual(plan.secretStoreAutoCreates, []);
assert.deepEqual(plan.secretStoreWarnings, []);
});
} finally {
repo.cleanup();
}
});

test('auto-creates event-service NEXTAUTH Secrets Store binding from .env.local', () => {
const repo = createRepo({
'.env.local': 'NEXTAUTH_SECRET=local-nextauth-secret\n',
Expand Down
138 changes: 133 additions & 5 deletions dev/local/env-sync/plan.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { spawnSync } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
Expand Down Expand Up @@ -219,12 +220,124 @@ function isProvidedByWranglerVars(
return entry.defaultValue === wranglerValue;
}

const DEV_GENERATED_BASE64_BYTES_FIELD_PREFIX = '__kilo_dev_generated_base64_bytes_';
const MAX_DEV_GENERATED_SECRET_BYTES = 1024;

function injectDevSecretGenerationMetadata(content: string): string {
let directiveIndex = 0;
const transformed = content.replace(
/^(\s*)\/\/\s*@dev-generate([^\r\n]*)$/gm,
(_line, indent, args: string) => {
const directive = args.trim().match(/^base64\s+(\d+)$/);
if (!directive) {
throw new Error(
'Invalid @dev-generate directive; expected `// @dev-generate base64 <bytes>`'
);
}

const bytes = Number(directive[1]);
if (!Number.isSafeInteger(bytes) || bytes < 1 || bytes > MAX_DEV_GENERATED_SECRET_BYTES) {
throw new Error(
`@dev-generate bytes must be between 1 and ${MAX_DEV_GENERATED_SECRET_BYTES}`
);
}

return `${indent}"${DEV_GENERATED_BASE64_BYTES_FIELD_PREFIX}${directiveIndex++}": ${bytes},`;
}
);

if (/\/\/\s*@dev-generate/.test(transformed)) {
throw new Error('@dev-generate must be a standalone comment inside a Secrets Store binding');
}
return transformed;
}

function rejectReservedDevSecretGenerationMetadata(value: unknown): void {
if (Array.isArray(value)) {
for (const item of value) rejectReservedDevSecretGenerationMetadata(item);
return;
}
if (!isJsonObject(value)) return;

if (Object.keys(value).some(key => key.startsWith(DEV_GENERATED_BASE64_BYTES_FIELD_PREFIX))) {
throw new Error('@dev-generate field prefix is reserved for generated-secret metadata');
}
for (const nestedValue of Object.values(value))
rejectReservedDevSecretGenerationMetadata(nestedValue);
}

function getDevGeneratedBase64Bytes(value: JsonObject): number | undefined {
const metadata = Object.entries(value).filter(([key]) =>
key.startsWith(DEV_GENERATED_BASE64_BYTES_FIELD_PREFIX)
);
if (metadata.length === 0) return undefined;
if (metadata.length > 1) {
throw new Error('A Secrets Store binding can have only one @dev-generate directive');
}

const bytes = metadata[0]?.[1];
if (
typeof bytes !== 'number' ||
!Number.isSafeInteger(bytes) ||
bytes < 1 ||
bytes > MAX_DEV_GENERATED_SECRET_BYTES
) {
throw new Error('Invalid @dev-generate metadata');
}
return bytes;
}

function validateDevSecretGenerationMetadata(value: unknown): void {
if (Array.isArray(value)) {
for (const item of value) validateDevSecretGenerationMetadata(item);
return;
}
if (!isJsonObject(value)) return;

if (getDevGeneratedBase64Bytes(value) !== undefined) {
if (
typeof value.binding !== 'string' ||
typeof value.store_id !== 'string' ||
typeof value.secret_name !== 'string'
) {
throw new Error('@dev-generate must annotate a Secrets Store binding object');
}
}
for (const nestedValue of Object.values(value)) validateDevSecretGenerationMetadata(nestedValue);
}

function extractSecretsStoreBindings(repoRoot: string, workerDir: string): SecretStoreBinding[] {
const config = readWranglerConfig(repoRoot, workerDir);
if (!config) return [];
const wranglerPath = path.join(repoRoot, workerDir, 'wrangler.jsonc');
if (!fs.existsSync(wranglerPath)) return [];

const content = fs.readFileSync(wranglerPath, 'utf-8');
try {
const originalConfig = parseJsonc(content);
if (!isJsonObject(originalConfig)) return [];
rejectReservedDevSecretGenerationMetadata(originalConfig);
} catch (error) {
if (error instanceof Error && error.message.includes('@dev-generate')) throw error;
return [];
}

let annotatedConfig: JsonObject;
try {
const parsed = parseJsonc(injectDevSecretGenerationMetadata(content));
if (!isJsonObject(parsed)) return [];
validateDevSecretGenerationMetadata(parsed);
annotatedConfig = parsed;
} catch (error) {
if (error instanceof Error && error.message.includes('@dev-generate')) throw error;
if (content.includes('@dev-generate')) {
throw new Error('@dev-generate must annotate a Secrets Store binding object', {
cause: error,
});
}
return [];
}

const envName = detectWranglerEnv(repoRoot, workerDir);
const secretsSection = getWranglerSection(config, envName, 'secrets_store_secrets');
const secretsSection = getWranglerSection(annotatedConfig, envName, 'secrets_store_secrets');
if (!Array.isArray(secretsSection)) return [];

const bindings: SecretStoreBinding[] = [];
Expand All @@ -240,7 +353,13 @@ function extractSecretsStoreBindings(repoRoot: string, workerDir: string): Secre
) {
continue;
}
bindings.push({ binding, store_id: storeId, secret_name: secretName });
const generatedBytes = getDevGeneratedBase64Bytes(secret);
bindings.push({
binding,
store_id: storeId,
secret_name: secretName,
...(typeof generatedBytes === 'number' ? { devGeneratedBase64Bytes: generatedBytes } : {}),
});
}
return bindings;
}
Expand Down Expand Up @@ -570,8 +689,17 @@ function computePlan(repoRoot: string, serviceFilter?: Set<string>): EnvSyncPlan
continue; // Secret exists, nothing to do
}

const source = resolveSecretStoreSource(b.secret_name, envLocal, localSecretSources);
if (b.devGeneratedBase64Bytes) {
secretStoreAutoCreates.push({
workerDir: svc.dir,
binding: b,
sourceKey: `@dev-generate base64 ${b.devGeneratedBase64Bytes}`,
value: randomBytes(b.devGeneratedBase64Bytes).toString('base64'),
});
continue;
}

const source = resolveSecretStoreSource(b.secret_name, envLocal, localSecretSources);
if (source) {
// Can auto-create from .env.local or another local worker's dev vars.
secretStoreAutoCreates.push({
Expand Down
1 change: 1 addition & 0 deletions dev/local/env-sync/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type SecretStoreBinding = {
binding: string;
store_id: string;
secret_name: string;
devGeneratedBase64Bytes?: number;
};

type SecretStoreWarning = {
Expand Down
12 changes: 10 additions & 2 deletions services/cloud-agent-next/Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ARG KILOCODE_CLI_VERSION="7.3.12"
#
# This builds kilo-cli from source and copies the linux-x64 binary here.

# Install latest stable git + git-lfs from the git-core PPA, plus supporting CLI tools,
# Install latest stable git + git-lfs from the git-core PPA, GitHub CLI, plus supporting CLI tools,
# and generate locales to suppress setlocale warnings.
# The default Ubuntu git (2.34.1 on 22.04) is outdated; the git-core PPA ships the latest
# stable release. git-lfs is installed from the same PPA so it stays in lockstep with git,
Expand All @@ -21,12 +21,20 @@ RUN set -eux; \
apt-get install -y --no-install-recommends \
ca-certificates \
gnupg \
software-properties-common; \
software-properties-common \
wget; \
add-apt-repository -y ppa:git-core/ppa; \
mkdir -p -m 755 /etc/apt/keyrings; \
wget -nv -O /etc/apt/keyrings/githubcli-archive-keyring.gpg https://cli.github.com/packages/githubcli-archive-keyring.gpg; \
chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg; \
mkdir -p -m 755 /etc/apt/sources.list.d; \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
| tee /etc/apt/sources.list.d/github-cli.list > /dev/null; \
apt-get update; \
apt-get install -y --no-install-recommends \
git \
git-lfs \
gh \
jq \
locales \
openssh-client \
Expand Down
2 changes: 1 addition & 1 deletion services/cloud-agent-next/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { default } from './server.js';
export { Sandbox, Sandbox as SandboxSmall, Sandbox as SandboxDIND } from '@cloudflare/sandbox';
export { Sandbox, SandboxSmall, SandboxDIND, ContainerProxy } from './sandbox-outbound.js';
export { CloudAgentSession } from './persistence/CloudAgentSession.js';
export { UserKiloFacade } from './kilo-facade/user-kilo-facade.js';
Loading