Merge branch 'master' into fix/configurable-claimed-api-key-path

This commit is contained in:
Wes Belt 2026-04-06 06:17:42 -04:00 committed by GitHub
commit c171ff901c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
165 changed files with 11349 additions and 1649 deletions

View file

@ -22,6 +22,9 @@ export type {
AdapterModel,
HireApprovedPayload,
HireApprovedHookResult,
ConfigFieldOption,
ConfigFieldSchema,
AdapterConfigSchema,
ServerAdapterModule,
QuotaWindow,
ProviderQuotaResult,

View file

@ -68,6 +68,7 @@ export function redactTranscriptEntryPaths(entry: TranscriptEntry, opts?: HomePa
case "stderr":
case "system":
case "stdout":
case "diff":
return { ...entry, text: redactHomePathUserSegments(entry.text, opts) };
case "tool_call":
return {

View file

@ -193,6 +193,174 @@ export function joinPromptSections(
.join(separator);
}
type PaperclipWakeIssue = {
id: string | null;
identifier: string | null;
title: string | null;
status: string | null;
priority: string | null;
};
type PaperclipWakeComment = {
id: string | null;
issueId: string | null;
body: string;
bodyTruncated: boolean;
createdAt: string | null;
authorType: string | null;
authorId: string | null;
};
type PaperclipWakePayload = {
reason: string | null;
issue: PaperclipWakeIssue | null;
commentIds: string[];
latestCommentId: string | null;
comments: PaperclipWakeComment[];
requestedCount: number;
includedCount: number;
missingCount: number;
truncated: boolean;
fallbackFetchNeeded: boolean;
};
function normalizePaperclipWakeIssue(value: unknown): PaperclipWakeIssue | null {
const issue = parseObject(value);
const id = asString(issue.id, "").trim() || null;
const identifier = asString(issue.identifier, "").trim() || null;
const title = asString(issue.title, "").trim() || null;
const status = asString(issue.status, "").trim() || null;
const priority = asString(issue.priority, "").trim() || null;
if (!id && !identifier && !title) return null;
return {
id,
identifier,
title,
status,
priority,
};
}
function normalizePaperclipWakeComment(value: unknown): PaperclipWakeComment | null {
const comment = parseObject(value);
const author = parseObject(comment.author);
const body = asString(comment.body, "");
if (!body.trim()) return null;
return {
id: asString(comment.id, "").trim() || null,
issueId: asString(comment.issueId, "").trim() || null,
body,
bodyTruncated: asBoolean(comment.bodyTruncated, false),
createdAt: asString(comment.createdAt, "").trim() || null,
authorType: asString(author.type, "").trim() || null,
authorId: asString(author.id, "").trim() || null,
};
}
export function normalizePaperclipWakePayload(value: unknown): PaperclipWakePayload | null {
const payload = parseObject(value);
const comments = Array.isArray(payload.comments)
? payload.comments
.map((entry) => normalizePaperclipWakeComment(entry))
.filter((entry): entry is PaperclipWakeComment => Boolean(entry))
: [];
const commentWindow = parseObject(payload.commentWindow);
const commentIds = Array.isArray(payload.commentIds)
? payload.commentIds
.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
.map((entry) => entry.trim())
: [];
if (comments.length === 0 && commentIds.length === 0) return null;
return {
reason: asString(payload.reason, "").trim() || null,
issue: normalizePaperclipWakeIssue(payload.issue),
commentIds,
latestCommentId: asString(payload.latestCommentId, "").trim() || null,
comments,
requestedCount: asNumber(commentWindow.requestedCount, comments.length || commentIds.length),
includedCount: asNumber(commentWindow.includedCount, comments.length),
missingCount: asNumber(commentWindow.missingCount, 0),
truncated: asBoolean(payload.truncated, false),
fallbackFetchNeeded: asBoolean(payload.fallbackFetchNeeded, false),
};
}
export function stringifyPaperclipWakePayload(value: unknown): string | null {
const normalized = normalizePaperclipWakePayload(value);
if (!normalized) return null;
return JSON.stringify(normalized);
}
export function renderPaperclipWakePrompt(
value: unknown,
options: { resumedSession?: boolean } = {},
): string {
const normalized = normalizePaperclipWakePayload(value);
if (!normalized) return "";
const resumedSession = options.resumedSession === true;
const lines = resumedSession
? [
"## Paperclip Resume Delta",
"",
"You are resuming an existing Paperclip session.",
"This heartbeat is scoped to the issue below. Do not switch to another issue until you have handled this wake.",
"Focus on the new wake delta below and continue the current task without restating the full heartbeat boilerplate.",
"Fetch the API thread only when `fallbackFetchNeeded` is true or you need broader history than this batch.",
"",
`- reason: ${normalized.reason ?? "unknown"}`,
`- issue: ${normalized.issue?.identifier ?? normalized.issue?.id ?? "unknown"}${normalized.issue?.title ? ` ${normalized.issue.title}` : ""}`,
`- pending comments: ${normalized.includedCount}/${normalized.requestedCount}`,
`- latest comment id: ${normalized.latestCommentId ?? "unknown"}`,
`- fallback fetch needed: ${normalized.fallbackFetchNeeded ? "yes" : "no"}`,
]
: [
"## Paperclip Wake Payload",
"",
"Treat this wake payload as the highest-priority change for the current heartbeat.",
"This heartbeat is scoped to the issue below. Do not switch to another issue until you have handled this wake.",
"Before generic repo exploration or boilerplate heartbeat updates, acknowledge the latest comment and explain how it changes your next action.",
"Use this inline wake data first before refetching the issue thread.",
"Only fetch the API thread when `fallbackFetchNeeded` is true or you need broader history than this batch.",
"",
`- reason: ${normalized.reason ?? "unknown"}`,
`- issue: ${normalized.issue?.identifier ?? normalized.issue?.id ?? "unknown"}${normalized.issue?.title ? ` ${normalized.issue.title}` : ""}`,
`- pending comments: ${normalized.includedCount}/${normalized.requestedCount}`,
`- latest comment id: ${normalized.latestCommentId ?? "unknown"}`,
`- fallback fetch needed: ${normalized.fallbackFetchNeeded ? "yes" : "no"}`,
];
if (normalized.issue?.status) {
lines.push(`- issue status: ${normalized.issue.status}`);
}
if (normalized.issue?.priority) {
lines.push(`- issue priority: ${normalized.issue.priority}`);
}
if (normalized.missingCount > 0) {
lines.push(`- omitted comments: ${normalized.missingCount}`);
}
lines.push("", "New comments in order:");
for (const [index, comment] of normalized.comments.entries()) {
const authorLabel = comment.authorId
? `${comment.authorType ?? "unknown"} ${comment.authorId}`
: comment.authorType ?? "unknown";
lines.push(
`${index + 1}. comment ${comment.id ?? "unknown"} at ${comment.createdAt ?? "unknown"} by ${authorLabel}`,
comment.body,
);
if (comment.bodyTruncated) {
lines.push("[comment body truncated]");
}
lines.push("");
}
return lines.join("\n").trim();
}
export function redactEnvForLogs(env: Record<string, string>): Record<string, string> {
const redacted: Record<string, string> = {};
for (const [key, value] of Object.entries(env)) {

View file

@ -41,6 +41,7 @@ export const LEGACY_SESSIONED_ADAPTER_TYPES = new Set([
"codex_local",
"cursor",
"gemini_local",
"hermes_local",
"opencode_local",
"pi_local",
]);
@ -76,6 +77,11 @@ export const ADAPTER_SESSION_MANAGEMENT: Record<string, AdapterSessionManagement
nativeContextManagement: "unknown",
defaultSessionCompaction: DEFAULT_SESSION_COMPACTION_POLICY,
},
hermes_local: {
supportsSessionResume: true,
nativeContextManagement: "confirmed",
defaultSessionCompaction: ADAPTER_MANAGED_SESSION_POLICY,
},
};
function isRecord(value: unknown): value is Record<string, unknown> {

View file

@ -261,6 +261,34 @@ export interface ProviderQuotaResult {
windows: QuotaWindow[];
}
// ---------------------------------------------------------------------------
// Adapter config schema — declarative UI config for external adapters
// ---------------------------------------------------------------------------
export interface ConfigFieldOption {
label: string;
value: string;
/** Optional group key for categorizing options (e.g. provider name) */
group?: string;
}
export interface ConfigFieldSchema {
key: string;
label: string;
type: "text" | "select" | "toggle" | "number" | "textarea" | "combobox";
options?: ConfigFieldOption[];
default?: unknown;
hint?: string;
required?: boolean;
group?: string;
/** Optional metadata — not rendered, but available to custom UI logic */
meta?: Record<string, unknown>;
}
export interface AdapterConfigSchema {
fields: ConfigFieldSchema[];
}
export interface ServerAdapterModule {
type: string;
execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult>;
@ -292,7 +320,14 @@ export interface ServerAdapterModule {
* Returns the detected model/provider and the config source, or null if
* the adapter does not support detection or no config is found.
*/
detectModel?: () => Promise<{ model: string; provider: string; source: string } | null>;
detectModel?: () => Promise<{ model: string; provider: string; source: string; candidates?: string[] } | null>;
/**
* Optional: return a declarative config schema so the UI can render
* adapter-specific form fields without shipping React components.
* Dynamic options (e.g. scanning a profiles directory) should be
* resolved inside this method the caller receives a fully hydrated schema.
*/
getConfigSchema?: () => Promise<AdapterConfigSchema> | AdapterConfigSchema;
}
// ---------------------------------------------------------------------------
@ -309,7 +344,8 @@ export type TranscriptEntry =
| { kind: "result"; ts: string; text: string; inputTokens: number; outputTokens: number; cachedTokens: number; costUsd: number; subtype: string; isError: boolean; errors: string[] }
| { kind: "stderr"; ts: string; text: string }
| { kind: "system"; ts: string; text: string }
| { kind: "stdout"; ts: string; text: string };
| { kind: "stdout"; ts: string; text: string }
| { kind: "diff"; ts: string; changeType: "add" | "remove" | "context" | "hunk" | "file_header" | "truncation"; text: string };
export type StdoutLineParser = (line: string, ts: string) => TranscriptEntry[];
@ -353,4 +389,6 @@ export interface CreateConfigValues {
maxTurnsPerRun: number;
heartbeatEnabled: boolean;
intervalSec: number;
/** Arbitrary key-value pairs populated by schema-driven config fields. */
adapterSchemaValues?: Record<string, unknown>;
}

View file

@ -21,7 +21,7 @@ Core fields:
- chrome (boolean, optional): pass --chrome when running Claude
- promptTemplate (string, optional): run prompt template
- maxTurnsPerRun (number, optional): max turns for one run
- dangerouslySkipPermissions (boolean, optional): pass --dangerously-skip-permissions to claude
- dangerouslySkipPermissions (boolean, optional, default true): pass --dangerously-skip-permissions to claude; defaults to true because Paperclip runs Claude in headless --print mode where interactive permission prompts cannot be answered
- command (string, optional): defaults to "claude"
- extraArgs (string[], optional): additional CLI args
- env (object, optional): KEY=VALUE environment variables

View file

@ -20,6 +20,8 @@ import {
ensurePathInEnv,
resolveCommandForLogs,
renderTemplate,
renderPaperclipWakePrompt,
stringifyPaperclipWakePayload,
runChildProcess,
} from "@paperclipai/adapter-utils/server-utils";
import {
@ -170,6 +172,7 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
const linkedIssueIds = Array.isArray(context.issueIds)
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
: [];
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
if (wakeTaskId) {
env.PAPERCLIP_TASK_ID = wakeTaskId;
@ -189,6 +192,9 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
if (linkedIssueIds.length > 0) {
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
}
if (wakePayloadJson) {
env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
}
if (effectiveWorkspaceCwd) {
env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
}
@ -317,7 +323,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const effort = asString(config.effort, "");
const chrome = asBoolean(config.chrome, false);
const maxTurns = asNumber(config.maxTurnsPerRun, 0);
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, false);
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, true);
const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
const instructionsFileDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
const commandNotes = instructionsFilePath
@ -398,20 +404,24 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
run: { id: runId, source: "on_demand" },
context,
};
const renderedPrompt = renderTemplate(promptTemplate, templateData);
const renderedBootstrapPrompt =
!sessionId && bootstrapPromptTemplate.trim().length > 0
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
: "";
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
const prompt = joinPromptSections([
renderedBootstrapPrompt,
wakePrompt,
sessionHandoffNote,
renderedPrompt,
]);
const promptMetrics = {
promptChars: prompt.length,
bootstrapPromptChars: renderedBootstrapPrompt.length,
wakePromptChars: wakePrompt.length,
sessionHandoffChars: sessionHandoffNote.length,
heartbeatPromptChars: renderedPrompt.length,
};

View file

@ -131,7 +131,7 @@ export async function testEnvironment(
const effort = asString(config.effort, "").trim();
const chrome = asBoolean(config.chrome, false);
const maxTurns = asNumber(config.maxTurnsPerRun, 0);
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, false);
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, true);
const extraArgs = (() => {
const fromExtraArgs = asStringArray(config.extraArgs);
if (fromExtraArgs.length > 0) return fromExtraArgs;

View file

@ -18,6 +18,8 @@ import {
resolveCommandForLogs,
resolvePaperclipDesiredSkillNames,
renderTemplate,
renderPaperclipWakePrompt,
stringifyPaperclipWakePayload,
joinPromptSections,
runChildProcess,
} from "@paperclipai/adapter-utils/server-utils";
@ -313,6 +315,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const linkedIssueIds = Array.isArray(context.issueIds)
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
: [];
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
if (wakeTaskId) {
env.PAPERCLIP_TASK_ID = wakeTaskId;
}
@ -331,6 +334,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
if (linkedIssueIds.length > 0) {
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
}
if (wakePayloadJson) {
env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
}
if (effectiveWorkspaceCwd) {
env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
}
@ -434,11 +440,36 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
}
const repoAgentsNote =
"Codex exec automatically applies repo-scoped AGENTS.md instructions from the current workspace; Paperclip does not currently suppress that discovery.";
const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
const templateData = {
agentId: agent.id,
companyId: agent.companyId,
runId,
company: { id: agent.companyId },
agent,
run: { id: runId, source: "on_demand" },
context,
};
const renderedBootstrapPrompt =
!sessionId && bootstrapPromptTemplate.trim().length > 0
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
: "";
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
const promptInstructionsPrefix = shouldUseResumeDeltaPrompt ? "" : instructionsPrefix;
instructionsChars = promptInstructionsPrefix.length;
const commandNotes = (() => {
if (!instructionsFilePath) {
return [repoAgentsNote];
}
if (instructionsPrefix.length > 0) {
if (shouldUseResumeDeltaPrompt) {
return [
`Loaded agent instructions from ${instructionsFilePath}`,
"Skipped stdin instruction reinjection because an existing Codex session is being resumed with a wake delta.",
repoAgentsNote,
];
}
return [
`Loaded agent instructions from ${instructionsFilePath}`,
`Prepended instructions + path directive to stdin prompt (relative references from ${instructionsDir}).`,
@ -450,25 +481,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
repoAgentsNote,
];
})();
const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
const templateData = {
agentId: agent.id,
companyId: agent.companyId,
runId,
company: { id: agent.companyId },
agent,
run: { id: runId, source: "on_demand" },
context,
};
const renderedPrompt = renderTemplate(promptTemplate, templateData);
const renderedBootstrapPrompt =
!sessionId && bootstrapPromptTemplate.trim().length > 0
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
: "";
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
const prompt = joinPromptSections([
instructionsPrefix,
promptInstructionsPrefix,
renderedBootstrapPrompt,
wakePrompt,
sessionHandoffNote,
renderedPrompt,
]);
@ -476,6 +494,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
promptChars: prompt.length,
instructionsChars,
bootstrapPromptChars: renderedBootstrapPrompt.length,
wakePromptChars: wakePrompt.length,
sessionHandoffChars: sessionHandoffNote.length,
heartbeatPromptChars: renderedPrompt.length,
};

View file

@ -19,6 +19,8 @@ import {
resolvePaperclipDesiredSkillNames,
removeMaintainerOnlySkillSymlinks,
renderTemplate,
renderPaperclipWakePrompt,
stringifyPaperclipWakePayload,
joinPromptSections,
runChildProcess,
} from "@paperclipai/adapter-utils/server-utils";
@ -219,6 +221,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const linkedIssueIds = Array.isArray(context.issueIds)
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
: [];
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
if (wakeTaskId) {
env.PAPERCLIP_TASK_ID = wakeTaskId;
}
@ -237,6 +240,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
if (linkedIssueIds.length > 0) {
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
}
if (wakePayloadJson) {
env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
}
if (effectiveWorkspaceCwd) {
env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
}
@ -352,16 +358,19 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
run: { id: runId, source: "on_demand" },
context,
};
const renderedPrompt = renderTemplate(promptTemplate, templateData);
const renderedBootstrapPrompt =
!sessionId && bootstrapPromptTemplate.trim().length > 0
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
: "";
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
const paperclipEnvNote = renderPaperclipEnvNote(env);
const prompt = joinPromptSections([
instructionsPrefix,
renderedBootstrapPrompt,
wakePrompt,
sessionHandoffNote,
paperclipEnvNote,
renderedPrompt,
@ -370,6 +379,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
promptChars: prompt.length,
instructionsChars,
bootstrapPromptChars: renderedBootstrapPrompt.length,
wakePromptChars: wakePrompt.length,
sessionHandoffChars: sessionHandoffNote.length,
runtimeNoteChars: paperclipEnvNote.length,
heartbeatPromptChars: renderedPrompt.length,

View file

@ -22,6 +22,8 @@ import {
removeMaintainerOnlySkillSymlinks,
parseObject,
renderTemplate,
renderPaperclipWakePrompt,
stringifyPaperclipWakePayload,
runChildProcess,
} from "@paperclipai/adapter-utils/server-utils";
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
@ -193,12 +195,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const linkedIssueIds = Array.isArray(context.issueIds)
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
: [];
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
if (wakeReason) env.PAPERCLIP_WAKE_REASON = wakeReason;
if (wakeCommentId) env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId;
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
if (effectiveWorkspaceCwd) env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
if (workspaceSource) env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource;
if (workspaceId) env.PAPERCLIP_WORKSPACE_ID = workspaceId;
@ -295,17 +299,20 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
run: { id: runId, source: "on_demand" },
context,
};
const renderedPrompt = renderTemplate(promptTemplate, templateData);
const renderedBootstrapPrompt =
!sessionId && bootstrapPromptTemplate.trim().length > 0
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
: "";
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
const paperclipEnvNote = renderPaperclipEnvNote(env);
const apiAccessNote = renderApiAccessNote(env);
const prompt = joinPromptSections([
instructionsPrefix,
renderedBootstrapPrompt,
wakePrompt,
sessionHandoffNote,
paperclipEnvNote,
apiAccessNote,
@ -315,6 +322,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
promptChars: prompt.length,
instructionsChars: instructionsPrefix.length,
bootstrapPromptChars: renderedBootstrapPrompt.length,
wakePromptChars: wakePrompt.length,
sessionHandoffChars: sessionHandoffNote.length,
runtimeNoteChars: paperclipEnvNote.length + apiAccessNote.length,
heartbeatPromptChars: renderedPrompt.length,

View file

@ -3,7 +3,14 @@ import type {
AdapterExecutionResult,
AdapterRuntimeServiceReport,
} from "@paperclipai/adapter-utils";
import { asNumber, asString, buildPaperclipEnv, parseObject } from "@paperclipai/adapter-utils/server-utils";
import {
asNumber,
asString,
buildPaperclipEnv,
parseObject,
renderPaperclipWakePrompt,
stringifyPaperclipWakePayload,
} from "@paperclipai/adapter-utils/server-utils";
import crypto, { randomUUID } from "node:crypto";
import { WebSocket } from "ws";
@ -341,7 +348,12 @@ function buildPaperclipEnvForWake(ctx: AdapterExecutionContext, wakePayload: Wak
return paperclipEnv;
}
function buildWakeText(payload: WakePayload, paperclipEnv: Record<string, string>, claimedApiKeyPath: string): string {
function buildWakeText(
payload: WakePayload,
paperclipEnv: Record<string, string>,
structuredWakePrompt: string,
): string {
const claimedApiKeyPath = "~/.openclaw/workspace/paperclip-claimed-api-key.json";
const orderedKeys = [
"PAPERCLIP_RUN_ID",
"PAPERCLIP_AGENT_ID",
@ -409,6 +421,12 @@ function buildWakeText(payload: WakePayload, paperclipEnv: Record<string, string
"- POST /api/issues/{issueId}/comments",
"- PATCH /api/issues/{issueId}",
"- POST /api/companies/{companyId}/issues (when asked to create a new issue)",
...(structuredWakePrompt
? [
"",
structuredWakePrompt,
]
: []),
"",
"Complete the workflow in this run.",
];
@ -420,6 +438,17 @@ function appendWakeText(baseText: string, wakeText: string): string {
return trimmedBase.length > 0 ? `${trimmedBase}\n\n${wakeText}` : wakeText;
}
function joinWakePayloadSections(structuredWakePrompt: string, structuredWakeJson: string): string {
const sections = [
structuredWakePrompt.trim(),
"Structured wake payload JSON:",
"```json",
structuredWakeJson,
"```",
].filter((entry) => entry.trim().length > 0);
return sections.join("\n");
}
function buildStandardPaperclipPayload(
ctx: AdapterExecutionContext,
wakePayload: WakePayload,
@ -452,6 +481,10 @@ function buildStandardPaperclipPayload(
approvalStatus: wakePayload.approvalStatus,
apiUrl: paperclipEnv.PAPERCLIP_API_URL ?? null,
};
const structuredWake = parseObject(ctx.context.paperclipWake);
if (Object.keys(structuredWake).length > 0) {
standardPaperclip.wake = structuredWake;
}
if (workspace) {
standardPaperclip.workspace = workspace;
@ -1058,8 +1091,15 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const wakePayload = buildWakePayload(ctx);
const paperclipEnv = buildPaperclipEnvForWake(ctx, wakePayload);
const claimedApiKeyPath = resolveClaimedApiKeyPath(ctx.config.claimedApiKeyPath);
const wakeText = buildWakeText(wakePayload, paperclipEnv, claimedApiKeyPath);
const structuredWakePrompt = renderPaperclipWakePrompt(ctx.context.paperclipWake);
const structuredWakeJson = stringifyPaperclipWakePayload(ctx.context.paperclipWake);
const wakeText = buildWakeText(
wakePayload,
paperclipEnv,
structuredWakeJson
? joinWakePayloadSections(structuredWakePrompt, structuredWakeJson)
: structuredWakePrompt,
);
const sessionKeyStrategy = normalizeSessionKeyStrategy(ctx.config.sessionKeyStrategy);
const configuredSessionKey = nonEmpty(ctx.config.sessionKey);
@ -1081,6 +1121,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
idempotencyKey: ctx.runId,
};
delete agentParams.text;
agentParams.paperclip = paperclipPayload;
const configuredAgentId = nonEmpty(ctx.config.agentId);
if (configuredAgentId && !nonEmpty(agentParams.agentId)) {

View file

@ -17,6 +17,8 @@ import {
ensurePathInEnv,
resolveCommandForLogs,
renderTemplate,
renderPaperclipWakePrompt,
stringifyPaperclipWakePayload,
runChildProcess,
readPaperclipRuntimeSkillEntries,
resolvePaperclipDesiredSkillNames,
@ -154,12 +156,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const linkedIssueIds = Array.isArray(context.issueIds)
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
: [];
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
if (wakeReason) env.PAPERCLIP_WAKE_REASON = wakeReason;
if (wakeCommentId) env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId;
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
if (effectiveWorkspaceCwd) env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
if (workspaceSource) env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource;
if (workspaceId) env.PAPERCLIP_WORKSPACE_ID = workspaceId;
@ -222,7 +226,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
`[paperclip] OpenCode session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`,
);
}
const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
const resolvedInstructionsFilePath = instructionsFilePath
? path.resolve(cwd, instructionsFilePath)
@ -271,15 +274,18 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
run: { id: runId, source: "on_demand" },
context,
};
const renderedPrompt = renderTemplate(promptTemplate, templateData);
const renderedBootstrapPrompt =
!sessionId && bootstrapPromptTemplate.trim().length > 0
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
: "";
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
const prompt = joinPromptSections([
instructionsPrefix,
renderedBootstrapPrompt,
wakePrompt,
sessionHandoffNote,
renderedPrompt,
]);
@ -287,6 +293,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
promptChars: prompt.length,
instructionsChars: instructionsPrefix.length,
bootstrapPromptChars: renderedBootstrapPrompt.length,
wakePromptChars: wakePrompt.length,
sessionHandoffChars: sessionHandoffNote.length,
heartbeatPromptChars: renderedPrompt.length,
};

View file

@ -20,6 +20,8 @@ import {
resolvePaperclipDesiredSkillNames,
removeMaintainerOnlySkillSymlinks,
renderTemplate,
renderPaperclipWakePrompt,
stringifyPaperclipWakePayload,
runChildProcess,
} from "@paperclipai/adapter-utils/server-utils";
import { isPiUnknownSessionError, parsePiJsonl } from "./parse.js";
@ -177,6 +179,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const linkedIssueIds = Array.isArray(context.issueIds)
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
: [];
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
if (wakeReason) env.PAPERCLIP_WAKE_REASON = wakeReason;
@ -184,6 +187,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
if (workspaceCwd) env.PAPERCLIP_WORKSPACE_CWD = workspaceCwd;
if (workspaceSource) env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource;
if (workspaceId) env.PAPERCLIP_WORKSPACE_ID = workspaceId;
@ -298,14 +302,17 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
context,
};
const renderedSystemPromptExtension = renderTemplate(systemPromptExtension, templateData);
const renderedHeartbeatPrompt = renderTemplate(promptTemplate, templateData);
const renderedBootstrapPrompt =
!canResumeSession && bootstrapPromptTemplate.trim().length > 0
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
: "";
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: canResumeSession });
const shouldUseResumeDeltaPrompt = canResumeSession && wakePrompt.length > 0;
const renderedHeartbeatPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
const userPrompt = joinPromptSections([
renderedBootstrapPrompt,
wakePrompt,
sessionHandoffNote,
renderedHeartbeatPrompt,
]);
@ -313,6 +320,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
systemPromptChars: renderedSystemPromptExtension.length,
promptChars: userPrompt.length,
bootstrapPromptChars: renderedBootstrapPrompt.length,
wakePromptChars: wakePrompt.length,
sessionHandoffChars: sessionHandoffNote.length,
heartbeatPromptChars: renderedHeartbeatPrompt.length,
};

View file

@ -606,7 +606,7 @@ export interface WorkerToHostMethods {
result: IssueComment[],
];
"issues.createComment": [
params: { issueId: string; body: string; companyId: string },
params: { issueId: string; body: string; companyId: string; authorAgentId?: string },
result: IssueComment,
];

View file

@ -405,7 +405,7 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
if (!isInCompany(issues.get(issueId), companyId)) return [];
return issueComments.get(issueId) ?? [];
},
async createComment(issueId, body, companyId) {
async createComment(issueId, body, companyId, options) {
requireCapability(manifest, capabilitySet, "issue.comments.create");
const parentIssue = issues.get(issueId);
if (!isInCompany(parentIssue, companyId)) {
@ -416,7 +416,7 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
id: randomUUID(),
companyId: parentIssue.companyId,
issueId,
authorAgentId: null,
authorAgentId: options?.authorAgentId ?? null,
authorUserId: null,
body,
createdAt: now,

View file

@ -909,7 +909,12 @@ export interface PluginIssuesClient {
companyId: string,
): Promise<Issue>;
listComments(issueId: string, companyId: string): Promise<IssueComment[]>;
createComment(issueId: string, body: string, companyId: string): Promise<IssueComment>;
createComment(
issueId: string,
body: string,
companyId: string,
options?: { authorAgentId?: string },
): Promise<IssueComment>;
/** Read and write issue documents. Requires `issue.documents.read` / `issue.documents.write`. */
documents: PluginIssueDocumentsClient;
}

View file

@ -610,8 +610,8 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
return callHost("issues.listComments", { issueId, companyId });
},
async createComment(issueId: string, body: string, companyId: string) {
return callHost("issues.createComment", { issueId, body, companyId });
async createComment(issueId: string, body: string, companyId: string, options?: { authorAgentId?: string }) {
return callHost("issues.createComment", { issueId, body, companyId, authorAgentId: options?.authorAgentId });
},
documents: {

View file

@ -0,0 +1,15 @@
import { z } from "zod";
import { AGENT_ADAPTER_TYPES } from "./constants.js";
export const agentAdapterTypeSchema = z
.string()
.trim()
.min(1)
.default("process")
.describe(`Known built-in adapters: ${AGENT_ADAPTER_TYPES.join(", ")}. External adapters may register additional non-empty string types at runtime.`);
export const optionalAgentAdapterTypeSchema = z
.string()
.trim()
.min(1)
.optional();

View file

@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { acceptInviteSchema, createAgentSchema, updateAgentSchema } from "./index.js";
describe("dynamic adapter type validation schemas", () => {
it("accepts external adapter types in create/update agent schemas", () => {
expect(
createAgentSchema.parse({
name: "External Agent",
adapterType: "external_adapter",
}).adapterType,
).toBe("external_adapter");
expect(
updateAgentSchema.parse({
adapterType: "external_adapter",
}).adapterType,
).toBe("external_adapter");
});
it("still rejects blank adapter types", () => {
expect(() =>
createAgentSchema.parse({
name: "Blank Adapter",
adapterType: " ",
}),
).toThrow();
});
it("accepts external adapter types in invite acceptance schema", () => {
expect(
acceptInviteSchema.parse({
requestType: "agent",
agentName: "External Joiner",
adapterType: "external_adapter",
}).adapterType,
).toBe("external_adapter");
});
});

View file

@ -31,9 +31,8 @@ export const AGENT_ADAPTER_TYPES = [
"pi_local",
"cursor",
"openclaw_gateway",
"hermes_local",
] as const;
export type AgentAdapterType = (typeof AGENT_ADAPTER_TYPES)[number];
export type AgentAdapterType = (typeof AGENT_ADAPTER_TYPES)[number] | (string & {});
export const AGENT_ROLES = [
"ceo",

View file

@ -0,0 +1,19 @@
import type { ExecutionWorkspace } from "./types/workspace-runtime.js";
type ExecutionWorkspaceGuardTarget = Pick<ExecutionWorkspace, "closedAt" | "mode" | "name" | "status">;
const CLOSED_EXECUTION_WORKSPACE_STATUSES = new Set<ExecutionWorkspace["status"]>(["archived", "cleanup_failed"]);
export function isClosedIsolatedExecutionWorkspace(
workspace: Pick<ExecutionWorkspaceGuardTarget, "closedAt" | "mode" | "status"> | null | undefined,
): boolean {
if (!workspace) return false;
if (workspace.mode !== "isolated_workspace") return false;
return workspace.closedAt != null || CLOSED_EXECUTION_WORKSPACE_STATUSES.has(workspace.status);
}
export function getClosedIsolatedExecutionWorkspaceMessage(
workspace: Pick<ExecutionWorkspaceGuardTarget, "name">,
): string {
return `This issue is linked to the closed workspace "${workspace.name}". Move it to an open workspace before adding comments or resuming work.`;
}

View file

@ -1,3 +1,4 @@
export { agentAdapterTypeSchema, optionalAgentAdapterTypeSchema } from "./adapter-type.js";
export {
COMPANY_STATUSES,
DEPLOYMENT_MODES,
@ -350,6 +351,11 @@ export {
DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION,
} from "./types/feedback.js";
export {
getClosedIsolatedExecutionWorkspaceMessage,
isClosedIsolatedExecutionWorkspace,
} from "./execution-workspace-guards.js";
export {
instanceGeneralSettingsSchema,
patchInstanceGeneralSettingsSchema,
@ -594,14 +600,19 @@ export { deriveProjectUrlKey, normalizeProjectUrlKey, hasNonAsciiContent } from
export {
AGENT_MENTION_SCHEME,
PROJECT_MENTION_SCHEME,
SKILL_MENTION_SCHEME,
buildAgentMentionHref,
buildProjectMentionHref,
buildSkillMentionHref,
extractAgentMentionIds,
extractSkillMentionIds,
parseAgentMentionHref,
parseProjectMentionHref,
parseSkillMentionHref,
extractProjectMentionIds,
type ParsedAgentMention,
type ParsedProjectMention,
type ParsedSkillMention,
} from "./project-mentions.js";
export {

View file

@ -2,10 +2,13 @@ import { describe, expect, it } from "vitest";
import {
buildAgentMentionHref,
buildProjectMentionHref,
buildSkillMentionHref,
extractAgentMentionIds,
extractProjectMentionIds,
extractSkillMentionIds,
parseAgentMentionHref,
parseProjectMentionHref,
parseSkillMentionHref,
} from "./project-mentions.js";
describe("project-mentions", () => {
@ -26,4 +29,13 @@ describe("project-mentions", () => {
});
expect(extractAgentMentionIds(`[@CodexCoder](${href})`)).toEqual(["agent-123"]);
});
it("round-trips skill mentions with slug metadata", () => {
const href = buildSkillMentionHref("skill-123", "release-changelog");
expect(parseSkillMentionHref(href)).toEqual({
skillId: "skill-123",
slug: "release-changelog",
});
expect(extractSkillMentionIds(`[/release-changelog](${href})`)).toEqual(["skill-123"]);
});
});

View file

@ -1,5 +1,6 @@
export const PROJECT_MENTION_SCHEME = "project://";
export const AGENT_MENTION_SCHEME = "agent://";
export const SKILL_MENTION_SCHEME = "skill://";
const HEX_COLOR_RE = /^[0-9a-f]{6}$/i;
const HEX_COLOR_SHORT_RE = /^[0-9a-f]{3}$/i;
@ -7,7 +8,9 @@ const HEX_COLOR_WITH_HASH_RE = /^#[0-9a-f]{6}$/i;
const HEX_COLOR_SHORT_WITH_HASH_RE = /^#[0-9a-f]{3}$/i;
const PROJECT_MENTION_LINK_RE = /\[[^\]]*]\((project:\/\/[^)\s]+)\)/gi;
const AGENT_MENTION_LINK_RE = /\[[^\]]*]\((agent:\/\/[^)\s]+)\)/gi;
const SKILL_MENTION_LINK_RE = /\[[^\]]*]\((skill:\/\/[^)\s]+)\)/gi;
const AGENT_ICON_NAME_RE = /^[a-z0-9-]+$/i;
const SKILL_SLUG_RE = /^[a-z0-9][a-z0-9-]*$/i;
export interface ParsedProjectMention {
projectId: string;
@ -19,6 +22,11 @@ export interface ParsedAgentMention {
icon: string | null;
}
export interface ParsedSkillMention {
skillId: string;
slug: string | null;
}
function normalizeHexColor(input: string | null | undefined): string | null {
if (!input) return null;
const trimmed = input.trim();
@ -103,6 +111,36 @@ export function parseAgentMentionHref(href: string): ParsedAgentMention | null {
};
}
export function buildSkillMentionHref(skillId: string, slug?: string | null): string {
const trimmedSkillId = skillId.trim();
const normalizedSlug = normalizeSkillSlug(slug ?? null);
if (!normalizedSlug) {
return `${SKILL_MENTION_SCHEME}${trimmedSkillId}`;
}
return `${SKILL_MENTION_SCHEME}${trimmedSkillId}?s=${encodeURIComponent(normalizedSlug)}`;
}
export function parseSkillMentionHref(href: string): ParsedSkillMention | null {
if (!href.startsWith(SKILL_MENTION_SCHEME)) return null;
let url: URL;
try {
url = new URL(href);
} catch {
return null;
}
if (url.protocol !== "skill:") return null;
const skillId = `${url.hostname}${url.pathname}`.replace(/^\/+/, "").trim();
if (!skillId) return null;
return {
skillId,
slug: normalizeSkillSlug(url.searchParams.get("s") ?? url.searchParams.get("slug")),
};
}
export function extractProjectMentionIds(markdown: string): string[] {
if (!markdown) return [];
const ids = new Set<string>();
@ -127,9 +165,28 @@ export function extractAgentMentionIds(markdown: string): string[] {
return [...ids];
}
export function extractSkillMentionIds(markdown: string): string[] {
if (!markdown) return [];
const ids = new Set<string>();
const re = new RegExp(SKILL_MENTION_LINK_RE);
let match: RegExpExecArray | null;
while ((match = re.exec(markdown)) !== null) {
const parsed = parseSkillMentionHref(match[1]);
if (parsed) ids.add(parsed.skillId);
}
return [...ids];
}
function normalizeAgentIcon(input: string | null | undefined): string | null {
if (!input) return null;
const trimmed = input.trim().toLowerCase();
if (!trimmed || !AGENT_ICON_NAME_RE.test(trimmed)) return null;
return trimmed;
}
function normalizeSkillSlug(input: string | null | undefined): string | null {
if (!input) return null;
const trimmed = input.trim().toLowerCase();
if (!trimmed || !SKILL_SLUG_RE.test(trimmed)) return null;
return trimmed;
}

View file

@ -58,6 +58,7 @@ export class TelemetryClient {
app,
schemaVersion,
installId: state.installId,
version: this.version,
events,
}),
signal: controller.signal,

View file

@ -23,6 +23,48 @@ export function trackCompanyImported(
});
}
export function trackProjectCreated(client: TelemetryClient): void {
client.track("project.created");
}
export function trackRoutineCreated(client: TelemetryClient): void {
client.track("routine.created");
}
export function trackRoutineRun(
client: TelemetryClient,
dims: { source: string; status: string },
): void {
client.track("routine.run", {
source: dims.source,
status: dims.status,
});
}
export function trackGoalCreated(
client: TelemetryClient,
dims?: { goalLevel?: string | null },
): void {
client.track("goal.created", dims?.goalLevel ? { goal_level: dims.goalLevel } : undefined);
}
export function trackAgentCreated(
client: TelemetryClient,
dims: { agentRole: string },
): void {
client.track("agent.created", { agent_role: dims.agentRole });
}
export function trackSkillImported(
client: TelemetryClient,
dims: { sourceType: string; skillRef?: string | null },
): void {
client.track("skill.imported", {
source_type: dims.sourceType,
...(dims.skillRef ? { skill_ref: dims.skillRef } : {}),
});
}
export function trackAgentFirstHeartbeat(
client: TelemetryClient,
dims: { agentRole: string },

View file

@ -5,6 +5,12 @@ export {
trackInstallStarted,
trackInstallCompleted,
trackCompanyImported,
trackProjectCreated,
trackRoutineCreated,
trackRoutineRun,
trackGoalCreated,
trackAgentCreated,
trackSkillImported,
trackAgentFirstHeartbeat,
trackAgentTaskCompleted,
trackErrorHandlerCrash,

View file

@ -24,6 +24,7 @@ export interface TelemetryEventEnvelope {
app: string;
schemaVersion: string;
installId: string;
version: string;
events: TelemetryEvent[];
}
@ -31,6 +32,12 @@ export type TelemetryEventName =
| "install.started"
| "install.completed"
| "company.imported"
| "project.created"
| "routine.created"
| "routine.run"
| "goal.created"
| "agent.created"
| "skill.imported"
| "agent.first_heartbeat"
| "agent.task_completed"
| "error.handler_crash"

View file

@ -1,11 +1,11 @@
import { z } from "zod";
import {
AGENT_ADAPTER_TYPES,
INVITE_JOIN_TYPES,
JOIN_REQUEST_STATUSES,
JOIN_REQUEST_TYPES,
PERMISSION_KEYS,
} from "../constants.js";
import { optionalAgentAdapterTypeSchema } from "../adapter-type.js";
export const createCompanyInviteSchema = z.object({
allowedJoinTypes: z.enum(INVITE_JOIN_TYPES).default("both"),
@ -26,7 +26,7 @@ export type CreateOpenClawInvitePrompt = z.infer<
export const acceptInviteSchema = z.object({
requestType: z.enum(JOIN_REQUEST_TYPES),
agentName: z.string().min(1).max(120).optional(),
adapterType: z.enum(AGENT_ADAPTER_TYPES).optional(),
adapterType: optionalAgentAdapterTypeSchema,
capabilities: z.string().max(4000).optional().nullable(),
agentDefaultsPayload: z.record(z.string(), z.unknown()).optional().nullable(),
// OpenClaw join compatibility fields accepted at top level.

View file

@ -1,11 +1,11 @@
import { z } from "zod";
import {
AGENT_ADAPTER_TYPES,
AGENT_ICON_NAMES,
AGENT_ROLES,
AGENT_STATUSES,
INBOX_MINE_ISSUE_STATUS_FILTER,
} from "../constants.js";
import { agentAdapterTypeSchema } from "../adapter-type.js";
import { envConfigSchema } from "./secret.js";
export const agentPermissionsSchema = z.object({
@ -52,7 +52,7 @@ export const createAgentSchema = z.object({
reportsTo: z.string().uuid().optional().nullable(),
capabilities: z.string().optional().nullable(),
desiredSkills: z.array(z.string().min(1)).optional(),
adapterType: z.enum(AGENT_ADAPTER_TYPES).optional().default("process"),
adapterType: agentAdapterTypeSchema,
adapterConfig: adapterConfigSchema.optional().default({}),
runtimeConfig: z.record(z.unknown()).optional().default({}),
budgetMonthlyCents: z.number().int().nonnegative().optional().default(0),