crewly 1.11.5 → 1.12.0

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 (208) hide show
  1. package/config/skills/agent/onboarding/synthesize-hierarchy/SKILL.md +65 -0
  2. package/config/skills/agent/onboarding/synthesize-hierarchy/execute.sh +61 -0
  3. package/config/skills/agent/web-search/SKILL.md +70 -0
  4. package/config/skills/agent/web-search/execute.sh +170 -0
  5. package/config/skills/agent/web-search/skill.json +23 -0
  6. package/dist/backend/backend/src/constants.d.ts +34 -1
  7. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  8. package/dist/backend/backend/src/constants.js +34 -1
  9. package/dist/backend/backend/src/constants.js.map +1 -1
  10. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +22 -0
  11. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
  12. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +58 -0
  13. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
  14. package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
  15. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +3 -1
  16. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
  17. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts +27 -0
  18. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts.map +1 -1
  19. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js +108 -0
  20. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js.map +1 -1
  21. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts +6 -2
  22. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts.map +1 -1
  23. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js +9 -3
  24. package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js.map +1 -1
  25. package/dist/backend/backend/src/index.d.ts.map +1 -1
  26. package/dist/backend/backend/src/index.js +36 -2
  27. package/dist/backend/backend/src/index.js.map +1 -1
  28. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +18 -0
  29. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
  30. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +24 -2
  31. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
  32. package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts +90 -0
  33. package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
  34. package/dist/backend/backend/src/services/backup/backup-archive.service.js +309 -0
  35. package/dist/backend/backend/src/services/backup/backup-archive.service.js.map +1 -0
  36. package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
  37. package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
  38. package/dist/backend/backend/src/services/backup/backup-cloud.client.js +134 -0
  39. package/dist/backend/backend/src/services/backup/backup-cloud.client.js.map +1 -0
  40. package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts +78 -0
  41. package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
  42. package/dist/backend/backend/src/services/backup/backup-restore.service.js +358 -0
  43. package/dist/backend/backend/src/services/backup/backup-restore.service.js.map +1 -0
  44. package/dist/backend/backend/src/services/backup/backup.types.d.ts +163 -0
  45. package/dist/backend/backend/src/services/backup/backup.types.d.ts.map +1 -0
  46. package/dist/backend/backend/src/services/backup/backup.types.js +13 -0
  47. package/dist/backend/backend/src/services/backup/backup.types.js.map +1 -0
  48. package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts +29 -2
  49. package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
  50. package/dist/backend/backend/src/services/cloud/cloud-sync.service.js +97 -13
  51. package/dist/backend/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
  52. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts +102 -0
  53. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -0
  54. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +164 -0
  55. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -0
  56. package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts +21 -0
  57. package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts.map +1 -1
  58. package/dist/backend/backend/src/services/fission/fission-guard.service.js +30 -0
  59. package/dist/backend/backend/src/services/fission/fission-guard.service.js.map +1 -1
  60. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts +4 -0
  61. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts.map +1 -1
  62. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js +8 -0
  63. package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js.map +1 -1
  64. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts +79 -58
  65. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts.map +1 -1
  66. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js +140 -65
  67. package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js.map +1 -1
  68. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts +117 -0
  69. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts.map +1 -0
  70. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js +189 -0
  71. package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js.map +1 -0
  72. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.d.ts.map +1 -1
  73. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js +1 -0
  74. package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js.map +1 -1
  75. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.d.ts.map +1 -1
  76. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js +2 -0
  77. package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js.map +1 -1
  78. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.d.ts.map +1 -1
  79. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js +17 -1
  80. package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js.map +1 -1
  81. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +50 -0
  82. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +71 -0
  84. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
  85. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +18 -0
  86. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
  87. package/dist/backend/backend/src/services/reconciler/reconciler.service.js +75 -1
  88. package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
  89. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts +115 -0
  90. package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
  91. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +189 -3
  92. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  93. package/dist/backend/backend/src/services/session/pty/pty-session.d.ts +28 -0
  94. package/dist/backend/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
  95. package/dist/backend/backend/src/services/session/pty/pty-session.js +61 -1
  96. package/dist/backend/backend/src/services/session/pty/pty-session.js.map +1 -1
  97. package/dist/backend/backend/src/services/template/template.service.d.ts.map +1 -1
  98. package/dist/backend/backend/src/services/template/template.service.js +67 -2
  99. package/dist/backend/backend/src/services/template/template.service.js.map +1 -1
  100. package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts +19 -1
  101. package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts.map +1 -1
  102. package/dist/backend/backend/src/services/v3/cascade-request-status.js +39 -2
  103. package/dist/backend/backend/src/services/v3/cascade-request-status.js.map +1 -1
  104. package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts +41 -0
  105. package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
  106. package/dist/backend/backend/src/services/v3/escalation-router.service.js +169 -0
  107. package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
  108. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts +4 -1
  109. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts.map +1 -1
  110. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js +21 -0
  111. package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js.map +1 -1
  112. package/dist/backend/backend/src/types/intent-task.types.d.ts.map +1 -1
  113. package/dist/backend/backend/src/types/intent-task.types.js +8 -0
  114. package/dist/backend/backend/src/types/intent-task.types.js.map +1 -1
  115. package/dist/backend/backend/src/types/v2/request.types.d.ts +1 -1
  116. package/dist/backend/backend/src/types/v2/request.types.d.ts.map +1 -1
  117. package/dist/backend/backend/src/types/v2/request.types.js +1 -0
  118. package/dist/backend/backend/src/types/v2/request.types.js.map +1 -1
  119. package/dist/cli/backend/src/constants.d.ts +34 -1
  120. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  121. package/dist/cli/backend/src/constants.js +34 -1
  122. package/dist/cli/backend/src/constants.js.map +1 -1
  123. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts +70 -0
  124. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts.map +1 -0
  125. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js +427 -0
  126. package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js.map +1 -0
  127. package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts +90 -0
  128. package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
  129. package/dist/cli/backend/src/services/backup/backup-archive.service.js +309 -0
  130. package/dist/cli/backend/src/services/backup/backup-archive.service.js.map +1 -0
  131. package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
  132. package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
  133. package/dist/cli/backend/src/services/backup/backup-cloud.client.js +134 -0
  134. package/dist/cli/backend/src/services/backup/backup-cloud.client.js.map +1 -0
  135. package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts +78 -0
  136. package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
  137. package/dist/cli/backend/src/services/backup/backup-restore.service.js +358 -0
  138. package/dist/cli/backend/src/services/backup/backup-restore.service.js.map +1 -0
  139. package/dist/cli/backend/src/services/backup/backup.types.d.ts +163 -0
  140. package/dist/cli/backend/src/services/backup/backup.types.d.ts.map +1 -0
  141. package/dist/cli/backend/src/services/backup/backup.types.js +13 -0
  142. package/dist/cli/backend/src/services/backup/backup.types.js.map +1 -0
  143. package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts +410 -0
  144. package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -0
  145. package/dist/cli/backend/src/services/cloud/cloud-client.service.js +863 -0
  146. package/dist/cli/backend/src/services/cloud/cloud-client.service.js.map +1 -0
  147. package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts +292 -0
  148. package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -0
  149. package/dist/cli/backend/src/services/cloud/cloud-sync.service.js +1093 -0
  150. package/dist/cli/backend/src/services/cloud/cloud-sync.service.js.map +1 -0
  151. package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts +328 -0
  152. package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -0
  153. package/dist/cli/backend/src/services/cloud/cloud-sync.types.js +171 -0
  154. package/dist/cli/backend/src/services/cloud/cloud-sync.types.js.map +1 -0
  155. package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts +89 -0
  156. package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts.map +1 -0
  157. package/dist/cli/backend/src/services/cloud/device-identity.service.js +148 -0
  158. package/dist/cli/backend/src/services/cloud/device-identity.service.js.map +1 -0
  159. package/dist/cli/backend/src/services/user/user-identity.service.d.ts +86 -0
  160. package/dist/cli/backend/src/services/user/user-identity.service.d.ts.map +1 -0
  161. package/dist/cli/backend/src/services/user/user-identity.service.js +190 -0
  162. package/dist/cli/backend/src/services/user/user-identity.service.js.map +1 -0
  163. package/dist/cli/cli/src/commands/backup.d.ts +31 -0
  164. package/dist/cli/cli/src/commands/backup.d.ts.map +1 -0
  165. package/dist/cli/cli/src/commands/backup.js +280 -0
  166. package/dist/cli/cli/src/commands/backup.js.map +1 -0
  167. package/dist/cli/cli/src/index.js +10 -0
  168. package/dist/cli/cli/src/index.js.map +1 -1
  169. package/package.json +9 -3
  170. package/packages/crewly-agent/README.md +27 -0
  171. package/packages/crewly-agent/bin/crewly-agent +33 -0
  172. package/packages/crewly-agent/package.json +39 -0
  173. package/packages/crewly-agent/src/cli.ts +168 -0
  174. package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +2355 -0
  175. package/packages/crewly-agent/src/runtime/agent-runner.service.ts +1827 -0
  176. package/packages/crewly-agent/src/runtime/agent-stream.service.test.ts +153 -0
  177. package/packages/crewly-agent/src/runtime/agent-stream.service.ts +225 -0
  178. package/packages/crewly-agent/src/runtime/agent-worker.test.ts +171 -0
  179. package/packages/crewly-agent/src/runtime/agent-worker.ts +193 -0
  180. package/packages/crewly-agent/src/runtime/api-client.ts +143 -0
  181. package/packages/crewly-agent/src/runtime/approval-queue.service.ts +307 -0
  182. package/packages/crewly-agent/src/runtime/audit-log.service.test.ts +208 -0
  183. package/packages/crewly-agent/src/runtime/audit-log.service.ts +332 -0
  184. package/packages/crewly-agent/src/runtime/audit-trail.service.test.ts +178 -0
  185. package/packages/crewly-agent/src/runtime/audit-trail.service.ts +151 -0
  186. package/packages/crewly-agent/src/runtime/auditor-tools.test.ts +274 -0
  187. package/packages/crewly-agent/src/runtime/auditor-tools.ts +311 -0
  188. package/packages/crewly-agent/src/runtime/cloud-config.ts +67 -0
  189. package/packages/crewly-agent/src/runtime/deepseek-sse-transform.test.ts +165 -0
  190. package/packages/crewly-agent/src/runtime/deepseek-sse-transform.ts +168 -0
  191. package/packages/crewly-agent/src/runtime/env-isolation.service.ts +246 -0
  192. package/packages/crewly-agent/src/runtime/in-process-log-buffer.test.ts +280 -0
  193. package/packages/crewly-agent/src/runtime/in-process-log-buffer.ts +317 -0
  194. package/packages/crewly-agent/src/runtime/index.ts +38 -0
  195. package/packages/crewly-agent/src/runtime/mcp-tool-bridge.test.ts +352 -0
  196. package/packages/crewly-agent/src/runtime/mcp-tool-bridge.ts +244 -0
  197. package/packages/crewly-agent/src/runtime/model-manager.test.ts +326 -0
  198. package/packages/crewly-agent/src/runtime/model-manager.ts +363 -0
  199. package/packages/crewly-agent/src/runtime/output-filter.service.ts +175 -0
  200. package/packages/crewly-agent/src/runtime/prompt-guard.service.ts +303 -0
  201. package/packages/crewly-agent/src/runtime/rate-limiter.test.ts +228 -0
  202. package/packages/crewly-agent/src/runtime/rate-limiter.ts +353 -0
  203. package/packages/crewly-agent/src/runtime/tool-registry.test.ts +2510 -0
  204. package/packages/crewly-agent/src/runtime/tool-registry.ts +2104 -0
  205. package/packages/crewly-agent/src/runtime/types.test.ts +519 -0
  206. package/packages/crewly-agent/src/runtime/types.ts +637 -0
  207. package/packages/crewly-agent/src/runtime/web-search.tool.test.ts +131 -0
  208. package/packages/crewly-agent/src/runtime/web-search.tool.ts +140 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * DeepSeek SSE Transform — extracts `reasoning_content` from DeepSeek-R1 streaming responses.
