flow-external-agent-security.test.mjs
96 lines 3.1 KB
Raw
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 20 hours ago
1 /**
2 * Tier 7 — SECURITY: scope denial, no secrets in envelopes, injection inert.
3 */
4 import { describe, it, beforeEach, afterEach } from 'node:test';
5 import assert from 'node:assert/strict';
6 import fs from 'node:fs';
7 import path from 'node:path';
8 import { fileURLToPath } from 'node:url';
9
10 import { handleFlowProjectRequest } from '../lib/flow/flow-handlers.mjs';
11 import {
12 handleFlowExternalGrantMintRequest,
13 validateExternalGrantBearer,
14 } from '../lib/flow/external-agent.mjs';
15 import { upsertFlowVersion } from '../lib/flow/flow-store.mjs';
16 import {
17 writeExternalAgentPolicy,
18 makeExternalToolFlowBundle,
19 } from './fixtures/flow/external-agent-helpers.mjs';
20
21 const __dirname = path.dirname(fileURLToPath(import.meta.url));
22 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-flow-external-agent-security');
23
24 describe('Flow external-agent — security', () => {
25 const dataDir = path.join(tmpRoot, 'data');
26 const vaultId = 'default';
27
28 beforeEach(() => {
29 fs.rmSync(tmpRoot, { recursive: true, force: true });
30 fs.mkdirSync(dataDir, { recursive: true });
31 writeExternalAgentPolicy(dataDir);
32 process.env.FLOW_EXTERNAL_AGENT_ENABLED = '1';
33 const bundle = makeExternalToolFlowBundle();
34 bundle.steps[0].instruction = '<script>alert(1)</script> untrusted';
35 upsertFlowVersion(dataDir, vaultId, bundle.flow, bundle.steps);
36 });
37
38 afterEach(() => {
39 fs.rmSync(tmpRoot, { recursive: true, force: true });
40 delete process.env.FLOW_EXTERNAL_AGENT_ENABLED;
41 });
42
43 it('unknown_flow for unreadable scope — no existence leak', () => {
44 const result = handleFlowProjectRequest({
45 dataDir,
46 vaultId,
47 flowId: 'flow_ext_agent_test',
48 harness: 'agent_bundle',
49 cliScopes: ['org'],
50 });
51 assert.equal(result.ok, false);
52 assert.equal(result.code, 'unknown_flow');
53 });
54
55 it('grant cannot exceed allowlist ∩ flow refs', () => {
56 const result = handleFlowExternalGrantMintRequest({
57 dataDir,
58 vaultId,
59 flowId: 'flow_ext_agent_test',
60 flowVersion: '1.0.0',
61 requestedTools: ['slack_notify'],
62 });
63 assert.equal(result.ok, false);
64 assert.equal(result.code, 'FLOW_EXTERNAL_TOOL_UNKNOWN');
65 });
66
67 it('expired bearer denied; bundle JSON is data not executed', () => {
68 const project = handleFlowProjectRequest({
69 dataDir,
70 vaultId,
71 flowId: 'flow_ext_agent_test',
72 harness: 'agent_bundle',
73 cliScopes: ['personal'],
74 });
75 const inner = JSON.parse(project.payload.projection.rendered);
76 assert.equal(inner.steps[0].instruction.includes('<script>'), true);
77 assert.equal(typeof inner.steps[0].instruction, 'string');
78
79 const mint = handleFlowExternalGrantMintRequest({
80 dataDir,
81 vaultId,
82 flowId: 'flow_ext_agent_test',
83 flowVersion: '1.0.0',
84 requestedTools: ['web_search'],
85 ttlSeconds: 1,
86 });
87 assert.equal(mint.ok, true);
88 assert.equal(JSON.stringify(mint.payload.grant).includes('bearer'), false);
89 const bad = validateExternalGrantBearer({
90 dataDir,
91 vaultId,
92 bearer: 'fgrnt_bearer_not_real',
93 });
94 assert.equal(bad.ok, false);
95 });
96 });
File History 1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 20 hours ago