automation-ingest-security.test.mjs
107 lines 4.4 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 11 days ago
1 /**
2 * AIP-b security: agent cannot approve/CRUD; session hook isolation; elevated; no vault:write required;
3 * self-apply still refuses agent_access; Scooling fingerprints unchanged.
4 */
5 import { describe, it } from 'node:test';
6 import assert from 'node:assert/strict';
7 import fs from 'node:fs';
8 import path from 'node:path';
9 import { fileURLToPath } from 'node:url';
10 import {
11 agentScopesPermitMethod,
12 DEFAULT_AGENT_SCOPES,
13 ALLOWED_AGENT_SCOPES,
14 } from '../hub/lib/agent-credential-core.mjs';
15 import { roleEligibleForPersonalSelfApply } from '../lib/hub-proposal-personal-self-apply.mjs';
16 import { filterNotesByListOptions } from '../lib/list-notes.mjs';
17 import { isIngestContractBody, routeAutomationIngest, normalizeRuleForSave } from '../lib/automation-ingest-policy.mjs';
18
19 const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
21 describe('automation ingest security', () => {
22 it('ingest:automation is a known scope and is not a default mint scope', () => {
23 assert.equal(ALLOWED_AGENT_SCOPES.includes('ingest:automation'), true);
24 assert.equal(DEFAULT_AGENT_SCOPES.includes('ingest:automation'), false);
25 });
26
27 it('agent cannot approve or CRUD ingest-rules', () => {
28 const scopes = ['ingest:automation', 'vault:read'];
29 assert.equal(agentScopesPermitMethod(scopes, 'POST', 'api/v1/proposals/x/approve'), false);
30 assert.equal(agentScopesPermitMethod(scopes, 'POST', 'api/v1/proposals/x/discard'), false);
31 assert.equal(agentScopesPermitMethod(scopes, 'GET', 'api/v1/automation/ingest-rules'), false);
32 assert.equal(agentScopesPermitMethod(scopes, 'PUT', 'api/v1/automation/ingest-rules'), false);
33 assert.equal(agentScopesPermitMethod(scopes, 'POST', 'api/v1/automation/ingest-rules'), false);
34 assert.equal(agentScopesPermitMethod(scopes, 'DELETE', 'api/v1/automation/ingest-rules/ingr_1'), false);
35 });
36
37 it('session proposals with fingerprint are not an ingest contract unless ingest/class marker', () => {
38 assert.equal(
39 isIngestContractBody({ path: 'a.md', body: 'x', source_fingerprint: 'session-fp-01' }),
40 false
41 );
42 });
43
44 it('elevated body cannot stay direct_note', () => {
45 const r = normalizeRuleForSave({
46 label: 'direct',
47 disposition: 'direct_note',
48 match: { path_prefix: 'inbox/trends/' },
49 });
50 const routed = routeAutomationIngest(
51 {
52 path: 'inbox/trends/x.md',
53 body: 'ssn 123-45-6789 classified',
54 content_class: 'research',
55 triggers: {
56 literal_phrases: [{ match: 'classified', review_severity: 'elevated' }],
57 path_prefixes: [],
58 label_any: [],
59 },
60 },
61 [r]
62 );
63 assert.equal(routed.disposition, 'review_queue');
64 assert.equal(routed.elevated_override, true);
65 });
66
67 it('vault:write is not required for ingest route', () => {
68 assert.equal(
69 agentScopesPermitMethod(['ingest:automation', 'vault:read'], 'POST', '/api/v1/automation/ingest'),
70 true
71 );
72 });
73
74 it('roleEligibleForPersonalSelfApply still refuses agent_access', () => {
75 assert.equal(roleEligibleForPersonalSelfApply('editor', { tokenType: 'agent_access' }), false);
76 assert.equal(roleEligibleForPersonalSelfApply('member', { tokenType: 'agent_access' }), false);
77 });
78
79 it('content_class list filter exists (pre-AIP missing)', () => {
80 const notes = [
81 { path: 'a.md', content_class: 'research' },
82 { path: 'b.md', frontmatter: { content_class: 'ops' } },
83 ];
84 const research = filterNotesByListOptions(notes, { content_class: 'research' });
85 assert.equal(research.length, 1);
86 assert.equal(research[0].path, 'a.md');
87 });
88
89 it('Scooling fingerprint constants unchanged (source scan)', () => {
90 const src = fs.readFileSync(
91 path.join(__dirname, '../lib/hub-proposal-personal-self-apply.mjs'),
92 'utf8'
93 );
94 assert.match(src, /export const SCOOLING_REVIEW_TRAY_INTENT = 'scooling\.review_tray\.approve'/);
95 assert.match(src, /tokenType \|\| ''\)\.trim\(\) === 'agent_access'\) return false/);
96 assert.equal(src.includes('ingest:automation'), false);
97 });
98
99 it('Hub HTML exposes required ids', () => {
100 const html = fs.readFileSync(path.join(__dirname, '../web/hub/index.html'), 'utf8');
101 assert.match(html, /data-settings-tab="automation"/);
102 assert.match(html, /id="settings-panel-automation"/);
103 assert.match(html, /id="agent-cred-scope-ingest"/);
104 assert.match(html, /id="filter-content-class"/);
105 assert.equal(html.includes('checked /> ingest:automation'), false);
106 });
107 });
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 11 days ago