|
|
|
@@ -52,6 +52,8 @@ import type { StandardInteractionRequest } from "@/runtime/coordination/standard
|
|
|
|
|
import type { LineUpMiniAppSDK, MiniAppToolFailure, MiniAppToolProgress } from "@/runtime/app-management/miniapp-sdk";
|
|
|
|
|
import { MiniAppToolStateMachine, type MiniAppToolCallRecord } from "@/runtime/coordination/miniapp-tool-state";
|
|
|
|
|
import { validateMiniAppToolSchema } from "@/runtime/coordination/miniapp-tool-schema";
|
|
|
|
|
import { CapabilityRegistry } from "@/runtime/capabilities/capability-registry";
|
|
|
|
|
import { CapabilityAuditLog } from "@/runtime/capabilities/capability-audit";
|
|
|
|
|
|
|
|
|
|
export type RuntimeSession = {
|
|
|
|
|
api: string;
|
|
|
|
@@ -115,6 +117,8 @@ export class LineUpRuntime {
|
|
|
|
|
private readonly prepareIncoming: (item: ConversationItem) => ConversationItem | undefined;
|
|
|
|
|
private readonly orchestrator: AppOrchestrator;
|
|
|
|
|
private readonly toolRouter: ToolRouter;
|
|
|
|
|
private readonly capabilityRegistry = new CapabilityRegistry();
|
|
|
|
|
private readonly capabilityAudit = new CapabilityAuditLog();
|
|
|
|
|
/** SDK v1 inventory revision advertised by this Runtime instance. */
|
|
|
|
|
private readonly inventoryRevision = "catalog-1";
|
|
|
|
|
|
|
|
|
@@ -166,6 +170,8 @@ export class LineUpRuntime {
|
|
|
|
|
return this.miniappTools.list();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public capabilityAuditEntries() { return this.capabilityAudit.snapshot(); }
|
|
|
|
|
|
|
|
|
|
/** Binds an ordinary MiniApp Tool to its Runtime instance before delivery. */
|
|
|
|
|
public bindToolCallToInstance(callID: string, instanceID: string): boolean {
|
|
|
|
|
const instance = this.lifecycle.instances.get(instanceID);
|
|
|
|
@@ -381,7 +387,7 @@ export class LineUpRuntime {
|
|
|
|
|
request: async (event, data) => this.requestBundledSurface(appScope, instanceID, event, data),
|
|
|
|
|
},
|
|
|
|
|
capabilities: {
|
|
|
|
|
request: (name, reason, input = {}) => this.queueProtocolAction("app-result", "lineup.v1.app.call", { instance_id: instanceID, app_scope: appScope, capability: name, reason, arguments: input, expires_at: new Date(Date.parse(this.now()) + 15 * 60 * 1000).toISOString() }),
|
|
|
|
|
request: (name, reason, input = {}) => this.requestMiniAppCapability(instanceID, name, reason, input),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
@@ -456,10 +462,7 @@ export class LineUpRuntime {
|
|
|
|
|
const name = text("name");
|
|
|
|
|
const reason = text("reason");
|
|
|
|
|
if (!name || !reason || !isJsonObject(payload.input)) throw new Error("invalid_capability_request");
|
|
|
|
|
return this.bridgeReceipt(await this.queueProtocolAction("app-result", "lineup.v1.app.call", {
|
|
|
|
|
instance_id: instanceID, app_scope: instance.app_scope, capability: name, reason, arguments: payload.input,
|
|
|
|
|
expires_at: new Date(Date.parse(this.now()) + 15 * 60 * 1000).toISOString(),
|
|
|
|
|
}));
|
|
|
|
|
return this.bridgeReceipt(await this.requestMiniAppCapability(instanceID, name, reason, payload.input));
|
|
|
|
|
}
|
|
|
|
|
throw new Error("unsupported_bridge_method");
|
|
|
|
|
}
|
|
|
|
@@ -468,6 +471,44 @@ export class LineUpRuntime {
|
|
|
|
|
return receipt.disposition === "accepted" ? { disposition: "accepted", ...(receipt.local_id ? { local_id: receipt.local_id } : {}) } : { disposition: "rejected", code: receipt.code };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Capability requests are proposals, not direct Host handles. Runtime checks
|
|
|
|
|
* the installed Manifest, local Registry/Policy, argument schema and lifecycle
|
|
|
|
|
* before putting a request on the normal Agent confirmation path.
|
|
|
|
|
*/
|
|
|
|
|
private requestMiniAppCapability(instanceID: string, name: string, reason: string, input: JsonObject): Promise<CommandReceipt> {
|
|
|
|
|
const instance = this.lifecycle.instances.get(instanceID);
|
|
|
|
|
const app = instance ? this.apps.get(instance.app_scope) : undefined;
|
|
|
|
|
const resolution = this.capabilityRegistry.resolve(name);
|
|
|
|
|
const callID = this.newID("capability");
|
|
|
|
|
if (!instance || !app || app.kind !== "bundled" || !app.manifest.permissions.includes(name)) {
|
|
|
|
|
this.capabilityAudit.recordDecision(callID, name, "restricted", "rejected", this.now());
|
|
|
|
|
return Promise.resolve({ disposition: "rejected", code: "capability_not_declared" });
|
|
|
|
|
}
|
|
|
|
|
if (resolution.disposition !== "available") {
|
|
|
|
|
this.capabilityAudit.recordDecision(callID, name, "restricted", "rejected", this.now());
|
|
|
|
|
return Promise.resolve({ disposition: "rejected", code: "capability_unavailable" });
|
|
|
|
|
}
|
|
|
|
|
if (!reason.trim() || reason.length > 500 || !this.capabilityRegistry.validateArguments(name, input)) {
|
|
|
|
|
this.capabilityAudit.recordDecision(callID, name, resolution.definition.risk, "rejected", this.now());
|
|
|
|
|
return Promise.resolve({ disposition: "rejected", code: "invalid_capability_request" });
|
|
|
|
|
}
|
|
|
|
|
if (resolution.definition.foreground_required && instance.state !== "foreground") {
|
|
|
|
|
this.capabilityAudit.recordDecision(callID, name, resolution.definition.risk, "rejected", this.now());
|
|
|
|
|
return Promise.resolve({ disposition: "rejected", code: "capability_requires_foreground" });
|
|
|
|
|
}
|
|
|
|
|
this.capabilityAudit.recordDecision(callID, name, resolution.definition.risk, "pending", this.now());
|
|
|
|
|
return this.queueProtocolAction("app-result", "lineup.v1.app.call", {
|
|
|
|
|
call_id: callID,
|
|
|
|
|
instance_id: instanceID,
|
|
|
|
|
app_scope: instance.app_scope,
|
|
|
|
|
capability: name,
|
|
|
|
|
reason: reason.trim().replace(/\s+/gu, " ").slice(0, 500),
|
|
|
|
|
arguments: input,
|
|
|
|
|
expires_at: new Date(Date.parse(this.now()) + 15 * 60 * 1000).toISOString(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @deprecated Use openApp("chat") so App selection remains registry-driven. */
|
|
|
|
|
public openChatApp(): ChatRuntimeSDK {
|
|
|
|
|
return this.openApp("chat");
|
|
|
|
|