Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
04b2ac5
Build rev3 agent Settings status and runtime readbacks (TASK-546)
lilyshen0722 Oct 25, 2026
ad43d13
Keep primary harness facts tied to the last spawn receipt
lilyshen0722 Oct 25, 2026
f92ace5
Refresh completed rev3 captures for the final review tree
lilyshen0722 Oct 25, 2026
5af6041
Keep agent name destinations usable for owners and other members
lilyshen0722 Oct 25, 2026
17d085a
Keep next-spawn provider controls out of primary runtime facts
lilyshen0722 Oct 25, 2026
4e8e115
Respect TASK-002 catalogue scope and preserve local CLI model entry
lilyshen0722 Oct 25, 2026
38efa40
Keep completion tied to the required reviews and production walk
lilyshen0722 Oct 25, 2026
51fa7c3
fix(settings): retain removal and live identity controls
lilyshen0722 Oct 25, 2026
753b1d3
docs(settings): refresh captures with verified Signal fonts
lilyshen0722 Oct 25, 2026
7824d2e
fix(settings): resolve Status and Runtime UX holds
lilyshen0722 Oct 25, 2026
eba984a
fix(settings): clear obsolete placement state and strengthen capture …
lilyshen0722 Oct 25, 2026
55b6bdd
fix(settings): hide inactive heartbeat controls and size phone select
lilyshen0722 Oct 25, 2026
fbc4a93
fix(settings): size catalog search and emphasize revoke confirmation
lilyshen0722 Oct 25, 2026
0b31084
fix(settings): name owner-wide quota and localize reset time
lilyshen0722 Oct 25, 2026
ffa147f
fix(settings): localize access-key timestamps
lilyshen0722 Oct 25, 2026
eddc3c3
fix(settings): localize provider key metadata timestamp
lilyshen0722 Oct 25, 2026
e2af5ff
fix(settings): use app language for remaining report dates
lilyshen0722 Oct 25, 2026
80b283a
fix(settings): read instance-key and runtime-token task claims
lilyshen0722 Oct 25, 2026
6e8f541
test(settings): discriminate held-task member pod boundary
lilyshen0722 Oct 25, 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
85 changes: 85 additions & 0 deletions backend/__tests__/service/agent-manage-settings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ const Pod = require('../../models/Pod');
const { AgentInstallation } = require('../../models/AgentRegistry');
const Machine = require('../../models/Machine');
const AgentMemory = require('../../models/AgentMemory');
const AgentRun = require('../../models/AgentRun');
const manageSettings = require('../../routes/registry/manage-settings');
const podAgents = require('../../routes/registry/pod-agents');
const agentConfig = require('../../routes/registry/agent-config');
const tasksApi = require('../../routes/tasksApi');
const { projectSeatEnvironments, seatEnvironmentKey } = require('../../services/seatEnvironmentProjection');

