replicas-engine 0.1.268 → 0.1.270

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 (2) hide show
  1. package/dist/src/index.js +247 -13
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -286,7 +286,7 @@ var WORKSPACE_SIZES = ["small", "large"];
286
286
  var INVALID_WORKSPACE_SIZE_ERROR = `Invalid size: must be one of ${WORKSPACE_SIZES.join(", ")}`;
287
287
 
288
288
  // ../shared/src/e2b.ts
289
- var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-06-05-v5";
289
+ var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-06-05-v7";
290
290
 
291
291
  // ../shared/src/runtime-env.ts
292
292
  function parsePosixEnvFile(content) {
@@ -1967,6 +1967,13 @@ function isClaudeAuthErrorText(text) {
1967
1967
  return lower.includes("failed to authenticate") || lower.includes("authentication_error") || lower.includes("authentication_failed") || lower.includes("authentication failed") || lower.includes("invalid authentication credentials") || lower.includes("not logged in") || lower.includes("please run /login") || lower.includes("credit balance is too low") || lower.includes("401") && lower.includes("authentic");
1968
1968
  }
1969
1969
 
1970
+ // ../shared/src/codex-auth.ts
1971
+ function isCodexAuthError(error) {
1972
+ const msg = error instanceof Error ? error.message : String(error);
1973
+ const lower = msg.toLowerCase();
1974
+ return lower.includes("unauthorized") || lower.includes("authentication") || lower.includes("invalid api key") || lower.includes("incorrect api key") || lower.includes("api key") && lower.includes("invalid") || lower.includes("401") || lower.includes('codexerrorinfo="unauthorized"') || lower.includes("codexerrorinfo=unauthorized") || lower.includes("failed to refresh token") || lower.includes("your session has ended");
1975
+ }
1976
+
1970
1977
  // ../shared/src/engine/environment.ts
1971
1978
  var DESKTOP_NOVNC_PORT = 6080;
1972
1979
 
@@ -2032,7 +2039,7 @@ function classifyCanvasFilename(filename) {
2032
2039
  // ../shared/src/engine/v1.ts
2033
2040
  var MERGED_MESSAGE_SEPARATOR = "\n\n<!-- replicas:merged -->\n\n";
2034
2041
  function normalizeCodexAspTranscriptStatus(status, failed = false) {
2035
- if (failed || status === "failed") return "failed";
2042
+ if (failed || status === "failed" || status === "declined") return "failed";
2036
2043
  if (status === "completed") return "completed";
2037
2044
  return "in_progress";
2038
2045
  }
@@ -2191,6 +2198,60 @@ var CLAUDE_AUTH_ENV_KEYS_BY_METHOD = {
2191
2198
  ]
2192
2199
  };
2193
2200
 
2201
+ // ../shared/src/routes/command-protection.ts
2202
+ var COMMAND_PROTECTION_COMMAND_KEYS = ["command", "cmd", "script", "query", "endpoint", "path", "url", "mutation"];
2203
+ function stringifyCommandProtectionInput(value) {
2204
+ if (typeof value === "string") return value;
2205
+ if (value == null) return "";
2206
+ try {
2207
+ return JSON.stringify(value);
2208
+ } catch {
2209
+ return String(value);
2210
+ }
2211
+ }
2212
+ function normalizeCommandProtectionText(text) {
2213
+ return text.replace(/\\\n/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
2214
+ }
2215
+ function extractCommandProtectionCommandText(request, options = {}) {
2216
+ if (request.command && request.command.trim()) return request.command;
2217
+ const input = request.toolInput;
2218
+ if (isRecord(input)) {
2219
+ for (const key of COMMAND_PROTECTION_COMMAND_KEYS) {
2220
+ const value = input[key];
2221
+ if (typeof value === "string" && value.trim()) return value;
2222
+ }
2223
+ }
2224
+ if (typeof input === "string") return input;
2225
+ return options.stringifyFallback ? stringifyCommandProtectionInput(input) : null;
2226
+ }
2227
+ function findPrMergeSignals(request) {
2228
+ const command = normalizeCommandProtectionText(extractCommandProtectionCommandText(request, { stringifyFallback: true }) ?? "");
2229
+ const input = normalizeCommandProtectionText(stringifyCommandProtectionInput(request.toolInput));
2230
+ const combined = `${request.toolName.toLowerCase()} ${command} ${input}`;
2231
+ const checks = [
2232
+ ["gh-pr-merge", /\bgh\s+pr\s+merge\b/],
2233
+ ["gh-api-pulls-merge", /\bgh\s+api\b[\s\S]*\/pulls\/(?:\d+|[^/\s]+)\/merge\b/],
2234
+ ["github-rest-pulls-merge", /api\.github\.com\/repos\/[^/\s]+\/[^/\s]+\/pulls\/(?:\d+|[^/\s]+)\/merge\b/],
2235
+ ["github-rest-ref-update", /\b(?:gh\s+api|curl|wget|python3?)\b[\s\S]*(?:\/repos\/|repos\/)[^/\s]+\/[^/\s]+\/git\/refs\/heads\/(?:main|master|trunk|develop)\b[\s\S]*\b(?:patch|put|sha|oid|force)\b/],
2236
+ ["github-graphql-merge-pr", /\bmergepullrequest\b/],
2237
+ ["github-graphql-enable-auto-merge", /\benablepullrequestautomerge\b/],
2238
+ ["github-graphql-update-ref", /\bupdateref\b/],
2239
+ ["git-merge-default-branch", /\bgit\s+(?:checkout|switch)\s+(?:main|master|trunk|develop)\b[\s\S]*\bgit\s+merge\b/],
2240
+ ["git-merge-standalone", /\bgit\s+merge\s+(?:origin\/)?(main|master|trunk|develop)\b/],
2241
+ ["git-push-main", /\bgit\s+push\b[\s\S]*\b(origin\s+)?(main|master|trunk|develop)\b/],
2242
+ ["git-push-refspec-default-branch", /\bgit\s+push\b[\s\S]*\S+:(?:refs\/heads\/)?(main|master|trunk|develop)\b/],
2243
+ ["git-cherry-pick-default-branch", /\bgit\s+(?:checkout|switch)\s+(?:main|master|trunk|develop)\b[\s\S]*\bgit\s+cherry-pick\b[\s\S]*\bgit\s+push\b/],
2244
+ ["git-push-all", /\bgit\s+push\s+(-[^\s]+\s+)*(--all|--mirror)\b/],
2245
+ ["git-push-bare", /\bgit\s+push\s*(?:["{}\[\]]|$)/],
2246
+ ["hub-merge", /\bhub\s+merge\b/],
2247
+ ["obfuscated-gh-pr-merge", /printf\s+['"]\\x67\\x68['"][\s\S]*\bpr\b[\s\S]*printf\s+['"]m\\x65rge['"]/],
2248
+ ["mcp-merge-tool", /\bmcp__[^\s"]*merge[^\s"]*\b/],
2249
+ ["tool-merge-name", /\b(pr|pull[_-]?request).{0,40}\bmerge\b|\bmerge.{0,40}(pr|pull[_-]?request)\b/],
2250
+ ["merge-word", /(?:^|[^a-z0-9])merge(?:$|[^a-z0-9])/]
2251
+ ];
2252
+ return checks.flatMap(([name, pattern]) => pattern.test(combined) ? [name] : []);
2253
+ }
2254
+
2194
2255
  // ../shared/src/routes/workspaces.ts
2195
2256
  var WORKSPACE_FILE_UPLOAD_MAX_SIZE_BYTES = 20 * 1024 * 1024;
2196
2257
  var WORKSPACE_FILE_CONTENT_MAX_SIZE_BYTES = 1 * 1024 * 1024;
@@ -2551,6 +2612,7 @@ function loadEngineEnv() {
2551
2612
  AWS_REGION: readEnv("AWS_REGION"),
2552
2613
  REPLICAS_CLAUDE_AUTH_METHOD: parseClaudeAuthMethod(readEnv("REPLICAS_CLAUDE_AUTH_METHOD")),
2553
2614
  REPLICAS_CODEX_AUTH_METHOD: parseCodexAuthMethod(readEnv("REPLICAS_CODEX_AUTH_METHOD")),
2615
+ REPLICAS_DISABLE_GH_PR_MERGE: readEnv("REPLICAS_DISABLE_GH_PR_MERGE")?.toLowerCase() === "true",
2554
2616
  REPLICAS_ENV_SYSTEM_PROMPT: readEnv("REPLICAS_ENV_SYSTEM_PROMPT"),
2555
2617
  REPLICAS_ENV_START_HOOK: readEnv("REPLICAS_ENV_START_HOOK"),
2556
2618
  REPLICAS_DISABLE_AUTO_START_HOOKS: readEnv("REPLICAS_DISABLE_AUTO_START_HOOKS")?.toLowerCase() === "true"
@@ -5624,6 +5686,52 @@ var LinearEventForwarder = class {
5624
5686
  }
5625
5687
  };
5626
5688
 
5689
+ // src/services/command-protection-service.ts
5690
+ function asCommandProtectionResponse(value) {
5691
+ if (typeof value !== "object" || value === null) return null;
5692
+ const candidate = value;
5693
+ return typeof candidate.allowed === "boolean" && typeof candidate.enabled === "boolean" ? candidate : null;
5694
+ }
5695
+ function failClosed(reason, matchedSignals) {
5696
+ return {
5697
+ allowed: false,
5698
+ enabled: true,
5699
+ reason,
5700
+ matchedSignals
5701
+ };
5702
+ }
5703
+ async function evaluateCommandProtection(request, signal) {
5704
+ const matchedSignals = findPrMergeSignals(request);
5705
+ if (matchedSignals.length === 0) {
5706
+ return { allowed: true, enabled: true };
5707
+ }
5708
+ let response;
5709
+ try {
5710
+ response = await monolithRequest("/v1/engine/command-protection/evaluate", {
5711
+ body: request,
5712
+ signal
5713
+ });
5714
+ } catch (error) {
5715
+ const detail = error instanceof Error ? error.message : String(error);
5716
+ return failClosed(`Command protection failed closed: ${detail}`, matchedSignals);
5717
+ }
5718
+ if (!response.ok) {
5719
+ const detail = await response.text().catch(() => "");
5720
+ return failClosed(`Command protection failed closed: ${response.status} ${detail}`, matchedSignals);
5721
+ }
5722
+ let payload;
5723
+ try {
5724
+ payload = await response.json();
5725
+ } catch (error) {
5726
+ const detail = error instanceof Error ? error.message : String(error);
5727
+ return failClosed(`Command protection failed closed: ${detail}`, matchedSignals);
5728
+ }
5729
+ return asCommandProtectionResponse(payload) ?? failClosed("Command protection failed closed: invalid response from policy service", matchedSignals);
5730
+ }
5731
+ function extractToolCommand(input) {
5732
+ return extractCommandProtectionCommandText({ toolInput: input }, { stringifyFallback: false });
5733
+ }
5734
+
5627
5735
  // src/managers/claude-manager.ts
5628
5736
  var PromptStream = class {
5629
5737
  queue = [];
@@ -5698,6 +5806,18 @@ var ALWAYS_DISALLOWED_TOOLS = [
5698
5806
  "PushNotification"
5699
5807
  ];
5700
5808
  var INTERACTIVE_TOOL_NAMES = ["AskUserQuestion", "ExitPlanMode"];
5809
+ var COMMAND_PROTECTION_SAFE_TOOLS = /* @__PURE__ */ new Set([
5810
+ "Read",
5811
+ "Write",
5812
+ "Edit",
5813
+ "Glob",
5814
+ "Grep",
5815
+ "NotebookEdit",
5816
+ "TodoRead",
5817
+ "TodoWrite",
5818
+ "WebFetch",
5819
+ "LS"
5820
+ ]);
5701
5821
  var TOOL_INPUT_HANDLERS = {
5702
5822
  ExitPlanMode: {
5703
5823
  getRequest: () => ({
@@ -5917,6 +6037,29 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
5917
6037
  return result;
5918
6038
  };
5919
6039
  }
6040
+ buildCommandProtectionHook() {
6041
+ return async (input, toolUseID, options) => {
6042
+ if (input.hook_event_name !== "PreToolUse") return {};
6043
+ if (COMMAND_PROTECTION_SAFE_TOOLS.has(input.tool_name)) return {};
6044
+ const result = await evaluateCommandProtection({
6045
+ provider: "claude",
6046
+ source: "claude_pre_tool_use",
6047
+ toolName: input.tool_name,
6048
+ toolInput: input.tool_input,
6049
+ command: extractToolCommand(input.tool_input),
6050
+ cwd: input.cwd,
6051
+ toolUseId: toolUseID ?? input.tool_use_id
6052
+ }, options.signal);
6053
+ if (result.allowed) return {};
6054
+ return {
6055
+ hookSpecificOutput: {
6056
+ hookEventName: "PreToolUse",
6057
+ permissionDecision: "deny",
6058
+ permissionDecisionReason: result.reason || "Blocked by organization policy: agents may not merge pull requests."
6059
+ }
6060
+ };
6061
+ };
6062
+ }
5920
6063
  async processMessageInternal(request) {
5921
6064
  const MAX_AUTH_RETRIES = 2;
5922
6065
  let lastError;
@@ -6151,7 +6294,16 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
6151
6294
  env: queryEnv,
6152
6295
  model: resolvedModel,
6153
6296
  ...thinkingLevel ? { effort: thinkingLevel } : {},
6154
- canUseTool: this.buildCanUseTool()
6297
+ canUseTool: this.buildCanUseTool(),
6298
+ ...ENGINE_ENV.REPLICAS_DISABLE_GH_PR_MERGE ? {
6299
+ hooks: {
6300
+ PreToolUse: [{
6301
+ matcher: "*",
6302
+ hooks: [this.buildCommandProtectionHook()],
6303
+ timeout: 30
6304
+ }]
6305
+ }
6306
+ } : {}
6155
6307
  }
6156
6308
  });
6157
6309
  this.activeQuery = response;
@@ -6575,7 +6727,7 @@ var AspClient = class {
6575
6727
  // src/managers/codex-asp/app-server-process.ts
6576
6728
  var DEFAULT_CODEX_BINARY = "codex";
6577
6729
  var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
6578
- var ENGINE_PACKAGE_VERSION = "0.1.268";
6730
+ var ENGINE_PACKAGE_VERSION = "0.1.270";
6579
6731
  var INITIALIZE_METHOD = "initialize";
6580
6732
  var INITIALIZED_NOTIFICATION = "initialized";
6581
6733
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -6834,13 +6986,6 @@ var CodexQuotaStatusTracker = class {
6834
6986
  }
6835
6987
  };
6836
6988
 
6837
- // src/utils/codex-auth.ts
6838
- function isCodexAuthError(error) {
6839
- const msg = error instanceof Error ? error.message : String(error);
6840
- const lower = msg.toLowerCase();
6841
- return lower.includes("unauthorized") || lower.includes("authentication") || lower.includes("invalid api key") || lower.includes("incorrect api key") || lower.includes("api key") && lower.includes("invalid") || lower.includes("401") || lower.includes('codexerrorinfo="unauthorized"') || lower.includes("codexerrorinfo=unauthorized");
6842
- }
6843
-
6844
6989
  // src/managers/codex-asp/mappers.ts
6845
6990
  import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
6846
6991
  var localImageCache = /* @__PURE__ */ new Map();
@@ -6855,6 +7000,15 @@ var TURN_START_METHOD = "turn/start";
6855
7000
  var TURN_INTERRUPT_METHOD = "turn/interrupt";
6856
7001
  var ACCOUNT_RATE_LIMITS_READ_METHOD = "account/rateLimits/read";
6857
7002
  var MAX_CODEX_ASP_TRANSCRIPT_OUTPUT_CHARS = DEFAULT_HOOK_OUTPUT_PREVIEW_CHARS;
7003
+ function codexApprovalPolicyOverrides() {
7004
+ if (!ENGINE_ENV.REPLICAS_DISABLE_GH_PR_MERGE) {
7005
+ return {};
7006
+ }
7007
+ return {
7008
+ approvalPolicy: "untrusted",
7009
+ approvalsReviewer: "user"
7010
+ };
7011
+ }
6858
7012
  function toReasoningEffort(thinkingLevel) {
6859
7013
  return codexReasoningEffortForThinkingLevel(thinkingLevel);
6860
7014
  }
@@ -7250,6 +7404,7 @@ async function buildThreadStartParams(workingDirectory, request, developerInstru
7250
7404
  cwd: workingDirectory,
7251
7405
  runtimeWorkspaceRoots: additionalDirectories,
7252
7406
  sandbox: "danger-full-access",
7407
+ ...codexApprovalPolicyOverrides(),
7253
7408
  developerInstructions: developerInstructions ?? null,
7254
7409
  config: { web_search: "live" },
7255
7410
  experimentalRawEvents: false,
@@ -7264,6 +7419,7 @@ async function buildThreadResumeParams(workingDirectory, threadId, request, deve
7264
7419
  cwd: workingDirectory,
7265
7420
  runtimeWorkspaceRoots: additionalDirectories,
7266
7421
  sandbox: "danger-full-access",
7422
+ ...codexApprovalPolicyOverrides(),
7267
7423
  developerInstructions: developerInstructions ?? null,
7268
7424
  config: { web_search: "live" },
7269
7425
  excludeTurns: false,
@@ -7296,6 +7452,7 @@ async function buildTurnStartParams(threadId, request, developerInstructions) {
7296
7452
  threadId,
7297
7453
  input,
7298
7454
  model,
7455
+ ...codexApprovalPolicyOverrides(),
7299
7456
  ...effort ? { effort } : {},
7300
7457
  ...developerInstructions ? {
7301
7458
  collaborationMode: {
@@ -7808,12 +7965,75 @@ var CodexAspManager = class extends CodingAgentManager {
7808
7965
  const requestId = serverRequest.id;
7809
7966
  const method = serverRequest.method;
7810
7967
  switch (serverRequest.method) {
7811
- case "item/commandExecution/requestApproval":
7968
+ case "item/commandExecution/requestApproval": {
7969
+ if (ENGINE_ENV.REPLICAS_DISABLE_GH_PR_MERGE) {
7970
+ const result = await evaluateCommandProtection({
7971
+ provider: "codex",
7972
+ source: "codex_approval_request",
7973
+ toolName: "commandExecution",
7974
+ toolInput: {
7975
+ command: serverRequest.params.command,
7976
+ commandActions: serverRequest.params.commandActions ?? null
7977
+ },
7978
+ command: serverRequest.params.command,
7979
+ cwd: serverRequest.params.cwd ?? null,
7980
+ toolUseId: serverRequest.params.itemId ?? null
7981
+ });
7982
+ if (!result.allowed) {
7983
+ console.warn(`[CodexAspManager] blocked command approval by PR merge policy: ${result.reason ?? "unknown reason"}`);
7984
+ this.recordBlockedCommand(
7985
+ serverRequest.params.threadId,
7986
+ serverRequest.params.turnId,
7987
+ serverRequest.params.itemId,
7988
+ serverRequest.params.command ?? "",
7989
+ result.reason,
7990
+ serverRequest.params.startedAtMs
7991
+ );
7992
+ host.client.respond(requestId, { decision: "decline" });
7993
+ return;
7994
+ }
7995
+ }
7996
+ console.warn("[CodexAspManager] approval requested while sandbox is danger-full-access");
7997
+ host.client.respond(requestId, { decision: "accept" });
7998
+ return;
7999
+ }
7812
8000
  case "item/fileChange/requestApproval":
7813
8001
  console.warn("[CodexAspManager] approval requested while sandbox is danger-full-access");
7814
8002
  host.client.respond(requestId, { decision: "accept" });
7815
8003
  return;
7816
- case "execCommandApproval":
8004
+ case "execCommandApproval": {
8005
+ if (ENGINE_ENV.REPLICAS_DISABLE_GH_PR_MERGE) {
8006
+ const command = serverRequest.params.command.join(" ");
8007
+ const result = await evaluateCommandProtection({
8008
+ provider: "codex",
8009
+ source: "codex_approval_request",
8010
+ toolName: "execCommand",
8011
+ toolInput: {
8012
+ command: serverRequest.params.command,
8013
+ parsedCmd: serverRequest.params.parsedCmd ?? null
8014
+ },
8015
+ command,
8016
+ cwd: serverRequest.params.cwd ?? null,
8017
+ toolUseId: serverRequest.params.callId ?? null
8018
+ });
8019
+ if (!result.allowed) {
8020
+ console.warn(`[CodexAspManager] blocked legacy command approval by PR merge policy: ${result.reason ?? "unknown reason"}`);
8021
+ this.recordBlockedCommand(
8022
+ this.currentThreadId ?? serverRequest.params.conversationId,
8023
+ this.activeTurnId ?? `legacy-${requestId}`,
8024
+ serverRequest.params.callId,
8025
+ command,
8026
+ result.reason,
8027
+ Date.now()
8028
+ );
8029
+ host.client.respond(requestId, { decision: "denied" });
8030
+ return;
8031
+ }
8032
+ }
8033
+ console.warn("[CodexAspManager] legacy approval requested while sandbox is danger-full-access");
8034
+ host.client.respond(requestId, { decision: "approved" });
8035
+ return;
8036
+ }
7817
8037
  case "applyPatchApproval":
7818
8038
  console.warn("[CodexAspManager] legacy approval requested while sandbox is danger-full-access");
7819
8039
  host.client.respond(requestId, { decision: "approved" });
@@ -7861,6 +8081,20 @@ var CodexAspManager = class extends CodingAgentManager {
7861
8081
  nextTranscriptSequence() {
7862
8082
  return this.codexAspSequence++;
7863
8083
  }
8084
+ recordBlockedCommand(threadId, turnId, itemId, command, reason, startedAtMs) {
8085
+ const output = reason?.includes("Blocked by organization policy") ? reason : `Blocked by organization policy: ${reason || "agents may not merge pull requests."}`;
8086
+ const timestamp = timestampFromMilliseconds(startedAtMs);
8087
+ const blockedItem = {
8088
+ type: "commandExecution",
8089
+ id: itemId,
8090
+ command,
8091
+ aggregatedOutput: output,
8092
+ exitCode: null,
8093
+ status: "declined"
8094
+ };
8095
+ this.upsertTranscriptItem(threadId, turnId, blockedItem, timestamp, "failed", "completed");
8096
+ this.emitTranscriptUpdated(threadId, { immediate: true });
8097
+ }
7864
8098
  syncTranscriptSequence(transcript) {
7865
8099
  if (!transcript) return;
7866
8100
  let next = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.268",
3
+ "version": "0.1.270",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",