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
100 changes: 100 additions & 0 deletions apps/code/docs/AGENT_NATIVE_PROTOCOL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Codra Agent-Native Protocol Adoption

How Codra will adopt the Talocode Agent-Native Protocol for safe, audited agent operations.

## Current State

Codra Code is a CLI-based AI coding agent. It currently operates through:

- Thread-based conversations
- File read/write operations
- Terminal command execution
- Git operations
- Model relay to external providers
- Plan-based task execution

## Planned Codra Actions

### Read-Only Actions (No Approval Required)

| Action | Risk | Description |
|--------|------|-------------|
| `codra.plan.create` | read | Create an execution plan |
| `codra.thread.resume` | read | Resume a conversation thread |
| `codra.context.compress` | read | Compress context for token efficiency |
| `codra.file.read` | read | Read file contents |
| `codra.git.status` | read | Check git repository status |
| `codra.git.diff` | read | View file differences |

### Write Actions (Approval Required)

| Action | Risk | Description |
|--------|------|-------------|
| `codra.plan.run` | medium | Execute an approved plan |
| `codra.file.write` | medium | Write or modify file contents |
| `codra.git.commit` | medium | Create a git commit |
| `codra.command.run` | high | Execute terminal commands |

### Destructive Actions (Unsupported)

- `codra.git.force_push` — not supported
- `codra.file.delete` — not supported
- `codra.database.drop` — not supported

## Context Providers

| Provider | Privacy | Description |
|----------|---------|-------------|
| `codra.thread` | workspace | Current conversation thread |
| `codra.plan` | workspace | Current execution plan |
| `codra.files` | workspace | File tree and contents |
| `codra.git` | workspace | Repository state |
| `codra.model` | private | Model relay configuration |

## Permission Gates

Codra actions declare required permissions:

- `codra:read` — read files, git status, context
- `codra:write` — modify files, create commits
- `codra:execute` — run terminal commands

Write and execute actions require approval before execution.

## Audit Trail

Every Codra action creates an audit event with:

- Action type and description
- Input parameters (sanitized)
- Execution result
- Timestamp
- Actor identifier

No raw secrets, API keys, or file contents exceeding safe limits are logged.

## Adoption Status

| Component | Status |
|-----------|--------|
| Action registry | Planned |
| Context providers | Planned |
| Permission gates | Planned |
| Audit logging | Partial (existing run logging) |
| Approval workflows | Existing approval system |
| Protocol endpoint | Not yet implemented |

## Migration Plan

1. Map existing Codra actions to protocol format
2. Add protocol endpoint to Codra API
3. Integrate permission gates into file/git operations
4. Enhance audit logging with protocol metadata
5. Add context providers for thread/plan/file state

## Limitations

- Codra CLI currently operates locally — protocol is for when Codra becomes multi-user
- File operations are the primary write actions
- Terminal commands are the highest-risk actions
- No hosted execution yet
107 changes: 93 additions & 14 deletions apps/code/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,45 @@ import * as path from 'path';
import * as os from 'os';
import chalk from 'chalk';

const MAX_DEBUG_BODY_LENGTH = 200;

async function readJsonResponse(response: Response, context: string): Promise<any> {
const contentType = response.headers.get('content-type') || '';

if (!contentType.includes('application/json')) {
const rawBody = await response.text();
const isHtml = rawBody.trimStart().startsWith('<') || rawBody.includes('<html');
const isRsc = rawBody.includes('self.__next_f.push');

if (isHtml || isRsc) {
throw new Error(
`NON_JSON_RESPONSE: ${context} returned HTML instead of JSON (status ${response.status}). ` +
`This usually means the API endpoint is not deployed or is pointing to a web page. ` +
`Run "codra-code doctor" to check connectivity.`
);
}

throw new Error(
`NON_JSON_RESPONSE: ${context} returned unexpected content-type "${contentType}" (status ${response.status}). ` +
`Expected application/json.`
);
}

return response.json();
}

