mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 12:05:27 +08:00
feat(ql3): execute copilot diagnosis tools
This commit is contained in:
@@ -125,6 +125,11 @@
|
||||
"require": "./dist/trusted-tool/key-management/toolResultProjectedKeyring.js",
|
||||
"default": "./dist/trusted-tool/key-management/toolResultProjectedKeyring.js"
|
||||
},
|
||||
"./trusted-tool-invocation-keyring": {
|
||||
"types": "./dist/trusted-tool/key-management/toolInvocationProjectedKeyring.d.ts",
|
||||
"require": "./dist/trusted-tool/key-management/toolInvocationProjectedKeyring.js",
|
||||
"default": "./dist/trusted-tool/key-management/toolInvocationProjectedKeyring.js"
|
||||
},
|
||||
"./remote-completion": {
|
||||
"types": "./dist/remote-execution/remoteWorkerCompletionService.d.ts",
|
||||
"require": "./dist/remote-execution/remoteWorkerCompletionService.js",
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
export const CLUSTER_TOOL_INVOCATION_KEYRING_MANIFEST_SCHEMA =
|
||||
'qinglong/cluster-tool-invocation-projected-keyring@v1' as const;
|
||||
export const MAX_CLUSTER_TOOL_INVOCATION_KEYRING_BYTES = 64 * 1024;
|
||||
export const MAX_CLUSTER_TOOL_INVOCATION_PROJECTED_KEYS = 16;
|
||||
|
||||
const KEY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
||||
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
|
||||
const PROJECTION_DIGEST_DOMAIN = Buffer.from(
|
||||
'qinglong/cluster-tool-invocation-projected-keyring-digest@v1\0',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
export interface ClusterToolInvocationKeyringManifest {
|
||||
readonly schema: typeof CLUSTER_TOOL_INVOCATION_KEYRING_MANIFEST_SCHEMA;
|
||||
readonly activeKeyId: string;
|
||||
readonly keys: Readonly<Record<string, string>>;
|
||||
}
|
||||
|
||||
export interface ClusterToolInvocationKeyringSummary {
|
||||
readonly schemaVersion: 1;
|
||||
readonly activeKeyId: string;
|
||||
readonly keyIds: readonly string[];
|
||||
readonly projectionDigest: string;
|
||||
}
|
||||
|
||||
export class InvalidClusterToolInvocationKeyringManifestError extends TypeError {
|
||||
readonly code = 'QL3_CLUSTER_TOOL_INVOCATION_KEYRING_MANIFEST_INVALID';
|
||||
|
||||
constructor() {
|
||||
super('Cluster Tool invocation keyring manifest is invalid');
|
||||
this.name = 'InvalidClusterToolInvocationKeyringManifestError';
|
||||
}
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new InvalidClusterToolInvocationKeyringManifestError();
|
||||
}
|
||||
|
||||
function dataRecord(value: unknown): Record<string, unknown> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.getPrototypeOf(value) !== Object.prototype
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function exactKeys(value: object, expected: readonly string[]): boolean {
|
||||
const actual = Object.keys(value).sort();
|
||||
const canonical = [...expected].sort();
|
||||
return (
|
||||
actual.length === canonical.length &&
|
||||
actual.every((key, index) => key === canonical[index])
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeClusterToolInvocationKeyringManifest(
|
||||
value: unknown,
|
||||
): Readonly<ClusterToolInvocationKeyringManifest> {
|
||||
const manifest = dataRecord(value);
|
||||
if (
|
||||
!exactKeys(manifest, ['activeKeyId', 'keys', 'schema']) ||
|
||||
manifest.schema !== CLUSTER_TOOL_INVOCATION_KEYRING_MANIFEST_SCHEMA ||
|
||||
typeof manifest.activeKeyId !== 'string' ||
|
||||
!KEY_ID_PATTERN.test(manifest.activeKeyId)
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
const keys = dataRecord(manifest.keys);
|
||||
const entries = Object.entries(keys).sort(([left], [right]) =>
|
||||
left < right ? -1 : left > right ? 1 : 0,
|
||||
);
|
||||
if (
|
||||
entries.length < 1 ||
|
||||
entries.length > MAX_CLUSTER_TOOL_INVOCATION_PROJECTED_KEYS
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
const normalized: (readonly [string, string])[] = [];
|
||||
for (const [keyId, encoded] of entries) {
|
||||
let material: Buffer | undefined;
|
||||
try {
|
||||
if (
|
||||
!KEY_ID_PATTERN.test(keyId) ||
|
||||
typeof encoded !== 'string' ||
|
||||
!BASE64URL_PATTERN.test(encoded)
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
material = Buffer.from(encoded, 'base64url');
|
||||
if (
|
||||
material.byteLength !== 32 ||
|
||||
material.toString('base64url') !== encoded
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
normalized.push(Object.freeze([keyId, encoded] as const));
|
||||
} finally {
|
||||
material?.fill(0);
|
||||
}
|
||||
}
|
||||
const normalizedKeys = Object.freeze(Object.fromEntries(normalized));
|
||||
if (normalizedKeys[manifest.activeKeyId] === undefined) return invalid();
|
||||
return Object.freeze({
|
||||
schema: CLUSTER_TOOL_INVOCATION_KEYRING_MANIFEST_SCHEMA,
|
||||
activeKeyId: manifest.activeKeyId,
|
||||
keys: normalizedKeys,
|
||||
});
|
||||
}
|
||||
|
||||
export function parseClusterToolInvocationKeyringManifest(
|
||||
bytes: Buffer,
|
||||
): Readonly<ClusterToolInvocationKeyringManifest> {
|
||||
try {
|
||||
if (
|
||||
!Buffer.isBuffer(bytes) ||
|
||||
bytes.byteLength < 1 ||
|
||||
bytes.byteLength > MAX_CLUSTER_TOOL_INVOCATION_KEYRING_BYTES
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
return normalizeClusterToolInvocationKeyringManifest(
|
||||
JSON.parse(bytes.toString('utf8')),
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidClusterToolInvocationKeyringManifestError) {
|
||||
throw error;
|
||||
}
|
||||
return invalid();
|
||||
}
|
||||
}
|
||||
|
||||
export function canonicalClusterToolInvocationKeyringManifest(
|
||||
value: ClusterToolInvocationKeyringManifest,
|
||||
): Buffer {
|
||||
const manifest = normalizeClusterToolInvocationKeyringManifest(value);
|
||||
return Buffer.from(`${JSON.stringify(manifest)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
export function resolveClusterToolInvocationKeyringMaterial(
|
||||
value: ClusterToolInvocationKeyringManifest,
|
||||
keyId: string,
|
||||
): Readonly<{ keyId: string; key: Uint8Array }> | null {
|
||||
if (typeof keyId !== 'string' || !KEY_ID_PATTERN.test(keyId)) {
|
||||
return invalid();
|
||||
}
|
||||
const manifest = normalizeClusterToolInvocationKeyringManifest(value);
|
||||
const encoded = manifest.keys[keyId];
|
||||
if (encoded === undefined) return null;
|
||||
const key = Buffer.from(encoded, 'base64url');
|
||||
if (key.byteLength !== 32 || key.toString('base64url') !== encoded) {
|
||||
key.fill(0);
|
||||
return invalid();
|
||||
}
|
||||
return Object.freeze({ keyId, key });
|
||||
}
|
||||
|
||||
export function summarizeClusterToolInvocationKeyringManifest(
|
||||
value: ClusterToolInvocationKeyringManifest,
|
||||
): Readonly<ClusterToolInvocationKeyringSummary> {
|
||||
const manifest = normalizeClusterToolInvocationKeyringManifest(value);
|
||||
const keyIds = Object.freeze(Object.keys(manifest.keys).sort());
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
activeKeyId: manifest.activeKeyId,
|
||||
keyIds,
|
||||
projectionDigest: createHash('sha256')
|
||||
.update(PROJECTION_DIGEST_DOMAIN)
|
||||
.update(JSON.stringify(manifest))
|
||||
.digest('hex'),
|
||||
});
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
|
||||
import type { ToolInvocationArtifactKeyProvider } from '@qinglong/runtime-core/tool-invocation-artifact';
|
||||
|
||||
import { PrivateProjectedFileReader } from '../../security/privateProjectedFile';
|
||||
import {
|
||||
MAX_CLUSTER_TOOL_INVOCATION_KEYRING_BYTES,
|
||||
canonicalClusterToolInvocationKeyringManifest,
|
||||
parseClusterToolInvocationKeyringManifest,
|
||||
resolveClusterToolInvocationKeyringMaterial,
|
||||
summarizeClusterToolInvocationKeyringManifest,
|
||||
type ClusterToolInvocationKeyringManifest,
|
||||
type ClusterToolInvocationKeyringSummary,
|
||||
} from './toolInvocationKeyringManifest';
|
||||
|
||||
const DATA_FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,252}$/;
|
||||
|
||||
export {
|
||||
CLUSTER_TOOL_INVOCATION_KEYRING_MANIFEST_SCHEMA,
|
||||
MAX_CLUSTER_TOOL_INVOCATION_KEYRING_BYTES,
|
||||
MAX_CLUSTER_TOOL_INVOCATION_PROJECTED_KEYS,
|
||||
InvalidClusterToolInvocationKeyringManifestError,
|
||||
canonicalClusterToolInvocationKeyringManifest,
|
||||
normalizeClusterToolInvocationKeyringManifest,
|
||||
parseClusterToolInvocationKeyringManifest,
|
||||
resolveClusterToolInvocationKeyringMaterial,
|
||||
summarizeClusterToolInvocationKeyringManifest,
|
||||
type ClusterToolInvocationKeyringManifest,
|
||||
type ClusterToolInvocationKeyringSummary,
|
||||
} from './toolInvocationKeyringManifest';
|
||||
|
||||
export interface ClusterToolInvocationProjectedKeyringOptions {
|
||||
readonly rootDirectory: string;
|
||||
readonly dataFileName?: string;
|
||||
}
|
||||
|
||||
export class ClusterToolInvocationProjectedKeyringUnavailableError extends Error {
|
||||
readonly code = 'QL3_CLUSTER_TOOL_INVOCATION_PROJECTED_KEYRING_UNAVAILABLE';
|
||||
|
||||
constructor(options?: ErrorOptions) {
|
||||
super('Projected Cluster Tool invocation keyring is unavailable', options);
|
||||
this.name = 'ClusterToolInvocationProjectedKeyringUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
function unavailable(
|
||||
cause?: unknown,
|
||||
): ClusterToolInvocationProjectedKeyringUnavailableError {
|
||||
return new ClusterToolInvocationProjectedKeyringUnavailableError({
|
||||
cause: cause instanceof Error ? cause : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function dataFileName(value: unknown): string {
|
||||
if (typeof value !== 'string' || !DATA_FILE_NAME.test(value)) {
|
||||
throw unavailable();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function readManifest(
|
||||
reader: PrivateProjectedFileReader,
|
||||
fileName: string,
|
||||
): Promise<Readonly<ClusterToolInvocationKeyringManifest>> {
|
||||
let bytes: Buffer | undefined;
|
||||
let canonical: Buffer | undefined;
|
||||
try {
|
||||
bytes = await reader.read(fileName);
|
||||
const manifest = parseClusterToolInvocationKeyringManifest(bytes);
|
||||
canonical = canonicalClusterToolInvocationKeyringManifest(manifest);
|
||||
if (!canonical.equals(bytes)) throw unavailable();
|
||||
return manifest;
|
||||
} catch (cause) {
|
||||
throw cause instanceof ClusterToolInvocationProjectedKeyringUnavailableError
|
||||
? cause
|
||||
: unavailable(cause);
|
||||
} finally {
|
||||
bytes?.fill(0);
|
||||
canonical?.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
/** Read-only, no-cache invocation Artifact key authority for projections. */
|
||||
export class ClusterToolInvocationProjectedKeyring
|
||||
implements ToolInvocationArtifactKeyProvider
|
||||
{
|
||||
readonly #reader: PrivateProjectedFileReader;
|
||||
readonly #dataFileName: string;
|
||||
|
||||
constructor(options: ClusterToolInvocationProjectedKeyringOptions) {
|
||||
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
||||
throw unavailable();
|
||||
}
|
||||
try {
|
||||
this.#reader = new PrivateProjectedFileReader({
|
||||
rootDirectory: options.rootDirectory,
|
||||
minimumBytes: 1,
|
||||
maximumBytes: MAX_CLUSTER_TOOL_INVOCATION_KEYRING_BYTES,
|
||||
access: 'read_only_keyring',
|
||||
});
|
||||
this.#dataFileName = dataFileName(options.dataFileName ?? 'keyring.json');
|
||||
} catch (cause) {
|
||||
throw unavailable(cause);
|
||||
}
|
||||
}
|
||||
|
||||
async verify(): Promise<Readonly<ClusterToolInvocationKeyringSummary>> {
|
||||
return summarizeClusterToolInvocationKeyringManifest(
|
||||
await readManifest(this.#reader, this.#dataFileName),
|
||||
);
|
||||
}
|
||||
|
||||
async active(): ReturnType<ToolInvocationArtifactKeyProvider['active']> {
|
||||
try {
|
||||
const manifest = await readManifest(this.#reader, this.#dataFileName);
|
||||
return resolveClusterToolInvocationKeyringMaterial(
|
||||
manifest,
|
||||
manifest.activeKeyId,
|
||||
)!;
|
||||
} catch (cause) {
|
||||
throw cause instanceof
|
||||
ClusterToolInvocationProjectedKeyringUnavailableError
|
||||
? cause
|
||||
: unavailable(cause);
|
||||
}
|
||||
}
|
||||
|
||||
async resolve(
|
||||
keyId: string,
|
||||
): ReturnType<ToolInvocationArtifactKeyProvider['resolve']> {
|
||||
try {
|
||||
return resolveClusterToolInvocationKeyringMaterial(
|
||||
await readManifest(this.#reader, this.#dataFileName),
|
||||
keyId,
|
||||
);
|
||||
} catch (cause) {
|
||||
throw cause instanceof
|
||||
ClusterToolInvocationProjectedKeyringUnavailableError
|
||||
? cause
|
||||
: unavailable(cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createClusterToolInvocationProjectedKeyring(
|
||||
options: ClusterToolInvocationProjectedKeyringOptions,
|
||||
): Promise<Readonly<ClusterToolInvocationProjectedKeyring>> {
|
||||
const provider = new ClusterToolInvocationProjectedKeyring(options);
|
||||
await provider.verify();
|
||||
return provider;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { afterEach, test } = require('node:test');
|
||||
|
||||
const {
|
||||
CLUSTER_TOOL_INVOCATION_KEYRING_MANIFEST_SCHEMA,
|
||||
ClusterToolInvocationProjectedKeyring,
|
||||
ClusterToolInvocationProjectedKeyringUnavailableError,
|
||||
canonicalClusterToolInvocationKeyringManifest,
|
||||
createClusterToolInvocationProjectedKeyring,
|
||||
} = require('@qinglong/cluster-control/trusted-tool-invocation-keyring');
|
||||
|
||||
const roots = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
roots
|
||||
.splice(0)
|
||||
.map((root) => fs.rm(root, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
async function tempRoot() {
|
||||
const root = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), 'ql3-tool-invocation-keyring-'),
|
||||
);
|
||||
roots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
async function publish(root, generationName, manifest, mode = 0o440) {
|
||||
const generation = path.join(root, generationName);
|
||||
await fs.mkdir(generation, { mode: 0o750 });
|
||||
const target = path.join(generation, 'keyring.json');
|
||||
await fs.writeFile(
|
||||
target,
|
||||
canonicalClusterToolInvocationKeyringManifest(manifest),
|
||||
{ mode },
|
||||
);
|
||||
await fs.chmod(target, mode);
|
||||
const next = path.join(root, '..data-next');
|
||||
await fs.symlink(generationName, next);
|
||||
await fs.rename(next, path.join(root, '..data'));
|
||||
try {
|
||||
await fs.symlink('..data/keyring.json', path.join(root, 'keyring.json'));
|
||||
} catch (error) {
|
||||
if (error.code !== 'EEXIST') throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function manifest(activeKeyId, keys) {
|
||||
return Object.freeze({
|
||||
schema: CLUSTER_TOOL_INVOCATION_KEYRING_MANIFEST_SCHEMA,
|
||||
activeKeyId,
|
||||
keys: Object.freeze(keys),
|
||||
});
|
||||
}
|
||||
|
||||
test('projected Tool invocation keyring rotates active material without caching', async () => {
|
||||
const root = await tempRoot();
|
||||
const keyOne = Buffer.alloc(32, 0x41);
|
||||
const keyTwo = Buffer.alloc(32, 0x42);
|
||||
await publish(
|
||||
root,
|
||||
'..2026_08_15_01',
|
||||
manifest('invocation-key-one', {
|
||||
'invocation-key-one': keyOne.toString('base64url'),
|
||||
}),
|
||||
);
|
||||
const provider = await createClusterToolInvocationProjectedKeyring({
|
||||
rootDirectory: root,
|
||||
});
|
||||
const first = await provider.active();
|
||||
assert.equal(first.keyId, 'invocation-key-one');
|
||||
assert.deepEqual(Buffer.from(first.key), keyOne);
|
||||
first.key.fill(0);
|
||||
|
||||
await publish(
|
||||
root,
|
||||
'..2026_08_15_02',
|
||||
manifest('invocation-key-two', {
|
||||
'invocation-key-one': keyOne.toString('base64url'),
|
||||
'invocation-key-two': keyTwo.toString('base64url'),
|
||||
}),
|
||||
);
|
||||
const second = await provider.active();
|
||||
assert.equal(second.keyId, 'invocation-key-two');
|
||||
assert.deepEqual(Buffer.from(second.key), keyTwo);
|
||||
second.key.fill(0);
|
||||
const historical = await provider.resolve('invocation-key-one');
|
||||
assert.ok(historical);
|
||||
assert.deepEqual(Buffer.from(historical.key), keyOne);
|
||||
historical.key.fill(0);
|
||||
assert.equal(await provider.resolve('missing-key'), null);
|
||||
});
|
||||
|
||||
test('projected Tool invocation keyring rejects missing active and writable material', async () => {
|
||||
const root = await tempRoot();
|
||||
const key = Buffer.alloc(32, 0x51).toString('base64url');
|
||||
assert.throws(
|
||||
() =>
|
||||
canonicalClusterToolInvocationKeyringManifest(
|
||||
manifest('missing-key', { 'invocation-key-one': key }),
|
||||
),
|
||||
TypeError,
|
||||
);
|
||||
|
||||
await publish(
|
||||
root,
|
||||
'..2026_08_15_01',
|
||||
manifest('invocation-key-one', { 'invocation-key-one': key }),
|
||||
0o640,
|
||||
);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
new ClusterToolInvocationProjectedKeyring({
|
||||
rootDirectory: root,
|
||||
}).verify(),
|
||||
ClusterToolInvocationProjectedKeyringUnavailableError,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user