Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0cc35cc
chore(porch): builder-task-nhnj init pir
amrmelsayed Aug 12, 2026
aabb5bc
[PIR] Plan draft: enforce request authentication on the Tower API
amrmelsayed Aug 12, 2026
e1a6c9b
chore(porch): secfix-1 init pir
amrmelsayed Aug 12, 2026
2d71312
[PIR] Rename plan to porch-native id secfix-1
amrmelsayed Aug 12, 2026
1e53baa
chore(porch): secfix-1 plan-approval gate-requested
amrmelsayed Aug 12, 2026
5f10f72
chore(porch): secfix-1 plan-approval gate-approved
amrmelsayed Aug 12, 2026
edab49d
chore(porch): secfix-1 implement phase-transition
amrmelsayed Aug 12, 2026
e618126
[PIR] types: add request-authentication wire contracts
amrmelsayed Aug 12, 2026
37814ee
[PIR] server: enforce request authentication on the Tower API
amrmelsayed Aug 12, 2026
3a882b3
[PIR] clients: transport the shared key on HTTP and WebSocket
amrmelsayed Aug 12, 2026
b0d711f
[PIR] tests: request-authentication enforcement
amrmelsayed Aug 12, 2026
0c64c19
[PIR] thread: implement-phase progress + results
amrmelsayed Aug 12, 2026
40a0809
[PIR] types: centralize the terminal WS subprotocol builder
amrmelsayed Aug 12, 2026
5f4cef9
[PIR] server: same-origin key delivery, Host guard, CORS shell isolation
amrmelsayed Aug 12, 2026
f98f42e
[PIR] clients: web dashboard + annotator key transport
amrmelsayed Aug 12, 2026
9761070
[PIR] tests: Host guard + annotator allowlist coverage
amrmelsayed Aug 12, 2026
4f29b24
[PIR] thread: iteration-2 (cmap findings) progress
amrmelsayed Aug 12, 2026
19ae501
[PIR] server: bridge/tunnel Host handling, injection safety, diagnostics
amrmelsayed Aug 12, 2026
c7ebde9
[PIR] vscode: authenticate the SSE client
amrmelsayed Aug 12, 2026
0220b46
[PIR] annotator: vendor three.js locally; template hardening
amrmelsayed Aug 12, 2026
34f259a
[PIR] tests: bridge Host, no-slash workspace, SSE-stream + mock updates
amrmelsayed Aug 12, 2026
32dc434
[PIR] harden: narrow bridge Host to IP-literals; encode annotator XSS…
amrmelsayed Aug 12, 2026
aa2a718
[PIR] thread: iteration-4 + reaching dev-approval
amrmelsayed Aug 12, 2026
2bc7512
chore(porch): secfix-1 dev-approval gate-requested
amrmelsayed Aug 12, 2026
f0f4afe
[PIR] fix: make @cluesmith/codev-types a runtime dependency of codev
amrmelsayed Aug 12, 2026
dad963a
[PIR] thread: boot-regression root cause + dependency fix
amrmelsayed Aug 12, 2026
e8795de
[PIR] fix(local-install): pack + install @cluesmith/codev-types
amrmelsayed Aug 12, 2026
0ff32b6
[PIR] fix: authenticate the Tower startup readiness probe
amrmelsayed Aug 12, 2026
11978b5
[PIR] rename the auth header codev-web-key -> codev-tower-key (dual-a…
amrmelsayed Aug 12, 2026
aa2d6c9
[PIR] thread: readiness-probe fix + auth header rename
amrmelsayed Aug 12, 2026
f36a134
[PIR] chore(vscode): satisfy eslint curly rule in tunnel.ts
amrmelsayed Aug 12, 2026
2466756
chore(porch): secfix-1 dev-approval gate-approved
amrmelsayed Aug 12, 2026
03a5705
chore(porch): secfix-1 review phase-transition
amrmelsayed Aug 12, 2026
3892bd6
[PIR] Review + retrospective: Tower request-authentication hardening
amrmelsayed Aug 12, 2026
434b0a4
chore(porch): secfix-1 record PR #1421
amrmelsayed Aug 12, 2026
6b631b2
chore(porch): secfix-1 review build-complete
amrmelsayed Aug 12, 2026
d67c419
[PIR] review: address 3-way consult (docs + WS echo/vscode tests)
amrmelsayed Aug 12, 2026
c37312c
chore(porch): secfix-1 pr gate-requested
amrmelsayed Aug 12, 2026
acb8285
[PIR] fix(ci): pack + install codev-types; authenticate Tower e2e suites
amrmelsayed Aug 12, 2026
9b35ac1
[PIR] refactor(3d-viewer): vendor three.js at build time, not in git
amrmelsayed Aug 12, 2026
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
14 changes: 13 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,14 @@ jobs:
working-directory: packages/core
run: pnpm build

# codev-types is a runtime dependency of codev (server-utils imports its
# wire constants at module load), so the install-verification install must
# include its tarball too — else `afx --help` crashes at boot with
# "Cannot find module '@cluesmith/codev-types'".
- name: Build types package
working-directory: packages/types
run: pnpm build

- name: Build package
working-directory: packages/codev
run: pnpm build
Expand All @@ -250,11 +258,15 @@ jobs:
working-directory: packages/sdk
run: pnpm pack

- name: Pack types tarball
working-directory: packages/types
run: pnpm pack

- name: Pack tarball
working-directory: packages/codev
run: pnpm pack

- name: Verify install from tarball
working-directory: packages/codev
run: node scripts/verify-install.mjs cluesmith-codev-*.tgz ../core/cluesmith-codev-core-*.tgz ../sdk/cluesmith-codev-sdk-*.tgz
run: node scripts/verify-install.mjs cluesmith-codev-*.tgz ../core/cluesmith-codev-core-*.tgz ../sdk/cluesmith-codev-sdk-*.tgz ../types/cluesmith-codev-types-*.tgz

5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ test-results/
packages/codev/dashboard-dist/
apps/web/dist/

# three.js vendored at build time from the `three` devDependency (copy-three.mjs);
# copied into templates/vendor/ and shipped in the npm tarball, not committed.
packages/codev/templates/vendor/three.module.js
packages/codev/templates/vendor/three-*.js

# Checklister runtime state (per-project JSON files)
codev/checklists/*.json

Expand Down
30 changes: 28 additions & 2 deletions apps/vscode/src/__tests__/terminal-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ vi.mock('ws', () => {
closed = false;
sent: unknown[] = [];
private handlers: Record<string, Array<(...args: unknown[]) => void>> = {};
constructor(public url: string) { FakeWebSocket.instances.push(this); }
constructor(public url: string, public protocols?: string | string[]) { FakeWebSocket.instances.push(this); }
on(event: string, cb: (...args: unknown[]) => void): this {
(this.handlers[event] ||= []).push(cb);
return this;
Expand All @@ -64,7 +64,12 @@ vi.mock('@cluesmith/codev-sdk/escape-buffer', () => ({
},
}));

vi.mock('@cluesmith/codev-types', () => ({ FRAME_CONTROL: 0x00, FRAME_DATA: 0x01 }));
vi.mock('@cluesmith/codev-types', () => ({
FRAME_CONTROL: 0x00,
FRAME_DATA: 0x01,
terminalWsProtocols: (key: string | null | undefined) =>
(key ? ['codev.tower.v1', `codev-key.${key}`] : undefined),
}));

const FRAME_CONTROL = 0x00;
const FRAME_DATA = 0x01;
Expand Down Expand Up @@ -127,12 +132,33 @@ function makeAdapter() {
return { pty, writes };
}

/** Build an adapter with an explicit auth key (default makeAdapter uses null). */
function makeAdapterWithKey(authKey: string | null) {
const pty = new (CodevPseudoterminal as unknown as new (
url: string, authKey: string | null, ch: unknown,
) => { open(d: unknown): void }) ('ws://localhost:4100/x', authKey, fakeOutputChannel());
pty.open(undefined);
return WebSocket.instances[WebSocket.instances.length - 1] as unknown as { protocols?: string | string[] };
}

beforeEach(() => {
vi.useFakeTimers();
WebSocket.instances.length = 0;
hoisted.escapeBufferCount = 0;
});

describe('WS subprotocol auth (advisory GHSA-xvjp-7748-v88v)', () => {
it('offers the marker + codev-key token when an auth key is present', () => {
const sock = makeAdapterWithKey('SECRETKEY');
expect(sock.protocols).toEqual(['codev.tower.v1', 'codev-key.SECRETKEY']);
});

it('offers no subprotocol when there is no auth key', () => {
const sock = makeAdapterWithKey(null);
expect(sock.protocols).toBeUndefined();
});
});

describe('PIR #936 — adapter-owned reconnect loop', () => {
it('emits one backed-off notice per close, capping the delay at 30s', () => {
const { writes } = makeAdapter();
Expand Down
2 changes: 1 addition & 1 deletion apps/vscode/src/commands/tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function disconnectTunnel(connectionManager: ConnectionManager): Pr
},
'Disconnect',
);
if (choice !== 'Disconnect') return;
if (choice !== 'Disconnect') {return;}

await client.signalTunnel('disconnect');
vscode.window.showInformationMessage('Codev: Tower deregistered from Codev Cloud');
Expand Down
1 change: 1 addition & 0 deletions apps/vscode/src/connection-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export class ConnectionManager {
this.scheduleReconnect();
}
},
() => this.auth.getKeySync(),
);
this.sse.onEvent((type, data) => {
this.sseEventEmitter.fire({ type, data });
Expand Down
11 changes: 8 additions & 3 deletions apps/vscode/src/sse-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as vscode from 'vscode';
import { TOWER_KEY_HEADER } from '@cluesmith/codev-types';

export type SSEListener = (eventType: string, data: string) => void;

Expand All @@ -19,6 +20,7 @@ export class SSEClient {
private baseUrl: string,
private outputChannel: vscode.OutputChannel,
private onDisconnect: () => void,
private getAuthKey: () => string | null = () => null,
) {}

/**
Expand Down Expand Up @@ -61,9 +63,12 @@ export class SSEClient {

private async startSSE(url: string): Promise<void> {
try {
const response = await fetch(url, {
headers: { 'Accept': 'text/event-stream' },
});
// Request authentication (advisory GHSA-xvjp-7748-v88v): /api/events is a
// key-required route; SSE via fetch can carry the codev-tower-key header.
const headers: Record<string, string> = { 'Accept': 'text/event-stream' };
const key = this.getAuthKey();
if (key) { headers[TOWER_KEY_HEADER] = key; }
const response = await fetch(url, { headers });

if (!response.ok || !response.body) {
this.log('WARN', `SSE connection failed: ${response.status}`);
Expand Down
13 changes: 7 additions & 6 deletions apps/vscode/src/terminal-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as vscode from 'vscode';
import WebSocket from 'ws';
import { FRAME_CONTROL, FRAME_DATA, type ControlMessage } from '@cluesmith/codev-types';
import { FRAME_CONTROL, FRAME_DATA, terminalWsProtocols, type ControlMessage } from '@cluesmith/codev-types';
import { EscapeBuffer } from '@cluesmith/codev-sdk/escape-buffer';
import { BackoffController, classifyUpgradeError } from '@cluesmith/codev-sdk/reconnect-policy';

Expand Down Expand Up @@ -180,7 +180,9 @@ export class CodevPseudoterminal implements vscode.Pseudoterminal {

const url = this.connectUrl();
this.log('INFO', `Connecting to ${url}`);
const socket = new WebSocket(url);
// Request authentication (advisory GHSA-xvjp-7748-v88v): the shared key
// travels as a Sec-WebSocket-Protocol subprotocol, validated at the upgrade.
const socket = new WebSocket(url, terminalWsProtocols(this.authKey));
this.ws = socket;
this.ws.binaryType = 'arraybuffer';

Expand All @@ -194,10 +196,9 @@ export class CodevPseudoterminal implements vscode.Pseudoterminal {
// Wipe any in-progress retry notice before replayed buffer / normal
// output resumes, so it doesn't orphan in scrollback (#1001).
this.clearReconnectNotice();
// Send auth via control message (not query param)
if (this.authKey) {
this.sendControl({ type: 'ping', payload: { auth: this.authKey } });
}
// Auth is now carried by the Sec-WebSocket-Protocol subprotocol and
// validated at the upgrade (advisory GHSA-xvjp-7748-v88v), so no in-band
// auth frame is sent here.
// Sync Tower's PTY to the dimensions VSCode reported. Without this,
// the PTY stays at node-pty's 80×24 default until a manual resize,
// which makes Claude Code's TUI render its input box mid-screen and
Expand Down
1 change: 1 addition & 0 deletions apps/web/__tests__/TabBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TabBar, TAB_ICONS } from '../src/components/TabBar.js';
import type { Tab } from '../src/hooks/useTabs.js';

vi.mock('../src/lib/api.js', () => ({
getWebKey: () => null,
deleteTab: vi.fn(() => Promise.resolve()),
}));

Expand Down
1 change: 1 addition & 0 deletions apps/web/__tests__/Terminal.clipboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ vi.mock('@xterm/addon-web-links', () => ({
const mockUploadPasteImage = vi.fn();
vi.mock('../src/lib/api.js', () => ({
uploadPasteImage: (...args: unknown[]) => mockUploadPasteImage(...args),
getWebKey: () => null,
}));

// Mock WebSocket as a class
Expand Down
1 change: 1 addition & 0 deletions apps/web/__tests__/analytics.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { AnalyticsResponse } from '../src/lib/api.js';
const mockFetchAnalytics = vi.fn<(range: string, refresh?: boolean) => Promise<AnalyticsResponse>>();

vi.mock('../src/lib/api.js', () => ({
getWebKey: () => null,
fetchAnalytics: (...args: unknown[]) => mockFetchAnalytics(...(args as [string, boolean?])),
}));

Expand Down
1 change: 1 addition & 0 deletions apps/web/__tests__/useOverview.stability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const mockFetchOverview = vi.fn<() => Promise<OverviewData>>();
const mockRefreshOverview = vi.fn<() => Promise<void>>();

vi.mock('../src/lib/api.js', () => ({
getWebKey: () => null,
fetchOverview: (...args: unknown[]) => mockFetchOverview(...(args as [])),
refreshOverview: (...args: unknown[]) => mockRefreshOverview(...(args as [])),
getSSEEventsUrl: () => 'http://localhost:0/api/events',
Expand Down
Loading
Loading