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
71 changes: 71 additions & 0 deletions backend/__tests__/service/agent-manage-settings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const { AgentInstallation } = require('../../models/AgentRegistry');
const AgentMemory = require('../../models/AgentMemory');
const manageSettings = require('../../routes/registry/manage-settings');
const podAgents = require('../../routes/registry/pod-agents');
const agentConfig = require('../../routes/registry/agent-config');
const { projectSeatEnvironments, seatEnvironmentKey } = require('../../services/seatEnvironmentProjection');

const SECRET = 'agent-manage-settings-test';
const token = (user) => jwt.sign({ id: String(user._id) }, SECRET);
Expand All @@ -23,6 +25,7 @@ describe('per-installation agent settings', () => {
app.use(express.json());
app.use('/api/registry', manageSettings);
app.use('/api/registry', podAgents);
app.use('/api/registry', agentConfig);
owner = await User.create({ username: 'settings-owner', email: 'settings-owner@test.com', password: 'password123' });
other = await User.create({ username: 'settings-other', email: 'settings-other@test.com', password: 'password123' });
admin = await User.create({ username: 'settings-admin', email: 'settings-admin@test.com', password: 'password123', role: 'admin' });
Expand All @@ -42,6 +45,10 @@ describe('per-installation agent settings', () => {
]);
});

afterEach(async () => {
await User.deleteMany({ username: 'settings-runtime-bot' });
});

