Compare commits

...

4 Commits

Author SHA1 Message Date
gavrielc 31cc35ea32 Merge branch 'main' into cleanup/dispatch-longest-prefix 2026-07-04 16:33:27 +03:00
github-actions[bot] bf0a3d2612 chore: bump version to 2.1.29 2026-07-04 13:31:46 +00:00
gavrielc 6dc25a9b8a Merge pull request #2930 from nanocoai/cleanup/command-gate-start-and-failopen
command-gate: restore the /start filter and remove the fail-open admin check
2026-07-04 16:31:32 +03:00
gavrielc f094f56bf6 Fix ncl positional IDs for generated (dashed) identifiers
The dispatcher trimmed exactly one trailing dash-segment to recover the
target id, so any generated id containing dashes (UUIDs, sess-*, appr-*)
never matched a command and failed unknown-command; only --id worked.
Resolve by longest registered prefix instead: split the dash-joined
command, take the longest prefix that lookup() resolves as the command,
and treat the re-joined remainder as args.id. Server-side only, no wire
or client change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 16:05:02 +03:00
3 changed files with 40 additions and 9 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "nanoclaw",
"version": "2.1.28",
"version": "2.1.29",
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
"type": "module",
"packageManager": "pnpm@10.33.0",
+26
View File
@@ -185,6 +185,16 @@ register({
handler: async (args) => ({ id: (args as Record<string, unknown>).id, agent_group_id: 'g1' }),
});
// Echoes args back — used to assert dash-joined positional id resolution.
register({
name: 'groups-get',
description: 'test command (groups get)',
resource: 'groups',
access: 'open',
parseArgs: (raw) => raw,
handler: async (args) => ({ echo: args }),
});
import { dispatch } from './dispatch.js';
import type { CallerContext } from './frame.js';
@@ -584,3 +594,19 @@ describe('CLI scope enforcement', () => {
}
});
});
// --- Dash-joined positional id resolution (generated ids contain dashes) ---
describe('dash-joined positional id resolution', () => {
it('resolves `groups-get-<uuid-with-dashes>` to (groups get, id=<uuid>)', async () => {
const uuid = '550e8400-e29b-41d4-a716-446655440000';
const resp = await dispatch({ id: '1', command: `groups-get-${uuid}`, args: {} }, { caller: 'host' });
expect(resp.ok).toBe(true);
if (resp.ok) {
const data = resp.data as { echo: Record<string, unknown> };
expect(data.echo.id).toBe(uuid);
}
});
});
+13 -8
View File
@@ -26,19 +26,24 @@ export async function dispatch(
): Promise<ResponseFrame> {
let cmd = lookup(req.command);
// Fallback: if the full command isn't registered, trim the last
// dash-segment and treat it as the target ID. This lets clients join
// all positional args with dashes (e.g. `ncl groups get abc123`
// → command "groups-get-abc123"trim → "groups-get" + id "abc123").
// Fallback: if the full command isn't registered, split the dash-joined
// command and treat the longest registered prefix as the command, with the
// re-joined remainder as the target ID. Clients join all positional args
// with dashes (e.g. `ncl groups get abc123`command "groups-get-abc123"),
// and generated ids (UUIDs, `sess-…`, `appr-…`) themselves contain dashes,
// so trimming a single trailing segment isn't enough — walk prefixes from
// longest to shortest so `groups-get-<uuid-with-dashes>` still resolves to
// "groups-get" + id "<uuid-with-dashes>".
if (!cmd) {
const idx = req.command.lastIndexOf('-');
if (idx > 0) {
const shortened = req.command.slice(0, idx);
const tail = req.command.slice(idx + 1);
const parts = req.command.split('-');
for (let i = parts.length - 1; i > 0; i--) {
const shortened = parts.slice(0, i).join('-');
const fallback = lookup(shortened);
if (fallback) {
const tail = parts.slice(i).join('-');
cmd = fallback;
req = { ...req, command: shortened, args: { ...req.args, id: req.args.id ?? tail } };
break;
}
}
}