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
1 change: 1 addition & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ dependency guardrails.
| `foundation/schema` | zod schemas — the single data contract. Every cross-process / cross-end / post-abstraction message type derives from here (`z.infer`). |
| `foundation/transport` | Communication layer ("how messages travel"): local / ws / Socket.IO implementations, the `Hub` fan-out, and the versioned wire protocol. |
| `foundation/common` | Shared framework-agnostic utilities that do not belong to the data contract or a product layer (for example Zustand persistence helpers). |
| `foundation/providers` | The model-provider service directory plus `resolveBinding`, which picks the endpoint variant a given agent speaks. Shared by the daemon (session start) and the client (binding availability), so neither keeps its own copy. |
| `host/agent-adapter` | One adapter per agent (`claude-code` / `codex` / `opencode` / `pi` / `grok-build`) plus the abstraction layer that normalizes native events into `schema`. |
| `host/assets` | Managed-asset store for verified agent CLI pairs, in-process package closures, and standalone toolchains. |
| `host/engine` | The host engine: session lifecycle and agent orchestration, driving `agent-adapter`. |
Expand Down
1 change: 1 addition & 0 deletions packages/client/workbench/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@linkcode/client-core": "workspace:*",
"@linkcode/common": "workspace:*",
"@linkcode/i18n": "workspace:*",
"@linkcode/providers": "workspace:*",
"@linkcode/schema": "workspace:*",
"@linkcode/sdk": "workspace:*",
"@linkcode/transport": "workspace:*",
Expand Down
5 changes: 2 additions & 3 deletions packages/client/workbench/src/settings/agents-settings.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { resolveBinding } from '@linkcode/providers';
import type { AgentKind, AgentRuntimeAvailability } from '@linkcode/schema';
import { getAccounts, getProviderConfig, setProviderConfig } from '@linkcode/sdk';
import { AgentIcon, AgentOnboardingCard, SettingsCard } from '@linkcode/ui';
Expand All @@ -8,7 +9,6 @@ import { useTranslations } from 'use-intl';
import { useAgentRuntimes } from '../agent-runtime/hooks';
import { useAgentRuntimeOnboarding } from '../agent-runtime/onboarding';
import { useData, useMutation } from '../runtime/tayori';
import { bindingAvailability } from './providers/capability';
import { AGENT_KINDS, withEnabled } from './providers/view';
import { SimulatorAgentAccessCard } from './simulator-access';

Expand Down Expand Up @@ -50,8 +50,7 @@ export function AgentsSettingsPanel({
// A disabled agent's runtime gaps don't matter — no card, just the badge.
const cue = enabled ? onboarding.cues[kind] : undefined;
const translated =
boundAccount !== undefined &&
bindingAvailability(boundAccount, kind).tier === 'translate';
boundAccount !== undefined && resolveBinding(boundAccount, kind).tier === 'translate';
// With no bound account the agent follows the CLI login — show who that is when probed.
const cliIdentity =
boundAccount === undefined && runtime?.auth?.loggedIn === true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,55 @@ describe('non-subscription account creation', () => {
),
);
});

it('asks nothing about protocol for a service serving several shapes', () => {
render(
<AddAccountForm
serviceId="openrouter"
runtimes={undefined}
onboarding={onboarding()}
busy={false}
onBack={vi.fn()}
onSubmit={vi.fn()}
/>,
);

// Each agent resolves its own shape, so the user is never asked to pick one.
expect(screen.queryAllByRole('radio')).toHaveLength(0);
});

it('stores template values instead of a resolved endpoint', async () => {
const onSubmit = vi.fn();
const { container } = render(
<AddAccountForm
serviceId="cloudflare-gateway"
runtimes={undefined}
onboarding={onboarding()}
busy={false}
onBack={vi.fn()}
onSubmit={onSubmit}
/>,
);

fireEvent.change(screen.getByRole('textbox', { name: 'Account ID' }), {
target: { value: '8f3a' },
});
fireEvent.change(screen.getByRole('textbox', { name: 'Gateway ID' }), {
target: { value: 'prod' },
});
const secret = container.querySelector('input[type="password"]');
if (!secret) throw new Error('credential input missing');
fireEvent.change(secret, { target: { value: 'cf-token' } });
fireEvent.click(screen.getByRole('button', { name: 'form.submit' }));

await waitFor(() => expect(onSubmit).toHaveBeenCalled());
const account = onSubmit.mock.calls[0]?.[0];
expect(account).toMatchObject({
service: 'cloudflare-gateway',
credential: { type: 'auth-token', token: 'cf-token' },
endpointParams: { account_id: '8f3a', gateway_id: 'prod' },
});
// One key can resolve to a different endpoint per agent, so none is pinned here.
expect(account).not.toHaveProperty('endpoint');
});
});

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ describe('view helpers', () => {
service: 'claude-sub',
credential: { type: 'oauth', agent: 'claude-code' },
},
{
id: 'acc_c',
label: 'Proxy',
createdAt: 3,
service: 'openrouter',
endpoint: { baseUrl: 'https://proxy.internal/v1', protocol: 'openai-chat' },
credential: { type: 'auth-token', token: 'tok' },
},
];
const runtimes: AgentRuntimes = {
'claude-code': {
Expand All @@ -103,8 +111,9 @@ describe('view helpers', () => {
label: 'Primary API',
service: 'openai-api',
serviceLabel: 'OpenAI API',
endpoint: 'https://api.openai.com/v1',
protocol: 'openai-chat',
// The stored endpoint is the catalog's own, so the row describes the service's shapes —
// the same answer the resolver gives, rather than a pin it will ignore.
routing: { kind: 'catalog', protocols: ['openai-chat', 'openai-responses'] },
credentialType: 'api-key',
boundAgents: ['claude-code'],
},
Expand All @@ -117,6 +126,21 @@ describe('view helpers', () => {
auth: { loggedIn: true, email: 'claude@example.com' },
boundAgents: ['codex'],
},
{
id: 'acc_c',
label: 'Proxy',
service: 'openrouter',
serviceLabel: 'OpenRouter',
// A URL the catalog never produces is the user's own, so it stays pinned and the row
// reports that one endpoint rather than the service's shapes.
routing: {
kind: 'pinned',
baseUrl: 'https://proxy.internal/v1',
protocol: 'openai-chat',
},
credentialType: 'auth-token',
boundAgents: [],
},
],
detectedLogins: [{ service: 'chatgpt-sub', label: 'ChatGPT', email: 'codex@example.com' }],
bindingCount: 2,
Expand Down
Loading