feat(iteration): implement sdk v1 miniapp runtime loop
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* MiniApp SDK v1 的声明性 Manifest 契约。
|
||||
*
|
||||
* 该模块只验证 Runtime 接受的声明,不加载 Bundle、不创建 DOM,也不授予任何能力。
|
||||
* Runtime、Inventory 和 Tool Router 共享它,避免各自解释 Manifest。
|
||||
*/
|
||||
import type { JsonObject } from "@/runtime/protocol/lineup-v1";
|
||||
|
||||
export type MiniAppKind = "system" | "bundled";
|
||||
export type MiniAppToolHandling = "direct" | "interactive" | "launch" | "foreground" | "operation";
|
||||
|
||||
export type MiniAppToolTarget = Readonly<{
|
||||
app_scope: string;
|
||||
requires_foreground: boolean;
|
||||
restore_previous_focus: boolean;
|
||||
}>;
|
||||
|
||||
export type MiniAppToolDescriptor = Readonly<{
|
||||
id: string;
|
||||
version: 1;
|
||||
handling: MiniAppToolHandling;
|
||||
target?: MiniAppToolTarget;
|
||||
input_schema: JsonObject;
|
||||
output_schema: JsonObject;
|
||||
permissions?: readonly string[];
|
||||
timeout_ms?: number;
|
||||
}>;
|
||||
|
||||
export type MiniAppManifestV1 = Readonly<{
|
||||
app_scope: string;
|
||||
version: string;
|
||||
kind: MiniAppKind;
|
||||
tools: readonly MiniAppToolDescriptor[];
|
||||
subscriptions: readonly string[];
|
||||
requested_capabilities: readonly string[];
|
||||
host: Readonly<{
|
||||
min_version: string;
|
||||
surface_required: boolean;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
export type ManifestValidation =
|
||||
| Readonly<{ accepted: true }>
|
||||
| Readonly<{ accepted: false; code: "manifest_denied"; detail: string }>;
|
||||
|
||||
const APP_SCOPE = /^[a-z][a-z0-9-]{0,63}$/;
|
||||
const TOOL_ID = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/;
|
||||
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
||||
const CAPABILITY = /^[a-z][a-z0-9._-]{0,63}$/;
|
||||
const HANDLINGS = new Set<MiniAppToolHandling>(["direct", "interactive", "launch", "foreground", "operation"]);
|
||||
|
||||
/**
|
||||
* Checks only static, local rules. Policy and per-user capability approval are
|
||||
* intentionally deferred to Runtime, because a Manifest never grants access.
|
||||
*/
|
||||
export function validateMiniAppManifest(manifest: MiniAppManifestV1): ManifestValidation {
|
||||
if (!APP_SCOPE.test(manifest.app_scope)) return reject("app_scope must be a lowercase Runtime scope.");
|
||||
if (!SEMVER.test(manifest.version) || !SEMVER.test(manifest.host.min_version)) return reject("version and host.min_version must be semantic versions.");
|
||||
if (manifest.kind !== "system" && manifest.kind !== "bundled") return reject("kind must be system or bundled.");
|
||||
if (manifest.kind === "bundled" && manifest.host.surface_required !== true) return reject("bundled MiniApps require a restricted Surface.");
|
||||
if (!Array.isArray(manifest.tools) || !Array.isArray(manifest.subscriptions) || !Array.isArray(manifest.requested_capabilities)) return reject("manifest collections must be arrays.");
|
||||
if (!manifest.subscriptions.every(entry => typeof entry === "string" && entry.length > 0)) return reject("subscriptions must be non-empty strings.");
|
||||
if (!manifest.requested_capabilities.every(entry => CAPABILITY.test(entry))) return reject("requested capabilities contain an invalid identifier.");
|
||||
|
||||
const toolIDs = new Set<string>();
|
||||
for (const tool of manifest.tools) {
|
||||
if (!TOOL_ID.test(tool.id) || toolIDs.has(tool.id)) return reject("tool IDs must be unique dotted identifiers.");
|
||||
toolIDs.add(tool.id);
|
||||
if (tool.version !== 1 || !HANDLINGS.has(tool.handling)) return reject("tool version or handling is invalid.");
|
||||
if (!isJsonObject(tool.input_schema) || !isJsonObject(tool.output_schema)) return reject("tool schemas must be JSON objects.");
|
||||
if (tool.permissions && !tool.permissions.every((permission: string) => manifest.requested_capabilities.includes(permission))) return reject("tool permissions must be declared by the manifest.");
|
||||
if (tool.timeout_ms !== undefined && (!Number.isInteger(tool.timeout_ms) || tool.timeout_ms <= 0)) return reject("tool timeout_ms must be a positive integer.");
|
||||
|
||||
if (tool.handling === "interactive") {
|
||||
if (tool.target !== undefined) return reject("interactive Tools must not target a MiniApp.");
|
||||
if (tool.timeout_ms !== undefined) return reject("interactive Tools use interaction expiry, not tool timeout_ms.");
|
||||
} else if (!validTarget(tool.target)) {
|
||||
return reject("non-interactive Tools require a valid target.");
|
||||
}
|
||||
}
|
||||
|
||||
return { accepted: true };
|
||||
}
|
||||
|
||||
function validTarget(target: MiniAppToolTarget | undefined): target is MiniAppToolTarget {
|
||||
return Boolean(target
|
||||
&& APP_SCOPE.test(target.app_scope)
|
||||
&& typeof target.requires_foreground === "boolean"
|
||||
&& typeof target.restore_previous_focus === "boolean");
|
||||
}
|
||||
|
||||
function isJsonObject(value: unknown): value is JsonObject {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function reject(detail: string): ManifestValidation {
|
||||
return { accepted: false, code: "manifest_denied", detail };
|
||||
}
|
||||
Reference in New Issue
Block a user