Skip to content
Merged
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
6 changes: 6 additions & 0 deletions docs/operations/foundation-ci-supply-chain-2026-08-02.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ Hosted CI remains authoritative for the complete dependency, SAST, container,
OpenTofu, build, and release-environment gates. The generators never write
runtime evidence inside the repository during these checks.

The `release` environment's required reviewers and branch restrictions are
GitHub repository settings rather than workflow YAML. Before promoting to
`main`, an administrator must verify those settings and record the check in
the release evidence; the repository policy checker deliberately verifies the
workflow's environment reference but cannot infer external protection rules.

## Rollback

Revert the focused CI or generator commit that introduced the behavior, rerun
Expand Down
9 changes: 6 additions & 3 deletions docs/operations/foundation-local-infrastructure-2026-08-02.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ Task: `FND-003 — Close local infrastructure gaps`
check, providing the entry point for persistence evidence.
- `persistence-check` writes a five-minute Redis sentinel, restarts only Redis,
verifies the sentinel, and deletes it; it never flushes a database or volume.
- The documented lifecycle command set includes `config`, `preflight`, `check`,
`start`, `stop`, `reset`, `restart-check`, `persistence-check`, `status`,
`logs`, and the legacy `smoke` entry point.

## Verification

Expand All @@ -32,7 +35,6 @@ Passed:
- `node tools/repo-cli/src/local-services-smoke.mjs --help`
- `node tools/repo-cli/src/local-services.mjs config`
- `node tools/repo-cli/src/local-services.mjs preflight --min-free-gib=0`
- `node tools/repo-cli/src/local-services.mjs persistence-check` *(Docker-gated)*
- `git diff --check`

Environment-gated:
Expand All @@ -43,8 +45,9 @@ Environment-gated:
without starting containers; the evidence run used a zero-GiB threshold so
it remains independent of the workstation's available disk headroom.
- Live `compose up`, health polling, port-collision simulation, disk-pressure
threshold validation, and restart-persistence checks must run on a machine
with Docker Desktop/Compose v2 before FND-003 can become `verified`.
threshold validation, and restart-persistence checks (including
`persistence-check`) must run on a machine with Docker Desktop/Compose v2
before FND-003 can become `verified`.

## Rollback

Expand Down
14 changes: 14 additions & 0 deletions infrastructure/aws/modules/compute/main.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
locals {
common_tags = merge(var.tags, { Component = "compute" })
allowed_worker_memory_by_cpu = {
"256" = [512, 1024, 2048]
"512" = [1024, 2048, 3072, 4096]
"1024" = [2048, 3072, 4096, 5120, 6144, 7168, 8192]
"2048" = [4096, 5120, 6144, 7168, 8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384]
"4096" = [8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384, 17408, 18432, 19456, 20480, 21504, 22528, 23552, 24576, 25600, 26624, 27648, 28672, 29696, 30720]
}
}

