mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 02:27:44 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
+147
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
type LocalSqliteOptionalFeatureRuntimeDatabase,
|
||||
type ProjectPermission,
|
||||
ProjectPolicyEngine,
|
||||
ProjectPolicyUnavailableError,
|
||||
type SecurityAuditRecord,
|
||||
type SecurityPolicyDecision,
|
||||
type SecurityPrincipal,
|
||||
normalizeSecurityAuditRecord,
|
||||
normalizeSecurityPrincipal,
|
||||
} from './authorizationAuthority';
|
||||
import {
|
||||
LocalPluginPackagePromptAuthenticationError,
|
||||
LocalPluginPackagePromptAuthorizationError,
|
||||
type LocalPluginPackagePromptCommand,
|
||||
LocalPluginPackagePromptUnavailableError,
|
||||
} from './contracts';
|
||||
|
||||
const STRONG_USER_ASSURANCES = new Set([
|
||||
'multi_factor',
|
||||
'hardware',
|
||||
'local_console',
|
||||
]);
|
||||
export const PROMPT_PERMISSIONS = Object.freeze([
|
||||
'run.start',
|
||||
'model.invoke',
|
||||
'secret.use',
|
||||
] as const satisfies readonly ProjectPermission[]);
|
||||
|
||||
function strongUser(
|
||||
value: Readonly<SecurityPrincipal>,
|
||||
nowMs: number,
|
||||
): Readonly<SecurityPrincipal> {
|
||||
try {
|
||||
const principal = normalizeSecurityPrincipal(value, nowMs);
|
||||
if (
|
||||
principal.subject.type !== 'user' ||
|
||||
!STRONG_USER_ASSURANCES.has(principal.assurance)
|
||||
) {
|
||||
throw new LocalPluginPackagePromptAuthenticationError();
|
||||
}
|
||||
return principal;
|
||||
} catch (error) {
|
||||
if (error instanceof LocalPluginPackagePromptAuthenticationError) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalPluginPackagePromptAuthenticationError();
|
||||
}
|
||||
}
|
||||
|
||||
export function sameFence(
|
||||
left: NonNullable<SecurityPolicyDecision['fence']>,
|
||||
right: NonNullable<SecurityPolicyDecision['fence']>,
|
||||
): boolean {
|
||||
return (
|
||||
left.projectVersion === right.projectVersion &&
|
||||
left.bindingVersion === right.bindingVersion
|
||||
);
|
||||
}
|
||||
|
||||
export async function authorize(
|
||||
database: LocalSqliteOptionalFeatureRuntimeDatabase,
|
||||
principalValue: Readonly<SecurityPrincipal>,
|
||||
projectId: string,
|
||||
nowMs: number,
|
||||
permissions: readonly ProjectPermission[] = PROMPT_PERMISSIONS,
|
||||
): Promise<
|
||||
Readonly<{
|
||||
principal: Readonly<SecurityPrincipal>;
|
||||
decision: Readonly<SecurityPolicyDecision> & {
|
||||
readonly fence: NonNullable<SecurityPolicyDecision['fence']>;
|
||||
};
|
||||
}>
|
||||
> {
|
||||
const principal = strongUser(principalValue, nowMs);
|
||||
const policy = new ProjectPolicyEngine(database.projectPolicy);
|
||||
let selected: Readonly<SecurityPolicyDecision> | undefined;
|
||||
try {
|
||||
for (const permission of permissions) {
|
||||
const decision = await policy.authorize(principal, projectId, permission);
|
||||
if (
|
||||
decision.effect !== 'allow' ||
|
||||
!decision.fence ||
|
||||
decision.fence.bindingVersion === null
|
||||
) {
|
||||
throw new LocalPluginPackagePromptAuthorizationError();
|
||||
}
|
||||
if (selected?.fence && !sameFence(selected.fence, decision.fence)) {
|
||||
throw new LocalPluginPackagePromptUnavailableError();
|
||||
}
|
||||
selected = decision;
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof LocalPluginPackagePromptAuthorizationError ||
|
||||
error instanceof LocalPluginPackagePromptUnavailableError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof ProjectPolicyUnavailableError) {
|
||||
throw new LocalPluginPackagePromptUnavailableError({ cause: error });
|
||||
}
|
||||
throw new LocalPluginPackagePromptUnavailableError({
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
});
|
||||
}
|
||||
if (!selected?.fence || selected.fence.bindingVersion === null) {
|
||||
throw new LocalPluginPackagePromptUnavailableError();
|
||||
}
|
||||
return Object.freeze({
|
||||
principal,
|
||||
decision: Object.freeze({
|
||||
...selected,
|
||||
fence: Object.freeze({
|
||||
projectVersion: selected.fence.projectVersion,
|
||||
bindingVersion: selected.fence.bindingVersion,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function allowedAudit(
|
||||
command: Readonly<LocalPluginPackagePromptCommand>,
|
||||
principal: Readonly<SecurityPrincipal>,
|
||||
decision: Readonly<SecurityPolicyDecision> & {
|
||||
readonly fence: NonNullable<SecurityPolicyDecision['fence']>;
|
||||
},
|
||||
occurredAtMs: number,
|
||||
): Readonly<SecurityAuditRecord> {
|
||||
return normalizeSecurityAuditRecord({
|
||||
eventId: command.request.auditEventId,
|
||||
requestId: command.request.requestId,
|
||||
operationId:
|
||||
command.operation === 'prompt.execution.inspect'
|
||||
? 'prompt.execution.read'
|
||||
: command.operation === 'prompt.execution.output.read'
|
||||
? 'prompt.execution.output.read'
|
||||
: command.operation,
|
||||
projectId: command.request.projectId,
|
||||
subject: principal.subject,
|
||||
authenticationId: principal.authenticationId,
|
||||
outcome: 'allowed',
|
||||
reasons: decision.reasons,
|
||||
fence: decision.fence,
|
||||
occurredAtMs,
|
||||
});
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
export {
|
||||
ProjectPolicyEngine,
|
||||
ProjectPolicyUnavailableError,
|
||||
} from '@qinglong/runtime-core/project-policy';
|
||||
export type { ProjectPermission } from '@qinglong/runtime-core/project-policy';
|
||||
export { normalizeSecurityPrincipal } from '@qinglong/runtime-core/security';
|
||||
export type {
|
||||
SecurityPolicyDecision,
|
||||
SecurityPrincipal,
|
||||
} from '@qinglong/runtime-core/security';
|
||||
export { normalizeSecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
|
||||
export type { SecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
|
||||
export type { LocalSqliteOptionalFeatureRuntimeDatabase } from '@qinglong/local-sqlite/optional-feature-runtime';
|
||||
+531
@@ -0,0 +1,531 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
PrivateLocalCommandFileError,
|
||||
readPrivateLocalCommandFile,
|
||||
} from './codecAuthority';
|
||||
import {
|
||||
LocalPluginPackagePromptCommandConfigurationError,
|
||||
type LocalPluginPackagePromptCommand,
|
||||
type LocalPluginPackagePromptCommandOptions,
|
||||
type LocalPluginPackagePromptInspectCommandOptions,
|
||||
type LocalPluginPackagePromptOutputCommandOptions,
|
||||
type LocalPluginPackagePromptOutputIntent,
|
||||
} from './contracts';
|
||||
|
||||
const MAX_PATH_BYTES = 4_096;
|
||||
const MAX_TIMEOUT_MS = 10 * 60_000;
|
||||
const MAX_PARAMETER_COUNT = 128;
|
||||
const MAX_PARAMETER_VALUE_BYTES = 64 * 1_024;
|
||||
const MIN_OUTPUT_RETENTION_MS = 60 * 60_000;
|
||||
const MAX_OUTPUT_RETENTION_MS = 365 * 24 * 60 * 60_000;
|
||||
const UUID_V4_PATTERN =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
|
||||
const PACKAGE_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
||||
const RESOURCE_ID_PATTERN = /^[a-z][a-z0-9-]{0,62}$/;
|
||||
|
||||
function exactObject(
|
||||
value: unknown,
|
||||
required: readonly string[],
|
||||
optional: readonly string[],
|
||||
label: string,
|
||||
): asserts value is Record<string, unknown> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
`${label} must be an object`,
|
||||
);
|
||||
}
|
||||
const keys = Object.keys(value).sort();
|
||||
const allowed = new Set([...required, ...optional]);
|
||||
if (
|
||||
required.some((key) => !keys.includes(key)) ||
|
||||
keys.some((key) => !allowed.has(key))
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
`${label} shape is invalid`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function boundedPath(value: unknown, label: string): string {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
!path.isAbsolute(value) ||
|
||||
path.parse(value).root === value ||
|
||||
path.normalize(value) !== value ||
|
||||
value.includes('\0') ||
|
||||
Buffer.byteLength(value, 'utf8') > MAX_PATH_BYTES
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
`${label} must be a normalized bounded absolute non-root path`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function descendant(root: string, candidate: string, label: string): void {
|
||||
const relative = path.relative(root, candidate);
|
||||
if (
|
||||
relative.length === 0 ||
|
||||
relative === '..' ||
|
||||
relative.startsWith(`..${path.sep}`) ||
|
||||
path.isAbsolute(relative)
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
`${label} must be a descendant of deploymentRoot`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function identity(value: unknown, label: string): string {
|
||||
if (typeof value !== 'string' || !IDENTITY_PATTERN.test(value)) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
`${label} is invalid`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeOptions(
|
||||
value: unknown,
|
||||
operation: 'prompt.inspect' | 'prompt.execution.inspect',
|
||||
): Readonly<LocalPluginPackagePromptInspectCommandOptions>;
|
||||
function normalizeOptions(
|
||||
value: unknown,
|
||||
operation: 'prompt.execution.output.read',
|
||||
): Readonly<LocalPluginPackagePromptOutputCommandOptions>;
|
||||
function normalizeOptions(
|
||||
value: unknown,
|
||||
operation: 'prompt.execute',
|
||||
): Readonly<LocalPluginPackagePromptCommandOptions>;
|
||||
function normalizeOptions(
|
||||
value: unknown,
|
||||
operation:
|
||||
| 'prompt.inspect'
|
||||
| 'prompt.execution.inspect'
|
||||
| 'prompt.execution.output.read'
|
||||
| 'prompt.execute',
|
||||
): Readonly<
|
||||
| LocalPluginPackagePromptCommandOptions
|
||||
| LocalPluginPackagePromptInspectCommandOptions
|
||||
| LocalPluginPackagePromptOutputCommandOptions
|
||||
> {
|
||||
const execution = operation === 'prompt.execute';
|
||||
const outputRead = operation === 'prompt.execution.output.read';
|
||||
exactObject(
|
||||
value,
|
||||
[
|
||||
'credentialFilePath',
|
||||
'databasePath',
|
||||
'deploymentRoot',
|
||||
'ownerPepperKeyringDirectory',
|
||||
'profile',
|
||||
...(execution ? ['providerAuthorityFilePath', 'secretKeyringPath'] : []),
|
||||
...(outputRead ? ['promptOutputKeyringPath'] : []),
|
||||
],
|
||||
['busyTimeoutMs', ...(execution ? ['promptOutputKeyringPath'] : [])],
|
||||
'options',
|
||||
);
|
||||
const deploymentRoot = boundedPath(value.deploymentRoot, 'deploymentRoot');
|
||||
const basePaths = {
|
||||
databasePath: boundedPath(value.databasePath, 'databasePath'),
|
||||
ownerPepperKeyringDirectory: boundedPath(
|
||||
value.ownerPepperKeyringDirectory,
|
||||
'ownerPepperKeyringDirectory',
|
||||
),
|
||||
credentialFilePath: boundedPath(
|
||||
value.credentialFilePath,
|
||||
'credentialFilePath',
|
||||
),
|
||||
};
|
||||
const executionPaths = execution
|
||||
? {
|
||||
secretKeyringPath: boundedPath(
|
||||
value.secretKeyringPath,
|
||||
'secretKeyringPath',
|
||||
),
|
||||
providerAuthorityFilePath: boundedPath(
|
||||
value.providerAuthorityFilePath,
|
||||
'providerAuthorityFilePath',
|
||||
),
|
||||
}
|
||||
: {};
|
||||
const outputPaths =
|
||||
value.promptOutputKeyringPath === undefined
|
||||
? {}
|
||||
: {
|
||||
promptOutputKeyringPath: boundedPath(
|
||||
value.promptOutputKeyringPath,
|
||||
'promptOutputKeyringPath',
|
||||
),
|
||||
};
|
||||
const paths = { ...basePaths, ...executionPaths, ...outputPaths };
|
||||
for (const [label, candidate] of Object.entries(paths)) {
|
||||
descendant(deploymentRoot, candidate, label);
|
||||
}
|
||||
if (value.profile !== 'edge' && value.profile !== 'standalone') {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'profile must be edge or standalone',
|
||||
);
|
||||
}
|
||||
if (
|
||||
value.busyTimeoutMs !== undefined &&
|
||||
(!Number.isSafeInteger(value.busyTimeoutMs) ||
|
||||
(value.busyTimeoutMs as number) < 100 ||
|
||||
(value.busyTimeoutMs as number) > 30_000)
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'busyTimeoutMs is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
deploymentRoot,
|
||||
...paths,
|
||||
profile: value.profile,
|
||||
...(value.busyTimeoutMs === undefined
|
||||
? {}
|
||||
: { busyTimeoutMs: value.busyTimeoutMs as number }),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeParameters(value: unknown): Readonly<Record<string, string>> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.getPrototypeOf(value) !== Object.prototype
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'parameters must be a plain object',
|
||||
);
|
||||
}
|
||||
const entries = Object.entries(value);
|
||||
if (
|
||||
entries.length > MAX_PARAMETER_COUNT ||
|
||||
entries.some(
|
||||
([key, candidate]) =>
|
||||
!RESOURCE_ID_PATTERN.test(key) ||
|
||||
typeof candidate !== 'string' ||
|
||||
Buffer.byteLength(candidate, 'utf8') > MAX_PARAMETER_VALUE_BYTES,
|
||||
)
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'parameters are invalid or exceed the bounded input budget',
|
||||
);
|
||||
}
|
||||
return Object.freeze(Object.fromEntries(entries));
|
||||
}
|
||||
|
||||
function normalizeOutputIntent(
|
||||
value: unknown,
|
||||
): Readonly<LocalPluginPackagePromptOutputIntent> {
|
||||
exactObject(value, ['mode'], ['retentionPolicy'], 'output');
|
||||
if (value.mode === 'live_only') {
|
||||
exactObject(value, ['mode'], [], 'output');
|
||||
return Object.freeze({ mode: 'live_only' as const });
|
||||
}
|
||||
if (value.mode !== 'durable_artifact') {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'output mode is invalid',
|
||||
);
|
||||
}
|
||||
exactObject(value, ['mode', 'retentionPolicy'], [], 'output');
|
||||
exactObject(
|
||||
value.retentionPolicy,
|
||||
['retentionMs', 'revision'],
|
||||
[],
|
||||
'output.retentionPolicy',
|
||||
);
|
||||
if (
|
||||
!Number.isSafeInteger(value.retentionPolicy.retentionMs) ||
|
||||
(value.retentionPolicy.retentionMs as number) < MIN_OUTPUT_RETENTION_MS ||
|
||||
(value.retentionPolicy.retentionMs as number) > MAX_OUTPUT_RETENTION_MS
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'output retention is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
mode: 'durable_artifact' as const,
|
||||
retentionPolicy: Object.freeze({
|
||||
revision: identity(
|
||||
value.retentionPolicy.revision,
|
||||
'output.retentionPolicy.revision',
|
||||
),
|
||||
retentionMs: value.retentionPolicy.retentionMs as number,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeCommand(
|
||||
value: unknown,
|
||||
): Readonly<LocalPluginPackagePromptCommand> {
|
||||
exactObject(
|
||||
value,
|
||||
['operation', 'options', 'request', 'schemaVersion'],
|
||||
[],
|
||||
'command',
|
||||
);
|
||||
if (
|
||||
value.schemaVersion !== 1 ||
|
||||
(value.operation !== 'prompt.inspect' &&
|
||||
value.operation !== 'prompt.execution.inspect' &&
|
||||
value.operation !== 'prompt.execution.output.read' &&
|
||||
value.operation !== 'prompt.execute')
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'command version or operation is invalid',
|
||||
);
|
||||
}
|
||||
if (value.operation === 'prompt.inspect') {
|
||||
const options = normalizeOptions(value.options, 'prompt.inspect');
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'auditEventId',
|
||||
'failureAuditEventId',
|
||||
'packageName',
|
||||
'projectId',
|
||||
'requestId',
|
||||
],
|
||||
[],
|
||||
'request',
|
||||
);
|
||||
if (
|
||||
typeof value.request.packageName !== 'string' ||
|
||||
!PACKAGE_NAME_PATTERN.test(value.request.packageName) ||
|
||||
typeof value.request.auditEventId !== 'string' ||
|
||||
!UUID_V4_PATTERN.test(value.request.auditEventId) ||
|
||||
typeof value.request.failureAuditEventId !== 'string' ||
|
||||
!UUID_V4_PATTERN.test(value.request.failureAuditEventId) ||
|
||||
value.request.auditEventId === value.request.failureAuditEventId
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'request value is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: 'prompt.inspect',
|
||||
options,
|
||||
request: Object.freeze({
|
||||
projectId: identity(value.request.projectId, 'projectId'),
|
||||
packageName: value.request.packageName,
|
||||
requestId: identity(value.request.requestId, 'requestId'),
|
||||
auditEventId: value.request.auditEventId,
|
||||
failureAuditEventId: value.request.failureAuditEventId,
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (value.operation === 'prompt.execution.inspect') {
|
||||
const options = normalizeOptions(value.options, 'prompt.execution.inspect');
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'auditEventId',
|
||||
'executionRequestId',
|
||||
'failureAuditEventId',
|
||||
'packageName',
|
||||
'projectId',
|
||||
'promptId',
|
||||
'requestId',
|
||||
],
|
||||
[],
|
||||
'request',
|
||||
);
|
||||
if (
|
||||
typeof value.request.packageName !== 'string' ||
|
||||
!PACKAGE_NAME_PATTERN.test(value.request.packageName) ||
|
||||
typeof value.request.promptId !== 'string' ||
|
||||
!RESOURCE_ID_PATTERN.test(value.request.promptId) ||
|
||||
typeof value.request.auditEventId !== 'string' ||
|
||||
!UUID_V4_PATTERN.test(value.request.auditEventId) ||
|
||||
typeof value.request.failureAuditEventId !== 'string' ||
|
||||
!UUID_V4_PATTERN.test(value.request.failureAuditEventId) ||
|
||||
value.request.auditEventId === value.request.failureAuditEventId
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'request value is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: 'prompt.execution.inspect',
|
||||
options,
|
||||
request: Object.freeze({
|
||||
projectId: identity(value.request.projectId, 'projectId'),
|
||||
packageName: value.request.packageName,
|
||||
promptId: value.request.promptId,
|
||||
executionRequestId: identity(
|
||||
value.request.executionRequestId,
|
||||
'executionRequestId',
|
||||
),
|
||||
requestId: identity(value.request.requestId, 'requestId'),
|
||||
auditEventId: value.request.auditEventId,
|
||||
failureAuditEventId: value.request.failureAuditEventId,
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (value.operation === 'prompt.execution.output.read') {
|
||||
const options = normalizeOptions(
|
||||
value.options,
|
||||
'prompt.execution.output.read',
|
||||
);
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'auditEventId',
|
||||
'executionRequestId',
|
||||
'failureAuditEventId',
|
||||
'packageName',
|
||||
'projectId',
|
||||
'promptId',
|
||||
'requestId',
|
||||
],
|
||||
[],
|
||||
'request',
|
||||
);
|
||||
if (
|
||||
typeof value.request.packageName !== 'string' ||
|
||||
!PACKAGE_NAME_PATTERN.test(value.request.packageName) ||
|
||||
typeof value.request.promptId !== 'string' ||
|
||||
!RESOURCE_ID_PATTERN.test(value.request.promptId) ||
|
||||
typeof value.request.auditEventId !== 'string' ||
|
||||
!UUID_V4_PATTERN.test(value.request.auditEventId) ||
|
||||
typeof value.request.failureAuditEventId !== 'string' ||
|
||||
!UUID_V4_PATTERN.test(value.request.failureAuditEventId) ||
|
||||
value.request.auditEventId === value.request.failureAuditEventId
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'request value is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: 'prompt.execution.output.read',
|
||||
options,
|
||||
request: Object.freeze({
|
||||
projectId: identity(value.request.projectId, 'projectId'),
|
||||
packageName: value.request.packageName,
|
||||
promptId: value.request.promptId,
|
||||
executionRequestId: identity(
|
||||
value.request.executionRequestId,
|
||||
'executionRequestId',
|
||||
),
|
||||
requestId: identity(value.request.requestId, 'requestId'),
|
||||
auditEventId: value.request.auditEventId,
|
||||
failureAuditEventId: value.request.failureAuditEventId,
|
||||
}),
|
||||
});
|
||||
}
|
||||
const options = normalizeOptions(value.options, 'prompt.execute');
|
||||
const hasTemperature =
|
||||
!!value.request &&
|
||||
typeof value.request === 'object' &&
|
||||
!Array.isArray(value.request) &&
|
||||
Object.hasOwn(value.request, 'temperature');
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'auditEventId',
|
||||
'failureAuditEventId',
|
||||
'maxOutputTokens',
|
||||
'model',
|
||||
'output',
|
||||
'packageName',
|
||||
'parameters',
|
||||
'projectId',
|
||||
'promptId',
|
||||
'provider',
|
||||
'requestId',
|
||||
'timeoutMs',
|
||||
'traceId',
|
||||
],
|
||||
hasTemperature ? ['temperature'] : [],
|
||||
'request',
|
||||
);
|
||||
const request = value.request;
|
||||
if (
|
||||
typeof request.packageName !== 'string' ||
|
||||
!PACKAGE_NAME_PATTERN.test(request.packageName) ||
|
||||
typeof request.promptId !== 'string' ||
|
||||
!RESOURCE_ID_PATTERN.test(request.promptId) ||
|
||||
typeof request.auditEventId !== 'string' ||
|
||||
!UUID_V4_PATTERN.test(request.auditEventId) ||
|
||||
typeof request.failureAuditEventId !== 'string' ||
|
||||
!UUID_V4_PATTERN.test(request.failureAuditEventId) ||
|
||||
request.auditEventId === request.failureAuditEventId ||
|
||||
!Number.isSafeInteger(request.maxOutputTokens) ||
|
||||
(request.maxOutputTokens as number) < 1 ||
|
||||
(request.maxOutputTokens as number) > 1_000_000 ||
|
||||
!Number.isSafeInteger(request.timeoutMs) ||
|
||||
(request.timeoutMs as number) < 100 ||
|
||||
(request.timeoutMs as number) > MAX_TIMEOUT_MS ||
|
||||
(request.temperature !== undefined &&
|
||||
(typeof request.temperature !== 'number' ||
|
||||
!Number.isFinite(request.temperature) ||
|
||||
request.temperature < 0 ||
|
||||
request.temperature > 2))
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'request value is invalid',
|
||||
);
|
||||
}
|
||||
const output = normalizeOutputIntent(request.output);
|
||||
if (
|
||||
output.mode === 'durable_artifact' &&
|
||||
options.promptOutputKeyringPath === undefined
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'durable output requires promptOutputKeyringPath',
|
||||
);
|
||||
}
|
||||
if (
|
||||
output.mode === 'live_only' &&
|
||||
options.promptOutputKeyringPath !== undefined
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'live-only output must not configure promptOutputKeyringPath',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: 'prompt.execute',
|
||||
options,
|
||||
request: Object.freeze({
|
||||
projectId: identity(request.projectId, 'projectId'),
|
||||
packageName: request.packageName,
|
||||
promptId: request.promptId,
|
||||
requestId: identity(request.requestId, 'requestId'),
|
||||
traceId: identity(request.traceId, 'traceId'),
|
||||
auditEventId: request.auditEventId,
|
||||
failureAuditEventId: request.failureAuditEventId,
|
||||
parameters: normalizeParameters(request.parameters),
|
||||
provider: identity(request.provider, 'provider'),
|
||||
model: identity(request.model, 'model'),
|
||||
maxOutputTokens: request.maxOutputTokens as number,
|
||||
...(request.temperature === undefined
|
||||
? {}
|
||||
: { temperature: request.temperature as number }),
|
||||
timeoutMs: request.timeoutMs as number,
|
||||
output,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function readCommandFile(
|
||||
candidatePath: string,
|
||||
): Readonly<LocalPluginPackagePromptCommand> {
|
||||
try {
|
||||
return normalizeCommand(readPrivateLocalCommandFile(candidatePath));
|
||||
} catch (error) {
|
||||
if (error instanceof LocalPluginPackagePromptCommandConfigurationError) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'command file cannot be read',
|
||||
error instanceof PrivateLocalCommandFileError ? error : undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
PrivateLocalCommandFileError,
|
||||
readPrivateLocalCommandFile,
|
||||
} from '@qinglong/local-command-file';
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export type { PluginPackagePromptCatalogItem } from '@qinglong/ai/plugin-package-prompt-catalog';
|
||||
export type { PluginPackagePromptExecutionInspection } from '@qinglong/ai/plugin-package-prompt-execution-inspection';
|
||||
export type { PluginPackagePromptExecutionOutputReadResult } from '@qinglong/ai/plugin-package-prompt-execution-output-read';
|
||||
export type { PluginPackagePromptOutputArtifactReference } from '@qinglong/ai/plugin-package-prompt-output-artifact';
|
||||
export type { ModelGatewayProviderAuthority } from '@qinglong/ai/profile';
|
||||
export type { establishAuthenticatedLocalCommand } from '@qinglong/local-owner-console/authenticated-command';
|
||||
export type {
|
||||
LocalSqliteOptionalFeatureRuntimeDatabase,
|
||||
openLocalSqliteOptionalFeatureRuntimeDatabase,
|
||||
} from '@qinglong/local-sqlite/optional-feature-runtime';
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
import type {
|
||||
establishAuthenticatedLocalCommand,
|
||||
LocalSqliteOptionalFeatureRuntimeDatabase,
|
||||
ModelGatewayProviderAuthority,
|
||||
openLocalSqliteOptionalFeatureRuntimeDatabase,
|
||||
PluginPackagePromptCatalogItem,
|
||||
PluginPackagePromptExecutionInspection,
|
||||
PluginPackagePromptExecutionOutputReadResult,
|
||||
PluginPackagePromptOutputArtifactReference,
|
||||
} from './contractAuthority';
|
||||
|
||||
export interface LocalPluginPackagePromptInspectCommandOptions {
|
||||
readonly deploymentRoot: string;
|
||||
readonly databasePath: string;
|
||||
readonly profile: 'edge' | 'standalone';
|
||||
readonly ownerPepperKeyringDirectory: string;
|
||||
readonly credentialFilePath: string;
|
||||
readonly busyTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface LocalPluginPackagePromptCommandOptions
|
||||
extends LocalPluginPackagePromptInspectCommandOptions {
|
||||
readonly secretKeyringPath: string;
|
||||
readonly providerAuthorityFilePath: string;
|
||||
readonly promptOutputKeyringPath?: string;
|
||||
}
|
||||
|
||||
export interface LocalPluginPackagePromptOutputCommandOptions
|
||||
extends LocalPluginPackagePromptInspectCommandOptions {
|
||||
readonly promptOutputKeyringPath: string;
|
||||
}
|
||||
|
||||
export type LocalPluginPackagePromptOutputIntent =
|
||||
| Readonly<{ mode: 'live_only' }>
|
||||
| Readonly<{
|
||||
mode: 'durable_artifact';
|
||||
retentionPolicy: Readonly<{
|
||||
revision: string;
|
||||
retentionMs: number;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
export interface ExecuteLocalPluginPackagePromptCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'prompt.execute';
|
||||
readonly options: LocalPluginPackagePromptCommandOptions;
|
||||
readonly request: Readonly<{
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
promptId: string;
|
||||
requestId: string;
|
||||
traceId: string;
|
||||
auditEventId: string;
|
||||
failureAuditEventId: string;
|
||||
parameters: Readonly<Record<string, string>>;
|
||||
provider: string;
|
||||
model: string;
|
||||
maxOutputTokens: number;
|
||||
temperature?: number;
|
||||
timeoutMs: number;
|
||||
output: Readonly<LocalPluginPackagePromptOutputIntent>;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface InspectLocalPluginPackagePromptCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'prompt.inspect';
|
||||
readonly options: LocalPluginPackagePromptInspectCommandOptions;
|
||||
readonly request: Readonly<{
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
requestId: string;
|
||||
auditEventId: string;
|
||||
failureAuditEventId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface InspectLocalPluginPackagePromptExecutionCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'prompt.execution.inspect';
|
||||
readonly options: LocalPluginPackagePromptInspectCommandOptions;
|
||||
readonly request: Readonly<{
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
promptId: string;
|
||||
executionRequestId: string;
|
||||
requestId: string;
|
||||
auditEventId: string;
|
||||
failureAuditEventId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ReadLocalPluginPackagePromptExecutionOutputCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'prompt.execution.output.read';
|
||||
readonly options: LocalPluginPackagePromptOutputCommandOptions;
|
||||
readonly request: Readonly<{
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
promptId: string;
|
||||
executionRequestId: string;
|
||||
requestId: string;
|
||||
auditEventId: string;
|
||||
failureAuditEventId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type LocalPluginPackagePromptCommand =
|
||||
| ExecuteLocalPluginPackagePromptCommand
|
||||
| InspectLocalPluginPackagePromptCommand
|
||||
| InspectLocalPluginPackagePromptExecutionCommand
|
||||
| ReadLocalPluginPackagePromptExecutionOutputCommand;
|
||||
|
||||
export type LocalPluginPackagePromptCommandResult =
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'prompt.inspect';
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
found: boolean;
|
||||
publicationState: 'active' | 'withdrawn' | 'absent' | null;
|
||||
prompts: readonly Readonly<PluginPackagePromptCatalogItem>[];
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'prompt.execution.inspect';
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
promptId: string;
|
||||
executionRequestId: string;
|
||||
found: boolean;
|
||||
execution: Readonly<PluginPackagePromptExecutionInspection> | null;
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'prompt.execution.output.read';
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
promptId: string;
|
||||
executionRequestId: string;
|
||||
status: 'not_found';
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'prompt.execution.output.read';
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
promptId: string;
|
||||
executionRequestId: string;
|
||||
status: 'available';
|
||||
reference: Readonly<PluginPackagePromptOutputArtifactReference>;
|
||||
result: Extract<
|
||||
PluginPackagePromptExecutionOutputReadResult,
|
||||
{
|
||||
status: 'available';
|
||||
}
|
||||
>['result'];
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'prompt.execute';
|
||||
status: 'executed' | 'resumed' | 'existing';
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
promptId: string;
|
||||
requestId: string;
|
||||
invocationId: string;
|
||||
runId: string;
|
||||
stepRunId: string;
|
||||
planDigest: string;
|
||||
receiptDigest: string;
|
||||
finalizationDigest: string;
|
||||
runStatus: 'succeeded' | 'failed' | 'cancelled' | 'timed_out';
|
||||
result: Readonly<{
|
||||
provider: string;
|
||||
model: string;
|
||||
text: string;
|
||||
finishReason: string;
|
||||
usage: Readonly<{
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
totalTokens: number;
|
||||
costMicros?: number;
|
||||
}>;
|
||||
}> | null;
|
||||
outputArtifact?: Readonly<PluginPackagePromptOutputArtifactReference>;
|
||||
}>;
|
||||
|
||||
export interface LocalPluginPackagePromptCommandRunner {
|
||||
run(commandFilePath: string): Promise<LocalPluginPackagePromptCommandResult>;
|
||||
}
|
||||
|
||||
export interface LocalPluginPackagePromptCommandRunnerDependencies {
|
||||
readonly openDatabase: typeof openLocalSqliteOptionalFeatureRuntimeDatabase;
|
||||
readonly authenticate: typeof establishAuthenticatedLocalCommand;
|
||||
readonly loadProviders: (
|
||||
options: Readonly<{
|
||||
database: LocalSqliteOptionalFeatureRuntimeDatabase;
|
||||
command: Readonly<ExecuteLocalPluginPackagePromptCommand>;
|
||||
now: () => number;
|
||||
}>,
|
||||
) => Promise<ModelGatewayProviderAuthority>;
|
||||
readonly now: () => number;
|
||||
}
|
||||
|
||||
export class LocalPluginPackagePromptCommandConfigurationError extends TypeError {
|
||||
readonly code = 'LOCAL_PLUGIN_PACKAGE_PROMPT_COMMAND_CONFIGURATION_INVALID';
|
||||
|
||||
constructor(message: string, readonly cause?: unknown) {
|
||||
super(
|
||||
`Local Plugin Package Prompt command configuration is invalid: ${message}`,
|
||||
);
|
||||
this.name = 'LocalPluginPackagePromptCommandConfigurationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalPluginPackagePromptAuthenticationError extends Error {
|
||||
readonly code = 'LOCAL_PLUGIN_PACKAGE_PROMPT_AUTHENTICATION_REQUIRED';
|
||||
|
||||
constructor() {
|
||||
super('Local Plugin Package Prompt requires a current strong User');
|
||||
this.name = 'LocalPluginPackagePromptAuthenticationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalPluginPackagePromptAuthorizationError extends Error {
|
||||
readonly code = 'LOCAL_PLUGIN_PACKAGE_PROMPT_FORBIDDEN';
|
||||
|
||||
constructor() {
|
||||
super('Local Plugin Package Prompt is not authorized');
|
||||
this.name = 'LocalPluginPackagePromptAuthorizationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalPluginPackagePromptNotFoundError extends Error {
|
||||
readonly code = 'LOCAL_PLUGIN_PACKAGE_PROMPT_NOT_FOUND';
|
||||
|
||||
constructor() {
|
||||
super('Active Plugin Package Prompt is not available');
|
||||
this.name = 'LocalPluginPackagePromptNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalPluginPackagePromptUnavailableError extends Error {
|
||||
readonly code = 'LOCAL_PLUGIN_PACKAGE_PROMPT_UNAVAILABLE';
|
||||
|
||||
constructor(options?: ErrorOptions) {
|
||||
super('Local Plugin Package Prompt is unavailable', options);
|
||||
this.name = 'LocalPluginPackagePromptUnavailableError';
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
type ActiveModelGatewayCapability,
|
||||
BoundModelProviderCredentialProvider,
|
||||
EncryptedLocalSecretService,
|
||||
LocalModelProviderCredentialRepository,
|
||||
LocalSecretKeyringFileProvider,
|
||||
type LocalSqliteOptionalFeatureRuntimeDatabase,
|
||||
type ModelGatewayProviderAuthority,
|
||||
type PluginPackagePromptExecutionPlan,
|
||||
loadProjectedModelGatewayProviderAuthority,
|
||||
} from './supportAuthority';
|
||||
import {
|
||||
type ExecuteLocalPluginPackagePromptCommand,
|
||||
LocalPluginPackagePromptCommandConfigurationError,
|
||||
type LocalPluginPackagePromptCommandRunnerDependencies,
|
||||
LocalPluginPackagePromptUnavailableError,
|
||||
} from './contracts';
|
||||
|
||||
export async function defaultLoadProviders(
|
||||
input: Readonly<{
|
||||
database: LocalSqliteOptionalFeatureRuntimeDatabase;
|
||||
command: Readonly<ExecuteLocalPluginPackagePromptCommand>;
|
||||
now: () => number;
|
||||
}>,
|
||||
): Promise<ModelGatewayProviderAuthority> {
|
||||
const credentials = new LocalModelProviderCredentialRepository(
|
||||
input.database.authority,
|
||||
{ now: input.now },
|
||||
);
|
||||
const secrets = new EncryptedLocalSecretService(
|
||||
input.database.localSecrets,
|
||||
new LocalSecretKeyringFileProvider(input.command.options.secretKeyringPath),
|
||||
);
|
||||
return loadProjectedModelGatewayProviderAuthority({
|
||||
configFile: input.command.options.providerAuthorityFilePath,
|
||||
credentials: new BoundModelProviderCredentialProvider({
|
||||
bindings: credentials,
|
||||
audit: credentials,
|
||||
secrets,
|
||||
now: input.now,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function dependencies(
|
||||
value: LocalPluginPackagePromptCommandRunnerDependencies,
|
||||
): Readonly<LocalPluginPackagePromptCommandRunnerDependencies> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.keys(value).sort().join('\0') !==
|
||||
['authenticate', 'loadProviders', 'now', 'openDatabase']
|
||||
.sort()
|
||||
.join('\0') ||
|
||||
typeof value.openDatabase !== 'function' ||
|
||||
typeof value.authenticate !== 'function' ||
|
||||
typeof value.loadProviders !== 'function' ||
|
||||
typeof value.now !== 'function'
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'runner dependencies are invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({ ...value });
|
||||
}
|
||||
|
||||
export async function stopGateway(
|
||||
capability: ActiveModelGatewayCapability | undefined,
|
||||
): Promise<void> {
|
||||
if (!capability) return;
|
||||
for (let attempt = 0; attempt < 8; attempt += 1) {
|
||||
if ((await capability.stop()) === 'stopped') return;
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
}
|
||||
throw new LocalPluginPackagePromptUnavailableError();
|
||||
}
|
||||
|
||||
export function assertReplayRequest(
|
||||
plan: Readonly<PluginPackagePromptExecutionPlan>,
|
||||
command: Readonly<ExecuteLocalPluginPackagePromptCommand>,
|
||||
): void {
|
||||
const request = command.request;
|
||||
if (
|
||||
plan.target.projectId !== request.projectId ||
|
||||
plan.target.packageName !== request.packageName ||
|
||||
plan.target.promptId !== request.promptId ||
|
||||
plan.traceId !== request.traceId ||
|
||||
plan.provider !== request.provider ||
|
||||
plan.model !== request.model ||
|
||||
plan.maxOutputTokens !== request.maxOutputTokens ||
|
||||
plan.temperature !== (request.temperature ?? null) ||
|
||||
plan.output?.mode !== request.output.mode ||
|
||||
(request.output.mode === 'durable_artifact' &&
|
||||
(plan.output?.mode !== 'durable_artifact' ||
|
||||
JSON.stringify(plan.output.retentionPolicy) !==
|
||||
JSON.stringify(request.output.retentionPolicy)))
|
||||
) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'request conflicts with the durable Prompt plan',
|
||||
);
|
||||
}
|
||||
}
|
||||
+535
@@ -0,0 +1,535 @@
|
||||
import {
|
||||
type ActiveModelGatewayCapability,
|
||||
type AuthenticatedLocalCommand,
|
||||
type AuthorizedPluginPackagePromptExecutionInspection,
|
||||
LocalModelInvocationFeatureActivationRepository,
|
||||
LocalModelInvocationRepository,
|
||||
LocalModelPriceCatalogRepository,
|
||||
LocalPluginPackagePromptAdmissionRepository,
|
||||
LocalPluginPackagePromptExecutionInspectionRepository,
|
||||
LocalPluginPackagePromptExecutionOutputReferenceRepository,
|
||||
LocalPluginPackagePromptOutputArtifactRepository,
|
||||
LocalPluginPackagePromptOutputRetentionRepository,
|
||||
type LocalSqliteAuthenticatedUserCredentialFence,
|
||||
LocalSqliteAuthenticatedManagementFenceError,
|
||||
PluginPackagePromptExecutionInspectionAuthorizationFenceConflictError,
|
||||
type PluginPackagePromptExecutionPlan,
|
||||
PluginPackagePromptExecutionOutputReadService,
|
||||
PluginPackagePromptExecutor,
|
||||
type PluginPackagePromptOutputArtifactReadAuthorizer,
|
||||
type PluginPackagePromptOutputCompletionCapability,
|
||||
PluginPackagePromptOutputCompletionCoordinator,
|
||||
PluginPackagePromptOutputFileKeyring,
|
||||
PluginPackagePromptOutputReadService,
|
||||
ProjectPolicyEngine,
|
||||
assertLocalModelInvocationFeatureActive,
|
||||
bootstrapModelGatewayProfile,
|
||||
commitLocalSqliteSecurityAuditInTransaction,
|
||||
confirmLocalSqliteAuthenticatedUserCredentialFence,
|
||||
confirmLocalSqliteProjectPolicyFence,
|
||||
createPluginPackagePromptCatalogResult,
|
||||
establishAuthenticatedLocalCommand,
|
||||
normalizeSecurityAuditRecord,
|
||||
openLocalSqliteOptionalFeatureRuntimeDatabase,
|
||||
} from './runnerAuthority';
|
||||
import {
|
||||
LocalPluginPackagePromptAuthenticationError,
|
||||
LocalPluginPackagePromptAuthorizationError,
|
||||
type LocalPluginPackagePromptCommandResult,
|
||||
LocalPluginPackagePromptCommandConfigurationError,
|
||||
type LocalPluginPackagePromptCommandRunner,
|
||||
type LocalPluginPackagePromptCommandRunnerDependencies,
|
||||
LocalPluginPackagePromptNotFoundError,
|
||||
LocalPluginPackagePromptUnavailableError,
|
||||
} from './contracts';
|
||||
import { readCommandFile } from './codec';
|
||||
import {
|
||||
PROMPT_PERMISSIONS,
|
||||
allowedAudit,
|
||||
authorize,
|
||||
sameFence,
|
||||
} from './authorization';
|
||||
import {
|
||||
assertReplayRequest,
|
||||
defaultLoadProviders,
|
||||
dependencies,
|
||||
stopGateway,
|
||||
} from './executionSupport';
|
||||
|
||||
export function createLocalPluginPackagePromptCommandRunner(
|
||||
candidateDependencies: LocalPluginPackagePromptCommandRunnerDependencies = {
|
||||
openDatabase: openLocalSqliteOptionalFeatureRuntimeDatabase,
|
||||
authenticate: establishAuthenticatedLocalCommand,
|
||||
loadProviders: defaultLoadProviders,
|
||||
now: Date.now,
|
||||
},
|
||||
): LocalPluginPackagePromptCommandRunner {
|
||||
const adapters = dependencies(candidateDependencies);
|
||||
return Object.freeze({
|
||||
async run(commandFilePath: string) {
|
||||
const command = readCommandFile(commandFilePath);
|
||||
const database = await adapters.openDatabase({
|
||||
databasePath: command.options.databasePath,
|
||||
profile: command.options.profile,
|
||||
...(command.options.busyTimeoutMs === undefined
|
||||
? {}
|
||||
: { busyTimeoutMs: command.options.busyTimeoutMs }),
|
||||
});
|
||||
let capability: ActiveModelGatewayCapability | undefined;
|
||||
let featureReady = false;
|
||||
let authenticated: Readonly<AuthenticatedLocalCommand> | undefined;
|
||||
let authorization: Awaited<ReturnType<typeof authorize>> | undefined;
|
||||
try {
|
||||
const activation =
|
||||
command.operation === 'prompt.execute'
|
||||
? assertLocalModelInvocationFeatureActive(database.authority.client)
|
||||
: null;
|
||||
featureReady = true;
|
||||
authenticated = await adapters.authenticate(database, {
|
||||
deploymentRoot: command.options.deploymentRoot,
|
||||
databasePath: command.options.databasePath,
|
||||
ownerPepperKeyringDirectory:
|
||||
command.options.ownerPepperKeyringDirectory,
|
||||
credentialFilePath: command.options.credentialFilePath,
|
||||
authenticationNamespace: 'local_plugin_package_prompt',
|
||||
});
|
||||
await authenticated.confirm();
|
||||
const nowMs = adapters.now();
|
||||
if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
|
||||
throw new LocalPluginPackagePromptCommandConfigurationError(
|
||||
'clock is invalid',
|
||||
);
|
||||
}
|
||||
authorization = await authorize(
|
||||
database,
|
||||
authenticated.principal,
|
||||
command.request.projectId,
|
||||
nowMs,
|
||||
command.operation === 'prompt.inspect'
|
||||
? Object.freeze(['model.invoke'] as const)
|
||||
: command.operation === 'prompt.execution.inspect'
|
||||
? Object.freeze(['run.read'] as const)
|
||||
: command.operation === 'prompt.execution.output.read'
|
||||
? Object.freeze(['artifact.read'] as const)
|
||||
: PROMPT_PERMISSIONS,
|
||||
);
|
||||
const authorized = authorization;
|
||||
const credentialFence =
|
||||
authenticated.databaseFence as Readonly<LocalSqliteAuthenticatedUserCredentialFence>;
|
||||
const audit = allowedAudit(
|
||||
command,
|
||||
authorized.principal,
|
||||
authorized.decision,
|
||||
nowMs,
|
||||
);
|
||||
if (command.operation === 'prompt.inspect') {
|
||||
await database.securityAudit.record(audit);
|
||||
const publication = await database.automationPublications.findCurrent(
|
||||
command.request.projectId,
|
||||
command.request.packageName,
|
||||
);
|
||||
const catalog = createPluginPackagePromptCatalogResult(
|
||||
command.request.projectId,
|
||||
command.request.packageName,
|
||||
publication,
|
||||
);
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
projectId: catalog.projectId,
|
||||
packageName: catalog.packageName,
|
||||
found: catalog.found,
|
||||
publicationState: catalog.publicationState,
|
||||
prompts: catalog.prompts,
|
||||
});
|
||||
}
|
||||
if (command.operation === 'prompt.execution.inspect') {
|
||||
const inspections =
|
||||
new LocalPluginPackagePromptExecutionInspectionRepository(
|
||||
database.authority,
|
||||
Object.freeze({
|
||||
confirm(
|
||||
inspection: Readonly<AuthorizedPluginPackagePromptExecutionInspection>,
|
||||
auditReplay: boolean,
|
||||
) {
|
||||
try {
|
||||
if (
|
||||
inspection.actor.type !==
|
||||
authorized.principal.subject.type ||
|
||||
inspection.actor.id !== authorized.principal.subject.id ||
|
||||
inspection.projectId !== command.request.projectId ||
|
||||
!sameFence(inspection.fence, authorized.decision.fence)
|
||||
) {
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
confirmLocalSqliteAuthenticatedUserCredentialFence(
|
||||
database.authority,
|
||||
credentialFence,
|
||||
);
|
||||
confirmLocalSqliteProjectPolicyFence(
|
||||
database.authority,
|
||||
inspection.projectId,
|
||||
inspection.actor,
|
||||
inspection.fence,
|
||||
);
|
||||
commitLocalSqliteSecurityAuditInTransaction(
|
||||
database.authority,
|
||||
inspection.audit,
|
||||
auditReplay,
|
||||
);
|
||||
} catch {
|
||||
throw new PluginPackagePromptExecutionInspectionAuthorizationFenceConflictError();
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
const result = await inspections.inspectAuthorized({
|
||||
projectId: command.request.projectId,
|
||||
packageName: command.request.packageName,
|
||||
promptId: command.request.promptId,
|
||||
executionRequestId: command.request.executionRequestId,
|
||||
actor: authorized.principal.subject,
|
||||
fence: authorized.decision.fence,
|
||||
audit,
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
projectId: result.projectId,
|
||||
packageName: result.packageName,
|
||||
promptId: result.promptId,
|
||||
executionRequestId: result.executionRequestId,
|
||||
found: result.found,
|
||||
execution: result.execution,
|
||||
});
|
||||
}
|
||||
if (command.operation === 'prompt.execution.output.read') {
|
||||
await database.securityAudit.record(audit);
|
||||
const policy = new ProjectPolicyEngine(database.projectPolicy);
|
||||
const outputReader = new PluginPackagePromptOutputReadService({
|
||||
artifacts: new LocalPluginPackagePromptOutputArtifactRepository(
|
||||
database.authority,
|
||||
),
|
||||
authorizer: Object.freeze({
|
||||
async authorize(
|
||||
request: Parameters<
|
||||
PluginPackagePromptOutputArtifactReadAuthorizer['authorize']
|
||||
>[0],
|
||||
) {
|
||||
const decision = await policy.authorize(
|
||||
request.principal,
|
||||
request.projectId,
|
||||
'artifact.read',
|
||||
);
|
||||
return decision.effect === 'allow'
|
||||
? Object.freeze({ effect: 'allow' as const })
|
||||
: Object.freeze({
|
||||
effect: decision.effect,
|
||||
reasonCode: 'artifact_read_denied',
|
||||
});
|
||||
},
|
||||
}),
|
||||
retention: new LocalPluginPackagePromptOutputRetentionRepository(
|
||||
database.authority,
|
||||
),
|
||||
keys: new PluginPackagePromptOutputFileKeyring(
|
||||
command.options.promptOutputKeyringPath,
|
||||
),
|
||||
now: adapters.now,
|
||||
});
|
||||
const result =
|
||||
await new PluginPackagePromptExecutionOutputReadService({
|
||||
references:
|
||||
new LocalPluginPackagePromptExecutionOutputReferenceRepository(
|
||||
database.authority,
|
||||
),
|
||||
outputs: outputReader,
|
||||
}).read({
|
||||
principal: authorized.principal,
|
||||
projectId: command.request.projectId,
|
||||
packageName: command.request.packageName,
|
||||
promptId: command.request.promptId,
|
||||
executionRequestId: command.request.executionRequestId,
|
||||
});
|
||||
const target = Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
projectId: result.projectId,
|
||||
packageName: result.packageName,
|
||||
promptId: result.promptId,
|
||||
executionRequestId: result.executionRequestId,
|
||||
});
|
||||
return result.status === 'not_found'
|
||||
? Object.freeze({ ...target, status: 'not_found' as const })
|
||||
: Object.freeze({
|
||||
...target,
|
||||
status: 'available' as const,
|
||||
reference: result.reference,
|
||||
result: result.result,
|
||||
});
|
||||
}
|
||||
const admissions = new LocalPluginPackagePromptAdmissionRepository(
|
||||
database.authority,
|
||||
Object.freeze({
|
||||
confirm(
|
||||
plan: Readonly<PluginPackagePromptExecutionPlan>,
|
||||
replay: boolean,
|
||||
) {
|
||||
if (
|
||||
plan.requestedBySubject.type !==
|
||||
authorized.principal.subject.type ||
|
||||
plan.requestedBySubject.id !==
|
||||
authorized.principal.subject.id ||
|
||||
plan.target.projectId !== command.request.projectId ||
|
||||
!sameFence(plan.policyFence, authorized.decision.fence)
|
||||
) {
|
||||
throw new LocalSqliteAuthenticatedManagementFenceError();
|
||||
}
|
||||
confirmLocalSqliteAuthenticatedUserCredentialFence(
|
||||
database.authority,
|
||||
credentialFence,
|
||||
);
|
||||
confirmLocalSqliteProjectPolicyFence(
|
||||
database.authority,
|
||||
plan.target.projectId,
|
||||
plan.requestedBySubject,
|
||||
plan.policyFence,
|
||||
);
|
||||
commitLocalSqliteSecurityAuditInTransaction(
|
||||
database.authority,
|
||||
audit,
|
||||
replay,
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
const existing = await admissions.findPlanByRequestId(
|
||||
command.request.requestId,
|
||||
);
|
||||
if (existing) assertReplayRequest(existing, command);
|
||||
const publication = existing
|
||||
? await database.automationPublications.findByDigest(
|
||||
existing.target.publicationDigest,
|
||||
)
|
||||
: await database.automationPublications.findCurrent(
|
||||
command.request.projectId,
|
||||
command.request.packageName,
|
||||
);
|
||||
if (
|
||||
!publication ||
|
||||
publication.state !== 'active' ||
|
||||
publication.target.projectId !== command.request.projectId ||
|
||||
publication.target.packageName !== command.request.packageName ||
|
||||
!publication.definitions.prompts.some(
|
||||
({ id }) => id === command.request.promptId,
|
||||
)
|
||||
) {
|
||||
throw new LocalPluginPackagePromptNotFoundError();
|
||||
}
|
||||
if (
|
||||
existing &&
|
||||
(!sameFence(existing.policyFence, authorization.decision.fence) ||
|
||||
existing.requestedBySubject.type !==
|
||||
authorization.principal.subject.type ||
|
||||
existing.requestedBySubject.id !==
|
||||
authorization.principal.subject.id)
|
||||
) {
|
||||
throw new LocalPluginPackagePromptAuthorizationError();
|
||||
}
|
||||
const repository = new LocalModelInvocationRepository(
|
||||
database.authority,
|
||||
);
|
||||
const pricing = new LocalModelPriceCatalogRepository(
|
||||
database.authority,
|
||||
);
|
||||
let durableOutput:
|
||||
| PluginPackagePromptOutputCompletionCapability
|
||||
| undefined;
|
||||
const outputKeys =
|
||||
command.request.output.mode === 'durable_artifact'
|
||||
? new PluginPackagePromptOutputFileKeyring(
|
||||
command.options.promptOutputKeyringPath!,
|
||||
)
|
||||
: undefined;
|
||||
const gateway = await bootstrapModelGatewayProfile({
|
||||
enabled: true,
|
||||
profile: command.options.profile,
|
||||
loadStorage: async () =>
|
||||
Object.freeze({
|
||||
repository,
|
||||
pricing,
|
||||
close: async () => undefined,
|
||||
}),
|
||||
loadProviders: () =>
|
||||
adapters.loadProviders({
|
||||
database,
|
||||
command,
|
||||
now: adapters.now,
|
||||
}),
|
||||
...(outputKeys === undefined
|
||||
? {}
|
||||
: {
|
||||
createSuccessfulCompletion: (coordinator) => {
|
||||
durableOutput =
|
||||
new PluginPackagePromptOutputCompletionCoordinator({
|
||||
coordinator,
|
||||
keys: outputKeys,
|
||||
now: adapters.now,
|
||||
});
|
||||
return durableOutput;
|
||||
},
|
||||
}),
|
||||
confirmActive: async () => {
|
||||
await database.authority.enqueue(
|
||||
async () => {
|
||||
const current =
|
||||
new LocalModelInvocationFeatureActivationRepository(
|
||||
database.authority.client,
|
||||
).findCurrent();
|
||||
if (
|
||||
activation === null ||
|
||||
current?.state !== 'active' ||
|
||||
current.generation !== activation.generation ||
|
||||
current.transitionDigest !== activation.transitionDigest
|
||||
) {
|
||||
throw new LocalPluginPackagePromptUnavailableError();
|
||||
}
|
||||
},
|
||||
() => new LocalPluginPackagePromptUnavailableError(),
|
||||
);
|
||||
},
|
||||
audit: async () => undefined,
|
||||
maxConcurrent: 1,
|
||||
recoveryLimit: command.options.profile === 'edge' ? 4 : 16,
|
||||
now: adapters.now,
|
||||
});
|
||||
if (gateway.status !== 'active') {
|
||||
throw new LocalPluginPackagePromptUnavailableError();
|
||||
}
|
||||
capability = gateway.capability;
|
||||
const executor = new PluginPackagePromptExecutor({
|
||||
admissions,
|
||||
invocations: repository,
|
||||
gateway: capability,
|
||||
...(durableOutput === undefined ? {} : { durableOutput }),
|
||||
});
|
||||
const plannedAtMs = existing?.plannedAtMs ?? nowMs;
|
||||
const deadlineAtMs =
|
||||
existing?.deadlineAtMs ?? plannedAtMs + command.request.timeoutMs;
|
||||
const executed = await executor.execute({
|
||||
publication,
|
||||
expectedPublicationDigest: publication.publicationDigest,
|
||||
promptId: command.request.promptId,
|
||||
requestId: command.request.requestId,
|
||||
traceId: command.request.traceId,
|
||||
requestedBySubject: authorization.principal.subject,
|
||||
policyFence: authorization.decision.fence as Readonly<{
|
||||
projectVersion: number;
|
||||
bindingVersion: number;
|
||||
}>,
|
||||
parameters: command.request.parameters,
|
||||
provider: command.request.provider,
|
||||
model: command.request.model,
|
||||
maxOutputTokens: command.request.maxOutputTokens,
|
||||
...(command.request.temperature === undefined
|
||||
? {}
|
||||
: { temperature: command.request.temperature }),
|
||||
plannedAtMs,
|
||||
deadlineAtMs,
|
||||
output: command.request.output,
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
status: executed.status,
|
||||
projectId: command.request.projectId,
|
||||
packageName: command.request.packageName,
|
||||
promptId: command.request.promptId,
|
||||
requestId: executed.admission.requestId,
|
||||
invocationId: executed.admission.invocationId,
|
||||
runId: executed.admission.runId,
|
||||
stepRunId: executed.admission.stepRunId,
|
||||
planDigest: executed.admission.planDigest,
|
||||
receiptDigest: executed.admission.receiptDigest,
|
||||
finalizationDigest: executed.finalization.receiptDigest,
|
||||
runStatus: executed.finalization.runStatus,
|
||||
result: executed.result,
|
||||
...(executed.outputArtifact === undefined
|
||||
? {}
|
||||
: { outputArtifact: executed.outputArtifact }),
|
||||
});
|
||||
} catch (error) {
|
||||
if (featureReady) {
|
||||
const occurredAtMs = adapters.now();
|
||||
if (!Number.isSafeInteger(occurredAtMs) || occurredAtMs < 0) {
|
||||
throw new LocalPluginPackagePromptUnavailableError();
|
||||
}
|
||||
const outcome = !authenticated
|
||||
? ('authentication_rejected' as const)
|
||||
: error instanceof LocalPluginPackagePromptAuthorizationError
|
||||
? ('denied' as const)
|
||||
: ('authorization_unavailable' as const);
|
||||
try {
|
||||
await database.securityAudit.record(
|
||||
normalizeSecurityAuditRecord({
|
||||
eventId: command.request.failureAuditEventId,
|
||||
requestId: command.request.requestId,
|
||||
operationId: command.operation,
|
||||
projectId: command.request.projectId,
|
||||
subject: authenticated?.principal.subject ?? null,
|
||||
authenticationId:
|
||||
authenticated?.principal.authenticationId ?? null,
|
||||
outcome,
|
||||
reasons: [
|
||||
!authenticated
|
||||
? 'strong_authentication_required'
|
||||
: outcome === 'denied'
|
||||
? 'permission_missing'
|
||||
: command.operation === 'prompt.inspect'
|
||||
? 'prompt_catalog_unavailable'
|
||||
: command.operation === 'prompt.execution.inspect'
|
||||
? 'prompt_execution_inspection_unavailable'
|
||||
: command.operation === 'prompt.execution.output.read'
|
||||
? 'prompt_execution_output_read_unavailable'
|
||||
: 'prompt_execution_unavailable',
|
||||
],
|
||||
fence: authorization?.decision.fence ?? null,
|
||||
occurredAtMs,
|
||||
}),
|
||||
);
|
||||
} catch (auditError) {
|
||||
throw new LocalPluginPackagePromptUnavailableError({
|
||||
cause: auditError instanceof Error ? auditError : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (
|
||||
error instanceof LocalPluginPackagePromptCommandConfigurationError ||
|
||||
error instanceof LocalPluginPackagePromptAuthenticationError ||
|
||||
error instanceof LocalPluginPackagePromptAuthorizationError ||
|
||||
error instanceof LocalPluginPackagePromptNotFoundError
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new LocalPluginPackagePromptUnavailableError({
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
});
|
||||
} finally {
|
||||
let stopError: unknown;
|
||||
try {
|
||||
await stopGateway(capability);
|
||||
} catch (error) {
|
||||
stopError = error;
|
||||
}
|
||||
await database.close();
|
||||
if (stopError) throw stopError;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function runLocalPluginPackagePromptCommandFile(
|
||||
commandFilePath: string,
|
||||
): Promise<LocalPluginPackagePromptCommandResult> {
|
||||
return createLocalPluginPackagePromptCommandRunner().run(commandFilePath);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
export {
|
||||
LocalModelInvocationFeatureActivationRepository,
|
||||
assertLocalModelInvocationFeatureActive,
|
||||
} from '@qinglong/ai/local-feature-activation';
|
||||
export { LocalModelInvocationRepository } from '@qinglong/ai/local-model-invocation-storage';
|
||||
export { LocalModelPriceCatalogRepository } from '@qinglong/ai/local-price-catalog-storage';
|
||||
export { LocalPluginPackagePromptAdmissionRepository } from '@qinglong/ai/local-plugin-package-prompt-admission-storage';
|
||||
export { LocalPluginPackagePromptExecutionInspectionRepository } from '@qinglong/ai/local-plugin-package-prompt-execution-inspection';
|
||||
export { LocalPluginPackagePromptExecutionOutputReferenceRepository } from '@qinglong/ai/local-plugin-package-prompt-execution-output-reference-storage';
|
||||
export { LocalPluginPackagePromptOutputArtifactRepository } from '@qinglong/ai/local-plugin-package-prompt-output-artifact-storage';
|
||||
export { LocalPluginPackagePromptOutputRetentionRepository } from '@qinglong/ai/local-plugin-package-prompt-output-retention-storage';
|
||||
export { PluginPackagePromptExecutor } from '@qinglong/ai/plugin-package-prompt-executor';
|
||||
export { createPluginPackagePromptCatalogResult } from '@qinglong/ai/plugin-package-prompt-catalog';
|
||||
export { PluginPackagePromptExecutionInspectionAuthorizationFenceConflictError } from '@qinglong/ai/plugin-package-prompt-execution-inspection';
|
||||
export type { AuthorizedPluginPackagePromptExecutionInspection } from '@qinglong/ai/plugin-package-prompt-execution-inspection';
|
||||
export { PluginPackagePromptOutputCompletionCoordinator } from '@qinglong/ai/plugin-package-prompt-output-completion';
|
||||
export type { PluginPackagePromptOutputCompletionCapability } from '@qinglong/ai/plugin-package-prompt-output-completion';
|
||||
export { PluginPackagePromptOutputFileKeyring } from '@qinglong/ai/plugin-package-prompt-output-file-keyring';
|
||||
export { PluginPackagePromptOutputReadService } from '@qinglong/ai/plugin-package-prompt-output-read';
|
||||
export { PluginPackagePromptExecutionOutputReadService } from '@qinglong/ai/plugin-package-prompt-execution-output-read';
|
||||
export type { PluginPackagePromptExecutionPlan } from '@qinglong/ai/plugin-package-prompt-execution';
|
||||
export type { PluginPackagePromptOutputArtifactReadAuthorizer } from '@qinglong/ai/plugin-package-prompt-output-artifact';
|
||||
export { bootstrapModelGatewayProfile } from '@qinglong/ai/profile';
|
||||
export type { ActiveModelGatewayCapability } from '@qinglong/ai/profile';
|
||||
export { establishAuthenticatedLocalCommand } from '@qinglong/local-owner-console/authenticated-command';
|
||||
export type { AuthenticatedLocalCommand } from '@qinglong/local-owner-console/authenticated-command';
|
||||
export {
|
||||
commitLocalSqliteSecurityAuditInTransaction,
|
||||
confirmLocalSqliteAuthenticatedUserCredentialFence,
|
||||
confirmLocalSqliteProjectPolicyFence,
|
||||
LocalSqliteAuthenticatedManagementFenceError,
|
||||
openLocalSqliteOptionalFeatureRuntimeDatabase,
|
||||
} from '@qinglong/local-sqlite/optional-feature-runtime';
|
||||
export type { LocalSqliteAuthenticatedUserCredentialFence } from '@qinglong/local-sqlite/optional-feature-runtime';
|
||||
export { ProjectPolicyEngine } from '@qinglong/runtime-core/project-policy';
|
||||
export { normalizeSecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
export { LocalModelProviderCredentialRepository } from '@qinglong/ai/local-model-provider-credential-storage';
|
||||
export type { PluginPackagePromptExecutionPlan } from '@qinglong/ai/plugin-package-prompt-execution';
|
||||
export { BoundModelProviderCredentialProvider } from '@qinglong/ai/provider-credential';
|
||||
export { loadProjectedModelGatewayProviderAuthority } from '@qinglong/ai/projected-model-gateway-authority';
|
||||
export type {
|
||||
ActiveModelGatewayCapability,
|
||||
ModelGatewayProviderAuthority,
|
||||
} from '@qinglong/ai/profile';
|
||||
export {
|
||||
EncryptedLocalSecretService,
|
||||
LocalSecretKeyringFileProvider,
|
||||
} from '@qinglong/local-secret';
|
||||
export type { LocalSqliteOptionalFeatureRuntimeDatabase } from '@qinglong/local-sqlite/optional-feature-runtime';
|
||||
+435
@@ -0,0 +1,435 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
PrivateLocalCommandFileError,
|
||||
readPrivateLocalCommandFile,
|
||||
} from './codecAuthority';
|
||||
import {
|
||||
type CancelLocalPluginPackageWorkflowCommand,
|
||||
type InspectLocalPluginPackageWorkflowRunCommand,
|
||||
type ListLocalPluginPackageWorkflowRunEventsCommand,
|
||||
type ListLocalPluginPackageWorkflowRunsCommand,
|
||||
type ListLocalPluginPackageWorkflowStepRunsCommand,
|
||||
type LocalPluginPackageWorkflowCommand,
|
||||
LocalPluginPackageWorkflowCommandConfigurationError,
|
||||
type LocalPluginPackageWorkflowCommandOptions,
|
||||
type LocalPluginPackageWorkflowCommandRequestBase,
|
||||
type StartLocalPluginPackageWorkflowCommand,
|
||||
} from './contracts';
|
||||
|
||||
const MAX_PATH_BYTES = 4_096;
|
||||
const UUID_V4_PATTERN =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
|
||||
function exactObject(
|
||||
value: unknown,
|
||||
required: readonly string[],
|
||||
optional: readonly string[],
|
||||
label: string,
|
||||
): asserts value is Record<string, unknown> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
`${label} must be an object`,
|
||||
);
|
||||
}
|
||||
const keys = Object.keys(value).sort();
|
||||
const allowed = new Set([...required, ...optional]);
|
||||
if (
|
||||
required.some((key) => !keys.includes(key)) ||
|
||||
keys.some((key) => !allowed.has(key))
|
||||
) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
`${label} shape is invalid`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function boundedPath(value: unknown, label: string): string {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
!path.isAbsolute(value) ||
|
||||
path.parse(value).root === value ||
|
||||
path.normalize(value) !== value ||
|
||||
value.includes('\0') ||
|
||||
Buffer.byteLength(value, 'utf8') > MAX_PATH_BYTES
|
||||
) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
`${label} must be a normalized bounded absolute non-root path`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function descendant(root: string, candidate: string, label: string): void {
|
||||
const relative = path.relative(root, candidate);
|
||||
if (
|
||||
relative.length === 0 ||
|
||||
relative === '..' ||
|
||||
relative.startsWith(`..${path.sep}`) ||
|
||||
path.isAbsolute(relative)
|
||||
) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
`${label} must be a descendant of deploymentRoot`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOptions(
|
||||
value: unknown,
|
||||
): Readonly<LocalPluginPackageWorkflowCommandOptions> {
|
||||
exactObject(
|
||||
value,
|
||||
[
|
||||
'credentialFilePath',
|
||||
'databasePath',
|
||||
'deploymentRoot',
|
||||
'ownerPepperKeyringDirectory',
|
||||
'profile',
|
||||
],
|
||||
['busyTimeoutMs'],
|
||||
'options',
|
||||
);
|
||||
const deploymentRoot = boundedPath(value.deploymentRoot, 'deploymentRoot');
|
||||
const databasePath = boundedPath(value.databasePath, 'databasePath');
|
||||
const ownerPepperKeyringDirectory = boundedPath(
|
||||
value.ownerPepperKeyringDirectory,
|
||||
'ownerPepperKeyringDirectory',
|
||||
);
|
||||
const credentialFilePath = boundedPath(
|
||||
value.credentialFilePath,
|
||||
'credentialFilePath',
|
||||
);
|
||||
descendant(deploymentRoot, databasePath, 'databasePath');
|
||||
descendant(
|
||||
deploymentRoot,
|
||||
ownerPepperKeyringDirectory,
|
||||
'ownerPepperKeyringDirectory',
|
||||
);
|
||||
descendant(deploymentRoot, credentialFilePath, 'credentialFilePath');
|
||||
if (value.profile !== 'edge' && value.profile !== 'standalone') {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
'profile must be edge or standalone',
|
||||
);
|
||||
}
|
||||
if (
|
||||
value.busyTimeoutMs !== undefined &&
|
||||
(!Number.isSafeInteger(value.busyTimeoutMs) ||
|
||||
(value.busyTimeoutMs as number) < 100 ||
|
||||
(value.busyTimeoutMs as number) > 30_000)
|
||||
) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
'busyTimeoutMs is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
deploymentRoot,
|
||||
databasePath,
|
||||
profile: value.profile,
|
||||
ownerPepperKeyringDirectory,
|
||||
credentialFilePath,
|
||||
...(value.busyTimeoutMs === undefined
|
||||
? {}
|
||||
: { busyTimeoutMs: value.busyTimeoutMs as number }),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizedRequestBase(
|
||||
value: Record<string, unknown>,
|
||||
): LocalPluginPackageWorkflowCommandRequestBase {
|
||||
for (const key of ['projectId', 'packageName', 'requestId'] as const) {
|
||||
if (typeof value[key] !== 'string') {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
`${key} is invalid`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const key of ['auditEventId', 'failureAuditEventId'] as const) {
|
||||
if (typeof value[key] !== 'string' || !UUID_V4_PATTERN.test(value[key])) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
`${key} must be a UUID v4`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (value.auditEventId === value.failureAuditEventId) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
'audit identities must be distinct',
|
||||
);
|
||||
}
|
||||
return value as unknown as LocalPluginPackageWorkflowCommandRequestBase;
|
||||
}
|
||||
|
||||
function normalizeCommand(
|
||||
value: unknown,
|
||||
): Readonly<LocalPluginPackageWorkflowCommand> {
|
||||
exactObject(
|
||||
value,
|
||||
['operation', 'options', 'request', 'schemaVersion'],
|
||||
[],
|
||||
'command',
|
||||
);
|
||||
if (
|
||||
value.schemaVersion !== 1 ||
|
||||
(value.operation !== 'workflow.inspect' &&
|
||||
value.operation !== 'workflow.run.inspect' &&
|
||||
value.operation !== 'workflow.run.list' &&
|
||||
value.operation !== 'workflow.step.list' &&
|
||||
value.operation !== 'workflow.event.list' &&
|
||||
value.operation !== 'workflow.start' &&
|
||||
value.operation !== 'workflow.cancel')
|
||||
) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
'command version or operation is invalid',
|
||||
);
|
||||
}
|
||||
const options = normalizeOptions(value.options);
|
||||
if (value.operation === 'workflow.inspect') {
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'auditEventId',
|
||||
'failureAuditEventId',
|
||||
'packageName',
|
||||
'projectId',
|
||||
'requestId',
|
||||
],
|
||||
[],
|
||||
'request',
|
||||
);
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: value.operation,
|
||||
options,
|
||||
request: normalizedRequestBase(value.request),
|
||||
});
|
||||
}
|
||||
if (value.operation === 'workflow.run.inspect') {
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'auditEventId',
|
||||
'failureAuditEventId',
|
||||
'packageName',
|
||||
'projectId',
|
||||
'requestId',
|
||||
'runId',
|
||||
'workflowId',
|
||||
],
|
||||
[],
|
||||
'request',
|
||||
);
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: value.operation,
|
||||
options,
|
||||
request: normalizedRequestBase(
|
||||
value.request,
|
||||
) as InspectLocalPluginPackageWorkflowRunCommand['request'],
|
||||
});
|
||||
}
|
||||
if (value.operation === 'workflow.run.list') {
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'after',
|
||||
'auditEventId',
|
||||
'failureAuditEventId',
|
||||
'limit',
|
||||
'packageName',
|
||||
'projectId',
|
||||
'requestId',
|
||||
'workflowId',
|
||||
],
|
||||
[],
|
||||
'request',
|
||||
);
|
||||
if (
|
||||
!Number.isSafeInteger(value.request.limit) ||
|
||||
(value.request.limit as number) < 1 ||
|
||||
(value.request.limit as number) > 64
|
||||
) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
'run list limit is invalid',
|
||||
);
|
||||
}
|
||||
if (value.request.after !== null) {
|
||||
exactObject(
|
||||
value.request.after,
|
||||
['admittedAtMs', 'runId'],
|
||||
[],
|
||||
'request.after',
|
||||
);
|
||||
if (
|
||||
!Number.isSafeInteger(value.request.after.admittedAtMs) ||
|
||||
(value.request.after.admittedAtMs as number) < 0 ||
|
||||
typeof value.request.after.runId !== 'string' ||
|
||||
!UUID_V4_PATTERN.test(value.request.after.runId)
|
||||
) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
'run list cursor is invalid',
|
||||
);
|
||||
}
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: value.operation,
|
||||
options,
|
||||
request: normalizedRequestBase(
|
||||
value.request,
|
||||
) as ListLocalPluginPackageWorkflowRunsCommand['request'],
|
||||
});
|
||||
}
|
||||
if (value.operation === 'workflow.step.list') {
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'after',
|
||||
'auditEventId',
|
||||
'failureAuditEventId',
|
||||
'limit',
|
||||
'packageName',
|
||||
'projectId',
|
||||
'requestId',
|
||||
'runId',
|
||||
'workflowId',
|
||||
],
|
||||
[],
|
||||
'request',
|
||||
);
|
||||
if (
|
||||
!Number.isSafeInteger(value.request.limit) ||
|
||||
(value.request.limit as number) < 1 ||
|
||||
(value.request.limit as number) > 64
|
||||
) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
'StepRun list limit is invalid',
|
||||
);
|
||||
}
|
||||
if (value.request.after !== null) {
|
||||
exactObject(value.request.after, ['id', 'stepKey'], [], 'request.after');
|
||||
if (
|
||||
typeof value.request.after.id !== 'string' ||
|
||||
typeof value.request.after.stepKey !== 'string'
|
||||
) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
'StepRun list cursor is invalid',
|
||||
);
|
||||
}
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: value.operation,
|
||||
options,
|
||||
request: normalizedRequestBase(
|
||||
value.request,
|
||||
) as ListLocalPluginPackageWorkflowStepRunsCommand['request'],
|
||||
});
|
||||
}
|
||||
if (value.operation === 'workflow.event.list') {
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'afterSequence',
|
||||
'auditEventId',
|
||||
'failureAuditEventId',
|
||||
'limit',
|
||||
'packageName',
|
||||
'projectId',
|
||||
'requestId',
|
||||
'runId',
|
||||
'workflowId',
|
||||
],
|
||||
[],
|
||||
'request',
|
||||
);
|
||||
if (
|
||||
!Number.isSafeInteger(value.request.limit) ||
|
||||
(value.request.limit as number) < 1 ||
|
||||
(value.request.limit as number) > 64 ||
|
||||
!Number.isSafeInteger(value.request.afterSequence) ||
|
||||
(value.request.afterSequence as number) < 0
|
||||
) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
'RunEvent list page is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: value.operation,
|
||||
options,
|
||||
request: normalizedRequestBase(
|
||||
value.request,
|
||||
) as ListLocalPluginPackageWorkflowRunEventsCommand['request'],
|
||||
});
|
||||
}
|
||||
if (value.operation === 'workflow.cancel') {
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'auditEventId',
|
||||
'failureAuditEventId',
|
||||
'mutationId',
|
||||
'packageName',
|
||||
'projectId',
|
||||
'requestId',
|
||||
'runEventId',
|
||||
'runId',
|
||||
],
|
||||
[],
|
||||
'request',
|
||||
);
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: value.operation,
|
||||
options,
|
||||
request: normalizedRequestBase(
|
||||
value.request,
|
||||
) as CancelLocalPluginPackageWorkflowCommand['request'],
|
||||
});
|
||||
}
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'auditEventId',
|
||||
'failureAuditEventId',
|
||||
'packageName',
|
||||
'planId',
|
||||
'projectId',
|
||||
'requestId',
|
||||
'runId',
|
||||
'stepRunIds',
|
||||
'workflowId',
|
||||
],
|
||||
[],
|
||||
'request',
|
||||
);
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: value.operation,
|
||||
options,
|
||||
request: normalizedRequestBase(
|
||||
value.request,
|
||||
) as StartLocalPluginPackageWorkflowCommand['request'],
|
||||
});
|
||||
}
|
||||
|
||||
export function readCommandFile(
|
||||
candidatePath: string,
|
||||
): Readonly<LocalPluginPackageWorkflowCommand> {
|
||||
try {
|
||||
return normalizeCommand(readPrivateLocalCommandFile(candidatePath));
|
||||
} catch (error) {
|
||||
if (error instanceof LocalPluginPackageWorkflowCommandConfigurationError) {
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof PrivateLocalCommandFileError) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
'command file cannot be read',
|
||||
error,
|
||||
);
|
||||
}
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
'command file cannot be read',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
PrivateLocalCommandFileError,
|
||||
readPrivateLocalCommandFile,
|
||||
} from '@qinglong/local-command-file';
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
export type {
|
||||
PluginPackageWorkflowRunEventListResult,
|
||||
PluginPackageWorkflowRunInspectionResult,
|
||||
PluginPackageWorkflowRunListResult,
|
||||
PluginPackageWorkflowStepRunListResult,
|
||||
} from '@qinglong/runtime-core/plugin-package-workflow-administration';
|
||||
export type { createLocalPluginPackageWorkflowAdministrationService } from '@qinglong/local-admin/plugin-package-workflow-administration';
|
||||
export type { establishAuthenticatedLocalCommand } from '@qinglong/local-owner-console/authenticated-command';
|
||||
export type { openLocalSqlitePluginPackageWorkflowAdministrationDatabase } from '@qinglong/local-sqlite/plugin-package-workflow-administration';
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
import type {
|
||||
createLocalPluginPackageWorkflowAdministrationService,
|
||||
establishAuthenticatedLocalCommand,
|
||||
openLocalSqlitePluginPackageWorkflowAdministrationDatabase,
|
||||
PluginPackageWorkflowRunEventListResult,
|
||||
PluginPackageWorkflowRunInspectionResult,
|
||||
PluginPackageWorkflowRunListResult,
|
||||
PluginPackageWorkflowStepRunListResult,
|
||||
} from './contractAuthority';
|
||||
|
||||
export interface LocalPluginPackageWorkflowCommandOptions {
|
||||
readonly deploymentRoot: string;
|
||||
readonly databasePath: string;
|
||||
readonly profile: 'edge' | 'standalone';
|
||||
readonly ownerPepperKeyringDirectory: string;
|
||||
readonly credentialFilePath: string;
|
||||
readonly busyTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface LocalPluginPackageWorkflowCommandRequestBase {
|
||||
readonly projectId: string;
|
||||
readonly packageName: string;
|
||||
readonly requestId: string;
|
||||
readonly auditEventId: string;
|
||||
readonly failureAuditEventId: string;
|
||||
}
|
||||
|
||||
export interface InspectLocalPluginPackageWorkflowCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'workflow.inspect';
|
||||
readonly options: LocalPluginPackageWorkflowCommandOptions;
|
||||
readonly request: LocalPluginPackageWorkflowCommandRequestBase;
|
||||
}
|
||||
|
||||
export interface StartLocalPluginPackageWorkflowCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'workflow.start';
|
||||
readonly options: LocalPluginPackageWorkflowCommandOptions;
|
||||
readonly request: LocalPluginPackageWorkflowCommandRequestBase & {
|
||||
readonly workflowId: string;
|
||||
readonly planId: string;
|
||||
readonly runId: string;
|
||||
readonly stepRunIds: Readonly<Record<string, string>>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CancelLocalPluginPackageWorkflowCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'workflow.cancel';
|
||||
readonly options: LocalPluginPackageWorkflowCommandOptions;
|
||||
readonly request: LocalPluginPackageWorkflowCommandRequestBase & {
|
||||
readonly runId: string;
|
||||
readonly mutationId: string;
|
||||
readonly runEventId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InspectLocalPluginPackageWorkflowRunCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'workflow.run.inspect';
|
||||
readonly options: LocalPluginPackageWorkflowCommandOptions;
|
||||
readonly request: LocalPluginPackageWorkflowCommandRequestBase & {
|
||||
readonly workflowId: string;
|
||||
readonly runId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ListLocalPluginPackageWorkflowRunsCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'workflow.run.list';
|
||||
readonly options: LocalPluginPackageWorkflowCommandOptions;
|
||||
readonly request: LocalPluginPackageWorkflowCommandRequestBase & {
|
||||
readonly workflowId: string;
|
||||
readonly limit: number;
|
||||
readonly after: Readonly<{ admittedAtMs: number; runId: string }> | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ListLocalPluginPackageWorkflowStepRunsCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'workflow.step.list';
|
||||
readonly options: LocalPluginPackageWorkflowCommandOptions;
|
||||
readonly request: LocalPluginPackageWorkflowCommandRequestBase & {
|
||||
readonly workflowId: string;
|
||||
readonly runId: string;
|
||||
readonly limit: number;
|
||||
readonly after: Readonly<{ stepKey: string; id: string }> | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ListLocalPluginPackageWorkflowRunEventsCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'workflow.event.list';
|
||||
readonly options: LocalPluginPackageWorkflowCommandOptions;
|
||||
readonly request: LocalPluginPackageWorkflowCommandRequestBase & {
|
||||
readonly workflowId: string;
|
||||
readonly runId: string;
|
||||
readonly limit: number;
|
||||
readonly afterSequence: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type LocalPluginPackageWorkflowCommand =
|
||||
| InspectLocalPluginPackageWorkflowCommand
|
||||
| InspectLocalPluginPackageWorkflowRunCommand
|
||||
| ListLocalPluginPackageWorkflowRunsCommand
|
||||
| ListLocalPluginPackageWorkflowStepRunsCommand
|
||||
| ListLocalPluginPackageWorkflowRunEventsCommand
|
||||
| StartLocalPluginPackageWorkflowCommand
|
||||
| CancelLocalPluginPackageWorkflowCommand;
|
||||
|
||||
export type LocalPluginPackageWorkflowCommandResult =
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'workflow.inspect';
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
found: boolean;
|
||||
publicationState: 'active' | 'withdrawn' | 'absent' | null;
|
||||
workflows: readonly Readonly<{
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
steps: readonly Readonly<{
|
||||
id: string;
|
||||
task: string;
|
||||
needs: readonly string[];
|
||||
}>[];
|
||||
}>[];
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'workflow.run.inspect';
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
workflowId: string;
|
||||
runId: string;
|
||||
found: boolean;
|
||||
run: PluginPackageWorkflowRunInspectionResult['run'];
|
||||
stepCount: number | null;
|
||||
stepStatusCounts: PluginPackageWorkflowRunInspectionResult['stepStatusCounts'];
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'workflow.run.list';
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
workflowId: string;
|
||||
after: PluginPackageWorkflowRunListResult['after'];
|
||||
runs: PluginPackageWorkflowRunListResult['runs'];
|
||||
truncated: boolean;
|
||||
next: PluginPackageWorkflowRunListResult['next'];
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'workflow.step.list';
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
workflowId: string;
|
||||
runId: string;
|
||||
found: boolean;
|
||||
stepRuns: PluginPackageWorkflowStepRunListResult['stepRuns'];
|
||||
truncated: boolean;
|
||||
next: PluginPackageWorkflowStepRunListResult['next'];
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'workflow.event.list';
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
workflowId: string;
|
||||
runId: string;
|
||||
found: boolean;
|
||||
afterSequence: number;
|
||||
headSequence: number | null;
|
||||
events: PluginPackageWorkflowRunEventListResult['events'];
|
||||
truncated: boolean;
|
||||
nextAfterSequence: number | null;
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'workflow.start';
|
||||
status: 'created' | 'existing';
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
workflowId: string;
|
||||
runId: string;
|
||||
stepCount: number;
|
||||
admittedAtMs: number;
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'workflow.cancel';
|
||||
status:
|
||||
| 'accepted'
|
||||
| 'existing'
|
||||
| 'already_requested'
|
||||
| 'already_terminal';
|
||||
projectId: string;
|
||||
packageName: string;
|
||||
workflowId: string;
|
||||
runId: string;
|
||||
runStatus: string;
|
||||
runVersion: number;
|
||||
eventSequence: number;
|
||||
cancelRequestedAtMs?: number;
|
||||
cancelReason?: string;
|
||||
}>;
|
||||
|
||||
export interface LocalPluginPackageWorkflowCommandRunner {
|
||||
run(
|
||||
commandFilePath: string,
|
||||
): Promise<LocalPluginPackageWorkflowCommandResult>;
|
||||
}
|
||||
|
||||
export interface LocalPluginPackageWorkflowCommandRunnerDependencies {
|
||||
readonly openDatabase: typeof openLocalSqlitePluginPackageWorkflowAdministrationDatabase;
|
||||
readonly authenticate: typeof establishAuthenticatedLocalCommand;
|
||||
readonly createService: typeof createLocalPluginPackageWorkflowAdministrationService;
|
||||
readonly now: () => number;
|
||||
}
|
||||
|
||||
export class LocalPluginPackageWorkflowCommandConfigurationError extends TypeError {
|
||||
readonly code = 'LOCAL_PLUGIN_PACKAGE_WORKFLOW_COMMAND_CONFIGURATION_INVALID';
|
||||
|
||||
constructor(message: string, readonly cause?: unknown) {
|
||||
super(
|
||||
`Local Plugin Package Workflow command configuration is invalid: ${message}`,
|
||||
);
|
||||
this.name = 'LocalPluginPackageWorkflowCommandConfigurationError';
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
type AuthenticatedLocalCommand,
|
||||
AuthenticatedLocalCommandAuthenticationError,
|
||||
InvalidPluginPackageWorkflowExecutionPlanError,
|
||||
LocalPluginPackageWorkflowAdministrationAuthenticationError,
|
||||
LocalPluginPackageWorkflowAdministrationAuthorizationError,
|
||||
LocalPluginPackageWorkflowAdministrationConfigurationError,
|
||||
LocalPluginPackageWorkflowAdministrationNotFoundError,
|
||||
LocalPluginPackageWorkflowAdministrationUnavailableError,
|
||||
type LocalSqliteAuthenticatedUserCredentialFence,
|
||||
LocalSqliteAuthenticatedManagementFenceError,
|
||||
type LocalSqlitePluginPackageWorkflowAdministrationDatabase,
|
||||
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError,
|
||||
PluginPackageWorkflowAdministrationMutationConflictError,
|
||||
PluginPackageWorkflowAdmissionConflictError,
|
||||
PluginPackageWorkflowAdmissionNotAllowedError,
|
||||
type SecurityAuditRecord,
|
||||
} from './supportAuthority';
|
||||
import {
|
||||
type LocalPluginPackageWorkflowCommand,
|
||||
LocalPluginPackageWorkflowCommandConfigurationError,
|
||||
type LocalPluginPackageWorkflowCommandRunnerDependencies,
|
||||
} from './contracts';
|
||||
|
||||
export function failureAudit(
|
||||
command: Readonly<LocalPluginPackageWorkflowCommand>,
|
||||
authenticated: Readonly<AuthenticatedLocalCommand> | undefined,
|
||||
error: unknown,
|
||||
occurredAtMs: number,
|
||||
): Readonly<SecurityAuditRecord> | null {
|
||||
if (
|
||||
error instanceof
|
||||
LocalPluginPackageWorkflowAdministrationAuthenticationError ||
|
||||
error instanceof
|
||||
LocalPluginPackageWorkflowAdministrationAuthorizationError ||
|
||||
error instanceof LocalPluginPackageWorkflowAdministrationUnavailableError
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
let outcome: SecurityAuditRecord['outcome'];
|
||||
let reason: string;
|
||||
if (
|
||||
!authenticated ||
|
||||
error instanceof AuthenticatedLocalCommandAuthenticationError
|
||||
) {
|
||||
outcome = 'authentication_rejected';
|
||||
reason = 'credential_rejected';
|
||||
} else if (
|
||||
error instanceof LocalSqliteAuthenticatedManagementFenceError ||
|
||||
error instanceof
|
||||
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError
|
||||
) {
|
||||
outcome = 'denied';
|
||||
reason = 'credential_or_policy_fence_rejected';
|
||||
} else if (
|
||||
error instanceof PluginPackageWorkflowAdministrationMutationConflictError ||
|
||||
error instanceof PluginPackageWorkflowAdmissionConflictError
|
||||
) {
|
||||
outcome = 'denied';
|
||||
reason = 'workflow_admission_conflict';
|
||||
} else if (
|
||||
error instanceof LocalPluginPackageWorkflowAdministrationNotFoundError ||
|
||||
error instanceof
|
||||
LocalPluginPackageWorkflowAdministrationConfigurationError ||
|
||||
error instanceof LocalPluginPackageWorkflowCommandConfigurationError ||
|
||||
error instanceof InvalidPluginPackageWorkflowExecutionPlanError ||
|
||||
error instanceof PluginPackageWorkflowAdmissionNotAllowedError
|
||||
) {
|
||||
outcome = 'denied';
|
||||
reason = 'workflow_admission_rejected';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return Object.freeze({
|
||||
eventId: command.request.failureAuditEventId,
|
||||
requestId: command.request.requestId,
|
||||
operationId:
|
||||
command.operation === 'workflow.start'
|
||||
? 'workflow.start'
|
||||
: command.operation === 'workflow.cancel'
|
||||
? 'workflow.cancel'
|
||||
: command.operation === 'workflow.step.list'
|
||||
? 'workflow.step.list'
|
||||
: command.operation === 'workflow.event.list'
|
||||
? 'workflow.event.list'
|
||||
: command.operation === 'workflow.run.list'
|
||||
? 'workflow.run.list'
|
||||
: command.operation === 'workflow.run.inspect'
|
||||
? 'workflow.run.read'
|
||||
: 'workflow.read',
|
||||
projectId: command.request.projectId,
|
||||
subject: authenticated?.principal.subject ?? null,
|
||||
authenticationId: authenticated?.principal.authenticationId ?? null,
|
||||
outcome,
|
||||
reasons: Object.freeze([reason]),
|
||||
fence: null,
|
||||
occurredAtMs,
|
||||
});
|
||||
}
|
||||
|
||||
export function dependencies(
|
||||
value: LocalPluginPackageWorkflowCommandRunnerDependencies,
|
||||
): Readonly<LocalPluginPackageWorkflowCommandRunnerDependencies> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.keys(value).sort().join('\0') !==
|
||||
['authenticate', 'createService', 'now', 'openDatabase']
|
||||
.sort()
|
||||
.join('\0') ||
|
||||
typeof value.openDatabase !== 'function' ||
|
||||
typeof value.authenticate !== 'function' ||
|
||||
typeof value.createService !== 'function' ||
|
||||
typeof value.now !== 'function'
|
||||
) {
|
||||
throw new LocalPluginPackageWorkflowCommandConfigurationError(
|
||||
'runner dependencies are invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({ ...value });
|
||||
}
|
||||
|
||||
export async function activateFence(
|
||||
database: LocalSqlitePluginPackageWorkflowAdministrationDatabase,
|
||||
authenticated: Readonly<AuthenticatedLocalCommand>,
|
||||
): Promise<void> {
|
||||
await authenticated.confirm();
|
||||
database.activateUserCredentialFence(
|
||||
authenticated.databaseFence as Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
|
||||
);
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
import {
|
||||
type AuthenticatedLocalCommand,
|
||||
type LocalPluginPackageWorkflowAdministrationService,
|
||||
createLocalPluginPackageWorkflowAdministrationService,
|
||||
establishAuthenticatedLocalCommand,
|
||||
openLocalSqlitePluginPackageWorkflowAdministrationDatabase,
|
||||
} from './runnerAuthority';
|
||||
import {
|
||||
type LocalPluginPackageWorkflowCommandResult,
|
||||
type LocalPluginPackageWorkflowCommandRunner,
|
||||
type LocalPluginPackageWorkflowCommandRunnerDependencies,
|
||||
} from './contracts';
|
||||
import { readCommandFile } from './codec';
|
||||
import { activateFence, dependencies, failureAudit } from './executionSupport';
|
||||
|
||||
export function createLocalPluginPackageWorkflowCommandRunner(
|
||||
candidateDependencies: LocalPluginPackageWorkflowCommandRunnerDependencies = {
|
||||
openDatabase: openLocalSqlitePluginPackageWorkflowAdministrationDatabase,
|
||||
authenticate: establishAuthenticatedLocalCommand,
|
||||
createService: createLocalPluginPackageWorkflowAdministrationService,
|
||||
now: Date.now,
|
||||
},
|
||||
): LocalPluginPackageWorkflowCommandRunner {
|
||||
const adapters = dependencies(candidateDependencies);
|
||||
return Object.freeze({
|
||||
async run(commandFilePath: string) {
|
||||
const command = readCommandFile(commandFilePath);
|
||||
const database = await adapters.openDatabase({
|
||||
databasePath: command.options.databasePath,
|
||||
profile: command.options.profile,
|
||||
...(command.options.busyTimeoutMs === undefined
|
||||
? {}
|
||||
: { busyTimeoutMs: command.options.busyTimeoutMs }),
|
||||
});
|
||||
let authenticated: Readonly<AuthenticatedLocalCommand> | undefined;
|
||||
try {
|
||||
try {
|
||||
authenticated = await adapters.authenticate(database, {
|
||||
deploymentRoot: command.options.deploymentRoot,
|
||||
databasePath: command.options.databasePath,
|
||||
ownerPepperKeyringDirectory:
|
||||
command.options.ownerPepperKeyringDirectory,
|
||||
credentialFilePath: command.options.credentialFilePath,
|
||||
authenticationNamespace: 'local_plugin_package_workflow',
|
||||
});
|
||||
await activateFence(database, authenticated);
|
||||
const service: LocalPluginPackageWorkflowAdministrationService =
|
||||
adapters.createService(
|
||||
database.projectPolicy,
|
||||
database.automationPublications,
|
||||
database.materializedRevisions,
|
||||
database.workflowAdministration,
|
||||
database.securityAudit,
|
||||
{ now: adapters.now },
|
||||
);
|
||||
if (command.operation === 'workflow.inspect') {
|
||||
const { failureAuditEventId: _failure, ...request } =
|
||||
command.request;
|
||||
const result = await service.inspect({
|
||||
...request,
|
||||
principal: authenticated.principal,
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
projectId: command.request.projectId,
|
||||
packageName: command.request.packageName,
|
||||
...result,
|
||||
});
|
||||
}
|
||||
if (command.operation === 'workflow.cancel') {
|
||||
const { failureAuditEventId: _failure, ...request } =
|
||||
command.request;
|
||||
const result = await service.cancel({
|
||||
...request,
|
||||
principal: authenticated.principal,
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
...result,
|
||||
});
|
||||
}
|
||||
if (command.operation === 'workflow.run.inspect') {
|
||||
const { failureAuditEventId: _failure, ...request } =
|
||||
command.request;
|
||||
const result = await service.inspectRun({
|
||||
...request,
|
||||
principal: authenticated.principal,
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
projectId: result.projectId,
|
||||
packageName: result.packageName,
|
||||
workflowId: result.workflowId,
|
||||
runId: result.runId,
|
||||
found: result.found,
|
||||
run: result.run,
|
||||
stepCount: result.stepCount,
|
||||
stepStatusCounts: result.stepStatusCounts,
|
||||
});
|
||||
}
|
||||
if (command.operation === 'workflow.run.list') {
|
||||
const { failureAuditEventId: _failure, ...request } =
|
||||
command.request;
|
||||
const result = await service.listRuns({
|
||||
...request,
|
||||
principal: authenticated.principal,
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
projectId: result.projectId,
|
||||
packageName: result.packageName,
|
||||
workflowId: result.workflowId,
|
||||
after: result.after,
|
||||
runs: result.runs,
|
||||
truncated: result.truncated,
|
||||
next: result.next,
|
||||
});
|
||||
}
|
||||
if (command.operation === 'workflow.step.list') {
|
||||
const { failureAuditEventId: _failure, ...request } =
|
||||
command.request;
|
||||
const result = await service.listStepRuns({
|
||||
...request,
|
||||
principal: authenticated.principal,
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
projectId: result.projectId,
|
||||
packageName: result.packageName,
|
||||
workflowId: result.workflowId,
|
||||
runId: result.runId,
|
||||
found: result.found,
|
||||
stepRuns: result.stepRuns,
|
||||
truncated: result.truncated,
|
||||
next: result.next,
|
||||
});
|
||||
}
|
||||
if (command.operation === 'workflow.event.list') {
|
||||
const { failureAuditEventId: _failure, ...request } =
|
||||
command.request;
|
||||
const result = await service.listRunEvents({
|
||||
...request,
|
||||
principal: authenticated.principal,
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
projectId: result.projectId,
|
||||
packageName: result.packageName,
|
||||
workflowId: result.workflowId,
|
||||
runId: result.runId,
|
||||
found: result.found,
|
||||
afterSequence: result.afterSequence,
|
||||
headSequence: result.headSequence,
|
||||
events: result.events,
|
||||
truncated: result.truncated,
|
||||
nextAfterSequence: result.nextAfterSequence,
|
||||
});
|
||||
}
|
||||
const { failureAuditEventId: _failure, ...request } = command.request;
|
||||
const result = await service.start({
|
||||
...request,
|
||||
principal: authenticated.principal,
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
status: result.status,
|
||||
projectId: result.plan.target.projectId,
|
||||
packageName: result.plan.target.packageName,
|
||||
workflowId: result.plan.target.workflowId,
|
||||
runId: result.plan.runId,
|
||||
stepCount: result.plan.steps.length,
|
||||
admittedAtMs: result.receipt.admittedAtMs,
|
||||
});
|
||||
} catch (error) {
|
||||
const audit = failureAudit(
|
||||
command,
|
||||
authenticated,
|
||||
error,
|
||||
adapters.now(),
|
||||
);
|
||||
if (audit) await database.securityAudit.record(audit);
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
await database.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function runLocalPluginPackageWorkflowCommandFile(
|
||||
commandFilePath: string,
|
||||
): Promise<LocalPluginPackageWorkflowCommandResult> {
|
||||
return createLocalPluginPackageWorkflowCommandRunner().run(commandFilePath);
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export { createLocalPluginPackageWorkflowAdministrationService } from '@qinglong/local-admin/plugin-package-workflow-administration';
|
||||
export type { LocalPluginPackageWorkflowAdministrationService } from '@qinglong/local-admin/plugin-package-workflow-administration';
|
||||
export { establishAuthenticatedLocalCommand } from '@qinglong/local-owner-console/authenticated-command';
|
||||
export type { AuthenticatedLocalCommand } from '@qinglong/local-owner-console/authenticated-command';
|
||||
export { openLocalSqlitePluginPackageWorkflowAdministrationDatabase } from '@qinglong/local-sqlite/plugin-package-workflow-administration';
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
export {
|
||||
LocalPluginPackageWorkflowAdministrationAuthenticationError,
|
||||
LocalPluginPackageWorkflowAdministrationAuthorizationError,
|
||||
LocalPluginPackageWorkflowAdministrationConfigurationError,
|
||||
LocalPluginPackageWorkflowAdministrationNotFoundError,
|
||||
LocalPluginPackageWorkflowAdministrationUnavailableError,
|
||||
} from '@qinglong/local-admin/plugin-package-workflow-administration';
|
||||
export { AuthenticatedLocalCommandAuthenticationError } from '@qinglong/local-owner-console/authenticated-command';
|
||||
export type { AuthenticatedLocalCommand } from '@qinglong/local-owner-console/authenticated-command';
|
||||
export { LocalSqliteAuthenticatedManagementFenceError } from '@qinglong/local-sqlite/authenticated-management';
|
||||
export type { LocalSqliteAuthenticatedUserCredentialFence } from '@qinglong/local-sqlite/authenticated-management';
|
||||
export type { LocalSqlitePluginPackageWorkflowAdministrationDatabase } from '@qinglong/local-sqlite/plugin-package-workflow-administration';
|
||||
export {
|
||||
PluginPackageWorkflowAdministrationAuthorizationFenceConflictError,
|
||||
PluginPackageWorkflowAdministrationMutationConflictError,
|
||||
} from '@qinglong/runtime-core/plugin-package-workflow-administration';
|
||||
export {
|
||||
InvalidPluginPackageWorkflowExecutionPlanError,
|
||||
PluginPackageWorkflowAdmissionConflictError,
|
||||
PluginPackageWorkflowAdmissionNotAllowedError,
|
||||
} from '@qinglong/runtime-core/plugin-package-workflow-execution-plan';
|
||||
export type { SecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { runLocalPluginPackageCatalogCommandFile } from './pluginPackageCatalogCommand';
|
||||
|
||||
const USAGE =
|
||||
'Usage: ql3-package-catalog run --command-file /absolute/private-command.json';
|
||||
|
||||
async function main(argv: readonly string[]): Promise<void> {
|
||||
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
|
||||
process.stdout.write(`${USAGE}\n`);
|
||||
return;
|
||||
}
|
||||
const commandFilePath = argv[2];
|
||||
if (
|
||||
argv.length !== 3 ||
|
||||
argv[0] !== 'run' ||
|
||||
argv[1] !== '--command-file' ||
|
||||
commandFilePath === undefined
|
||||
) {
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
code: 'LOCAL_PLUGIN_PACKAGE_CATALOG_CLI_USAGE_INVALID',
|
||||
message: USAGE,
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 64;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result =
|
||||
await runLocalPluginPackageCatalogCommandFile(commandFilePath);
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
} catch (error) {
|
||||
const candidate = error as {
|
||||
readonly code?: unknown;
|
||||
readonly name?: unknown;
|
||||
};
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
code:
|
||||
typeof candidate.code === 'string'
|
||||
? candidate.code
|
||||
: 'LOCAL_PLUGIN_PACKAGE_CATALOG_CLI_FAILED',
|
||||
name:
|
||||
typeof candidate.name === 'string' ? candidate.name : 'Error',
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void main(process.argv.slice(2));
|
||||
@@ -0,0 +1,865 @@
|
||||
// Plugin Package owns recovery catalog publication and inspection commands.
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
collectLocalPluginPackageRecoveryCatalog,
|
||||
createLocalPluginPackagePublisherTrustRegistry,
|
||||
inspectLocalPluginPackageRecoveryCatalog,
|
||||
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
|
||||
publishLocalPluginPackageRecoveryCatalogEntry,
|
||||
type CollectLocalPluginPackageRecoveryCatalogOptions,
|
||||
type PublishLocalPluginPackageRecoveryCatalogOptions,
|
||||
} from '@qinglong/local-admin/package-recovery-catalog';
|
||||
import { assertLocalPluginPackagePublisherKeyPublicationAllowed } from '@qinglong/local-admin/package-publisher-trust';
|
||||
import {
|
||||
PrivateLocalCommandFileError,
|
||||
readPrivateLocalCommandFile,
|
||||
readPrivateLocalJsonFile,
|
||||
} from '@qinglong/local-command-file';
|
||||
import {
|
||||
establishAuthenticatedLocalCommand,
|
||||
type AuthenticatedLocalCommand,
|
||||
} from '@qinglong/local-owner-console/authenticated-command';
|
||||
import {
|
||||
openLocalSqliteAuthenticatedManagementDatabase,
|
||||
type LocalSqliteAuthenticatedManagementDatabase,
|
||||
type LocalSqliteProfile,
|
||||
} from '@qinglong/local-sqlite/authenticated-management';
|
||||
import { LocalSqlitePluginPackageInstallRepository } from '@qinglong/local-sqlite/plugin-package-install';
|
||||
|
||||
export const LOCAL_PLUGIN_PACKAGE_RECOVERY_PUBLICATION_SCHEMA =
|
||||
'qinglong/local-plugin-package-recovery-publication@v1' as const;
|
||||
|
||||
const MAX_PATH_BYTES = 4_096;
|
||||
const MAX_DESCRIPTOR_BYTES = 256 * 1024;
|
||||
const MAX_TRUST_BYTES = 256 * 1024;
|
||||
const UUID_V4_PATTERN =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||
const PROJECT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const PACKAGE_NAME_PATTERN =
|
||||
/^(?:@[a-z0-9][a-z0-9._-]{0,63}\/)?[a-z0-9][a-z0-9._-]{0,127}$/;
|
||||
const COLLECTION_LIMITS = Object.freeze({
|
||||
edge: 4,
|
||||
standalone: 16,
|
||||
} as const);
|
||||
|
||||
export interface LocalPluginPackageCatalogCommandOptions {
|
||||
readonly deploymentRoot: string;
|
||||
readonly databasePath: string;
|
||||
readonly profile: LocalSqliteProfile;
|
||||
readonly ownerPepperKeyringDirectory: string;
|
||||
readonly credentialFilePath: string;
|
||||
readonly catalogRoot: string;
|
||||
readonly bundleRoot: string;
|
||||
readonly trustRoot: string;
|
||||
readonly busyTimeoutMs?: number;
|
||||
}
|
||||
|
||||
interface MutationIdentity {
|
||||
readonly requestId: string;
|
||||
readonly auditEventId: string;
|
||||
readonly failureAuditEventId: string;
|
||||
}
|
||||
|
||||
export interface PublishLocalPluginPackageCatalogCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'plugin-package.catalog.publish';
|
||||
readonly options: LocalPluginPackageCatalogCommandOptions;
|
||||
readonly request: MutationIdentity & {
|
||||
readonly projectId: string;
|
||||
readonly packageName: string;
|
||||
readonly descriptorFilePath: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CollectLocalPluginPackageCatalogCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'plugin-package.catalog.collect';
|
||||
readonly options: LocalPluginPackageCatalogCommandOptions;
|
||||
readonly request: MutationIdentity & {
|
||||
readonly limit?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InspectLocalPluginPackageCatalogCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'plugin-package.catalog.inspect';
|
||||
readonly options: LocalPluginPackageCatalogCommandOptions;
|
||||
readonly request: Readonly<Record<never, never>>;
|
||||
}
|
||||
|
||||
export type LocalPluginPackageCatalogCommand =
|
||||
| PublishLocalPluginPackageCatalogCommand
|
||||
| CollectLocalPluginPackageCatalogCommand
|
||||
| InspectLocalPluginPackageCatalogCommand;
|
||||
|
||||
export type LocalPluginPackageCatalogCommandResult =
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'plugin-package.catalog.publish';
|
||||
status: 'published' | 'existing';
|
||||
lockDigest: string;
|
||||
artifactDigest: string;
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'plugin-package.catalog.collect';
|
||||
removedEntries: number;
|
||||
removedBundles: number;
|
||||
removedTransactions: number;
|
||||
remaining: boolean;
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'plugin-package.catalog.inspect';
|
||||
entryCount: number;
|
||||
bundleCount: number;
|
||||
unresolvedTransactions: number;
|
||||
currentEntries: number;
|
||||
staleEntries: number;
|
||||
}>;
|
||||
|
||||
export interface LocalPluginPackageCatalogCommandRunner {
|
||||
run(
|
||||
commandFilePath: string,
|
||||
): Promise<Readonly<LocalPluginPackageCatalogCommandResult>>;
|
||||
}
|
||||
|
||||
interface PublicationDescriptor {
|
||||
readonly schema: typeof LOCAL_PLUGIN_PACKAGE_RECOVERY_PUBLICATION_SCHEMA;
|
||||
readonly bundlePath: string;
|
||||
readonly manifest: PublishLocalPluginPackageRecoveryCatalogOptions['manifest'];
|
||||
readonly signature: PublishLocalPluginPackageRecoveryCatalogOptions['signature'];
|
||||
}
|
||||
|
||||
interface LocalPluginPackageCatalogCommandRunnerDependencies {
|
||||
readonly openDatabase: typeof openLocalSqliteAuthenticatedManagementDatabase;
|
||||
readonly authenticate: typeof establishAuthenticatedLocalCommand;
|
||||
readonly publish: typeof publishLocalPluginPackageRecoveryCatalogEntry;
|
||||
readonly inspect: typeof inspectLocalPluginPackageRecoveryCatalog;
|
||||
readonly collect: typeof collectLocalPluginPackageRecoveryCatalog;
|
||||
readonly now: () => number;
|
||||
}
|
||||
|
||||
export class LocalPluginPackageCatalogCommandConfigurationError extends TypeError {
|
||||
readonly code = 'LOCAL_PLUGIN_PACKAGE_CATALOG_COMMAND_CONFIGURATION_INVALID';
|
||||
|
||||
constructor(message: string, readonly cause?: unknown) {
|
||||
super(
|
||||
`Local Plugin Package catalog command configuration is invalid: ${message}`,
|
||||
);
|
||||
this.name = 'LocalPluginPackageCatalogCommandConfigurationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalPluginPackageCatalogCommandConflictError extends Error {
|
||||
readonly code = 'LOCAL_PLUGIN_PACKAGE_CATALOG_COMMAND_CONFLICT';
|
||||
|
||||
constructor(message: string) {
|
||||
super(
|
||||
`Local Plugin Package catalog command conflicts with durable state: ${message}`,
|
||||
);
|
||||
this.name = 'LocalPluginPackageCatalogCommandConflictError';
|
||||
}
|
||||
}
|
||||
|
||||
function exactObject(
|
||||
value: unknown,
|
||||
expectedKeys: readonly string[],
|
||||
label: string,
|
||||
): asserts value is Record<string, unknown> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
(Object.getPrototypeOf(value) !== Object.prototype &&
|
||||
Object.getPrototypeOf(value) !== null)
|
||||
) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
`${label} must be an object`,
|
||||
);
|
||||
}
|
||||
const descriptors = Object.getOwnPropertyDescriptors(value);
|
||||
const actual = Object.keys(value).sort();
|
||||
const expected = [...expectedKeys].sort();
|
||||
if (
|
||||
Object.values(descriptors).some(
|
||||
(descriptor) =>
|
||||
descriptor.get !== undefined ||
|
||||
descriptor.set !== undefined ||
|
||||
descriptor.enumerable !== true,
|
||||
) ||
|
||||
actual.length !== expected.length ||
|
||||
actual.some((key, index) => key !== expected[index])
|
||||
) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
`${label} shape is invalid`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function boundedPath(value: unknown, label: string): string {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
value.length === 0 ||
|
||||
Buffer.byteLength(value, 'utf8') > MAX_PATH_BYTES ||
|
||||
value.includes('\0') ||
|
||||
!path.isAbsolute(value) ||
|
||||
path.normalize(value) !== value ||
|
||||
path.parse(value).root === value
|
||||
) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
`${label} must be a normalized bounded absolute non-root path`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function descendant(root: string, candidate: string, label: string): void {
|
||||
const relative = path.relative(root, candidate);
|
||||
if (
|
||||
relative.length === 0 ||
|
||||
relative === '..' ||
|
||||
relative.startsWith(`..${path.sep}`) ||
|
||||
path.isAbsolute(relative)
|
||||
) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
`${label} must be a descendant of deploymentRoot`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function options(value: unknown): LocalPluginPackageCatalogCommandOptions {
|
||||
const hasBusyTimeout =
|
||||
!!value &&
|
||||
typeof value === 'object' &&
|
||||
!Array.isArray(value) &&
|
||||
Object.hasOwn(value, 'busyTimeoutMs');
|
||||
exactObject(
|
||||
value,
|
||||
[
|
||||
'bundleRoot',
|
||||
'catalogRoot',
|
||||
'credentialFilePath',
|
||||
'databasePath',
|
||||
'deploymentRoot',
|
||||
'ownerPepperKeyringDirectory',
|
||||
'profile',
|
||||
'trustRoot',
|
||||
...(hasBusyTimeout ? ['busyTimeoutMs'] : []),
|
||||
],
|
||||
'options',
|
||||
);
|
||||
const deploymentRoot = boundedPath(value.deploymentRoot, 'deploymentRoot');
|
||||
const result = {
|
||||
deploymentRoot,
|
||||
databasePath: boundedPath(value.databasePath, 'databasePath'),
|
||||
profile: value.profile,
|
||||
ownerPepperKeyringDirectory: boundedPath(
|
||||
value.ownerPepperKeyringDirectory,
|
||||
'ownerPepperKeyringDirectory',
|
||||
),
|
||||
credentialFilePath: boundedPath(
|
||||
value.credentialFilePath,
|
||||
'credentialFilePath',
|
||||
),
|
||||
catalogRoot: boundedPath(value.catalogRoot, 'catalogRoot'),
|
||||
bundleRoot: boundedPath(value.bundleRoot, 'bundleRoot'),
|
||||
trustRoot: boundedPath(value.trustRoot, 'trustRoot'),
|
||||
...(hasBusyTimeout ? { busyTimeoutMs: value.busyTimeoutMs as number } : {}),
|
||||
};
|
||||
if (result.profile !== 'edge' && result.profile !== 'standalone') {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'profile must be edge or standalone',
|
||||
);
|
||||
}
|
||||
if (
|
||||
hasBusyTimeout &&
|
||||
(!Number.isSafeInteger(result.busyTimeoutMs) ||
|
||||
(result.busyTimeoutMs as number) < 100 ||
|
||||
(result.busyTimeoutMs as number) > 30_000)
|
||||
) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'busyTimeoutMs is invalid',
|
||||
);
|
||||
}
|
||||
const authorityPaths = [
|
||||
result.databasePath,
|
||||
result.ownerPepperKeyringDirectory,
|
||||
result.credentialFilePath,
|
||||
result.catalogRoot,
|
||||
result.bundleRoot,
|
||||
result.trustRoot,
|
||||
];
|
||||
for (const [index, authorityPath] of authorityPaths.entries()) {
|
||||
descendant(deploymentRoot, authorityPath, `authority path ${index}`);
|
||||
}
|
||||
if (new Set(authorityPaths).size !== authorityPaths.length) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'authority paths must be distinct',
|
||||
);
|
||||
}
|
||||
return Object.freeze(result as LocalPluginPackageCatalogCommandOptions);
|
||||
}
|
||||
|
||||
function mutationIdentity(
|
||||
value: Record<string, unknown>,
|
||||
extraKeys: readonly string[],
|
||||
label: string,
|
||||
): asserts value is Record<string, unknown> & MutationIdentity {
|
||||
exactObject(
|
||||
value,
|
||||
['auditEventId', 'failureAuditEventId', 'requestId', ...extraKeys],
|
||||
label,
|
||||
);
|
||||
if (
|
||||
typeof value.requestId !== 'string' ||
|
||||
value.requestId.length < 1 ||
|
||||
value.requestId.length > 128 ||
|
||||
typeof value.auditEventId !== 'string' ||
|
||||
!UUID_V4_PATTERN.test(value.auditEventId) ||
|
||||
typeof value.failureAuditEventId !== 'string' ||
|
||||
!UUID_V4_PATTERN.test(value.failureAuditEventId) ||
|
||||
value.auditEventId === value.failureAuditEventId
|
||||
) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
`${label} identity is invalid`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCommand(
|
||||
value: unknown,
|
||||
): Readonly<LocalPluginPackageCatalogCommand> {
|
||||
exactObject(
|
||||
value,
|
||||
['operation', 'options', 'request', 'schemaVersion'],
|
||||
'command',
|
||||
);
|
||||
if (value.schemaVersion !== 1) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'schemaVersion is invalid',
|
||||
);
|
||||
}
|
||||
const commandOptions = options(value.options);
|
||||
if (value.operation === 'plugin-package.catalog.publish') {
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'auditEventId',
|
||||
'descriptorFilePath',
|
||||
'failureAuditEventId',
|
||||
'packageName',
|
||||
'projectId',
|
||||
'requestId',
|
||||
],
|
||||
'publication request',
|
||||
);
|
||||
mutationIdentity(
|
||||
value.request,
|
||||
['descriptorFilePath', 'packageName', 'projectId'],
|
||||
'publication request',
|
||||
);
|
||||
if (
|
||||
typeof value.request.projectId !== 'string' ||
|
||||
!PROJECT_ID_PATTERN.test(value.request.projectId) ||
|
||||
typeof value.request.packageName !== 'string' ||
|
||||
!PACKAGE_NAME_PATTERN.test(value.request.packageName)
|
||||
) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'publication package identity is invalid',
|
||||
);
|
||||
}
|
||||
const descriptorFilePath = boundedPath(
|
||||
value.request.descriptorFilePath,
|
||||
'descriptorFilePath',
|
||||
);
|
||||
descendant(
|
||||
commandOptions.deploymentRoot,
|
||||
descriptorFilePath,
|
||||
'descriptorFilePath',
|
||||
);
|
||||
if (
|
||||
new Set([
|
||||
commandOptions.databasePath,
|
||||
commandOptions.credentialFilePath,
|
||||
commandOptions.catalogRoot,
|
||||
commandOptions.bundleRoot,
|
||||
commandOptions.trustRoot,
|
||||
descriptorFilePath,
|
||||
]).size !== 6
|
||||
) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'publication authority paths must be distinct',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: value.operation,
|
||||
options: commandOptions,
|
||||
request: Object.freeze({
|
||||
...value.request,
|
||||
descriptorFilePath,
|
||||
}),
|
||||
} as PublishLocalPluginPackageCatalogCommand);
|
||||
}
|
||||
if (value.operation === 'plugin-package.catalog.collect') {
|
||||
const hasLimit =
|
||||
!!value.request &&
|
||||
typeof value.request === 'object' &&
|
||||
!Array.isArray(value.request) &&
|
||||
Object.hasOwn(value.request, 'limit');
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'auditEventId',
|
||||
'failureAuditEventId',
|
||||
'requestId',
|
||||
...(hasLimit ? ['limit'] : []),
|
||||
],
|
||||
'collection request',
|
||||
);
|
||||
mutationIdentity(
|
||||
value.request,
|
||||
hasLimit ? ['limit'] : [],
|
||||
'collection request',
|
||||
);
|
||||
const limit = value.request.limit;
|
||||
if (
|
||||
limit !== undefined &&
|
||||
(!Number.isSafeInteger(limit) ||
|
||||
(limit as number) < 1 ||
|
||||
(limit as number) > COLLECTION_LIMITS[commandOptions.profile])
|
||||
) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'collection limit is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: value.operation,
|
||||
options: commandOptions,
|
||||
request: Object.freeze({
|
||||
requestId: value.request.requestId,
|
||||
auditEventId: value.request.auditEventId,
|
||||
failureAuditEventId: value.request.failureAuditEventId,
|
||||
...(limit === undefined ? {} : { limit: limit as number }),
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (value.operation === 'plugin-package.catalog.inspect') {
|
||||
exactObject(value.request, [], 'inspection request');
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: value.operation,
|
||||
options: commandOptions,
|
||||
request: Object.freeze({}),
|
||||
});
|
||||
}
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'operation is invalid',
|
||||
);
|
||||
}
|
||||
|
||||
function readCommandFile(
|
||||
commandFilePath: string,
|
||||
): Readonly<LocalPluginPackageCatalogCommand> {
|
||||
try {
|
||||
return normalizeCommand(readPrivateLocalCommandFile(commandFilePath));
|
||||
} catch (error) {
|
||||
if (error instanceof LocalPluginPackageCatalogCommandConfigurationError) {
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof PrivateLocalCommandFileError) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'command file cannot be read',
|
||||
error,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function publicationDescriptor(
|
||||
filePath: string,
|
||||
deploymentRoot: string,
|
||||
): Readonly<PublicationDescriptor> {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = readPrivateLocalJsonFile(filePath, {
|
||||
maxBytes: MAX_DESCRIPTOR_BYTES,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'publication descriptor cannot be read',
|
||||
error,
|
||||
);
|
||||
}
|
||||
exactObject(
|
||||
parsed,
|
||||
['bundlePath', 'manifest', 'schema', 'signature'],
|
||||
'publication descriptor',
|
||||
);
|
||||
if (parsed.schema !== LOCAL_PLUGIN_PACKAGE_RECOVERY_PUBLICATION_SCHEMA) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'publication descriptor schema is invalid',
|
||||
);
|
||||
}
|
||||
const bundlePath = boundedPath(parsed.bundlePath, 'bundlePath');
|
||||
descendant(deploymentRoot, bundlePath, 'bundlePath');
|
||||
return Object.freeze({
|
||||
schema: LOCAL_PLUGIN_PACKAGE_RECOVERY_PUBLICATION_SCHEMA,
|
||||
bundlePath,
|
||||
manifest:
|
||||
parsed.manifest as PublishLocalPluginPackageRecoveryCatalogOptions['manifest'],
|
||||
signature:
|
||||
parsed.signature as PublishLocalPluginPackageRecoveryCatalogOptions['signature'],
|
||||
});
|
||||
}
|
||||
|
||||
function trustFile(
|
||||
filePath: string,
|
||||
): ReturnType<typeof createLocalPluginPackagePublisherTrustRegistry> {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = readPrivateLocalJsonFile(filePath, {
|
||||
maxBytes: MAX_TRUST_BYTES,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'publisher trust file cannot be read',
|
||||
error,
|
||||
);
|
||||
}
|
||||
if (
|
||||
!parsed ||
|
||||
typeof parsed !== 'object' ||
|
||||
Array.isArray(parsed) ||
|
||||
(parsed as { schema?: unknown }).schema !==
|
||||
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA
|
||||
) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'publisher trust file schema is invalid',
|
||||
);
|
||||
}
|
||||
return createLocalPluginPackagePublisherTrustRegistry(parsed);
|
||||
}
|
||||
|
||||
async function confirmOwner(
|
||||
database: LocalSqliteAuthenticatedManagementDatabase,
|
||||
authenticated: Readonly<AuthenticatedLocalCommand>,
|
||||
): Promise<void> {
|
||||
await authenticated.confirm();
|
||||
database.confirmUserCredentialFence(authenticated.databaseFence);
|
||||
database.confirmDefaultProjectOwnerFence(authenticated.databaseFence);
|
||||
}
|
||||
|
||||
type SecurityAudit = Parameters<
|
||||
LocalSqliteAuthenticatedManagementDatabase['securityAudit']['record']
|
||||
>[0];
|
||||
|
||||
function databaseTime(
|
||||
database: LocalSqliteAuthenticatedManagementDatabase,
|
||||
eventId: string,
|
||||
now: () => number,
|
||||
): number {
|
||||
const existing = database.authority.client
|
||||
.prepare(
|
||||
`SELECT "occurred_at_ms" AS "occurredAtMs"
|
||||
FROM "QingLong3SecurityAuditEvents"
|
||||
WHERE "event_id" = ?`,
|
||||
)
|
||||
.get(eventId) as { readonly occurredAtMs?: unknown } | undefined;
|
||||
const value = existing?.occurredAtMs ?? now();
|
||||
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'audit clock is invalid',
|
||||
);
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
async function recordAuthorized(
|
||||
command:
|
||||
| Readonly<PublishLocalPluginPackageCatalogCommand>
|
||||
| Readonly<CollectLocalPluginPackageCatalogCommand>,
|
||||
database: LocalSqliteAuthenticatedManagementDatabase,
|
||||
authenticated: Readonly<AuthenticatedLocalCommand>,
|
||||
projectId: string | null,
|
||||
now: () => number,
|
||||
): Promise<void> {
|
||||
const audit: SecurityAudit = Object.freeze({
|
||||
eventId: command.request.auditEventId,
|
||||
requestId: command.request.requestId,
|
||||
operationId:
|
||||
command.operation === 'plugin-package.catalog.publish'
|
||||
? 'plugin_package_catalog_publish'
|
||||
: 'plugin_package_catalog_collect',
|
||||
projectId,
|
||||
subject: authenticated.principal.subject,
|
||||
authenticationId: authenticated.principal.authenticationId,
|
||||
outcome: 'allowed',
|
||||
reasons: Object.freeze(['catalog_mutation_authorized']),
|
||||
fence: null,
|
||||
occurredAtMs: databaseTime(database, command.request.auditEventId, now),
|
||||
});
|
||||
await database.securityAudit.record(audit);
|
||||
}
|
||||
|
||||
async function staleLockDigests(
|
||||
repository: LocalSqlitePluginPackageInstallRepository,
|
||||
lockDigests: readonly string[],
|
||||
): Promise<readonly string[]> {
|
||||
const stale: string[] = [];
|
||||
for (const lockDigest of lockDigests) {
|
||||
const lock = await repository.findLock(lockDigest);
|
||||
if (!lock) {
|
||||
stale.push(lockDigest);
|
||||
continue;
|
||||
}
|
||||
const head = await repository.find(lock.projectId, lock.packageName);
|
||||
if (!head || head.lockDigest !== lockDigest) stale.push(lockDigest);
|
||||
}
|
||||
return Object.freeze(stale);
|
||||
}
|
||||
|
||||
async function execute(
|
||||
command: Readonly<LocalPluginPackageCatalogCommand>,
|
||||
database: LocalSqliteAuthenticatedManagementDatabase,
|
||||
authenticated: Readonly<AuthenticatedLocalCommand>,
|
||||
adapters: Readonly<LocalPluginPackageCatalogCommandRunnerDependencies>,
|
||||
): Promise<Readonly<LocalPluginPackageCatalogCommandResult>> {
|
||||
await confirmOwner(database, authenticated);
|
||||
const repository = new LocalSqlitePluginPackageInstallRepository(
|
||||
database.authority,
|
||||
);
|
||||
if (command.operation === 'plugin-package.catalog.publish') {
|
||||
const head = await repository.find(
|
||||
command.request.projectId,
|
||||
command.request.packageName,
|
||||
);
|
||||
if (!head || head.state === 'failed') {
|
||||
throw new LocalPluginPackageCatalogCommandConflictError(
|
||||
'the current install head is unavailable',
|
||||
);
|
||||
}
|
||||
const lock = await repository.findLock(head.lockDigest);
|
||||
if (
|
||||
!lock ||
|
||||
lock.projectId !== command.request.projectId ||
|
||||
lock.packageName !== command.request.packageName
|
||||
) {
|
||||
throw new LocalPluginPackageCatalogCommandConflictError(
|
||||
'the current PackageLock is unavailable',
|
||||
);
|
||||
}
|
||||
const descriptor = publicationDescriptor(
|
||||
command.request.descriptorFilePath,
|
||||
command.options.deploymentRoot,
|
||||
);
|
||||
if (
|
||||
descriptor.bundlePath === command.request.descriptorFilePath ||
|
||||
descriptor.bundlePath === command.options.catalogRoot ||
|
||||
descriptor.bundlePath === command.options.bundleRoot ||
|
||||
descriptor.bundlePath === command.options.trustRoot
|
||||
) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'publication descriptor authorities must be distinct',
|
||||
);
|
||||
}
|
||||
const trust = trustFile(
|
||||
path.join(command.options.trustRoot, 'current.json'),
|
||||
);
|
||||
const result = await adapters.publish({
|
||||
catalogRoot: command.options.catalogRoot,
|
||||
bundleRoot: command.options.bundleRoot,
|
||||
sourceBundlePath: descriptor.bundlePath,
|
||||
lock,
|
||||
manifest: descriptor.manifest,
|
||||
signature: descriptor.signature,
|
||||
trust,
|
||||
confirmPublicationAllowed() {
|
||||
assertLocalPluginPackagePublisherKeyPublicationAllowed({
|
||||
trustRoot: command.options.trustRoot,
|
||||
publisher: descriptor.signature.publisher,
|
||||
keyId: descriptor.signature.keyId,
|
||||
});
|
||||
},
|
||||
async beforePublish() {
|
||||
await confirmOwner(database, authenticated);
|
||||
await recordAuthorized(
|
||||
command,
|
||||
database,
|
||||
authenticated,
|
||||
lock.projectId,
|
||||
adapters.now,
|
||||
);
|
||||
await confirmOwner(database, authenticated);
|
||||
},
|
||||
} satisfies PublishLocalPluginPackageRecoveryCatalogOptions);
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: command.operation,
|
||||
status: result.status,
|
||||
lockDigest: result.lockDigest,
|
||||
artifactDigest: result.artifactDigest,
|
||||
});
|
||||
}
|
||||
|
||||
const inspection = adapters.inspect({
|
||||
catalogRoot: command.options.catalogRoot,
|
||||
bundleRoot: command.options.bundleRoot,
|
||||
});
|
||||
const stale = await staleLockDigests(repository, inspection.lockDigests);
|
||||
if (command.operation === 'plugin-package.catalog.inspect') {
|
||||
await confirmOwner(database, authenticated);
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: command.operation,
|
||||
entryCount: inspection.entryCount,
|
||||
bundleCount: inspection.bundleCount,
|
||||
unresolvedTransactions: inspection.unresolvedTransactions,
|
||||
currentEntries: inspection.entryCount - stale.length,
|
||||
staleEntries: stale.length,
|
||||
});
|
||||
}
|
||||
const result = await adapters.collect({
|
||||
catalogRoot: command.options.catalogRoot,
|
||||
bundleRoot: command.options.bundleRoot,
|
||||
candidateLockDigests: stale,
|
||||
maxDeletes:
|
||||
command.request.limit ?? COLLECTION_LIMITS[command.options.profile],
|
||||
async beforeDelete() {
|
||||
await confirmOwner(database, authenticated);
|
||||
await recordAuthorized(
|
||||
command,
|
||||
database,
|
||||
authenticated,
|
||||
null,
|
||||
adapters.now,
|
||||
);
|
||||
await confirmOwner(database, authenticated);
|
||||
},
|
||||
} satisfies CollectLocalPluginPackageRecoveryCatalogOptions);
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: command.operation,
|
||||
...result,
|
||||
});
|
||||
}
|
||||
|
||||
function failureAudit(
|
||||
command: Readonly<LocalPluginPackageCatalogCommand>,
|
||||
database: LocalSqliteAuthenticatedManagementDatabase,
|
||||
authenticated: Readonly<AuthenticatedLocalCommand> | undefined,
|
||||
now: () => number,
|
||||
): Readonly<SecurityAudit> | null {
|
||||
if (command.operation === 'plugin-package.catalog.inspect') return null;
|
||||
return Object.freeze({
|
||||
eventId: command.request.failureAuditEventId,
|
||||
requestId: command.request.requestId,
|
||||
operationId:
|
||||
command.operation === 'plugin-package.catalog.publish'
|
||||
? 'plugin_package_catalog_publish'
|
||||
: 'plugin_package_catalog_collect',
|
||||
projectId:
|
||||
command.operation === 'plugin-package.catalog.publish'
|
||||
? command.request.projectId
|
||||
: null,
|
||||
subject: authenticated?.principal.subject ?? null,
|
||||
authenticationId: authenticated?.principal.authenticationId ?? null,
|
||||
outcome: authenticated
|
||||
? 'authorization_unavailable'
|
||||
: 'authentication_rejected',
|
||||
reasons: Object.freeze([
|
||||
authenticated ? 'catalog_mutation_failed' : 'credential_rejected',
|
||||
]),
|
||||
fence: null,
|
||||
occurredAtMs: databaseTime(
|
||||
database,
|
||||
command.request.failureAuditEventId,
|
||||
now,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function dependencies(
|
||||
value: LocalPluginPackageCatalogCommandRunnerDependencies,
|
||||
): Readonly<LocalPluginPackageCatalogCommandRunnerDependencies> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.keys(value).sort().join('\0') !==
|
||||
['authenticate', 'collect', 'inspect', 'now', 'openDatabase', 'publish']
|
||||
.sort()
|
||||
.join('\0') ||
|
||||
typeof value.openDatabase !== 'function' ||
|
||||
typeof value.authenticate !== 'function' ||
|
||||
typeof value.publish !== 'function' ||
|
||||
typeof value.inspect !== 'function' ||
|
||||
typeof value.collect !== 'function' ||
|
||||
typeof value.now !== 'function'
|
||||
) {
|
||||
throw new LocalPluginPackageCatalogCommandConfigurationError(
|
||||
'runner dependencies are invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({ ...value });
|
||||
}
|
||||
|
||||
export function createLocalPluginPackageCatalogCommandRunner(
|
||||
candidateDependencies: LocalPluginPackageCatalogCommandRunnerDependencies = {
|
||||
openDatabase: openLocalSqliteAuthenticatedManagementDatabase,
|
||||
authenticate: establishAuthenticatedLocalCommand,
|
||||
publish: publishLocalPluginPackageRecoveryCatalogEntry,
|
||||
inspect: inspectLocalPluginPackageRecoveryCatalog,
|
||||
collect: collectLocalPluginPackageRecoveryCatalog,
|
||||
now: Date.now,
|
||||
},
|
||||
): LocalPluginPackageCatalogCommandRunner {
|
||||
const adapters = dependencies(candidateDependencies);
|
||||
return Object.freeze({
|
||||
async run(commandFilePath: string) {
|
||||
const command = readCommandFile(commandFilePath);
|
||||
const database = await adapters.openDatabase({
|
||||
databasePath: command.options.databasePath,
|
||||
profile: command.options.profile,
|
||||
...(command.options.busyTimeoutMs === undefined
|
||||
? {}
|
||||
: { busyTimeoutMs: command.options.busyTimeoutMs }),
|
||||
});
|
||||
let authenticated: Readonly<AuthenticatedLocalCommand> | undefined;
|
||||
try {
|
||||
try {
|
||||
authenticated = await adapters.authenticate(database, {
|
||||
deploymentRoot: command.options.deploymentRoot,
|
||||
databasePath: command.options.databasePath,
|
||||
ownerPepperKeyringDirectory:
|
||||
command.options.ownerPepperKeyringDirectory,
|
||||
credentialFilePath: command.options.credentialFilePath,
|
||||
authenticationNamespace: 'local_package_catalog',
|
||||
});
|
||||
return await execute(command, database, authenticated, adapters);
|
||||
} catch (error) {
|
||||
const audit = failureAudit(
|
||||
command,
|
||||
database,
|
||||
authenticated,
|
||||
adapters.now,
|
||||
);
|
||||
if (audit) await database.securityAudit.record(audit);
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
await database.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function runLocalPluginPackageCatalogCommandFile(
|
||||
commandFilePath: string,
|
||||
): Promise<Readonly<LocalPluginPackageCatalogCommandResult>> {
|
||||
return createLocalPluginPackageCatalogCommandRunner().run(commandFilePath);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { runLocalPluginPackageCommandFile } from './pluginPackageCommand';
|
||||
|
||||
const USAGE =
|
||||
'Usage: ql3-package run --command-file /absolute/private-command.json';
|
||||
|
||||
async function main(argv: readonly string[]): Promise<void> {
|
||||
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
|
||||
process.stdout.write(`${USAGE}\n`);
|
||||
return;
|
||||
}
|
||||
const commandFilePath = argv[2];
|
||||
if (
|
||||
argv.length !== 3 ||
|
||||
argv[0] !== 'run' ||
|
||||
argv[1] !== '--command-file' ||
|
||||
commandFilePath === undefined
|
||||
) {
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
code: 'LOCAL_PLUGIN_PACKAGE_CLI_USAGE_INVALID',
|
||||
message: USAGE,
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 64;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await runLocalPluginPackageCommandFile(commandFilePath);
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
} catch (error) {
|
||||
const candidate = error as {
|
||||
readonly code?: unknown;
|
||||
readonly name?: unknown;
|
||||
readonly message?: unknown;
|
||||
};
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
code:
|
||||
typeof candidate.code === 'string'
|
||||
? candidate.code
|
||||
: 'LOCAL_PLUGIN_PACKAGE_CLI_FAILED',
|
||||
name: typeof candidate.name === 'string' ? candidate.name : 'Error',
|
||||
message:
|
||||
typeof candidate.message === 'string'
|
||||
? candidate.message
|
||||
: 'Local Plugin Package command failed',
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void main(process.argv.slice(2));
|
||||
@@ -0,0 +1,982 @@
|
||||
// Plugin Package owns its authenticated lifecycle command surface.
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
PrivateLocalCommandFileError,
|
||||
readPrivateLocalCommandFile,
|
||||
} from '@qinglong/local-command-file';
|
||||
import {
|
||||
establishAuthenticatedLocalCommand,
|
||||
type AuthenticatedLocalCommand,
|
||||
} from '@qinglong/local-owner-console/authenticated-command';
|
||||
import {
|
||||
openLocalSqlitePluginPackageManagementDatabase,
|
||||
type LocalSqlitePluginPackageManagementDatabase,
|
||||
type LocalSqliteProfile,
|
||||
} from '@qinglong/local-sqlite/package-management';
|
||||
import type {
|
||||
ApprovalRequestRecord,
|
||||
ApprovedActionDispatchRecord,
|
||||
} from '@qinglong/runtime-core/approved-action';
|
||||
import type { ApprovedActionDispatchBatchSummary } from '@qinglong/runtime-core/approved-action-dispatcher';
|
||||
import {
|
||||
normalizePluginPackageLifecycleImpact,
|
||||
type PluginPackageLifecycleAction,
|
||||
type PluginPackageLifecycleImpact,
|
||||
type PluginPackageLifecycleReceipt,
|
||||
} from '@qinglong/runtime-core/plugin-package-lifecycle';
|
||||
import {
|
||||
pluginPackageInstallRecoveryAction,
|
||||
type PluginPackageInstallActionInput,
|
||||
type PluginPackageInstallInventoryItem,
|
||||
} from '@qinglong/runtime-core/plugin-package-install';
|
||||
import type { PluginPackageInstallProposal } from '@qinglong/runtime-core/plugin-package-proposal';
|
||||
|
||||
import { createLocalPluginPackageManagementService } from '@qinglong/local-admin/package-management';
|
||||
import { createLocalPluginPackageLifecycleService } from '@qinglong/local-admin/package-lifecycle';
|
||||
|
||||
const MAX_PATH_BYTES = 4096;
|
||||
const MAX_DISPATCH_LIMIT = 64;
|
||||
const LOCAL_INSTALLATION_INVENTORY_LIMITS = Object.freeze({
|
||||
edge: 16,
|
||||
standalone: 64,
|
||||
});
|
||||
const LOCAL_INSTALLATION_INVENTORY_DEFAULTS = Object.freeze({
|
||||
edge: 8,
|
||||
standalone: 32,
|
||||
});
|
||||
const PROJECT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const PACKAGE_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
||||
const COMMAND_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const REASON_CODE_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
|
||||
const LOCAL_PACKAGE_CONSUMER = Object.freeze({
|
||||
subject: Object.freeze({
|
||||
type: 'system' as const,
|
||||
id: 'local_plugin_package_consumer',
|
||||
}),
|
||||
authenticationId: 'local_plugin_package_consumer_v1',
|
||||
});
|
||||
|
||||
export interface LocalPluginPackageCommandOptions {
|
||||
readonly deploymentRoot: string;
|
||||
readonly databasePath: string;
|
||||
readonly profile: LocalSqliteProfile;
|
||||
readonly ownerPepperKeyringDirectory: string;
|
||||
readonly credentialFilePath: string;
|
||||
readonly busyTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface ProposeLocalPluginPackageCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'plugin-package.propose';
|
||||
readonly options: LocalPluginPackageCommandOptions;
|
||||
readonly request: {
|
||||
readonly actionRef: string;
|
||||
readonly approvalRequestId: string;
|
||||
readonly proposalAuditEventId: string;
|
||||
readonly approvalAuditEventId: string;
|
||||
readonly actionInput: PluginPackageInstallActionInput;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DecideLocalPluginPackageCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'plugin-package.decide';
|
||||
readonly options: LocalPluginPackageCommandOptions;
|
||||
readonly request: {
|
||||
readonly actionRef: string;
|
||||
readonly approvalRequestId: string;
|
||||
readonly expectedVersion: number;
|
||||
readonly decisionId: string;
|
||||
readonly auditEventId: string;
|
||||
readonly decision: 'approved' | 'rejected';
|
||||
readonly reasonCode: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ConsumeLocalPluginPackageCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'plugin-package.consume';
|
||||
readonly options: LocalPluginPackageCommandOptions;
|
||||
readonly request: {
|
||||
readonly actionRef: string;
|
||||
readonly approvalRequestId: string;
|
||||
readonly expectedVersion: number;
|
||||
readonly consumptionId: string;
|
||||
readonly dispatchId: string;
|
||||
readonly auditEventId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InspectLocalPluginPackageCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'plugin-package.inspect';
|
||||
readonly options: LocalPluginPackageCommandOptions;
|
||||
readonly request: {
|
||||
readonly actionRef: string;
|
||||
readonly approvalRequestId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InspectLocalPluginPackageInstallationCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'plugin-package.installation.inspect';
|
||||
readonly options: LocalPluginPackageCommandOptions;
|
||||
readonly request: {
|
||||
readonly projectId: string;
|
||||
readonly packageName: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ListLocalPluginPackageInstallationsCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'plugin-package.installation.list';
|
||||
readonly options: LocalPluginPackageCommandOptions;
|
||||
readonly request: {
|
||||
readonly projectId: string;
|
||||
readonly limit?: number;
|
||||
readonly after?: Readonly<{ packageName: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DispatchLocalPluginPackageCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'plugin-package.dispatch';
|
||||
readonly options: LocalPluginPackageCommandOptions;
|
||||
readonly request: {
|
||||
readonly limit?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PlanLocalPluginPackageLifecycleCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'plugin-package.lifecycle.plan';
|
||||
readonly options: LocalPluginPackageCommandOptions;
|
||||
readonly request: {
|
||||
readonly action: PluginPackageLifecycleAction;
|
||||
readonly projectId: string;
|
||||
readonly packageName: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExecuteLocalPluginPackageLifecycleCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'plugin-package.lifecycle.execute';
|
||||
readonly options: LocalPluginPackageCommandOptions;
|
||||
readonly request: {
|
||||
readonly impact: PluginPackageLifecycleImpact;
|
||||
readonly approvalRequestId: string;
|
||||
readonly decisionId: string;
|
||||
readonly consumptionId: string;
|
||||
readonly dispatchId: string;
|
||||
readonly approvalAuditEventId: string;
|
||||
readonly decisionAuditEventId: string;
|
||||
readonly consumptionAuditEventId: string;
|
||||
readonly reasonCode: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type LocalPluginPackageCommand =
|
||||
| ProposeLocalPluginPackageCommand
|
||||
| DecideLocalPluginPackageCommand
|
||||
| ConsumeLocalPluginPackageCommand
|
||||
| InspectLocalPluginPackageCommand
|
||||
| InspectLocalPluginPackageInstallationCommand
|
||||
| ListLocalPluginPackageInstallationsCommand
|
||||
| DispatchLocalPluginPackageCommand
|
||||
| PlanLocalPluginPackageLifecycleCommand
|
||||
| ExecuteLocalPluginPackageLifecycleCommand;
|
||||
|
||||
export interface LocalPluginPackageCommandRunner {
|
||||
run(
|
||||
commandFilePath: string,
|
||||
): Promise<Readonly<LocalPluginPackageCommandResult>>;
|
||||
}
|
||||
|
||||
export type LocalPluginPackageCommandResult =
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'plugin-package.propose';
|
||||
proposalStatus: 'created' | 'existing';
|
||||
approvalStatus: 'created' | 'existing';
|
||||
proposal: ReturnType<typeof proposalSummary>;
|
||||
approval: ReturnType<typeof approvalSummary>;
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'plugin-package.decide';
|
||||
status: 'decided' | 'existing';
|
||||
approval: ReturnType<typeof approvalSummary>;
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'plugin-package.consume';
|
||||
status: 'consumed' | 'existing';
|
||||
approval: ReturnType<typeof approvalSummary>;
|
||||
dispatch: ReturnType<typeof dispatchSummary>;
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'plugin-package.inspect';
|
||||
proposal: ReturnType<typeof proposalSummary> | null;
|
||||
approval: ReturnType<typeof approvalSummary> | null;
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'plugin-package.installation.inspect';
|
||||
installation: ReturnType<typeof installationSummary> | null;
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'plugin-package.installation.list';
|
||||
installations: readonly ReturnType<typeof installationSummary>[];
|
||||
truncated: boolean;
|
||||
next: Readonly<{ packageName: string }> | null;
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'plugin-package.dispatch';
|
||||
summary: Readonly<ApprovedActionDispatchBatchSummary>;
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'plugin-package.lifecycle.plan';
|
||||
impact: Readonly<PluginPackageLifecycleImpact>;
|
||||
summary: ReturnType<typeof lifecycleImpactSummary>;
|
||||
}>
|
||||
| Readonly<{
|
||||
schemaVersion: 1;
|
||||
operation: 'plugin-package.lifecycle.execute';
|
||||
status: 'created' | 'existing';
|
||||
approval: ReturnType<typeof approvalSummary>;
|
||||
receipt: ReturnType<typeof lifecycleReceiptSummary>;
|
||||
}>;
|
||||
|
||||
export class LocalPluginPackageCommandConfigurationError extends TypeError {
|
||||
readonly code = 'LOCAL_PLUGIN_PACKAGE_COMMAND_CONFIGURATION_INVALID';
|
||||
|
||||
constructor(message: string, readonly cause?: unknown) {
|
||||
super(`Local Plugin Package command configuration is invalid: ${message}`);
|
||||
this.name = 'LocalPluginPackageCommandConfigurationError';
|
||||
}
|
||||
}
|
||||
|
||||
interface LocalPluginPackageCommandRunnerDependencies {
|
||||
readonly openDatabase: typeof openLocalSqlitePluginPackageManagementDatabase;
|
||||
readonly authenticate: typeof establishAuthenticatedLocalCommand;
|
||||
}
|
||||
|
||||
function exactObject(
|
||||
value: unknown,
|
||||
expectedKeys: readonly string[],
|
||||
label: string,
|
||||
): asserts value is Record<string, unknown> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
`${label} must be an object`,
|
||||
);
|
||||
}
|
||||
const actual = Object.keys(value).sort();
|
||||
const expected = [...expectedKeys].sort();
|
||||
if (
|
||||
actual.length !== expected.length ||
|
||||
actual.some((key, index) => key !== expected[index])
|
||||
) {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
`${label} shape is invalid`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function boundedPath(value: unknown, label: string): string {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
!path.isAbsolute(value) ||
|
||||
path.parse(value).root === value ||
|
||||
path.normalize(value) !== value ||
|
||||
value.includes('\0') ||
|
||||
Buffer.byteLength(value, 'utf8') > MAX_PATH_BYTES
|
||||
) {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
`${label} must be a normalized bounded absolute non-root path`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function options(value: unknown): LocalPluginPackageCommandOptions {
|
||||
const hasBusyTimeout =
|
||||
!!value &&
|
||||
typeof value === 'object' &&
|
||||
!Array.isArray(value) &&
|
||||
Object.hasOwn(value, 'busyTimeoutMs');
|
||||
exactObject(
|
||||
value,
|
||||
[
|
||||
'deploymentRoot',
|
||||
'databasePath',
|
||||
'profile',
|
||||
'ownerPepperKeyringDirectory',
|
||||
'credentialFilePath',
|
||||
...(hasBusyTimeout ? ['busyTimeoutMs'] : []),
|
||||
],
|
||||
'options',
|
||||
);
|
||||
for (const key of [
|
||||
'deploymentRoot',
|
||||
'databasePath',
|
||||
'ownerPepperKeyringDirectory',
|
||||
'credentialFilePath',
|
||||
] as const) {
|
||||
boundedPath(value[key], key);
|
||||
}
|
||||
if (value.profile !== 'edge' && value.profile !== 'standalone') {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
'profile must be edge or standalone',
|
||||
);
|
||||
}
|
||||
if (
|
||||
value.busyTimeoutMs !== undefined &&
|
||||
(!Number.isSafeInteger(value.busyTimeoutMs) ||
|
||||
(value.busyTimeoutMs as number) < 100 ||
|
||||
(value.busyTimeoutMs as number) > 30_000)
|
||||
) {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
'busyTimeoutMs is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze(value as unknown as LocalPluginPackageCommandOptions);
|
||||
}
|
||||
|
||||
function normalizeCommand(value: unknown): Readonly<LocalPluginPackageCommand> {
|
||||
exactObject(
|
||||
value,
|
||||
['schemaVersion', 'operation', 'options', 'request'],
|
||||
'command',
|
||||
);
|
||||
if (value.schemaVersion !== 1) {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
'schemaVersion is invalid',
|
||||
);
|
||||
}
|
||||
const commandOptions = options(value.options);
|
||||
switch (value.operation) {
|
||||
case 'plugin-package.propose':
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'actionRef',
|
||||
'approvalRequestId',
|
||||
'proposalAuditEventId',
|
||||
'approvalAuditEventId',
|
||||
'actionInput',
|
||||
],
|
||||
'proposal request',
|
||||
);
|
||||
break;
|
||||
case 'plugin-package.decide':
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'actionRef',
|
||||
'approvalRequestId',
|
||||
'expectedVersion',
|
||||
'decisionId',
|
||||
'auditEventId',
|
||||
'decision',
|
||||
'reasonCode',
|
||||
],
|
||||
'decision request',
|
||||
);
|
||||
break;
|
||||
case 'plugin-package.consume':
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'actionRef',
|
||||
'approvalRequestId',
|
||||
'expectedVersion',
|
||||
'consumptionId',
|
||||
'dispatchId',
|
||||
'auditEventId',
|
||||
],
|
||||
'consumption request',
|
||||
);
|
||||
break;
|
||||
case 'plugin-package.inspect':
|
||||
exactObject(
|
||||
value.request,
|
||||
['actionRef', 'approvalRequestId'],
|
||||
'inspection request',
|
||||
);
|
||||
break;
|
||||
case 'plugin-package.installation.inspect':
|
||||
exactObject(
|
||||
value.request,
|
||||
['packageName', 'projectId'],
|
||||
'installation inspection request',
|
||||
);
|
||||
if (
|
||||
typeof value.request.projectId !== 'string' ||
|
||||
!PROJECT_ID_PATTERN.test(value.request.projectId) ||
|
||||
typeof value.request.packageName !== 'string' ||
|
||||
!PACKAGE_NAME_PATTERN.test(value.request.packageName)
|
||||
) {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
'installation inspection identity is invalid',
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'plugin-package.installation.list': {
|
||||
const request =
|
||||
value.request && typeof value.request === 'object'
|
||||
? (value.request as Record<string, unknown>)
|
||||
: {};
|
||||
const hasLimit = Object.hasOwn(request, 'limit');
|
||||
const hasAfter = Object.hasOwn(request, 'after');
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'projectId',
|
||||
...(hasLimit ? ['limit'] : []),
|
||||
...(hasAfter ? ['after'] : []),
|
||||
],
|
||||
'installation list request',
|
||||
);
|
||||
if (
|
||||
typeof request.projectId !== 'string' ||
|
||||
!PROJECT_ID_PATTERN.test(request.projectId) ||
|
||||
(request.limit !== undefined &&
|
||||
(!Number.isSafeInteger(request.limit) ||
|
||||
(request.limit as number) < 1 ||
|
||||
(request.limit as number) >
|
||||
LOCAL_INSTALLATION_INVENTORY_LIMITS[commandOptions.profile]))
|
||||
) {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
'installation list request is invalid',
|
||||
);
|
||||
}
|
||||
if (request.after !== undefined) {
|
||||
exactObject(request.after, ['packageName'], 'installation list cursor');
|
||||
if (
|
||||
typeof request.after.packageName !== 'string' ||
|
||||
!PACKAGE_NAME_PATTERN.test(request.after.packageName)
|
||||
) {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
'installation list cursor is invalid',
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'plugin-package.dispatch': {
|
||||
const hasLimit =
|
||||
!!value.request &&
|
||||
typeof value.request === 'object' &&
|
||||
!Array.isArray(value.request) &&
|
||||
Object.hasOwn(value.request, 'limit');
|
||||
exactObject(value.request, hasLimit ? ['limit'] : [], 'dispatch request');
|
||||
if (
|
||||
(value.request as { limit?: unknown }).limit !== undefined &&
|
||||
(!Number.isSafeInteger((value.request as { limit?: unknown }).limit) ||
|
||||
((value.request as { limit?: number }).limit as number) < 1 ||
|
||||
((value.request as { limit?: number }).limit as number) >
|
||||
MAX_DISPATCH_LIMIT)
|
||||
) {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
'dispatch limit is invalid',
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'plugin-package.lifecycle.plan':
|
||||
exactObject(
|
||||
value.request,
|
||||
['action', 'packageName', 'projectId'],
|
||||
'lifecycle plan request',
|
||||
);
|
||||
if (
|
||||
(value.request.action !== 'disable' &&
|
||||
value.request.action !== 'enable' &&
|
||||
value.request.action !== 'uninstall') ||
|
||||
typeof value.request.projectId !== 'string' ||
|
||||
!PROJECT_ID_PATTERN.test(value.request.projectId) ||
|
||||
typeof value.request.packageName !== 'string' ||
|
||||
!PACKAGE_NAME_PATTERN.test(value.request.packageName)
|
||||
) {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
'lifecycle plan request is invalid',
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'plugin-package.lifecycle.execute':
|
||||
exactObject(
|
||||
value.request,
|
||||
[
|
||||
'approvalAuditEventId',
|
||||
'approvalRequestId',
|
||||
'consumptionAuditEventId',
|
||||
'consumptionId',
|
||||
'decisionAuditEventId',
|
||||
'decisionId',
|
||||
'dispatchId',
|
||||
'impact',
|
||||
'reasonCode',
|
||||
],
|
||||
'lifecycle execution request',
|
||||
);
|
||||
try {
|
||||
normalizePluginPackageLifecycleImpact(
|
||||
value.request.impact as PluginPackageLifecycleImpact,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
'lifecycle impact is invalid',
|
||||
error,
|
||||
);
|
||||
}
|
||||
if (
|
||||
[
|
||||
value.request.approvalRequestId,
|
||||
value.request.decisionId,
|
||||
value.request.consumptionId,
|
||||
value.request.dispatchId,
|
||||
value.request.approvalAuditEventId,
|
||||
value.request.decisionAuditEventId,
|
||||
value.request.consumptionAuditEventId,
|
||||
].some(
|
||||
(candidate) =>
|
||||
typeof candidate !== 'string' ||
|
||||
!COMMAND_IDENTIFIER_PATTERN.test(candidate),
|
||||
) ||
|
||||
typeof value.request.reasonCode !== 'string' ||
|
||||
!REASON_CODE_PATTERN.test(value.request.reasonCode)
|
||||
) {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
'lifecycle execution identity is invalid',
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
'operation is invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
...value,
|
||||
options: commandOptions,
|
||||
} as unknown as LocalPluginPackageCommand);
|
||||
}
|
||||
|
||||
function readCommandFile(
|
||||
commandFilePath: string,
|
||||
): Readonly<LocalPluginPackageCommand> {
|
||||
try {
|
||||
return normalizeCommand(readPrivateLocalCommandFile(commandFilePath));
|
||||
} catch (error) {
|
||||
if (error instanceof LocalPluginPackageCommandConfigurationError) {
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof PrivateLocalCommandFileError) {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
'command file cannot be read',
|
||||
error,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function proposalSummary(proposal: Readonly<PluginPackageInstallProposal>) {
|
||||
return Object.freeze({
|
||||
actionRef: proposal.actionRef,
|
||||
projectId: proposal.projectId,
|
||||
packageName: proposal.actionInput.manifest.metadata.name,
|
||||
packageVersion: proposal.actionInput.manifest.metadata.version,
|
||||
operation: proposal.actionInput.plan.operation,
|
||||
sourceKind: proposal.actionInput.source.kind,
|
||||
architecture: proposal.actionInput.architecture,
|
||||
deploymentProfile: proposal.actionInput.deploymentProfile,
|
||||
targetGeneration: proposal.actionInput.targetGeneration,
|
||||
actionDigest: proposal.actionDigest,
|
||||
previewDigest: proposal.previewDigest,
|
||||
proposalDigest: proposal.proposalDigest,
|
||||
createdAtMs: proposal.createdAtMs,
|
||||
});
|
||||
}
|
||||
|
||||
function approvalSummary(approval: Readonly<ApprovalRequestRecord>) {
|
||||
return Object.freeze({
|
||||
id: approval.id,
|
||||
projectId: approval.projectId,
|
||||
version: approval.version,
|
||||
state: approval.state,
|
||||
risk: approval.risk,
|
||||
decisionMode: approval.decisionMode,
|
||||
requestedAtMs: approval.requestedAtMs,
|
||||
expiresAtMs: approval.expiresAtMs,
|
||||
decision: approval.decision,
|
||||
decisionReasonCode: approval.decisionReasonCode,
|
||||
decidedAtMs: approval.decidedAtMs,
|
||||
dispatchId: approval.dispatchId,
|
||||
consumedAtMs: approval.consumedAtMs,
|
||||
actionDigest: approval.action.actionDigest,
|
||||
previewDigest: approval.action.previewDigest,
|
||||
});
|
||||
}
|
||||
|
||||
function dispatchSummary(dispatch: Readonly<ApprovedActionDispatchRecord>) {
|
||||
return Object.freeze({
|
||||
id: dispatch.id,
|
||||
approvalRequestId: dispatch.approvalRequestId,
|
||||
projectId: dispatch.projectId,
|
||||
actionRef: dispatch.action.actionRef,
|
||||
actionDigest: dispatch.action.actionDigest,
|
||||
createdAtMs: dispatch.createdAtMs,
|
||||
expiresAtMs: dispatch.expiresAtMs,
|
||||
});
|
||||
}
|
||||
|
||||
function installationSummary(
|
||||
item: Readonly<PluginPackageInstallInventoryItem>,
|
||||
) {
|
||||
const { record, quarantine } = item;
|
||||
return Object.freeze({
|
||||
installationId: record.installationId,
|
||||
projectId: record.projectId,
|
||||
packageName: record.packageName,
|
||||
packageVersion: record.packageVersion,
|
||||
operation: record.operation,
|
||||
state: record.state,
|
||||
targetGeneration: record.targetGeneration,
|
||||
activeLockDigest: record.activeLockDigest,
|
||||
previousActiveLockDigest: record.previousActiveLockDigest,
|
||||
recoveryAction: pluginPackageInstallRecoveryAction(record),
|
||||
availability: quarantine
|
||||
? ('quarantined' as const)
|
||||
: record.state === 'active'
|
||||
? ('active' as const)
|
||||
: ('not_active' as const),
|
||||
quarantineReason: quarantine?.reasonCode ?? null,
|
||||
quarantineAuthorizationMode: quarantine?.authorizationMode ?? null,
|
||||
quarantineEventDigest: quarantine?.eventDigest ?? null,
|
||||
quarantinedAtMs: quarantine?.occurredAtMs ?? null,
|
||||
withdrawalStatus: quarantine?.capabilityStatus ?? null,
|
||||
withdrawalReceiptDigest: quarantine?.receiptDigest ?? null,
|
||||
withdrawalCommittedAtMs: quarantine?.committedAtMs ?? null,
|
||||
failureReason: record.failure?.reason ?? null,
|
||||
failedFrom: record.failure?.failedFrom ?? null,
|
||||
failedAtMs: record.failure?.failedAtMs ?? null,
|
||||
version: record.version,
|
||||
createdAtMs: record.createdAtMs,
|
||||
updatedAtMs: record.updatedAtMs,
|
||||
recordDigest: record.recordDigest,
|
||||
});
|
||||
}
|
||||
|
||||
function lifecycleImpactSummary(
|
||||
impact: Readonly<PluginPackageLifecycleImpact>,
|
||||
) {
|
||||
return Object.freeze({
|
||||
action: impact.action,
|
||||
projectId: impact.target.projectId,
|
||||
packageName: impact.target.packageName,
|
||||
installationId: impact.target.installationId,
|
||||
lockDigest: impact.target.lockDigest,
|
||||
installVersion: impact.target.installVersion,
|
||||
installRecordDigest: impact.target.installRecordDigest,
|
||||
expectedVersion: impact.expected.version,
|
||||
expectedDisposition: impact.expected.disposition,
|
||||
expectedEventDigest: impact.expected.eventDigest,
|
||||
generationDigest: impact.generationDigest,
|
||||
materializedRevisionDigest: impact.materializedRevisionDigest,
|
||||
currentToolSnapshotDigest: impact.currentToolSnapshotDigest,
|
||||
taskIds: impact.taskIds,
|
||||
resourceCounts: impact.resourceCounts,
|
||||
referenceGraphDigest: impact.referenceGraphDigest,
|
||||
blockingReferences: impact.blockingReferences,
|
||||
impactDigest: impact.impactDigest,
|
||||
});
|
||||
}
|
||||
|
||||
function lifecycleReceiptSummary(
|
||||
receipt: Readonly<PluginPackageLifecycleReceipt>,
|
||||
) {
|
||||
return Object.freeze({
|
||||
eventDigest: receipt.eventDigest,
|
||||
action: receipt.action,
|
||||
projectId: receipt.target.projectId,
|
||||
packageName: receipt.target.packageName,
|
||||
installationId: receipt.target.installationId,
|
||||
lockDigest: receipt.target.lockDigest,
|
||||
lifecycleVersion: receipt.lifecycle.version,
|
||||
disposition: receipt.lifecycle.disposition,
|
||||
capabilityStatus: receipt.capability.status,
|
||||
taskTransitions: receipt.capability.taskTransitions.length,
|
||||
previousActiveVectorDigest:
|
||||
receipt.capability.previousActiveVectorDigest,
|
||||
currentActiveVectorDigest: receipt.capability.currentActiveVectorDigest,
|
||||
currentToolSnapshotDigest: receipt.capability.currentToolSnapshotDigest,
|
||||
retainedSourceCount: receipt.capability.retainedSourceCount,
|
||||
committedAtMs: receipt.committedAtMs,
|
||||
receiptDigest: receipt.receiptDigest,
|
||||
});
|
||||
}
|
||||
|
||||
async function execute(
|
||||
command: Readonly<LocalPluginPackageCommand>,
|
||||
database: LocalSqlitePluginPackageManagementDatabase,
|
||||
authenticated: Readonly<AuthenticatedLocalCommand>,
|
||||
): Promise<Readonly<LocalPluginPackageCommandResult>> {
|
||||
await authenticated.confirm();
|
||||
if (command.operation === 'plugin-package.lifecycle.plan') {
|
||||
const service = createLocalPluginPackageLifecycleService({
|
||||
authority: database.authority,
|
||||
});
|
||||
const impact = await service.plan(
|
||||
command.request.action,
|
||||
command.request.projectId,
|
||||
command.request.packageName,
|
||||
authenticated.principal,
|
||||
);
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: command.operation,
|
||||
impact,
|
||||
summary: lifecycleImpactSummary(impact),
|
||||
});
|
||||
}
|
||||
if (command.operation === 'plugin-package.lifecycle.execute') {
|
||||
const service = createLocalPluginPackageLifecycleService({
|
||||
authority: database.authority,
|
||||
});
|
||||
const result = await service.execute({
|
||||
...command.request,
|
||||
principal: authenticated.principal,
|
||||
confirmAuthorization: authenticated.confirm,
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: command.operation,
|
||||
status: result.status,
|
||||
approval: approvalSummary(result.approval),
|
||||
receipt: lifecycleReceiptSummary(result.receipt),
|
||||
});
|
||||
}
|
||||
if (command.operation === 'plugin-package.installation.inspect') {
|
||||
const { LocalSqlitePluginPackageInstallRepository } = await import(
|
||||
'@qinglong/local-sqlite/plugin-package-install'
|
||||
);
|
||||
const repository = new LocalSqlitePluginPackageInstallRepository(
|
||||
database.authority,
|
||||
);
|
||||
const item = await repository.findCurrent(
|
||||
command.request.projectId,
|
||||
command.request.packageName,
|
||||
);
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: command.operation,
|
||||
installation: item ? installationSummary(item) : null,
|
||||
});
|
||||
}
|
||||
if (command.operation === 'plugin-package.installation.list') {
|
||||
const { LocalSqlitePluginPackageInstallRepository } = await import(
|
||||
'@qinglong/local-sqlite/plugin-package-install'
|
||||
);
|
||||
const repository = new LocalSqlitePluginPackageInstallRepository(
|
||||
database.authority,
|
||||
);
|
||||
const page = await repository.listCurrentPage({
|
||||
projectId: command.request.projectId,
|
||||
limit:
|
||||
command.request.limit ??
|
||||
LOCAL_INSTALLATION_INVENTORY_DEFAULTS[command.options.profile],
|
||||
...(command.request.after === undefined
|
||||
? {}
|
||||
: { after: command.request.after }),
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: command.operation,
|
||||
installations: Object.freeze(page.items.map(installationSummary)),
|
||||
truncated: page.truncated,
|
||||
next: page.next ?? null,
|
||||
});
|
||||
}
|
||||
const service = createLocalPluginPackageManagementService({
|
||||
authority: database.authority,
|
||||
profile: command.options.profile,
|
||||
consumer: LOCAL_PACKAGE_CONSUMER,
|
||||
dispatcher: {
|
||||
owner: 'local_plugin_package_dispatcher',
|
||||
defaultBatchSize: 4,
|
||||
createId: randomUUID,
|
||||
},
|
||||
});
|
||||
switch (command.operation) {
|
||||
case 'plugin-package.propose': {
|
||||
const current = await service.inspect(
|
||||
command.request.actionRef,
|
||||
command.request.approvalRequestId,
|
||||
);
|
||||
const result = await service.propose({
|
||||
...command.request,
|
||||
requestedAtMs:
|
||||
current.approvalRequest?.requestedAtMs ??
|
||||
current.proposal?.createdAtMs ??
|
||||
Date.now(),
|
||||
principal: authenticated.principal,
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: command.operation,
|
||||
proposalStatus: result.proposalStatus,
|
||||
approvalStatus: result.approvalStatus,
|
||||
proposal: proposalSummary(result.proposal),
|
||||
approval: approvalSummary(result.approvalRequest),
|
||||
});
|
||||
}
|
||||
case 'plugin-package.decide': {
|
||||
const { actionRef, ...decisionRequest } = command.request;
|
||||
const current = await service.inspect(
|
||||
actionRef,
|
||||
command.request.approvalRequestId,
|
||||
);
|
||||
if (
|
||||
current.approvalRequest?.action.actionRef === actionRef &&
|
||||
current.approvalRequest.decisionId === command.request.decisionId &&
|
||||
current.approvalRequest.decision === command.request.decision &&
|
||||
current.approvalRequest.decisionReasonCode ===
|
||||
command.request.reasonCode &&
|
||||
current.approvalRequest.decidedBy?.type ===
|
||||
authenticated.principal.subject.type &&
|
||||
current.approvalRequest.decidedBy.id ===
|
||||
authenticated.principal.subject.id
|
||||
) {
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: command.operation,
|
||||
status: 'existing',
|
||||
approval: approvalSummary(current.approvalRequest),
|
||||
});
|
||||
}
|
||||
const result = await service.decide({
|
||||
...decisionRequest,
|
||||
decidedAtMs: Date.now(),
|
||||
principal: authenticated.principal,
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: command.operation,
|
||||
status: result.status,
|
||||
approval: approvalSummary(result.request),
|
||||
});
|
||||
}
|
||||
case 'plugin-package.consume': {
|
||||
const { actionRef, ...consumptionRequest } = command.request;
|
||||
const current = await service.inspect(
|
||||
actionRef,
|
||||
command.request.approvalRequestId,
|
||||
);
|
||||
const result = await service.consume({
|
||||
...consumptionRequest,
|
||||
consumedAtMs:
|
||||
current.approvalRequest?.consumptionId ===
|
||||
command.request.consumptionId &&
|
||||
current.approvalRequest.dispatchId === command.request.dispatchId &&
|
||||
current.approvalRequest.consumedAtMs !== null
|
||||
? current.approvalRequest.consumedAtMs
|
||||
: Date.now(),
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: command.operation,
|
||||
status: result.status,
|
||||
approval: approvalSummary(result.request),
|
||||
dispatch: dispatchSummary(result.dispatch),
|
||||
});
|
||||
}
|
||||
case 'plugin-package.inspect': {
|
||||
const result = await service.inspect(
|
||||
command.request.actionRef,
|
||||
command.request.approvalRequestId,
|
||||
);
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: command.operation,
|
||||
proposal: result.proposal ? proposalSummary(result.proposal) : null,
|
||||
approval: result.approvalRequest
|
||||
? approvalSummary(result.approvalRequest)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
case 'plugin-package.dispatch': {
|
||||
const result = await service.dispatch(command.request.limit);
|
||||
return Object.freeze({
|
||||
schemaVersion: 1,
|
||||
operation: command.operation,
|
||||
summary: result,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dependencies(
|
||||
value: LocalPluginPackageCommandRunnerDependencies,
|
||||
): Readonly<LocalPluginPackageCommandRunnerDependencies> {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.keys(value).length !== 2 ||
|
||||
typeof value.openDatabase !== 'function' ||
|
||||
typeof value.authenticate !== 'function'
|
||||
) {
|
||||
throw new LocalPluginPackageCommandConfigurationError(
|
||||
'runner dependencies are invalid',
|
||||
);
|
||||
}
|
||||
return Object.freeze({ ...value });
|
||||
}
|
||||
|
||||
export function createLocalPluginPackageCommandRunner(
|
||||
candidateDependencies: LocalPluginPackageCommandRunnerDependencies = {
|
||||
openDatabase: openLocalSqlitePluginPackageManagementDatabase,
|
||||
authenticate: establishAuthenticatedLocalCommand,
|
||||
},
|
||||
): LocalPluginPackageCommandRunner {
|
||||
const adapters = dependencies(candidateDependencies);
|
||||
return Object.freeze({
|
||||
async run(commandFilePath: string) {
|
||||
const command = readCommandFile(commandFilePath);
|
||||
const database = await adapters.openDatabase({
|
||||
databasePath: command.options.databasePath,
|
||||
profile: command.options.profile,
|
||||
...(command.options.busyTimeoutMs === undefined
|
||||
? {}
|
||||
: { busyTimeoutMs: command.options.busyTimeoutMs }),
|
||||
});
|
||||
try {
|
||||
const authenticated = await adapters.authenticate(database, {
|
||||
deploymentRoot: command.options.deploymentRoot,
|
||||
databasePath: command.options.databasePath,
|
||||
ownerPepperKeyringDirectory:
|
||||
command.options.ownerPepperKeyringDirectory,
|
||||
credentialFilePath: command.options.credentialFilePath,
|
||||
authenticationNamespace: 'local_package',
|
||||
});
|
||||
return await execute(command, database, authenticated);
|
||||
} finally {
|
||||
await database.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function runLocalPluginPackageCommandFile(
|
||||
commandFilePath: string,
|
||||
): Promise<Readonly<LocalPluginPackageCommandResult>> {
|
||||
return createLocalPluginPackageCommandRunner().run(commandFilePath);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { runLocalPluginPackagePromptCommandFile } from './pluginPackagePromptCommand';
|
||||
|
||||
const USAGE =
|
||||
'Usage: ql3-prompt run --command-file /absolute/private-command.json';
|
||||
|
||||
async function main(argv: readonly string[]): Promise<void> {
|
||||
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
|
||||
process.stdout.write(`${USAGE}\n`);
|
||||
return;
|
||||
}
|
||||
const commandFilePath = argv[2];
|
||||
if (
|
||||
argv.length !== 3 ||
|
||||
argv[0] !== 'run' ||
|
||||
argv[1] !== '--command-file' ||
|
||||
commandFilePath === undefined
|
||||
) {
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
code: 'LOCAL_PLUGIN_PACKAGE_PROMPT_CLI_USAGE_INVALID',
|
||||
message: USAGE,
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 64;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await runLocalPluginPackagePromptCommandFile(
|
||||
commandFilePath,
|
||||
);
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
} catch (error) {
|
||||
const candidate = error as {
|
||||
readonly code?: unknown;
|
||||
readonly name?: unknown;
|
||||
};
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
code:
|
||||
typeof candidate.code === 'string'
|
||||
? candidate.code
|
||||
: 'LOCAL_PLUGIN_PACKAGE_PROMPT_CLI_FAILED',
|
||||
name: typeof candidate.name === 'string' ? candidate.name : 'Error',
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void main(process.argv.slice(2));
|
||||
@@ -0,0 +1,26 @@
|
||||
// Plugin Package owns server-derived Prompt execution commands.
|
||||
export {
|
||||
LocalPluginPackagePromptAuthenticationError,
|
||||
LocalPluginPackagePromptAuthorizationError,
|
||||
LocalPluginPackagePromptCommandConfigurationError,
|
||||
LocalPluginPackagePromptNotFoundError,
|
||||
LocalPluginPackagePromptUnavailableError,
|
||||
} from './plugin-package-prompt-command/contracts';
|
||||
export type {
|
||||
ExecuteLocalPluginPackagePromptCommand,
|
||||
InspectLocalPluginPackagePromptCommand,
|
||||
InspectLocalPluginPackagePromptExecutionCommand,
|
||||
LocalPluginPackagePromptCommand,
|
||||
LocalPluginPackagePromptCommandOptions,
|
||||
LocalPluginPackagePromptCommandResult,
|
||||
LocalPluginPackagePromptCommandRunner,
|
||||
LocalPluginPackagePromptCommandRunnerDependencies,
|
||||
LocalPluginPackagePromptInspectCommandOptions,
|
||||
LocalPluginPackagePromptOutputCommandOptions,
|
||||
LocalPluginPackagePromptOutputIntent,
|
||||
ReadLocalPluginPackagePromptExecutionOutputCommand,
|
||||
} from './plugin-package-prompt-command/contracts';
|
||||
export {
|
||||
createLocalPluginPackagePromptCommandRunner,
|
||||
runLocalPluginPackagePromptCommandFile,
|
||||
} from './plugin-package-prompt-command/runner';
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { runLocalPluginPackagePublisherTrustCommandFile } from './pluginPackagePublisherTrustCommand';
|
||||
|
||||
const USAGE =
|
||||
'Usage: ql3-package-trust run --command-file /absolute/private-command.json';
|
||||
|
||||
async function main(argv: readonly string[]): Promise<void> {
|
||||
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
|
||||
process.stdout.write(`${USAGE}\n`);
|
||||
return;
|
||||
}
|
||||
const commandFilePath = argv[2];
|
||||
if (
|
||||
argv.length !== 3 ||
|
||||
argv[0] !== 'run' ||
|
||||
argv[1] !== '--command-file' ||
|
||||
commandFilePath === undefined
|
||||
) {
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
code: 'LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_CLI_USAGE_INVALID',
|
||||
message: USAGE,
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 64;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result =
|
||||
await runLocalPluginPackagePublisherTrustCommandFile(commandFilePath);
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
} catch (error) {
|
||||
const candidate = error as {
|
||||
readonly code?: unknown;
|
||||
readonly name?: unknown;
|
||||
};
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
code:
|
||||
typeof candidate.code === 'string'
|
||||
? candidate.code
|
||||
: 'LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_CLI_FAILED',
|
||||
name:
|
||||
typeof candidate.name === 'string' ? candidate.name : 'Error',
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void main(process.argv.slice(2));
|
||||
+1041
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { runLocalPluginPackageWorkflowCommandFile } from './pluginPackageWorkflowCommand';
|
||||
|
||||
const USAGE =
|
||||
'Usage: ql3-workflow run --command-file /absolute/private-command.json';
|
||||
|
||||
async function main(argv: readonly string[]): Promise<void> {
|
||||
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
|
||||
process.stdout.write(`${USAGE}\n`);
|
||||
return;
|
||||
}
|
||||
const commandFilePath = argv[2];
|
||||
if (
|
||||
argv.length !== 3 ||
|
||||
argv[0] !== 'run' ||
|
||||
argv[1] !== '--command-file' ||
|
||||
commandFilePath === undefined
|
||||
) {
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
code: 'LOCAL_PLUGIN_PACKAGE_WORKFLOW_CLI_USAGE_INVALID',
|
||||
message: USAGE,
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 64;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await runLocalPluginPackageWorkflowCommandFile(
|
||||
commandFilePath,
|
||||
);
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
} catch (error) {
|
||||
const candidate = error as {
|
||||
readonly code?: unknown;
|
||||
readonly name?: unknown;
|
||||
};
|
||||
process.stderr.write(
|
||||
`${JSON.stringify({
|
||||
code:
|
||||
typeof candidate.code === 'string'
|
||||
? candidate.code
|
||||
: 'LOCAL_PLUGIN_PACKAGE_WORKFLOW_CLI_FAILED',
|
||||
name: typeof candidate.name === 'string' ? candidate.name : 'Error',
|
||||
})}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void main(process.argv.slice(2));
|
||||
@@ -0,0 +1,20 @@
|
||||
// Plugin Package owns generation-bound Workflow administration commands.
|
||||
export { LocalPluginPackageWorkflowCommandConfigurationError } from './plugin-package-workflow-command/contracts';
|
||||
export type {
|
||||
CancelLocalPluginPackageWorkflowCommand,
|
||||
InspectLocalPluginPackageWorkflowCommand,
|
||||
InspectLocalPluginPackageWorkflowRunCommand,
|
||||
ListLocalPluginPackageWorkflowRunEventsCommand,
|
||||
ListLocalPluginPackageWorkflowRunsCommand,
|
||||
ListLocalPluginPackageWorkflowStepRunsCommand,
|
||||
LocalPluginPackageWorkflowCommand,
|
||||
LocalPluginPackageWorkflowCommandOptions,
|
||||
LocalPluginPackageWorkflowCommandResult,
|
||||
LocalPluginPackageWorkflowCommandRunner,
|
||||
LocalPluginPackageWorkflowCommandRunnerDependencies,
|
||||
StartLocalPluginPackageWorkflowCommand,
|
||||
} from './plugin-package-workflow-command/contracts';
|
||||
export {
|
||||
createLocalPluginPackageWorkflowCommandRunner,
|
||||
runLocalPluginPackageWorkflowCommandFile,
|
||||
} from './plugin-package-workflow-command/runner';
|
||||
Reference in New Issue
Block a user