it('lists only installations the caller may manage', async () => {
const response = await request(app).get('/api/registry/agents/settings-agent/manage-settings?instanceId=same')
.set('Authorization', `Bearer ${token(owner)}`);
Expand All @@ -60,6 +67,70 @@ describe('per-installation agent settings', () => {
expect(otherResponse.body.writablePodId).toBe(String(podB._id));
});

it('reads and writes the same effort declaration the daemon selects', async () => {
await User.create({
username: 'settings-runtime-bot', email: 'settings-runtime-bot@test.com', password: 'password123',
isBot: true, botMetadata: { agentName: 'settings-agent', instanceId: 'same', machineId: 'machine-a' },
});
await AgentInstallation.updateOne({ podId: podA._id, agentName: 'settings-agent' }, {
$set: { config: {} },
});
await AgentInstallation.updateOne({ podId: podB._id, agentName: 'settings-agent' }, {
$set: { installedBy: owner._id, 'config.environment': { model: 'opus', effort: 'high' } },
});
const url = '/api/registry/agents/settings-agent/manage-settings?instanceId=same';
const before = await request(app).get(url).set('Authorization', `Bearer ${token(owner)}`);
expect(before.status).toBe(200);
expect(before.body.writablePodId).toBe(String(podA._id));
expect(before.body.runtimeSourcePodId).toBe(String(podB._id));
expect(before.body.runtimeEditable).toBe(true);
expect(before.body.runtimeManaged).toBe(true);
expect(before.body.runtimeControls).toEqual(expect.objectContaining({ effort: 'high' }));

const saved = await request(app)
.patch(`/api/registry/pods/${before.body.runtimeSourcePodId}/agents/settings-agent`)
.set('Authorization', `Bearer ${token(owner)}`)
.send({ instanceId: 'same', config: { environment: { effort: 'xhigh' } } });
expect(saved.status).toBe(200);
const seat = (await projectSeatEnvironments({ installedBy: owner._id }))
.get(seatEnvironmentKey('settings-agent', 'same'));
expect(seat.environment.effort).toBe('xhigh');
expect(seat.sourcePodId).toBe(String(podA._id));
const after = await request(app).get(url).set('Authorization', `Bearer ${token(owner)}`);
expect(after.body.runtimeManaged).toBe(true);
expect(after.body.runtimeControls.runtimeType).toBe('wrapper');
expect(after.body.runtimeControls.effort).toBe('xhigh');
});

it('does not present a standalone CLI preference as a running effort', async () => {
await AgentInstallation.updateOne({ podId: podA._id, agentName: 'settings-agent' }, {
$set: { 'config.environment': { model: 'opus', effort: 'max' } },
});
const response = await request(app)
.get('/api/registry/agents/settings-agent/manage-settings?instanceId=same')
.set('Authorization', `Bearer ${token(owner)}`);
expect(response.status).toBe(200);
expect(response.body.runtimeManaged).toBe(false);
expect(response.body.runtimeEditable).toBe(false);
expect(response.body.runtimeControls.effort).toBe('');
});

it('does not present a hosted runtime preference as its running model or effort', async () => {
await AgentInstallation.updateOne({ podId: podA._id, agentName: 'settings-agent' }, {
$set: { 'config.runtime': { runtimeType: 'internal' },
'config.environment': { model: 'opus', effort: 'max' } },
});
const response = await request(app)
.get('/api/registry/agents/settings-agent/manage-settings?instanceId=same')
.set('Authorization', `Bearer ${token(owner)}`);
expect(response.status).toBe(200);
expect(response.body.runtimeManaged).toBe(false);
expect(response.body.runtimeEditable).toBe(false);
expect(response.body.runtimeSourcePodId).toBeNull();
expect(response.body.runtimeControls.model).toBe('');
expect(response.body.runtimeControls.effort).toBe('');
});

it('shows the installation owner’s quota when an admin opens settings', async () => {
await User.updateOne({ _id: owner._id }, { $set: { 'entitlements.pro': true } });
const response = await request(app).get('/api/registry/agents/settings-agent/manage-settings?instanceId=same')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ describe('projectSeatEnvironments', () => {
// The pair comes from the OLDEST active declaration (`_id` ascending),
// which for a duplicated identity is a rule rather than the stored order.
expect(entry.runtime).toEqual({ adapter: 'claude' });
expect(entry.sourcePodId).toBe(String(podOne));

const theirs = await projectSeatEnvironments({ installedBy: b._id });
expect(theirs.get(seatEnvironmentKey('proj-seat', 'default')).runtime).toEqual({ adapter: 'pi' });
Expand Down
46 changes: 44 additions & 2 deletions backend/routes/registry/manage-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import rateLimit from 'express-rate-limit';
import { cloudflareIpRateLimitKeyGenerator } from '../../middleware/ipRateLimit';
import { hostedCapsForOwner, userRunCeilingForOwner } from '../../services/hostedRuntimeService';
import { projectSeatEnvironments, seatEnvironmentKey } from '../../services/seatEnvironmentProjection';
const express = require('express');
const auth = require('../../middleware/auth');
const { AgentInstallation } = require('../../models/AgentRegistry');
Expand Down Expand Up @@ -57,14 +58,55 @@ router.get('/agents/:name/manage-settings', settingsRateLimit, auth, async (req:
.select('name').lean();
const names = new Map<string, string>(pods.map((pod: any) => [String(pod._id), String(pod.name)]));
const visible = manageable.filter((entry: any) => names.has(String(entry.podId)));
const writable = visible.find((entry: any) => entry.status === 'active') || visible[0];
const quotaOwner = writable?.installedBy || userId;
const fallback = visible.find((entry: any) => entry.status === 'active') || visible[0];
const quotaOwner = fallback?.installedBy || userId;
// The daemon runs the oldest active declaration, not whichever pod this
// page happened to load first. Use its projection for both the displayed
// controls and the installation selected for edits.
const seat = (await projectSeatEnvironments({ installedBy: quotaOwner,
agentName: req.params.name }))
.get(seatEnvironmentKey(req.params.name, req.query.instanceId));
const source = visible.find((entry: any) => String(entry.podId) === seat?.sourcePodId);
const writable = fallback;
const runtime = seat?.runtime && typeof seat.runtime === 'object' ? seat.runtime as Record<string, unknown> : {};
const environment = seat?.environment || {};
const runtimeControls = Object.fromEntries(['model', 'effort']
.map((key) => [key, environment[key] ?? runtime[key]])
.filter(([, value]) => typeof value === 'string'));
for (const key of ['runtimeType', 'adapter']) {
if (typeof runtime[key] === 'string') runtimeControls[key] = runtime[key];
}
if (runtime.host === 'cloud' || runtime.host === 'byo') runtimeControls.host = runtime.host;
// The identity binding, rather than this pod's runtime declaration, tells
// us whether the daemon owns the next spawn. A Settings write can make an
// older pod the projection source even when it has no runtimeType field.
const binding = await User.findOne({
isBot: true,
'botMetadata.agentName': String(req.params.name).toLowerCase(),
'botMetadata.instanceId': normalizeInstanceId(req.query.instanceId),
}).select('botMetadata.machineId').lean();
const runtimeManaged = Boolean(binding?.botMetadata?.machineId);
if (runtimeManaged && !runtimeControls.runtimeType) runtimeControls.runtimeType = 'wrapper';
// A standalone `agent run` reads its local token record. Hosted/native
// turns read their own run config (installation.config.model or the service
// default), not the daemon projection. Neither path can be attested by an
// installation runtime preference, so display only daemon-managed values.
if (!runtimeManaged) {
runtimeControls.model = '';
runtimeControls.effort = '';
}
const [{ turnsPerDay }, userRunCeiling] = await Promise.all([
hostedCapsForOwner(quotaOwner), userRunCeilingForOwner(quotaOwner),
]);
return res.json({ turnCap: turnsPerDay, userRunCeiling, podCount: installations.length,
activePodCount: installations.filter((entry: any) => entry.status === 'active').length,
writablePodId: writable ? String(writable.podId) : null,
runtimeSourcePodId: !runtimeManaged ? null
: source ? String(source.podId)
: (!seat?.sourcePodId && writable ? String(writable.podId) : null),
runtimeControls,
runtimeManaged,
runtimeEditable: runtimeManaged && (!seat?.sourcePodId || Boolean(source)),
installations: visible
.map((entry: any) => settings(entry, names.get(String(entry.podId)) || 'Pod')) });
} catch (error) {
Expand Down
6 changes: 5 additions & 1 deletion backend/services/seatEnvironmentProjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export type SeatEnvironmentEntry = {
agentName: string;
instanceId: string;
podIds: string[];
sourcePodId: string | null;
runtime: unknown;
environment: Record<string, unknown> | null;
};
Expand All @@ -135,6 +136,7 @@ export type SeatEnvironmentQuery = {
* Mongo happened to order first, which is a guess dressed as a verdict.
*/
installedBy?: unknown;
agentName?: string;
};

/**
Expand Down Expand Up @@ -164,6 +166,7 @@ export const projectSeatEnvironments = async (
): Promise<Map<string, SeatEnvironmentEntry>> => {
const filter: Record<string, unknown> = { status: 'active' };
if (query.installedBy) filter.installedBy = query.installedBy;
if (query.agentName) filter.agentName = normalizeIdentityPart(query.agentName);
const installs = await AgentInstallation.find(filter)
.select('agentName instanceId podId config')
.sort({ _id: 1 })
Expand All @@ -175,7 +178,7 @@ export const projectSeatEnvironments = async (
const instanceId = normalizeIdentityPart(install.instanceId) || 'default';
const key = seatEnvironmentKey(agentName, instanceId);
const entry = byIdentity.get(key) || {
agentName, instanceId, podIds: [], runtime: null, environment: null,
agentName, instanceId, podIds: [], sourcePodId: null, runtime: null, environment: null,
};
if (install.podId) entry.podIds.push(String(install.podId));
// AgentInstallation.config is a Mongoose Map; lean() yields a plain
Expand All @@ -189,6 +192,7 @@ export const projectSeatEnvironments = async (
// empty row shadow a newer row's real pair (Vera, on 268370c1).
const environment = config.environment ? projectEnvironment(config.environment) : null;
if (!sourced.has(key) && (runtime || environment)) {
entry.sourcePodId = install.podId ? String(install.podId) : null;
entry.runtime = runtime;
entry.environment = environment;
sourced.add(key);
Expand Down
30 changes: 30 additions & 0 deletions cli/__tests__/daemon-supervisor.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// effect injected — no processes, no network, no real timers.
import { jest } from '@jest/globals';
import { EventEmitter } from 'events';
import fs from 'fs';
import os from 'os';
import path from 'path';

import {
backoffMs,
Expand All @@ -10,6 +13,7 @@ import {
createDaemonSupervisor,
} from '../src/lib/daemon-supervisor.js';
import { assertNoSandboxDeclared } from '../src/lib/adapters/pi.js';
import claude from '../src/lib/adapters/claude.js';

const record = {
machineDbId: '507f1f77bcf86cd799439011',
Expand Down Expand Up @@ -450,6 +454,32 @@ describe('tick', () => {
expect(supervisor.agentStates()).toEqual([
expect.objectContaining({ adapter: 'claude', model: 'sonnet', effort: 'high' }),
]);

// The environment value selected from the Settings declaration must be
// the value the actual adapter places in argv, not the runtime overlay.
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'runtime-effort-argv-'));
const calls = [];
try {
await claude.spawn('check', {
cwd, sessionId: null,
environment: {
model: tokens['wren-test'].environment.model,
effort: tokens['wren-test'].environment.effort,
},
_spawnImpl: (command, args) => {
calls.push(args);
const child = new EventEmitter();
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
setImmediate(() => child.emit('close', 0));
return child;
},
});
expect(calls[0][calls[0].indexOf('--effort') + 1]).toBe('high');
expect(calls[0][calls[0].indexOf('--model') + 1]).toBe('sonnet');
} finally {
fs.rmSync(cwd, { recursive: true, force: true });
}
});

test('a refused adapter change is reported as the adapter that kept running (TASK-065)', async () => {
Expand Down
4 changes: 2 additions & 2 deletions cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@commonlyai/cli",
"version": "0.1.139",
"version": "0.1.142",
"license": "Apache-2.0",
"description": "The Commonly CLI — connect agents, manage pods, iterate fast",
"type": "module",
Expand Down
10 changes: 10 additions & 0 deletions docs/development/agent-experience-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -4200,3 +4200,13 @@ Gates that never appear in metadata decided several more, including the client a
**Repair:** after a post, resolve known mentioned handles against pod membership and return a sender-only notice for each known non-member. V2 shows the sentence below the sent message; the MCP post result carries the same sentence and a structured `MENTION_NOT_IN_POD` field. Unknown handles stay silent. The notice names only the handle the sender typed and says nothing about other pods.

**Lesson:** accepting a message is not proof its address resolved. A tool that lets an agent address someone must report a known delivery failure in the same turn.

## 84. An installation preference appeared as the running effort (2026-10-23, kite)

*Origin: TASK-434. Settings showed Effort `max` while a verified CLI spawn used `high`.*

Settings read one writable pod's `config.environment`, while the daemon selected the oldest active runtime declaration across the owner's installations. A standalone CLI seat read its local token record instead of either server row. The same label therefore presented different sources as if each were the running value.

**Repair:** Settings now uses the daemon's environment projection for model and effort, saves through that projection's source pod, and shows standalone CLI controls as locally set and read-only. Focused route, UI and adapter tests connect the saved effort to the spawn argv.

**Lesson:** a displayed runtime setting must identify the source the runner reads. A server preference cannot attest a local process that does not consume it.
2 changes: 2 additions & 0 deletions frontend/src/v2/__tests__/V2AgentRuntimeGrants.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ beforeEach(() => {

test('runtime labels only recognize walked adapters and distinguish daemon from a single CLI', () => {
expect(describeRuntime({ runtimeType: 'wrapper', adapter: 'claude' })).toEqual({ mode: 'daemon', adapter: 'Claude Code' });
expect(describeRuntime({ runtimeType: 'wrapper', adapter: 'claude', managed: false }))
.toEqual({ mode: 'cli', adapter: 'Claude Code' });
expect(describeRuntime({ runtimeType: 'webhook', host: 'byo', adapter: 'dsh' })).toEqual({ mode: 'cli', adapter: 'DeepSeek Harness' });
expect(describeRuntime({ runtimeType: 'wrapper', adapter: 'unwalked' })).toEqual({ mode: 'daemon', adapter: null });
expect(describeRuntime({ runtimeType: 'hosted' })).toEqual({ mode: 'hosted', adapter: null });
Expand Down
49 changes: 49 additions & 0 deletions frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -403,3 +403,52 @@ test('runtime editor keeps a wrapper model when no environment override exists',
expect(await screen.findByLabelText('Model')).toHaveValue('selected-model');
expect(screen.getByLabelText('Effort')).toHaveValue('medium');
});

test('runtime editor shows the daemon-selected effort instead of a different pod config', async () => {
axios.patch.mockResolvedValue({ data: { updated: true } });
axios.get.mockImplementation((url) => {
if (url.includes('/manage-settings')) return Promise.resolve({ data: {
writablePodId: 'skills-pod', runtimeSourcePodId: 'source-pod',
runtimeControls: { runtimeType: 'wrapper', adapter: 'claude', model: 'opus', effort: 'high' },
runtimeEditable: true, installations: [
{ podId: 'skills-pod', podName: 'Work', status: 'active' },
{ podId: 'source-pod', podName: 'Source', status: 'active' },
],
} });
if (url.startsWith('/api/registry/pods/')) return Promise.resolve({ data: { agent: {
name: 'maya-claude', instanceId: 'default', displayName: 'Maya',
runtime: { runtimeType: 'wrapper', adapter: 'claude' },
config: { environment: { model: 'opus', effort: 'max' } },
} } });
return Promise.resolve({ data: [] });
});
renderPage();
const effort = await screen.findByLabelText('Effort');
expect(effort).toHaveValue('high');
fireEvent.change(effort, { target: { value: 'xhigh' } });
fireEvent.click(screen.getByRole('button', { name: 'Save runtime' }));
await waitFor(() => expect(axios.patch).toHaveBeenCalledWith(
'/api/registry/pods/source-pod/agents/maya-claude',
{ config: { environment: { effort: 'xhigh' } }, instanceId: 'default' },
));
});

test('standalone CLI settings do not claim a local effort or offer a remote save', async () => {
axios.get.mockImplementation((url) => {
if (url.includes('/manage-settings')) return Promise.resolve({ data: {
writablePodId: 'pod-1', runtimeSourcePodId: null, runtimeManaged: false,
runtimeEditable: false, runtimeControls: { runtimeType: 'wrapper', adapter: 'claude', model: '', effort: '' },
installations: [{ podId: 'pod-1', podName: 'Work', status: 'active' }],
} });
if (url.startsWith('/api/registry/pods/')) return Promise.resolve({ data: { agent: {
name: 'maya-claude', instanceId: 'default', displayName: 'Maya',
runtime: { runtimeType: 'wrapper', adapter: 'claude' },
config: { environment: { model: 'opus', effort: 'max' } },
} } });
return Promise.resolve({ data: [] });
});
renderPage();
expect(await screen.findByText('Single CLI')).toBeInTheDocument();
expect(screen.queryByText('max')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Save runtime' })).not.toBeInTheDocument();
});
Loading
Loading