@tickernelz/paperclip-pro-adapter-claude-local 2026.925.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 (206) hide show
  1. package/LICENSE +22 -0
  2. package/dist/cli/format-event.d.ts +2 -0
  3. package/dist/cli/format-event.d.ts.map +1 -0
  4. package/dist/cli/format-event.js +141 -0
  5. package/dist/cli/format-event.js.map +1 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +2 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/cli/quota-probe.d.ts +3 -0
  11. package/dist/cli/quota-probe.d.ts.map +1 -0
  12. package/dist/cli/quota-probe.js +106 -0
  13. package/dist/cli/quota-probe.js.map +1 -0
  14. package/dist/index.d.ts +13 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +90 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.test.d.ts +2 -0
  19. package/dist/index.test.d.ts.map +1 -0
  20. package/dist/index.test.js +35 -0
  21. package/dist/index.test.js.map +1 -0
  22. package/dist/server/acp.auth.test.d.ts +2 -0
  23. package/dist/server/acp.auth.test.d.ts.map +1 -0
  24. package/dist/server/acp.auth.test.js +581 -0
  25. package/dist/server/acp.auth.test.js.map +1 -0
  26. package/dist/server/acp.d.ts +68 -0
  27. package/dist/server/acp.d.ts.map +1 -0
  28. package/dist/server/acp.js +779 -0
  29. package/dist/server/acp.js.map +1 -0
  30. package/dist/server/acp.quota.test.d.ts +2 -0
  31. package/dist/server/acp.quota.test.d.ts.map +1 -0
  32. package/dist/server/acp.quota.test.js +114 -0
  33. package/dist/server/acp.quota.test.js.map +1 -0
  34. package/dist/server/acp.test.d.ts +2 -0
  35. package/dist/server/acp.test.d.ts.map +1 -0
  36. package/dist/server/acp.test.js +1153 -0
  37. package/dist/server/acp.test.js.map +1 -0
  38. package/dist/server/auth-check.d.ts +8 -0
  39. package/dist/server/auth-check.d.ts.map +1 -0
  40. package/dist/server/auth-check.js +8 -0
  41. package/dist/server/auth-check.js.map +1 -0
  42. package/dist/server/claude-config.d.ts +49 -0
  43. package/dist/server/claude-config.d.ts.map +1 -0
  44. package/dist/server/claude-config.js +279 -0
  45. package/dist/server/claude-config.js.map +1 -0
  46. package/dist/server/claude-config.test.d.ts +2 -0
  47. package/dist/server/claude-config.test.d.ts.map +1 -0
  48. package/dist/server/claude-config.test.js +197 -0
  49. package/dist/server/claude-config.test.js.map +1 -0
  50. package/dist/server/cli-capabilities.d.ts +26 -0
  51. package/dist/server/cli-capabilities.d.ts.map +1 -0
  52. package/dist/server/cli-capabilities.js +115 -0
  53. package/dist/server/cli-capabilities.js.map +1 -0
  54. package/dist/server/config-schema.d.ts +3 -0
  55. package/dist/server/config-schema.d.ts.map +1 -0
  56. package/dist/server/config-schema.js +67 -0
  57. package/dist/server/config-schema.js.map +1 -0
  58. package/dist/server/engine-availability.test.d.ts +2 -0
  59. package/dist/server/engine-availability.test.d.ts.map +1 -0
  60. package/dist/server/engine-availability.test.js +41 -0
  61. package/dist/server/engine-availability.test.js.map +1 -0
  62. package/dist/server/execute.acp-fallback.test.d.ts +2 -0
  63. package/dist/server/execute.acp-fallback.test.d.ts.map +1 -0
  64. package/dist/server/execute.acp-fallback.test.js +104 -0
  65. package/dist/server/execute.acp-fallback.test.js.map +1 -0
  66. package/dist/server/execute.d.ts +23 -0
  67. package/dist/server/execute.d.ts.map +1 -0
  68. package/dist/server/execute.js +1089 -0
  69. package/dist/server/execute.js.map +1 -0
  70. package/dist/server/execute.remote.test.d.ts +2 -0
  71. package/dist/server/execute.remote.test.d.ts.map +1 -0
  72. package/dist/server/execute.remote.test.js +484 -0
  73. package/dist/server/execute.remote.test.js.map +1 -0
  74. package/dist/server/index.d.ts +16 -0
  75. package/dist/server/index.d.ts.map +1 -0
  76. package/dist/server/index.js +84 -0
  77. package/dist/server/index.js.map +1 -0
  78. package/dist/server/models.d.ts +13 -0
  79. package/dist/server/models.d.ts.map +1 -0
  80. package/dist/server/models.js +153 -0
  81. package/dist/server/models.js.map +1 -0
  82. package/dist/server/parse.d.ts +58 -0
  83. package/dist/server/parse.d.ts.map +1 -0
  84. package/dist/server/parse.js +459 -0
  85. package/dist/server/parse.js.map +1 -0
  86. package/dist/server/parse.test.d.ts +2 -0
  87. package/dist/server/parse.test.d.ts.map +1 -0
  88. package/dist/server/parse.test.js +419 -0
  89. package/dist/server/parse.test.js.map +1 -0
  90. package/dist/server/permissions.d.ts +14 -0
  91. package/dist/server/permissions.d.ts.map +1 -0
  92. package/dist/server/permissions.js +13 -0
  93. package/dist/server/permissions.js.map +1 -0
  94. package/dist/server/permissions.test.d.ts +2 -0
  95. package/dist/server/permissions.test.d.ts.map +1 -0
  96. package/dist/server/permissions.test.js +22 -0
  97. package/dist/server/permissions.test.js.map +1 -0
  98. package/dist/server/probe-diagnostics.d.ts +102 -0
  99. package/dist/server/probe-diagnostics.d.ts.map +1 -0
  100. package/dist/server/probe-diagnostics.js +140 -0
  101. package/dist/server/probe-diagnostics.js.map +1 -0
  102. package/dist/server/probe-diagnostics.test.d.ts +2 -0
  103. package/dist/server/probe-diagnostics.test.d.ts.map +1 -0
  104. package/dist/server/probe-diagnostics.test.js +114 -0
  105. package/dist/server/probe-diagnostics.test.js.map +1 -0
  106. package/dist/server/probe-env.d.ts +42 -0
  107. package/dist/server/probe-env.d.ts.map +1 -0
  108. package/dist/server/probe-env.js +149 -0
  109. package/dist/server/probe-env.js.map +1 -0
  110. package/dist/server/probe-env.test.d.ts +2 -0
  111. package/dist/server/probe-env.test.d.ts.map +1 -0
  112. package/dist/server/probe-env.test.js +135 -0
  113. package/dist/server/probe-env.test.js.map +1 -0
  114. package/dist/server/probe-redaction.test.d.ts +2 -0
  115. package/dist/server/probe-redaction.test.d.ts.map +1 -0
  116. package/dist/server/probe-redaction.test.js +86 -0
  117. package/dist/server/probe-redaction.test.js.map +1 -0
  118. package/dist/server/prompt-cache.d.ts +17 -0
  119. package/dist/server/prompt-cache.d.ts.map +1 -0
  120. package/dist/server/prompt-cache.js +126 -0
  121. package/dist/server/prompt-cache.js.map +1 -0
  122. package/dist/server/quota-keychain.test.d.ts +2 -0
  123. package/dist/server/quota-keychain.test.d.ts.map +1 -0
  124. package/dist/server/quota-keychain.test.js +83 -0
  125. package/dist/server/quota-keychain.test.js.map +1 -0
  126. package/dist/server/quota.d.ts +30 -0
  127. package/dist/server/quota.d.ts.map +1 -0
  128. package/dist/server/quota.js +543 -0
  129. package/dist/server/quota.js.map +1 -0
  130. package/dist/server/setup-token-characterization.test.d.ts +2 -0
  131. package/dist/server/setup-token-characterization.test.d.ts.map +1 -0
  132. package/dist/server/setup-token-characterization.test.js +123 -0
  133. package/dist/server/setup-token-characterization.test.js.map +1 -0
  134. package/dist/server/setup-token-parse.d.ts +47 -0
  135. package/dist/server/setup-token-parse.d.ts.map +1 -0
  136. package/dist/server/setup-token-parse.js +506 -0
  137. package/dist/server/setup-token-parse.js.map +1 -0
  138. package/dist/server/setup-token-parse.test.d.ts +2 -0
  139. package/dist/server/setup-token-parse.test.d.ts.map +1 -0
  140. package/dist/server/setup-token-parse.test.js +587 -0
  141. package/dist/server/setup-token-parse.test.js.map +1 -0
  142. package/dist/server/setup-token-runner.d.ts +115 -0
  143. package/dist/server/setup-token-runner.d.ts.map +1 -0
  144. package/dist/server/setup-token-runner.js +317 -0
  145. package/dist/server/setup-token-runner.js.map +1 -0
  146. package/dist/server/setup-token-runner.test.d.ts +2 -0
  147. package/dist/server/setup-token-runner.test.d.ts.map +1 -0
  148. package/dist/server/setup-token-runner.test.js +336 -0
  149. package/dist/server/setup-token-runner.test.js.map +1 -0
  150. package/dist/server/skills.d.ts +8 -0
  151. package/dist/server/skills.d.ts.map +1 -0
  152. package/dist/server/skills.js +42 -0
  153. package/dist/server/skills.js.map +1 -0
  154. package/dist/server/test.d.ts +3 -0
  155. package/dist/server/test.d.ts.map +1 -0
  156. package/dist/server/test.js +434 -0
  157. package/dist/server/test.js.map +1 -0
  158. package/dist/server/test.probe.test.d.ts +2 -0
  159. package/dist/server/test.probe.test.d.ts.map +1 -0
  160. package/dist/server/test.probe.test.js +608 -0
  161. package/dist/server/test.probe.test.js.map +1 -0
  162. package/dist/server/test.remote.test.d.ts +2 -0
  163. package/dist/server/test.remote.test.d.ts.map +1 -0
  164. package/dist/server/test.remote.test.js +149 -0
  165. package/dist/server/test.remote.test.js.map +1 -0
  166. package/dist/ui/build-config.d.ts +3 -0
  167. package/dist/ui/build-config.d.ts.map +1 -0
  168. package/dist/ui/build-config.js +76 -0
  169. package/dist/ui/build-config.js.map +1 -0
  170. package/dist/ui/build-config.test.d.ts +2 -0
  171. package/dist/ui/build-config.test.d.ts.map +1 -0
  172. package/dist/ui/build-config.test.js +68 -0
  173. package/dist/ui/build-config.test.js.map +1 -0
  174. package/dist/ui/index.d.ts +3 -0
  175. package/dist/ui/index.d.ts.map +1 -0
  176. package/dist/ui/index.js +3 -0
  177. package/dist/ui/index.js.map +1 -0
  178. package/dist/ui/parse-stdout.d.ts +3 -0
  179. package/dist/ui/parse-stdout.d.ts.map +1 -0
  180. package/dist/ui/parse-stdout.js +153 -0
  181. package/dist/ui/parse-stdout.js.map +1 -0
  182. package/package.json +63 -0
  183. package/skills/agentmail/SKILL.md +70 -0
  184. package/skills/paperclip/SKILL.md +712 -0
  185. package/skills/paperclip/references/api-reference.md +1675 -0
  186. package/skills/paperclip/references/artifacts.md +158 -0
  187. package/skills/paperclip/references/cases.md +295 -0
  188. package/skills/paperclip/references/company-skills.md +266 -0
  189. package/skills/paperclip/references/issue-workspaces.md +80 -0
  190. package/skills/paperclip/references/routines.md +231 -0
  191. package/skills/paperclip/references/workflows.md +141 -0
  192. package/skills/paperclip/scripts/paperclip-upload-artifact.sh +592 -0
  193. package/skills/paperclip-board/SKILL.md +619 -0
  194. package/skills/paperclip-converting-plans-to-tasks/SKILL.md +60 -0
  195. package/skills/paperclip-create-agent/SKILL.md +179 -0
  196. package/skills/paperclip-create-agent/references/agent-instruction-templates.md +123 -0
  197. package/skills/paperclip-create-agent/references/agents/coder.md +64 -0
  198. package/skills/paperclip-create-agent/references/agents/qa.md +88 -0
  199. package/skills/paperclip-create-agent/references/agents/securityengineer.md +135 -0
  200. package/skills/paperclip-create-agent/references/agents/uxdesigner.md +115 -0
  201. package/skills/paperclip-create-agent/references/api-reference.md +110 -0
  202. package/skills/paperclip-create-agent/references/baseline-role-guide.md +168 -0
  203. package/skills/paperclip-create-agent/references/draft-review-checklist.md +95 -0
  204. package/skills/para-memory-files/SKILL.md +100 -0
  205. package/skills/para-memory-files/references/schemas.md +35 -0
  206. package/skills/slack/SKILL.md +65 -0
