/** * Skill install jobs — `install-skill` as a background job that reports back. * * An agent that lacks a capability runs `install-skill --id `. This * service decides whether it may (trust rule), downloads the skill from the * marketplace when it is not bundled (reusing the marketplace installer), * runs its setup block, and — when the job ends — sends the requesting agent * a message through the normal message queue (`system_event` to its * session, the path event-bus notifications use). The agent is woken by that * message and carries on: tells the user it is ready, then does the task it * paused. * * ## Trust rule * * - **Official** skills (see skill-discovery.service) install without asking, * including the system dependencies their setup block declares. * - **Third-party** skills are refused unless the owner approved: * - the owner acting from the dashboard (`X-Crewly-Caller: dashboard`, no * agent header), or * - an agent passing `approvedByOwner`, which is **verified**: there must be * a genuine owner-authored chat message in the last * OWNER_APPROVAL_LOOKBACK_MS that approves it — the same evidence the * commitment-approval gate uses (chat-v2 `user` rows an agent cannot * write). A claim with no such message is refused. If the chat cannot be * read, the install is refused (fail closed: this runs someone else's code). * * @module services/skill-setup/skill-install-job.service */ import type { EnqueueMessageInput } from '../../types/messaging.types.js'; import type { MarketplaceItem, MarketplaceOperationResult } from '../../types/marketplace.types.js'; import { type SkillDiscoveryService } from './skill-discovery.service.js'; import { type SetupResult, type SkillSetupRunner } from './skill-setup-runner.service.js'; /** Job lifecycle. */ export type InstallJobState = 'running' | 'succeeded' | 'failed'; /** Public view of an install job. */ export interface SkillInstallJob { jobId: string; skillId: string; state: InstallJobState; official: boolean; officialReason: string; estimatedMinutes: number; startedAt: string; finishedAt?: string; /** Sessions that get the completion message */ requesterSessions: string[]; /** What the requester was doing (echoed back so it can resume) */ resumeNote?: string; /** Tail of the job log */ log: string; /** One-line outcome */ message?: string; /** Setup log file */ logFile?: string; /** Completion messages enqueued */ notified: boolean; /** Where the skill lives once installed */ executePath?: string; } /** Result of asking for an install. */ export type StartInstallResult = { kind: 'job'; job: SkillInstallJob; } | { kind: 'already-ready'; skill: { id: string; executePath?: string; officialReason: string; }; }; /** Input to {@link SkillInstallJobService.startInstall}. */ export interface StartInstallInput { /** Skill id (local id, directory name or registry id) */ skillId: string; /** Agent session to notify on completion (from X-Agent-Session) */ requesterSession?: string; /** The agent says the owner approved (verified against owner chat) */ approvedByOwner?: boolean; /** The request comes from the owner in the dashboard */ ownerDashboard?: boolean; /** What the agent was doing, echoed in the completion message */ resumeNote?: string; /** Re-run setup even when a check says everything is there */ force?: boolean; /** What the agent quotes as the owner's approval (X-Agent-Authorization), recorded in the job log */ ownerClaim?: string; } /** Error with a machine-readable code for the REST layer. */ export declare class SkillInstallError extends Error { readonly code: 'not_found' | 'owner_approval_required' | 'owner_approval_not_found' | 'owner_approval_unverifiable' | 'invalid_setup' | 'job_not_found'; readonly details: Record; /** * @param code - `not_found`, `owner_approval_required`, `owner_approval_not_found`, * `owner_approval_unverifiable`, `invalid_setup`, `job_not_found` * @param message - Human-readable message, written for the agent * @param details - Extra fields for the response */ constructor(code: 'not_found' | 'owner_approval_required' | 'owner_approval_not_found' | 'owner_approval_unverifiable' | 'invalid_setup' | 'job_not_found', message: string, details?: Record); } /** Injectable dependencies. */ export interface SkillInstallJobDeps { discovery?: SkillDiscoveryService; runner?: SkillSetupRunner; installMarketplaceItem?: (item: MarketplaceItem) => Promise; installPathFor?: (item: MarketplaceItem) => string; /** Owner-authored chat messages since a time (throws when chat is unreadable) */ recentOwnerMessages?: (sinceMs: number) => string[]; /** Enqueue a message for an agent (null queue = cannot notify) */ enqueue?: ((input: EnqueueMessageInput) => void) | null; now?: () => number; idFactory?: () => string; } /** * Whether one owner message approves installing a skill. * * Counts a clear approval ("go ahead", "批准", "同意" — the commitment * gate's tokens) or a short yes that names the skill ("ok install * shady-ocr", "可以,装 shady-ocr"). A question never counts. * * @param message - Owner message text * @param skill - Skill id and name * @returns True when it approves */ export declare function isOwnerInstallApproval(message: string, skill: { id: string; name?: string; }): boolean; /** * The message the requesting agent receives when a job ends. * * @param job - Finished job * @param setup - Setup result (absent when the job failed before setup) * @param durationMs - Job duration * @returns Message text */ export declare function formatCompletionMessage(job: SkillInstallJob, setup: SetupResult | null, durationMs: number): string; /** Runs and tracks skill install jobs. */ export declare class SkillInstallJobService { private readonly discovery; private readonly runner; private readonly installMarketplaceItem; private readonly installPathFor; private readonly recentOwnerMessages; private readonly enqueue; private readonly now; private readonly idFactory; private readonly jobs; private readonly logger; /** * @param deps - Injectable dependencies (all optional) */ constructor(deps?: SkillInstallJobDeps); /** * Ask for a skill to be installed and set up. * * Returns at once: either the skill is already usable, or a job id. When * a job for the same skill is running, its id is returned and the new * requester is added to the completion message's recipients. * * @param input - Skill id, requester, approval flags * @returns The job, or `already-ready` * @throws SkillInstallError when the skill is unknown, untrusted, or its setup block is invalid */ startInstall(input: StartInstallInput): Promise; /** * Look up a job. * * @param jobId - Job id * @returns The job * @throws SkillInstallError `job_not_found` */ getJob(jobId: string): SkillInstallJob; /** * Wait for a job to finish (tests, CLI). * * @param jobId - Job id * @returns The finished job * @throws SkillInstallError `job_not_found` */ waitForJob(jobId: string): Promise; /** * Apply the trust rule. * * @param skill - Resolved skill * @param input - Request * @returns The owner message that approved a third-party install (undefined for official / dashboard) * @throws SkillInstallError when a third-party skill lacks owner approval */ private checkTrust; /** * Install (when needed), set up, then notify. * * @param record - Job record (mutated) * @param skill - Resolved skill */ private execute; /** * Read the raw `setup` block of an installed skill. * * @param skillDir - Skill directory * @returns The block, or undefined */ private readSetup; /** * Send the completion message to every requester. * * @param record - Finished job (mutated: notified) * @param setup - Setup result * @param durationMs - Duration */ private notify; /** * Append to a job log, keeping the tail. * * @param record - Job * @param text - Text */ private append; /** Forget finished jobs after the retention window. */ private pruneFinished; /** * Public view of a job. * * @param r - Record * @returns Job */ private toPublic; } /** * One-line "I'm installing it" text the agent can relay to the user. * * @param job - Job * @returns Message */ export declare function describeStartedJob(job: SkillInstallJob): string; /** * The process-wide job service. * * @returns The service */ export declare function getSkillInstallJobService(): SkillInstallJobService; //# sourceMappingURL=skill-install-job.service.d.ts.map