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
48 changes: 48 additions & 0 deletions backend/__tests__/unit/routes/agentBinding.phase2.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,54 @@ describe('placement request', () => {
expect(res.status).toBe(403);
});

// C14 rehearsal follow-up (sprint-review on #2365): "On my computer" with
// the name of the person's hosted agent stamped a placement the daemon's
// runtime-token mint then refused, so the page said "requested" and nothing
// ran. Refused up front with the mint's own code, and nothing is stamped.
describe('an agent Commonly runs in-process cannot be placed on a machine', () => {
const runInProcess = (config) => AgentInstallation.updateOne(
{ agentName: 'wren-test', instanceId: 'default' },
{ $set: { config } },
);

it.each([
['hosted on the native engine', { runtime: { runtimeType: 'hosted' }, hosted: { engine: 'native' } }],
['native', { runtime: { runtimeType: 'native' } }],
])('%s: 409 in_process_agent_no_token, and no request is stamped', async (_label, config) => {
await runInProcess(config);
const res = await requestPlacement('machine-a');
expect(res.status).toBe(409);
expect(res.body.code).toBe('in_process_agent_no_token');
expect(res.body.message).toMatch(/^wren-test is your hosted agent/);
const identity = await User.findById(bot._id).lean();
expect(identity.botMetadata.requestedMachineId ?? null).toBeNull();
const seenByA = await assigned(DAEMON_A);
expect(seenByA.body.agents).toEqual([]);
});

it('a request stamped before the check can still be withdrawn', async () => {
await User.updateOne({ _id: bot._id }, { $set: { 'botMetadata.requestedMachineId': 'machine-a' } });
await runInProcess({ runtime: { runtimeType: 'native' } });
const res = await requestPlacement(null);
expect(res.status).toBe(200);
const identity = await User.findById(bot._id).lean();
expect(identity.botMetadata.requestedMachineId).toBeNull();
});

it('ownership is checked first: another user learns nothing about how it runs', async () => {
await runInProcess({ runtime: { runtimeType: 'native' } });
global.__CALLER_ID = String(new mongoose.Types.ObjectId());
const res = await requestPlacement('machine-a');
expect(res.status).toBe(403);
expect(res.body.code).not.toBe('in_process_agent_no_token');
});

it('an agent the person runs themself is still placed', async () => {
const res = await requestPlacement('machine-a');
expect(res.status).toBe(200);
});
});

it('409s when the agent is bound to a different machine', async () => {
await User.updateOne({ _id: bot._id }, { $set: { 'botMetadata.machineId': 'machine-b' } });
const res = await requestPlacement('machine-a');
Expand Down
15 changes: 15 additions & 0 deletions backend/routes/agentBinding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const User = require('../models/User');
const {
getRuntimeTokenHashesForAgent,
revokeRuntimeTokensForAgent,
runsInProcess,
} = require('./registry/tokens');

const router = express.Router();
Expand Down Expand Up @@ -216,6 +217,20 @@ router.post('/request', bindingRateLimit, auth, async (req: express.Request & {
});
}

// An agent Commonly runs in-process (the person's hosted agent) can't also
// run on a machine: its daemon would be refused at the runtime-token mint,
// so the page said "requested" and nothing ever ran. Refused here, with the
// mint's code, after the ownership check so another user's agent never
// reveals how it runs. Withdrawing (machineId: null) stays allowed, so a
// request stamped before this check can still be cleared.
if (requestedMachineId && await runsInProcess(null, { agentName, instanceId })) {
return res.status(409).json({
code: 'in_process_agent_no_token',
message: `${agentName} is your hosted agent, and Commonly runs it, so it can't also run on your computer. `
+ 'Choose a different name for an agent you run yourself.',
});
}

if (requestedMachineId) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const Machine = require('../models/Machine');
Expand Down
1 change: 1 addition & 0 deletions backend/routes/registry/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ module.exports = {
revokeRuntimeTokensAfterLastUninstall,
issueRuntimeTokenForAgent,
InProcessAgentTokenRefused,
runsInProcess,
issueRuntimeTokenForInstallation,
issueUserTokenForInstallation,
};
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/v2/__tests__/V2AgentBYOMachine.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,39 @@ describe('BYO on-my-computer mode', () => {
// route), so this asserts the exclusion is real rather than an empty set.
expect(container.querySelectorAll('.v2-rail__account .v2-avatar').length).toBe(1);
});

// The placement route refuses an agent Commonly runs in-process (the
// person's hosted agent) with in_process_agent_no_token. The page says so
// in plain words and shows no "waiting for your computer" result. Matched on
// the words both the server's message and the page's own copy for that code
// share, so it holds whichever one renders.
test("placing the person's hosted agent on a computer is refused in plain words", async () => {
mockGet();
axios.post.mockImplementation((url) => {
if (url === '/api/registry/install') {
return Promise.reject({ response: { status: 400, data: { error: 'Agent already installed in this pod' } } });
}
if (url === '/api/agent-binding/request') {
return Promise.reject({
response: {
status: 409,
data: {
code: 'in_process_agent_no_token',
message: "sam-hosted is your hosted agent, and Commonly runs it, so it can't also run on your computer. Choose a different name for an agent you run yourself.",
},
},
});
}
return Promise.resolve({ data: {} });
});
renderPage();
await waitFor(() => expect(screen.getByTestId('byo-mode-machine')).toBeInTheDocument());
fireEvent.click(screen.getByTestId('byo-mode-machine'));
fireEvent.change(screen.getByPlaceholderText('my-mcp-agent'), { target: { value: 'sam-hosted' } });
fireEvent.click(screen.getByText('Add to this computer'));

expect(await screen.findByText(/sam-hosted is your hosted agent/)).toBeInTheDocument();
expect(screen.queryByTestId('byo-machine-result')).toBeNull();
expect(screen.queryByText(/has no runtime token/)).toBeNull();
});
});
Loading