Skip to content
Open
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 apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@agenetes/runtime": "workspace:*",
"@agentclientprotocol/sdk": "^0.22.1",
"@agentlet/protocol": "workspace:*",
"@agentlet/resources": "workspace:*",
"@earendil-works/pi-agent-core": "^0.81.1",
"@earendil-works/pi-ai": "^0.81.1",
"@fastify/compress": "^8.3.1",
Expand Down
35 changes: 35 additions & 0 deletions apps/server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { unlinkSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import {
enumerateLocalResources,
resolveResourceRoot,
} from '@agentlet/resources';
import compress from '@fastify/compress';
import cors from '@fastify/cors';
import multipart from '@fastify/multipart';
Expand All @@ -31,6 +35,10 @@ import {
listProfiles as listLegacyAcpProfiles,
removeProfiles as removeLegacyAcpProfiles,
} from './modules/agent/acp/profile-store.js';
import {
HUABU_RESOURCES,
huabuResourceValidationPort,
} from './modules/agent/acp/resources.js';
import agentRoutes from './modules/agent/agent.route.js';
import llmRoutes from './modules/agent/llm.route.js';
import { registerOpCounterHook } from './modules/agent/memory/op-counter-hook.js';
Expand Down Expand Up @@ -309,6 +317,20 @@ try {
app.log.warn({ err }, '[acp] could not remove legacy acp-config.json');
}
}
const localResources = enumerateLocalResources(
resolveResourceRoot(),
getSupervisedAgentletId(),
);
for (const diagnostic of localResources.diagnostics) {
app.log.warn(
{
receiptPath: diagnostic.receiptPath,
code: diagnostic.code,
},
`[agent-resources] ${diagnostic.message}`,
);
}

