feat(plugins): agents and workspaces carry plugin metadata

Plugins had nowhere to keep their own data on an agent or workspace, so
Phase 5 and 6 surfaces would each have invented a side store. Both now
hold an opaque Record<string, JsonValue> that Paseo stores, snapshots and
hands to hooks without reading a key.

Set at creation or changed with agent.metadata.update.request /
workspace.metadata.update.request (top-level keys replace, null removes).
Persisted on the agent and workspace records, so it survives a restart.
Reaches agent.create.before (which may add to it), agent.open.before
(read-only), agent.create.after and workspace.create.before/after.

Gated once on server_info.features.entityMetadata; the client throws
"Update the host" instead of sending to a host without it.
This commit is contained in:
Mohamed Boudra
2026-11-04 11:30:32 +01:00
parent 766673e485
commit 7a04727ebb
26 changed files with 591 additions and 62 deletions
+54
View File
@@ -303,6 +303,9 @@ import type {
// unknown avoids zod-aot's recursive z.json() object-codegen regression.
const JsonWireValueSchema = z.unknown() as z.ZodType<JsonValue>;
/** Plugin-owned data on an agent or workspace. Paseo stores it and never reads a key. */
const EntityMetadataSchema = z.record(z.string(), JsonWireValueSchema);
export const AgentStatusSchema = z.enum(AGENT_LIFECYCLE_STATUSES);
const AgentModeSchema: z.ZodType<AgentMode> = z.object({
@@ -922,6 +925,7 @@ export const AgentSnapshotPayloadSchema = z.object({
lastError: z.string().optional(),
title: z.string().nullable(),
labels: z.record(z.string(), z.string()).default({}),
metadata: EntityMetadataSchema.optional(),
requiresAttention: z.boolean().optional(),
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
attentionTimestamp: z.string().nullable().optional(),
@@ -1071,6 +1075,22 @@ export const WorkspaceTitleSetRequestSchema = z.object({
requestId: z.string(),
});
export const AgentMetadataUpdateRequestSchema = z.object({
type: z.literal("agent.metadata.update.request"),
agentId: z.string(),
// Top-level keys replace; null removes the key.
patch: EntityMetadataSchema,
requestId: z.string(),
});
export const WorkspaceMetadataUpdateRequestSchema = z.object({
type: z.literal("workspace.metadata.update.request"),
workspaceId: z.string(),
// Top-level keys replace; null removes the key.
patch: EntityMetadataSchema,
requestId: z.string(),
});
export const WorkspacePinSetRequestSchema = z.object({
type: z.literal("workspace.pin.set.request"),
workspaceId: z.string(),
@@ -1847,6 +1867,7 @@ export const CreateAgentRequestMessageSchema = z.object({
// Omitted on agent-scoped creation, the caller agent's own value applies.
background: z.boolean().optional(),
labels: z.record(z.string(), z.string()).default({}),
metadata: EntityMetadataSchema.optional(),
requestId: z.string(),
});
@@ -2225,6 +2246,28 @@ export const WorkspaceTitleSetResponseSchema = z.object({
payload: WorkspaceTitleSetResponsePayloadSchema,
});
export const AgentMetadataUpdateResponseSchema = z.object({
type: z.literal("agent.metadata.update.response"),
payload: z.object({
requestId: z.string(),
agentId: z.string(),
accepted: z.boolean(),
metadata: EntityMetadataSchema.nullable(),
error: z.string().nullable(),
}),
});
export const WorkspaceMetadataUpdateResponseSchema = z.object({
type: z.literal("workspace.metadata.update.response"),
payload: z.object({
requestId: z.string(),
workspaceId: z.string(),
accepted: z.boolean(),
metadata: EntityMetadataSchema.nullable(),
error: z.string().nullable(),
}),
});
export const WorkspacePinSetResponsePayloadSchema = z.object({
requestId: z.string(),
workspaceId: z.string(),
@@ -2780,6 +2823,7 @@ export const WorkspaceCreateRequestSchema = z.object({
idempotencyKey: z.string().min(1).max(512).optional(),
// Optional user-set title applied to the created workspace.
title: z.string().optional(),
metadata: EntityMetadataSchema.optional(),
// Optional prompt context for workspace-level name/branch generation.
firstAgentContext: FirstAgentContextSchema.optional(),
// Retained for wire parsing; public internal creation is no longer supported.
@@ -3377,6 +3421,8 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
ProjectIconSetRequestSchema,
ProjectRemoveRequestSchema,
WorkspaceTitleSetRequestSchema,
AgentMetadataUpdateRequestSchema,
WorkspaceMetadataUpdateRequestSchema,
WorkspacePinSetRequestSchema,
WorkspaceLabelListRequestSchema,
WorkspaceLabelAssignmentSetRequestSchema,
@@ -3917,6 +3963,7 @@ export const ServerInfoStatusPayloadSchema = z
explicitEventSubscriptions: z.boolean().optional(),
ownedSubscriptions: z.boolean().optional(),
providerOptionControls: z.boolean().optional(),
entityMetadata: z.boolean().optional(),
binaryChunkingV1: z.boolean().optional(),
workspaceSetupObservation: z.boolean().optional(),
workspaceSortTimes: z.boolean().optional(),
@@ -4251,6 +4298,7 @@ export const WorkspaceDescriptorPayloadSchema = z
pinnedAt: z.string().nullable().optional(),
// COMPAT(workspaceLabels): added in v0.5.0, remove optional after 2027-08-14.
labels: z.array(z.string()).optional(),
metadata: EntityMetadataSchema.optional(),
// Retained for wire parsing of older daemon descriptors.
internal: z.boolean().optional(),
background: z.boolean().optional(),
@@ -7267,6 +7315,8 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
ProjectIconSetResponseSchema,
ProjectRemoveResponseSchema,
WorkspaceTitleSetResponseSchema,
AgentMetadataUpdateResponseSchema,
WorkspaceMetadataUpdateResponseSchema,
WorkspacePinSetResponseSchema,
WorkspaceRecoveryInspectResponseSchema,
WorkspaceRecoveryRestoreResponseSchema,
@@ -7636,6 +7686,10 @@ export type ProjectIconSetRequest = z.infer<typeof ProjectIconSetRequestSchema>;
export type ProjectRemoveRequest = z.infer<typeof ProjectRemoveRequestSchema>;
export type WorkspaceTitleSetRequest = z.infer<typeof WorkspaceTitleSetRequestSchema>;
export type WorkspacePinSetRequest = z.infer<typeof WorkspacePinSetRequestSchema>;
export type AgentMetadataUpdateRequest = z.infer<typeof AgentMetadataUpdateRequestSchema>;
export type WorkspaceMetadataUpdateRequest = z.infer<typeof WorkspaceMetadataUpdateRequestSchema>;
export type AgentMetadataUpdateResponse = z.infer<typeof AgentMetadataUpdateResponseSchema>;
export type WorkspaceMetadataUpdateResponse = z.infer<typeof WorkspaceMetadataUpdateResponseSchema>;
export type WorkspaceRecoveryInspectRequest = z.infer<typeof WorkspaceRecoveryInspectRequestSchema>;
export type WorkspaceRecoveryRestoreRequest = z.infer<typeof WorkspaceRecoveryRestoreRequestSchema>;
export type SetAgentModeRequestMessage = z.infer<typeof SetAgentModeRequestMessageSchema>;