feat: finish the ai plugin (B18)

This commit is contained in:
LukeGus committed 2026-09-24 13:43:26 -05:00
1 parent 6b4d1432ab
commit 21937a6b02
193 files changed
+23429 -4244

No files matched your search

+23
View File
@@ -219,6 +219,19 @@ export interface PluginServices {
}
export interface PluginSecrets {
/**
* The acting user's own copy of a secret this plugin stored, decrypted, or
* null when nothing is stored. Needs secrets:own and an actor. Not audited:
* a plugin reads its keys on every request that uses them.
*/
get: (key: string) => Promise<string | null>;
/**
* Stores a secret for the acting user, encrypted at rest. Null clears it.
* Needs secrets:own and an actor. Audited without the value.
*/
set: (key: string, value: string | null) => Promise<void>;
/** Removes the acting user's secret. Needs secrets:own. Audited. */
delete: (key: string) => Promise<void>;
offer: (
key: string,
resolve: (userId: string) => Promise<string | null> | string | null,
@@ -580,6 +593,14 @@ export interface PluginHosts {
hostId: number,
patch: PluginHostUpdateInput,
) => Promise<PluginHostRecord | null>;
/**
* Deletes a host the acting user owns, the same way the host editor's
* delete does: its access grants, activity and plugin settings go with it
* and remote sync gets a tombstone. Needs hosts:write, and the acting user
* needs the core hosts.delete permission. False when the host does not
* exist or is not theirs.
*/
delete: (hostId: number) => Promise<boolean>;
/**
* Every host the acting user owns, decrypted and wide (PluginHostRecord,
* not the narrow list() summary). For a plugin that scans its own hosts in
@@ -1317,6 +1338,8 @@ export interface PluginFetchInit {
* private is refused, and redirects are never followed.
*/
allowPrivateHosts?: readonly string[];
/** Aborts the request, and a streamed body, when the caller gives up. */
signal?: AbortSignal;
}
/**
+5
View File
@@ -600,6 +600,11 @@ export interface TermixApp extends TermixAppInfo {
* desktop app's connected remote server, anything else is `api`.
*/
apiFor: (origin?: unknown) => PluginApiClient;
/**
* A raw fetch on /plugin-api/<id>/<path> with core's auth, for a response
* read as a stream (server-sent events), which `api` cannot do.
*/
fetch: (path: string, init?: RequestInit) => Promise<Response>;
/** WebSocket URL and auth subprotocols for /plugin-ws/<id>/<path>. */
wsUrl: (
path: string,
+37
View File
@@ -185,6 +185,8 @@ export interface FakePluginContext {
channelIds: number[];
notification: PluginNotification;
}>;
/** Backing store behind ctx.secrets.get/set, keyed "<userId>:<key>". */
secretStore: Map<string, string>;
/** Every ctx.fetch call, in order. */
fetches: Array<{ url: string; init?: PluginFetchInit }>;
/** Every ctx.audit.record entry, in order. */
@@ -290,6 +292,7 @@ export function createFakeContext(
const credentialReads: FakePluginContext["credentialReads"] = [];
const notifications: FakePluginContext["notifications"] = [];
const fetches: FakePluginContext["fetches"] = [];
const secretStore = new Map<string, string>();
const audits: FakePluginContext["audits"] = [];
const activities: FakePluginContext["activities"] = [];
const trackedSessions: number[] = [];
@@ -491,6 +494,14 @@ export function createFakeContext(
},
secrets: {
get: async (key) => secretStore.get(`${actor ?? ""}:${key}`) ?? null,
set: async (key, value) => {
if (value === null) secretStore.delete(`${actor ?? ""}:${key}`);
else secretStore.set(`${actor ?? ""}:${key}`, value);
},
delete: async (key) => {
secretStore.delete(`${actor ?? ""}:${key}`);
},
offer: () => {},
withdraw: () => false,
getShared: async () => null,
@@ -629,6 +640,11 @@ export function createFakeContext(
hostRecordsById.set(hostId, updated);
return updated;
},
delete: async (hostId: number): Promise<boolean> => {
const existed =
hostRecordsById.delete(hostId) || hostsById.delete(hostId);
return existed;
},
listOwned: async (): Promise<PluginHostRecord[]> => [
...hostRecordsById.values(),
],
@@ -836,6 +852,7 @@ export function createFakeContext(
credentialReads,
notifications,
fetches,
secretStore,
audits,
activities,
trackedSessions,
@@ -1076,6 +1093,10 @@ export function createMockCtx(
require("hosts:write");
return ctx.hosts.update(hostId, patch);
},
delete: async (hostId) => {
require("hosts:write");
return ctx.hosts.delete(hostId);
},
listOwned: async () => {
require("hosts:write");
return ctx.hosts.listOwned();
@@ -1220,6 +1241,22 @@ export function createMockCtx(
return ctx.fetch(url, init);
},
secrets: {
...ctx.secrets,
get: async (key) => {
require("secrets:own");
return ctx.secrets.get(key);
},
set: async (key, value) => {
require("secrets:own");
return ctx.secrets.set(key, value);
},
delete: async (key) => {
require("secrets:own");
return ctx.secrets.delete(key);
},
},
capabilities: {
has: async (capability) => granted.has(capability),
require: async (capability) => {