mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
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>
82 lines
2.9 KiB
TypeScript
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 });
|
|
});
|
|
});
|