feat(iteration): implement sdk v1 miniapp runtime loop
This commit is contained in:
@@ -10,7 +10,12 @@
|
||||
* or a marketplace namespace. The initial registry is static, but the same
|
||||
* record shape is the boundary that the later App Registry/Market will own.
|
||||
*/
|
||||
import { validateMiniAppManifest, type MiniAppManifestV1 } from "@/runtime/app-management/miniapp-manifest";
|
||||
import { BUNDLED_REFERENCE_MANIFESTS } from "@/runtime/app-management/reference-miniapps";
|
||||
import type { JsonObject } from "@/runtime/protocol/lineup-v1";
|
||||
|
||||
export type AppScope = "chat" | "app-registry" | "settings" | "runtime" | (string & {});
|
||||
export type MiniAppKind = "system" | "bundled";
|
||||
|
||||
export type AppToolDefinition = {
|
||||
name: string;
|
||||
@@ -18,6 +23,13 @@ export type AppToolDefinition = {
|
||||
/** JSON-object property names accepted by this tool. */
|
||||
parameters: readonly string[];
|
||||
requires_permission?: string;
|
||||
input_schema?: JsonObject;
|
||||
output_schema?: JsonObject;
|
||||
target?: {
|
||||
app_scope: AppScope;
|
||||
requires_foreground: boolean;
|
||||
restore_previous_focus: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -33,16 +45,19 @@ export type AppManifest = {
|
||||
|
||||
export type CoreAppRecord = {
|
||||
app_scope: AppScope;
|
||||
kind: "core" | "extension";
|
||||
kind: MiniAppKind;
|
||||
enabled: boolean;
|
||||
default_eligible: boolean;
|
||||
recovery: boolean;
|
||||
manifest: AppManifest;
|
||||
};
|
||||
|
||||
/** Transitional input accepted while older host adapters are upgraded. */
|
||||
export type CoreAppRecordInput = Omit<CoreAppRecord, "kind"> & { kind: MiniAppKind | "core" | "extension" };
|
||||
|
||||
const INITIAL_CORE_APPS: readonly CoreAppRecord[] = [
|
||||
{
|
||||
app_scope: "chat", kind: "core", enabled: true, default_eligible: true, recovery: true,
|
||||
app_scope: "chat", kind: "system", enabled: true, default_eligible: true, recovery: true,
|
||||
manifest: {
|
||||
app_scope: "chat", version: "1.0.0", permissions: [],
|
||||
tools: [
|
||||
@@ -61,8 +76,11 @@ const INITIAL_CORE_APPS: readonly CoreAppRecord[] = [
|
||||
export class CoreAppRegistry {
|
||||
private readonly apps = new Map<AppScope, CoreAppRecord>();
|
||||
|
||||
public constructor(records: readonly CoreAppRecord[] = INITIAL_CORE_APPS) {
|
||||
public constructor(records: readonly CoreAppRecordInput[] = INITIAL_CORE_APPS) {
|
||||
for (const record of records) this.register(record);
|
||||
if (records === INITIAL_CORE_APPS) {
|
||||
for (const manifest of BUNDLED_REFERENCE_MANIFESTS) this.installMiniAppManifest(manifest);
|
||||
}
|
||||
}
|
||||
|
||||
public list(): readonly CoreAppRecord[] {
|
||||
@@ -83,13 +101,39 @@ export class CoreAppRegistry {
|
||||
}
|
||||
|
||||
/** Runtime-only installation path; extension Apps never receive this object. */
|
||||
public install(record: CoreAppRecord): void { this.register(record); }
|
||||
public install(record: CoreAppRecordInput): void { this.register(record); }
|
||||
|
||||
private register(record: CoreAppRecord): void {
|
||||
/** Admits a validated SDK v1 Manifest without exposing the Manifest object to Apps. */
|
||||
public installMiniAppManifest(manifest: MiniAppManifestV1, options: Pick<CoreAppRecord, "enabled" | "default_eligible" | "recovery"> = { enabled: true, default_eligible: false, recovery: false }): void {
|
||||
const validation = validateMiniAppManifest(manifest);
|
||||
if (!validation.accepted) throw new Error(`MiniApp Manifest denied: ${validation.detail}`);
|
||||
this.install({
|
||||
app_scope: manifest.app_scope,
|
||||
kind: manifest.kind,
|
||||
...options,
|
||||
manifest: {
|
||||
app_scope: manifest.app_scope,
|
||||
version: manifest.version,
|
||||
permissions: [...manifest.requested_capabilities],
|
||||
tools: manifest.tools.map(tool => ({
|
||||
name: tool.id,
|
||||
handling: tool.handling,
|
||||
parameters: [],
|
||||
input_schema: tool.input_schema,
|
||||
output_schema: tool.output_schema,
|
||||
target: tool.target,
|
||||
...(tool.permissions?.[0] ? { requires_permission: tool.permissions[0] } : {}),
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private register(record: CoreAppRecordInput): void {
|
||||
if (!isAppScope(record.app_scope) || record.manifest.app_scope !== record.app_scope) throw new Error("Core App Registry received an invalid app_scope.");
|
||||
if (this.apps.has(record.app_scope)) throw new Error(`Core App Registry has duplicate app_scope: ${record.app_scope}`);
|
||||
if (!isManifest(record.manifest)) throw new Error(`Core App Registry has an invalid manifest: ${record.app_scope}`);
|
||||
this.apps.set(record.app_scope, copyRecord(record));
|
||||
const kind: MiniAppKind = record.kind === "core" ? "system" : record.kind === "extension" ? "bundled" : record.kind;
|
||||
this.apps.set(record.app_scope, copyRecord({ ...record, kind }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +143,7 @@ function copyRecord(record: CoreAppRecord): CoreAppRecord {
|
||||
manifest: {
|
||||
...record.manifest,
|
||||
permissions: [...record.manifest.permissions],
|
||||
tools: record.manifest.tools.map(tool => ({ ...tool, parameters: [...tool.parameters] })),
|
||||
tools: record.manifest.tools.map(tool => ({ ...tool, parameters: [...tool.parameters], ...(tool.input_schema ? { input_schema: clone(tool.input_schema) } : {}), ...(tool.output_schema ? { output_schema: clone(tool.output_schema) } : {}) })),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -113,12 +157,17 @@ function isManifest(manifest: AppManifest): boolean {
|
||||
&& !toolNames.has(tool.name)
|
||||
&& ["direct", "interactive", "launch", "foreground", "operation"].includes(tool.handling)
|
||||
&& tool.parameters.every(parameter => /^[a-z][a-z0-9_]{0,63}$/.test(parameter))
|
||||
&& (!tool.input_schema || isJsonObject(tool.input_schema))
|
||||
&& (!tool.output_schema || isJsonObject(tool.output_schema))
|
||||
&& (!tool.requires_permission || manifest.permissions.includes(tool.requires_permission));
|
||||
toolNames.add(tool.name);
|
||||
return valid;
|
||||
});
|
||||
}
|
||||
|
||||
function clone<T>(value: T): T { return JSON.parse(JSON.stringify(value)) as T; }
|
||||
function isJsonObject(value: unknown): value is JsonObject { return Boolean(value && typeof value === "object" && !Array.isArray(value)); }
|
||||
|
||||
export function isAppScope(value: unknown): value is AppScope {
|
||||
return typeof value === "string" && /^[a-z][a-z0-9-]{0,63}$/.test(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user