flow-execution-security.test.mjs
107 lines 3.6 KB
Raw
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 13 hours ago
1 /**
2 * Tier 7 — SECURITY: scope denial, injection inert, no secrets in records.
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 {
11 handleFlowRunGetRequest,
12 handleFlowRunExecuteAutomatableRequest,
13 consentForClient,
14 } from '../lib/flow/flow-execution.mjs';
15 import { handleFlowProposeRequest } from '../lib/flow/flow-authoring.mjs';
16 import { createProposal } from '../hub/proposals-store.mjs';
17 import { writeExecutionPolicy, seedAutomatableFlow } from './fixtures/flow/execution-helpers.mjs';
18
19 const __dirname = path.dirname(fileURLToPath(import.meta.url));
20 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-flow-execution-security');
21
22 describe('Flow execution — security', () => {
23 beforeEach(() => {
24 fs.rmSync(tmpRoot, { recursive: true, force: true });
25 fs.mkdirSync(tmpRoot, { recursive: true });
26 delete process.env.FLOW_RUN_WRITES_ENABLED;
27 delete process.env.FLOW_AUTOMATABLE_EXECUTION_ENABLED;
28 });
29
30 afterEach(() => {
31 fs.rmSync(tmpRoot, { recursive: true, force: true });
32 delete process.env.FLOW_RUN_WRITES_ENABLED;
33 delete process.env.FLOW_AUTOMATABLE_EXECUTION_ENABLED;
34 });
35
36 it('unknown_run for scope-invisible run (no existence leak)', () => {
37 const dataDir = path.join(tmpRoot, 'scope');
38 fs.mkdirSync(dataDir);
39 writeExecutionPolicy(dataDir, { runWrites: true });
40 seedAutomatableFlow(dataDir, 'default');
41 process.env.FLOW_RUN_WRITES_ENABLED = '1';
42 const start = handleFlowRunGetRequest({
43 dataDir,
44 vaultId: 'default',
45 cliScopes: ['personal'],
46 runId: 'run_nonexistent',
47 });
48 assert.equal(start.ok, false);
49 assert.equal(start.code, 'unknown_run');
50 });
51
52 it('consent record contains no secrets', () => {
53 const client = consentForClient({
54 schema: 'knowtation.flow_execution_consent/v0',
55 consent_id: 'fcons_x',
56 vault_id: 'default',
57 scope: 'personal',
58 run_id: 'run_x',
59 flow_id: 'flow_x',
60 flow_version: '1.0.0',
61 allowed_lanes: ['local_default'],
62 cost_cap_units: 10,
63 cost_consumed_units: 0,
64 actor_hash: 'deadbeef',
65 expires_at: '2026-06-20T13:00:00Z',
66 revoked_at: null,
67 });
68 const json = JSON.stringify(client);
69 assert.equal(json.includes('password'), false);
70 assert.equal(json.includes('api_key'), false);
71 });
72
73 it('import with forbidden automatable denied when policy forbids', () => {
74 const dataDir = path.join(tmpRoot, 'import');
75 fs.mkdirSync(dataDir);
76 writeExecutionPolicy(dataDir, { automatableForbidden: true });
77 process.env.FLOW_AUTHORING_WRITES = '1';
78 const bundle = seedAutomatableFlow(dataDir, 'default');
79 const result = handleFlowProposeRequest({
80 dataDir,
81 vaultId: 'default',
82 cliScopes: ['personal', 'project', 'org'],
83 kind: 'import',
84 bundle: { flow: bundle.flow, steps: bundle.steps },
85 intent: 'import automatable',
86 createProposal,
87 });
88 assert.equal(result.ok, false);
89 assert.equal(result.code, 'FLOW_IMPORT_AUTOMATABLE_DENIED');
90 delete process.env.FLOW_AUTHORING_WRITES;
91 });
92
93 it('gates off ⇒ execute unreachable', () => {
94 const dataDir = path.join(tmpRoot, 'off');
95 fs.mkdirSync(dataDir);
96 const result = handleFlowRunExecuteAutomatableRequest({
97 dataDir,
98 vaultId: 'default',
99 cliScopes: ['personal', 'project', 'org'],
100 runId: 'run_x',
101 stepId: 'flow_automatable_test#1',
102 consentId: 'fcons_x',
103 });
104 assert.equal(result.ok, false);
105 assert.equal(result.code, 'FLOW_AUTOMATABLE_EXECUTION_DISABLED');
106 });
107 });
File History 1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 13 hours ago