export class LoginError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode?: number,
public readonly endpoint?: string
) {
super(message);
this.name = 'LoginError';
}
}

const AUTH_FILE = path.join(os.homedir(), '.codra', 'auth.json');
const DEFAULT_AUTH_URL = 'https://teraai.chat';
const AUTH_DEV_BYPASS = process.env.CODRA_AUTH_DEV_BYPASS === '1';
Expand Down Expand Up @@ -98,27 +137,56 @@ export async function clearAuthToken(): Promise<void> {

export async function startLogin(options: { noBrowser?: boolean; authUrl?: string } = {}): Promise<boolean> {
const authBaseUrl = options.authUrl || getAuthBaseUrl();

const startEndpoint = `${authBaseUrl}/api/codra/auth/device/start`;

console.log(chalk.cyan('\n Codra Code Authentication'));
console.log(chalk.gray(' Starting Tera login flow...\n'));

try {
// Start device auth session
const startResponse = await fetch(`${authBaseUrl}/api/codra/auth/device/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cli_version: '0.1.6',
platform: process.platform
})
});
let startResponse: Response;
try {
startResponse = await fetch(startEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cli_version: '0.1.6',
platform: process.platform
})
});
} catch (fetchErr) {
const msg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
throw new LoginError(
`Network error connecting to Tera: ${msg}`,
'NETWORK_ERROR',
undefined,
'/api/codra/auth/device/start'
);
}

if (!startResponse.ok) {
const error = await startResponse.text();
throw new Error(`Failed to start auth session: ${error}`);
try {
await readJsonResponse(startResponse, 'Tera auth start endpoint');
Comment on lines +168 to +169

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Throw after parsing JSON auth errors

If the device start endpoint returns a non-2xx JSON response, such as a 400 or 429 error, this call succeeds and consumes the response body, then execution falls through to parse the same Response again below. That produces a generic body-already-used failure instead of the intended HTTP error, so the non-OK branch should throw after reading the JSON error rather than continuing.

Useful? React with 👍 / 👎.

} catch (parseErr) {
if (parseErr instanceof Error && parseErr.message.startsWith('NON_JSON_RESPONSE')) {
throw new LoginError(
`Tera returned a web page instead of an API response (HTTP ${startResponse.status}). ` +
`The login endpoint may not be deployed yet. Run "codra-code doctor" to check.`,
'NON_JSON_RESPONSE',
startResponse.status,
'/api/codra/auth/device/start'
);
}
throw new LoginError(
`Auth session start failed (HTTP ${startResponse.status})`,
'HTTP_ERROR',
startResponse.status,
'/api/codra/auth/device/start'
);
}
}

const startData = await startResponse.json();
const startData = await readJsonResponse(startResponse, 'Tera auth start endpoint');
const { device_code, user_code, verification_url, expires_at, interval } = startData;

console.log(chalk.gray(' Device Code:'), chalk.white(user_code));
Expand Down Expand Up @@ -191,6 +259,7 @@ async function openBrowser(url: string): Promise<void> {
async function pollForAuth(deviceCode: string, authBaseUrl: string, interval: number): Promise<AuthToken | null> {
const maxAttempts = 150; // 5 minutes with 2-second intervals
const pollInterval = interval * 1000;
let nonJsonWarningShown = false;

for (let i = 0; i < maxAttempts; i++) {
try {
Expand All @@ -201,8 +270,18 @@ async function pollForAuth(deviceCode: string, authBaseUrl: string, interval: nu
});

if (response.ok) {
const data = await response.json();

let data: any;
try {
data = await readJsonResponse(response, 'Tera auth poll endpoint');
} catch {
if (!nonJsonWarningShown) {
console.log(chalk.yellow(' Warning: Tera poll endpoint returned non-JSON response.'));
nonJsonWarningShown = true;
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
continue;
}

if (data.status === 'approved' && data.token) {
return {
userId: data.user_id || data.userId,
Expand Down
37 changes: 37 additions & 0 deletions apps/code/src/commands/doctor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import chalk from 'chalk';
import { getConfig } from '../config.js';
import { createProvider } from '../providers/index.js';
import { getAuthBaseUrl, isAuthenticated } from '../auth/index.js';
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
Expand Down Expand Up @@ -69,6 +70,42 @@ export async function doctorCommand() {
console.log(chalk.gray(` Project config: ${fs.existsSync(mcpConfigPath) ? 'Found' : 'Not found'}`));
console.log(chalk.gray(` User config: ${fs.existsSync(userMcpConfigPath) ? 'Found' : 'Not found'}`));

// Check Tera API connectivity
console.log(chalk.cyan('\n Tera API:'));
const teraBaseUrl = getAuthBaseUrl();
console.log(chalk.gray(` Base URL: ${teraBaseUrl}`));
console.log(chalk.gray(` Authenticated: ${isAuthenticated() ? 'Yes' : 'No'}`));

try {
const testUrl = `${teraBaseUrl}/api/codra/auth/device/start`;
const testResponse = await fetch(testUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cli_version: '0.1.6', platform: process.platform }),
signal: AbortSignal.timeout(10000)
});

const contentType = testResponse.headers.get('content-type') || '';
const isJson = contentType.includes('application/json');

if (isJson && testResponse.ok) {
console.log(chalk.green(' Login endpoint: Reachable (JSON)'));
} else if (isJson && !testResponse.ok) {
console.log(chalk.yellow(` Login endpoint: Returns JSON but status ${testResponse.status}`));
} else {
console.log(chalk.red(' Login endpoint: Returns HTML instead of JSON'));
console.log(chalk.red(' This usually means the API route is not deployed or is misconfigured.'));
console.log(chalk.gray(' Try: codra-code login --no-browser'));
}
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes('timeout') || msg.includes('Timeout')) {
console.log(chalk.yellow(' Login endpoint: Timed out (10s)'));
} else {
console.log(chalk.red(` Login endpoint: Unreachable (${msg})`));
}
}

// Check plugins
console.log(chalk.cyan('\n Plugins:'));
const pluginsDir = path.join(process.cwd(), 'plugins');
Expand Down
6 changes: 5 additions & 1 deletion apps/code/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import type { Provider } from './types.js';
import { MockProvider } from './mock.js';
import { OpenAIProvider } from './openai.js';
import { OllamaProvider } from './ollama.js';
import { TeraProvider } from './tera.js';

export type { Provider, Message, ProviderResponse } from './types.js';
export { MockProvider } from './mock.js';
export { OpenAIProvider } from './openai.js';
export { OllamaProvider } from './ollama.js';
export { TeraProvider } from './tera.js';

export function createProvider(providerName: string, config?: { baseUrl?: string; apiKey?: string }): Provider {
switch (providerName.toLowerCase()) {
Expand All @@ -16,8 +18,10 @@ export function createProvider(providerName: string, config?: { baseUrl?: string
return new OpenAIProvider(config?.baseUrl, config?.apiKey);
case 'ollama':
return new OllamaProvider(config?.baseUrl);
case 'tera':
return new TeraProvider(config?.baseUrl);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass configured API keys to TeraProvider

When a user configures provider: "tera" with an apiKey in Codra config, this factory drops that key while OpenAI receives it. TeraProvider only checks CODRA_API_KEY and ~/.codra/auth.json, so configured Tera keys are ignored and requests fail with an authentication error unless the same key is duplicated in the environment.

Useful? React with 👍 / 👎.

default:
throw new Error(`Unknown provider: ${providerName}. Supported: mock, openai, ollama`);
throw new Error(`Unknown provider: ${providerName}. Supported: mock, openai, ollama, tera`);
}
}

Expand Down
Loading
Loading