3
+ *
4
+ * **Why this exists:**
5
+ * DeepSeek-R1 (`deepseek-reasoner`) returns chain-of-thought as `delta.reasoning_content`
6
+ * inside each SSE chunk. The Vercel AI SDK's @ai-sdk/openai chat-completions parser
7
+ * (3.0.41) accumulates `reasoning_tokens` into `usage.reasoningTokens` (correct billing
8
+ * surface) but **does not** map `reasoning_content` text to any content-part or
9
+ * `reasoningText` field. Result: users pay for reasoning tokens but the reasoning
10
+ * text is silently dropped.
11
+ *
12
+ * **What this module does:**
13
+ * - Pure stream transformer: takes a DeepSeek SSE response body, tees it,
14
+ * passes one branch through unchanged (for AI SDK to consume normally),
15
+ * and parses the other branch to extract reasoning_content into an
16
+ * accumulator string.
17
+ * - Zero dependency on AI SDK internals — operates only on the raw SSE byte
18
+ * stream that AI SDK is about to consume.
19
+ *
20
+ * **Architectural note (Sam memo reconciliation):**
21
+ * Earlier scope memo referenced wrapping a "PR #425 translator output" layer.
22
+ * Verified: PR #425 is the frontend Settings UI fix; no Crewly-owned translator
23
+ * exists. The only seam Crewly owns between DeepSeek and the AI SDK is the
24
+ * `createOpenAI({ fetch })` upstream hook in model-manager.ts. This module is
25
+ * the body of that hook.
26
+ *
27
+ * **DeepClaude pattern reference (read-only):**
28
+ * Reasoning-content extraction approach borrowed conceptually from public
29
+ * DeepClaude pattern (HTTP proxy that splits R1 reasoning from final answer).
30
+ * **No code, no fork, no dependency** — pattern reference only, per Anthropic
31
+ * ToS guardrail flagged by Arch.
32
+ *
33
+ * @module services/agent/crewly-agent/deepseek-sse-transform
34
+ */
35
+
36
+ /**
37
+ * Shape of a single SSE `data:` payload from DeepSeek's chat-completions endpoint.
38
+ * Only the fields we read are typed; the rest is left open.
39
+ */
40
+ interface DeepseekSseChunk {
41
+ choices?: Array<{
42
+ delta?: {
43
+ content?: string;
44
+ reasoning_content?: string;
45
+ };
46
+ finish_reason?: string | null;
47
+ }>;
48
+ }
49
+
50
+ /**
51
+ * Result of teeing and parsing a DeepSeek SSE body.
52
+ *
53
+ * - `passthroughBody` is the un-tampered byte stream that should be handed
54
+ * to the consumer (AI SDK) as the response body.
55
+ * - `getReasoning()` returns the accumulated reasoning_content text once
56
+ * the underlying stream has fully drained. Calling it before drain
57
+ * returns whatever has been parsed so far.
58
+ */
59
+ export interface ParsedDeepseekSse {
60
+ passthroughBody: ReadableStream<Uint8Array>;
61
+ getReasoning(): string;
62
+ /** True once the parser has seen the SSE `[DONE]` sentinel or stream end. */
63
+ isDrained(): boolean;
64
+ }
65
+
66
+ /**
67
+ * Parse a single SSE event-block (one or more `data:` lines + a blank line).
68
+ *
69
+ * Returns the accumulated reasoning_content string from this block, or
70
+ * empty string if the block had no reasoning content. Returns `null` if
71
+ * the block is the `[DONE]` sentinel.
72
+ *
73
+ * @param block - One SSE event block (between blank-line delimiters)
74
+ * @returns reasoning_content string, or `null` if `[DONE]`
75
+ */
76
+ export function parseSseBlock(block: string): string | null {
77
+ const lines = block.split('\n');
78
+ let reasoning = '';
79
+ for (const line of lines) {
80
+ if (!line.startsWith('data:')) continue;
81
+ const payload = line.slice(5).trim();
82
+ if (!payload) continue;
83
+ if (payload === '[DONE]') return null;
84
+ try {
85
+ const chunk = JSON.parse(payload) as DeepseekSseChunk;
86
+ const r = chunk.choices?.[0]?.delta?.reasoning_content;
87
+ if (typeof r === 'string') {
88
+ reasoning += r;
89
+ }
90
+ } catch {
91
+ // Malformed JSON — skip silently; AI SDK consumer will surface
92
+ // any real error itself when it parses the same chunk.
93
+ }
94
+ }
95
+ return reasoning;
96
+ }
97
+
98
+ /**
99
+ * Tee a DeepSeek SSE response body and parse one branch for reasoning_content
100
+ * while passing the other branch through to the AI SDK consumer unchanged.
101
+ *
102
+ * The parser runs as a fire-and-forget background reader on the cloned stream.
103
+ * It accumulates reasoning text into an internal buffer that the caller can
104
+ * read via `getReasoning()` after the consumer has drained the passthrough.
105
+ *
106
+ * **Stream safety:**
107
+ * - The tee is symmetric: backpressure on the consumer branch does not
108
+ * stall the parser branch (and vice versa) thanks to ReadableStream.tee()
109
+ * internal buffering.
110
+ * - Parser errors are caught and logged but never propagated to the consumer.
111
+ *
112
+ * @param body - Raw SSE response body from DeepSeek
113
+ * @returns Object with passthrough body and reasoning accumulator
114
+ */
115
+ export function teeAndParse(body: ReadableStream<Uint8Array>): ParsedDeepseekSse {
116
+ const [consumerBranch, parserBranch] = body.tee();
117
+ let reasoning = '';
118
+ let drained = false;
119
+
120
+ // Background reader: drain parserBranch, parse SSE, accumulate reasoning.
121
+ // Errors are swallowed — consumer branch is independent and unaffected.
122
+ void (async () => {
123
+ const reader = parserBranch.getReader();
124
+ const decoder = new TextDecoder();
125
+ let buffer = '';
126
+ try {
127
+ while (true) {
128
+ const { done, value } = await reader.read();
129
+ if (done) break;
130
+ buffer += decoder.decode(value, { stream: true });
131
+ // SSE event blocks are delimited by blank lines (\n\n).
132
+ let blankIdx: number;
133
+ while ((blankIdx = buffer.indexOf('\n\n')) >= 0) {
134
+ const block = buffer.slice(0, blankIdx);
135
+ buffer = buffer.slice(blankIdx + 2);
136
+ const result = parseSseBlock(block);
137
+ if (result === null) {
138
+ drained = true;
139
+ } else if (result) {
140
+ reasoning += result;
141
+ }
142
+ }
143
+ }
144
+ // Flush trailing partial block (if SSE source ended mid-block — defensive)
145
+ if (buffer.trim()) {
146
+ const result = parseSseBlock(buffer);
147
+ if (result && result !== null) reasoning += result;
148
+ }
149
+ } catch (err) {
150
+ // Parser failures must not break consumer flow — log and exit.
151
+ // eslint-disable-next-line no-console
152
+ console.warn('[DeepSeek SSE Transform] parser branch error (consumer unaffected):', err);
153
+ } finally {
154
+ drained = true;
155
+ try {
156
+ reader.releaseLock();
157
+ } catch {
158
+ /* already released */
159
+ }
160
+ }
161
+ })();
162
+
163
+ return {
164
+ passthroughBody: consumerBranch,
165
+ getReasoning: () => reasoning,
166
+ isDrained: () => drained,
167
+ };
168
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Environment Isolation Service
3
+ *
4
+ * Strips sensitive environment variables (API keys, tokens, secrets) from
5
+ * child process environments when agents execute bash commands. Maintains
6
+ * an allowlist of safe variables and provides a mechanism to explicitly
7
+ * pass specific vars when needed.
8
+ *
9
+ * @module services/agent/crewly-agent/env-isolation.service
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const isolation = new EnvIsolationService();
14
+ * const safeEnv = isolation.createSafeEnv(process.env);
15
+ * // safeEnv has PATH, HOME, etc. but no ANTHROPIC_API_KEY
16
+ *
17
+ * const withOverrides = isolation.createSafeEnv(process.env, ['MY_CUSTOM_VAR']);
18
+ * // safeEnv + MY_CUSTOM_VAR explicitly included
19
+ * ```
20
+ */
21
+
22
+ /**
23
+ * Environment variables that are always safe to pass to child processes.
24
+ * These are essential for basic shell operation and don't contain secrets.
25
+ */
26
+ export const SAFE_ENV_ALLOWLIST: readonly string[] = [
27
+ // System essentials
28
+ 'PATH',
29
+ 'HOME',
30
+ 'USER',
31
+ 'LOGNAME',
32
+ 'SHELL',
33
+ 'TERM',
34
+ 'TERM_PROGRAM',
35
+ 'LANG',
36
+ 'LC_ALL',
37
+ 'LC_CTYPE',
38
+ 'TZ',
39
+ 'TMPDIR',
40
+ 'TEMP',
41
+ 'TMP',
42
+
43
+ // Node.js / runtime
44
+ 'NODE_ENV',
45
+ 'NODE_PATH',
46
+ 'NODE_OPTIONS',
47
+ 'NPM_CONFIG_PREFIX',
48
+ 'NVM_DIR',
49
+ 'NVM_BIN',
50
+
51
+ // Build tools
52
+ 'FORCE_COLOR',
53
+ 'NO_COLOR',
54
+ 'CI',
55
+ 'EDITOR',
56
+ 'VISUAL',
57
+ 'PAGER',
58
+
59
+ // Git (non-secret)
60
+ 'GIT_AUTHOR_NAME',
61
+ 'GIT_AUTHOR_EMAIL',
62
+ 'GIT_COMMITTER_NAME',
63
+ 'GIT_COMMITTER_EMAIL',
64
+
65
+ // Platform
66
+ 'HOSTNAME',
67
+ 'PWD',
68
+ 'OLDPWD',
69
+ 'SHLVL',
70
+ 'XDG_CONFIG_HOME',
71
+ 'XDG_DATA_HOME',
72
+ 'XDG_CACHE_HOME',
73
+ 'XDG_RUNTIME_DIR',
74
+
75
+ // macOS specifics
76
+ 'COMMAND_MODE',
77
+ '__CF_USER_TEXT_ENCODING',
78
+
79
+ // Python
80
+ 'PYTHONPATH',
81
+ 'VIRTUAL_ENV',
82
+
83
+ // Rust
84
+ 'CARGO_HOME',
85
+ 'RUSTUP_HOME',
86
+
87
+ // Go
88
+ 'GOPATH',
89
+ 'GOROOT',
90
+
91
+ // Java
92
+ 'JAVA_HOME',
93
+
94
+ // Homebrew
95
+ 'HOMEBREW_PREFIX',
96
+ 'HOMEBREW_CELLAR',
97
+ 'HOMEBREW_REPOSITORY',
98
+ ] as const;
99
+
100
+ /**
101
+ * Patterns that identify sensitive environment variable names.
102
+ * Variables matching these patterns are stripped from child processes.
103
+ */
104
+ export const SENSITIVE_ENV_PATTERNS: readonly RegExp[] = [
105
+ /api[_-]?key/i,
106
+ /api[_-]?secret/i,
107
+ /api[_-]?token/i,
108
+ /secret[_-]?key/i,
109
+ /access[_-]?key/i,
110
+ /access[_-]?token/i,
111
+ /auth[_-]?token/i,
112
+ /bearer[_-]?token/i,
113
+ /private[_-]?key/i,
114
+ /password/i,
115
+ /passwd/i,
116
+ /credential/i,
117
+ /^AWS_SECRET/i,
118
+ /^AWS_SESSION/i,
119
+ /^ANTHROPIC_API/i,
120
+ /^OPENAI_API/i,
121
+ /^GOOGLE_API/i,
122
+ /^GOOGLE_APPLICATION_CREDENTIALS/i,
123
+ /^STRIPE_SECRET/i,
124
+ /^SUPABASE_SERVICE_ROLE/i,
125
+ /^DATABASE_URL$/i,
126
+ /^REDIS_URL$/i,
127
+ /^MONGODB_URI$/i,
128
+ /^GITHUB_TOKEN$/i,
129
+ /^GH_TOKEN$/i,
130
+ /^NPM_TOKEN$/i,
131
+ /^DOCKER_PASSWORD$/i,
132
+ /^DOCKER_AUTH$/i,
133
+ /connection[_-]?string/i,
134
+ /^SENTRY_DSN$/i,
135
+ /^DATADOG_API_KEY$/i,
136
+ /^SENDGRID_API_KEY$/i,
137
+ /^TWILIO_AUTH_TOKEN$/i,
138
+ /^SLACK_BOT_TOKEN$/i,
139
+ /^SLACK_TOKEN$/i,
140
+ /^DISCORD_TOKEN$/i,
141
+ /^WEBHOOK_SECRET$/i,
142
+ /^SIGNING_SECRET$/i,
143
+ /^ENCRYPTION_KEY$/i,
144
+ /^JWT_SECRET$/i,
145
+ /^SESSION_SECRET$/i,
146
+ ] as const;
147
+
148
+ /**
149
+ * Service that creates sanitized environment objects for child processes.
150
+ */
151
+ export class EnvIsolationService {
152
+ private readonly allowlist: Set<string>;
153
+ private readonly sensitivePatterns: readonly RegExp[];
154
+
155
+ /**
156
+ * Creates a new EnvIsolationService.
157
+ *
158
+ * @param additionalSafeVars - Extra variable names to always allow
159
+ * @param additionalSensitivePatterns - Extra patterns to block
160
+ */
161
+ constructor(
162
+ additionalSafeVars?: string[],
163
+ additionalSensitivePatterns?: RegExp[],
164
+ ) {
165
+ this.allowlist = new Set([...SAFE_ENV_ALLOWLIST, ...(additionalSafeVars || [])]);
166
+ this.sensitivePatterns = additionalSensitivePatterns
167
+ ? [...SENSITIVE_ENV_PATTERNS, ...additionalSensitivePatterns]
168
+ : SENSITIVE_ENV_PATTERNS;
169
+ }
170
+
171
+ /**
172
+ * Creates a safe environment object by filtering out sensitive variables.
173
+ *
174
+ * Strategy:
175
+ * 1. Start with all env vars
176
+ * 2. Keep vars on the allowlist
177
+ * 3. Remove vars matching sensitive patterns
178
+ * 4. Add explicitly requested vars (overrides)
179
+ *
180
+ * @param sourceEnv - Source environment (typically process.env)
181
+ * @param explicitVars - Variable names to explicitly include even if they'd be filtered
182
+ * @returns Sanitized environment object safe for child processes
183
+ */
184
+ createSafeEnv(
185
+ sourceEnv: Record<string, string | undefined>,
186
+ explicitVars?: string[],
187
+ ): Record<string, string> {
188
+ const safeEnv: Record<string, string> = {};
189
+ const explicitSet = new Set(explicitVars || []);
190
+
191
+ for (const [key, value] of Object.entries(sourceEnv)) {
192
+ if (value === undefined) continue;
193
+
194
+ // Always include explicitly requested vars
195
+ if (explicitSet.has(key)) {
196
+ safeEnv[key] = value;
197
+ continue;
198
+ }
199
+
200
+ // Include if on allowlist
201
+ if (this.allowlist.has(key)) {
202
+ safeEnv[key] = value;
203
+ continue;
204
+ }
205
+
206
+ // Skip if matches a sensitive pattern
207
+ if (this.isSensitive(key)) {
208
+ continue;
209
+ }
210
+
211
+ // Default: include non-sensitive vars not on either list
212
+ safeEnv[key] = value;
213
+ }
214
+
215
+ return safeEnv;
216
+ }
217
+
218
+ /**
219
+ * Checks if an environment variable name matches any sensitive pattern.
220
+ *
221
+ * @param varName - Environment variable name to check
222
+ * @returns True if the variable is considered sensitive
223
+ */
224
+ isSensitive(varName: string): boolean {
225
+ return this.sensitivePatterns.some((pattern) => pattern.test(varName));
226
+ }
227
+
228
+ /**
229
+ * Returns the list of sensitive variable names found in the given environment.
230
+ * Useful for audit logging.
231
+ *
232
+ * @param sourceEnv - Environment to scan
233
+ * @returns Array of sensitive variable names found
234
+ */
235
+ findSensitiveVars(sourceEnv: Record<string, string | undefined>): string[] {
236
+ return Object.keys(sourceEnv).filter((key) => this.isSensitive(key));
237
+ }
238
+
239
+ /**
240
+ * Returns the current allowlist as an array.
241
+ * @returns Copy of the safe variable allowlist
242
+ */
243
+ getAllowlist(): string[] {
244
+ return Array.from(this.allowlist);
245
+ }
246
+ }
@@ -0,0 +1,280 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { InProcessLogBuffer } from './in-process-log-buffer.js';
6
+
7
+ // Constants inlined here too — the standalone runtime owns its own values.
8
+ const CREWLY_CONSTANTS = {
9
+ PATHS: {
10
+ CREWLY_HOME: '.crewly',
11
+ LOGS_DIR: 'logs',
12
+ },
13
+ } as const;
14
+
15
+ const LOG_ROTATION_CONSTANTS = {
16
+ SESSIONS_LOG_DIR: 'sessions',
17
+ } as const;
18
+
19
+ describe('InProcessLogBuffer', () => {
20
+ let buffer: InProcessLogBuffer;
21
+
22
+ beforeEach(() => {
23
+ InProcessLogBuffer.resetInstance();
24
+ buffer = InProcessLogBuffer.getInstance();
25
+ });
26
+
27
+ describe('singleton', () => {
28
+ it('should return the same instance', () => {
29
+ const a = InProcessLogBuffer.getInstance();
30
+ const b = InProcessLogBuffer.getInstance();
31
+ expect(a).toBe(b);
32
+ });
33
+
34
+ it('should return new instance after reset', () => {
35
+ const a = InProcessLogBuffer.getInstance();
36
+ InProcessLogBuffer.resetInstance();
37
+ const b = InProcessLogBuffer.getInstance();
38
+ expect(a).not.toBe(b);
39
+ });
40
+ });
41
+
42
+ describe('append', () => {
43
+ it('should create session on first append', () => {
44
+ expect(buffer.hasSession('test')).toBe(false);
45
+ buffer.append('test', 'info', 'hello');
46
+ expect(buffer.hasSession('test')).toBe(true);
47
+ });
48
+
49
+ it('should enforce ring buffer limit', () => {
50
+ for (let i = 0; i < 600; i++) {
51
+ buffer.append('test', 'info', `line ${i}`);
52
+ }
53
+ const output = buffer.capture('test', 600);
54
+ const lines = output.split('\n');
55
+ expect(lines.length).toBe(500);
56
+ expect(lines[lines.length - 1]).toContain('line 599');
57
+ });
58
+ });
59
+
60
+ describe('capture', () => {
61
+ it('should return placeholder for empty session', () => {
62
+ buffer.registerSession('empty');
63
+ const output = buffer.capture('empty');
64
+ expect(output).toContain('No output yet');
65
+ });
66
+
67
+ it('should return placeholder for unknown session', () => {
68
+ const output = buffer.capture('nonexistent');
69
+ expect(output).toContain('No output yet');
70
+ });
71
+
72
+ it('should format entries with timestamps', () => {
73
+ buffer.append('test', 'info', 'hello world');
74
+ const output = buffer.capture('test');
75
+ expect(output).toMatch(/\[\d{2}:\d{2}:\d{2}\.\d{3}\] hello world/);
76
+ });
77
+
78
+ it('should prefix error and warn levels', () => {
79
+ buffer.append('test', 'error', 'something broke');
80
+ buffer.append('test', 'warn', 'be careful');
81
+ buffer.append('test', 'debug', 'details');
82
+ const output = buffer.capture('test');
83
+ expect(output).toContain('ERROR: something broke');
84
+ expect(output).toContain('WARN: be careful');
85
+ expect(output).toContain('DEBUG: details');
86
+ });
87
+
88
+ it('should respect lines parameter', () => {
89
+ buffer.append('test', 'info', 'line 1');
90
+ buffer.append('test', 'info', 'line 2');
91
+ buffer.append('test', 'info', 'line 3');
92
+ const output = buffer.capture('test', 2);
93
+ const lines = output.split('\n');
94
+ expect(lines.length).toBe(2);
95
+ expect(lines[1]).toContain('line 3');
96
+ });
97
+ });
98
+
99
+ describe('session management', () => {
100
+ it('should register empty session', () => {
101
+ buffer.registerSession('new');
102
+ expect(buffer.hasSession('new')).toBe(true);
103
+ expect(buffer.capture('new')).toContain('No output yet');
104
+ });
105
+
106
+ it('should remove session', () => {
107
+ buffer.append('test', 'info', 'data');
108
+ buffer.removeSession('test');
109
+ expect(buffer.hasSession('test')).toBe(false);
110
+ });
111
+
112
+ it('should list session names', () => {
113
+ buffer.registerSession('a');
114
+ buffer.registerSession('b');
115
+ buffer.append('c', 'info', 'test');
116
+ expect(buffer.getSessionNames().sort()).toEqual(['a', 'b', 'c']);
117
+ });
118
+
119
+ it('should clear all sessions', () => {
120
+ buffer.registerSession('a');
121
+ buffer.registerSession('b');
122
+ buffer.clear();
123
+ expect(buffer.getSessionNames()).toEqual([]);
124
+ });
125
+ });
126
+
127
+ describe('EventEmitter data events', () => {
128
+ it('should emit data event on append', () => new Promise<void>((resolve, reject) => {
129
+ buffer.on('data', (sessionName: string, formattedLine: string) => {
130
+ try {
131
+ expect(sessionName).toBe('test-session');
132
+ expect(formattedLine).toContain('hello world');
133
+ expect(formattedLine).toMatch(/\[\d{2}:\d{2}:\d{2}\.\d{3}\] hello world/);
134
+ resolve();
135
+ } catch (err) { reject(err); }
136
+ });
137
+ buffer.append('test-session', 'info', 'hello world');
138
+ }));
139
+
140
+ it('should emit data event with level prefix for errors', () => new Promise<void>((resolve, reject) => {
141
+ buffer.on('data', (sessionName: string, formattedLine: string) => {
142
+ try {
143
+ expect(sessionName).toBe('test-session');
144
+ expect(formattedLine).toContain('ERROR: something broke');
145
+ resolve();
146
+ } catch (err) { reject(err); }
147
+ });
148
+ buffer.append('test-session', 'error', 'something broke');
149
+ }));
150
+
151
+ it('should emit data events for each append', () => {
152
+ const events: string[] = [];
153
+ buffer.on('data', (_session: string, line: string) => {
154
+ events.push(line);
155
+ });
156
+
157
+ buffer.append('s1', 'info', 'first');
158
+ buffer.append('s2', 'warn', 'second');
159
+
160
+ expect(events.length).toBe(2);
161
+ expect(events[0]).toContain('first');
162
+ expect(events[1]).toContain('WARN: second');
163
+ });
164
+
165
+ it('should include session name in data event', () => {
166
+ const sessions: string[] = [];
167
+ buffer.on('data', (session: string) => {
168
+ sessions.push(session);
169
+ });
170
+
171
+ buffer.append('agent-a', 'info', 'msg1');
172
+ buffer.append('agent-b', 'info', 'msg2');
173
+
174
+ expect(sessions).toEqual(['agent-a', 'agent-b']);
175
+ });
176
+
177
+ it('should clean up listeners on reset', () => {
178
+ const handler = vi.fn();
179
+ buffer.on('data', handler);
180
+
181
+ InProcessLogBuffer.resetInstance();
182
+ const newBuffer = InProcessLogBuffer.getInstance();
183
+ newBuffer.append('test', 'info', 'after reset');
184
+
185
+ // Old handler should NOT be called since instance was reset
186
+ expect(handler).not.toHaveBeenCalled();
187
+ });
188
+ });
189
+
190
+ describe('file persistence', () => {
191
+ const sessionLogsDir = path.join(
192
+ os.homedir(),
193
+ CREWLY_CONSTANTS.PATHS.CREWLY_HOME,
194
+ CREWLY_CONSTANTS.PATHS.LOGS_DIR,
195
+ LOG_ROTATION_CONSTANTS.SESSIONS_LOG_DIR,
196
+ );
197
+
198
+ const testSessionName = `__test-inprocess-${Date.now()}`;
199
+
200
+ afterEach(() => {
201
+ // Clean up test log file
202
+ const logPath = path.join(sessionLogsDir, `${testSessionName}.log`);
203
+ try { fs.unlinkSync(logPath); } catch { /* may not exist */ }
204
+ });
205
+
206
+ it('should create a log file on registerSession', (done) => {
207
+ buffer.registerSession(testSessionName);
208
+ const logPath = path.join(sessionLogsDir, `${testSessionName}.log`);
209
+ // WriteStream opens the file lazily — give it a tick to flush the header
210
+ setTimeout(() => {
211
+ expect(fs.existsSync(logPath)).toBe(true);
212
+ done();
213
+ }, 100);
214
+ });
215
+
216
+ it('should write log entries to disk on append', (done) => {
217
+ buffer.registerSession(testSessionName);
218
+ buffer.append(testSessionName, 'info', 'disk test message');
219
+ buffer.append(testSessionName, 'error', 'disk error message');
220
+
221
+ // WriteStream is async — give it a tick to flush
222
+ setTimeout(() => {
223
+ const logPath = path.join(sessionLogsDir, `${testSessionName}.log`);
224
+ const content = fs.readFileSync(logPath, 'utf8');
225
+ expect(content).toContain('SESSION STARTED');
226
+ expect(content).toContain('disk test message');
227
+ expect(content).toContain('ERROR: disk error message');
228
+ done();
229
+ }, 100);
230
+ });
231
+
232
+ it('should write SESSION ENDED marker on removeSession', (done) => {
233
+ buffer.registerSession(testSessionName);
234
+ buffer.append(testSessionName, 'info', 'before shutdown');
235
+ buffer.removeSession(testSessionName);
236
+
237
+ setTimeout(() => {
238
+ const logPath = path.join(sessionLogsDir, `${testSessionName}.log`);
239
+ const content = fs.readFileSync(logPath, 'utf8');
240
+ expect(content).toContain('SESSION ENDED');
241
+ done();
242
+ }, 100);
243
+ });
244
+
245
+ it('should write RESTARTED marker when session is re-registered', (done) => {
246
+ buffer.registerSession(testSessionName);
247
+ buffer.append(testSessionName, 'info', 'first run');
248
+ buffer.removeSession(testSessionName);
249
+
250
+ // Re-register same session (simulates restart)
251
+ setTimeout(() => {
252
+ buffer.registerSession(testSessionName);
253
+ buffer.append(testSessionName, 'info', 'second run');
254
+
255
+ setTimeout(() => {
256
+ const logPath = path.join(sessionLogsDir, `${testSessionName}.log`);
257
+ const content = fs.readFileSync(logPath, 'utf8');
258
+ expect(content).toContain('SESSION STARTED');
259
+ expect(content).toContain('first run');
260
+ expect(content).toContain('SESSION RESTARTED');
261
+ expect(content).toContain('second run');
262
+ done();
263
+ }, 100);
264
+ }, 100);
265
+ });
266
+
267
+ it('should include full ISO timestamp in file entries', (done) => {
268
+ buffer.registerSession(testSessionName);
269
+ buffer.append(testSessionName, 'info', 'timestamp check');
270
+
271
+ setTimeout(() => {
272
+ const logPath = path.join(sessionLogsDir, `${testSessionName}.log`);
273
+ const content = fs.readFileSync(logPath, 'utf8');
274
+ // File entries should have full ISO timestamp like "2026-03-18T16:01:00.000Z"
275
+ expect(content).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z.*timestamp check/);
276
+ done();
277
+ }, 100);
278
+ });
279
+ });
280
+ });