open-design/apps/daemon/tests/plugins-tool-token-gate.test.ts
Cursor Agent 44ea30c986
feat(plugins): trust mutation + connector tool-token gate
Plan §3.A2 (trust mutation) and §3.A3 (connector capability gate).

- New POST /api/plugins/:id/trust accepts { capabilities[], action: 'grant' | 'revoke' }
  and unions the spec §5.3 vocabulary into installed_plugins.capabilities_granted.
  Unknown / malformed strings come back as HTTP 400 with the rejected list so the
  CLI can surface exit-2 usage advice. The implicit prompt:inject floor is
  preserved on revoke so a plugin never falls below the spec minimum.
- trust.ts gains validateCapabilityList / grantCapabilities / revokeCapabilities.
  These are the only writers of installed_plugins.capabilities_granted outside
  of install.
- od plugin trust calls the new endpoint instead of printing the deferred
  Phase 3 stub. --revoke flag and --grant-caps flag added; PLUGIN_BOOLEAN_FLAGS
  / PLUGIN_STRING_FLAGS extended.
- Tool-token grants now optionally carry pluginSnapshotId / pluginTrust /
  pluginCapabilitiesGranted. The mint site in startChatRun resolves these
  off the run's appliedPluginSnapshotId so the connector execute route can
  re-validate per-call without re-reading SQLite.
- /api/tools/connectors/execute calls a new pure helper
  checkConnectorAccess(grant, connectorId). Trusted / bundled implicitly
  carry connector:*; restricted plugins must list connector:<id> (or the
  coarse connector capability) — otherwise 403 CONNECTOR_NOT_GRANTED.

Daemon test suite: 1417 / 1417 (added plugins-trust + plugins-tool-token-gate).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 11:14:51 +00:00

82 lines
2.9 KiB
TypeScript

// Plan §3.A3 / spec §9: connector tool-token capability gate.
//
// Token-level §5.3 enforcement (no SQLite reads needed at execute time):
//
// 1. Non-plugin runs: grant has no plugin context; gate is bypassed.
// 2. Trusted plugins: implicit `connector:*`; any connector id passes.
// 3. Restricted plugins: must list `connector:<id>` in
// pluginCapabilitiesGranted; otherwise the call is rejected so a
// replayed token can't reach a connector that was never granted.
import { describe, expect, it } from 'vitest';
import {
checkConnectorAccess,
ToolTokenRegistry,
type ToolTokenGrant,
} from '../src/tool-tokens.js';
function mintGrant(registry: ToolTokenRegistry, overrides: Partial<{
pluginSnapshotId: string;
pluginTrust: 'trusted' | 'restricted' | 'bundled';
pluginCapabilitiesGranted: string[];
}> = {}): ToolTokenGrant {
return registry.mint({
runId: 'run-1',
projectId: 'project-1',
...overrides,
});
}
describe('checkConnectorAccess', () => {
it('lets non-plugin runs through (no snapshot id on the grant)', () => {
const registry = new ToolTokenRegistry();
const grant = mintGrant(registry);
expect(checkConnectorAccess(grant, 'slack')).toEqual({ ok: true });
});
it('lets trusted plugins through (implicit connector:*)', () => {
const registry = new ToolTokenRegistry();
const grant = mintGrant(registry, {
pluginSnapshotId: 'snap-1',
pluginTrust: 'trusted',
pluginCapabilitiesGranted: ['prompt:inject'],
});
expect(checkConnectorAccess(grant, 'slack')).toEqual({ ok: true });
});
it('rejects restricted plugins missing connector:<id>', () => {
const registry = new ToolTokenRegistry();
const grant = mintGrant(registry, {
pluginSnapshotId: 'snap-2',
pluginTrust: 'restricted',
pluginCapabilitiesGranted: ['prompt:inject', 'fs:read'],
});
const result = checkConnectorAccess(grant, 'slack');
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.reason).toMatch(/connector:slack/);
}
});
it('accepts restricted plugins with the explicit connector:<id> grant', () => {
const registry = new ToolTokenRegistry();
const grant = mintGrant(registry, {
pluginSnapshotId: 'snap-3',
pluginTrust: 'restricted',
pluginCapabilitiesGranted: ['prompt:inject', 'connector:notion'],
});
expect(checkConnectorAccess(grant, 'notion')).toEqual({ ok: true });
expect(checkConnectorAccess(grant, 'slack').ok).toBe(false);
});
it('accepts the coarse `connector` grant for any id', () => {
const registry = new ToolTokenRegistry();
const grant = mintGrant(registry, {
pluginSnapshotId: 'snap-4',
pluginTrust: 'restricted',
pluginCapabilitiesGranted: ['prompt:inject', 'connector'],
});
expect(checkConnectorAccess(grant, 'slack')).toEqual({ ok: true });
expect(checkConnectorAccess(grant, 'notion')).toEqual({ ok: true });
});
});