mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): distribute copilot console via signed admin image
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { randomBytes } = require('node:crypto');
|
||||
const { request: httpRequest } = require('node:http');
|
||||
const { createServer, request: httpRequest } = require('node:http');
|
||||
const { mkdtemp, mkdir, cp, writeFile } = require('node:fs/promises');
|
||||
const { tmpdir } = require('node:os');
|
||||
const { join, resolve } = require('node:path');
|
||||
@@ -19,6 +19,7 @@ const {
|
||||
normalizeClusterCopilotConsoleReadRequest,
|
||||
} = require('../dist/copilot-console/contracts.js');
|
||||
const {
|
||||
ClusterCopilotConsoleConfigurationError,
|
||||
clusterCopilotConsoleSessionDigest,
|
||||
startClusterCopilotConsoleServer,
|
||||
} = require('../dist/copilot-console/server.js');
|
||||
@@ -165,6 +166,20 @@ async function fixture(execute = async () => inspection()) {
|
||||
};
|
||||
}
|
||||
|
||||
async function unusedPort() {
|
||||
const probe = createServer();
|
||||
await new Promise((resolve, reject) => {
|
||||
probe.once('error', reject);
|
||||
probe.listen(0, '127.0.0.1', resolve);
|
||||
});
|
||||
const address = probe.address();
|
||||
assert.notEqual(typeof address, 'string');
|
||||
assert.notEqual(address, null);
|
||||
const port = address.port;
|
||||
await new Promise((resolve) => probe.close(resolve));
|
||||
return port;
|
||||
}
|
||||
|
||||
test('normalizes only the two read operations into the shared client contract', () => {
|
||||
assert.deepEqual(
|
||||
clusterCopilotConsoleClientCommand(
|
||||
@@ -244,6 +259,30 @@ test('serves an immutable same-origin shell with a closed browser policy', async
|
||||
assert.equal(javascript.headers['content-type'], 'text/javascript; charset=utf-8');
|
||||
});
|
||||
|
||||
test('allows only an explicit fixed-port container listener behind host loopback publication', async (t) => {
|
||||
const token = randomBytes(32).toString('base64url');
|
||||
await assert.rejects(
|
||||
startClusterCopilotConsoleServer({
|
||||
assets: loadClusterCopilotConsoleAssets(moduleDirectory),
|
||||
executor: { execute: async () => inspection() },
|
||||
networkBoundary: 'container-published-loopback',
|
||||
port: 0,
|
||||
sessionDigest: clusterCopilotConsoleSessionDigest(token),
|
||||
}),
|
||||
ClusterCopilotConsoleConfigurationError,
|
||||
);
|
||||
const server = await startClusterCopilotConsoleServer({
|
||||
assets: loadClusterCopilotConsoleAssets(moduleDirectory),
|
||||
executor: { execute: async () => inspection() },
|
||||
networkBoundary: 'container-published-loopback',
|
||||
port: await unusedPort(),
|
||||
sessionDigest: clusterCopilotConsoleSessionDigest(token),
|
||||
});
|
||||
t.after(() => server.close());
|
||||
assert.match(server.origin, /^http:\/\/127\.0\.0\.1:[0-9]+$/);
|
||||
assert.equal((await request(server.origin)).statusCode, 200);
|
||||
});
|
||||
|
||||
test('keeps the Cluster credential server-side and forwards one exact inspect', async (t) => {
|
||||
const commands = [];
|
||||
const { server, headers } = await fixture(async (command) => {
|
||||
|
||||
@@ -163,8 +163,9 @@ test('CLI exposes deterministic help and a low-sensitive failure surface', async
|
||||
'Usage:',
|
||||
' ql3-copilot-console --config /absolute/client.json --credential /absolute/credential --session /absolute/session [--port=0..65535]',
|
||||
' ql3-copilot-console --check --config /absolute/client.json --credential /absolute/credential --session /absolute/session',
|
||||
' ql3-copilot-console --container-published-loopback --port=1024..65535 --config /absolute/client.json --credential /absolute/credential --session /absolute/session [--check]',
|
||||
'',
|
||||
'The Console binds only 127.0.0.1 and exposes inspect/output reads.',
|
||||
'Native mode binds 127.0.0.1. Container mode requires host-loopback port publication.',
|
||||
'The browser session key remains in a separate owner-private 0600 file.',
|
||||
].join('\n');
|
||||
assert.deepEqual(await runCli(['--help']), {
|
||||
@@ -209,7 +210,8 @@ test('preflight proves private authority and unauthenticated TLS 1.3 readiness',
|
||||
component: 'qinglong3-cluster-copilot-console',
|
||||
event: 'preflight_checked',
|
||||
ready: true,
|
||||
listenAddress: '127.0.0.1',
|
||||
networkBoundary: 'host-loopback',
|
||||
publishedHostAddress: '127.0.0.1',
|
||||
browserCredential: 'forbidden',
|
||||
clusterCredential: 'server_only',
|
||||
operations: ['inspect', 'output'],
|
||||
@@ -249,6 +251,8 @@ test('serve mode starts an ephemeral loopback origin and shuts down cleanly', as
|
||||
assert.match(started.origin, /^http:\/\/127\.0\.0\.1:[0-9]+$/);
|
||||
assert.deepEqual(started.operations, ['inspect', 'output']);
|
||||
assert.equal(started.mutation, false);
|
||||
assert.equal(started.networkBoundary, 'host-loopback');
|
||||
assert.equal(started.publishedHostAddress, '127.0.0.1');
|
||||
const shell = await get(started.origin);
|
||||
assert.equal(shell.statusCode, 200);
|
||||
assert.match(shell.body, /Cluster field console/);
|
||||
@@ -259,3 +263,19 @@ test('serve mode starts an ephemeral loopback origin and shuts down cleanly', as
|
||||
});
|
||||
assert.deepEqual(result, { status: 0, signal: null });
|
||||
});
|
||||
|
||||
test('container mode requires an explicit publish port before any authority read', async () => {
|
||||
const result = await runCli([
|
||||
'--container-published-loopback',
|
||||
'--config',
|
||||
'/private/client.json',
|
||||
'--credential',
|
||||
'/private/credential',
|
||||
'--session',
|
||||
'/private/session',
|
||||
]);
|
||||
assert.equal(result.status, 64);
|
||||
assert.equal(result.stdout, '');
|
||||
assert.match(result.stderr, /container-published-loopback/);
|
||||
assert.doesNotMatch(result.stderr, /\/private\//);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user