feat(runtime): establish miniapp kernel and sdk design
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Runtime 本地 Core App 注册表。
|
||||
*
|
||||
* 当前 MVP 只注册默认且可恢复的 `chat`;后续 App Registry/市场只能通过 Runtime
|
||||
* 管理记录,App 自身不得直接篡改注册表。
|
||||
*
|
||||
* Runtime-local application registry for the first App MVP.
|
||||
*
|
||||
* `app_scope` deliberately is a local routing key, not a publisher identity
|
||||
* 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.
|
||||
*/
|
||||
export type AppScope = "chat" | "app-registry" | "settings" | "runtime" | (string & {});
|
||||
|
||||
export type AppToolDefinition = {
|
||||
name: string;
|
||||
handling: "direct" | "interactive" | "launch" | "foreground" | "operation";
|
||||
/** JSON-object property names accepted by this tool. */
|
||||
parameters: readonly string[];
|
||||
requires_permission?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A Runtime-validated app manifest. This deliberately contains declarative
|
||||
* metadata only: no bundle URL, executable code, or Host capability handle.
|
||||
*/
|
||||
export type AppManifest = {
|
||||
app_scope: AppScope;
|
||||
version: string;
|
||||
tools: readonly AppToolDefinition[];
|
||||
permissions: readonly string[];
|
||||
};
|
||||
|
||||
export type CoreAppRecord = {
|
||||
app_scope: AppScope;
|
||||
kind: "core" | "extension";
|
||||
enabled: boolean;
|
||||
default_eligible: boolean;
|
||||
recovery: boolean;
|
||||
manifest: AppManifest;
|
||||
};
|
||||
|
||||
const INITIAL_CORE_APPS: readonly CoreAppRecord[] = [
|
||||
{
|
||||
app_scope: "chat", kind: "core", enabled: true, default_eligible: true, recovery: true,
|
||||
manifest: {
|
||||
app_scope: "chat", version: "1.0.0", permissions: [],
|
||||
tools: [
|
||||
{ name: "choice", handling: "interactive", parameters: ["call_id", "tool", "title", "prompt", "data", "expires_at"] },
|
||||
{ name: "confirm", handling: "interactive", parameters: ["call_id", "tool", "title", "prompt", "data", "expires_at"] },
|
||||
{ name: "input", handling: "interactive", parameters: ["call_id", "tool", "title", "prompt", "data", "expires_at"] },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* The Runtime owns this registry. Apps may later request operations through
|
||||
* a Runtime SDK, but they never mutate this collection directly.
|
||||
*/
|
||||
export class CoreAppRegistry {
|
||||
private readonly apps = new Map<AppScope, CoreAppRecord>();
|
||||
|
||||
public constructor(records: readonly CoreAppRecord[] = INITIAL_CORE_APPS) {
|
||||
for (const record of records) this.register(record);
|
||||
}
|
||||
|
||||
public list(): readonly CoreAppRecord[] {
|
||||
return [...this.apps.values()].map(copyRecord);
|
||||
}
|
||||
|
||||
public get(appScope: AppScope): CoreAppRecord | undefined {
|
||||
const record = this.apps.get(appScope);
|
||||
return record ? copyRecord(record) : undefined;
|
||||
}
|
||||
|
||||
public defaultApp(): CoreAppRecord {
|
||||
const selected = this.list().find(record => record.enabled && record.default_eligible);
|
||||
if (selected) return selected;
|
||||
const recovery = this.list().find(record => record.enabled && record.recovery);
|
||||
if (recovery) return recovery;
|
||||
throw new Error("Runtime has no enabled recovery application.");
|
||||
}
|
||||
|
||||
/** Runtime-only installation path; extension Apps never receive this object. */
|
||||
public install(record: CoreAppRecord): void { this.register(record); }
|
||||
|
||||
private register(record: CoreAppRecord): 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));
|
||||
}
|
||||
}
|
||||
|
||||
function copyRecord(record: CoreAppRecord): CoreAppRecord {
|
||||
return {
|
||||
...record,
|
||||
manifest: {
|
||||
...record.manifest,
|
||||
permissions: [...record.manifest.permissions],
|
||||
tools: record.manifest.tools.map(tool => ({ ...tool, parameters: [...tool.parameters] })),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isManifest(manifest: AppManifest): boolean {
|
||||
const toolNames = new Set<string>();
|
||||
return typeof manifest.version === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(manifest.version)
|
||||
&& manifest.permissions.every(permission => /^[a-z][a-z0-9._-]{0,63}$/.test(permission))
|
||||
&& manifest.tools.every(tool => {
|
||||
const valid = /^[a-z][a-z0-9._-]{0,63}$/.test(tool.name)
|
||||
&& !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.requires_permission || manifest.permissions.includes(tool.requires_permission));
|
||||
toolNames.add(tool.name);
|
||||
return valid;
|
||||
});
|
||||
}
|
||||
|
||||
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