cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

816 lines
28 KiB
TypeScript

import { eventIterator, oc } from "@orpc/contract";
import * as z from "zod";
import { AiConsentQuerySchema, AiConsentStatusSchema } from "./ai-consent.js";
import { ATTACHMENT_MAX_BASE64_LENGTH, ATTACHMENT_MAX_COUNT } from "./attachments.js";
import {
ActionApprovalRuleSchema,
ActionAutoReviewSettingsSchema,
AgentSecretInputSchema,
AgentSecretSchema,
AgentSkillCatalogEntrySchema,
AgentSkillSchema,
AppBootstrapSchema,
ArtifactSchema,
ArtifactWithContentSchema,
AvatarStyleSchema,
BotMcpServerSchema,
BotSchema,
BotSectionSchema,
CapabilityInstallSchema,
ComputerModeSchema,
ComputerReleaseReasonSchema,
ComputerStatusSchema,
ComputerUpdateSchema,
ConnectionCatalogItemSchema,
ConnectionSchema,
CreateAgentSkillInput,
CreateBotInput,
CreateGroupInput,
CreateRoutineInput,
CreateScratchpadItemInput,
DeploymentSettingsSchema,
ExportManifestSchema,
ExternalConversationPolicySchema,
GroupDetailSchema,
GroupSchema,
IntegrationCatalogResultSchema,
McpServerConfigInput,
McpServerSchema,
MemoryDocumentSchema,
MemoryScopeSchema,
MeSchema,
MessagingAgentConnectionSchema,
MessagingChannelMembershipSchema,
MessagingLinkedIdentitySchema,
MessagingStatusSchema,
ModelCatalogEntrySchema,
ModelConnectInputSchema,
ModelCredentialSchema,
ModelOAuthBeginSchema,
REPLY_QUOTE_MAX_LENGTH,
ReorderBotsInput,
RoutineSchema,
ScratchpadItemSchema,
ScratchpadItemStatusSchema,
ServerUpdateCheckSchema,
ServerUpdateRequestSchema,
ServerUpdateRunSchema,
ServerUpdateStatusSchema,
SkillPlaybookSchema,
SpaceMemoryConfigSchema,
SpaceNavigationSchema,
SpaceSchema,
TaughtSkillSchema,
TeachRecordingEventSchema,
ThreadMessagePageSchema,
ThreadSnapshotSchema,
UpdateAgentSkillInput,
UpdateBotInput,
UpdateExternalConversationPolicyInput,
UpdateGroupInput,
UsageRecordSchema,
VoiceCatalogEntrySchema,
VoiceCredentialSchema,
VoiceInfoSchema,
VoiceStatusSchema,
} from "./domain.js";
import { ProductEventSchema } from "./events.js";
import { Id, IsoDate } from "./ids.js";
import {
IntegrationProviderConfigSchema,
IntegrationSetupStateSchema,
} from "./integration-settings.js";
import { MessageReactionSchema } from "./reactions.js";
import {
CreateProjectGoalInput,
ProjectGoalSchema,
UpdateProjectGoalInput,
} from "./project-goals.js";
import { ProjectSessionsOutputSchema } from "./project-sessions.js";
import { RunsListOutputSchema } from "./runs.js";
import { SearchQueryOutputSchema } from "./search.js";
const botId = z.object({ botId: Id });
const groupId = z.object({ groupId: Id });
const threadTarget = z
.object({
botId: Id.optional(),
groupId: Id.optional(),
})
.superRefine((input, ctx) => {
const hasBot = Boolean(input.botId);
const hasGroup = Boolean(input.groupId);
if (hasBot === hasGroup) {
ctx.addIssue({
code: "custom",
message: "Provide exactly one of botId or groupId",
path: ["botId"],
});
}
});
const structuredMentionTarget = z.discriminatedUnion("kind", [
z.object({ kind: z.literal("bot"), id: Id }),
z.object({ kind: z.literal("group"), id: Id }),
z.object({ kind: z.literal("routine"), id: Id }),
z.object({ kind: z.literal("connector"), id: Id }),
]);
const threadSendInput = threadTarget
.safeExtend({
text: z.string().optional(),
artifactIds: z.array(Id).max(ATTACHMENT_MAX_COUNT).optional(),
/** Bare bot ids (legacy) or typed mention chips from the composer. */
mentions: z
.array(z.union([Id, structuredMentionTarget]))
.max(64)
.optional(),
replyToMessageId: Id.optional(),
replyQuote: z.string().trim().min(1).max(REPLY_QUOTE_MAX_LENGTH).optional(),
clientNonce: z.string().min(1).max(200).optional(),
})
.superRefine((input, ctx) => {
const text = input.text?.trim() ?? "";
const artifactIds = input.artifactIds ?? [];
if (!text && artifactIds.length === 0) {
ctx.addIssue({
code: "custom",
message: "Provide text or at least one attachment",
path: ["text"],
});
}
if (input.replyQuote && !input.replyToMessageId) {
ctx.addIssue({
code: "custom",
message: "replyQuote requires replyToMessageId",
path: ["replyQuote"],
});
}
});
export const appContract = {
aiConsent: {
status: oc.input(AiConsentQuerySchema).output(AiConsentStatusSchema),
allow: oc
.input(
z.object({
scope: z.string(),
version: z.string(),
keys: z.array(z.string()).min(1).max(200),
}),
)
.output(AiConsentStatusSchema),
revoke: oc.input(z.object({ key: z.string().nullable() })).output(AiConsentStatusSchema),
},
health: oc.output(z.object({ ok: z.literal(true), version: z.string() })),
me: oc.output(MeSchema),
preferences: {
update: oc.input(z.object({ avatarStyle: AvatarStyleSchema })).output(MeSchema),
},
spaces: {
list: oc.output(SpaceNavigationSchema),
create: oc.input(z.object({ name: z.string().trim().min(1).max(60) })).output(SpaceSchema),
remove: oc
.input(z.object({ spaceId: Id }))
.output(z.object({ ok: z.literal(true), activeSpaceId: Id })),
},
bootstrap: oc.input(z.object({ botId: Id.optional() })).output(AppBootstrapSchema),
deployment: {
get: oc.output(DeploymentSettingsSchema),
update: oc
.input(
z.object({
signupsEnabled: z.boolean().optional(),
signupAllowlist: z.array(z.string()).optional(),
computerHost: z.enum(["docker", "this-mac"]).nullable().optional(),
}),
)
.output(DeploymentSettingsSchema),
},
/**
* Deployment-owner product updates. When the Compose updater sidecar is reachable, these proxy
* to its `/state` `/plan` `/apply` contract. Rollback stays on the sidecar for ops only and is
* not exposed here. Never git-fetch from the API process.
*/
updater: {
status: oc.output(ServerUpdateStatusSchema),
check: oc.input(ServerUpdateRequestSchema).output(ServerUpdateCheckSchema),
apply: oc.input(ServerUpdateRequestSchema).output(ServerUpdateRunSchema),
},
models: {
list: oc.output(z.array(ModelCatalogEntrySchema)),
credentials: oc.output(z.array(ModelCredentialSchema)),
connect: oc.input(ModelConnectInputSchema).output(ModelCredentialSchema),
probeOpenAiCompatible: oc
.input(
z.object({
baseUrl: z.string(),
apiKey: z.string().optional(),
}),
)
.output(z.object({ models: z.array(z.string()) })),
beginOAuth: oc
.input(
z.object({
provider: z.string(),
label: z.string().optional(),
modelId: z.string().optional(),
}),
)
.output(ModelOAuthBeginSchema),
submitOAuthCode: oc
.input(z.object({ loginId: z.string(), code: z.string().trim().min(1).max(8_192) }))
.output(z.object({ ok: z.literal(true) })),
completeOAuth: oc
.input(z.object({ loginId: z.string() }))
.output(
z.discriminatedUnion("status", [
z.object({ status: z.literal("pending") }),
z.object({ status: z.literal("ready") }),
z.object({ status: z.literal("error"), error: z.string() }),
]),
),
finishOAuth: oc.input(z.object({ loginId: z.string() })).output(ModelCredentialSchema),
cancelOAuth: oc
.input(z.object({ loginId: z.string() }))
.output(z.object({ ok: z.literal(true) })),
setDefault: oc
.input(z.object({ provider: z.string(), modelId: z.string() }))
.output(z.object({ ok: z.literal(true) })),
},
bots: {
list: oc.output(z.array(BotSchema)),
listArchived: oc.output(z.array(BotSchema)),
get: oc.input(botId).output(BotSchema),
create: oc.input(CreateBotInput).output(BotSchema),
duplicate: oc.input(botId).output(BotSchema),
reorder: oc.input(ReorderBotsInput).output(z.object({ ok: z.literal(true) })),
update: oc.input(UpdateBotInput).output(BotSchema),
setComputer: oc.input(z.object({ botId: Id, mode: ComputerModeSchema })).output(BotSchema),
archive: oc.input(botId).output(z.object({ ok: z.literal(true) })),
restore: oc.input(botId).output(z.object({ ok: z.literal(true) })),
remove: oc
.input(z.object({ botId: Id, deleteMemories: z.boolean().default(false) }))
.output(z.object({ ok: z.literal(true) })),
rotateWebhookSecret: oc.input(botId).output(
z.object({
secret: z.string(),
path: z.string(),
webhookConfigured: z.literal(true),
}),
),
},
groups: {
create: oc.input(CreateGroupInput).output(GroupSchema),
list: oc.output(z.array(GroupSchema)),
listArchived: oc.output(z.array(GroupSchema)),
get: oc.input(groupId).output(GroupDetailSchema),
listSessions: oc.input(groupId).output(ProjectSessionsOutputSchema),
duplicate: oc.input(groupId).output(GroupSchema),
update: oc.input(UpdateGroupInput).output(GroupSchema),
archive: oc.input(groupId).output(z.object({ ok: z.literal(true) })),
restore: oc.input(groupId).output(z.object({ ok: z.literal(true) })),
remove: oc.input(groupId).output(z.object({ ok: z.literal(true) })),
},
botSections: {
list: oc.output(z.array(BotSectionSchema)),
create: oc
.input(threadTarget.safeExtend({ name: z.string().trim().min(1).max(60) }))
.output(BotSectionSchema),
update: oc
.input(z.object({ sectionId: Id, name: z.string().trim().min(1).max(60) }))
.output(BotSectionSchema),
},
projectGoals: {
list: oc
.input(
z.object({
sectionId: Id,
groupId: Id.optional(),
seedDefaults: z.boolean().optional(),
}),
)
.output(z.array(ProjectGoalSchema)),
create: oc.input(CreateProjectGoalInput).output(ProjectGoalSchema),
update: oc.input(UpdateProjectGoalInput).output(ProjectGoalSchema),
},
threads: {
head: oc.input(threadTarget).output(
z.object({
threadId: Id,
cursor: z.number().int().min(-1),
}),
),
get: oc.input(threadTarget).output(ThreadSnapshotSchema),
messages: oc
.input(
threadTarget.safeExtend({
before: z.number().int().nonnegative().optional(),
includePeerRuns: z.boolean().optional(),
includePeerReceipts: z.boolean().optional(),
around: z
.object({
messageId: Id.optional(),
seq: z.number().int().nonnegative().optional(),
})
.optional(),
}),
)
.output(ThreadMessagePageSchema),
subscribe: oc
.input(threadTarget.safeExtend({ cursor: z.number().int().min(-1) }))
.output(eventIterator(ProductEventSchema)),
send: oc.input(threadSendInput).output(
z.object({
taskId: Id,
runId: Id,
seq: z.number().int(),
runIds: z.array(Id).optional(),
}),
),
react: oc
.input(
threadTarget.safeExtend({
messageId: Id,
reaction: MessageReactionSchema,
clientNonce: z.string().min(1).max(200),
}),
)
.output(z.object({ ok: z.literal(true) })),
stop: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
followUp: oc
.input(threadTarget.safeExtend({ text: z.string().min(1) }))
.output(z.object({ ok: z.literal(true) })),
clear: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
answer: oc
.input(
threadTarget.safeExtend({
runId: Id,
messageId: Id,
answer: z.string().min(1),
}),
)
.output(z.object({ ok: z.literal(true) })),
markRead: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
markUnread: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
},
computer: {
status: oc.input(botId).output(ComputerStatusSchema),
boot: oc.input(botId).output(ComputerStatusSchema),
stop: oc.input(botId).output(ComputerStatusSchema),
recover: oc.input(botId).output(ComputerUpdateSchema),
reset: oc.input(botId).output(ComputerStatusSchema),
update: oc.input(botId).output(ComputerUpdateSchema),
updates: oc.output(z.array(ComputerUpdateSchema)),
releaseInterrupted: oc
.input(z.object({ id: Id, workersStopped: z.literal(true) }))
.output(z.object({ ok: z.literal(true) })),
dismissUpdate: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
takeover: oc.input(botId).output(z.object({ leaseId: Id, expiresAt: z.string() })),
release: oc
.input(
z.object({
botId: Id,
reason: ComputerReleaseReasonSchema.optional(),
}),
)
.output(z.object({ ok: z.literal(true) })),
input: oc
.input(
z.object({
botId: Id,
kind: z.enum(["key", "pointer", "clipboard", "scroll"]),
payload: z.record(z.string(), z.unknown()),
}),
)
.output(z.object({ ok: z.literal(true) })),
files: oc
.input(z.object({ botId: Id, path: z.string().default("/") }))
.output(
z.array(z.object({ path: z.string(), kind: z.enum(["file", "dir"]), size: z.number() })),
),
readFile: oc
.input(z.object({ botId: Id, path: z.string() }))
.output(z.object({ path: z.string(), content: z.string() })),
screenUrl: oc.input(botId).output(z.object({ url: z.string().nullable() })),
heartbeat: oc.input(botId).output(z.object({ ok: z.literal(true) })),
},
memory: {
list: oc
.input(z.object({ botId: Id.optional(), scope: z.enum(["bot", "user"]).optional() }))
.output(z.array(MemoryDocumentSchema)),
update: oc
.input(z.object({ documentId: Id, content: z.string() }))
.output(MemoryDocumentSchema),
exportMarkdown: oc.input(z.object({ botId: Id.optional() })).output(z.string()),
providerConfig: oc.output(SpaceMemoryConfigSchema.nullable()),
connectProvider: oc
.input(
z.object({
provider: z.string().min(1),
settings: z.record(z.string(), z.string()),
credentials: z.record(z.string(), z.string()),
defaultMemoryScope: MemoryScopeSchema.default("isolated"),
}),
)
.output(SpaceMemoryConfigSchema),
setDefaultScope: oc
.input(z.object({ defaultMemoryScope: MemoryScopeSchema }))
.output(SpaceMemoryConfigSchema),
disconnectProvider: oc.output(z.object({ ok: z.literal(true) })),
},
routines: {
list: oc.input(botId).output(z.array(RoutineSchema)),
create: oc.input(CreateRoutineInput).output(RoutineSchema),
update: oc
.input(
z
.object({
routineId: Id,
name: z.string().optional(),
prompt: z.string().optional(),
crons: z.array(z.string().min(1)).optional(),
timezone: z.string().optional(),
active: z.boolean().optional(),
notify: z.boolean().optional(),
webhookEnabled: z.boolean().optional(),
githubEnabled: z.boolean().optional(),
messageProvider: z
.string()
.min(1)
.max(50)
.regex(/^[a-z0-9._-]+$/i)
.nullable()
.optional(),
/** ISO datetime to arm a never-run one-shot. */
runAt: IsoDate.optional(),
})
.superRefine((value, ctx) => {
if (
value.crons &&
value.crons.length === 0 &&
value.webhookEnabled === false &&
value.githubEnabled === false &&
value.messageProvider === null
) {
ctx.addIssue({
code: "custom",
message: "Add a schedule, webhook, GitHub, or message trigger",
path: ["crons"],
});
}
}),
)
.output(RoutineSchema),
remove: oc.input(z.object({ routineId: Id })).output(z.object({ ok: z.literal(true) })),
testRun: oc
.input(
z.object({
routineId: Id,
clientNonce: z.string().min(1).max(200).optional(),
}),
)
.output(z.object({ runId: Id })),
},
scratchpad: {
list: oc
.input(
z.object({
botId: Id,
status: ScratchpadItemStatusSchema.optional(),
includeDone: z.boolean().optional(),
}),
)
.output(z.array(ScratchpadItemSchema)),
create: oc.input(CreateScratchpadItemInput).output(ScratchpadItemSchema),
update: oc
.input(
z.object({
itemId: Id,
title: z.string().min(1).max(200).optional(),
status: ScratchpadItemStatusSchema.optional(),
notes: z.string().max(4_000).optional(),
}),
)
.output(ScratchpadItemSchema),
remove: oc.input(z.object({ itemId: Id })).output(z.object({ ok: z.literal(true) })),
},
skills: {
list: oc.input(botId).output(z.array(TaughtSkillSchema)),
get: oc.input(z.object({ skillId: Id })).output(TaughtSkillSchema),
start: oc
.input(z.object({ botId: Id, goal: z.string().min(1).max(4000) }))
.output(TaughtSkillSchema),
appendEvent: oc
.input(z.object({ skillId: Id, event: TeachRecordingEventSchema }))
.output(TaughtSkillSchema),
snapshot: oc.input(z.object({ skillId: Id })).output(TaughtSkillSchema),
stop: oc.input(z.object({ skillId: Id })).output(TaughtSkillSchema),
updateDraft: oc
.input(
z.object({
skillId: Id,
name: z.string().optional(),
playbook: SkillPlaybookSchema,
}),
)
.output(TaughtSkillSchema),
save: oc
.input(z.object({ skillId: Id, name: z.string().optional() }))
.output(TaughtSkillSchema),
testRun: oc
.input(z.object({ skillId: Id, prompt: z.string().optional() }))
.output(z.object({ runId: Id })),
remove: oc.input(z.object({ skillId: Id })).output(z.object({ ok: z.literal(true) })),
},
/** Claude Agent Skills (SKILL.md recipes) shared across assistants (not taught/demo skills). Pi already understands this format; we persist and inject them. */
agentSkills: {
list: oc.output(z.array(AgentSkillCatalogEntrySchema)),
get: oc
.input(
z
.object({ skillId: Id.optional(), name: z.string().min(1).max(80).optional() })
.superRefine((input, ctx) => {
if (!input.skillId && !input.name?.trim()) {
ctx.addIssue({
code: "custom",
message: "Provide skillId or name",
path: ["skillId"],
});
}
}),
)
.output(AgentSkillSchema),
create: oc.input(CreateAgentSkillInput).output(AgentSkillSchema),
update: oc.input(UpdateAgentSkillInput).output(AgentSkillSchema),
remove: oc.input(z.object({ skillId: Id })).output(z.object({ ok: z.literal(true) })),
},
capabilities: {
list: oc.output(z.array(CapabilityInstallSchema)),
catalogSearch: oc
.input(
z.object({
query: z.string().trim().max(253).default(""),
usePublicCatalog: z.boolean().default(false),
}),
)
.output(
z.object({
enabled: z.boolean(),
results: z.array(IntegrationCatalogResultSchema),
}),
),
install: oc
.input(
z.object({
kind: z.enum(["skill", "plugin", "mcp", "api", "graphql"]),
name: z.string().min(1).max(120),
source: z.string().min(1).max(2048),
config: z.record(z.string(), z.unknown()).default({}),
credential: z.string().max(16_384).optional(),
}),
)
.output(CapabilityInstallSchema),
remove: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
},
mcp: {
servers: {
list: oc.output(z.array(McpServerSchema)),
create: oc.input(McpServerConfigInput).output(McpServerSchema),
update: oc
.input(
z.union([
z.object({ id: Id, config: McpServerConfigInput }),
z.object({ id: Id, secret: z.string().min(1).max(16384) }),
]),
)
.output(McpServerSchema),
remove: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
},
assignments: {
list: oc.input(botId).output(z.array(BotMcpServerSchema)),
all: oc.output(z.array(BotMcpServerSchema)),
approve: oc.input(z.object({ botId: Id, serverId: Id })).output(BotMcpServerSchema),
replace: oc
.input(
z.object({
botId: Id,
assignments: z.array(
z.object({
serverId: Id,
allowAllTools: z.boolean().default(true),
allowedTools: z.array(z.string().min(1).max(200)).max(500).default([]),
}),
),
}),
)
.output(z.array(BotMcpServerSchema)),
},
oauth: {
begin: oc.input(z.object({ serverId: Id, redirectUri: z.string().url() })).output(
z.discriminatedUnion("status", [
z.object({
status: z.literal("authorization_required"),
sessionId: Id,
authorizationUrl: z.string().url(),
}),
z.object({
status: z.enum(["already_connected", "authorization_not_requested"]),
}),
]),
),
complete: oc
.input(z.object({ sessionId: Id, code: z.string().min(1), state: z.string().min(1) }))
.output(z.object({ ok: z.literal(true) })),
disconnect: oc.input(z.object({ serverId: Id })).output(z.object({ ok: z.literal(true) })),
},
},
onboarding: {
/** Seed the first-run greeting into the bot's thread (focus card is separate). */
start: oc.input(z.object({ botId: Id })).output(z.object({ ok: z.literal(true) })),
/** Post the focus choice card when the thread is still idle. */
promptFocus: oc.input(z.object({ botId: Id })).output(z.object({ ok: z.literal(true) })),
/** Answer the focus choice; posts the app cards. Does not rename the bot. */
choose: oc
.input(z.object({ botId: Id, optionId: z.string() }))
.output(z.object({ ok: z.literal(true) })),
/** Dismiss the unanswered focus card without choosing an option. */
dismissFocus: oc.input(z.object({ botId: Id })).output(z.object({ ok: z.literal(true) })),
/** Flip an app_connect card to connected after authorization completes. */
appConnected: oc
.input(
z.object({ botId: Id, provider: z.string(), connectorId: z.string().default("composio") }),
)
.output(z.object({ ok: z.literal(true) })),
},
integrationSetup: {
get: oc.output(IntegrationSetupStateSchema),
save: oc.input(IntegrationProviderConfigSchema).output(z.object({ ok: z.literal(true) })),
},
connections: {
catalog: oc
.input(z.object({ query: z.string().optional(), connectorId: z.string().optional() }))
.output(z.array(ConnectionCatalogItemSchema)),
list: oc.output(z.array(ConnectionSchema)),
begin: oc
.input(
z.object({
connectorId: z.string().default("composio"),
provider: z.string(),
displayName: z.string(),
}),
)
.output(z.object({ connectionId: Id, authorizationUrl: z.string().nullable() })),
complete: oc
.input(z.object({ connectionId: Id, code: z.string().optional() }))
.output(ConnectionSchema),
rename: oc
.input(z.object({ connectionId: Id, displayName: z.string().trim().min(1).max(80) }))
.output(ConnectionSchema),
revoke: oc.input(z.object({ connectionId: Id })).output(z.object({ ok: z.literal(true) })),
/** Tools the connected provider exposes. Read-only; no per-tool allowlist yet. */
tools: oc.input(z.object({ connectorId: z.string(), provider: z.string() })).output(
z.array(
z.object({
name: z.string(),
description: z.string(),
}),
),
),
},
/** External messaging surface: link state, group channels, agent connections. */
messaging: {
status: oc.output(MessagingStatusSchema),
link: {
/** Issue a short-lived code the user sends to the line from a chat app. */
start: oc
.input(z.object({ botId: Id }))
.output(z.object({ code: z.string(), expiresAt: z.string() })),
},
identities: {
setBot: oc
.input(z.object({ identityId: Id, botId: Id }))
.output(MessagingLinkedIdentitySchema),
unlink: oc.input(z.object({ identityId: Id })).output(z.object({ ok: z.literal(true) })),
},
channels: {
list: oc.output(z.array(MessagingChannelMembershipSchema)),
// Addressed by membership, not channel: one user can have two linked
// chat apps in the same group, and each answers for its own agent.
respond: oc
.input(z.object({ membershipId: Id, accept: z.boolean() }))
.output(MessagingChannelMembershipSchema),
leave: oc.input(z.object({ membershipId: Id })).output(z.object({ ok: z.literal(true) })),
},
connections: {
list: oc.output(z.array(MessagingAgentConnectionSchema)),
respond: oc
.input(z.object({ connectionId: Id, accept: z.boolean() }))
.output(MessagingAgentConnectionSchema),
revoke: oc.input(z.object({ connectionId: Id })).output(z.object({ ok: z.literal(true) })),
},
},
approvalRules: {
list: oc.output(z.array(ActionApprovalRuleSchema)),
set: oc
.input(
z.object({
effect: z.enum(["always_allow", "require_approval"]),
matchKind: z.enum(["tool", "connector", "category"]),
matchValue: z.string().min(1),
}),
)
.output(ActionApprovalRuleSchema),
remove: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
},
autoReview: {
get: oc.output(ActionAutoReviewSettingsSchema),
set: oc.input(z.object({ enabled: z.boolean() })).output(ActionAutoReviewSettingsSchema),
},
artifacts: {
list: oc.input(botId).output(z.array(ArtifactSchema)),
create: oc
.input(
threadTarget.and(
z.object({
name: z.string().min(1).max(255),
mimeType: z.string().min(1),
contentBase64: z.string().min(1).max(ATTACHMENT_MAX_BASE64_LENGTH),
}),
),
)
.output(ArtifactSchema),
get: oc.input(threadTarget.and(z.object({ artifactId: Id }))).output(ArtifactWithContentSchema),
},
usage: {
list: oc.output(z.array(UsageRecordSchema)),
summary: oc.output(
z.object({
inputTokens: z.number(),
outputTokens: z.number(),
runs: z.number(),
}),
),
},
export: {
bot: oc.input(botId).output(ExportManifestSchema),
},
notifications: {
registerPush: oc
.input(z.object({ token: z.string().min(8).max(512) }))
.output(z.object({ ok: z.literal(true) })),
unregisterPush: oc.output(z.object({ ok: z.literal(true) })),
},
search: {
query: oc.input(z.object({ q: z.string().max(200) })).output(SearchQueryOutputSchema),
},
runs: {
list: oc.input(z.object({ filter: z.enum(["active", "recent"]) })).output(RunsListOutputSchema),
},
voice: {
catalog: oc.output(z.array(VoiceCatalogEntrySchema)),
status: oc.output(VoiceStatusSchema),
credentials: oc.output(z.array(VoiceCredentialSchema)),
connect: oc
.input(
z.object({
provider: z.string(),
apiKey: z.string().min(8),
voiceId: z.string().max(120).optional(),
}),
)
.output(VoiceCredentialSchema),
disconnect: oc
.input(z.object({ provider: z.string().min(1) }))
.output(z.object({ ok: z.literal(true) })),
setVoice: oc
.input(z.object({ voiceId: z.string().min(1).max(120), provider: z.string().optional() }))
.output(VoiceStatusSchema),
voices: oc
.input(z.object({ provider: z.string().optional() }))
.output(z.array(VoiceInfoSchema)),
prepare: oc
.input(
z.object({
text: z.string().max(20000),
voiceId: z.string().max(120).optional(),
botId: Id.optional(),
}),
)
.output(z.object({ ready: z.boolean(), utterances: z.array(z.string()) })),
},
externalConversations: {
updatePolicy: oc
.input(UpdateExternalConversationPolicyInput)
.output(ExternalConversationPolicySchema),
},
agentSecrets: {
list: oc.output(z.array(AgentSecretSchema)),
put: oc.input(AgentSecretInputSchema).output(AgentSecretSchema),
remove: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
},
};
export type AppContract = typeof appContract;