Skip to content

Commit 5da033c

Browse files
committed
fix: sanitize OneCLI agent identifiers
1 parent 1b08b58 commit 5da033c

4 files changed

Lines changed: 93 additions & 7 deletions

File tree

src/container-runner.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { readContainerConfig, writeContainerConfig } from './container-config.js
2323
import { CONTAINER_RUNTIME_BIN, hostGatewayArgs, readonlyMountArgs, stopContainer } from './container-runtime.js';
2424
import { composeGroupClaudeMd } from './claude-md-compose.js';
2525
import { getAgentGroup } from './db/agent-groups.js';
26+
import { toOneCliAgentIdentifier } from './onecli-agent-identifier.js';
2627
import { getDb, hasTable } from './db/connection.js';
2728
import { initGroupFilesystem } from './group-init.js';
2829
import { stopTypingRefresh } from './modules/typing/index.js';
@@ -134,9 +135,9 @@ async function spawnContainer(session: Session): Promise<void> {
134135

135136
const mounts = buildMounts(agentGroup, session, containerConfig, contribution);
136137
const containerName = `nanoclaw-v2-${agentGroup.folder}-${Date.now()}`;
137-
// OneCLI agent identifier is always the agent group id — stable across
138-
// sessions and reversible via getAgentGroup() for approval routing.
139-
const agentIdentifier = agentGroup.id;
138+
// OneCLI agent identifier is the stable agent group id normalized to
139+
// OneCLI's identifier format.
140+
const agentIdentifier = toOneCliAgentIdentifier(agentGroup.id);
140141
const args = await buildContainerArgs(
141142
mounts,
142143
containerName,

src/modules/approvals/onecli-approvals.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { OneCLI, type ApprovalRequest, type ManualApprovalHandle } from '@onecli
2121

2222
import { pickApprovalDelivery, pickApprover } from './primitive.js';
2323
import { ONECLI_API_KEY, ONECLI_URL } from '../../config.js';
24-
import { getAgentGroup } from '../../db/agent-groups.js';
24+
import { resolveAgentGroupByOneCliIdentifier } from '../../onecli-agent-identifier.js';
2525
import {
2626
createPendingApproval,
2727
deletePendingApproval,
@@ -114,9 +114,15 @@ async function handleRequest(request: ApprovalRequest): Promise<Decision> {
114114
if (!adapterRef) return 'deny';
115115

116116
// Originating agent group is carried on the request via OneCLI's agent
117-
// identifier (set by container-runner.ts to agentGroup.id). Use it as
118-
// the scope for approver selection: admin @ group → global admin → owner.
119-
const originGroup = request.agent.externalId ? getAgentGroup(request.agent.externalId) : undefined;
117+
// identifier. Resolve it back to the database id for scoped approver selection:
118+
// admin @ group → global admin → owner.
119+
const originGroup = request.agent.externalId ? resolveAgentGroupByOneCliIdentifier(request.agent.externalId) : undefined;
120+
if (request.agent.externalId && !originGroup) {
121+
log.warn('OneCLI approval agent identifier did not resolve to a unique agent group', {
122+
id: request.id,
123+
agent: request.agent.externalId,
124+
});
125+
}
120126
const agentGroupId = originGroup?.id ?? null;
121127
const approvers = pickApprover(agentGroupId);
122128
if (approvers.length === 0) {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { beforeEach, describe, expect, it } from 'vitest';
2+
import { createAgentGroup } from './db/agent-groups.js';
3+
import { closeDb, initTestDb, runMigrations } from './db/index.js';
4+
import { resolveAgentGroupByOneCliIdentifier, toOneCliAgentIdentifier } from './onecli-agent-identifier.js';
5+
6+
describe('OneCLI agent identifiers', () => {
7+
beforeEach(() => {
8+
closeDb();
9+
const db = initTestDb();
10+
runMigrations(db);
11+
});
12+
13+
it('converts agent group IDs from underscores to hyphens', () => {
14+
expect(toOneCliAgentIdentifier('ag_f249a3521081')).toBe('ag-f249a3521081');
15+
});
16+
17+
it('converts all underscores and satisfies OneCLI validation', () => {
18+
const identifier = toOneCliAgentIdentifier('ag_foo_bar');
19+
20+
expect(identifier).toBe('ag-foo-bar');
21+
expect(identifier).toMatch(/^[a-z0-9-]+$/);
22+
});
23+
24+
it('resolves a normalized OneCLI identifier back to the database agent group', () => {
25+
createAgentGroup({
26+
id: 'ag_f249a3521081',
27+
name: 'Test Agent',
28+
folder: 'test-agent',
29+
agent_provider: 'claude',
30+
created_at: new Date().toISOString(),
31+
});
32+
33+
expect(resolveAgentGroupByOneCliIdentifier('ag-f249a3521081')?.id).toBe('ag_f249a3521081');
34+
});
35+
36+
it('throws when normalization still produces an invalid OneCLI identifier', () => {
37+
expect(() => toOneCliAgentIdentifier('ag_Foo')).toThrow(/invalid OneCLI identifier/);
38+
expect(() => toOneCliAgentIdentifier('ag/foo')).toThrow(/invalid OneCLI identifier/);
39+
expect(() => toOneCliAgentIdentifier('')).toThrow(/invalid OneCLI identifier/);
40+
});
41+
42+
it('does not resolve ambiguous normalized identifiers', () => {
43+
createAgentGroup({
44+
id: 'ag_foo',
45+
name: 'Underscore Agent',
46+
folder: 'underscore-agent',
47+
agent_provider: 'claude',
48+
created_at: new Date().toISOString(),
49+
});
50+
51+
createAgentGroup({
52+
id: 'ag-foo',
53+
name: 'Hyphen Agent',
54+
folder: 'hyphen-agent',
55+
agent_provider: 'claude',
56+
created_at: new Date().toISOString(),
57+
});
58+
59+
expect(resolveAgentGroupByOneCliIdentifier('ag-foo')).toBeUndefined();
60+
});
61+
});

src/onecli-agent-identifier.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { getAllAgentGroups } from './db/agent-groups.js';
2+
3+
const ONECLI_AGENT_IDENTIFIER_PATTERN = /^[a-z0-9-]+$/;
4+
5+
export function toOneCliAgentIdentifier(agentGroupId: string): string {
6+
const identifier = agentGroupId.replace(/_/g, '-');
7+
8+
if (!ONECLI_AGENT_IDENTIFIER_PATTERN.test(identifier)) {
9+
throw new Error(`Agent group ID "${agentGroupId}" produces an invalid OneCLI identifier: "${identifier}"`);
10+
}
11+
12+
return identifier;
13+
}
14+
15+
export function resolveAgentGroupByOneCliIdentifier(identifier: string) {
16+
const matches = getAllAgentGroups().filter((agentGroup) => toOneCliAgentIdentifier(agentGroup.id) === identifier);
17+
return matches.length === 1 ? matches[0] : undefined;
18+
}

0 commit comments

Comments
 (0)