@@ -0,0 +1,779 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parseLocalProcessFilesystemScope, parseLocalProcessNetworkScope, } from "@tickernelz/paperclip-pro-adapter-utils/local-process-sandbox";
5
+ import { ensureAdapterExecutionTargetCommandResolvable, ensureAdapterExecutionTargetDirectory, readAdapterExecutionTarget, resolveAdapterExecutionTargetCwd, runAdapterExecutionTargetProcess, } from "@tickernelz/paperclip-pro-adapter-utils/execution-target";
6
+ import { DEFAULT_ACP_ENGINE_MODE, DEFAULT_ACP_ENGINE_NON_INTERACTIVE_PERMISSIONS, DEFAULT_ACP_ENGINE_PERMISSION_MODE, DEFAULT_ACP_ENGINE_WARM_HANDLE_IDLE_MS, } from "@tickernelz/paperclip-pro-adapter-utils/acpx-engine/constants";
7
+ import { asBoolean, asNumber, asString, parseObject, } from "@tickernelz/paperclip-pro-adapter-utils/server-utils";
8
+ import { materializeRemoteClaudeConfig, prepareClaudeConfigSeed, prepareSandboxClaudeProbeRuntime, } from "./claude-config.js";
9
+ import { buildAdapterTestTargetCheck, buildClaudeLoginRequiredHint, classifyThrownErrorClass, logSandboxProbeDiagnostic, } from "./probe-diagnostics.js";
10
+ import { createWorkspaceRestoreTeardown } from "@tickernelz/paperclip-pro-adapter-utils/workspace-restore-teardown";
11
+ import { buildLocalAdapterTestProbeEnv } from "./probe-env.js";
12
+ import { detectClaudeLoginRequired, extractClaudeRetryNotBefore, isClaudeProviderQuotaError, parseClaudeStreamJson } from "./parse.js";
13
+ import { buildClaudeProbePermissionArgs, claudeSandboxPermissionEnv } from "./permissions.js";
14
+ import { ADAPTER_AUTH_MISSING_CHECK_CODE } from "./auth-check.js";
15
+ import { resolveClaudeModel, SANDBOX_INSTALL_COMMAND } from "../index.js";
16
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
17
+ const packageRootDir = path.resolve(moduleDir, "../..");
18
+ const MIN_ACP_NODE_VERSION = "24.11.0";
19
+ function normalizeEngine(value) {
20
+ const raw = typeof value === "string" ? value.trim().toLowerCase() : "";
21
+ if (raw === "acp")
22
+ return { engine: "acp", explicit: true };
23
+ if (raw === "cli")
24
+ return { engine: "cli", explicit: true };
25
+ return { engine: "acp", explicit: false };
26
+ }
27
+ export function resolveClaudeExecutionEngine(config) {
28
+ return normalizeEngine(config.engine);
29
+ }
30
+ export async function resolveClaudeExecutionEngineForRun(input) {
31
+ const selection = normalizeEngine(input.config.engine);
32
+ // Engine availability must never change the agent's execution or permission contract.
33
+ if (selection.engine === "cli")
34
+ return selection;
35
+ const unavailable = (reason) => ({
36
+ ...selection,
37
+ unavailableReason: `${reason} Repair the ACP setup, or explicitly set engine=cli to use the CLI engine.`,
38
+ });
39
+ const filesystemScope = parseLocalProcessFilesystemScope(input.config.filesystemScope);
40
+ const networkScope = parseLocalProcessNetworkScope(input.config.networkScope);
41
+ if (filesystemScope || networkScope) {
42
+ return unavailable("Local filesystem/network confinement requires the Claude CLI engine; ACP confinement is not supported.");
43
+ }
44
+ const reason = await claudeAcpUnavailableReason(input);
45
+ return reason ? unavailable(reason) : selection;
46
+ }
47
+ function firstNonEmptyString(...values) {
48
+ for (const value of values) {
49
+ if (typeof value !== "string")
50
+ continue;
51
+ const trimmed = value.trim();
52
+ if (trimmed.length > 0)
53
+ return trimmed;
54
+ }
55
+ return undefined;
56
+ }
57
+ export function buildClaudeAcpConfig(config, inheritedEnv = {}) {
58
+ const env = parseObject(config.env);
59
+ const model = resolveClaudeModel(config.model, { ...inheritedEnv, ...env });
60
+ const agentCommand = firstNonEmptyString(config.agentCommand, config.acpAgentCommand);
61
+ const stateDir = firstNonEmptyString(config.stateDir, config.acpStateDir);
62
+ const mode = firstNonEmptyString(config.mode, config.acpMode) ?? DEFAULT_ACP_ENGINE_MODE;
63
+ const permissionMode = firstNonEmptyString(config.permissionMode, config.acpPermissionMode) ??
64
+ DEFAULT_ACP_ENGINE_PERMISSION_MODE;
65
+ const nonInteractivePermissions = firstNonEmptyString(config.nonInteractivePermissions, config.acpNonInteractivePermissions) ??
66
+ DEFAULT_ACP_ENGINE_NON_INTERACTIVE_PERMISSIONS;
67
+ const warmHandleIdleMs = config.warmHandleIdleMs ??
68
+ config.acpWarmHandleIdleMs ??
69
+ DEFAULT_ACP_ENGINE_WARM_HANDLE_IDLE_MS;
70
+ return {
71
+ ...config,
72
+ model,
73
+ // ACP reads ANTHROPIC_MODEL at startup; keep it aligned with CLI precedence.
74
+ ...(model ? { env: { ...env, ANTHROPIC_MODEL: model } } : {}),
75
+ agent: "claude",
76
+ mode,
77
+ permissionMode,
78
+ nonInteractivePermissions,
79
+ warmHandleIdleMs,
80
+ ...(agentCommand ? { agentCommand } : {}),
81
+ ...(stateDir ? { stateDir } : {}),
82
+ };
83
+ }
84
+ /**
85
+ * Classify billing the same way the Claude CLI lane does so ACP runs land in
86
+ * the cost ledger with a real provider/billingType instead of acpx/unknown.
87
+ * Host env only counts for local execution targets; remote targets see just
88
+ * the adapter-config env.
89
+ */
90
+ export function resolveClaudeAcpBillingIdentity(ctx) {
91
+ const envConfig = parseObject(parseObject(ctx.config).env);
92
+ const target = readAdapterExecutionTarget({
93
+ executionTarget: ctx.executionTarget,
94
+ legacyRemoteExecution: ctx.executionTransport?.remoteExecution,
95
+ });
96
+ const considerHostEnv = target?.kind !== "remote";
97
+ const readEnvValue = (key) => {
98
+ const fromConfig = envConfig[key];
99
+ if (typeof fromConfig === "string" && fromConfig.trim())
100
+ return fromConfig.trim();
101
+ const fromHost = considerHostEnv ? process.env[key] : undefined;
102
+ return typeof fromHost === "string" ? fromHost.trim() : "";
103
+ };
104
+ const bedrockFlag = readEnvValue("CLAUDE_CODE_USE_BEDROCK");
105
+ const bedrock = bedrockFlag === "1" || bedrockFlag === "true" || Boolean(readEnvValue("ANTHROPIC_BEDROCK_BASE_URL"));
106
+ const billingType = bedrock
107
+ ? "metered_api"
108
+ : readEnvValue("ANTHROPIC_API_KEY")
109
+ ? "api"
110
+ : "subscription";
111
+ return {
112
+ provider: "anthropic",
113
+ biller: bedrock ? "aws_bedrock" : "anthropic",
114
+ billingType,
115
+ };
116
+ }
117
+ /**
118
+ * Claude remote managed-home seed for the runner-backed remote sandbox ACP lane.
119
+ * Mirrors the Claude CLI lane (`claude-local/execute.ts`): ship a sanitized
120
+ * config seed (settings.json + CLAUDE.md, no credentials) as the `config-seed`
121
+ * asset, materialize it into an in-sandbox config dir (copying the sandbox's own
122
+ * `$HOME/.claude` credentials in), then repoint `CLAUDE_CONFIG_DIR` onto that
123
+ * in-sandbox config dir. Claude has no credential copy-back (its CLI lane has
124
+ * none — mirroring the CLI is the contract). The teardown hook therefore only
125
+ * syncs the sandbox workspace back to the host; it does not touch credentials.
126
+ *
127
+ * An explicit `CLAUDE_CONFIG_DIR` (user-managed) is honored only if it can reach
128
+ * the remote sandbox; a host-only path cannot, so we do NOT forward it verbatim
129
+ * (that would start remote Claude with no config/credentials). See the branch
130
+ * below for the two portable dispositions. The engine's `useRemoteProcessSession`
131
+ * gate already guarantees the remote sandbox (managed-home) target.
132
+ */
133
+ async function prepareClaudeRemoteManagedHome(input) {
134
+ const { env, runId, onLog, executionTarget } = input;
135
+ // Fail-open workspace sync-back for every exit path (mirrors the Claude CLI
136
+ // lane's restore-hook finally and the Codex ACP seam's teardown). Claude has no
137
+ // credential copy-back, so the teardown only syncs the sandbox workspace back to
138
+ // the host. A restore miss is logged and never fails the run.
139
+ const registerWorkspaceSyncBack = (stagedRuntime) => createWorkspaceRestoreTeardown({
140
+ stagedRuntime,
141
+ onLog,
142
+ startMessage: "[paperclip] Restoring workspace changes from the sandbox.\n",
143
+ failurePrefix: "[paperclip] Claude ACP teardown workspace restore failed",
144
+ });
145
+ const envConfig = parseObject(input.config.env);
146
+ const explicitClaudeConfigDir = typeof envConfig.CLAUDE_CONFIG_DIR === "string" && envConfig.CLAUDE_CONFIG_DIR.trim().length > 0
147
+ ? envConfig.CLAUDE_CONFIG_DIR.trim()
148
+ : "";
149
+ if (explicitClaudeConfigDir && !input.config.managedAiConnection) {
150
+ // User-managed escape hatch. Unlike the Claude CLI lane
151
+ // (`claude-local/execute.ts`), which runs the process on the same host and can
152
+ // forward the operator's path verbatim, the remote ACP lane spawns Claude
153
+ // inside a sandbox that CANNOT see host paths. Forwarding an absolute host
154
+ // path unchanged would leave remote Claude without the requested config or
155
+ // credentials, so we choose one of two portable dispositions:
156
+ // 1. The path lives INSIDE the staged workspace → remap its prefix onto the
157
+ // in-sandbox workspace dir so it resolves against the copied files.
158
+ // 2. The path is host-only (outside the workspace) → it cannot cross into
159
+ // the sandbox, so ignore the un-portable override and seed the managed
160
+ // config instead (falling through below), which guarantees working
161
+ // config/credentials. Logged loudly so the substitution is diagnosable.
162
+ const relativeToWorkspace = path.relative(input.workspaceLocalDir, explicitClaudeConfigDir);
163
+ const isUnderWorkspace = relativeToWorkspace.length > 0 &&
164
+ !relativeToWorkspace.startsWith("..") &&
165
+ !path.isAbsolute(relativeToWorkspace);
166
+ if (isUnderWorkspace) {
167
+ const stagedRuntime = await input.stage([]);
168
+ const remoteWorkspaceDir = stagedRuntime.workspaceRemoteDir ?? input.workspaceLocalDir;
169
+ const remappedConfigDir = path.posix.join(remoteWorkspaceDir, relativeToWorkspace.split(path.sep).join(path.posix.sep));
170
+ env.CLAUDE_CONFIG_DIR = remappedConfigDir;
171
+ await onLog("stdout", `[paperclip] Remapped operator CLAUDE_CONFIG_DIR from host path ${explicitClaudeConfigDir} onto the in-sandbox workspace path ${remappedConfigDir} for the remote ACP run.\n`);
172
+ return { stagedRuntime, teardown: registerWorkspaceSyncBack(stagedRuntime) };
173
+ }
174
+ await onLog("stderr", `[paperclip] operator-provided CLAUDE_CONFIG_DIR=${explicitClaudeConfigDir} is outside the staged workspace and cannot reach the remote sandbox; ignoring the host-only path and seeding the managed Claude config instead.\n`);
175
+ }
176
+ // Content-addressed sanitized seed (managed cache under the instance root, not
177
+ // a temp dir — reused across runs, so no teardown cleanup).
178
+ const claudeConfigSeedDir = input.config.managedAiConnection
179
+ ? explicitClaudeConfigDir
180
+ : await prepareClaudeConfigSeed(process.env, onLog, input.companyId);
181
+ // Ship the per-run skill bundle, staged only when the run selected at
182
+ // least one skill. The bundle directory holds a plain copy of each
183
+ // selected skill's files (`materializePaperclipSkillCopy` never copies a
184
+ // symbolic link, at the root or at any depth). So the bundle asset stages
185
+ // with `followSymlinks: false`: staging never needs to carry a symbolic
186
+ // link's target content, and refusing to follow one stops a link planted
187
+ // in the bundle directory after materialization (for example by a
188
+ // concurrent writer) from pulling an arbitrary host file into the sandbox.
189
+ // The engine rewrites the prompt onto the in-sandbox copy once this asset
190
+ // is staged.
191
+ const stagedRuntime = await input.stage([
192
+ { key: "config-seed", localDir: claudeConfigSeedDir, followSymlinks: true },
193
+ ...(input.skillsBundleDir
194
+ ? [{ key: "skills", localDir: input.skillsBundleDir, followSymlinks: false }]
195
+ : []),
196
+ ]);
197
+ const remoteClaudeRuntimeRoot = stagedRuntime.runtimeRootDir ??
198
+ path.posix.join(stagedRuntime.workspaceRemoteDir ?? input.workspaceLocalDir, ".paperclip-runtime", "claude");
199
+ const remoteClaudeConfigSeedDir = stagedRuntime.assetDirs["config-seed"] ?? path.posix.join(remoteClaudeRuntimeRoot, "config-seed");
200
+ const remoteClaudeConfigDir = path.posix.join(remoteClaudeRuntimeRoot, "config");
201
+ await onLog("stdout", `[paperclip] Materializing Claude auth/config into ${remoteClaudeConfigDir}.\n`);
202
+ await materializeRemoteClaudeConfig({
203
+ runId,
204
+ target: executionTarget,
205
+ remoteClaudeConfigDir,
206
+ remoteClaudeConfigSeedDir,
207
+ options: {
208
+ cwd: stagedRuntime.workspaceRemoteDir ?? input.workspaceLocalDir,
209
+ env,
210
+ timeoutSec: Math.max(input.timeoutSec, 15),
211
+ graceSec: 20,
212
+ onLog,
213
+ },
214
+ });
215
+ // Repoint CLAUDE_CONFIG_DIR onto the in-sandbox config dir.
216
+ env.CLAUDE_CONFIG_DIR = remoteClaudeConfigDir;
217
+ return { stagedRuntime, teardown: registerWorkspaceSyncBack(stagedRuntime) };
218
+ }
219
+ export function classifyClaudeTerminalSessionFailure(failure, now) {
220
+ // `limit` also includes context, turn, rate and configured budget limits.
221
+ // Only the provider's quota wording qualifies for a quota wait.
222
+ if (failure.category !== "limit")
223
+ return null;
224
+ const surface = { errorMessage: [failure.title, failure.details].filter(Boolean).join("\n") };
225
+ // claude-agent-acp uses this exact quota_exhausted fallback when no provider
226
+ // title is available. It does not match the CLI's usage-limit wording.
227
+ const isQuotaFallback = failure.title === "The Claude account has no available quota.";
228
+ if (!isQuotaFallback && !isClaudeProviderQuotaError(surface))
229
+ return null;
230
+ const retryNotBefore = extractClaudeRetryNotBefore(surface, now)?.toISOString();
231
+ return {
232
+ errorCode: "provider_quota",
233
+ errorFamily: "provider_quota",
234
+ ...(retryNotBefore ? { retryNotBefore } : {}),
235
+ };
236
+ }
237
+ function withClaudeAcpDefaults(options) {
238
+ return {
239
+ resolveBillingIdentity: resolveClaudeAcpBillingIdentity,
240
+ prepareRemoteManagedHome: prepareClaudeRemoteManagedHome,
241
+ classifyTerminalSessionFailure: classifyClaudeTerminalSessionFailure,
242
+ ...options,
243
+ adapterType: "claude_local",
244
+ moduleDir,
245
+ packageRootDir,
246
+ };
247
+ }
248
+ /**
249
+ * The generic error code the shared acpx engine emits when a run fails because
250
+ * the agent has no ready authentication. The shared engine stays vendor-neutral,
251
+ * so it keeps this generic code. See `adapter-utils/acpx-engine/execute.ts`.
252
+ */
253
+ const ACPX_AUTH_REQUIRED_ERROR_CODE = "acpx_auth_required";
254
+ /**
255
+ * The Claude-specific error code the user interface reads to show the Claude
256
+ * login affordance on a run. The Claude CLI lane already emits this code. See
257
+ * `execute.ts` and the user interface gate in `ui/src/pages/AgentDetail.tsx`.
258
+ */
259
+ const CLAUDE_AUTH_REQUIRED_ERROR_CODE = "claude_auth_required";
260
+ /**
261
+ * Translate the generic acpx auth-required code into the Claude-specific code at
262
+ * the claude-local boundary. The shared acpx engine reports the generic
263
+ * `acpx_auth_required` code for every adapter. The user interface run gate reads
264
+ * the Claude-specific `claude_auth_required` code, the same code the Claude CLI
265
+ * lane emits. Without this translation the default ACP run never shows the login
266
+ * prompt. The function changes only the error code and keeps every other field,
267
+ * so the error message and the error metadata stay intact.
268
+ */
269
+ export function mapClaudeAcpAuthErrorCode(result) {
270
+ if (result.errorCode !== ACPX_AUTH_REQUIRED_ERROR_CODE)
271
+ return result;
272
+ return { ...result, errorCode: CLAUDE_AUTH_REQUIRED_ERROR_CODE };
273
+ }
274
+ export function createClaudeAcpExecutor(options = {}) {
275
+ let executor = null;
276
+ return async (ctx) => {
277
+ let currentExecutor = executor;
278
+ if (!currentExecutor) {
279
+ const { createAcpxEngineExecutor } = await import("@tickernelz/paperclip-pro-adapter-utils/acpx-engine/execute");
280
+ currentExecutor = createAcpxEngineExecutor(withClaudeAcpDefaults(options));
281
+ executor = currentExecutor;
282
+ }
283
+ const target = readAdapterExecutionTarget({
284
+ executionTarget: ctx.executionTarget,
285
+ legacyRemoteExecution: ctx.executionTransport?.remoteExecution,
286
+ });
287
+ const result = await currentExecutor({
288
+ ...ctx,
289
+ config: buildClaudeAcpConfig(ctx.config, target?.kind === "remote" ? {} : process.env),
290
+ });
291
+ return mapClaudeAcpAuthErrorCode(result);
292
+ };
293
+ }
294
+ function parseVersion(version) {
295
+ const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)/);
296
+ if (!match)
297
+ return [0, 0, 0];
298
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
299
+ }
300
+ export function nodeVersionMeetsClaudeAcpMinimum(version = process.version) {
301
+ const [major, minor, patch] = parseVersion(version);
302
+ const [minMajor, minMinor, minPatch] = parseVersion(MIN_ACP_NODE_VERSION);
303
+ if (major !== minMajor)
304
+ return major > minMajor;
305
+ if (minor !== minMinor)
306
+ return minor > minMinor;
307
+ return patch >= minPatch;
308
+ }
309
+ async function pathExists(candidate) {
310
+ return fs.access(candidate).then(() => true).catch(() => false);
311
+ }
312
+ function hasPathSeparator(command) {
313
+ return command.includes("/") || command.includes("\\");
314
+ }
315
+ function looksLikeShellCommand(command) {
316
+ return /\s/.test(command.trim());
317
+ }
318
+ async function findCommandOnPath(binName) {
319
+ const pathValue = process.env.PATH ?? "";
320
+ for (const segment of pathValue.split(path.delimiter)) {
321
+ if (!segment)
322
+ continue;
323
+ const candidate = path.join(segment, binName);
324
+ if (await pathExists(candidate))
325
+ return candidate;
326
+ }
327
+ return null;
328
+ }
329
+ async function findAncestorBin(startDir, binName) {
330
+ let current = path.resolve(startDir);
331
+ while (true) {
332
+ const candidate = path.join(current, "node_modules", ".bin", binName);
333
+ if (await pathExists(candidate))
334
+ return candidate;
335
+ const parent = path.dirname(current);
336
+ if (parent === current)
337
+ return null;
338
+ current = parent;
339
+ }
340
+ }
341
+ async function commandIsResolvable(command, input) {
342
+ const trimmed = command.trim();
343
+ if (!trimmed)
344
+ return false;
345
+ if (looksLikeShellCommand(trimmed))
346
+ return true;
347
+ const target = readAdapterExecutionTarget({
348
+ executionTarget: input?.executionTarget,
349
+ legacyRemoteExecution: input?.executionTransport?.remoteExecution,
350
+ });
351
+ if (target?.kind === "remote") {
352
+ try {
353
+ await ensureAdapterExecutionTargetCommandResolvable(trimmed, target, resolveAdapterExecutionTargetCwd(target, asString(input?.config.cwd, ""), process.cwd()), process.env);
354
+ return true;
355
+ }
356
+ catch {
357
+ return false;
358
+ }
359
+ }
360
+ if (path.isAbsolute(trimmed) || hasPathSeparator(trimmed))
361
+ return pathExists(trimmed);
362
+ return (await findCommandOnPath(trimmed)) !== null;
363
+ }
364
+ async function resolveClaudeAcpCommand(config) {
365
+ const configured = firstNonEmptyString(config.agentCommand, config.acpAgentCommand);
366
+ if (configured)
367
+ return configured;
368
+ return ((await findAncestorBin(packageRootDir, "claude-agent-acp")) ??
369
+ (await findCommandOnPath("claude-agent-acp")) ??
370
+ path.join(packageRootDir, "node_modules", ".bin", "claude-agent-acp"));
371
+ }
372
+ function sandboxTargetHasProcessSessionBridge(target) {
373
+ return target?.kind === "remote" && target.transport === "sandbox" && Boolean(target.runner);
374
+ }
375
+ async function resolveClaudeAcpCommandForTarget(config, target) {
376
+ const configured = firstNonEmptyString(config.agentCommand, config.acpAgentCommand);
377
+ if (configured)
378
+ return configured;
379
+ if (target?.kind === "remote")
380
+ return "claude-agent-acp";
381
+ return resolveClaudeAcpCommand(config);
382
+ }
383
+ async function claudeAcpUnavailableReason(input) {
384
+ const target = readAdapterExecutionTarget({
385
+ executionTarget: input.executionTarget,
386
+ legacyRemoteExecution: input.executionTransport?.remoteExecution,
387
+ });
388
+ if (target?.kind === "remote" && !sandboxTargetHasProcessSessionBridge(target)) {
389
+ if (target.transport === "sandbox") {
390
+ return "Claude ACP requires a bidirectional remote process target; this sandbox exposes only one-shot command execution.";
391
+ }
392
+ return "Claude ACP supports sandbox remote targets only; this run targets a non-sandbox remote environment.";
393
+ }
394
+ if (!nodeVersionMeetsClaudeAcpMinimum()) {
395
+ return `Node ${process.version} (${process.execPath}) does not satisfy Claude ACP's Node >=${MIN_ACP_NODE_VERSION} prerequisite.`;
396
+ }
397
+ const command = await resolveClaudeAcpCommandForTarget(input.config, target);
398
+ if (!(await commandIsResolvable(command, input))) {
399
+ return `Claude ACP server command is not available: ${command}.`;
400
+ }
401
+ return null;
402
+ }
403
+ function summarizeStatus(checks) {
404
+ if (checks.some((check) => check.level === "error"))
405
+ return "fail";
406
+ if (checks.some((check) => check.level === "warn"))
407
+ return "warn";
408
+ return "pass";
409
+ }
410
+ function isNonEmpty(value) {
411
+ return typeof value === "string" && value.trim().length > 0;
412
+ }
413
+ /**
414
+ * Build the checks that tell the user the probed target has no ready Claude
415
+ * authentication. Every target gets the descriptive warn check, so
416
+ * `summarizeStatus` never reports a pass without auth. Only a sandbox target
417
+ * gets the neutral canonical `adapter_auth_missing` code, because only a
418
+ * sandbox target can start an in-place login. The user interface reads the
419
+ * canonical code to offer login and gates that affordance to sandbox targets.
420
+ */
421
+ function buildAcpAuthMissingChecks(input) {
422
+ const checks = [
423
+ {
424
+ code: "claude_hello_probe_auth_required",
425
+ level: "warn",
426
+ message: "Claude ACP is available, but login is required.",
427
+ hint: buildClaudeLoginRequiredHint(input.loginUrl),
428
+ },
429
+ ];
430
+ if (input.targetIsSandbox) {
431
+ checks.push({
432
+ code: ADAPTER_AUTH_MISSING_CHECK_CODE,
433
+ level: "warn",
434
+ message: "This environment has no ready authentication for this adapter.",
435
+ hint: "Provide credentials for this adapter, or start login in the environment.",
436
+ });
437
+ }
438
+ return checks;
439
+ }
440
+ /**
441
+ * Build the check that tells the user a Claude login probe could not run on the
442
+ * ACP path. The check is a warn, not an info, so `summarizeStatus` never
443
+ * reports a pass. The check code is distinct from `adapter_auth_missing`, so the
444
+ * user interface never shows the login affordance for a probe that could not
445
+ * confirm the login state. A Test without available auth must not report a
446
+ * success.
447
+ */
448
+ function buildAcpLoginProbeUnavailableCheck(message, targetIsSandbox = false) {
449
+ return {
450
+ code: "claude_acp_login_probe_unavailable",
451
+ level: "warn",
452
+ message,
453
+ hint: targetIsSandbox
454
+ ? "Verify that the environment can run `claude` and retry the Test. Set engine=cli to use the Claude CLI lane."
455
+ : "Verify that `claude` can run in this environment and retry the Test. Set engine=cli to use the Claude CLI lane.",
456
+ };
457
+ }
458
+ /**
459
+ * Probe the stored Claude login for the probed target on the ACP path. The ACP
460
+ * engine and the Claude CLI share the same stored Claude login, so the probe
461
+ * runs the `claude` command with a short hello turn. The probe runs against any
462
+ * target: a local host, an SSH remote, or a sandbox. On a local target the
463
+ * probe builds the child env and the executable from the shared
464
+ * deny-by-default builder, so a hostile caller value can neither select the
465
+ * executable nor reach the child. On a remote target the caller passes the
466
+ * prepared `env`, so the probe reads the managed `CLAUDE_CONFIG_DIR` the same
467
+ * way the CLI lane does.
468
+ *
469
+ * When the probe reports that login is required, the function returns the
470
+ * auth-required checks. Only a sandbox target also gets the canonical
471
+ * `adapter_auth_missing` code, so the user interface offers login for sandbox
472
+ * targets only.
473
+ *
474
+ * The function keeps two signals distinct. It returns the auth-required check
475
+ * only when the probe ran and login is required. It returns a separate warn
476
+ * check when the probe could not run, timed out, or did not complete. It never
477
+ * maps "probe could not run" to a silent pass.
478
+ */
479
+ export async function probeClaudeAcpSandboxLogin(input) {
480
+ const { config, target } = input;
481
+ const targetIsRemote = target?.kind === "remote";
482
+ const targetIsSandbox = target?.kind === "remote" && target.transport === "sandbox";
483
+ // The caller-derived env. On a local target the shared builder filters it to
484
+ // a deny-by-default allowlist. On a remote target the prepared env is used
485
+ // directly, because the remote transport owns its own env sanitization.
486
+ let callerEnv;
487
+ if (input.env) {
488
+ callerEnv = input.env;
489
+ }
490
+ else {
491
+ const envConfig = parseObject(config.env);
492
+ callerEnv = {};
493
+ for (const [key, value] of Object.entries(envConfig)) {
494
+ if (typeof value === "string")
495
+ callerEnv[key] = value;
496
+ }
497
+ }
498
+ let command;
499
+ let env;
500
+ let cwd;
501
+ if (targetIsRemote && target) {
502
+ command = "claude";
503
+ env = callerEnv;
504
+ cwd = target.kind === "remote" ? target.remoteCwd : process.cwd();
505
+ }
506
+ else {
507
+ const built = await buildLocalAdapterTestProbeEnv({
508
+ callerEnv,
509
+ trustedEnv: process.env,
510
+ });
511
+ if (!built.command) {
512
+ return [buildAcpLoginProbeUnavailableCheck("Claude is not installed on the Paperclip host.")];
513
+ }
514
+ command = built.command;
515
+ env = built.env;
516
+ cwd = asString(config.cwd, process.cwd());
517
+ }
518
+ Object.assign(env, claudeSandboxPermissionEnv({
519
+ dangerouslySkipPermissions: asBoolean(config.dangerouslySkipPermissions, true), targetIsSandbox,
520
+ }));
521
+ const args = ["--print", "-", "--output-format", "stream-json", "--verbose"];
522
+ if (config.managedAiConnection)
523
+ args.push("--setting-sources", "user");
524
+ args.push(...buildClaudeProbePermissionArgs({
525
+ dangerouslySkipPermissions: asBoolean(config.dangerouslySkipPermissions, true),
526
+ targetIsRemote,
527
+ localProcessUid: process.getuid?.() ?? null,
528
+ }));
529
+ const timeoutSec = Math.max(1, asNumber(config.helloProbeTimeoutSec, targetIsSandbox ? 90 : 45));
530
+ const runId = `claude-acp-authprobe-${Date.now()}-${Math.random().toString(16).slice(2)}`;
531
+ let probe;
532
+ try {
533
+ probe = await runAdapterExecutionTargetProcess(runId, target, command, args, {
534
+ cwd,
535
+ env,
536
+ timeoutSec,
537
+ graceSec: 5,
538
+ stdin: "Respond with hello.",
539
+ onLog: async () => { },
540
+ });
541
+ }
542
+ catch (err) {
543
+ // Keep the raw error out of the Test-result check and the server log. Log
544
+ // only the fixed context, the allowlisted classification, and a safe error
545
+ // class name.
546
+ logSandboxProbeDiagnostic("Claude ACP login probe could not run", "spawn_error", {
547
+ errorClass: classifyThrownErrorClass(err),
548
+ });
549
+ return [
550
+ buildAcpLoginProbeUnavailableCheck(targetIsSandbox
551
+ ? "The Claude login probe could not run in the sandbox."
552
+ : "The Claude login probe could not run.", targetIsSandbox),
553
+ ];
554
+ }
555
+ if (probe.timedOut) {
556
+ return [buildAcpLoginProbeUnavailableCheck("The Claude login probe timed out.", targetIsSandbox)];
557
+ }
558
+ const parsedStream = parseClaudeStreamJson(probe.stdout);
559
+ const loginMeta = detectClaudeLoginRequired({
560
+ parsed: parsedStream.resultJson,
561
+ stdout: probe.stdout,
562
+ stderr: probe.stderr,
563
+ });
564
+ if (loginMeta.requiresLogin) {
565
+ return buildAcpAuthMissingChecks({ targetIsSandbox, loginUrl: loginMeta.loginUrl });
566
+ }
567
+ if ((probe.exitCode ?? 1) !== 0) {
568
+ // Keep the raw stderr and stdout out of the Test-result check and the
569
+ // server log. Log only the fixed context, the allowlisted classification,
570
+ // and the safe exit code.
571
+ logSandboxProbeDiagnostic("Claude ACP login probe did not complete", "nonzero_exit", {
572
+ exitCode: probe.exitCode ?? null,
573
+ });
574
+ return [buildAcpLoginProbeUnavailableCheck("The Claude login probe did not complete.", targetIsSandbox)];
575
+ }
576
+ return [];
577
+ }
578
+ export async function testClaudeAcpEnvironment(ctx) {
579
+ const checks = [];
580
+ const config = parseObject(ctx.config);
581
+ const target = ctx.executionTarget ?? null;
582
+ const targetIsRemote = target?.kind === "remote";
583
+ const targetIsSandbox = target?.kind === "remote" && target.transport === "sandbox";
584
+ checks.push({
585
+ code: "claude_engine_selected",
586
+ level: "info",
587
+ message: "Execution engine selected: ACP.",
588
+ hint: "Set engine=cli to use the existing Claude Code CLI lane.",
589
+ });
590
+ // Always name the target the Test probed, so a pass result never hides which
591
+ // target it checked. A local probe reports the fixed host label.
592
+ checks.push(buildAdapterTestTargetCheck({ targetIsRemote, environmentName: ctx.environmentName }));
593
+ if (targetIsRemote) {
594
+ checks.push({
595
+ code: "claude_acp_remote_target",
596
+ level: "info",
597
+ message: "Claude ACP will run against the remote execution environment.",
598
+ hint: "Remote ACP requires a bidirectional process target such as SSH or Paperclip's sandbox process-session bridge.",
599
+ });
600
+ }
601
+ const cwd = resolveAdapterExecutionTargetCwd(target, asString(config.cwd, ""), process.cwd());
602
+ try {
603
+ await ensureAdapterExecutionTargetDirectory(`claude-acp-envtest-${Date.now()}`, target, cwd, {
604
+ cwd,
605
+ env: {},
606
+ createIfMissing: true,
607
+ });
608
+ checks.push({
609
+ code: "claude_acp_cwd_valid",
610
+ level: "info",
611
+ message: `Working directory is valid: ${cwd}`,
612
+ });
613
+ }
614
+ catch (err) {
615
+ checks.push({
616
+ code: "claude_acp_cwd_invalid",
617
+ level: "error",
618
+ message: err instanceof Error ? err.message : "Invalid working directory",
619
+ detail: cwd,
620
+ });
621
+ }
622
+ checks.push({
623
+ code: nodeVersionMeetsClaudeAcpMinimum() ? "claude_acp_node_supported" : "claude_acp_node_unsupported",
624
+ level: nodeVersionMeetsClaudeAcpMinimum() ? "info" : "error",
625
+ message: nodeVersionMeetsClaudeAcpMinimum()
626
+ ? `Node ${process.version} satisfies Claude ACP runtime requirements.`
627
+ : `Node ${process.version} (${process.execPath}) does not satisfy Claude ACP runtime requirements.`,
628
+ hint: nodeVersionMeetsClaudeAcpMinimum()
629
+ ? undefined
630
+ : `Run Claude ACP with Node >=${MIN_ACP_NODE_VERSION} or switch engine=cli.`,
631
+ });
632
+ const command = await resolveClaudeAcpCommandForTarget(config, target);
633
+ const commandResolvable = await commandIsResolvable(command, {
634
+ config,
635
+ executionTarget: ctx.executionTarget,
636
+ });
637
+ checks.push({
638
+ code: commandResolvable ? "claude_acp_command_resolvable" : "claude_acp_command_missing",
639
+ level: commandResolvable ? "info" : "error",
640
+ message: commandResolvable
641
+ ? `Claude ACP server command is executable: ${command}`
642
+ : `Claude ACP server command is not available: ${command}`,
643
+ hint: commandResolvable
644
+ ? undefined
645
+ : "Install dependencies so @agentclientprotocol/claude-agent-acp is present, or set agentCommand to a valid Claude ACP server command.",
646
+ });
647
+ const envConfig = parseObject(config.env);
648
+ const considerHostEnv = !targetIsRemote && !config.managedAiConnection;
649
+ const hasBedrock = envConfig.CLAUDE_CODE_USE_BEDROCK === "1" ||
650
+ envConfig.CLAUDE_CODE_USE_BEDROCK === "true" ||
651
+ (considerHostEnv && process.env.CLAUDE_CODE_USE_BEDROCK === "1") ||
652
+ (considerHostEnv && process.env.CLAUDE_CODE_USE_BEDROCK === "true") ||
653
+ isNonEmpty(envConfig.ANTHROPIC_BEDROCK_BASE_URL) ||
654
+ (considerHostEnv && isNonEmpty(process.env.ANTHROPIC_BEDROCK_BASE_URL));
655
+ const configApiKey = envConfig.ANTHROPIC_API_KEY;
656
+ const hostApiKey = considerHostEnv ? process.env.ANTHROPIC_API_KEY : undefined;
657
+ const hostOauthToken = considerHostEnv ? process.env.CLAUDE_CODE_OAUTH_TOKEN : undefined;
658
+ const hostAuthToken = considerHostEnv ? process.env.ANTHROPIC_AUTH_TOKEN : undefined;
659
+ const hostConfigDir = considerHostEnv ? process.env.CLAUDE_CONFIG_DIR : undefined;
660
+ if (hasBedrock) {
661
+ checks.push({
662
+ code: "claude_acp_bedrock_auth",
663
+ level: "info",
664
+ message: "AWS Bedrock auth detected. Claude ACP will use Bedrock for inference.",
665
+ hint: "Ensure AWS credentials and AWS_REGION are configured in this environment.",
666
+ });
667
+ }
668
+ else if (isNonEmpty(configApiKey) || isNonEmpty(hostApiKey)) {
669
+ const source = isNonEmpty(configApiKey) ? "adapter config env" : "server environment";
670
+ const selectedApiKey = Boolean(config.managedAiConnection) || isNonEmpty(configApiKey);
671
+ checks.push({
672
+ code: "claude_acp_anthropic_api_key_detected",
673
+ level: selectedApiKey ? "info" : "warn",
674
+ message: selectedApiKey ? "Using the selected Claude API connection." : "ANTHROPIC_API_KEY is set. Claude ACP will use API-key auth instead of subscription credentials.",
675
+ detail: `Detected in ${source}.`,
676
+ hint: selectedApiKey ? undefined : "Unset ANTHROPIC_API_KEY if you want subscription-based Claude login behavior.",
677
+ });
678
+ }
679
+ else if (isNonEmpty(envConfig.CLAUDE_CODE_OAUTH_TOKEN) ||
680
+ (considerHostEnv && isNonEmpty(process.env.CLAUDE_CODE_OAUTH_TOKEN))) {
681
+ const source = isNonEmpty(envConfig.CLAUDE_CODE_OAUTH_TOKEN)
682
+ ? "configured environment variables"
683
+ : "server environment";
684
+ checks.push({
685
+ code: "claude_oauth_token_configured",
686
+ level: "info",
687
+ message: "CLAUDE_CODE_OAUTH_TOKEN is set. Claude ACP will authenticate with the configured subscription token; no stored login is needed on the execution target.",
688
+ detail: `Detected in ${source}.`,
689
+ });
690
+ }
691
+ else if (!targetIsRemote) {
692
+ checks.push({
693
+ code: "claude_acp_subscription_mode_possible",
694
+ level: "info",
695
+ message: "ANTHROPIC_API_KEY is not set; subscription-based auth can be used if Claude is logged in.",
696
+ });
697
+ }
698
+ // Run a real hello probe for every target when Bedrock and a config API key
699
+ // are both absent. A local target inherits the host environment, so the real
700
+ // ACP run authenticates with a host ANTHROPIC_API_KEY. The probe seeds the
701
+ // same host key below when the config sets none, so the probe uses the
702
+ // credential the real run receives and does not report a false auth-required.
703
+ // A remote target does not inherit the host env, so considerHostEnv is false
704
+ // and the probe never reads the host key. The CLI lane already probes every
705
+ // target; the ACP lane now matches it, so a local or SSH target no longer
706
+ // reports a pass without a credential check. Prepare the sandbox the same way
707
+ // the CLI lane does — install the Claude CLI when it is absent and materialize
708
+ // the managed CLAUDE_CONFIG_DIR. The preparation is a no-op for a local or SSH
709
+ // target. The probe returns the canonical adapter_auth_missing signal only for
710
+ // a sandbox target, and a distinct warn check when the probe cannot run. The
711
+ // user interface reads the canonical signal to offer login on the sandbox ACP
712
+ // path.
713
+ if (!hasBedrock && !isNonEmpty(configApiKey)) {
714
+ const probeEnv = {};
715
+ for (const [key, value] of Object.entries(envConfig)) {
716
+ if (typeof value === "string")
717
+ probeEnv[key] = value;
718
+ }
719
+ // Seed the host ANTHROPIC_API_KEY when the config sets no key, so the probe
720
+ // env matches the credential the real local run inherits from the host.
721
+ if (isNonEmpty(hostApiKey) && !isNonEmpty(probeEnv.ANTHROPIC_API_KEY)) {
722
+ probeEnv.ANTHROPIC_API_KEY = hostApiKey.trim();
723
+ }
724
+ // Seed the host CLAUDE_CODE_OAUTH_TOKEN the same way. A local ACP run
725
+ // inherits a host subscription OAuth token, so the probe must receive the
726
+ // same token. Without this seed a valid host OAuth-token setup reports a
727
+ // false claude_hello_probe_auth_required and fails the Test lane.
728
+ if (isNonEmpty(hostOauthToken) && !isNonEmpty(probeEnv.CLAUDE_CODE_OAUTH_TOKEN)) {
729
+ probeEnv.CLAUDE_CODE_OAUTH_TOKEN = hostOauthToken.trim();
730
+ }
731
+ // Seed the host ANTHROPIC_AUTH_TOKEN the same way. A local ACP run inherits
732
+ // a host bearer auth token, so the probe must receive the same token.
733
+ // Without this seed a valid host ANTHROPIC_AUTH_TOKEN setup reports a false
734
+ // claude_hello_probe_auth_required and fails the Test lane.
735
+ if (isNonEmpty(hostAuthToken) && !isNonEmpty(probeEnv.ANTHROPIC_AUTH_TOKEN)) {
736
+ probeEnv.ANTHROPIC_AUTH_TOKEN = hostAuthToken.trim();
737
+ }
738
+ // Seed the host CLAUDE_CONFIG_DIR the same way. A local ACP run reads the
739
+ // stored Claude login from the host CLAUDE_CONFIG_DIR, so the probe must
740
+ // read the same stored login. Without this seed a valid host stored login
741
+ // reports a false claude_hello_probe_auth_required and fails the Test lane.
742
+ if (isNonEmpty(hostConfigDir) && !isNonEmpty(probeEnv.CLAUDE_CONFIG_DIR)) {
743
+ probeEnv.CLAUDE_CONFIG_DIR = hostConfigDir.trim();
744
+ }
745
+ const runId = `claude-acp-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`;
746
+ checks.push(...(await prepareSandboxClaudeProbeRuntime({
747
+ managedAiConnection: Boolean(config.managedAiConnection),
748
+ runId,
749
+ target,
750
+ cwd,
751
+ companyId: ctx.companyId,
752
+ env: probeEnv,
753
+ installCommand: SANDBOX_INSTALL_COMMAND,
754
+ detectCommand: "claude",
755
+ targetIsRemote,
756
+ targetIsSandbox,
757
+ helloProbeTimeoutSec: asNumber(config.helloProbeTimeoutSec, targetIsSandbox ? 90 : 45),
758
+ })));
759
+ const canProbe = !checks.some((check) => check.code === "claude_managed_config_dir_failed");
760
+ if (canProbe) {
761
+ checks.push(...(await probeClaudeAcpSandboxLogin({ config, target, env: probeEnv })));
762
+ }
763
+ }
764
+ const mode = firstNonEmptyString(config.mode, config.acpMode) ?? DEFAULT_ACP_ENGINE_MODE;
765
+ const warmHandleIdleMs = asNumber(config.warmHandleIdleMs ?? config.acpWarmHandleIdleMs, DEFAULT_ACP_ENGINE_WARM_HANDLE_IDLE_MS);
766
+ checks.push({
767
+ code: "claude_acp_runtime_scaffold",
768
+ level: "info",
769
+ message: "Claude ACP runtime execution is available through the shared ACP engine.",
770
+ detail: `mode=${mode}; warmHandleIdleMs=${warmHandleIdleMs}`,
771
+ });
772
+ return {
773
+ adapterType: ctx.adapterType,
774
+ status: summarizeStatus(checks),
775
+ checks,
776
+ testedAt: new Date().toISOString(),
777
+ };
778
+ }
779
+ //# sourceMappingURL=acp.js.map