replicas-engine 0.1.633 → 0.1.634

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.
@@ -226,14 +226,43 @@ function gitIdentityConfigCommands(identity, scope = "global") {
226
226
 
227
227
  // ../shared/src/headless-agent.ts
228
228
  import { z } from "zod";
229
- var headlessAgentRequestSchema = z.object({
229
+ var headlessAgentRequestBaseSchema = z.object({
230
230
  agent: z.custom(
231
231
  (value) => typeof value === "string" && isValidCodingAgentProvider(value)
232
232
  ),
233
233
  model: z.string().min(1),
234
234
  prompt: z.string(),
235
- outputSchema: z.record(z.string(), z.json()),
236
- workingDirectory: z.string().min(1)
235
+ workingDirectory: z.string().min(1),
236
+ timeoutSeconds: z.number().int().min(1).max(1800).default(300)
237
+ });
238
+ var headlessAgentInputFileSchema = z.object({
239
+ path: z.string().min(1),
240
+ downloadUrl: z.url()
241
+ });
242
+ var headlessAgentOutputFileSchema = z.object({
243
+ path: z.string().min(1),
244
+ uploadUrl: z.url(),
245
+ contentType: z.string().min(1),
246
+ maxChars: z.number().int().positive().optional()
247
+ });
248
+ var headlessAgentRequestSchema = z.discriminatedUnion("mode", [
249
+ headlessAgentRequestBaseSchema.extend({
250
+ mode: z.literal("structured"),
251
+ outputSchema: z.record(z.string(), z.json())
252
+ }),
253
+ headlessAgentRequestBaseSchema.extend({
254
+ mode: z.literal("filesystem"),
255
+ inputFiles: z.array(headlessAgentInputFileSchema),
256
+ outputFiles: z.array(headlessAgentOutputFileSchema),
257
+ sensitiveValues: z.array(z.string())
258
+ })
259
+ ]);
260
+ var headlessFilesystemAgentResultSchema = z.object({
261
+ message: z.string(),
262
+ files: z.record(z.string(), z.object({
263
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
264
+ sizeBytes: z.number().int().nonnegative()
265
+ }))
237
266
  });
238
267
 
239
268
  // ../shared/src/engine/environment.ts
@@ -876,6 +905,53 @@ function paginateChatHistory(full, params) {
876
905
  };
877
906
  }
878
907
 
908
+ // ../shared/src/memory-safety.ts
909
+ var SECRET_PATTERNS = [
910
+ /sk-[A-Za-z0-9_-]{10,}/g,
911
+ /ghp_[A-Za-z0-9]{20,}/g,
912
+ /github_pat_[A-Za-z0-9_]{20,}/g,
913
+ /xox[abps]-[A-Za-z0-9-]{10,}/g,
914
+ /AKIA[0-9A-Z]{16}/g,
915
+ /eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}/g,
916
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
917
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/g,
918
+ /\b(?:authorization\s*:\s*bearer|bearer)\s+[a-z0-9._~+/=-]{16,}/gi,
919
+ /\b(?:api[_-]?key|token|secret|password|passwd)\s*[:=]\s*["']?[a-z0-9._~+/=-]{12,}["']?/gi,
920
+ /\b[A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)\s*=\s*[^\s]{12,}/g
921
+ ];
922
+ var MIN_SECRET_MATCH_CHARS = 16;
923
+ var flatten = (value) => value.toLowerCase().replace(/[^a-z0-9]/g, "");
924
+ function containsSecret(value) {
925
+ return SECRET_PATTERNS.some((pattern) => {
926
+ pattern.lastIndex = 0;
927
+ return pattern.test(value);
928
+ });
929
+ }
930
+ function secretEncodings(secret) {
931
+ const bytes = new TextEncoder().encode(secret);
932
+ let binary = "";
933
+ for (const byte of bytes) binary += String.fromCharCode(byte);
934
+ const base64 = btoa(binary);
935
+ return [
936
+ secret,
937
+ base64,
938
+ base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""),
939
+ [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join(""),
940
+ [...secret].reverse().join("")
941
+ ];
942
+ }
943
+ function containsKnownSecret(value, secrets) {
944
+ const haystack = flatten(value);
945
+ if (!haystack) return false;
946
+ return secrets.some((secret) => secretEncodings(secret).some((needle) => {
947
+ const flattened = flatten(needle);
948
+ return flattened.length >= MIN_SECRET_MATCH_CHARS && haystack.includes(flattened);
949
+ }));
950
+ }
951
+ function isUnsafeMemoryOutput(value, secrets, maxChars) {
952
+ return maxChars !== void 0 && value.length > maxChars || containsSecret(value) || containsKnownSecret(value, secrets);
953
+ }
954
+
879
955
  // ../shared/src/async.ts
880
956
  var TIMEOUT = /* @__PURE__ */ Symbol("timeout");
881
957
  async function raceWithTimeout(promise, ms) {
@@ -5514,6 +5590,39 @@ function isSkillRegistryManifest(value) {
5514
5590
  );
5515
5591
  }
5516
5592
 
5593
+ // src/utils/presigned-upload.ts
5594
+ import { createReadStream } from "fs";
5595
+ import { request as httpRequest } from "http";
5596
+ import { request as httpsRequest } from "https";
5597
+ async function putPresignedFile(urlValue, filePath, size, contentType) {
5598
+ await new Promise((resolve, reject) => {
5599
+ const url = new URL(urlValue);
5600
+ const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, {
5601
+ method: "PUT",
5602
+ headers: {
5603
+ "content-length": String(size),
5604
+ "content-type": contentType
5605
+ }
5606
+ }, (response) => {
5607
+ response.setEncoding("utf8");
5608
+ let body = "";
5609
+ response.on("data", (chunk) => {
5610
+ body += chunk;
5611
+ });
5612
+ response.on("end", () => {
5613
+ const status = response.statusCode ?? 0;
5614
+ if (status >= 200 && status < 300) resolve();
5615
+ else reject(new Error(`upload failed: ${status} ${body}`));
5616
+ });
5617
+ response.on("error", reject);
5618
+ });
5619
+ request.on("error", reject);
5620
+ const file = size > 0 ? createReadStream(filePath, { start: 0, end: size - 1 }) : createReadStream(filePath);
5621
+ file.on("error", (error) => request.destroy(error));
5622
+ file.pipe(request);
5623
+ });
5624
+ }
5625
+
5517
5626
  // src/engine-env.ts
5518
5627
  import { readFileSync as readFileSync2 } from "fs";
5519
5628
  import { homedir as homedir2 } from "os";
@@ -5930,7 +6039,7 @@ var DEFAULT_CODEX_ARGS = [
5930
6039
  var MIN_CODEX_CLI_VERSION = "0.144.6";
5931
6040
  var CODEX_UPGRADE_TIMEOUT_MS = 12e4;
5932
6041
  var codexCliVersionEnsured = null;
5933
- var ENGINE_PACKAGE_VERSION = "0.1.633";
6042
+ var ENGINE_PACKAGE_VERSION = "0.1.634";
5934
6043
  var INITIALIZE_METHOD = "initialize";
5935
6044
  var INITIALIZED_NOTIFICATION = "initialized";
5936
6045
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -6247,7 +6356,9 @@ export {
6247
6356
  parseAgentEvents,
6248
6357
  parseDisplayMessages,
6249
6358
  parseChatTranscriptArtifact,
6359
+ isUnsafeMemoryOutput,
6250
6360
  isSkillRegistryManifest,
6361
+ putPresignedFile,
6251
6362
  isRecord4 as isRecord2,
6252
6363
  IS_WARMING_MODE,
6253
6364
  ENGINE_ENV,
@@ -2,24 +2,106 @@
2
2
  import {
3
3
  AGENT,
4
4
  AppServerProcess,
5
- headlessAgentRequestSchema
6
- } from "./chunk-NPHY6LVR.js";
5
+ headlessAgentRequestSchema,
6
+ isUnsafeMemoryOutput,
7
+ putPresignedFile
8
+ } from "./chunk-GT5CS5BI.js";
7
9
 
8
10
  // src/headless-agent.ts
9
- import { readFile, writeFile } from "fs/promises";
11
+ import { createHash } from "crypto";
12
+ import { createWriteStream } from "fs";
13
+ import { mkdir, readFile, stat, writeFile } from "fs/promises";
14
+ import { request as httpRequest } from "http";
15
+ import { request as httpsRequest } from "https";
16
+ import path from "path";
10
17
  import { query } from "@anthropic-ai/claude-agent-sdk";
11
- var TURN_TIMEOUT_MS = 3e5;
18
+ var FILESYSTEM_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep"];
19
+ function resolveFile(root, relativePath) {
20
+ if (path.isAbsolute(relativePath)) throw new Error("Headless agent file paths must be relative");
21
+ const resolved = path.resolve(root, relativePath);
22
+ if (resolved === root || !resolved.startsWith(`${root}${path.sep}`)) {
23
+ throw new Error("Headless agent file path escapes its working directory");
24
+ }
25
+ return resolved;
26
+ }
27
+ async function downloadFile(urlValue, destination) {
28
+ await mkdir(path.dirname(destination), { recursive: true, mode: 448 });
29
+ await new Promise((resolve, reject) => {
30
+ const url = new URL(urlValue);
31
+ const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, (response) => {
32
+ const status = response.statusCode ?? 0;
33
+ if (status < 200 || status >= 300) {
34
+ response.resume();
35
+ reject(new Error(`File download failed with status ${status}`));
36
+ return;
37
+ }
38
+ const file = createWriteStream(destination, { mode: 384 });
39
+ response.on("error", (error) => file.destroy(error));
40
+ file.on("error", reject);
41
+ file.on("finish", resolve);
42
+ response.pipe(file);
43
+ });
44
+ request.on("error", reject);
45
+ request.end();
46
+ });
47
+ }
48
+ async function stageFilesystemInputs(request) {
49
+ await Promise.all(request.inputFiles.map((file) => downloadFile(
50
+ file.downloadUrl,
51
+ resolveFile(request.workingDirectory, file.path)
52
+ )));
53
+ }
54
+ async function publishFilesystemOutputs(request) {
55
+ const files = {};
56
+ const uploads = [];
57
+ for (const output of request.outputFiles) {
58
+ const filePath = resolveFile(request.workingDirectory, output.path);
59
+ const content = (await readFile(filePath, "utf8")).trim();
60
+ if (isUnsafeMemoryOutput(content, request.sensitiveValues, output.maxChars)) {
61
+ throw new Error(`Headless agent output failed validation: ${output.path}`);
62
+ }
63
+ await writeFile(filePath, content, { mode: 384 });
64
+ const { size } = await stat(filePath);
65
+ files[output.path] = { sha256: createHash("sha256").update(content).digest("hex"), sizeBytes: size };
66
+ uploads.push({ url: output.uploadUrl, path: filePath, contentType: output.contentType, size });
67
+ }
68
+ await Promise.all(uploads.map((upload) => putPresignedFile(
69
+ upload.url,
70
+ upload.path,
71
+ upload.size,
72
+ upload.contentType
73
+ )));
74
+ return files;
75
+ }
76
+ function filesystemPermission(root) {
77
+ return async (toolName, input) => {
78
+ const candidate = input.file_path ?? input.path;
79
+ if (typeof candidate === "string") {
80
+ const resolved = path.resolve(root, candidate);
81
+ if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
82
+ return { behavior: "deny", message: "Memory maintenance is restricted to its working directory." };
83
+ }
84
+ }
85
+ if (toolName === "Glob" && typeof input.pattern === "string") {
86
+ if (path.isAbsolute(input.pattern) || input.pattern.split(/[\\/]/).includes("..")) {
87
+ return { behavior: "deny", message: "Memory maintenance is restricted to its working directory." };
88
+ }
89
+ }
90
+ return { behavior: "allow", updatedInput: input };
91
+ };
92
+ }
12
93
  async function runClaude(request) {
13
94
  const response = query({
14
95
  prompt: request.prompt,
15
96
  options: {
16
97
  cwd: request.workingDirectory,
17
98
  model: request.model,
18
- tools: [],
19
- permissionMode: "dontAsk",
99
+ tools: request.mode === "filesystem" ? FILESYSTEM_TOOLS : [],
100
+ permissionMode: request.mode === "filesystem" ? "default" : "dontAsk",
101
+ ...request.mode === "filesystem" ? { canUseTool: filesystemPermission(request.workingDirectory) } : {},
20
102
  settingSources: [],
21
103
  persistSession: false,
22
- outputFormat: { type: "json_schema", schema: request.outputSchema },
104
+ ...request.mode === "structured" ? { outputFormat: { type: "json_schema", schema: request.outputSchema } } : {},
23
105
  env: { ...process.env, CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1" }
24
106
  }
25
107
  });
@@ -33,9 +115,10 @@ async function runClaude(request) {
33
115
  }
34
116
  if (!result) throw new Error("Claude Agent SDK returned no result");
35
117
  if (result.subtype !== "success") throw new Error(result.errors.join("\n") || result.subtype);
118
+ if (request.mode === "filesystem") return result.result;
36
119
  return result.structured_output ?? JSON.parse(result.result);
37
120
  }
38
- function runCodexTurn(client, params) {
121
+ function runCodexTurn(client, params, timeoutMs) {
39
122
  return new Promise((resolve, reject) => {
40
123
  const cleanup = () => {
41
124
  clearTimeout(timeout);
@@ -54,7 +137,7 @@ function runCodexTurn(client, params) {
54
137
  const timeout = setTimeout(() => {
55
138
  cleanup();
56
139
  reject(new Error("Codex ASP turn timed out"));
57
- }, TURN_TIMEOUT_MS);
140
+ }, timeoutMs);
58
141
  client.on("notification", onNotification);
59
142
  client.on("dispose", onDispose);
60
143
  void client.request("turn/start", params).catch((error) => {
@@ -78,7 +161,7 @@ async function runCodex(request) {
78
161
  cwd: request.workingDirectory,
79
162
  approvalPolicy: "never",
80
163
  approvalsReviewer: "user",
81
- sandbox: "read-only",
164
+ sandbox: request.mode === "filesystem" ? "workspace-write" : "read-only",
82
165
  ephemeral: true,
83
166
  environments: [],
84
167
  dynamicTools: [],
@@ -87,14 +170,15 @@ async function runCodex(request) {
87
170
  const turn = await runCodexTurn(client, {
88
171
  threadId: thread.thread.id,
89
172
  input: [{ type: "text", text: request.prompt, text_elements: [] }],
90
- outputSchema: request.outputSchema,
173
+ ...request.mode === "structured" ? { outputSchema: request.outputSchema } : {},
91
174
  approvalPolicy: "never",
92
175
  approvalsReviewer: "user",
93
176
  environments: []
94
- });
177
+ }, request.timeoutSeconds * 1e3);
95
178
  if (turn.status === "failed") throw new Error(turn.error?.message ?? "Codex ASP turn failed");
96
179
  const message = turn.items.findLast((item) => item.type === "agentMessage");
97
180
  if (!message || message.type !== "agentMessage") throw new Error("Codex ASP returned no final message");
181
+ if (request.mode === "filesystem") return message.text;
98
182
  return JSON.parse(message.text);
99
183
  } finally {
100
184
  await appServer.stop();
@@ -104,6 +188,7 @@ async function main() {
104
188
  const [requestPath, outputPath] = process.argv.slice(2);
105
189
  if (!requestPath || !outputPath) throw new Error("Usage: replicas-headless-agent <request.json> <output.json>");
106
190
  const request = headlessAgentRequestSchema.parse(JSON.parse(await readFile(requestPath, "utf8")));
191
+ if (request.mode === "filesystem") await stageFilesystemInputs(request);
107
192
  let result;
108
193
  switch (request.agent) {
109
194
  case AGENT.CLAUDE:
@@ -115,6 +200,10 @@ async function main() {
115
200
  default:
116
201
  throw new Error(`Unsupported headless agent: ${request.agent}`);
117
202
  }
203
+ if (request.mode === "filesystem") {
204
+ if (typeof result !== "string") throw new Error("Filesystem agent returned an invalid result");
205
+ result = { message: result, files: await publishFilesystemOutputs(request) };
206
+ }
118
207
  await writeFile(outputPath, JSON.stringify(result), { mode: 384 });
119
208
  }
120
209
  main().catch((error) => {
package/dist/src/index.js CHANGED
@@ -140,6 +140,7 @@ import {
140
140
  parseMcpToolName,
141
141
  parseReplicasConfigString,
142
142
  percentage,
143
+ putPresignedFile,
143
144
  raceWithTimeout,
144
145
  resolveClaudeAuthMethodForMessage,
145
146
  resolveWarmHookConfig,
@@ -148,7 +149,7 @@ import {
148
149
  setAgentCredentialSnapshot,
149
150
  shellQuotePosix,
150
151
  stripAgentDiagnosticErrors
151
- } from "./chunk-NPHY6LVR.js";
152
+ } from "./chunk-GT5CS5BI.js";
152
153
 
153
154
  // src/index.ts
154
155
  import { serve } from "@hono/node-server";
@@ -10099,8 +10100,6 @@ async function reconcileCanvasItems(filenames) {
10099
10100
  import { createReadStream } from "fs";
10100
10101
  import { createHash as createHash2 } from "crypto";
10101
10102
  import { readdir as readdir8, readFile as readFile14, stat as stat4 } from "fs/promises";
10102
- import { request as httpRequest } from "http";
10103
- import { request as httpsRequest } from "https";
10104
10103
  import { basename as basename2, join as join23 } from "path";
10105
10104
 
10106
10105
  // src/services/chat/chat-senders.ts
@@ -10127,34 +10126,6 @@ var HISTORY_DIRS = [
10127
10126
  join23(ENGINE_DIR2, "relay-histories"),
10128
10127
  join23(ENGINE_DIR2, "codex-histories")
10129
10128
  ];
10130
- async function putTranscript(uploadUrl, filePath, size) {
10131
- await new Promise((resolve5, reject) => {
10132
- const url = new URL(uploadUrl);
10133
- const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, {
10134
- method: "PUT",
10135
- headers: {
10136
- "content-length": String(size),
10137
- "content-type": "application/x-ndjson"
10138
- }
10139
- }, (response) => {
10140
- response.setEncoding("utf8");
10141
- let body = "";
10142
- response.on("data", (chunk) => {
10143
- body += chunk;
10144
- });
10145
- response.on("end", () => {
10146
- const status = response.statusCode ?? 0;
10147
- if (status >= 200 && status < 300) resolve5();
10148
- else reject(new Error(`upload failed: ${status} ${body}`));
10149
- });
10150
- response.on("error", reject);
10151
- });
10152
- request.on("error", reject);
10153
- const file = createReadStream(filePath, { start: 0, end: size - 1 });
10154
- file.on("error", (error) => request.destroy(error));
10155
- file.pipe(request);
10156
- });
10157
- }
10158
10129
  async function flushAllChatTranscripts(chatsById = /* @__PURE__ */ new Map(), capture) {
10159
10130
  let flushed = 0;
10160
10131
  let failed = 0;
@@ -10222,7 +10193,7 @@ async function uploadChatTranscript(chatId, filePath, chat, capture) {
10222
10193
  throw new Error("prepare failed: invalid response");
10223
10194
  }
10224
10195
  if (prepareBody.uploadUrl === null) return null;
10225
- await putTranscript(prepareBody.uploadUrl, filePath, size);
10196
+ await putPresignedFile(prepareBody.uploadUrl, filePath, size, "application/x-ndjson");
10226
10197
  const finalizeResponse = await monolithRequest("/v1/engine/chat-transcripts/finalize", {
10227
10198
  body: uploadRequest
10228
10199
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.633",
3
+ "version": "0.1.634",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",