The main agent is meant to know the user, but nothing could read or change the user model the foundation added. Onboarding pasted the user's answers into the first bot's instructions as a "User Context" block, and the user had no way to see or remove what their bots knew about them. This makes the user model a user-controlled resource: - db: a profile repository scoped by user. One atomic update writes the account name, profile fields, new facts (deduplicated by normalized text, capped per user), fact edits including per-space visibility, and confirmations. Health and values facts wait for confirmation unless the user wrote them. Forgetting hard-deletes facts by id, category or all, clears the summary that may restate them and marks earlier chats as consolidated so they cannot teach them again; forgetting everything also clears the profile fields. Writes take a per-user advisory lock. - api/contracts: profile.get, profile.update and profile.forget. Unknown fact ids are a 404 on update and ignored on forget; spaces outside the user's memberships are rejected. - adapters: an update_user_profile tool for the main bot only, hidden from specialists and shared messaging channels and refused if called anyway. Facts carry source "conversation" and the bot, thread and message they came from. The scripted runtime records "learn about me: [category:] statement" for tests. - web: onboarding writes the name, time zone (browser default, now prefilled and editable), locale, chosen language, communication style and the hobby and work-style answers to the profile instead of overwriting the bot's instructions, and the harvested memory section no longer repeats them. A new "About you" settings section lists profile fields and facts by category with their origin, confirms pending facts, and forgets one fact or everything. Tests cover the contracts, the repository (unit and PostgreSQL), tool selection and refusal, the scripted trigger, the onboarding mapping and view helpers, an API suite that runs the tool through the executor with a real main bot, authorization for the new procedures, and an E2E spec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
843 lines
29 KiB
TypeScript
843 lines
29 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 {
|
|
CreateProjectGoalInput,
|
|
ProjectGoalSchema,
|
|
UpdateProjectGoalInput,
|
|
} from "./project-goals.js";
|
|
import { ProjectSessionsOutputSchema } from "./project-sessions.js";
|
|
import { MessageReactionSchema } from "./reactions.js";
|
|
import { RunsListOutputSchema } from "./runs.js";
|
|
import { SearchQueryOutputSchema } from "./search.js";
|
|
import {
|
|
ForgetUserFactsInput,
|
|
UpdateUserProfileInput,
|
|
UserProfileStateSchema,
|
|
} from "./user-profile.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),
|
|
},
|
|
/** What the user's bots know about the user, across spaces. */
|
|
profile: {
|
|
get: oc.output(UserProfileStateSchema),
|
|
update: oc.input(UpdateUserProfileInput).output(UserProfileStateSchema),
|
|
forget: oc.input(ForgetUserFactsInput).output(UserProfileStateSchema),
|
|
},
|
|
spaces: {
|
|
list: oc.output(SpaceNavigationSchema),
|
|
create: oc
|
|
.input(
|
|
z.object({
|
|
name: z.string().trim().min(1).max(60),
|
|
workspacePath: z.string().trim().min(1).max(500).optional(),
|
|
}),
|
|
)
|
|
.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) })),
|
|
scan: oc.input(z.object({ botId: Id.optional() })).output(z.object({ scanId: z.string() })),
|
|
scanStatus: oc.input(z.object({ scanId: z.string() })).output(
|
|
z.object({
|
|
status: z.enum(["scanning", "connecting", "building", "briefing", "complete", "failed"]),
|
|
progress: z.number(),
|
|
message: z.string(),
|
|
result: z.any().optional(),
|
|
}),
|
|
),
|
|
},
|
|
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;
|