crewly 1.20.144 → 1.20.146

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 (106) hide show
  1. package/README.md +5 -1
  2. package/config/roles/orchestrator/prompt.md +9 -0
  3. package/config/skills/orchestrator/harness-login/SKILL.md +88 -0
  4. package/config/skills/orchestrator/harness-login/execute.sh +75 -0
  5. package/config/skills/orchestrator/harness-login/execute.test.sh +72 -0
  6. package/dist/backend/backend/src/constants.d.ts +12 -0
  7. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  8. package/dist/backend/backend/src/constants.js +12 -0
  9. package/dist/backend/backend/src/constants.js.map +1 -1
  10. package/dist/backend/backend/src/controllers/harness/harness.routes.d.ts +1 -0
  11. package/dist/backend/backend/src/controllers/harness/harness.routes.d.ts.map +1 -1
  12. package/dist/backend/backend/src/controllers/harness/harness.routes.js +3 -0
  13. package/dist/backend/backend/src/controllers/harness/harness.routes.js.map +1 -1
  14. package/dist/backend/backend/src/controllers/harness/owner-login.controller.d.ts +61 -0
  15. package/dist/backend/backend/src/controllers/harness/owner-login.controller.d.ts.map +1 -0
  16. package/dist/backend/backend/src/controllers/harness/owner-login.controller.js +133 -0
  17. package/dist/backend/backend/src/controllers/harness/owner-login.controller.js.map +1 -0
  18. package/dist/backend/backend/src/index.d.ts.map +1 -1
  19. package/dist/backend/backend/src/index.js +4 -1
  20. package/dist/backend/backend/src/index.js.map +1 -1
  21. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  22. package/dist/backend/backend/src/services/agent/agent-registration.service.js +23 -1
  23. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  24. package/dist/backend/backend/src/services/agent/codex-runtime.service.d.ts +11 -1
  25. package/dist/backend/backend/src/services/agent/codex-runtime.service.d.ts.map +1 -1
  26. package/dist/backend/backend/src/services/agent/codex-runtime.service.js +18 -0
  27. package/dist/backend/backend/src/services/agent/codex-runtime.service.js.map +1 -1
  28. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +12 -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 +18 -3
  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/agent/runtime-agent.service.abstract.d.ts +29 -0
  33. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
  34. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +48 -0
  35. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
  36. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  37. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
  38. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  39. package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts +14 -0
  40. package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
  41. package/dist/backend/backend/src/services/cloud/cloud-sync.service.js +18 -2
  42. package/dist/backend/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
  43. package/dist/backend/backend/src/services/harness/harness-relogin.service.d.ts +147 -5
  44. package/dist/backend/backend/src/services/harness/harness-relogin.service.d.ts.map +1 -1
  45. package/dist/backend/backend/src/services/harness/harness-relogin.service.js +210 -20
  46. package/dist/backend/backend/src/services/harness/harness-relogin.service.js.map +1 -1
  47. package/dist/backend/backend/src/services/harness/owner-login-request.d.ts +87 -0
  48. package/dist/backend/backend/src/services/harness/owner-login-request.d.ts.map +1 -0
  49. package/dist/backend/backend/src/services/harness/owner-login-request.js +191 -0
  50. package/dist/backend/backend/src/services/harness/owner-login-request.js.map +1 -0
  51. package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts +68 -9
  52. package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts.map +1 -1
  53. package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.js +121 -11
  54. package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.js.map +1 -1
  55. package/dist/backend/backend/src/utils/agent-reply-filter.utils.d.ts +75 -0
  56. package/dist/backend/backend/src/utils/agent-reply-filter.utils.d.ts.map +1 -0
  57. package/dist/backend/backend/src/utils/agent-reply-filter.utils.js +127 -0
  58. package/dist/backend/backend/src/utils/agent-reply-filter.utils.js.map +1 -0
  59. package/dist/backend/build-info.json +2 -2
  60. package/dist/cli/backend/src/constants.d.ts +12 -0
  61. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  62. package/dist/cli/backend/src/constants.js +12 -0
  63. package/dist/cli/backend/src/constants.js.map +1 -1
  64. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  65. package/dist/cli/backend/src/services/agent/agent-registration.service.js +23 -1
  66. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
  67. package/dist/cli/backend/src/services/agent/codex-runtime.service.d.ts +11 -1
  68. package/dist/cli/backend/src/services/agent/codex-runtime.service.d.ts.map +1 -1
  69. package/dist/cli/backend/src/services/agent/codex-runtime.service.js +18 -0
  70. package/dist/cli/backend/src/services/agent/codex-runtime.service.js.map +1 -1
  71. package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +12 -0
  72. package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
  73. package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +18 -3
  74. package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
  75. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.d.ts +29 -0
  76. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
  77. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js +48 -0
  78. package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
  79. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  80. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
  81. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  82. package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts +14 -0
  83. package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
  84. package/dist/cli/backend/src/services/cloud/cloud-sync.service.js +18 -2
  85. package/dist/cli/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
  86. package/dist/cli/backend/src/services/harness/harness-relogin.service.d.ts +147 -5
  87. package/dist/cli/backend/src/services/harness/harness-relogin.service.d.ts.map +1 -1
  88. package/dist/cli/backend/src/services/harness/harness-relogin.service.js +210 -20
  89. package/dist/cli/backend/src/services/harness/harness-relogin.service.js.map +1 -1
  90. package/dist/cli/backend/src/services/harness/owner-login-request.d.ts +87 -0
  91. package/dist/cli/backend/src/services/harness/owner-login-request.d.ts.map +1 -0
  92. package/dist/cli/backend/src/services/harness/owner-login-request.js +191 -0
  93. package/dist/cli/backend/src/services/harness/owner-login-request.js.map +1 -0
  94. package/dist/cli/backend/src/utils/agent-reply-filter.utils.d.ts +75 -0
  95. package/dist/cli/backend/src/utils/agent-reply-filter.utils.d.ts.map +1 -0
  96. package/dist/cli/backend/src/utils/agent-reply-filter.utils.js +127 -0
  97. package/dist/cli/backend/src/utils/agent-reply-filter.utils.js.map +1 -0
  98. package/package.json +1 -1
  99. package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +39 -0
  100. package/packages/crewly-agent/src/runtime/agent-runner.service.ts +17 -17
  101. package/packages/crewly-agent/src/runtime/interactive-login-guard.test.ts +60 -0
  102. package/packages/crewly-agent/src/runtime/interactive-login-guard.ts +76 -0
  103. package/packages/crewly-agent/src/runtime/reply-tool-calls.test.ts +53 -0
  104. package/packages/crewly-agent/src/runtime/reply-tool-calls.ts +76 -0
  105. package/packages/crewly-agent/src/runtime/tool-registry.test.ts +8 -0
  106. package/packages/crewly-agent/src/runtime/tool-registry.ts +14 -0
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Interactive harness login guard for the `bash_exec` tool.
3
+ *
4
+ * `claude setup-token`, `claude /login`, `claude auth login`, `codex login`
5
+ * and an Antigravity (`agy`) login print a link and then wait for the user
6
+ * to paste a code into the same process. `bash_exec` is one-shot: the
7
+ * process is killed when the tool call returns (or times out), so a code the
8
+ * owner pastes back later can never be redeemed.
9
+ *
10
+ * Incident 2026-09-26: the orchestrator ran `claude setup-token` this way,
11
+ * sent the owner the link, and every authorization code the owner pasted
12
+ * back went stale. Crewly's login broker keeps the login alive in its own
13
+ * terminal; the orchestrator reaches it with the `harness-login` skill.
14
+ *
15
+ * Status checks (`claude auth status`, `codex login status`) stay allowed.
16
+ *
17
+ * @module runtime/interactive-login-guard
18
+ */
19
+
20
+ /** Where a command word may start: line start, a separator, a quote, or a path slash. */
21
+ const CMD_START = String.raw`(?:^|[\s;&|()'"\x60/])`;
22
+
23
+ /** Flags without values between the command and its subcommand (`codex --verbose login`). */
24
+ const FLAGS = String.raw`(?:-{1,2}[\w-]+\s+)*`;
25
+
26
+ /** Interactive login commands, each with a label for the refusal message. */
27
+ const INTERACTIVE_LOGIN_PATTERNS: ReadonlyArray<{ pattern: RegExp; label: string }> = [
28
+ { pattern: new RegExp(`${CMD_START}claude\\s[^\\n;&|]*?\\bsetup-token\\b`, 'i'), label: 'claude setup-token' },
29
+ { pattern: new RegExp(`${CMD_START}claude(?=\\s)[^\\n;&|]*?[\\s'"]/login\\b`, 'i'), label: 'claude /login' },
30
+ { pattern: new RegExp(`${CMD_START}claude\\s+${FLAGS}(?:auth\\s+)?login\\b`, 'i'), label: 'claude auth login' },
31
+ // `/login` typed into claude through a pipe: `echo /login | claude`.
32
+ { pattern: new RegExp(`/login\\b[^\\n;&]*\\|\\s*(?:\\S*/)?claude\\b`, 'i'), label: 'claude /login' },
33
+ { pattern: new RegExp(`${CMD_START}codex\\s+${FLAGS}login\\b(?!\\s+status\\b)`, 'i'), label: 'codex login' },
34
+ { pattern: new RegExp(`${CMD_START}(?:agy|antigravity)\\s+${FLAGS}(?:auth\\s+)?login\\b`, 'i'), label: 'agy login' },
35
+ ];
36
+
37
+ /** Where the orchestrator's login skill lives (relative to the Crewly project root). */
38
+ export const HARNESS_LOGIN_SKILL_PATH = 'config/skills/orchestrator/harness-login/execute.sh';
39
+
40
+ /**
41
+ * Which interactive harness login a shell command starts, if any.
42
+ *
43
+ * @param command - Raw shell command
44
+ * @returns A label such as "claude setup-token", or null when the command is not one
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * detectInteractiveLogin('claude setup-token'); // 'claude setup-token'
49
+ * detectInteractiveLogin('codex login status'); // null
50
+ * ```
51
+ */
52
+ export function detectInteractiveLogin(command: string): string | null {
53
+ if (typeof command !== 'string' || !command) return null;
54
+ for (const { pattern, label } of INTERACTIVE_LOGIN_PATTERNS) {
55
+ if (pattern.test(command)) return label;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * The refusal `bash_exec` returns for an interactive harness login, or null
62
+ * when the command may run.
63
+ *
64
+ * @param command - Raw shell command
65
+ * @returns Refusal message pointing at the `harness-login` skill, or null
66
+ */
67
+ export function checkInteractiveLoginCommand(command: string): string | null {
68
+ const label = detectInteractiveLogin(command);
69
+ if (!label) return null;
70
+ return (
71
+ `Refused: \`${label}\` is an interactive login. It would be killed when this tool call returns, so the code the owner pastes back would be stale. ` +
72
+ `Use the harness-login skill instead: bash ${HARNESS_LOGIN_SKILL_PATH} --harness claude|codex [--switch-account] ` +
73
+ '(orchestrator only — any other agent asks the orchestrator). Crewly keeps the login alive, sends the owner the link and types in the code they paste. ' +
74
+ 'Do not retry this command or a variation.'
75
+ );
76
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Tests for reply tool detection and the empty-text summary fallback rule.
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { SUMMARY_FALLBACK_PROMPT, isReplyToolCall, shouldRequestSummaryFallback } from './reply-tool-calls.js';
7
+
8
+ const REPLY_CHAT = {
9
+ toolName: 'bash_exec',
10
+ args: { command: `bash config/skills/orchestrator/reply-chat/execute.sh '{"conversationId":"a721f48d","content":"链接在下面"}'` },
11
+ };
12
+
13
+ describe('isReplyToolCall', () => {
14
+ it('recognises reply tools and reply skills run through the shell', () => {
15
+ expect(isReplyToolCall({ toolName: 'reply_slack' })).toBe(true);
16
+ expect(isReplyToolCall({ toolName: 'reply-slack' })).toBe(true);
17
+ expect(isReplyToolCall(REPLY_CHAT)).toBe(true);
18
+ expect(isReplyToolCall({ toolName: 'bash_exec', args: { command: 'bash config/skills/agent/core/reply-channel/execute.sh --text hi' } })).toBe(true);
19
+ expect(isReplyToolCall({ toolName: 'Bash', args: { command: 'bash config/skills/orchestrator/reply-slack/execute.sh x' } })).toBe(true);
20
+ });
21
+
22
+ it('ignores other tools and other shell commands', () => {
23
+ expect(isReplyToolCall({ toolName: 'get_team_status' })).toBe(false);
24
+ expect(isReplyToolCall({ toolName: 'bash_exec', args: { command: 'claude auth status' } })).toBe(false);
25
+ expect(isReplyToolCall({ toolName: 'bash_exec', args: {} })).toBe(false);
26
+ expect(isReplyToolCall({ toolName: 'read_file', args: { command: 'reply-chat' } })).toBe(false);
27
+ });
28
+ });
29
+
30
+ describe('shouldRequestSummaryFallback', () => {
31
+ it('asks for a closing reply when tools ran and nothing was said', () => {
32
+ expect(shouldRequestSummaryFallback('', [{ toolName: 'bash_exec', args: { command: 'claude --version' } }])).toBe(true);
33
+ });
34
+
35
+ it('stays silent when the reply already went out with a reply tool (2026-09-26 noise)', () => {
36
+ expect(shouldRequestSummaryFallback('', [{ toolName: 'bash_exec', args: { command: 'claude --version' } }, REPLY_CHAT])).toBe(false);
37
+ });
38
+
39
+ it('never runs when there is text or no tool call', () => {
40
+ expect(shouldRequestSummaryFallback('Done.', [REPLY_CHAT])).toBe(false);
41
+ expect(shouldRequestSummaryFallback('', [])).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe('SUMMARY_FALLBACK_PROMPT', () => {
46
+ it('asks for a short user-facing reply in their language, not a status report', () => {
47
+ expect(SUMMARY_FALLBACK_PROMPT).toMatch(/in their language/);
48
+ expect(SUMMARY_FALLBACK_PROMPT).toMatch(/No status report/);
49
+ expect(SUMMARY_FALLBACK_PROMPT).toMatch(/do not say you sent anything unless a tool actually sent it/);
50
+ expect(SUMMARY_FALLBACK_PROMPT).not.toMatch(/summarize what you just did/i);
51
+ expect(SUMMARY_FALLBACK_PROMPT).not.toMatch(/report-status/);
52
+ });
53
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Reply tool calls and the empty-text summary fallback.
3
+ *
4
+ * When a turn ends with tool calls but no text, the runner used to ask the
5
+ * model to "summarize what you just did, what you found, and any issues".
6
+ * For an agent answering a person, that summary is what reaches them: the
7
+ * owner's Slack DM got several English meta reports in a row — "I've sent the
8
+ * reply. Here's my status: *What the user asked* … *What I did* … *Next step*
9
+ * … *Note on the ticket instruction*" (2026-09-26). The status-report shape
10
+ * then stayed in the conversation history and the model kept repeating it.
11
+ *
12
+ * Two changes live here:
13
+ * - {@link shouldRequestSummaryFallback}: no fallback when the turn already
14
+ * sent its reply with a reply tool — the answer is out; anything more is
15
+ * noise.
16
+ * - {@link SUMMARY_FALLBACK_PROMPT}: the fallback asks for a short reply to
17
+ * the person, in their language, not a status report.
18
+ *
19
+ * @module runtime/reply-tool-calls
20
+ */
21
+
22
+ /** The slice of a tool call record these helpers read. */
23
+ export interface ToolCallLike {
24
+ toolName: string;
25
+ args?: Record<string, unknown>;
26
+ }
27
+
28
+ /** Tools that post a reply to a person by themselves. */
29
+ const REPLY_TOOL_NAMES: ReadonlySet<string> = new Set(['reply_slack', 'reply-slack', 'reply_chat', 'reply-chat', 'send_chat_response']);
30
+
31
+ /** Shell tools whose command can run a reply skill. */
32
+ const SHELL_TOOL_NAMES: ReadonlySet<string> = new Set(['bash_exec', 'bash', 'Bash', 'shell']);
33
+
34
+ /** Reply skills run through the shell (`bash config/skills/…/reply-chat/execute.sh …`). */
35
+ const REPLY_SKILL_PATTERN = /\b(?:reply-chat|reply-slack|reply-gchat|reply-remote|reply-channel|send-chat-response)\b/;
36
+
37
+ /**
38
+ * Whether a tool call sent a reply to a person.
39
+ *
40
+ * @param call - Tool call record
41
+ * @returns True for a reply tool, or a shell call that runs a reply skill
42
+ */
43
+ export function isReplyToolCall(call: ToolCallLike): boolean {
44
+ if (REPLY_TOOL_NAMES.has(call.toolName)) return true;
45
+ if (!SHELL_TOOL_NAMES.has(call.toolName)) return false;
46
+ const command = call.args?.command;
47
+ return typeof command === 'string' && REPLY_SKILL_PATTERN.test(command);
48
+ }
49
+
50
+ /**
51
+ * Whether the runner should ask the model for a closing text.
52
+ *
53
+ * Only when the turn produced no text, made tool calls, and none of them
54
+ * sent the reply already.
55
+ *
56
+ * @param text - The turn's final text
57
+ * @param toolCalls - The turn's tool calls
58
+ * @returns True when the fallback should run
59
+ */
60
+ export function shouldRequestSummaryFallback(text: string, toolCalls: readonly ToolCallLike[]): boolean {
61
+ if (text || toolCalls.length === 0) return false;
62
+ return !toolCalls.some(isReplyToolCall);
63
+ }
64
+
65
+ /**
66
+ * Follow-up prompt when a turn ended with tool calls and no text: a short
67
+ * answer to whoever wrote, in their language — never a status report, never
68
+ * a claim of having sent something.
69
+ */
70
+ export const SUMMARY_FALLBACK_PROMPT =
71
+ '请直接回复给你发消息的人:用对方的语言,简短(1-3 句),只写答案或结果本身。' +
72
+ '不要写状态汇报(不要"用户问了什么 / 我做了什么 / 下一步"之类的小标题),不要描述你调用了哪些工具,' +
73
+ '也不要说你已经发送了什么——除非这一轮真的用工具发出去了。\n' +
74
+ 'Reply directly to the person who wrote to you: in their language, short (1-3 sentences), only the answer or result itself. ' +
75
+ 'No status report (no "What the user asked / What I did / Next step" sections), do not describe your tool calls, ' +
76
+ 'and do not say you sent anything unless a tool actually sent it in this turn.';
@@ -2245,6 +2245,14 @@ describe('Tool Registry', () => {
2245
2245
  expect(result.error).toContain('blocked');
2246
2246
  });
2247
2247
 
2248
+ it('refuses interactive harness logins and points at harness-login (2026-09-26)', async () => {
2249
+ const result = await bashTools.bash_exec.execute({ command: 'claude setup-token' }) as any;
2250
+ expect(result.success).toBe(false);
2251
+ expect(result.exitCode).toBe(126);
2252
+ expect(result.error).toContain('harness-login');
2253
+ expect(result.stdout).toBe('');
2254
+ });
2255
+
2248
2256
  it('should handle command failure gracefully', async () => {
2249
2257
  const result = await bashTools.bash_exec.execute({ command: 'false' }) as any;
2250
2258
  expect(result.success).toBe(false);
@@ -21,6 +21,7 @@ import { OutputFilterService } from './output-filter.service.js';
21
21
  import { createWebSearchTool } from './web-search.tool.js';
22
22
  import { createComputerTool } from './computer.tool.js';
23
23
  import { createDesktopTaskTool } from './desktop-task.tool.js';
24
+ import { checkInteractiveLoginCommand } from './interactive-login-guard.js';
24
25
 
25
26
  /** TTL for delegation idle event subscriptions (minutes) */
26
27
  const DELEGATION_SUBSCRIPTION_TTL_MINUTES = 120;
@@ -1821,6 +1822,19 @@ export function createTools(client: CrewlyApiClient, sessionName: string, projec
1821
1822
  execute: async ({ command, cwd, timeout }) => {
1822
1823
  const cmd = command as string;
1823
1824
 
1825
+ // Interactive harness logins die with this one-shot call; the owner's
1826
+ // pasted code would go stale (2026-09-26). Point at harness-login.
1827
+ const loginRefusal = checkInteractiveLoginCommand(cmd);
1828
+ if (loginRefusal) {
1829
+ return {
1830
+ success: false,
1831
+ exitCode: 126,
1832
+ stdout: '',
1833
+ stderr: loginRefusal,
1834
+ error: loginRefusal,
1835
+ };
1836
+ }
1837
+
1824
1838
  // Check command against blocklist before execution
1825
1839
  const blockReason = validateBashCommand(cmd);
1826
1840
  if (blockReason) {