resource "aws_ecs_cluster" "this" {
Expand Down Expand Up @@ -151,6 +158,13 @@ resource "aws_ecs_task_definition" "worker" {
condition = var.environment != "production" || can(regex("@sha256:[0-9a-f]{64}$", var.worker_image))
error_message = "Production worker deployments must use an immutable image digest."
}
precondition {
condition = contains(
lookup(local.allowed_worker_memory_by_cpu, tostring(var.worker_cpu), []),
var.worker_memory,
)
error_message = "worker_memory must be an AWS-supported Fargate size for worker_cpu."
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions infrastructure/local/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ named volumes prefixed by the Compose project name; no repository directory is
mounted for database, object, or mail data. The volumes are disposable and are
not removed by the lifecycle commands. Remove the named volumes only when you
explicitly want to discard local state.
Every published port is bound to `127.0.0.1`, so the development credentials and
data endpoints are not reachable from other hosts on the local network.
Container JSON logs are capped at 10 MiB per file with three retained files so
diagnostics cannot silently consume the host disk.

Expand Down
18 changes: 9 additions & 9 deletions infrastructure/local/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ services:
POSTGRES_USER: ${POSTGRES_USER:-databreeze}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-databreeze-local-change-me}
ports:
- '${POSTGRES_PORT:-5432}:5432'
- '127.0.0.1:${POSTGRES_PORT:-5432}:5432'
networks: [local]
logging: *default-logging
volumes:
Expand All @@ -36,7 +36,7 @@ services:
init: true
command: ['redis-server', '--appendonly', 'yes']
ports:
- '${REDIS_PORT:-6379}:6379'
- '127.0.0.1:${REDIS_PORT:-6379}:6379'
networks: [local]
logging: *default-logging
volumes:
Expand All @@ -56,8 +56,8 @@ services:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-databreeze}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-databreeze-local-change-me}
ports:
- '${MINIO_API_PORT:-9000}:9000'
- '${MINIO_CONSOLE_PORT:-9001}:9001'
- '127.0.0.1:${MINIO_API_PORT:-9000}:9000'
- '127.0.0.1:${MINIO_CONSOLE_PORT:-9001}:9001'
networks: [local]
logging: *default-logging
volumes:
Expand Down Expand Up @@ -95,8 +95,8 @@ services:
MP_MAX_MESSAGES: ${MAILPIT_MAX_MESSAGES:-5000}
MP_DATABASE: /data/mailpit.db
ports:
- '${MAILPIT_SMTP_PORT:-1025}:1025'
- '${MAILPIT_UI_PORT:-8025}:8025'
- '127.0.0.1:${MAILPIT_SMTP_PORT:-1025}:1025'
- '127.0.0.1:${MAILPIT_UI_PORT:-8025}:8025'
networks: [local]
logging: *default-logging
volumes:
Expand All @@ -113,9 +113,9 @@ services:
init: true
command: ['--config=/etc/otelcol-contrib/config.yaml']
ports:
- '${OTEL_GRPC_PORT:-4317}:4317'
- '${OTEL_HTTP_PORT:-4318}:4318'
- '${OTEL_HEALTH_PORT:-13133}:13133'
- '127.0.0.1:${OTEL_GRPC_PORT:-4317}:4317'
- '127.0.0.1:${OTEL_HTTP_PORT:-4318}:4318'
- '127.0.0.1:${OTEL_HEALTH_PORT:-13133}:13133'
networks: [local]
logging: *default-logging
volumes:
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"prettier": "3.6.2",
"turbo": "2.5.6",
"typescript": "5.9.2",
"typescript-eslint": "8.43.0"
"typescript-eslint": "8.43.0",
"yaml": "2.8.1"
}
}
75 changes: 57 additions & 18 deletions packages/telemetry/src/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,25 @@ function safeScalar(key: string, value: unknown): TelemetryScalarV1 | undefined
return safeString(key, value);
}