const SECRET = 'agent-manage-settings-test';
Expand All @@ -27,6 +29,7 @@ describe('per-installation agent settings', () => {
app.use('/api/registry', manageSettings);
app.use('/api/registry', podAgents);
app.use('/api/registry', agentConfig);
app.use('/api/tasks', tasksApi);
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 @@ -38,6 +41,7 @@ describe('per-installation agent settings', () => {

beforeEach(async () => {
await AgentInstallation.deleteMany({ agentName: 'settings-agent' });
await AgentRun.deleteMany({ agentName: 'settings-agent' });
await AgentInstallation.create([
{ agentName: 'settings-agent', instanceId: 'same', podId: podA._id, version: '1', installedBy: owner._id,
config: new Map([['wakeOnMessage', { enabled: true, humanOnly: true }], ['boardWake', { enabled: false }], ['runtime', { runtimeType: 'wrapper' }]]) },
Expand Down Expand Up @@ -69,6 +73,87 @@ describe('per-installation agent settings', () => {
expect(otherResponse.body.writablePodId).toBe(String(podB._id));
});

it('reads the actual latest run model only within the managed owner and pod', async () => {
const base = { agentName: 'settings-agent', instanceId: 'same', trigger: 'manual', status: 'succeeded' };
await AgentRun.create([
{ ...base, userId: String(owner._id), podId: podA._id, startedAt: new Date('2026-10-25T01:00:00Z'), turns: [{ turnIndex: 0, model: 'actual-model', elapsedMs: 1 }] },
{ ...base, userId: String(other._id), podId: podA._id, startedAt: new Date('2026-10-25T02:00:00Z'), turns: [{ turnIndex: 0, model: 'private-owner-model', elapsedMs: 1 }] },
{ ...base, userId: String(owner._id), podId: podB._id, startedAt: new Date('2026-10-25T03:00:00Z'), turns: [{ turnIndex: 0, model: 'private-pod-model', elapsedMs: 1 }] },
]);
await AgentInstallation.updateOne({ podId: podA._id, agentName: 'settings-agent' }, {
$set: { 'config.runtime': { runtimeType: 'hosted', model: 'saved-but-never-run' } },
});
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.runtimeRun).toEqual(expect.objectContaining({ model: 'actual-model', status: 'succeeded' }));
expect(JSON.stringify(response.body.runtimeRun)).not.toContain('private-owner-model');
expect(JSON.stringify(response.body.runtimeRun)).not.toContain('private-pod-model');
await AgentRun.create({ ...base, userId: String(owner._id), podId: podA._id,
startedAt: new Date('2026-10-25T04:00:00Z'), status: 'failed', turns: [] });
const failed = await request(app).get('/api/registry/agents/settings-agent/manage-settings?instanceId=same')
.set('Authorization', `Bearer ${token(owner)}`);
expect(failed.body.runtimeRun.model).toBeNull();
await AgentRun.deleteMany({ agentName: 'settings-agent' });
});

it('reads the hosted engine meter and omits usage for non-hosted installs', async () => {
await AgentRun.create({ agentName: 'settings-agent', instanceId: 'same', trigger: 'manual',
status: 'succeeded', userId: String(owner._id), podId: podA._id, startedAt: new Date(), turns: [] });
const url = '/api/registry/agents/settings-agent/manage-settings?instanceId=same';
const read = () => request(app).get(url).set('Authorization', `Bearer ${token(owner)}`);
await AgentInstallation.updateOne({ podId: podA._id, agentName: 'settings-agent' }, {
$set: { 'config.runtime': { runtimeType: 'hosted' }, 'config.hosted': { engine: 'native', provisionedAt: new Date().toISOString() } },
});
const native = await read();
expect(native.status).toBe(200);
expect(native.body.usage).toEqual(expect.objectContaining({ used: 1, cap: 30 }));
await AgentInstallation.updateOne({ podId: podA._id, agentName: 'settings-agent' }, {
$set: { 'config.hosted': { engine: 'durable-object' } },
});
const worker = await read();
expect(worker.body.usage).toEqual(expect.objectContaining({ used: 0, cap: 30 }));
await AgentInstallation.updateOne({ podId: podA._id, agentName: 'settings-agent' }, {
$set: { 'config.runtime': { runtimeType: 'native' } },
});
expect((await read()).body.usage).toBeNull();
});

it.each(['runtime-token claim', 'instance-id claim'])('shows a held task to members only (%s)', async (claimForm) => {
const Task = require('../../models/Task');
await Task.deleteMany({ taskId: 'TASK-546' });
await Pod.updateOne({ _id: podB._id }, { $pull: { members: admin._id } });
await User.deleteMany({ username: 'settings-runtime-bot' });
const bot = await User.create({ username: 'settings-runtime-bot', email: 'settings-runtime-bot@test.com',
password: 'password123', isBot: true, botType: 'agent',
botMetadata: { agentName: 'settings-agent', instanceId: 'same' },
agentRuntimeTokens: [{ tokenHash: require('crypto').createHash('sha256').update('cm_agent_settings_claim_fixture').digest('hex'), label: 'test', createdAt: new Date() }] });
await Task.create({ podId: podA._id, taskNum: 546, taskId: 'TASK-546', title: 'Private title',
status: 'pending' });
await Pod.updateOne({ _id: podA._id }, { $addToSet: { members: bot._id } });
const claim = await request(app).post(`/api/tasks/${podA._id}/TASK-546/claim`)
.set('Authorization', 'Bearer cm_agent_settings_claim_fixture');
expect(claim.status).toBe(200);
expect((await Task.findOne({ podId: podA._id, taskId: 'TASK-546' })).claimedBy).toBe(String(bot._id));
if (claimForm === 'instance-id claim') {
await Task.updateOne({ podId: podA._id, taskId: 'TASK-546' }, { $set: { claimedBy: 'same' } });
}
const url = '/api/registry/agents/settings-agent/manage-settings?instanceId=same';
const member = await request(app).get(url).set('Authorization', `Bearer ${token(owner)}`);
expect(member.body.heldTask).toEqual({ taskId: 'TASK-546', podId: String(podA._id), podName: 'Settings A' });
expect(JSON.stringify(member.body.heldTask)).not.toContain('Private title');
// Keep memberPodIds nonempty: an all-or-nothing empty-list guard must not
// stand in for filtering each claimed task's own pod.
await Pod.updateOne({ _id: podB._id }, { $addToSet: { members: admin._id } });
expect((await Pod.findById(podA._id)).members.map(String)).not.toContain(String(admin._id));
const outsider = await request(app).get(url).set('Authorization', `Bearer ${token(admin)}`);
expect(outsider.status).toBe(200);
expect(outsider.body.installations.map((entry) => entry.podId)).toContain(String(podB._id));
expect(outsider.body.heldTask).toBeNull();
await Pod.updateOne({ _id: podB._id }, { $pull: { members: admin._id } });
await Task.deleteMany({ taskId: 'TASK-546' });
});

it('reads and writes the same effort declaration the daemon selects', async () => {
await Machine.create({ ownerUserId: owner._id, machineId: 'machine-a', name: 'Test computer',
inventoryAt: new Date(), runtimeInventory: [{ harness: 'claude', installed: true, version: '2.1', models: null, efforts: ['high', 'xhigh'] }] });
Expand Down
34 changes: 31 additions & 3 deletions backend/routes/registry/manage-settings.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// Owner/admin controls for one agent installation at a time.
import rateLimit from 'express-rate-limit';
import { cloudflareIpRateLimitKeyGenerator } from '../../middleware/ipRateLimit';
import { hostedCapsForOwner, userRunCeilingForOwner } from '../../services/hostedRuntimeService';
import { hostedCapsForOwner, userRunCeilingForOwner, meterAllowsTurn } from '../../services/hostedRuntimeService';
import { hostedRunsNative } from '../../services/hostedEngine';
import { projectSeatEnvironments, seatEnvironmentKey } from '../../services/seatEnvironmentProjection';
const { ownKeyCapability } = require('../../services/agentProviderKey');
const Machine = require('../../models/Machine');
const AgentRuntimeObservation = require('../../models/AgentRuntimeObservation');
const AgentRun = require('../../models/AgentRun');
const Task = require('../../models/Task');
const express = require('express');
const auth = require('../../middleware/auth');
const { AgentInstallation } = require('../../models/AgentRegistry');
Expand Down Expand Up @@ -58,7 +61,7 @@ router.get('/agents/:name/manage-settings', settingsRateLimit, auth, async (req:
}).sort({ createdAt: 1, _id: 1 }).lean();
const manageable = installations.filter((entry: any) => canManage(entry, userId, isAdmin));
const pods = await Pod.find({ _id: { $in: manageable.map((entry: any) => entry.podId) } })
.select('name').lean();
.select('name members').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 fallback = visible.find((entry: any) => entry.status === 'active') || visible[0];
Expand Down Expand Up @@ -89,14 +92,31 @@ router.get('/agents/:name/manage-settings', settingsRateLimit, auth, async (req:
isBot: true,
'botMetadata.agentName': String(req.params.name).toLowerCase(),
'botMetadata.instanceId': normalizeInstanceId(req.query.instanceId),
}).select('botMetadata.machineId').lean();
}).select('botMetadata.machineId botMetadata.instanceId botMetadata.agentName').lean();
const machine = binding?.botMetadata?.machineId ? await Machine.findOne({
machineId: binding.botMetadata.machineId, ownerUserId: quotaOwner,
}).select('runtimeInventory inventoryAt lastSeenAt agentStates').lean() : null;
const observation = binding && machine ? await AgentRuntimeObservation.findOne({
agentUserId: binding._id, machineId: binding.botMetadata.machineId,
}).select('connectedAt lastSeenAt lastSpawn lastSpawnOutcome turnRunning lastTurnAt -_id').lean() : null;
const runtimeManaged = Boolean(binding?.botMetadata?.machineId);
// A saved preference is not evidence of the model a turn actually used.
const run = !runtimeManaged && visible.length ? await AgentRun.findOne({
agentName: String(req.params.name).toLowerCase(),
instanceId: normalizeInstanceId(req.query.instanceId),
userId: String(quotaOwner),
podId: { $in: visible.map((entry: any) => entry.podId) },
}).sort({ startedAt: -1, _id: -1 }).select('startedAt completedAt status turns.model').lean() : null;
// Managing an installation does not grant access to its pod's task board.
const memberPodIds = pods.filter((pod: any) => (pod.members || [])
.some((member: any) => String(member) === String(userId))).map((pod: any) => pod._id);
// Claims persisted by seat callers may use the instance key; runtime-token
// claims at this boundary also use the bot ObjectId. Read both forms.
const claimKeys = binding ? [String(binding._id),
binding.botMetadata?.instanceId || binding.botMetadata?.agentName].filter(Boolean) : [];
const heldTask = binding && memberPodIds.length ? await Task.findOne({
podId: { $in: memberPodIds }, claimedBy: { $in: claimKeys }, status: 'claimed',
}).sort({ claimedAt: -1, _id: -1 }).select('taskId podId -_id').lean() : null;
if (runtimeManaged && !runtimeControls.runtimeType) runtimeControls.runtimeType = 'wrapper';
const runtimeSavedControls = { ...runtimeControls };
// A standalone `agent run` reads its local token record. Hosted/native
Expand All @@ -110,7 +130,11 @@ router.get('/agents/:name/manage-settings', settingsRateLimit, auth, async (req:
const [{ turnsPerDay }, userRunCeiling] = await Promise.all([
hostedCapsForOwner(quotaOwner), userRunCeilingForOwner(quotaOwner),
]);
const usage = normalizeConfigMap(fallback?.config)?.runtime?.runtimeType === 'hosted'
? await meterAllowsTurn(req.params.name, normalizeInstanceId(req.query.instanceId), turnsPerDay,
{ countRuns: hostedRunsNative(fallback) }) : null;
return res.json({ turnCap: turnsPerDay, userRunCeiling, podCount: installations.length,
usage,
activePodCount: installations.filter((entry: any) => entry.status === 'active').length,
writablePodId: writable ? String(writable.podId) : null,
runtimeSourcePodId: !runtimeManaged ? null
Expand All @@ -120,6 +144,10 @@ router.get('/agents/:name/manage-settings', settingsRateLimit, auth, async (req:
runtimeSavedControls,
ownKeyCapability: runtimeManaged ? ownKeyCapability(runtime, environment) : { available: false, provider: null },
runtimeObservation: observation ? { ...observation, confirmed: null } : null,
runtimeRun: run ? { model: run.turns?.at(-1)?.model || null,
startedAt: run.startedAt, completedAt: run.completedAt || null, status: run.status } : null,
heldTask: heldTask ? { taskId: heldTask.taskId, podId: String(heldTask.podId),
podName: names.get(String(heldTask.podId)) } : null,
processConnected: machine ? Boolean(machine.lastSeenAt && Date.now() - new Date(machine.lastSeenAt).getTime() < 90_000
&& machine.agentStates?.some((entry: any) => entry.agentName === String(req.params.name).toLowerCase()
&& entry.instanceId === normalizeInstanceId(req.query.instanceId) && entry.state === 'running')) : null,
Expand Down
7 changes: 7 additions & 0 deletions docs/agents/daemon-seat-state-surfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,10 @@ ps -axo pid=,ppid=,args= | grep -E '[c]ommonlyai/mcp|mcp/0\.3'
- [review-checklist.md](../development/review-checklist.md) — rule 38: a read of
the installed artifact is not a read of the process running it, and the two
disagree for as long as the process outlives the file


## Settings rev3 readbacks (TASK-546)

The owner/admin manage-settings response keeps evidence separate from saved preferences. Machine-managed agents use the existing `runtimeObservation.lastSpawn`; hosted/native agents use `runtimeRun`, projected from the newest `AgentRun` for the managed owner and visible installation pods. `runtimeRun.model` is the last recorded turn's model. A run with no recorded model returns null; the UI must not substitute its saved model or a display default. The DTO contains no prompt, completion or tool arguments.

`heldTask` contains only task ID and pod identity, and only where the viewer is a pod member. Installation administration alone does not reveal a board claim. Claims can contain the bot ObjectId or its instance key; readbacks accept both persisted forms. The service regression claims through the actual runtime-token task endpoint (which writes the ObjectId at this head), then separately checks the instance-key form. Do not infer the stored key from `resolveAgentInstanceId` alone: that helper reads `req.user`, while runtime authentication populates `req.agentUser`. Service coverage is in `agent-manage-settings.test.js`, including a newer foreign-owner run and a non-member instance admin. The rev3 evidence directory under `docs/design/evidence/` contains fixture captures; these do not replace the required non-author production walk.
Loading
Loading