crewly 1.20.131 → 1.20.133

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/config/constants.ts +49 -0
  2. package/config/index.ts +2 -0
  3. package/config/skills/agent/core/find-skill/SKILL.md +58 -0
  4. package/config/skills/agent/core/find-skill/execute.sh +67 -0
  5. package/config/skills/agent/core/find-skill/execute.test.sh +69 -0
  6. package/config/skills/agent/core/generate-pdf/SKILL.md +24 -29
  7. package/config/skills/agent/core/generate-pdf/execute.sh +22 -78
  8. package/config/skills/agent/core/generate-pdf/execute.test.sh +45 -0
  9. package/config/skills/agent/core/install-skill/SKILL.md +67 -0
  10. package/config/skills/agent/core/install-skill/execute.sh +80 -0
  11. package/config/skills/agent/core/install-skill/execute.test.sh +74 -0
  12. package/config/skills/agent/pdf-tools/SKILL.md +103 -0
  13. package/config/skills/agent/pdf-tools/execute.sh +301 -0
  14. package/config/skills/agent/pdf-tools/execute.test.sh +218 -0
  15. package/config/skills/agent/pdf-tools/instructions.md +14 -0
  16. package/config/skills/agent/pdf-tools/pdf_tools.py +268 -0
  17. package/config/skills/agent/pdf-tools/skill.json +103 -0
  18. package/config/skills/agent/pdf-tools/style.css +143 -0
  19. package/config/skills/agent/transcribe-audio/SKILL.md +42 -5
  20. package/config/skills/agent/transcribe-audio/execute.sh +44 -11
  21. package/config/skills/agent/transcribe-audio/execute.test.sh +90 -0
  22. package/config/skills/agent/transcribe-audio/install-whisper-cpp.sh +126 -0
  23. package/config/skills/agent/transcribe-audio/install-whisper-cpp.test.sh +69 -0
  24. package/config/skills/agent/transcribe-audio/skill.json +67 -6
  25. package/config/skills/marketplace-registry.ts +8 -1
  26. package/config/skills/registry.json +337 -3
  27. package/dist/backend/backend/src/constants.d.ts +71 -0
  28. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  29. package/dist/backend/backend/src/constants.js +96 -0
  30. package/dist/backend/backend/src/constants.js.map +1 -1
  31. package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.d.ts +56 -0
  32. package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.d.ts.map +1 -0
  33. package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.js +114 -0
  34. package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.js.map +1 -0
  35. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +25 -0
  36. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
  37. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +73 -55
  38. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
  39. package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts +3 -0
  40. package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
  41. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +9 -0
  42. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
  43. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.d.ts +49 -0
  44. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.d.ts.map +1 -0
  45. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.js +185 -0
  46. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.js.map +1 -0
  47. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.d.ts +21 -0
  48. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.d.ts.map +1 -0
  49. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.js +29 -0
  50. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.js.map +1 -0
  51. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  52. package/dist/backend/backend/src/routes/api.routes.js +5 -0
  53. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  54. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +14 -0
  55. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  56. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +33 -1
  57. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  58. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.d.ts +132 -0
  59. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.d.ts.map +1 -0
  60. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.js +228 -0
  61. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.js.map +1 -0
  62. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.d.ts +115 -0
  63. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.d.ts.map +1 -0
  64. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.js +160 -0
  65. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.js.map +1 -0
  66. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
  67. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +8 -0
  68. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
  69. package/dist/backend/backend/src/services/marketplace/marketplace.service.d.ts.map +1 -1
  70. package/dist/backend/backend/src/services/marketplace/marketplace.service.js +11 -3
  71. package/dist/backend/backend/src/services/marketplace/marketplace.service.js.map +1 -1
  72. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.d.ts +281 -0
  73. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.d.ts.map +1 -0
  74. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.js +559 -0
  75. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.js.map +1 -0
  76. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.d.ts +243 -0
  77. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.d.ts.map +1 -0
  78. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.js +439 -0
  79. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.js.map +1 -0
  80. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.d.ts +138 -0
  81. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.d.ts.map +1 -0
  82. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.js +281 -0
  83. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.js.map +1 -0
  84. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.d.ts +338 -0
  85. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.d.ts.map +1 -0
  86. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.js +761 -0
  87. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.js.map +1 -0
  88. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  89. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +6 -0
  90. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  91. package/dist/backend/backend/src/types/marketplace.types.d.ts +16 -1
  92. package/dist/backend/backend/src/types/marketplace.types.d.ts.map +1 -1
  93. package/dist/backend/backend/src/utils/inbound-file-hint.utils.d.ts +55 -0
  94. package/dist/backend/backend/src/utils/inbound-file-hint.utils.d.ts.map +1 -0
  95. package/dist/backend/backend/src/utils/inbound-file-hint.utils.js +78 -0
  96. package/dist/backend/backend/src/utils/inbound-file-hint.utils.js.map +1 -0
  97. package/dist/backend/build-info.json +2 -2
  98. package/dist/backend/config/constants.d.ts +48 -0
  99. package/dist/backend/config/constants.d.ts.map +1 -1
  100. package/dist/backend/config/constants.js +48 -0
  101. package/dist/backend/config/constants.js.map +1 -1
  102. package/dist/backend/config/index.d.ts +2 -2
  103. package/dist/backend/config/index.d.ts.map +1 -1
  104. package/dist/backend/config/index.js +2 -2
  105. package/dist/backend/config/index.js.map +1 -1
  106. package/dist/cli/backend/src/constants.d.ts +71 -0
  107. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  108. package/dist/cli/backend/src/constants.js +96 -0
  109. package/dist/cli/backend/src/constants.js.map +1 -1
  110. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +14 -0
  111. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  112. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +33 -1
  113. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  114. package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.d.ts +132 -0
  115. package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.d.ts.map +1 -0
  116. package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.js +228 -0
  117. package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.js.map +1 -0
  118. package/dist/cli/backend/src/services/marketplace/marketplace.service.d.ts +101 -0
  119. package/dist/cli/backend/src/services/marketplace/marketplace.service.d.ts.map +1 -0
  120. package/dist/cli/backend/src/services/marketplace/marketplace.service.js +392 -0
  121. package/dist/cli/backend/src/services/marketplace/marketplace.service.js.map +1 -0
  122. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.d.ts +281 -0
  123. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.d.ts.map +1 -0
  124. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.js +559 -0
  125. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.js.map +1 -0
  126. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.d.ts +138 -0
  127. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.d.ts.map +1 -0
  128. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.js +281 -0
  129. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.js.map +1 -0
  130. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.d.ts +338 -0
  131. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.d.ts.map +1 -0
  132. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.js +761 -0
  133. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.js.map +1 -0
  134. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  135. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +6 -0
  136. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  137. package/dist/cli/backend/src/types/marketplace.types.d.ts +219 -0
  138. package/dist/cli/backend/src/types/marketplace.types.d.ts.map +1 -0
  139. package/dist/cli/backend/src/types/marketplace.types.js +6 -0
  140. package/dist/cli/backend/src/types/marketplace.types.js.map +1 -0
  141. package/dist/cli/backend/src/utils/inbound-file-hint.utils.d.ts +55 -0
  142. package/dist/cli/backend/src/utils/inbound-file-hint.utils.d.ts.map +1 -0
  143. package/dist/cli/backend/src/utils/inbound-file-hint.utils.js +78 -0
  144. package/dist/cli/backend/src/utils/inbound-file-hint.utils.js.map +1 -0
  145. package/dist/cli/backend/src/utils/package-root.d.ts +15 -0
  146. package/dist/cli/backend/src/utils/package-root.d.ts.map +1 -0
  147. package/dist/cli/backend/src/utils/package-root.js +38 -0
  148. package/dist/cli/backend/src/utils/package-root.js.map +1 -0
  149. package/dist/cli/cli/src/commands/cloud.d.ts +45 -25
  150. package/dist/cli/cli/src/commands/cloud.d.ts.map +1 -1
  151. package/dist/cli/cli/src/commands/cloud.js +153 -47
  152. package/dist/cli/cli/src/commands/cloud.js.map +1 -1
  153. package/dist/cli/cli/src/commands/install.d.ts +13 -1
  154. package/dist/cli/cli/src/commands/install.d.ts.map +1 -1
  155. package/dist/cli/cli/src/commands/install.js +40 -3
  156. package/dist/cli/cli/src/commands/install.js.map +1 -1
  157. package/dist/cli/cli/src/commands/onboard-checklist.js +3 -3
  158. package/dist/cli/cli/src/commands/onboard-checklist.js.map +1 -1
  159. package/dist/cli/cli/src/commands/skills.d.ts +58 -0
  160. package/dist/cli/cli/src/commands/skills.d.ts.map +1 -0
  161. package/dist/cli/cli/src/commands/skills.js +132 -0
  162. package/dist/cli/cli/src/commands/skills.js.map +1 -0
  163. package/dist/cli/cli/src/index.js +18 -7
  164. package/dist/cli/cli/src/index.js.map +1 -1
  165. package/dist/cli/cli/src/utils/archive-creator.d.ts +14 -0
  166. package/dist/cli/cli/src/utils/archive-creator.d.ts.map +1 -1
  167. package/dist/cli/cli/src/utils/archive-creator.js +24 -3
  168. package/dist/cli/cli/src/utils/archive-creator.js.map +1 -1
  169. package/dist/cli/cli/src/utils/cloud-submit.d.ts.map +1 -1
  170. package/dist/cli/cli/src/utils/cloud-submit.js +14 -5
  171. package/dist/cli/cli/src/utils/cloud-submit.js.map +1 -1
  172. package/dist/cli/cli/src/utils/package-validator.d.ts +2 -0
  173. package/dist/cli/cli/src/utils/package-validator.d.ts.map +1 -1
  174. package/dist/cli/cli/src/utils/package-validator.js +16 -0
  175. package/dist/cli/cli/src/utils/package-validator.js.map +1 -1
  176. package/dist/cli/config/constants.d.ts +48 -0
  177. package/dist/cli/config/constants.d.ts.map +1 -1
  178. package/dist/cli/config/constants.js +48 -0
  179. package/dist/cli/config/constants.js.map +1 -1
  180. package/dist/cli/config/index.d.ts +2 -2
  181. package/dist/cli/config/index.d.ts.map +1 -1
  182. package/dist/cli/config/index.js +2 -2
  183. package/dist/cli/config/index.js.map +1 -1
  184. package/frontend/dist/assets/{index-5b98bf77.js → index-e6c403b6.js} +265 -265
  185. package/frontend/dist/index.html +1 -1
  186. package/package.json +1 -1
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Skill install jobs — `install-skill` as a background job that reports back.
3
+ *
4
+ * An agent that lacks a capability runs `install-skill --id <skill>`. This
5
+ * service decides whether it may (trust rule), downloads the skill from the
6
+ * marketplace when it is not bundled (reusing the marketplace installer),
7
+ * runs its setup block, and — when the job ends — sends the requesting agent
8
+ * a message through the normal message queue (`system_event` to its
9
+ * session, the path event-bus notifications use). The agent is woken by that
10
+ * message and carries on: tells the user it is ready, then does the task it
11
+ * paused.
12
+ *
13
+ * ## Trust rule
14
+ *
15
+ * - **Official** skills (see skill-discovery.service) install without asking,
16
+ * including the system dependencies their setup block declares.
17
+ * - **Third-party** skills are refused unless the owner approved:
18
+ * - the owner acting from the dashboard (`X-Crewly-Caller: dashboard`, no
19
+ * agent header), or
20
+ * - an agent passing `approvedByOwner`, which is **verified**: there must be
21
+ * a genuine owner-authored chat message in the last
22
+ * OWNER_APPROVAL_LOOKBACK_MS that approves it — the same evidence the
23
+ * commitment-approval gate uses (chat-v2 `user` rows an agent cannot
24
+ * write). A claim with no such message is refused. If the chat cannot be
25
+ * read, the install is refused (fail closed: this runs someone else's code).
26
+ *
27
+ * @module services/skill-setup/skill-install-job.service
28
+ */
29
+ import type { EnqueueMessageInput } from '../../types/messaging.types.js';
30
+ import type { MarketplaceItem, MarketplaceOperationResult } from '../../types/marketplace.types.js';
31
+ import { type SkillDiscoveryService } from './skill-discovery.service.js';
32
+ import { type SetupResult, type SkillSetupRunner } from './skill-setup-runner.service.js';
33
+ /** Job lifecycle. */
34
+ export type InstallJobState = 'running' | 'succeeded' | 'failed';
35
+ /** Public view of an install job. */
36
+ export interface SkillInstallJob {
37
+ jobId: string;
38
+ skillId: string;
39
+ state: InstallJobState;
40
+ official: boolean;
41
+ officialReason: string;
42
+ estimatedMinutes: number;
43
+ startedAt: string;
44
+ finishedAt?: string;
45
+ /** Sessions that get the completion message */
46
+ requesterSessions: string[];
47
+ /** What the requester was doing (echoed back so it can resume) */
48
+ resumeNote?: string;
49
+ /** Tail of the job log */
50
+ log: string;
51
+ /** One-line outcome */
52
+ message?: string;
53
+ /** Setup log file */
54
+ logFile?: string;
55
+ /** Completion messages enqueued */
56
+ notified: boolean;
57
+ /** Where the skill lives once installed */
58
+ executePath?: string;
59
+ }
60
+ /** Result of asking for an install. */
61
+ export type StartInstallResult = {
62
+ kind: 'job';
63
+ job: SkillInstallJob;
64
+ } | {
65
+ kind: 'already-ready';
66
+ skill: {
67
+ id: string;
68
+ executePath?: string;
69
+ officialReason: string;
70
+ };
71
+ };
72
+ /** Input to {@link SkillInstallJobService.startInstall}. */
73
+ export interface StartInstallInput {
74
+ /** Skill id (local id, directory name or registry id) */
75
+ skillId: string;
76
+ /** Agent session to notify on completion (from X-Agent-Session) */
77
+ requesterSession?: string;
78
+ /** The agent says the owner approved (verified against owner chat) */
79
+ approvedByOwner?: boolean;
80
+ /** The request comes from the owner in the dashboard */
81
+ ownerDashboard?: boolean;
82
+ /** What the agent was doing, echoed in the completion message */
83
+ resumeNote?: string;
84
+ /** Re-run setup even when a check says everything is there */
85
+ force?: boolean;
86
+ /** What the agent quotes as the owner's approval (X-Agent-Authorization), recorded in the job log */
87
+ ownerClaim?: string;
88
+ }
89
+ /** Error with a machine-readable code for the REST layer. */
90
+ export declare class SkillInstallError extends Error {
91
+ readonly code: 'not_found' | 'owner_approval_required' | 'owner_approval_not_found' | 'owner_approval_unverifiable' | 'invalid_setup' | 'job_not_found';
92
+ readonly details: Record<string, unknown>;
93
+ /**
94
+ * @param code - `not_found`, `owner_approval_required`, `owner_approval_not_found`,
95
+ * `owner_approval_unverifiable`, `invalid_setup`, `job_not_found`
96
+ * @param message - Human-readable message, written for the agent
97
+ * @param details - Extra fields for the response
98
+ */
99
+ constructor(code: 'not_found' | 'owner_approval_required' | 'owner_approval_not_found' | 'owner_approval_unverifiable' | 'invalid_setup' | 'job_not_found', message: string, details?: Record<string, unknown>);
100
+ }
101
+ /** Injectable dependencies. */
102
+ export interface SkillInstallJobDeps {
103
+ discovery?: SkillDiscoveryService;
104
+ runner?: SkillSetupRunner;
105
+ installMarketplaceItem?: (item: MarketplaceItem) => Promise<MarketplaceOperationResult>;
106
+ installPathFor?: (item: MarketplaceItem) => string;
107
+ /** Owner-authored chat messages since a time (throws when chat is unreadable) */
108
+ recentOwnerMessages?: (sinceMs: number) => string[];
109
+ /** Enqueue a message for an agent (null queue = cannot notify) */
110
+ enqueue?: ((input: EnqueueMessageInput) => void) | null;
111
+ now?: () => number;
112
+ idFactory?: () => string;
113
+ }
114
+ /**
115
+ * Whether one owner message approves installing a skill.
116
+ *
117
+ * Counts a clear approval ("go ahead", "批准", "同意" — the commitment
118
+ * gate's tokens) or a short yes that names the skill ("ok install
119
+ * shady-ocr", "可以,装 shady-ocr"). A question never counts.
120
+ *
121
+ * @param message - Owner message text
122
+ * @param skill - Skill id and name
123
+ * @returns True when it approves
124
+ */
125
+ export declare function isOwnerInstallApproval(message: string, skill: {
126
+ id: string;
127
+ name?: string;
128
+ }): boolean;
129
+ /**
130
+ * The message the requesting agent receives when a job ends.
131
+ *
132
+ * @param job - Finished job
133
+ * @param setup - Setup result (absent when the job failed before setup)
134
+ * @param durationMs - Job duration
135
+ * @returns Message text
136
+ */
137
+ export declare function formatCompletionMessage(job: SkillInstallJob, setup: SetupResult | null, durationMs: number): string;
138
+ /** Runs and tracks skill install jobs. */
139
+ export declare class SkillInstallJobService {
140
+ private readonly discovery;
141
+ private readonly runner;
142
+ private readonly installMarketplaceItem;
143
+ private readonly installPathFor;
144
+ private readonly recentOwnerMessages;
145
+ private readonly enqueue;
146
+ private readonly now;
147
+ private readonly idFactory;
148
+ private readonly jobs;
149
+ private readonly logger;
150
+ /**
151
+ * @param deps - Injectable dependencies (all optional)
152
+ */
153
+ constructor(deps?: SkillInstallJobDeps);
154
+ /**
155
+ * Ask for a skill to be installed and set up.
156
+ *
157
+ * Returns at once: either the skill is already usable, or a job id. When
158
+ * a job for the same skill is running, its id is returned and the new
159
+ * requester is added to the completion message's recipients.
160
+ *
161
+ * @param input - Skill id, requester, approval flags
162
+ * @returns The job, or `already-ready`
163
+ * @throws SkillInstallError when the skill is unknown, untrusted, or its setup block is invalid
164
+ */
165
+ startInstall(input: StartInstallInput): Promise<StartInstallResult>;
166
+ /**
167
+ * Look up a job.
168
+ *
169
+ * @param jobId - Job id
170
+ * @returns The job
171
+ * @throws SkillInstallError `job_not_found`
172
+ */
173
+ getJob(jobId: string): SkillInstallJob;
174
+ /**
175
+ * Wait for a job to finish (tests, CLI).
176
+ *
177
+ * @param jobId - Job id
178
+ * @returns The finished job
179
+ * @throws SkillInstallError `job_not_found`
180
+ */
181
+ waitForJob(jobId: string): Promise<SkillInstallJob>;
182
+ /**
183
+ * Apply the trust rule.
184
+ *
185
+ * @param skill - Resolved skill
186
+ * @param input - Request
187
+ * @returns The owner message that approved a third-party install (undefined for official / dashboard)
188
+ * @throws SkillInstallError when a third-party skill lacks owner approval
189
+ */
190
+ private checkTrust;
191
+ /**
192
+ * Install (when needed), set up, then notify.
193
+ *
194
+ * @param record - Job record (mutated)
195
+ * @param skill - Resolved skill
196
+ */
197
+ private execute;
198
+ /**
199
+ * Read the raw `setup` block of an installed skill.
200
+ *
201
+ * @param skillDir - Skill directory
202
+ * @returns The block, or undefined
203
+ */
204
+ private readSetup;
205
+ /**
206
+ * Send the completion message to every requester.
207
+ *
208
+ * @param record - Finished job (mutated: notified)
209
+ * @param setup - Setup result
210
+ * @param durationMs - Duration
211
+ */
212
+ private notify;
213
+ /**
214
+ * Append to a job log, keeping the tail.
215
+ *
216
+ * @param record - Job
217
+ * @param text - Text
218
+ */
219
+ private append;
220
+ /** Forget finished jobs after the retention window. */
221
+ private pruneFinished;
222
+ /**
223
+ * Public view of a job.
224
+ *
225
+ * @param r - Record
226
+ * @returns Job
227
+ */
228
+ private toPublic;
229
+ }
230
+ /**
231
+ * One-line "I'm installing it" text the agent can relay to the user.
232
+ *
233
+ * @param job - Job
234
+ * @returns Message
235
+ */
236
+ export declare function describeStartedJob(job: SkillInstallJob): string;
237
+ /**
238
+ * The process-wide job service.
239
+ *
240
+ * @returns The service
241
+ */
242
+ export declare function getSkillInstallJobService(): SkillInstallJobService;
243
+ //# sourceMappingURL=skill-install-job.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-install-job.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/skill-setup/skill-install-job.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Skill install jobs — `install-skill` as a background job that reports back.
3
+ *
4
+ * An agent that lacks a capability runs `install-skill --id <skill>`. This
5
+ * service decides whether it may (trust rule), downloads the skill from the
6
+ * marketplace when it is not bundled (reusing the marketplace installer),
7
+ * runs its setup block, and — when the job ends — sends the requesting agent
8
+ * a message through the normal message queue (`system_event` to its
9
+ * session, the path event-bus notifications use). The agent is woken by that
10
+ * message and carries on: tells the user it is ready, then does the task it
11
+ * paused.
12
+ *
13
+ * ## Trust rule
14
+ *
15
+ * - **Official** skills (see skill-discovery.service) install without asking,
16
+ * including the system dependencies their setup block declares.
17
+ * - **Third-party** skills are refused unless the owner approved:
18
+ * - the owner acting from the dashboard (`X-Crewly-Caller: dashboard`, no
19
+ * agent header), or
20
+ * - an agent passing `approvedByOwner`, which is **verified**: there must be
21
+ * a genuine owner-authored chat message in the last
22
+ * OWNER_APPROVAL_LOOKBACK_MS that approves it — the same evidence the
23
+ * commitment-approval gate uses (chat-v2 `user` rows an agent cannot
24
+ * write). A claim with no such message is refused. If the chat cannot be
25
+ * read, the install is refused (fail closed: this runs someone else's code).
26
+ *
27
+ * @module services/skill-setup/skill-install-job.service
28
+ */
29
+ import { randomUUID } from 'crypto';
30
+ import * as fs from 'fs';
31
+ import * as path from 'path';
32
+ import { SKILL_SETUP_CONSTANTS, MESSAGE_SOURCES } from '../../constants.js';
33
+ import { LoggerService } from '../core/logger.service.js';
34
+ import { getInstallPath } from '../marketplace/marketplace.service.js';
35
+ import { installItem } from '../marketplace/marketplace-installer.service.js';
36
+ import { getMessageQueueInstance } from '../messaging/index.js';
37
+ import { containsApprovalToken } from '../orchestrator/commitment-approval-guard.js';
38
+ import { getChatV2Service } from '../chat-v2/chat-v2.singleton.js';
39
+ import { validateSetupManifest } from './skill-setup-manifest.js';
40
+ import { getSkillDiscoveryService } from './skill-discovery.service.js';
41
+ import { getSkillSetupRunner } from './skill-setup-runner.service.js';
42
+ /** Error with a machine-readable code for the REST layer. */
43
+ export class SkillInstallError extends Error {
44
+ code;
45
+ details;
46
+ /**
47
+ * @param code - `not_found`, `owner_approval_required`, `owner_approval_not_found`,
48
+ * `owner_approval_unverifiable`, `invalid_setup`, `job_not_found`
49
+ * @param message - Human-readable message, written for the agent
50
+ * @param details - Extra fields for the response
51
+ */
52
+ constructor(code, message, details = {}) {
53
+ super(message);
54
+ this.code = code;
55
+ this.details = details;
56
+ this.name = 'SkillInstallError';
57
+ }
58
+ }
59
+ /** Short affirmatives that approve an install when the message also names the skill. */
60
+ const SHORT_YES = [/\byes\b/i, /\bok(ay)?\b/i, /\bsure\b/i, /\binstall\b/i, /好/, /可以/, /行/, /装/, /同意/];
61
+ /** A question is never approval. */
62
+ const QUESTION = /[??]|吗\s*$|要不要|是否/;
63
+ /**
64
+ * Whether one owner message approves installing a skill.
65
+ *
66
+ * Counts a clear approval ("go ahead", "批准", "同意" — the commitment
67
+ * gate's tokens) or a short yes that names the skill ("ok install
68
+ * shady-ocr", "可以,装 shady-ocr"). A question never counts.
69
+ *
70
+ * @param message - Owner message text
71
+ * @param skill - Skill id and name
72
+ * @returns True when it approves
73
+ */
74
+ export function isOwnerInstallApproval(message, skill) {
75
+ if (!message || QUESTION.test(message))
76
+ return false;
77
+ if (containsApprovalToken(message))
78
+ return true;
79
+ const lower = message.toLowerCase();
80
+ const names = [skill.id, skill.name].filter((n) => !!n && n.length >= 3).map((n) => n.toLowerCase());
81
+ return names.some((n) => lower.includes(n)) && SHORT_YES.some((re) => re.test(message));
82
+ }
83
+ /**
84
+ * Human duration.
85
+ *
86
+ * @param ms - Milliseconds
87
+ * @returns e.g. "4m 10s"
88
+ */
89
+ function formatDuration(ms) {
90
+ const s = Math.max(0, Math.round(ms / 1000));
91
+ return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
92
+ }
93
+ /**
94
+ * The message the requesting agent receives when a job ends.
95
+ *
96
+ * @param job - Finished job
97
+ * @param setup - Setup result (absent when the job failed before setup)
98
+ * @param durationMs - Job duration
99
+ * @returns Message text
100
+ */
101
+ export function formatCompletionMessage(job, setup, durationMs) {
102
+ const H = SKILL_SETUP_CONSTANTS.COMPLETION_HEADERS;
103
+ const steps = setup?.steps.length
104
+ ? setup.steps.map((s) => `${s.id} ${s.status === 'satisfied' ? 'already there' : s.status}`).join(' · ')
105
+ : '';
106
+ const resume = job.resumeNote ? `\nYou paused: "${job.resumeNote}"` : '';
107
+ if (job.state === 'succeeded') {
108
+ return [
109
+ `${H.SUCCEEDED} ${job.skillId} is installed and ready (job ${job.jobId}, ${formatDuration(durationMs)}).`,
110
+ ...(steps ? [`Setup: ${steps}`] : []),
111
+ ...(job.executePath ? [`Run it: bash ${job.executePath} (see SKILL.md next to it)`] : []),
112
+ ].join('\n') + `${resume}\nNext: tell the user in one line that it is ready, then do the task you paused — now, without waiting to be asked.`;
113
+ }
114
+ return [
115
+ `${H.FAILED} ${job.skillId} (job ${job.jobId}, ${formatDuration(durationMs)}): ${job.message ?? 'unknown error'}`,
116
+ ...(steps ? [`Setup: ${steps}`] : []),
117
+ ...(job.logFile ? [`Log: ${job.logFile}`] : []),
118
+ ].join('\n') + `${resume}\nNext: tell the user plainly what is missing and the fix quoted above (e.g. the command the owner can run). Do not retry the same install until something has changed.`;
119
+ }
120
+ /** Runs and tracks skill install jobs. */
121
+ export class SkillInstallJobService {
122
+ discovery;
123
+ runner;
124
+ installMarketplaceItem;
125
+ installPathFor;
126
+ recentOwnerMessages;
127
+ enqueue;
128
+ now;
129
+ idFactory;
130
+ jobs = new Map();
131
+ logger = LoggerService.getInstance().createComponentLogger('SkillInstallJobs');
132
+ /**
133
+ * @param deps - Injectable dependencies (all optional)
134
+ */
135
+ constructor(deps = {}) {
136
+ this.discovery = deps.discovery ?? getSkillDiscoveryService();
137
+ this.runner = deps.runner ?? getSkillSetupRunner();
138
+ this.installMarketplaceItem = deps.installMarketplaceItem ?? installItem;
139
+ this.installPathFor = deps.installPathFor ?? ((item) => getInstallPath(item.type, item.id));
140
+ this.recentOwnerMessages = deps.recentOwnerMessages ?? ((since) => getChatV2Service().getRecentOwnerMessageContents(since));
141
+ // Resolved per job: the queue is created after this service at startup.
142
+ this.enqueue =
143
+ deps.enqueue !== undefined
144
+ ? () => deps.enqueue ?? null
145
+ : () => {
146
+ const queue = getMessageQueueInstance();
147
+ return queue ? (input) => void queue.enqueue(input) : null;
148
+ };
149
+ this.now = deps.now ?? Date.now;
150
+ this.idFactory = deps.idFactory ?? (() => randomUUID().slice(0, 8));
151
+ }
152
+ /**
153
+ * Ask for a skill to be installed and set up.
154
+ *
155
+ * Returns at once: either the skill is already usable, or a job id. When
156
+ * a job for the same skill is running, its id is returned and the new
157
+ * requester is added to the completion message's recipients.
158
+ *
159
+ * @param input - Skill id, requester, approval flags
160
+ * @returns The job, or `already-ready`
161
+ * @throws SkillInstallError when the skill is unknown, untrusted, or its setup block is invalid
162
+ */
163
+ async startInstall(input) {
164
+ const skill = await this.discovery.resolve(input.skillId);
165
+ if (!skill) {
166
+ throw new SkillInstallError('not_found', `No skill "${input.skillId}" is bundled with Crewly or listed in the marketplace. Run find-skill --query "<what you need>" to search.`);
167
+ }
168
+ // A job already running for this skill passed the trust check: join it.
169
+ this.pruneFinished();
170
+ const running = [...this.jobs.values()].find((j) => j.skillId === skill.id && j.state === 'running');
171
+ if (running) {
172
+ if (input.requesterSession && !running.requesterSessions.includes(input.requesterSession)) {
173
+ running.requesterSessions.push(input.requesterSession);
174
+ }
175
+ return { kind: 'job', job: this.toPublic(running) };
176
+ }
177
+ // Nothing to install: say so instead of making the agent wait for a message.
178
+ if (!input.force && skill.installed && !skill.manifestError) {
179
+ await this.discovery.probe(skill);
180
+ if (skill.ready) {
181
+ return { kind: 'already-ready', skill: { id: skill.id, executePath: skill.executePath, officialReason: skill.officialReason } };
182
+ }
183
+ }
184
+ const evidence = this.checkTrust(skill, input);
185
+ if (skill.manifestError) {
186
+ throw new SkillInstallError('invalid_setup', `The setup block of ${skill.id} is invalid, so it was not run: ${skill.manifestError}`);
187
+ }
188
+ const record = {
189
+ jobId: this.idFactory(),
190
+ skillId: skill.id,
191
+ state: 'running',
192
+ official: skill.official,
193
+ officialReason: skill.officialReason,
194
+ estimatedMinutes: skill.setup.estimatedMinutes ?? SKILL_SETUP_CONSTANTS.DEFAULT_ESTIMATED_MINUTES,
195
+ startedAt: new Date(this.now()).toISOString(),
196
+ requesterSessions: input.requesterSession ? [input.requesterSession] : [],
197
+ ...(input.resumeNote ? { resumeNote: input.resumeNote } : {}),
198
+ log: evidence
199
+ ? `Third-party skill approved by the owner in chat: "${evidence.slice(0, 300)}"` +
200
+ (input.ownerClaim ? `\nThe agent cited: "${input.ownerClaim.slice(0, 300)}"` : '') + '\n'
201
+ : input.ownerDashboard && !skill.official
202
+ ? 'Third-party skill installed by the owner from the dashboard.\n'
203
+ : '',
204
+ notified: false,
205
+ executePath: skill.executePath,
206
+ finishedAtMs: null,
207
+ done: null,
208
+ };
209
+ this.jobs.set(record.jobId, record);
210
+ this.logger.info('Skill install started', { jobId: record.jobId, skillId: skill.id, official: skill.official, requesters: record.requesterSessions });
211
+ record.done = this.execute(record, skill);
212
+ return { kind: 'job', job: this.toPublic(record) };
213
+ }
214
+ /**
215
+ * Look up a job.
216
+ *
217
+ * @param jobId - Job id
218
+ * @returns The job
219
+ * @throws SkillInstallError `job_not_found`
220
+ */
221
+ getJob(jobId) {
222
+ const record = this.jobs.get(jobId);
223
+ if (!record)
224
+ throw new SkillInstallError('job_not_found', `Install job not found: ${jobId}`);
225
+ return this.toPublic(record);
226
+ }
227
+ /**
228
+ * Wait for a job to finish (tests, CLI).
229
+ *
230
+ * @param jobId - Job id
231
+ * @returns The finished job
232
+ * @throws SkillInstallError `job_not_found`
233
+ */
234
+ async waitForJob(jobId) {
235
+ const record = this.jobs.get(jobId);
236
+ if (!record)
237
+ throw new SkillInstallError('job_not_found', `Install job not found: ${jobId}`);
238
+ if (record.done)
239
+ await record.done;
240
+ return this.toPublic(record);
241
+ }
242
+ /**
243
+ * Apply the trust rule.
244
+ *
245
+ * @param skill - Resolved skill
246
+ * @param input - Request
247
+ * @returns The owner message that approved a third-party install (undefined for official / dashboard)
248
+ * @throws SkillInstallError when a third-party skill lacks owner approval
249
+ */
250
+ checkTrust(skill, input) {
251
+ if (skill.official || input.ownerDashboard)
252
+ return undefined;
253
+ const who = `"${skill.id}" is a third-party skill (${skill.officialReason})`;
254
+ if (!input.approvedByOwner) {
255
+ throw new SkillInstallError('owner_approval_required', `${who}, so it is not installed automatically. Ask the owner in chat whether to install it (say what it is and who publishes it). ` +
256
+ `Only after they say yes, run: install-skill --id ${skill.id} --approved-by-owner`, { official: false, officialReason: skill.officialReason });
257
+ }
258
+ let messages;
259
+ try {
260
+ messages = this.recentOwnerMessages(this.now() - SKILL_SETUP_CONSTANTS.OWNER_APPROVAL_LOOKBACK_MS);
261
+ }
262
+ catch (error) {
263
+ throw new SkillInstallError('owner_approval_unverifiable', `${who}. --approved-by-owner could not be checked because the owner's chat history is unreadable (${error instanceof Error ? error.message : String(error)}); not installing.`);
264
+ }
265
+ const evidence = messages.find((m) => isOwnerInstallApproval(m, { id: skill.id, name: skill.name }));
266
+ if (!evidence) {
267
+ const hours = Math.round(SKILL_SETUP_CONSTANTS.OWNER_APPROVAL_LOOKBACK_MS / 3_600_000);
268
+ throw new SkillInstallError('owner_approval_not_found', `${who}. --approved-by-owner was passed, but no owner message in the last ${hours}h approves installing it. ` +
269
+ `Ask the owner in chat first (name the skill); run this again after they say yes.`);
270
+ }
271
+ this.logger.info('Third-party skill install approved by owner message', { skillId: skill.id, evidence: evidence.slice(0, 200), claim: input.ownerClaim });
272
+ return evidence;
273
+ }
274
+ /**
275
+ * Install (when needed), set up, then notify.
276
+ *
277
+ * @param record - Job record (mutated)
278
+ * @param skill - Resolved skill
279
+ */
280
+ async execute(record, skill) {
281
+ const started = this.now();
282
+ let setup = null;
283
+ try {
284
+ let skillDir = skill.skillDir;
285
+ let manifest = skill.manifest;
286
+ if (!skill.installed || !skillDir) {
287
+ const item = skill.registryItem;
288
+ if (!item)
289
+ throw new Error('not installed and not in the registry');
290
+ this.append(record, `Downloading ${item.name} v${item.version} from the marketplace…\n`);
291
+ let installed = item;
292
+ let res = await this.installMarketplaceItem(item);
293
+ if (!res.success && item.fallback) {
294
+ // Premium archives can be missing from the CDN; the public copy of the same skill installs.
295
+ this.append(record, `${res.message}; trying the public registry copy…\n`);
296
+ installed = item.fallback;
297
+ res = await this.installMarketplaceItem(item.fallback);
298
+ }
299
+ if (!res.success)
300
+ throw new Error(`marketplace install failed: ${res.message}`);
301
+ this.append(record, `${res.message}\n`);
302
+ skillDir = this.installPathFor(installed);
303
+ record.executePath = path.join(skillDir, 'execute.sh');
304
+ // The downloaded skill.json is authoritative for setup.
305
+ const local = this.readSetup(skillDir);
306
+ if (local !== undefined) {
307
+ const v = validateSetupManifest(local);
308
+ if (!v.valid)
309
+ throw new Error(`the installed skill's setup block is invalid: ${v.errors.join('; ')}`);
310
+ manifest = v.manifest;
311
+ }
312
+ }
313
+ if (manifest) {
314
+ setup = await this.runner.runSetup({
315
+ skillId: skill.id,
316
+ skillDir,
317
+ manifest,
318
+ onProgress: (e) => this.append(record, `[${e.stepId}] ${e.phase}: ${e.message}\n`),
319
+ });
320
+ record.logFile = setup.logFile;
321
+ if (!setup.success)
322
+ throw new Error(setup.error ?? 'setup failed');
323
+ }
324
+ else {
325
+ this.append(record, 'No setup needed.\n');
326
+ }
327
+ record.state = 'succeeded';
328
+ record.message = `${skill.id} is installed and ready`;
329
+ }
330
+ catch (error) {
331
+ record.state = 'failed';
332
+ record.message = error instanceof Error ? error.message : String(error);
333
+ this.append(record, `FAILED: ${record.message}\n`);
334
+ }
335
+ record.finishedAtMs = this.now();
336
+ record.finishedAt = new Date(record.finishedAtMs).toISOString();
337
+ this.logger.info('Skill install finished', { jobId: record.jobId, skillId: record.skillId, state: record.state, message: record.message });
338
+ this.notify(record, setup, record.finishedAtMs - started);
339
+ }
340
+ /**
341
+ * Read the raw `setup` block of an installed skill.
342
+ *
343
+ * @param skillDir - Skill directory
344
+ * @returns The block, or undefined
345
+ */
346
+ readSetup(skillDir) {
347
+ try {
348
+ return JSON.parse(fs.readFileSync(path.join(skillDir, 'skill.json'), 'utf-8')).setup;
349
+ }
350
+ catch {
351
+ return undefined;
352
+ }
353
+ }
354
+ /**
355
+ * Send the completion message to every requester.
356
+ *
357
+ * @param record - Finished job (mutated: notified)
358
+ * @param setup - Setup result
359
+ * @param durationMs - Duration
360
+ */
361
+ notify(record, setup, durationMs) {
362
+ if (record.requesterSessions.length === 0)
363
+ return;
364
+ const enqueue = this.enqueue();
365
+ if (!enqueue) {
366
+ this.logger.warn('Message queue unavailable; install result not delivered', { jobId: record.jobId });
367
+ return;
368
+ }
369
+ const content = formatCompletionMessage(this.toPublic(record), setup, durationMs);
370
+ for (const session of record.requesterSessions) {
371
+ try {
372
+ enqueue({
373
+ content,
374
+ conversationId: SKILL_SETUP_CONSTANTS.COMPLETION_CONVERSATION_ID,
375
+ source: MESSAGE_SOURCES.SYSTEM_EVENT,
376
+ targetSession: session,
377
+ });
378
+ record.notified = true;
379
+ }
380
+ catch (error) {
381
+ this.logger.warn('Failed to enqueue install result', { jobId: record.jobId, session, error: error instanceof Error ? error.message : String(error) });
382
+ }
383
+ }
384
+ }
385
+ /**
386
+ * Append to a job log, keeping the tail.
387
+ *
388
+ * @param record - Job
389
+ * @param text - Text
390
+ */
391
+ append(record, text) {
392
+ const next = record.log + text;
393
+ const max = SKILL_SETUP_CONSTANTS.JOB_LOG_MAX_CHARS;
394
+ record.log = next.length > max ? next.slice(next.length - max) : next;
395
+ }
396
+ /** Forget finished jobs after the retention window. */
397
+ pruneFinished() {
398
+ const cutoff = this.now() - SKILL_SETUP_CONSTANTS.JOB_RETENTION_MS;
399
+ for (const [id, job] of this.jobs) {
400
+ if (job.finishedAtMs !== null && job.finishedAtMs < cutoff)
401
+ this.jobs.delete(id);
402
+ }
403
+ }
404
+ /**
405
+ * Public view of a job.
406
+ *
407
+ * @param r - Record
408
+ * @returns Job
409
+ */
410
+ toPublic(r) {
411
+ const view = { ...r, requesterSessions: [...r.requesterSessions] };
412
+ delete view.finishedAtMs;
413
+ delete view.done;
414
+ return view;
415
+ }
416
+ }
417
+ /**
418
+ * One-line "I'm installing it" text the agent can relay to the user.
419
+ *
420
+ * @param job - Job
421
+ * @returns Message
422
+ */
423
+ export function describeStartedJob(job) {
424
+ return (`Installing ${job.skillId} in the background (about ${job.estimatedMinutes} min). ` +
425
+ `Tell the user now, in one line, that you are installing it and roughly how long it takes; ` +
426
+ `you will get a ${SKILL_SETUP_CONSTANTS.COMPLETION_HEADERS.SUCCEEDED} or ${SKILL_SETUP_CONSTANTS.COMPLETION_HEADERS.FAILED} message when it ends — continue then.`);
427
+ }
428
+ let jobServiceSingleton = null;
429
+ /**
430
+ * The process-wide job service.
431
+ *
432
+ * @returns The service
433
+ */
434
+ export function getSkillInstallJobService() {
435
+ if (!jobServiceSingleton)
436
+ jobServiceSingleton = new SkillInstallJobService();
437
+ return jobServiceSingleton;
438
+ }
439
+ //# sourceMappingURL=skill-install-job.service.js.map