replicas-engine 0.1.269 → 0.1.271

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 +240 -6
  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-v6";
289
+ var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-06-06-v2";
290
290
 
291
291
  // ../shared/src/runtime-env.ts
292
292
  function parsePosixEnvFile(content) {
@@ -2039,7 +2039,7 @@ function classifyCanvasFilename(filename) {
2039
2039
  // ../shared/src/engine/v1.ts
2040
2040
  var MERGED_MESSAGE_SEPARATOR = "\n\n<!-- replicas:merged -->\n\n";
2041
2041
  function normalizeCodexAspTranscriptStatus(status, failed = false) {
2042
- if (failed || status === "failed") return "failed";
2042
+ if (failed || status === "failed" || status === "declined") return "failed";
2043
2043
  if (status === "completed") return "completed";
2044
2044
  return "in_progress";
2045
2045
  }
@@ -2198,6 +2198,60 @@ var CLAUDE_AUTH_ENV_KEYS_BY_METHOD = {
2198
2198
  ]
2199
2199
  };
2200
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
+
2201
2255
  // ../shared/src/routes/workspaces.ts
2202
2256
  var WORKSPACE_FILE_UPLOAD_MAX_SIZE_BYTES = 20 * 1024 * 1024;
2203
2257
  var WORKSPACE_FILE_CONTENT_MAX_SIZE_BYTES = 1 * 1024 * 1024;
@@ -2558,6 +2612,7 @@ function loadEngineEnv() {
2558
2612
  AWS_REGION: readEnv("AWS_REGION"),
2559
2613
  REPLICAS_CLAUDE_AUTH_METHOD: parseClaudeAuthMethod(readEnv("REPLICAS_CLAUDE_AUTH_METHOD")),
2560
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",
2561
2616
  REPLICAS_ENV_SYSTEM_PROMPT: readEnv("REPLICAS_ENV_SYSTEM_PROMPT"),
2562
2617
  REPLICAS_ENV_START_HOOK: readEnv("REPLICAS_ENV_START_HOOK"),
2563
2618
  REPLICAS_DISABLE_AUTO_START_HOOKS: readEnv("REPLICAS_DISABLE_AUTO_START_HOOKS")?.toLowerCase() === "true"
@@ -5631,6 +5686,52 @@ var LinearEventForwarder = class {
5631
5686
  }
5632
5687
  };
5633
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
+
5634
5735
  // src/managers/claude-manager.ts
5635
5736
  var PromptStream = class {
5636
5737
  queue = [];
@@ -5705,6 +5806,18 @@ var ALWAYS_DISALLOWED_TOOLS = [
5705
5806
  "PushNotification"
5706
5807
  ];
5707
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
+ ]);
5708
5821
  var TOOL_INPUT_HANDLERS = {
5709
5822
  ExitPlanMode: {
5710
5823
  getRequest: () => ({
@@ -5924,6 +6037,29 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
5924
6037
  return result;
5925
6038
  };
5926
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
+ }
5927
6063
  async processMessageInternal(request) {
5928
6064
  const MAX_AUTH_RETRIES = 2;
5929
6065
  let lastError;
@@ -6158,7 +6294,16 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
6158
6294
  env: queryEnv,
6159
6295
  model: resolvedModel,
6160
6296
  ...thinkingLevel ? { effort: thinkingLevel } : {},
6161
- 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
+ } : {}
6162
6307
  }
6163
6308
  });
6164
6309
  this.activeQuery = response;
@@ -6582,7 +6727,7 @@ var AspClient = class {
6582
6727
  // src/managers/codex-asp/app-server-process.ts
6583
6728
  var DEFAULT_CODEX_BINARY = "codex";
6584
6729
  var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
6585
- var ENGINE_PACKAGE_VERSION = "0.1.269";
6730
+ var ENGINE_PACKAGE_VERSION = "0.1.271";
6586
6731
  var INITIALIZE_METHOD = "initialize";
6587
6732
  var INITIALIZED_NOTIFICATION = "initialized";
6588
6733
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -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.269",
3
+ "version": "0.1.271",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",