function ownDataEntries(input: Record<string, unknown>): Array<[string, unknown]> {
function readOwnDataEntries(input: Record<string, unknown>): {
entries: Array<[string, unknown]>;
readable: boolean;
} {
const entries: Array<[string, unknown]> = [];
for (const key of Object.keys(input)) {
const descriptor = Object.getOwnPropertyDescriptor(input, key);
if (descriptor && 'value' in descriptor) entries.push([key, descriptor.value]);
try {
for (const key of Object.keys(input)) {
const descriptor = Object.getOwnPropertyDescriptor(input, key);
if (!descriptor || !('value' in descriptor)) return { entries: [], readable: false };
entries.push([key, descriptor.value]);
}
} catch {
return { entries: [], readable: false };
}
return entries;
return { entries, readable: true };
}

function ownDataEntries(input: Record<string, unknown>): Array<[string, unknown]> {
return readOwnDataEntries(input).entries;
}

export function sanitizeTelemetryAttributesV1(
Expand All @@ -192,17 +204,14 @@ export function sanitizeTelemetryAttributesV1(
export function assertSafeTelemetryAttributesV1(
input: Record<string, unknown>,
): asserts input is SafeTelemetryAttributesV1 {
for (const key of Object.keys(input ?? {})) {
const readable = readOwnDataEntries(input ?? {});
if (!readable.readable) throw new UnsafeTelemetryAttributeErrorV1('unreadable');
for (const [key, value] of readable.entries) {
assertBoundedKey(key);
if (forbiddenKeyPattern.test(key) || !safeAttributeSet.has(key)) {
throw new UnsafeTelemetryAttributeErrorV1(key);
}
const descriptor = Object.getOwnPropertyDescriptor(input, key);
if (
!descriptor ||
!('value' in descriptor) ||
safeScalar(key, descriptor.value) === undefined
) {
if (safeScalar(key, value) === undefined) {
throw new UnsafeTelemetryAttributeErrorV1(key);
}
}
Expand Down Expand Up @@ -263,18 +272,48 @@ function readSingleHeader(
name: string,
): string | undefined {
const values: string[] = [];
for (const key of Object.keys(headers)) {
const descriptor = Object.getOwnPropertyDescriptor(headers, key);
let keys: string[];
try {
keys = Object.keys(headers);
} catch {
throw new Error(`Unreadable telemetry ${name} header`);
}
for (const key of keys) {
let descriptor: PropertyDescriptor | undefined;
try {
descriptor = Object.getOwnPropertyDescriptor(headers, key);
} catch {
throw new Error(`Unreadable telemetry ${name} header`);
}
if (!descriptor || !('value' in descriptor))
throw new Error(`Unreadable telemetry ${name} header`);
const value = descriptor.value as string | string[] | undefined;
if (key.toLowerCase() !== name) continue;
if (Array.isArray(value)) {
if (!value.every((item) => typeof item === 'string')) {
let arrayValue = false;
try {
arrayValue = Array.isArray(value);
} catch {
throw new Error(`Unreadable telemetry ${name} header`);
}
if (arrayValue) {
let valid = false;
try {
valid = Array.prototype.every.call(value, (item: unknown) => typeof item === 'string');
} catch {
throw new Error(`Unreadable telemetry ${name} header`);
}
if (!valid) {
throw new Error(`Unreadable telemetry ${name} header`);
}
values.push(...value);
} else if (value !== undefined) values.push(value);
try {
values.push(...(value as string[]));
} catch {
throw new Error(`Unreadable telemetry ${name} header`);
}
} else if (value !== undefined) {
if (typeof value !== 'string') throw new Error(`Unreadable telemetry ${name} header`);
values.push(value);
}
}
if (values.length > 1) throw new Error(`Ambiguous telemetry ${name} header`);
if (values.length === 0) return undefined;
Expand Down
30 changes: 30 additions & 0 deletions packages/telemetry/test/telemetry-v1.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,32 @@ test('telemetry never executes accessor-backed correlation headers', () => {
assert.equal(accessed, false);
});

test('telemetry rejects proxies that fail during reflection without exposing trap errors', () => {
const hostileAttributes = new Proxy(
{},
{
ownKeys() {
throw new Error('attribute trap cause');
},
},
);
assert.deepEqual(sanitizeTelemetryAttributesV1(hostileAttributes), {});
assert.throws(
() => assertSafeTelemetryAttributesV1(hostileAttributes),
UnsafeTelemetryAttributeErrorV1,
);

const hostileHeaders = new Proxy(
{},
{
ownKeys() {
throw new Error('header trap cause');
},
},
);
assert.throws(() => correlationFromHeadersV1(hostileHeaders), /Unreadable telemetry/u);
});

test('correlation headers round-trip without accepting malformed identifiers', () => {
const context = createCorrelationContextV1({
correlationId,
Expand Down Expand Up @@ -118,6 +144,10 @@ test('correlation headers round-trip without accepting malformed identifiers', (
}),
);
assert.throws(() => correlationFromHeadersV1({}));
assert.throws(
() => correlationFromHeadersV1({ 'x-correlation-id': 1 }),
/Unreadable telemetry x-correlation-id header/u,
);
assert.throws(() =>
correlationFromHeadersV1({ 'x-correlation-id': [correlationId, correlationId] }),
);
Expand Down
Loading
Loading