import { randomUUID } from 'crypto'; import { resolve as resolvePath } from 'path'; import { inferProvider } from '../cost-table'; import { canonicalJson, computeSignerFingerprint, sha256Hex } from '../decision-log'; import { signDagNode, verifyReceiptDag, type ReceiptCapabilityLevel, type ReceiptDagEnvelope, type ReceiptDagNode } from '../receipts/dag'; import { AgentGuardBlockedError, SpendGuard, type SpendGuardConfig } from '../spend-guard'; import type { CallContext, CapabilityTier, Provider, SignedDecisionLogEntry, SpendDecision, SpendPolicy, SpendScope } from '../types'; import { safeRequestShape } from './common'; export type HermesKanbanProfile = 'researcher' | 'writer' | 'reviewer' | 'director' | 'orchestrator' | string; export type HermesKanbanTaskState = 'triage' | 'blocked' | 'scheduled' | 'running' | 'complete' | 'revoked'; export type HermesKanbanGovernanceMode = 'lightweight' | 'full'; export interface HermesKanbanProfilePolicy { capability: ReceiptCapabilityLevel; toolAllowlist?: string[]; capCents?: number; } export interface HermesKanbanAdapterOptions { policy: SpendPolicy; scope: SpendScope; config?: Omit; licenseKey?: string; defaultModel?: string; defaultCapCents?: number; runtimeBudgetCentsPerMinute?: number; lightweightThresholdCents?: number; lightweightCapabilityCeiling?: ReceiptCapabilityLevel; profileMap?: Record; reviewerCascade?: { enabled?: boolean; highRiskThreshold?: number; drafterModel?: string; reviewerModel?: string; drafterMaxCostCents?: number; reviewerMaxCostCents?: number; }; } export interface HermesKanbanTaskInput { taskId: string; boardSlug: string; profile: HermesKanbanProfile; parentTaskIds?: string[]; workspacePath?: string; maxRuntimeMinutes?: number; maxRuntimeMs?: number; metadata?: Record; builderCode?: string; } export interface HermesKanbanEnvelope { taskId: string; taskHash: string; boardHash: string; profile: string; state: HermesKanbanTaskState; capabilityCeiling: ReceiptCapabilityLevel; toolAllowlist: string[]; capCents: number; parentTaskIds: string[]; parentEntryHashes: string[]; workspacePath?: string; governanceMode: HermesKanbanGovernanceMode; scope: SpendScope; builderCode?: string; createdAt: string; revokedAt?: string; completedAt?: string; } export interface HermesKanbanDispatchInput { taskId: string; model?: string; provider?: Provider; inputTokens: number; outputTokens: number; metadata?: Record; } export interface HermesKanbanDispatchResult { allow: boolean; decision: SpendDecision; signed: SignedDecisionLogEntry | null; } export interface HermesKanbanCapabilityInput { taskId: string; requestedCapability: ReceiptCapabilityLevel; toolName?: string; toolArgs?: Record; } export interface HermesKanbanCapabilityResult { approved: boolean; reason?: string; signed?: SignedDecisionLogEntry | null; decision?: SpendDecision; editedArgs?: Record; } export interface HermesKanbanReceiptInput { taskId: string; parentTaskIds?: string[]; capability?: ReceiptCapabilityLevel; modelIds?: string[]; tokenCounts?: { inputTokens?: number; outputTokens?: number }; totalCostCents?: number; metadata?: Record; workspacePath?: string; highRiskClassifierScore?: number; keywordRisk?: boolean; reviewerVerdict?: 'drafter_only' | 'review_required' | 'approved' | 'rejected'; } export interface HermesKanbanReceiptResult { node: ReceiptDagNode; envelope: HermesKanbanEnvelope; dag: ReceiptDagEnvelope; verification?: Awaited>; } const DEFAULT_PROFILE_MAP: Record = { researcher: { capability: 'READ_ONLY', toolAllowlist: ['web.fetch', 'file.read', 'notes.read', 'search.query'] }, writer: { capability: 'TRANSACT', toolAllowlist: ['draft.create', 'file.write', 'notes.write', 'email.draft'] }, reviewer: { capability: 'READ_ONLY', toolAllowlist: ['review.read', 'notes.read', 'decision.record'] }, director: { capability: 'ORCHESTRATE', toolAllowlist: ['task.assign', 'board.plan', 'decision.record'] }, orchestrator: { capability: 'ORCHESTRATE', toolAllowlist: ['task.assign', 'board.plan', 'decision.record'] }, }; const CAPABILITY_RANK: Record = { READ_ONLY: 0, TRANSACT: 1, ADMIN: 2, ORCHESTRATE: 3, }; const DATA_PLANE_KEYS = /^(title|summary|prompt|completion|content|messages|input|output|text|raw|source|notes|draft|body|transcript)$/i; const SAFE_METADATA_KEYS = new Set([ 'assignee', 'board_hash', 'duration_ms', 'event', 'model', 'model_id', 'outcome', 'priority', 'profile', 'provider', 'risk_score', 'state', 'task_id', 'ticket_id', 'token_count', 'workflow_id', 'workspace_hash', 'workspace_path', ]); export function createHermesKanbanAdapter(opts: HermesKanbanAdapterOptions): HermesKanbanAdapter { return new HermesKanbanAdapter(opts); } export class HermesKanbanAdapter { private readonly taskGuards = new Map(); private readonly envelopes = new Map(); private readonly nodesByTaskId = new Map(); private readonly nodes: ReceiptDagNode[] = []; private readonly profileMap: Record; private readonly config: Omit; constructor(private readonly opts: HermesKanbanAdapterOptions) { this.profileMap = { ...DEFAULT_PROFILE_MAP, ...(opts.profileMap ?? {}) }; this.config = opts.config ?? {}; } governTask(input: HermesKanbanTaskInput): HermesKanbanEnvelope { assertTaskInput(input); const profilePolicy = this.profilePolicy(input.profile); const capCents = capForTask(input, profilePolicy, this.opts); const boardHash = hashValue(input.boardSlug); const taskHash = hashValue(input.taskId); const scope: SpendScope = { ...this.opts.scope, taskId: input.taskId }; const envelope: HermesKanbanEnvelope = { taskId: input.taskId, taskHash, boardHash, profile: normalizeProfile(input.profile), state: 'running', capabilityCeiling: profilePolicy.capability, toolAllowlist: [...(profilePolicy.toolAllowlist ?? [])], capCents, parentTaskIds: [...new Set(input.parentTaskIds ?? [])], parentEntryHashes: [], workspacePath: resolveWorkspacePath(input.workspacePath), governanceMode: governanceModeFor(profilePolicy.capability, capCents, this.opts), scope, builderCode: safeBuilderCode(input.builderCode), createdAt: new Date().toISOString(), }; const taskPolicy: SpendPolicy = { ...this.opts.policy, id: `${this.opts.policy.id}:hermes-kanban:${taskHash.slice(0, 12)}`, name: `${this.opts.policy.name} Hermes Kanban task`, scope, caps: [{ window: 'per_day', amountCents: capCents, action: 'block', reason: 'Hermes Kanban task cap' }], requiredCapability: spendCapabilityForReceiptCapability(profilePolicy.capability), }; this.envelopes.set(input.taskId, envelope); this.taskGuards.set(input.taskId, new SpendGuard({ policy: taskPolicy, ...this.config, licenseKey: this.opts.licenseKey ?? this.config.licenseKey, })); return { ...envelope }; } async enforceProviderCall(input: HermesKanbanDispatchInput): Promise { const envelope = this.requireEnvelope(input.taskId); assertActive(envelope); assertMetadataOnly(input.metadata ?? {}); validateTokens(input.inputTokens, input.outputTokens); const guard = this.requireGuard(input.taskId); const model = safeLabel(input.model) ?? this.opts.defaultModel ?? 'openai/gpt-4o-mini'; const call: CallContext = { provider: input.provider ?? inferProvider(model), model, inputTokens: input.inputTokens, outputTokens: input.outputTokens, scope: envelope.scope, workflowId: envelope.boardHash, subagentId: envelope.taskId, label: 'hermes-kanban', capabilityClaim: spendCapabilityForReceiptCapability(envelope.capabilityCeiling), requestShape: safeRequestShape({ framework: 'hermes-kanban', event: 'provider-call', taskId: envelope.taskId, taskHash: envelope.taskHash, boardHash: envelope.boardHash, profile: envelope.profile, metadata: filterReceiptMetadata(input.metadata ?? {}), }), }; const { decision, signed } = await guard.decide(call); return { allow: decision.action !== 'block', decision, signed }; } async guardCapability(input: HermesKanbanCapabilityInput): Promise { const envelope = this.requireEnvelope(input.taskId); assertActive(envelope); assertMetadataOnly(input.toolArgs ?? {}); if (CAPABILITY_RANK[input.requestedCapability] > CAPABILITY_RANK[envelope.capabilityCeiling]) { return { approved: false, reason: `Hermes Kanban profile ${envelope.profile} is capped at ${envelope.capabilityCeiling}`, }; } if (input.toolName && envelope.toolAllowlist.length > 0 && !toolAllowed(input.toolName, envelope.toolAllowlist)) { return { approved: false, reason: `Tool ${input.toolName} is outside the Hermes Kanban profile allowlist` }; } const gate = await this.requireGuard(input.taskId).guardToolCall({ toolName: input.toolName ?? 'hermes-kanban.tool', toolArgs: safeRequestShape(input.toolArgs ?? {}), workflowId: envelope.boardHash, scope: envelope.scope, }); return { approved: gate.approved, reason: gate.decision.reasons.join('; '), signed: gate.signed, decision: gate.decision, editedArgs: gate.editedArgs, }; } async revokeTask(taskId: string, reason = 'stale_lock_reclaimed'): Promise { const envelope = this.requireEnvelope(taskId); if (envelope.state === 'revoked') return this.receiptResult(envelope, this.requireNode(taskId)); envelope.state = 'revoked'; envelope.revokedAt = new Date().toISOString(); const finalSpendCents = this.spendCentsSoFar(taskId); const node = await this.signTaskNode(envelope, { event: 'revoked', action: 'block', blocked: true, capability: envelope.capabilityCeiling, trustScore: 0.7, revocationReason: safeLabel(reason) ?? 'stale_lock_reclaimed', finalSpendCents, postRevocationSpendCents: 0, }); this.nodesByTaskId.set(taskId, node); this.nodes.push(node); return this.receiptResult(envelope, node); } async emitTaskReceipt(input: HermesKanbanReceiptInput): Promise { const envelope = this.requireEnvelope(input.taskId); assertActive(envelope); assertMetadataOnly(input.metadata ?? {}); if (input.workspacePath) envelope.workspacePath = resolveWorkspacePath(input.workspacePath); envelope.state = 'complete'; envelope.completedAt = new Date().toISOString(); const parentTaskIds = [...new Set(input.parentTaskIds ?? envelope.parentTaskIds)]; const parentNodes = parentTaskIds.map((id) => this.nodesByTaskId.get(id)).filter(Boolean) as ReceiptDagNode[]; envelope.parentEntryHashes = parentNodes.map((node) => node.entryHash); const capability = input.capability ?? envelope.capabilityCeiling; const triggers = reviewerCascadeTriggers(envelope, input, this.opts); const node = await this.signTaskNode(envelope, { event: 'completed', action: 'allow', blocked: false, capability, trustScore: triggers.length > 0 ? 0.9 : 0.94, parentNodes, metadata: input.metadata, modelIds: input.modelIds, tokenCounts: input.tokenCounts, totalCostCents: input.totalCostCents, reviewerCascade: triggers.length > 0 ? buildReviewerCascadeReceipt(input, this.opts, triggers) : undefined, }); this.nodesByTaskId.set(input.taskId, node); this.nodes.push(node); return this.receiptResult(envelope, node); } getEnvelope(taskId: string): HermesKanbanEnvelope | null { const envelope = this.envelopes.get(taskId); return envelope ? { ...envelope, parentTaskIds: [...envelope.parentTaskIds], parentEntryHashes: [...envelope.parentEntryHashes], toolAllowlist: [...envelope.toolAllowlist] } : null; } getReceiptDag(rootTaskId?: string): ReceiptDagEnvelope { const root = rootTaskId ? this.nodesByTaskId.get(rootTaskId) : this.nodes[this.nodes.length - 1]; return { nodes: [...this.nodes], rootId: root?.entryHash ?? '0'.repeat(64), workflowId: this.nodes[0]?.workflowId ?? null, }; } outboundPayloads(): Record[] { return []; } async verifyDag(publicKeyHex?: string): Promise>> { const key = publicKeyHex ?? this.publicKeyHex(); return verifyReceiptDag(this.getReceiptDag(), key); } private async signTaskNode( envelope: HermesKanbanEnvelope, args: { event: 'completed' | 'revoked'; action: 'allow' | 'block'; blocked: boolean; capability: ReceiptCapabilityLevel; trustScore: number; parentNodes?: ReceiptDagNode[]; metadata?: Record; modelIds?: string[]; tokenCounts?: { inputTokens?: number; outputTokens?: number }; totalCostCents?: number; reviewerCascade?: Record; revocationReason?: string; finalSpendCents?: number; postRevocationSpendCents?: number; }, ): Promise { if (!this.config.signingKeys) throw new Error('Hermes Kanban receipts require signingKeys'); const parentNodes = args.parentNodes ?? []; const decision = taskDecision(envelope, args, this.opts.policy); return signDagNode({ sequence: this.nodes.length, decision, privateKey: this.config.signingKeys.privateKey, publicKey: this.config.signingKeys.publicKey, parents: parentNodes, workflowId: envelope.boardHash, trustScore: args.trustScore, blocked: args.blocked, capability: args.capability, builderCode: envelope.builderCode ?? null, }); } private receiptResult(envelope: HermesKanbanEnvelope, node: ReceiptDagNode): HermesKanbanReceiptResult { return { node, envelope: { ...envelope, parentTaskIds: [...envelope.parentTaskIds], parentEntryHashes: [...envelope.parentEntryHashes], toolAllowlist: [...envelope.toolAllowlist] }, dag: this.getReceiptDag(envelope.taskId), }; } private spendCentsSoFar(taskId: string): number { let total = 0; for (const node of this.nodes) { const receipt = node.decision?.outcomeReceipt as Record | undefined; if (receipt?.taskId === taskId) total += safeNonNegativeInteger(receipt.totalCostCents); } return total; } private requireEnvelope(taskId: string): HermesKanbanEnvelope { const envelope = this.envelopes.get(taskId); if (!envelope) throw new Error(`Unknown Hermes Kanban task ${taskId}`); return envelope; } private requireGuard(taskId: string): SpendGuard { const guard = this.taskGuards.get(taskId); if (!guard) throw new Error(`Unknown Hermes Kanban task ${taskId}`); return guard; } private requireNode(taskId: string): ReceiptDagNode { const node = this.nodesByTaskId.get(taskId); if (!node) throw new Error(`Hermes Kanban task ${taskId} has no receipt`); return node; } private profilePolicy(profile: string): HermesKanbanProfilePolicy { return this.profileMap[normalizeProfile(profile)] ?? { capability: 'READ_ONLY', toolAllowlist: [] }; } private publicKeyHex(): string { if (!this.config.signingKeys) throw new Error('Hermes Kanban receipts require signingKeys'); return Buffer.from(this.config.signingKeys.publicKey).toString('hex'); } } function taskDecision( envelope: HermesKanbanEnvelope, args: { event: 'completed' | 'revoked'; action: 'allow' | 'block'; capability: ReceiptCapabilityLevel; parentNodes?: ReceiptDagNode[]; metadata?: Record; modelIds?: string[]; tokenCounts?: { inputTokens?: number; outputTokens?: number }; totalCostCents?: number; reviewerCascade?: Record; revocationReason?: string; finalSpendCents?: number; postRevocationSpendCents?: number; }, policy: SpendPolicy, ): SpendDecision { const parentNodes = args.parentNodes ?? []; const inputTokens = safeNonNegativeInteger(args.tokenCounts?.inputTokens); const outputTokens = safeNonNegativeInteger(args.tokenCounts?.outputTokens); const totalCostCents = safeNonNegativeInteger(args.totalCostCents); const receipt: Record = { type: 'hermes_kanban_task', event: args.event, taskId: envelope.taskId, taskHash: envelope.taskHash, boardHash: envelope.boardHash, profile: envelope.profile, capability: args.capability, capabilityCeiling: envelope.capabilityCeiling, state: envelope.state, parentTaskIds: [...envelope.parentTaskIds], parentEntryHashes: parentNodes.map((node) => node.entryHash), parentMsgHashes: parentNodes.map((node) => node.msgHash), boardScopeHash: envelope.boardHash, workspaceProvenance: envelope.workspacePath ? { type: 'directory', location: `dir:${envelope.workspacePath}` } : null, governanceMode: envelope.governanceMode, lightweight: envelope.governanceMode === 'lightweight', modelIds: safeModelIds(args.modelIds ?? []), inputTokens, outputTokens, totalCostCents, metadata: filterReceiptMetadata(args.metadata ?? {}), }; if (args.reviewerCascade) receipt.reviewerCascade = args.reviewerCascade; if (args.event === 'revoked') { receipt.revocation = { reason: args.revocationReason ?? 'stale_lock_reclaimed', finalSpendCents: safeNonNegativeInteger(args.finalSpendCents), postRevocationSpendCents: safeNonNegativeInteger(args.postRevocationSpendCents), closedAt: envelope.revokedAt ?? new Date().toISOString(), assertion: 'no_spend_after_revoke', }; } const decision: SpendDecision = { decisionId: randomUUID(), timestamp: new Date().toISOString(), action: args.action, triggeredCap: null, triggeredScopeKey: null, projectedCents: totalCostCents, windowSpendBefore: 0, windowSpendAfter: totalCostCents, provider: 'unknown', modelRequested: 'hermes-kanban', modelResolved: 'hermes-kanban', policyId: policy.id, policyVersion: policy.version, enforcementMode: policy.mode, reasons: [`Hermes Kanban task ${args.event}: ${envelope.taskId}`], entryType: 'outcome', outcomeReceipt: receipt, }; if (envelope.builderCode) decision.builderCode = envelope.builderCode; return decision; } function buildReviewerCascadeReceipt(input: HermesKanbanReceiptInput, opts: HermesKanbanAdapterOptions, triggers: string[]): Record { const cascade = opts.reviewerCascade ?? {}; return { outcome: safeLabel(input.metadata?.outcome) ?? 'hermes_kanban_task', drafter: { model: cascade.drafterModel ?? opts.defaultModel ?? 'openai/gpt-4o-mini', maxCostCents: safeNonNegativeInteger(cascade.drafterMaxCostCents ?? 25), }, reviewer: { model: cascade.reviewerModel ?? 'anthropic/claude-sonnet-4-6', maxCostCents: safeNonNegativeInteger(cascade.reviewerMaxCostCents ?? 75), }, triggerFired: triggers, reviewerVerdict: input.reviewerVerdict ?? 'review_required', }; } function reviewerCascadeTriggers(envelope: HermesKanbanEnvelope, input: HermesKanbanReceiptInput, opts: HermesKanbanAdapterOptions): string[] { if (opts.reviewerCascade?.enabled === false || envelope.governanceMode === 'lightweight') return []; const threshold = opts.reviewerCascade?.highRiskThreshold ?? 0.55; const triggers: string[] = []; if (typeof input.highRiskClassifierScore === 'number' && input.highRiskClassifierScore >= threshold) triggers.push(`risk_score_above:${threshold}`); if (input.keywordRisk === true) triggers.push('keyword_prefilter'); return [...new Set(triggers)]; } function governanceModeFor(capability: ReceiptCapabilityLevel, capCents: number, opts: HermesKanbanAdapterOptions): HermesKanbanGovernanceMode { const threshold = opts.lightweightThresholdCents ?? 5; const ceiling = opts.lightweightCapabilityCeiling ?? 'READ_ONLY'; return CAPABILITY_RANK[capability] <= CAPABILITY_RANK[ceiling] && capCents <= threshold ? 'lightweight' : 'full'; } function resolveWorkspacePath(value: unknown): string | undefined { if (typeof value !== 'string') return undefined; const trimmed = value.trim(); if (!trimmed) return undefined; if (trimmed.length > 1024 || /[\u0000-\u001f\u007f]/.test(trimmed)) throw new Error('Hermes Kanban workspacePath must be a metadata path'); return resolvePath(trimmed); } function capForTask(input: HermesKanbanTaskInput, profilePolicy: HermesKanbanProfilePolicy, opts: HermesKanbanAdapterOptions): number { if (Number.isSafeInteger(profilePolicy.capCents) && profilePolicy.capCents! >= 0) return profilePolicy.capCents!; const runtimeMinutes = runtimeMinutesFor(input); const runtimeBudget = opts.runtimeBudgetCentsPerMinute ?? 10; const cap = Math.ceil(runtimeMinutes * runtimeBudget); return Math.max(1, Math.min(cap, opts.defaultCapCents ?? cap)); } function runtimeMinutesFor(input: HermesKanbanTaskInput): number { if (Number.isFinite(input.maxRuntimeMinutes) && input.maxRuntimeMinutes! > 0) return input.maxRuntimeMinutes!; if (Number.isFinite(input.maxRuntimeMs) && input.maxRuntimeMs! > 0) return input.maxRuntimeMs! / 60_000; return 5; } function spendCapabilityForReceiptCapability(capability: ReceiptCapabilityLevel): CapabilityTier { if (capability === 'ORCHESTRATE') return 'payment_execute'; if (capability === 'ADMIN') return 'payment_initiate'; if (capability === 'TRANSACT') return 'data_write'; return 'read_only'; } function assertTaskInput(input: HermesKanbanTaskInput): void { assertPlainRecord(input, 'task'); const allowed = new Set(['taskId', 'boardSlug', 'profile', 'parentTaskIds', 'workspacePath', 'maxRuntimeMinutes', 'maxRuntimeMs', 'metadata', 'builderCode']); for (const key of Object.keys(input as unknown as Record)) { if (!allowed.has(key)) throw new Error(`Hermes Kanban task input cannot include data-plane or unknown field: ${key}`); } if (!safeLabel(input.taskId)) throw new Error('Hermes Kanban taskId is required'); if (!safeLabel(input.boardSlug)) throw new Error('Hermes Kanban boardSlug is required'); if (!safeLabel(input.profile)) throw new Error('Hermes Kanban profile is required'); assertMetadataOnly(input.metadata ?? {}); for (const parent of input.parentTaskIds ?? []) { if (!safeLabel(parent)) throw new Error('Hermes Kanban parent task id must be a metadata identifier'); } } function assertMetadataOnly(value: unknown, path = 'metadata'): void { if (value == null) return; if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) assertMetadataOnly(value[i], `${path}[${i}]`); return; } if (typeof value !== 'object') return; for (const [key, child] of Object.entries(value as Record)) { if (DATA_PLANE_KEYS.test(key)) throw new Error(`Hermes Kanban metadata cannot include data-plane field: ${path}.${key}`); assertMetadataOnly(child, `${path}.${key}`); } } function assertPlainRecord(value: unknown, name: string): void { if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`Hermes Kanban ${name} must be an object`); } function assertActive(envelope: HermesKanbanEnvelope): void { if (envelope.state === 'revoked') throw new AgentGuardBlockedError(blockedDecision(envelope, 'revoked'), envelope.scope); if (envelope.state === 'complete') throw new AgentGuardBlockedError(blockedDecision(envelope, 'complete'), envelope.scope); } function blockedDecision(envelope: HermesKanbanEnvelope, state: string): SpendDecision { return { decisionId: randomUUID(), timestamp: new Date().toISOString(), action: 'block', triggeredCap: null, triggeredScopeKey: null, projectedCents: 0, windowSpendBefore: 0, windowSpendAfter: 0, provider: 'unknown', modelRequested: 'hermes-kanban', modelResolved: 'hermes-kanban', policyId: 'hermes-kanban', policyVersion: 1, enforcementMode: 'enforce', reasons: [`Hermes Kanban task ${envelope.taskId} is ${state}`], }; } function filterReceiptMetadata(metadata: Record): Record { assertMetadataOnly(metadata); const out: Record = {}; for (const [key, value] of Object.entries(metadata)) { if (!SAFE_METADATA_KEYS.has(key)) continue; if (value == null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { out[key] = value; } } return out; } function validateTokens(inputTokens: number, outputTokens: number): void { if (!Number.isSafeInteger(inputTokens) || inputTokens < 0) throw new Error('inputTokens must be a non-negative safe integer'); if (!Number.isSafeInteger(outputTokens) || outputTokens < 0) throw new Error('outputTokens must be a non-negative safe integer'); } function hashValue(value: string): string { return sha256Hex(canonicalJson({ value })); } function normalizeProfile(profile: string): string { return profile.trim().toLowerCase().replace(/[^a-z0-9_.:-]/g, '_'); } function safeLabel(value: unknown): string | undefined { if (typeof value !== 'string') return undefined; const trimmed = value.trim(); if (!trimmed || !/^[A-Za-z0-9_.:/-]{1,160}$/.test(trimmed)) return undefined; return trimmed; } function safeBuilderCode(value: unknown): string | undefined { const label = safeLabel(value); return label ? label.slice(0, 64) : undefined; } function safeModelIds(models: string[]): string[] { return models.map((model) => safeLabel(model)).filter(Boolean) as string[]; } function safeNonNegativeInteger(value: unknown): number { return Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : 0; } function toolAllowed(toolName: string, allowlist: string[]): boolean { return allowlist.some((allowed) => toolName === allowed || toolName.startsWith(`${allowed}.`)); } export function hermesKanbanSignerFingerprint(opts: HermesKanbanAdapterOptions): string | null { return opts.config?.signingKeys ? computeSignerFingerprint(opts.config.signingKeys.publicKey) : null; }