Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ecef338
feat(agentlet): add capability-aware structured harness launches
mydmdm Sep 20, 2026
6bf8837
feat(agenetes): persist structured Profile launch recipes
mydmdm Sep 20, 2026
4803723
feat: add external Agent defaults and model preferences (#203)
mydmdm Sep 20, 2026
db698b3
test: align CI fixtures with explicit Agent defaults
mydmdm Sep 21, 2026
be2d517
feat(agentlet): unify harness and Custom command wrappers
mydmdm Sep 21, 2026
6b0c61b
feat(agenetes): support versioned editable Profile templates
mydmdm Sep 21, 2026
36d8004
feat: unify capability-driven Profile creation and editing (#203)
mydmdm Sep 21, 2026
0502e35
feat: default supported discovered Profiles to auto-approval (#203)
mydmdm Sep 23, 2026
134ce38
feat(acp): enable isolated Job execution with existing session lifecycle
mydmdm Sep 23, 2026
e52df2f
feat: run text metadata tasks through external Agent Profiles (#203)
mydmdm Sep 23, 2026
1f53d31
fix: autosave default Agent settings (#203)
mydmdm Sep 23, 2026
dd400ed
feat: validate ACP image input and fold host report events
mydmdm Sep 24, 2026
0b806e1
feat: run image labeling and Ink intent through external Agents (#203)
mydmdm Sep 24, 2026
b27a2ec
Merge main into fix/issue-203 and adapt external Agent integration
mydmdm Sep 28, 2026
a261757
feat(agent): restore explicit Built-In Pi alongside external defaults
mydmdm Sep 29, 2026
1974f08
fix(agent): discover typed profiles alongside legacy commands
mydmdm Sep 29, 2026
0fcddbd
test(agent): align defaults contract with Built-In support
mydmdm Sep 29, 2026
e26018f
chore(agentlet): annotate intentional Custom shell execution
mydmdm Sep 29, 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
6 changes: 6 additions & 0 deletions apps/server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
} from './modules/agent/acp/index.js';
import { buildLegacyCommandProfiles } from './modules/agent/acp/legacy-profile-migration.js';
import { listProfiles as listLegacyAcpProfiles } from './modules/agent/acp/profile-store.js';
import { initializeAgentDefaults } from './modules/agent/agent-defaults.js';
import agentDefaultsRoutes from './modules/agent/agent-defaults.route.js';
import agentRoutes from './modules/agent/agent.route.js';
import agentChangeReviewConfigRoutes from './modules/agent/change-review-config.route.js';
import llmRoutes from './modules/agent/llm.route.js';
Expand Down Expand Up @@ -321,10 +323,13 @@ const agentletGateway = mountAgenetes(app, {
});
let unregisterHarnessDiscovery: (() => void) | undefined;
app.addHook('onReady', async () => {
const registry = getAgentProfileRegistry();
if (registry) initializeAgentDefaults(registry.listProfiles());
unregisterHarnessDiscovery = registerHarnessProfileDiscovery({
gateway: agentletGateway,
getRegistry: getAgentProfileRegistry,
log: app.log,
onProfilesDiscovered: initializeAgentDefaults,
});
});
app.addHook('preClose', async () => unregisterHarnessDiscovery?.());
Expand Down Expand Up @@ -353,6 +358,7 @@ app.addHook('onListen', async () => {
// (M3). See modules/agent/acp/profile-cache-port.ts.
installAcpProfileCachePort();
app.register(acpProfilesRoutes, { prefix: '/api/acp' });
app.register(agentDefaultsRoutes, { prefix: '/api/agent/defaults' });
app.register(acpAgentletRoutes, { prefix: '/api/acp' });
app.register(acpAgentCliRoutes, { prefix: '/api/acp' });
app.register(acpThreadsRoutes, { prefix: '/api/acp' });
Expand Down
49 changes: 49 additions & 0 deletions apps/server/src/modules/agent/acp/agent-cli.route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import { markBasicAuthenticated } from '../../security/owner.js';

import type { FastifyInstance } from 'fastify';

const mocks = vi.hoisted(() => ({ getProfile: vi.fn(), discover: vi.fn() }));
vi.mock('@agenetes/agentlet-host', () => ({
getAgentProfileRegistry: () => ({ getProfile: mocks.getProfile }),
getSupervisedAgentletId: () => 'supervised',
getAgentletGateway: () => ({ discoverHarnesses: mocks.discover }),
}));

let app: FastifyInstance | undefined;

afterEach(async () => {
Expand Down Expand Up @@ -106,4 +113,46 @@ describe('ACP agent CLI route', () => {
expect(response.json().code).toBe('harness_discovery_unavailable');
expect(response.json()).not.toHaveProperty('agents');
});

it('queries the edited Profile machine and projects Custom for older catalogues', async () => {
mocks.getProfile.mockReturnValue({ agentletId: 'remote-machine' });
mocks.discover.mockResolvedValue({ harnesses: [] });
app = Fastify({ logger: false });
await app.register(createAcpAgentCliRoutes(), { prefix: '/api/acp' });
const response = await app.inject(
'/api/acp/agent-cli?profileId=remote-profile',
);
expect(response.statusCode).toBe(200);
expect(mocks.discover).toHaveBeenCalledWith('remote-machine', {
prepareWorkspaces: false,
});
expect(response.json().agents).toEqual([
expect.objectContaining({
id: 'custom',
capabilities: {
customLaunchCommand: 'supported',
autoApprove: 'unsupported',
modelOverride: 'unsupported',
sessionPersistence: 'unsupported',
},
}),
]);
});

it('rejects invalid query fields and missing Profiles without detection', async () => {
const detect = vi.fn(async () => []);
mocks.getProfile.mockReturnValue(undefined);
app = Fastify({ logger: false });
await app.register(createAcpAgentCliRoutes(detect), { prefix: '/api/acp' });
expect((await app.inject('/api/acp/agent-cli?profileId=')).statusCode).toBe(
400,
);
expect(
(await app.inject('/api/acp/agent-cli?unexpected=value')).statusCode,
).toBe(400);
expect(
(await app.inject('/api/acp/agent-cli?profileId=missing')).statusCode,
).toBe(404);
expect(detect).not.toHaveBeenCalled();
});
});
56 changes: 50 additions & 6 deletions apps/server/src/modules/agent/acp/agent-cli.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@
import {
getAgentletGateway,
getSupervisedAgentletId,
getAgentProfileRegistry,
} from '@agenetes/agentlet-host';
import {
CUSTOM_COMMAND_CAPABILITIES,
CUSTOM_COMMAND_WRAPPER_ID,
} from '@agentlet/protocol';

import { acpAgentCliQuerySchema } from '@huabu/shared';

import { isOwnerRequest } from '../../security/owner.js';

Expand All @@ -29,13 +36,34 @@ import type {
} from '@huabu/shared';
import type { FastifyPluginAsync } from 'fastify';

async function detectAgentClis(): Promise<AcpAgentCliInfo[]> {
async function detectAgentClis(profileId?: string): Promise<AcpAgentCliInfo[]> {
const gateway = getAgentletGateway();
if (!gateway) throw new Error('Agentlet Gateway is not ready');
const result = await gateway.discoverHarnesses(getSupervisedAgentletId(), {
prepareWorkspaces: false,
});
return result.harnesses;
const profile = profileId
? getAgentProfileRegistry()?.getProfile(profileId)
: undefined;
if (profileId && !profile) throw new Error('Agent Profile is unavailable');
const result = await gateway.discoverHarnesses(
profile?.agentletId ?? getSupervisedAgentletId(),
{
prepareWorkspaces: false,
},
);
if (result.harnesses.some((entry) => entry.id === CUSTOM_COMMAND_WRAPPER_ID))
return result.harnesses;
return [
...result.harnesses,
{
id: CUSTOM_COMMAND_WRAPPER_ID,
displayName: 'Custom command',
binary: CUSTOM_COMMAND_WRAPPER_ID,
acpArgs: [],
autoApprove: null,
installed: false,
installHint: '',
capabilities: CUSTOM_COMMAND_CAPABILITIES,
},
];
}

export function createAcpAgentCliRoutes(
Expand All @@ -51,8 +79,24 @@ export function createAcpAgentCliRoutes(
'Forbidden: agent CLI detection requires owner authorization',
});
}
const parsed = acpAgentCliQuerySchema.safeParse(request.query);
if (!parsed.success) {
return reply.status(400).send({
code: 'validation_failed',
message: 'Invalid harness catalogue query',
});
}
try {
return { agents: await detect() };
if (
parsed.data.profileId &&
!getAgentProfileRegistry()?.getProfile(parsed.data.profileId)
) {
return reply.status(404).send({
code: 'profile_not_found',
message: 'Agent Profile is unavailable',
});
}
return { agents: await detect(parsed.data.profileId) };
} catch (error) {
request.log.warn(
{ err: error },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ function createHarness(options?: {
truncated: false,
},
});
const subscribeProfileCache = vi.fn();
const service = new ExternalAgentRealizationService({
resolveAgentNode: vi
.fn()
Expand All @@ -167,7 +168,7 @@ function createHarness(options?: {
readRecord: vi.fn(() => durableRecord),
createHandle,
buildSpec,
subscribeProfileCache: vi.fn(),
subscribeProfileCache,
subscribeTitles,
ensureSession,
...(options?.bindingCoordinator
Expand All @@ -184,6 +185,7 @@ function createHarness(options?: {
createHandle,
subscribeTitles,
buildSpec,
subscribeProfileCache,
collectSpacePrompt,
ensureSession,
promote,
Expand Down Expand Up @@ -464,6 +466,7 @@ describe('ExternalAgentRealizationService', () => {
workloadType: 'Deployment',
spec: {
binding: { alias: 'Fixed Agent', profileId: 'profile-fixed' },
profileExecutionRevision: 3,
agentletId: 'agentlet-1',
cwd: '/fixed/work',
recipe: null,
Expand Down Expand Up @@ -491,6 +494,11 @@ describe('ExternalAgentRealizationService', () => {
});

expect(realized.spec).toBe(persisted);
expect(harness.subscribeProfileCache).toHaveBeenCalledWith(
'thread-1',
'profile-fixed',
3,
);
expect(harness.buildSpec).not.toHaveBeenCalled();
expect(harness.collectSpacePrompt).not.toHaveBeenCalled();
expect(harness.subscribeTitles).toHaveBeenCalledWith(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ async function ensureSessionFromCanonicalSpec(
threadId: spec.threadId,
binding: spec.spec.binding,
namespace: spec.namespace,
profileExecutionRevision: spec.spec.profileExecutionRevision ?? 0,
...(spec.spec.cwd !== undefined && { cwd: spec.spec.cwd }),
...(spec.spec.recipe !== undefined && { recipe: spec.spec.recipe }),
...(env !== undefined && { env }),
Expand Down Expand Up @@ -298,6 +299,7 @@ export class ExternalAgentRealizationService {
this.dependencies.subscribeProfileCache(
options.threadId,
binding.profileId,
spec.spec.profileExecutionRevision ?? 0,
);
this.dependencies.subscribeTitles?.(
options.canvasId ?? '',
Expand Down Expand Up @@ -390,6 +392,7 @@ export class ExternalAgentRealizationService {
this.dependencies.subscribeProfileCache(
options.threadId,
binding.profileId,
spec.spec.profileExecutionRevision ?? 0,
);
this.dependencies.subscribeTitles?.(
options.canvasId ?? '',
Expand Down
Loading
Loading