fixup(cli-scope): build error, false-positive on custom ops, tests, drop FORK.md
Addresses review feedback on this branch: - Fix TS2352 build error in dispatch.ts: `getSession()` returns `Session`, which has no index signature, so `(s as Record<string, unknown>)` is rejected by tsc. `Session.agent_group_id` exists — read it directly. - Fix a regression introduced by dropping the `groupField in data` guard: the post-handler scope check now runs for *every* command under a whitelisted resource, including custom ops, which return ad-hoc shapes. `ncl groups config get` (access:open, reachable by a group-scoped agent) returns a config object with no `id` field → `data['id'] !== ctx.agentGroupId` → `forbidden`, even on the agent's own config. Fix: tag the auto-generated list/get handlers with `generic: 'list' | 'get'` on `CommandDef` (set in `registerResource`) and run the post-handler check only when `cmd.generic` is set. Generic handlers return raw DB rows that carry `scopeField`; custom ops are already pinned to the caller's group by the pre-handler `--id` auto-fill or the approval gate. Fail-closed-when-`scopeField`-missing is preserved (now scoped to generic list/get). - Tests: `dispatch.test.ts` mocks `getResource` (the real resources aren't registered in this unit), tags the two post-handler test commands as `generic`, and adds coverage for: custom op returning a non-row object not being rejected; `sessions-get` pre-handler returning "session not found" for foreign and non-existent UUIDs (no existence oracle) and allowing the caller's own session; generic list/get failing closed when a resource declares no `scopeField`. Full suite: 323 passing. - Remove FORK.md from the PR diff — it's the fork's personal README, carried in because the branch was cut from the fork's `main` rather than upstream. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,7 +94,7 @@ export async function dispatch(req: RequestFrame, ctx: CallerContext): Promise<R
|
||||
// existence oracle across group boundaries.
|
||||
if (cmd.resource === 'sessions' && req.command === 'sessions-get' && req.args.id) {
|
||||
const s = getSession(req.args.id as string);
|
||||
if (!s || (s as Record<string, unknown>).agent_group_id !== ctx.agentGroupId) {
|
||||
if (!s || s.agent_group_id !== ctx.agentGroupId) {
|
||||
return err(req.id, 'handler-error', `session not found: ${req.args.id}`);
|
||||
}
|
||||
}
|
||||
@@ -135,17 +135,26 @@ export async function dispatch(req: RequestFrame, ctx: CallerContext): Promise<R
|
||||
try {
|
||||
let data = await cmd.handler(parsed, ctx);
|
||||
|
||||
// Post-handler group scope enforcement: filter/verify results belong
|
||||
// to the caller's agent group. Catches leaks that pre-handler auto-fill
|
||||
// can't prevent (e.g. `groups list` where the id arg is skipped by the
|
||||
// generic list handler, or `sessions get` by UUID).
|
||||
if (ctx.caller === 'agent' && cmd.resource) {
|
||||
// Post-handler group-scope enforcement. Applies only to the auto-generated
|
||||
// `list` / `get` handlers (`cmd.generic`), which return raw DB rows carrying
|
||||
// the resource's `scopeField`:
|
||||
// - `list` → drop rows that don't belong to the caller's agent group
|
||||
// (covers `groups list`, where the generic list handler ignores
|
||||
// the auto-filled `--id`)
|
||||
// - `get` → reject if the single row belongs to another group
|
||||
// Custom operations return ad-hoc shapes (e.g. `groups config get` → a config
|
||||
// object with no `id`) and are NOT checked here — they would be falsely
|
||||
// rejected, and they're already pinned to the caller's group by the
|
||||
// pre-handler `--id` auto-fill (groups/destinations) or gated behind approval,
|
||||
// so they can't reach another group's data anyway.
|
||||
if (ctx.caller === 'agent' && cmd.resource && cmd.generic) {
|
||||
const configRow = getContainerConfig(ctx.agentGroupId);
|
||||
if ((configRow?.cli_scope ?? 'group') === 'group') {
|
||||
const def = getResource(cmd.resource);
|
||||
const groupField = def?.scopeField;
|
||||
if (!groupField) {
|
||||
// Fail closed: resource not declared as group-scope safe.
|
||||
// Fail closed: a whitelisted resource exposing list/get must declare
|
||||
// `scopeField` so its rows can be filtered.
|
||||
return err(req.id, 'forbidden', `"${cmd.resource}" is not available in group scope.`);
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
|
||||
Reference in New Issue
Block a user