Skip to content
Closed
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
34 changes: 34 additions & 0 deletions services/platform/tests/integration/container-smoke-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,40 @@ async function main(): Promise<number> {

// Platform: Vite server with platform-ready marker.
const platformContainer = await compose.containerName('platform');

// The deployment control channel is docker-exec localhost, never public.
// The plain edge refusal distinguishes it from a backend token refusal.
for (const path of [
'/api/control',
'/api/control/',
'/api/control/drain-status?probe=1',
'/api/%63ontrol/drain-status',
]) {
const probe = await capture([
'docker',
'exec',
platformContainer,
// nosemgrep: tools.opengrep.rules.trailofbits.generic.curl-insecure.curl-insecure -- Isolated smoke network and self-signed proxy; no credentials or external traffic.
'curl',
'--insecure',
'--silent',
'--show-error',
'--max-time',
'10',
'--connect-to',
'localhost:443:proxy:443',
'--write-out',
'\n%{http_code}',
`https://localhost${path}`,
]);
const label = `Public proxy refuses deployment control ${path}`;
if (probe.exitCode === 0 && probe.stdout === 'Not found\n404') {
r.pass(label);
} else {
r.fail(label);
}
}

const viteCode = await httpStatus('http://localhost:13000/api/health', 10);
if (viteCode === '200') {
r.pass(`Platform /api/health: HTTP ${viteCode}`);
Expand Down
1 change: 1 addition & 0 deletions services/platform/tests/manual/reference/automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ Legend: ✅ fully automated · 🔶 partially automated · ⛔ manual-only (no s
| [navigation](../suites/navigation.md) | Optional analytics: runtime disablement, safe SPA pageviews, private route templates, DNT/GPC and collector boundary | ✅ automated | `packages/ui/src/analytics/browser.test.ts`, `packages/ui/src/analytics/server.test.ts` |
| [navigation](../suites/navigation.md) | `NAV-B12` (the logic half): a read TanStack Query cancels when a navigation unmounts its page rejects as an `AbortError` that never flags the backend unreachable, so the offline overlay stays down; an event whose thrown value is an `AbortError` — also in the SDK's `AbortError: …` and stringified `Error: AbortError: …` shapes — is dropped before it is sent, while a failure merely caused by one is kept | 🔶 unit + component | `app/lib/backend/api-client.test.ts`, `app/components/connectivity/online-gate.test.tsx` (a real `QueryClient` read cancelled by an unmount, under the real connection state), `app/lib/sentry-normalize.test.ts`, `lib/utils/abort-error.test.ts`; the throttled real-browser round with a live DSN stays manual |
| [navigation](../suites/navigation.md) | `NAV-B14`: the error displays' **contact support** link follows `TALE_CONTACT_SUPPORT_URL` from the environment through `window.__ENV__` and the app-root `SupportUrlProvider` to the rendered link, which opens in a new tab with the organization as `organizationId` (replacing one the URL carries). The web tier hands the page the value only as an absolute http(s) URL, verbatim and script-safe, and leaves anything else out with a warning, so the link keeps `https://tale.dev/contact`; a display's own `supportUrl` wins over the provider | ✅ e2e + component + server | `tests/e2e/specs/navigation.spec.ts` (*the contact-support link opens the operator's page*), `packages/ui/src/components/error-boundaries/core/support-url.test.tsx`, `packages/ui/src/components/error-boundaries/displays/error-display-compact.test.tsx`, `packages/ui/src/components/error-boundaries/displays/global-error-display.test.tsx`, `lib/contact-support-url.test.ts`, `lib/utils/inline-script.test.ts`, `vite-plugins/inject-env.test.ts`, `server.test.ts` |
| [settings](../suites/settings.md) | Public deployment-control paths are refused by the proxy while the CLI keeps its authenticated in-container channel | ✅ automated | `tests/integration/container-smoke-test.ts` (root, descendants, query and encoded path through the real proxy), `tools/cli/src/lib/compose/services/compose-parity.test.ts`, `tools/cli/src/lib/docker/control-call.test.ts` |
| [settings](../suites/settings.md) | Foreground egress proxy boots, stops gracefully and restarts with read-only `/tmp` | ✅ automated | `tests/integration/container-smoke-test.ts` and `compose.test.yml` (real proxy health, refused temporary-file creation, shutdown exit/log and same-container restart) |
| [settings](../suites/settings.md) | Sandbox memory lifetime: completed commands release request/consumer data while a sibling remains active, and disposed replay releases its opaque checkpoint state; held-open stdin refuses whole lines above its 8 MiB pending queue and accepts again after draining; HTTP intake and device tunnel completion detach their listeners | 🔶 unit + real processes + HTTP | `services/sandbox-runtime/daemon/src/exec-manager.memory.test.ts` (bundled manager under Node), `exec-manager.test.ts`, `http-body.test.ts`, `main.test.ts`; `services/sandbox/src/devices/tunnel.test.ts`; production workload memory profiling remains operational |
| [settings](../suites/settings.md) | The egress proxy serves `SANDBOX_EGRESS_MAX_CLIENTS` connections at once (2000 by default) for the whole fleet, keeps its 600 s tunnel timeout, refuses to start on a value that is no whole number above 0, raises its soft open-file limit to two descriptors per connection as far as the hard limit allows (a Kubernetes Pod sets none) and leaves a limit that already holds them alone, and warns when even the hard limit cannot hold them; both compose pipelines pass the same default and size the container's pids and open-file limits for it | ✅ automated | `services/sandbox/src/egress-proxy-config.test.ts` (the real entrypoint and template), `tools/cli/src/lib/compose/services/compose-parity.test.ts` (`egress connection capacity parity`) |
Expand Down
12 changes: 9 additions & 3 deletions services/proxy/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,8 @@ sed -i "s|{[\$]DOCS_ORIGIN:[^}]*}|${DOCS_URL}|" "$CADDYFILE"
# Everything the pg backend owns is listed here explicitly — auth, the app
# API, the hint stream, both machine doors, SSO/SCIM/trusted-headers on BOTH
# their 0.5-native and 0.4 `/http_api/...` paths (registered IdP redirect
# URIs carry the old ones), the control channel the CLI drains through, the
# cloud-import OAuth callbacks and the WebDAV protocol door.
# URIs carry the old ones), the cloud-import OAuth callbacks and the WebDAV
# protocol door. Deployment control stays internal to the backend container.
#
# BACKEND_UPSTREAM began life as the cutover's reversibility switch (unset ⇒
# lanes fall back to Convex). The Convex runtime is gone, so an unset value
Expand Down Expand Up @@ -338,8 +338,14 @@ BACKEND_BLOCK=$(cat <<EOF
}
reverse_proxy ${BACKEND_UPSTREAM}
}
# The CLI calls localhost inside the selected backend container. Refuse
# both the root and descendants here: removing this lane would expose
# them again through the generic /api/* fallback in the base Caddyfile.
handle /api/control {
respond "Not found" 404
}
handle /api/control/* {
reverse_proxy ${BACKEND_UPSTREAM}
respond "Not found" 404
}
handle /api/sso/* {
reverse_proxy ${BACKEND_UPSTREAM}
Expand Down
19 changes: 19 additions & 0 deletions tools/cli/src/lib/compose/services/compose-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,25 @@ describe('blob-backend parity (the deployment cannot accept an upload without it
expect(entrypoint).not.toContain('-n "${BACKEND_UPSTREAM');
});

test('public control paths are refused before the generic API fallback', () => {
const entrypoint = readFileSync(
resolve(repoRoot, 'services/proxy/docker-entrypoint.sh'),
'utf8',
);
for (const path of ['/api/control', '/api/control/*']) {
const handle = entrypoint.split(`handle ${path} {`)[1]?.split('\n\t}')[0];
expect(handle).toBeDefined();
expect(handle).toContain('respond "Not found" 404');
expect(handle).not.toContain('reverse_proxy');
}
// Removing the explicit control route alone leaves /api/* forwarding it.
const caddyfile = readFileSync(
resolve(repoRoot, 'services/proxy/Caddyfile'),
'utf8',
);
expect(caddyfile).toContain('handle /api/* {');
});

test('nothing routes to the retired runtime any more', () => {
// The proxy used to fall back to `convex:*` for everything the backend
// list did not name. That service is gone, so a fallback is a 502 — every
Expand Down
Loading