const agentletGateway = mountAgenetes(app, {
connectionToken: getConnectionToken(),
dataDir: getDataDir(),
Expand All @@ -323,6 +345,18 @@ const agentletGateway = mountAgenetes(app, {
// docs/architecture/agent-reachback.md ("Environment injection and isolation").
hostEnvPrefix: 'HUABU_',
hostEnvAllowlist: [],
hostEnvDenylist: [
'TAVILY_API_KEY',
'RAPIDAPI_KEY',
'AZURE_OPENAI_API_KEY',
'AZURE_OPENAI_API_ENDPOINT',
'AZURE_OPENAI_API_DEPLOYMENT_NAME',
],
resources: {
storageDir: join(getDataDir(), 'agent-resources'),
initialResources: [...HUABU_RESOURCES, ...localResources.records],
reconciledProviders: ['huabu', getSupervisedAgentletId()],
},
agentTeam: {
storageDir: join(getDataDir(), 'agent-team'),
secretStore: {
Expand All @@ -335,6 +369,7 @@ const agentletGateway = mountAgenetes(app, {
process.cwd(),
),
onLegacyProfilesMigrated: removeLegacyAcpProfiles,
resourceValidationPort: huabuResourceValidationPort,
},
});
// Legacy `agent-team` ACP records predate managed Agent Teams. They can't
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/modules/agent-team/agent-team.route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import type { FastifyInstance } from 'fastify';

function profile() {
return {
schemaVersion: 2 as const,
id: 'profile-1',
alias: 'Reviewer',
agentletId: 'machine-a',
workingDirPath: '/teams/reviewer/workspaces/copilot',
resourceIds: [],
launch: {
kind: 'agent-team-manifest' as const,
manifestPath: '/teams/reviewer/agentlet.yaml',
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/modules/agent-team/agent-team.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ const badRequestCodes = new Set([
'invalid_config_value',
'invalid_profile_kind',
'invalid_profile_patch',
'invalid_resource_ids',
'invalid_root',
'invalid_working_directory',
'unsupported_harness',
Expand Down Expand Up @@ -267,6 +268,7 @@ export function createAgentTeamRoutes(
alias: parsed.data.alias,
agentletId: parsed.data.agentletId,
workingDirPath,
resourceIds: parsed.data.resourceIds,
manifestPath: parsed.data.launch.manifestPath,
harness: parsed.data.launch.harness,
...(parsed.data.customData === undefined
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/agent/acp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
export {
mountAgenetes,
getAgentTeamRegistry,
getResourceRegistry,
getSupervisedAgentletId,
ACP_UPGRADE_PATH,
} from '@agenetes/agentlet-host';
Expand Down
34 changes: 34 additions & 0 deletions apps/server/src/modules/agent/acp/profiles.route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ const mocks = vi.hoisted(() => ({
listSelectableProfileIds: vi.fn(),
createProfile: vi.fn(),
},
resourceRegistry: {
list: vi.fn(),
},
}));

vi.mock('@agenetes/agentlet-host', () => ({
getAgentTeamRegistry: () => mocks.registry,
getResourceRegistry: () => mocks.resourceRegistry,
getDaemonSupervisor: () => ({
getStatus: () => ({ online: true, restartAttempt: 0 }),
}),
Expand All @@ -33,18 +37,22 @@ vi.mock('./profile-schema-cache.js', () => ({
}));

const commandProfile = {
schemaVersion: 2,
id: 'command-1',
alias: 'Copilot',
agentletId: 'machine-a',
workingDirPath: '/work/project',
resourceIds: [],
launch: { kind: 'acp-command' as const, command: 'copilot --acp' },
};

const manifestProfile = {
schemaVersion: 2,
id: 'team-1',
alias: 'Reviewer',
agentletId: 'machine-b',
workingDirPath: '/teams/reviewer/workspaces/claude',
resourceIds: [],
launch: {
kind: 'agent-team-manifest' as const,
manifestPath: '/teams/reviewer/agentlet.yaml',
Expand Down Expand Up @@ -85,11 +93,37 @@ describe('ACP Profile catalog routes', () => {
agentletId: 'machine-a',
command: 'copilot --acp',
workingDirPath: '/work/project',
resourceIds: [],
metadata: { cliId: 'copilot' },
});

expect(response.json()).toEqual(commandProfile);
});

it('lists the owner-facing Agent Resource catalogue', async () => {
const resources = [
{
schemaVersion: 1,
id: 'huabu-access',
name: 'Huabu Access',
provider: 'huabu',
description: 'Access the Space',
instructions: 'Fetch $HUABU_RFS_URL/skill.',
},
];
mocks.resourceRegistry.list.mockReturnValue(resources);
app = Fastify({ logger: false });
await app.register(acpProfilesRoutes, { prefix: '/api/acp' });

const response = await app.inject({
method: 'GET',
url: '/api/acp/resources',
});

expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ resources });
});

it('lists every Profile but selects only runtime-ready resources', async () => {
mocks.registry.listProfiles.mockReturnValue([
commandProfile,
Expand Down
97 changes: 77 additions & 20 deletions apps/server/src/modules/agent/acp/profiles.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
*/

import {
AgentTeamError,
getAgentTeamRegistry,
getDaemonSupervisor,
getResourceRegistry,
getSupervisedAgentletId,
} from '@agenetes/agentlet-host';

Expand All @@ -41,12 +43,14 @@ import {
deleteProfile as deleteLegacyProfile,
getProfile as getLegacyProfile,
} from './profile-store.js';
import { ResourceRegistryUnavailableError } from './resources.js';
import { isOwnerRequest } from '../../security/owner.js';

import type { AcpCommandProfile, AgentProfile } from '@agenetes/agentlet-host';
import type {
AcpProfileMutationResponse,
AcpProfilesListResponse,
AgentResourceListResponse,
ApiResult,
} from '@huabu/shared';
import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';
Expand All @@ -64,6 +68,28 @@ function isCommandProfile(profile: AgentProfile): profile is AcpCommandProfile {
return profile.launch.kind === 'acp-command';
}

function sendProfileError(error: unknown, reply: FastifyReply): FastifyReply {
if (error instanceof ResourceRegistryUnavailableError) {
return reply.status(503).send({
message: error.message,
code: 'resource_registry_unavailable',
});
}
if (error instanceof AgentTeamError) {
const status =
error.code === 'invalid_resource_ids'
? 400
: error.code === 'profile_not_found'
? 404
: 409;
return reply.status(status).send({
message: error.message,
code: error.code,
});
}
throw error;
}

const acpProfilesRoutes: FastifyPluginAsync = async (app) => {
// ── List ─────────────────────────────────────────────────────────────
app.get<{ Reply: ApiResult<AcpProfilesListResponse> }>(
Expand All @@ -80,6 +106,21 @@ const acpProfilesRoutes: FastifyPluginAsync = async (app) => {
},
);

app.get<{ Reply: ApiResult<AgentResourceListResponse> }>(
'/resources',
async (request, reply) => {
if (denyRemote(request, reply)) return;
const registry = getResourceRegistry();
if (!registry) {
return reply.status(503).send({
message: 'Agent Resource Registry is not ready',
code: 'resource_registry_unavailable',
});
}
return { resources: registry.list() };
},
);

// ── Create ──────────────────────────────────────────────────────────
app.post<{ Reply: ApiResult<AcpProfileMutationResponse> }>(
'/profiles',
Expand All @@ -99,17 +140,23 @@ const acpProfilesRoutes: FastifyPluginAsync = async (app) => {
code: 'profile_registry_unavailable',
});
}
const created = registry.createProfile({
launchKind: 'acp-command',
alias: parsed.data.alias,
agentletId: getSupervisedAgentletId(),
command: parsed.data.launch.command,
workingDirPath: parsed.data.workingDirPath,
...(parsed.data.metadata && { metadata: parsed.data.metadata }),
...(parsed.data.customData === undefined
? {}
: { customData: parsed.data.customData }),
});
let created: AgentProfile;
try {
created = registry.createProfile({
launchKind: 'acp-command',
alias: parsed.data.alias,
agentletId: getSupervisedAgentletId(),
command: parsed.data.launch.command,
workingDirPath: parsed.data.workingDirPath,
resourceIds: parsed.data.resourceIds,
...(parsed.data.metadata && { metadata: parsed.data.metadata }),
...(parsed.data.customData === undefined
? {}
: { customData: parsed.data.customData }),
});
} catch (error) {
return sendProfileError(error, reply);
}
if (!isCommandProfile(created)) {
throw new Error('Agent Profile registry returned an invalid kind');
}
Expand Down Expand Up @@ -155,15 +202,25 @@ const acpProfilesRoutes: FastifyPluginAsync = async (app) => {
if (!registry) {
throw new Error('Agent Profile registry became unavailable');
}
const updated = registry.patchProfile(request.params.id, {
...(parsed.data.alias === undefined ? {} : { alias: parsed.data.alias }),
...(parsed.data.customData === undefined
? {}
: { customData: parsed.data.customData }),
...(parsed.data.metadata === undefined
? {}
: { metadata: parsed.data.metadata }),
});
let updated: AgentProfile;
try {
updated = registry.patchProfile(request.params.id, {
...(parsed.data.alias === undefined
? {}
: { alias: parsed.data.alias }),
...(parsed.data.customData === undefined
? {}
: { customData: parsed.data.customData }),
...(parsed.data.metadata === undefined
? {}
: { metadata: parsed.data.metadata }),
...(parsed.data.resourceIds === undefined
? {}
: { resourceIds: parsed.data.resourceIds }),
});
} catch (error) {
return sendProfileError(error, reply);
}
if (!isCommandProfile(updated)) {
throw new Error('Agent Profile registry returned an invalid kind');
}
Expand Down
Loading
Loading