companion-shell-security.test.mjs
188 lines 6.3 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Tier 7 — SECURITY: Phase 5 bind/lifecycle layer centerpiece tests.
3 */
4
5 import { describe, it } from 'node:test';
6 import assert from 'node:assert/strict';
7 import http from 'node:http';
8 import { readFile } from 'node:fs/promises';
9
10 import { createCompanionInferenceListener } from '../lib/companion-inference-listener.mjs';
11 import { createScrubbedRuntimeEnv } from '../lib/companion-spawn-adapter.mjs';
12 import { createCompanionResourceProbe } from '../lib/companion-resource-probe.mjs';
13 import {
14 computeCompanionAvailable,
15 validateManifestTrustAnchor,
16 } from '../lib/companion-shell.mjs';
17
18 function rawHttpStatus({ port, headers }) {
19 return new Promise((resolve, reject) => {
20 const req = http.request({
21 host: '127.0.0.1',
22 port,
23 path: '/v1/models',
24 method: 'GET',
25 headers,
26 }, (res) => {
27 res.resume();
28 res.on('end', () => resolve(res.statusCode));
29 });
30 req.on('error', reject);
31 req.end();
32 });
33 }
34
35 describe('architecture/import boundary', () => {
36 it('runtime group modules import no authority modules', async () => {
37 const files = [
38 'lib/companion-spawn-adapter.mjs',
39 'lib/companion-download-adapter.mjs',
40 'lib/companion-resource-probe.mjs',
41 'lib/companion-runtime-manager.mjs',
42 ];
43 const forbidden = [
44 'companion-token-custody',
45 'companion-oauth-pkce',
46 'companion-keychain',
47 'canister',
48 'vault',
49 'auth-session',
50 ];
51 for (const file of files) {
52 const source = await readFile(new URL(`../${file}`, import.meta.url), 'utf8');
53 for (const needle of forbidden) {
54 assert.equal(source.includes(`from './${needle}`), false, `${file} imports ${needle}`);
55 assert.equal(source.includes(`from '../${needle}`), false, `${file} imports ${needle}`);
56 }
57 }
58 });
59 });
60
61 describe('child environment contains no secret', () => {
62 it('strips SESSION_SECRET, token, API key, JWT, and keychain refs', () => {
63 const env = createScrubbedRuntimeEnv({
64 HOME: '/Users/a',
65 TMPDIR: '/tmp',
66 SESSION_SECRET: 's',
67 ACCESS_TOKEN: 's',
68 REFRESH_TOKEN: 's',
69 OPENROUTER_API_KEY: 's',
70 JWT: 's',
71 KEYCHAIN_ACCOUNT: 's',
72 });
73 assert.deepEqual(env, { HOME: '/Users/a', TMPDIR: '/tmp' });
74 });
75 });
76
77 describe('loopback-only bind and browser attack rejection', () => {
78 it('rejects wildcard and routable bind hosts before listen', () => {
79 assert.throws(() => createCompanionInferenceListener({ host: '0.0.0.0', expectedToken: 't', runtimeRequest() {} }));
80 assert.throws(() => createCompanionInferenceListener({ host: '::', expectedToken: 't', runtimeRequest() {} }));
81 assert.throws(() => createCompanionInferenceListener({ host: '192.168.1.20', expectedToken: 't', runtimeRequest() {} }));
82 });
83
84 it('DNS-rebinding host and cross-origin requests stay 403 before runtime work', async () => {
85 let hits = 0;
86 const listener = createCompanionInferenceListener({
87 expectedToken: 'token',
88 runtimeRequest(_req, res) {
89 hits += 1;
90 res.statusCode = 200;
91 res.end('ok');
92 },
93 });
94 const bound = await listener.start();
95 try {
96 const cross = await fetch(`http://127.0.0.1:${bound.port}/v1/models`, {
97 headers: { Origin: 'https://attacker.example', Authorization: 'Bearer token' },
98 });
99 assert.equal(cross.status, 403);
100 assert.equal(cross.headers.get('access-control-allow-origin'), null);
101 assert.equal(hits, 0);
102
103 const rebindStatus = await rawHttpStatus({
104 port: bound.port,
105 headers: { Host: `evil.example:${bound.port}`, Authorization: 'Bearer token' },
106 });
107 assert.equal(rebindStatus, 403);
108 assert.equal(hits, 0);
109 } finally {
110 await listener.close();
111 }
112 });
113 });
114
115 describe('manifest trust anchor', () => {
116 it('accepts first-party manifest only when it is out-of-band from the model host', () => {
117 const base = {
118 expectedDigest: 'b'.repeat(64),
119 expectedSizeBytes: 128,
120 allowedSourceUrls: ['https://cdn.knowtation-models.com/'],
121 };
122 assert.equal(validateManifestTrustAnchor({
123 ...base,
124 manifestUrl: 'https://gateway.knowtation.com/manifest.json',
125 modelUrl: 'https://cdn.knowtation-models.com/model.bin',
126 }).ok, true);
127 assert.equal(validateManifestTrustAnchor({
128 ...base,
129 manifestUrl: 'https://cdn.knowtation-models.com/manifest.json',
130 modelUrl: 'https://cdn.knowtation-models.com/model.bin',
131 }).ok, false);
132 assert.equal(validateManifestTrustAnchor({
133 ...base,
134 manifestUrl: 'http://gateway.knowtation.com/manifest.json',
135 modelUrl: 'https://cdn.knowtation-models.com/model.bin',
136 }).ok, false);
137 });
138 });
139
140 describe('resource probe privacy', () => {
141 it('does not invoke GPU process-table tools', async () => {
142 const commands = [];
143 const probe = createCompanionResourceProbe({
144 pid: 999,
145 platform: 'darwin',
146 execFile: async (cmd, args) => {
147 commands.push([cmd, args]);
148 return { stdout: '1024 2.5\n' };
149 },
150 });
151 const obs = await probe.statResources();
152 assert.equal(obs.vramBytes, 0);
153 assert.equal(commands.some(([cmd]) => String(cmd).includes('nvidia-smi')), false);
154 assert.equal(commands.some(([cmd]) => String(cmd).includes('ioreg')), false);
155 });
156 });
157
158 describe('fail-closed companionAvailable and no secret in outputs', () => {
159 it('never flips true without all readiness conditions', () => {
160 assert.equal(computeCompanionAvailable({ now: 1 }), false);
161 assert.equal(computeCompanionAvailable({
162 now: 1,
163 integrityVerified: true,
164 lifecycleState: { state: 'ready' },
165 lastHealthOkAt: 1,
166 listenerBound: true,
167 loopbackTokenPresent: false,
168 }), false);
169 });
170
171 it('fixed failure reasons do not include supplied secrets', async () => {
172 const listener = createCompanionInferenceListener({
173 expectedToken: 'super-secret-loopback-token',
174 runtimeRequest() {},
175 });
176 const bound = await listener.start();
177 try {
178 const res = await fetch(`http://127.0.0.1:${bound.port}/v1/models`, {
179 headers: { Authorization: 'Bearer wrong-secret' },
180 });
181 const body = await res.text();
182 assert.equal(body.includes('super-secret-loopback-token'), false);
183 assert.equal(body.includes('wrong-secret'), false);
184 } finally {
185 await listener.close();
186 }
187 });
188 });
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 2 days ago