codeharbor 0.1.13 → 0.1.15

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.
package/dist/cli.js CHANGED
@@ -24,30 +24,78 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
- var import_node_child_process6 = require("child_process");
27
+ var import_node_child_process7 = require("child_process");
28
28
  var import_node_fs11 = __toESM(require("fs"));
29
- var import_node_path12 = __toESM(require("path"));
29
+ var import_node_path14 = __toESM(require("path"));
30
30
  var import_commander = require("commander");
31
31
 
32
32
  // src/app.ts
33
- var import_node_child_process4 = require("child_process");
34
- var import_node_util2 = require("util");
33
+ var import_node_child_process5 = require("child_process");
34
+ var import_node_util3 = require("util");
35
35
 
36
36
  // src/admin-server.ts
37
- var import_node_child_process2 = require("child_process");
37
+ var import_node_child_process3 = require("child_process");
38
38
  var import_node_fs4 = __toESM(require("fs"));
39
39
  var import_node_http = __toESM(require("http"));
40
- var import_node_path4 = __toESM(require("path"));
41
- var import_node_util = require("util");
40
+ var import_node_path5 = __toESM(require("path"));
41
+ var import_node_util2 = require("util");
42
42
 
43
43
  // src/init.ts
44
44
  var import_node_fs = __toESM(require("fs"));
45
- var import_node_path = __toESM(require("path"));
45
+ var import_node_path2 = __toESM(require("path"));
46
46
  var import_promises = require("readline/promises");
47
47
  var import_dotenv = __toESM(require("dotenv"));
48
+
49
+ // src/codex-bin.ts
50
+ var import_node_child_process = require("child_process");
51
+ var import_node_os = __toESM(require("os"));
52
+ var import_node_path = __toESM(require("path"));
53
+ var import_node_util = require("util");
54
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
55
+ function buildCodexBinCandidates(configuredBin, env = process.env) {
56
+ const normalized = configuredBin.trim() || "codex";
57
+ const home = env.HOME?.trim() || import_node_os.default.homedir();
58
+ const npmGlobalBin = home ? import_node_path.default.resolve(home, ".npm-global/bin/codex") : "";
59
+ const candidates = [
60
+ normalized,
61
+ "codex",
62
+ npmGlobalBin,
63
+ "/usr/bin/codex",
64
+ "/usr/local/bin/codex",
65
+ "/opt/homebrew/bin/codex"
66
+ ];
67
+ const seen = /* @__PURE__ */ new Set();
68
+ const output = [];
69
+ for (const candidate of candidates) {
70
+ const trimmed = candidate.trim();
71
+ if (!trimmed || seen.has(trimmed)) {
72
+ continue;
73
+ }
74
+ seen.add(trimmed);
75
+ output.push(trimmed);
76
+ }
77
+ return output;
78
+ }
79
+ async function findWorkingCodexBin(configuredBin, options = {}) {
80
+ const checkBinary = options.checkBinary ?? defaultCheckBinary;
81
+ const candidates = buildCodexBinCandidates(configuredBin, options.env);
82
+ for (const candidate of candidates) {
83
+ try {
84
+ await checkBinary(candidate);
85
+ return candidate;
86
+ } catch {
87
+ }
88
+ }
89
+ return null;
90
+ }
91
+ async function defaultCheckBinary(bin) {
92
+ await execFileAsync(bin, ["--version"]);
93
+ }
94
+
95
+ // src/init.ts
48
96
  async function runInitCommand(options = {}) {
49
97
  const cwd = options.cwd ?? process.cwd();
50
- const envPath = import_node_path.default.resolve(cwd, ".env");
98
+ const envPath = import_node_path2.default.resolve(cwd, ".env");
51
99
  const templatePath = resolveInitTemplatePath(cwd);
52
100
  const input = options.input ?? process.stdin;
53
101
  const output = options.output ?? process.stdout;
@@ -70,6 +118,7 @@ async function runInitCommand(options = {}) {
70
118
  output.write("CodeHarbor setup wizard\n");
71
119
  output.write(`Target file: ${envPath}
72
120
  `);
121
+ const detectedCodexBin = await findWorkingCodexBin(existingValues.CODEX_BIN ?? "codex");
73
122
  const questions = [
74
123
  {
75
124
  key: "MATRIX_HOMESERVER",
@@ -109,14 +158,14 @@ async function runInitCommand(options = {}) {
109
158
  {
110
159
  key: "CODEX_BIN",
111
160
  label: "Codex binary",
112
- fallbackValue: "codex"
161
+ fallbackValue: detectedCodexBin ?? "codex"
113
162
  },
114
163
  {
115
164
  key: "CODEX_WORKDIR",
116
165
  label: "Codex working directory",
117
166
  fallbackValue: cwd,
118
167
  validate: (value) => {
119
- const resolved = import_node_path.default.resolve(cwd, value);
168
+ const resolved = import_node_path2.default.resolve(cwd, value);
120
169
  if (!import_node_fs.default.existsSync(resolved) || !import_node_fs.default.statSync(resolved).isDirectory()) {
121
170
  return `Directory does not exist: ${resolved}`;
122
171
  }
@@ -144,8 +193,8 @@ async function runInitCommand(options = {}) {
144
193
  }
145
194
  function resolveInitTemplatePath(cwd) {
146
195
  const candidates = [
147
- import_node_path.default.resolve(cwd, ".env.example"),
148
- import_node_path.default.resolve(__dirname, "..", ".env.example")
196
+ import_node_path2.default.resolve(cwd, ".env.example"),
197
+ import_node_path2.default.resolve(__dirname, "..", ".env.example")
149
198
  ];
150
199
  for (const candidate of candidates) {
151
200
  if (import_node_fs.default.existsSync(candidate)) {
@@ -221,33 +270,33 @@ async function askYesNo(rl, question, defaultValue) {
221
270
  }
222
271
 
223
272
  // src/service-manager.ts
224
- var import_node_child_process = require("child_process");
273
+ var import_node_child_process2 = require("child_process");
225
274
  var import_node_fs3 = __toESM(require("fs"));
226
- var import_node_os2 = __toESM(require("os"));
227
- var import_node_path3 = __toESM(require("path"));
275
+ var import_node_os3 = __toESM(require("os"));
276
+ var import_node_path4 = __toESM(require("path"));
228
277
 
229
278
  // src/runtime-home.ts
230
279
  var import_node_fs2 = __toESM(require("fs"));
231
- var import_node_os = __toESM(require("os"));
232
- var import_node_path2 = __toESM(require("path"));
280
+ var import_node_os2 = __toESM(require("os"));
281
+ var import_node_path3 = __toESM(require("path"));
233
282
  var LEGACY_RUNTIME_HOME = "/opt/codeharbor";
234
283
  var USER_RUNTIME_HOME_DIR = ".codeharbor";
235
- var DEFAULT_RUNTIME_HOME = import_node_path2.default.resolve(import_node_os.default.homedir(), USER_RUNTIME_HOME_DIR);
284
+ var DEFAULT_RUNTIME_HOME = import_node_path3.default.resolve(import_node_os2.default.homedir(), USER_RUNTIME_HOME_DIR);
236
285
  var RUNTIME_HOME_ENV_KEY = "CODEHARBOR_HOME";
237
286
  function resolveRuntimeHome(env = process.env) {
238
287
  const configured = env[RUNTIME_HOME_ENV_KEY]?.trim();
239
288
  if (configured) {
240
- return import_node_path2.default.resolve(configured);
289
+ return import_node_path3.default.resolve(configured);
241
290
  }
242
- const legacyEnvPath = import_node_path2.default.resolve(LEGACY_RUNTIME_HOME, ".env");
291
+ const legacyEnvPath = import_node_path3.default.resolve(LEGACY_RUNTIME_HOME, ".env");
243
292
  if (import_node_fs2.default.existsSync(legacyEnvPath)) {
244
293
  return LEGACY_RUNTIME_HOME;
245
294
  }
246
295
  return resolveUserRuntimeHome(env);
247
296
  }
248
297
  function resolveUserRuntimeHome(env = process.env) {
249
- const home = env.HOME?.trim() || import_node_os.default.homedir();
250
- return import_node_path2.default.resolve(home, USER_RUNTIME_HOME_DIR);
298
+ const home = env.HOME?.trim() || import_node_os2.default.homedir();
299
+ return import_node_path3.default.resolve(home, USER_RUNTIME_HOME_DIR);
251
300
  }
252
301
 
253
302
  // src/service-manager.ts
@@ -266,7 +315,7 @@ function resolveDefaultRunUser(env = process.env) {
266
315
  return user;
267
316
  }
268
317
  try {
269
- return import_node_os2.default.userInfo().username;
318
+ return import_node_os3.default.userInfo().username;
270
319
  } catch {
271
320
  return "root";
272
321
  }
@@ -274,17 +323,17 @@ function resolveDefaultRunUser(env = process.env) {
274
323
  function resolveRuntimeHomeForUser(runUser, env = process.env, explicitRuntimeHome) {
275
324
  const configuredRuntimeHome = explicitRuntimeHome?.trim() || env[RUNTIME_HOME_ENV_KEY]?.trim();
276
325
  if (configuredRuntimeHome) {
277
- return import_node_path3.default.resolve(configuredRuntimeHome);
326
+ return import_node_path4.default.resolve(configuredRuntimeHome);
278
327
  }
279
328
  const userHome = resolveUserHome(runUser);
280
329
  if (userHome) {
281
- return import_node_path3.default.resolve(userHome, USER_RUNTIME_HOME_DIR);
330
+ return import_node_path4.default.resolve(userHome, USER_RUNTIME_HOME_DIR);
282
331
  }
283
- return import_node_path3.default.resolve(import_node_os2.default.homedir(), USER_RUNTIME_HOME_DIR);
332
+ return import_node_path4.default.resolve(import_node_os3.default.homedir(), USER_RUNTIME_HOME_DIR);
284
333
  }
285
334
  function buildMainServiceUnit(options) {
286
335
  validateUnitOptions(options);
287
- const runtimeHome2 = import_node_path3.default.resolve(options.runtimeHome);
336
+ const runtimeHome2 = import_node_path4.default.resolve(options.runtimeHome);
288
337
  return [
289
338
  "[Unit]",
290
339
  "Description=CodeHarbor main service",
@@ -296,7 +345,7 @@ function buildMainServiceUnit(options) {
296
345
  `User=${options.runUser}`,
297
346
  `WorkingDirectory=${runtimeHome2}`,
298
347
  `Environment=CODEHARBOR_HOME=${runtimeHome2}`,
299
- `ExecStart=${import_node_path3.default.resolve(options.nodeBinPath)} ${import_node_path3.default.resolve(options.cliScriptPath)} start`,
348
+ `ExecStart=${import_node_path4.default.resolve(options.nodeBinPath)} ${import_node_path4.default.resolve(options.cliScriptPath)} start`,
300
349
  "Restart=always",
301
350
  "RestartSec=3",
302
351
  "NoNewPrivileges=true",
@@ -312,7 +361,7 @@ function buildMainServiceUnit(options) {
312
361
  }
313
362
  function buildAdminServiceUnit(options) {
314
363
  validateUnitOptions(options);
315
- const runtimeHome2 = import_node_path3.default.resolve(options.runtimeHome);
364
+ const runtimeHome2 = import_node_path4.default.resolve(options.runtimeHome);
316
365
  return [
317
366
  "[Unit]",
318
367
  "Description=CodeHarbor admin service",
@@ -324,7 +373,7 @@ function buildAdminServiceUnit(options) {
324
373
  `User=${options.runUser}`,
325
374
  `WorkingDirectory=${runtimeHome2}`,
326
375
  `Environment=CODEHARBOR_HOME=${runtimeHome2}`,
327
- `ExecStart=${import_node_path3.default.resolve(options.nodeBinPath)} ${import_node_path3.default.resolve(options.cliScriptPath)} admin serve`,
376
+ `ExecStart=${import_node_path4.default.resolve(options.nodeBinPath)} ${import_node_path4.default.resolve(options.cliScriptPath)} admin serve`,
328
377
  "Restart=always",
329
378
  "RestartSec=3",
330
379
  "NoNewPrivileges=true",
@@ -343,7 +392,7 @@ function buildRestartSudoersPolicy(options) {
343
392
  const systemctlPath = options.systemctlPath.trim();
344
393
  validateSimpleValue(runUser, "runUser");
345
394
  validateSimpleValue(systemctlPath, "systemctlPath");
346
- if (!import_node_path3.default.isAbsolute(systemctlPath)) {
395
+ if (!import_node_path4.default.isAbsolute(systemctlPath)) {
347
396
  throw new Error("systemctlPath must be an absolute path.");
348
397
  }
349
398
  return [
@@ -358,7 +407,7 @@ function installSystemdServices(options) {
358
407
  assertRootPrivileges();
359
408
  const output = options.output ?? process.stdout;
360
409
  const runUser = options.runUser.trim();
361
- const runtimeHome2 = import_node_path3.default.resolve(options.runtimeHome);
410
+ const runtimeHome2 = import_node_path4.default.resolve(options.runtimeHome);
362
411
  validateSimpleValue(runUser, "runUser");
363
412
  validateSimpleValue(runtimeHome2, "runtimeHome");
364
413
  validateSimpleValue(options.nodeBinPath, "nodeBinPath");
@@ -367,9 +416,9 @@ function installSystemdServices(options) {
367
416
  const runGroup = resolveUserGroup(runUser);
368
417
  import_node_fs3.default.mkdirSync(runtimeHome2, { recursive: true });
369
418
  runCommand("chown", ["-R", `${runUser}:${runGroup}`, runtimeHome2]);
370
- const mainPath = import_node_path3.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
371
- const adminPath = import_node_path3.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
372
- const restartSudoersPath = import_node_path3.default.join(SUDOERS_DIR, RESTART_SUDOERS_FILE);
419
+ const mainPath = import_node_path4.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
420
+ const adminPath = import_node_path4.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
421
+ const restartSudoersPath = import_node_path4.default.join(SUDOERS_DIR, RESTART_SUDOERS_FILE);
373
422
  const unitOptions = {
374
423
  runUser,
375
424
  runtimeHome: runtimeHome2,
@@ -417,9 +466,9 @@ function uninstallSystemdServices(options) {
417
466
  assertLinuxWithSystemd();
418
467
  assertRootPrivileges();
419
468
  const output = options.output ?? process.stdout;
420
- const mainPath = import_node_path3.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
421
- const adminPath = import_node_path3.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
422
- const restartSudoersPath = import_node_path3.default.join(SUDOERS_DIR, RESTART_SUDOERS_FILE);
469
+ const mainPath = import_node_path4.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
470
+ const adminPath = import_node_path4.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
471
+ const restartSudoersPath = import_node_path4.default.join(SUDOERS_DIR, RESTART_SUDOERS_FILE);
423
472
  stopAndDisableIfPresent(MAIN_SERVICE_NAME);
424
473
  if (import_node_fs3.default.existsSync(mainPath)) {
425
474
  import_node_fs3.default.unlinkSync(mainPath);
@@ -492,7 +541,7 @@ function assertLinuxWithSystemd() {
492
541
  throw new Error("Systemd service install only supports Linux.");
493
542
  }
494
543
  try {
495
- (0, import_node_child_process.execFileSync)("systemctl", ["--version"], { stdio: "ignore" });
544
+ (0, import_node_child_process2.execFileSync)("systemctl", ["--version"], { stdio: "ignore" });
496
545
  } catch {
497
546
  throw new Error("systemctl is required but not found.");
498
547
  }
@@ -539,13 +588,13 @@ function runSystemctlIgnoreFailure(args) {
539
588
  }
540
589
  function resolveSystemctlPath() {
541
590
  const candidates = [];
542
- const pathEntries = (process.env.PATH ?? "").split(import_node_path3.default.delimiter).filter(Boolean);
591
+ const pathEntries = (process.env.PATH ?? "").split(import_node_path4.default.delimiter).filter(Boolean);
543
592
  for (const entry of pathEntries) {
544
- candidates.push(import_node_path3.default.join(entry, "systemctl"));
593
+ candidates.push(import_node_path4.default.join(entry, "systemctl"));
545
594
  }
546
595
  candidates.push("/usr/bin/systemctl", "/bin/systemctl", "/usr/local/bin/systemctl");
547
596
  for (const candidate of candidates) {
548
- if (import_node_path3.default.isAbsolute(candidate) && import_node_fs3.default.existsSync(candidate)) {
597
+ if (import_node_path4.default.isAbsolute(candidate) && import_node_fs3.default.existsSync(candidate)) {
549
598
  return candidate;
550
599
  }
551
600
  }
@@ -553,7 +602,7 @@ function resolveSystemctlPath() {
553
602
  }
554
603
  function runCommand(file, args) {
555
604
  try {
556
- return (0, import_node_child_process.execFileSync)(file, args, {
605
+ return (0, import_node_child_process2.execFileSync)(file, args, {
557
606
  encoding: "utf8",
558
607
  stdio: ["ignore", "pipe", "pipe"]
559
608
  });
@@ -581,7 +630,7 @@ function bufferToTrimmedString(value) {
581
630
  }
582
631
 
583
632
  // src/admin-server.ts
584
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
633
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
585
634
  var HttpError = class extends Error {
586
635
  statusCode;
587
636
  constructor(statusCode, message) {
@@ -597,6 +646,7 @@ var AdminServer = class {
597
646
  host;
598
647
  port;
599
648
  adminToken;
649
+ adminTokens;
600
650
  adminIpAllowlist;
601
651
  adminAllowedOrigins;
602
652
  cwd;
@@ -613,6 +663,7 @@ var AdminServer = class {
613
663
  this.host = options.host;
614
664
  this.port = options.port;
615
665
  this.adminToken = options.adminToken;
666
+ this.adminTokens = buildAdminTokenMap(options.adminTokens ?? []);
616
667
  this.adminIpAllowlist = normalizeAllowlist(options.adminIpAllowlist ?? []);
617
668
  this.adminAllowedOrigins = normalizeOriginAllowlist(options.adminAllowedOrigins ?? []);
618
669
  this.cwd = options.cwd ?? process.cwd();
@@ -704,10 +755,19 @@ var AdminServer = class {
704
755
  this.sendHtml(res, renderAdminConsoleHtml());
705
756
  return;
706
757
  }
707
- if (url.pathname.startsWith("/api/admin/") && !this.isAuthorized(req)) {
758
+ const requiredRole = requiredAdminRoleForRequest(req.method, url.pathname);
759
+ const authIdentity = requiredRole ? this.resolveAdminIdentity(req) : null;
760
+ if (requiredRole && !authIdentity) {
708
761
  this.sendJson(res, 401, {
709
762
  ok: false,
710
- error: "Unauthorized. Provide Authorization: Bearer <ADMIN_TOKEN>."
763
+ error: "Unauthorized. Provide Authorization: Bearer <ADMIN_TOKEN> (or token from ADMIN_TOKENS_JSON)."
764
+ });
765
+ return;
766
+ }
767
+ if (requiredRole && authIdentity && !hasRequiredAdminRole(authIdentity.role, requiredRole)) {
768
+ this.sendJson(res, 403, {
769
+ ok: false,
770
+ error: "Forbidden. This endpoint requires admin write permission."
711
771
  });
712
772
  return;
713
773
  }
@@ -721,7 +781,7 @@ var AdminServer = class {
721
781
  }
722
782
  if (req.method === "PUT" && url.pathname === "/api/admin/config/global") {
723
783
  const body = await readJsonBody(req);
724
- const actor = readActor(req);
784
+ const actor = resolveAuditActor(req, authIdentity);
725
785
  const result = this.updateGlobalConfig(body, actor);
726
786
  this.sendJson(res, 200, {
727
787
  ok: true,
@@ -749,13 +809,13 @@ var AdminServer = class {
749
809
  }
750
810
  if (req.method === "PUT") {
751
811
  const body = await readJsonBody(req);
752
- const actor = readActor(req);
812
+ const actor = resolveAuditActor(req, authIdentity);
753
813
  const room = this.updateRoomConfig(roomId, body, actor);
754
814
  this.sendJson(res, 200, { ok: true, data: room });
755
815
  return;
756
816
  }
757
817
  if (req.method === "DELETE") {
758
- const actor = readActor(req);
818
+ const actor = resolveAuditActor(req, authIdentity);
759
819
  this.configService.deleteRoomSettings(roomId, actor);
760
820
  this.sendJson(res, 200, { ok: true, roomId });
761
821
  return;
@@ -785,7 +845,7 @@ var AdminServer = class {
785
845
  if (req.method === "POST" && url.pathname === "/api/admin/service/restart") {
786
846
  const body = asObject(await readJsonBody(req), "service restart payload");
787
847
  const restartAdmin = normalizeBoolean(body.withAdmin, false);
788
- const actor = readActor(req);
848
+ const actor = resolveAuditActor(req, authIdentity);
789
849
  try {
790
850
  const result = await this.restartServices(restartAdmin);
791
851
  this.stateStore.appendConfigRevision(
@@ -839,7 +899,7 @@ var AdminServer = class {
839
899
  updatedKeys.push("matrixCommandPrefix");
840
900
  }
841
901
  if ("codexWorkdir" in body) {
842
- const workdir = import_node_path4.default.resolve(String(body.codexWorkdir ?? "").trim());
902
+ const workdir = import_node_path5.default.resolve(String(body.codexWorkdir ?? "").trim());
843
903
  ensureDirectory(workdir, "codexWorkdir");
844
904
  this.config.codexWorkdir = workdir;
845
905
  envUpdates.CODEX_WORKDIR = workdir;
@@ -1043,14 +1103,34 @@ var AdminServer = class {
1043
1103
  summary: normalizeOptionalString(body.summary)
1044
1104
  });
1045
1105
  }
1046
- isAuthorized(req) {
1047
- if (!this.adminToken) {
1048
- return true;
1106
+ resolveAdminIdentity(req) {
1107
+ if (!this.adminToken && this.adminTokens.size === 0) {
1108
+ return {
1109
+ role: "admin",
1110
+ actor: null,
1111
+ source: "open"
1112
+ };
1113
+ }
1114
+ const token = readAdminToken(req);
1115
+ if (!token) {
1116
+ return null;
1049
1117
  }
1050
- const authorization = req.headers.authorization ?? "";
1051
- const bearer = authorization.startsWith("Bearer ") ? authorization.slice("Bearer ".length).trim() : "";
1052
- const fromHeader = normalizeHeaderValue(req.headers["x-admin-token"]);
1053
- return bearer === this.adminToken || fromHeader === this.adminToken;
1118
+ if (this.adminToken && token === this.adminToken) {
1119
+ return {
1120
+ role: "admin",
1121
+ actor: null,
1122
+ source: "legacy"
1123
+ };
1124
+ }
1125
+ const mappedIdentity = this.adminTokens.get(token);
1126
+ if (!mappedIdentity) {
1127
+ return null;
1128
+ }
1129
+ return {
1130
+ role: mappedIdentity.role,
1131
+ actor: mappedIdentity.actor,
1132
+ source: "scoped"
1133
+ };
1054
1134
  }
1055
1135
  isClientAllowed(req) {
1056
1136
  if (this.adminIpAllowlist.length === 0) {
@@ -1063,8 +1143,8 @@ var AdminServer = class {
1063
1143
  return this.adminIpAllowlist.includes(normalizedRemote);
1064
1144
  }
1065
1145
  persistEnvUpdates(updates) {
1066
- const envPath = import_node_path4.default.resolve(this.cwd, ".env");
1067
- const examplePath = import_node_path4.default.resolve(this.cwd, ".env.example");
1146
+ const envPath = import_node_path5.default.resolve(this.cwd, ".env");
1147
+ const examplePath = import_node_path5.default.resolve(this.cwd, ".env.example");
1068
1148
  const template = import_node_fs4.default.existsSync(envPath) ? import_node_fs4.default.readFileSync(envPath, "utf8") : import_node_fs4.default.existsSync(examplePath) ? import_node_fs4.default.readFileSync(examplePath, "utf8") : "";
1069
1149
  const next = applyEnvOverrides(template, updates);
1070
1150
  import_node_fs4.default.writeFileSync(envPath, next, "utf8");
@@ -1342,10 +1422,54 @@ function normalizeHeaderValue(value) {
1342
1422
  }
1343
1423
  return value.trim();
1344
1424
  }
1345
- function readActor(req) {
1425
+ function readAdminToken(req) {
1426
+ const authorization = normalizeHeaderValue(req.headers.authorization);
1427
+ if (authorization) {
1428
+ const match = /^bearer\s+(.+)$/i.exec(authorization);
1429
+ const token = match?.[1]?.trim() ?? "";
1430
+ if (token) {
1431
+ return token;
1432
+ }
1433
+ }
1434
+ const fromHeader = normalizeHeaderValue(req.headers["x-admin-token"]);
1435
+ return fromHeader || null;
1436
+ }
1437
+ function resolveAuditActor(req, identity) {
1438
+ if (identity?.source === "scoped") {
1439
+ if (identity.actor) {
1440
+ return identity.actor;
1441
+ }
1442
+ return identity.role === "admin" ? "admin-token" : "viewer-token";
1443
+ }
1346
1444
  const actor = normalizeHeaderValue(req.headers["x-admin-actor"]);
1347
1445
  return actor || null;
1348
1446
  }
1447
+ function requiredAdminRoleForRequest(method, pathname) {
1448
+ if (!pathname.startsWith("/api/admin/")) {
1449
+ return null;
1450
+ }
1451
+ const normalizedMethod = (method ?? "GET").toUpperCase();
1452
+ if (normalizedMethod === "GET" || normalizedMethod === "HEAD") {
1453
+ return "viewer";
1454
+ }
1455
+ return "admin";
1456
+ }
1457
+ function hasRequiredAdminRole(role, requiredRole) {
1458
+ if (requiredRole === "viewer") {
1459
+ return role === "viewer" || role === "admin";
1460
+ }
1461
+ return role === "admin";
1462
+ }
1463
+ function buildAdminTokenMap(tokens) {
1464
+ const mapped = /* @__PURE__ */ new Map();
1465
+ for (const token of tokens) {
1466
+ mapped.set(token.token, {
1467
+ role: token.role,
1468
+ actor: token.actor
1469
+ });
1470
+ }
1471
+ return mapped;
1472
+ }
1349
1473
  function formatError(error) {
1350
1474
  if (error instanceof Error) {
1351
1475
  return error.message;
@@ -2219,7 +2343,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
2219
2343
  `;
2220
2344
  async function defaultCheckCodex(bin) {
2221
2345
  try {
2222
- const { stdout } = await execFileAsync(bin, ["--version"]);
2346
+ const { stdout } = await execFileAsync2(bin, ["--version"]);
2223
2347
  return {
2224
2348
  ok: true,
2225
2349
  version: stdout.trim() || null,
@@ -2262,8 +2386,8 @@ async function defaultCheckMatrix(homeserver, timeoutMs) {
2262
2386
 
2263
2387
  // src/channels/matrix-channel.ts
2264
2388
  var import_promises2 = __toESM(require("fs/promises"));
2265
- var import_node_os3 = __toESM(require("os"));
2266
- var import_node_path5 = __toESM(require("path"));
2389
+ var import_node_os4 = __toESM(require("os"));
2390
+ var import_node_path6 = __toESM(require("path"));
2267
2391
  var import_matrix_js_sdk = require("matrix-js-sdk");
2268
2392
 
2269
2393
  // src/utils/message.ts
@@ -2686,10 +2810,10 @@ var MatrixChannel = class {
2686
2810
  }
2687
2811
  const bytes = Buffer.from(await response.arrayBuffer());
2688
2812
  const extension = resolveFileExtension(fileName, mimeType);
2689
- const directory = import_node_path5.default.join(import_node_os3.default.tmpdir(), "codeharbor-media");
2813
+ const directory = import_node_path6.default.join(import_node_os4.default.tmpdir(), "codeharbor-media");
2690
2814
  await import_promises2.default.mkdir(directory, { recursive: true });
2691
2815
  const safeEventId = sanitizeFilename(eventId);
2692
- const targetPath = import_node_path5.default.join(directory, `${safeEventId}-${index}${extension}`);
2816
+ const targetPath = import_node_path6.default.join(directory, `${safeEventId}-${index}${extension}`);
2693
2817
  await import_promises2.default.writeFile(targetPath, bytes);
2694
2818
  return targetPath;
2695
2819
  }
@@ -2774,7 +2898,7 @@ function sanitizeFilename(value) {
2774
2898
  return value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
2775
2899
  }
2776
2900
  function resolveFileExtension(fileName, mimeType) {
2777
- const ext = import_node_path5.default.extname(fileName).trim();
2901
+ const ext = import_node_path6.default.extname(fileName).trim();
2778
2902
  if (ext) {
2779
2903
  return ext;
2780
2904
  }
@@ -2792,13 +2916,13 @@ function resolveFileExtension(fileName, mimeType) {
2792
2916
 
2793
2917
  // src/config-service.ts
2794
2918
  var import_node_fs5 = __toESM(require("fs"));
2795
- var import_node_path6 = __toESM(require("path"));
2919
+ var import_node_path7 = __toESM(require("path"));
2796
2920
  var ConfigService = class {
2797
2921
  stateStore;
2798
2922
  defaultWorkdir;
2799
2923
  constructor(stateStore, defaultWorkdir) {
2800
2924
  this.stateStore = stateStore;
2801
- this.defaultWorkdir = import_node_path6.default.resolve(defaultWorkdir);
2925
+ this.defaultWorkdir = import_node_path7.default.resolve(defaultWorkdir);
2802
2926
  }
2803
2927
  resolveRoomConfig(roomId, fallbackPolicy) {
2804
2928
  const room = this.stateStore.getRoomSettings(roomId);
@@ -2869,7 +2993,7 @@ function normalizeRoomSettingsInput(input) {
2869
2993
  if (!roomId) {
2870
2994
  throw new Error("roomId is required.");
2871
2995
  }
2872
- const workdir = import_node_path6.default.resolve(input.workdir);
2996
+ const workdir = import_node_path7.default.resolve(input.workdir);
2873
2997
  if (!import_node_fs5.default.existsSync(workdir) || !import_node_fs5.default.statSync(workdir).isDirectory()) {
2874
2998
  throw new Error(`workdir does not exist or is not a directory: ${workdir}`);
2875
2999
  }
@@ -2885,7 +3009,7 @@ function normalizeRoomSettingsInput(input) {
2885
3009
  }
2886
3010
 
2887
3011
  // src/executor/codex-executor.ts
2888
- var import_node_child_process3 = require("child_process");
3012
+ var import_node_child_process4 = require("child_process");
2889
3013
  var import_node_readline = __toESM(require("readline"));
2890
3014
  var CodexExecutionCancelledError = class extends Error {
2891
3015
  constructor(message = "codex execution cancelled") {
@@ -2903,7 +3027,7 @@ var CodexExecutor = class {
2903
3027
  }
2904
3028
  startExecution(prompt, sessionId, onProgress, startOptions) {
2905
3029
  const args = buildCodexArgs(prompt, sessionId, this.options, startOptions);
2906
- const child = (0, import_node_child_process3.spawn)(this.options.bin, args, {
3030
+ const child = (0, import_node_child_process4.spawn)(this.options.bin, args, {
2907
3031
  cwd: startOptions?.workdir ?? this.options.workdir,
2908
3032
  env: {
2909
3033
  ...process.env,
@@ -3136,17 +3260,17 @@ function stringify(value) {
3136
3260
 
3137
3261
  // src/orchestrator.ts
3138
3262
  var import_async_mutex = require("async-mutex");
3139
- var import_promises3 = __toESM(require("fs/promises"));
3263
+ var import_promises4 = __toESM(require("fs/promises"));
3140
3264
 
3141
3265
  // src/compat/cli-compat-recorder.ts
3142
3266
  var import_node_fs6 = __toESM(require("fs"));
3143
- var import_node_path7 = __toESM(require("path"));
3267
+ var import_node_path8 = __toESM(require("path"));
3144
3268
  var CliCompatRecorder = class {
3145
3269
  filePath;
3146
3270
  chain = Promise.resolve();
3147
3271
  constructor(filePath) {
3148
- this.filePath = import_node_path7.default.resolve(filePath);
3149
- import_node_fs6.default.mkdirSync(import_node_path7.default.dirname(this.filePath), { recursive: true });
3272
+ this.filePath = import_node_path8.default.resolve(filePath);
3273
+ import_node_fs6.default.mkdirSync(import_node_path8.default.dirname(this.filePath), { recursive: true });
3150
3274
  }
3151
3275
  append(entry) {
3152
3276
  const payload = `${JSON.stringify(entry)}
@@ -3623,6 +3747,296 @@ function createIdleWorkflowSnapshot() {
3623
3747
  };
3624
3748
  }
3625
3749
 
3750
+ // src/workflow/autodev.ts
3751
+ var import_promises3 = __toESM(require("fs/promises"));
3752
+ var import_node_path9 = __toESM(require("path"));
3753
+ function parseAutoDevCommand(text) {
3754
+ const normalized = text.trim();
3755
+ if (!/^\/autodev(?:\s|$)/i.test(normalized)) {
3756
+ return null;
3757
+ }
3758
+ const parts = normalized.split(/\s+/);
3759
+ if (parts.length === 1 || parts[1]?.toLowerCase() === "status") {
3760
+ return { kind: "status" };
3761
+ }
3762
+ if (parts[1]?.toLowerCase() !== "run") {
3763
+ return null;
3764
+ }
3765
+ const taskId = normalized.replace(/^\/autodev\s+run\s*/i, "").trim();
3766
+ return {
3767
+ kind: "run",
3768
+ taskId: taskId || null
3769
+ };
3770
+ }
3771
+ async function loadAutoDevContext(workdir) {
3772
+ const requirementsPath = import_node_path9.default.join(workdir, "REQUIREMENTS.md");
3773
+ const taskListPath = import_node_path9.default.join(workdir, "TASK_LIST.md");
3774
+ const requirementsContent = await readOptionalFile(requirementsPath);
3775
+ const taskListContent = await readOptionalFile(taskListPath);
3776
+ return {
3777
+ workdir,
3778
+ requirementsPath,
3779
+ taskListPath,
3780
+ requirementsContent,
3781
+ taskListContent,
3782
+ tasks: taskListContent ? parseTasks(taskListContent) : []
3783
+ };
3784
+ }
3785
+ function summarizeAutoDevTasks(tasks) {
3786
+ const summary = {
3787
+ total: tasks.length,
3788
+ pending: 0,
3789
+ inProgress: 0,
3790
+ completed: 0,
3791
+ cancelled: 0,
3792
+ blocked: 0
3793
+ };
3794
+ for (const task of tasks) {
3795
+ if (task.status === "pending") {
3796
+ summary.pending += 1;
3797
+ continue;
3798
+ }
3799
+ if (task.status === "in_progress") {
3800
+ summary.inProgress += 1;
3801
+ continue;
3802
+ }
3803
+ if (task.status === "completed") {
3804
+ summary.completed += 1;
3805
+ continue;
3806
+ }
3807
+ if (task.status === "cancelled") {
3808
+ summary.cancelled += 1;
3809
+ continue;
3810
+ }
3811
+ summary.blocked += 1;
3812
+ }
3813
+ return summary;
3814
+ }
3815
+ function selectAutoDevTask(tasks, taskId) {
3816
+ if (taskId) {
3817
+ const normalizedTarget = taskId.trim().toLowerCase();
3818
+ return tasks.find((task) => task.id.toLowerCase() === normalizedTarget) ?? null;
3819
+ }
3820
+ const inProgressTask = tasks.find((task) => task.status === "in_progress");
3821
+ if (inProgressTask) {
3822
+ return inProgressTask;
3823
+ }
3824
+ return tasks.find((task) => task.status === "pending") ?? null;
3825
+ }
3826
+ function buildAutoDevObjective(task) {
3827
+ return [
3828
+ "\u4F60\u6B63\u5728\u6267\u884C CodeHarbor AutoDev \u4EFB\u52A1\uFF0C\u8BF7\u5728\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u5B8C\u6210\u6307\u5B9A\u5F00\u53D1\u76EE\u6807\u3002",
3829
+ "",
3830
+ `\u4EFB\u52A1ID: ${task.id}`,
3831
+ `\u4EFB\u52A1\u63CF\u8FF0: ${task.description}`,
3832
+ "",
3833
+ "\u4E0A\u4E0B\u6587\u6587\u4EF6\uFF1A",
3834
+ "- REQUIREMENTS.md\uFF08\u9700\u6C42\u57FA\u7EBF\uFF09",
3835
+ "- TASK_LIST.md\uFF08\u4EFB\u52A1\u72B6\u6001\uFF09",
3836
+ "",
3837
+ "\u6267\u884C\u8981\u6C42\uFF1A",
3838
+ "1. \u5148\u8BFB\u53D6 REQUIREMENTS.md \u548C TASK_LIST.md\uFF0C\u786E\u8BA4\u8FB9\u754C\u4E0E\u7EA6\u675F\u3002",
3839
+ "2. \u5728\u5F53\u524D\u4ED3\u5E93\u76F4\u63A5\u5B8C\u6210\u4EE3\u7801\u4E0E\u6D4B\u8BD5\u6539\u52A8\u3002",
3840
+ "3. \u8FD0\u884C\u53D7\u5F71\u54CD\u9A8C\u8BC1\u547D\u4EE4\u5E76\u6C47\u603B\u7ED3\u679C\u3002",
3841
+ "4. \u8F93\u51FA\u6539\u52A8\u6587\u4EF6\u548C\u98CE\u9669\u8BF4\u660E\u3002"
3842
+ ].join("\n");
3843
+ }
3844
+ function formatTaskForDisplay(task) {
3845
+ return `${task.id} ${task.description} (${statusToSymbol(task.status)})`;
3846
+ }
3847
+ function statusToSymbol(status) {
3848
+ if (status === "pending") {
3849
+ return "\u2B1C";
3850
+ }
3851
+ if (status === "in_progress") {
3852
+ return "\u{1F504}";
3853
+ }
3854
+ if (status === "completed") {
3855
+ return "\u2705";
3856
+ }
3857
+ if (status === "cancelled") {
3858
+ return "\u274C";
3859
+ }
3860
+ return "\u{1F6AB}";
3861
+ }
3862
+ async function updateAutoDevTaskStatus(taskListPath, task, nextStatus) {
3863
+ const content = await import_promises3.default.readFile(taskListPath, "utf8");
3864
+ const lines = splitLines(content);
3865
+ if (task.lineIndex < 0 || task.lineIndex >= lines.length) {
3866
+ throw new Error(`task ${task.id} line index out of range`);
3867
+ }
3868
+ const updatedLine = replaceLineStatus(lines[task.lineIndex] ?? "", task.id, nextStatus);
3869
+ if (!updatedLine) {
3870
+ throw new Error(`failed to update task status for ${task.id}`);
3871
+ }
3872
+ lines[task.lineIndex] = updatedLine;
3873
+ await import_promises3.default.writeFile(taskListPath, lines.join("\n"), "utf8");
3874
+ return {
3875
+ ...task,
3876
+ status: nextStatus
3877
+ };
3878
+ }
3879
+ async function readOptionalFile(filePath) {
3880
+ try {
3881
+ return await import_promises3.default.readFile(filePath, "utf8");
3882
+ } catch (error) {
3883
+ if (error.code === "ENOENT") {
3884
+ return null;
3885
+ }
3886
+ throw error;
3887
+ }
3888
+ }
3889
+ function parseTasks(content) {
3890
+ const lines = splitLines(content);
3891
+ const tasks = [];
3892
+ for (let index = 0; index < lines.length; index += 1) {
3893
+ const line = lines[index] ?? "";
3894
+ const tableTask = parseTableTaskLine(line, index);
3895
+ if (tableTask) {
3896
+ tasks.push(tableTask);
3897
+ continue;
3898
+ }
3899
+ const listTask = parseListTaskLine(line, index);
3900
+ if (listTask) {
3901
+ tasks.push(listTask);
3902
+ }
3903
+ }
3904
+ return tasks;
3905
+ }
3906
+ function parseTableTaskLine(line, lineIndex) {
3907
+ const trimmed = line.trim();
3908
+ if (!trimmed.startsWith("|")) {
3909
+ return null;
3910
+ }
3911
+ const cells = trimmed.split("|").slice(1, -1).map((cell) => cell.trim());
3912
+ if (cells.length < 3) {
3913
+ return null;
3914
+ }
3915
+ const taskId = cells[0] ?? "";
3916
+ if (!isLikelyTaskId(taskId)) {
3917
+ return null;
3918
+ }
3919
+ const statusCell = cells[cells.length - 1] ?? "";
3920
+ const status = parseStatusToken(statusCell);
3921
+ if (!status) {
3922
+ return null;
3923
+ }
3924
+ return {
3925
+ id: taskId,
3926
+ description: cells[1] ?? taskId,
3927
+ status,
3928
+ lineIndex
3929
+ };
3930
+ }
3931
+ function parseListTaskLine(line, lineIndex) {
3932
+ const checkboxMatch = line.match(/^\s*[-*]\s+\[( |x|X)\]\s+(.+)$/);
3933
+ if (checkboxMatch) {
3934
+ const rawText2 = checkboxMatch[2]?.trim() ?? "";
3935
+ const taskId2 = extractTaskId(rawText2);
3936
+ if (!taskId2) {
3937
+ return null;
3938
+ }
3939
+ return {
3940
+ id: taskId2,
3941
+ description: stripTaskIdPrefix(rawText2, taskId2),
3942
+ status: checkboxMatch[1]?.toLowerCase() === "x" ? "completed" : "pending",
3943
+ lineIndex
3944
+ };
3945
+ }
3946
+ const symbolMatch = line.match(/^\s*[-*]\s*(⬜|🔄|✅|❌|🚫)\s+(.+)$/);
3947
+ if (!symbolMatch) {
3948
+ return null;
3949
+ }
3950
+ const rawText = symbolMatch[2]?.trim() ?? "";
3951
+ const taskId = extractTaskId(rawText);
3952
+ if (!taskId) {
3953
+ return null;
3954
+ }
3955
+ const status = parseStatusToken(symbolMatch[1] ?? "");
3956
+ if (!status) {
3957
+ return null;
3958
+ }
3959
+ return {
3960
+ id: taskId,
3961
+ description: stripTaskIdPrefix(rawText, taskId),
3962
+ status,
3963
+ lineIndex
3964
+ };
3965
+ }
3966
+ function stripTaskIdPrefix(text, taskId) {
3967
+ const normalized = text.trim();
3968
+ const escapedId = escapeRegex(taskId);
3969
+ return normalized.replace(new RegExp(`^${escapedId}[\\s:\uFF1A\\-]+`, "i"), "").trim() || normalized;
3970
+ }
3971
+ function extractTaskId(text) {
3972
+ const normalized = text.trim();
3973
+ const bracketMatch = normalized.match(/\(([A-Za-z][A-Za-z0-9._-]*)\)/);
3974
+ if (bracketMatch?.[1] && isLikelyTaskId(bracketMatch[1])) {
3975
+ return bracketMatch[1];
3976
+ }
3977
+ const token = normalized.split(/\s+/)[0]?.replace(/[,::|]+$/, "") ?? "";
3978
+ if (!isLikelyTaskId(token)) {
3979
+ return null;
3980
+ }
3981
+ return token;
3982
+ }
3983
+ function isLikelyTaskId(taskId) {
3984
+ if (!/^[A-Za-z][A-Za-z0-9._-]*$/.test(taskId)) {
3985
+ return false;
3986
+ }
3987
+ return /\d/.test(taskId);
3988
+ }
3989
+ function parseStatusToken(text) {
3990
+ const normalized = text.trim().toLowerCase();
3991
+ if (!normalized) {
3992
+ return null;
3993
+ }
3994
+ if (normalized.includes("\u2705") || normalized.includes("[x]") || normalized.includes("done")) {
3995
+ return "completed";
3996
+ }
3997
+ if (normalized.includes("\u2B1C") || normalized.includes("\u2610") || normalized.includes("[ ]") || normalized === "todo") {
3998
+ return "pending";
3999
+ }
4000
+ if (normalized.includes("\u{1F504}") || normalized.includes("\u8FDB\u884C\u4E2D") || normalized.includes("in progress")) {
4001
+ return "in_progress";
4002
+ }
4003
+ if (normalized.includes("\u274C") || normalized.includes("\u53D6\u6D88") || normalized.includes("cancel")) {
4004
+ return "cancelled";
4005
+ }
4006
+ if (normalized.includes("\u{1F6AB}") || normalized.includes("\u963B\u585E") || normalized.includes("block")) {
4007
+ return "blocked";
4008
+ }
4009
+ return null;
4010
+ }
4011
+ function replaceLineStatus(line, taskId, status) {
4012
+ const trimmed = line.trim();
4013
+ const symbol = statusToSymbol(status);
4014
+ if (trimmed.startsWith("|")) {
4015
+ const cells = trimmed.split("|").slice(1, -1).map((cell) => cell.trim());
4016
+ if (cells.length >= 3 && cells[0]?.toLowerCase() === taskId.toLowerCase()) {
4017
+ const rawParts = line.split("|");
4018
+ if (rawParts.length >= 3) {
4019
+ rawParts[rawParts.length - 2] = ` ${symbol} `;
4020
+ return rawParts.join("|");
4021
+ }
4022
+ }
4023
+ }
4024
+ if (/\[( |x|X)\]/.test(line)) {
4025
+ const checkbox = status === "completed" ? "[x]" : "[ ]";
4026
+ return line.replace(/\[( |x|X)\]/, checkbox);
4027
+ }
4028
+ if (/(⬜|🔄|✅|❌|🚫)/.test(line)) {
4029
+ return line.replace(/(⬜|🔄|✅|❌|🚫)/, symbol);
4030
+ }
4031
+ return null;
4032
+ }
4033
+ function splitLines(content) {
4034
+ return content.replace(/\r\n/g, "\n").split("\n");
4035
+ }
4036
+ function escapeRegex(value) {
4037
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4038
+ }
4039
+
3626
4040
  // src/orchestrator.ts
3627
4041
  var RequestMetrics = class {
3628
4042
  total = 0;
@@ -3710,6 +4124,7 @@ var Orchestrator = class {
3710
4124
  cliCompatRecorder;
3711
4125
  workflowRunner;
3712
4126
  workflowSnapshots = /* @__PURE__ */ new Map();
4127
+ autoDevSnapshots = /* @__PURE__ */ new Map();
3713
4128
  metrics = new RequestMetrics();
3714
4129
  lastLockPruneAt = 0;
3715
4130
  constructor(channel, executor, stateStore, logger, options) {
@@ -3804,11 +4219,17 @@ var Orchestrator = class {
3804
4219
  return;
3805
4220
  }
3806
4221
  const workflowCommand = this.workflowRunner.isEnabled() ? parseWorkflowCommand(route.prompt) : null;
4222
+ const autoDevCommand = this.workflowRunner.isEnabled() ? parseAutoDevCommand(route.prompt) : null;
3807
4223
  if (workflowCommand?.kind === "status") {
3808
4224
  await this.handleWorkflowStatusCommand(sessionKey, message);
3809
4225
  this.stateStore.markEventProcessed(sessionKey, message.eventId);
3810
4226
  return;
3811
4227
  }
4228
+ if (autoDevCommand?.kind === "status") {
4229
+ await this.handleAutoDevStatusCommand(sessionKey, message, roomConfig.workdir);
4230
+ this.stateStore.markEventProcessed(sessionKey, message.eventId);
4231
+ return;
4232
+ }
3812
4233
  const rateDecision = this.rateLimiter.tryAcquire({
3813
4234
  userId: message.senderId,
3814
4235
  roomId: message.conversationId
@@ -3857,6 +4278,37 @@ var Orchestrator = class {
3857
4278
  }
3858
4279
  return;
3859
4280
  }
4281
+ if (autoDevCommand?.kind === "run") {
4282
+ const executionStartedAt = Date.now();
4283
+ let sendDurationMs2 = 0;
4284
+ this.stateStore.activateSession(sessionKey, this.sessionActiveWindowMs);
4285
+ try {
4286
+ const sendStartedAt = Date.now();
4287
+ await this.handleAutoDevRunCommand(
4288
+ autoDevCommand.taskId,
4289
+ sessionKey,
4290
+ message,
4291
+ requestId,
4292
+ roomConfig.workdir
4293
+ );
4294
+ sendDurationMs2 += Date.now() - sendStartedAt;
4295
+ this.stateStore.markEventProcessed(sessionKey, message.eventId);
4296
+ this.metrics.record("success", queueWaitMs, Date.now() - executionStartedAt, sendDurationMs2);
4297
+ } catch (error) {
4298
+ sendDurationMs2 += await this.sendAutoDevFailure(message.conversationId, error);
4299
+ this.stateStore.commitExecutionHandled(sessionKey, message.eventId);
4300
+ const status = classifyExecutionOutcome(error);
4301
+ this.metrics.record(status, queueWaitMs, Date.now() - executionStartedAt, sendDurationMs2);
4302
+ this.logger.error("AutoDev request failed", {
4303
+ requestId,
4304
+ sessionKey,
4305
+ error: formatError2(error)
4306
+ });
4307
+ } finally {
4308
+ rateDecision.release?.();
4309
+ }
4310
+ return;
4311
+ }
3860
4312
  this.stateStore.activateSession(sessionKey, this.sessionActiveWindowMs);
3861
4313
  const previousCodexSessionId = this.stateStore.getCodexSessionId(sessionKey);
3862
4314
  const executionPrompt = this.buildExecutionPrompt(route.prompt, message);
@@ -4078,6 +4530,7 @@ var Orchestrator = class {
4078
4530
  const limiter = this.rateLimiter.snapshot();
4079
4531
  const runtime = this.sessionRuntime.getRuntimeStats();
4080
4532
  const workflow = this.workflowSnapshots.get(sessionKey) ?? createIdleWorkflowSnapshot();
4533
+ const autoDev = this.autoDevSnapshots.get(sessionKey) ?? createIdleAutoDevSnapshot();
4081
4534
  await this.channel.sendNotice(
4082
4535
  message.conversationId,
4083
4536
  `[CodeHarbor] \u5F53\u524D\u72B6\u6001
@@ -4091,7 +4544,8 @@ var Orchestrator = class {
4091
4544
  - \u5E73\u5747\u8017\u65F6: queue=${metrics.avgQueueMs}ms, exec=${metrics.avgExecMs}ms, send=${metrics.avgSendMs}ms
4092
4545
  - \u9650\u6D41\u5E76\u53D1: global=${limiter.activeGlobal}, users=${limiter.activeUsers}, rooms=${limiter.activeRooms}
4093
4546
  - CLI runtime: workers=${runtime.workerCount}, running=${runtime.runningCount}, compat_mode=${this.cliCompat.enabled ? "on" : "off"}
4094
- - Multi-Agent workflow: enabled=${this.workflowRunner.isEnabled() ? "on" : "off"}, state=${workflow.state}`
4547
+ - Multi-Agent workflow: enabled=${this.workflowRunner.isEnabled() ? "on" : "off"}, state=${workflow.state}
4548
+ - AutoDev: enabled=${this.workflowRunner.isEnabled() ? "on" : "off"}, state=${autoDev.state}, task=${autoDev.taskId ?? "N/A"}`
4095
4549
  );
4096
4550
  }
4097
4551
  async handleWorkflowStatusCommand(sessionKey, message) {
@@ -4108,11 +4562,158 @@ var Orchestrator = class {
4108
4562
  - error: ${snapshot.error ?? "N/A"}`
4109
4563
  );
4110
4564
  }
4565
+ async handleAutoDevStatusCommand(sessionKey, message, workdir) {
4566
+ const snapshot = this.autoDevSnapshots.get(sessionKey) ?? createIdleAutoDevSnapshot();
4567
+ try {
4568
+ const context = await loadAutoDevContext(workdir);
4569
+ const summary = summarizeAutoDevTasks(context.tasks);
4570
+ const nextTask = selectAutoDevTask(context.tasks);
4571
+ await this.channel.sendNotice(
4572
+ message.conversationId,
4573
+ `[CodeHarbor] AutoDev \u72B6\u6001
4574
+ - workdir: ${workdir}
4575
+ - REQUIREMENTS.md: ${context.requirementsContent ? "found" : "missing"}
4576
+ - TASK_LIST.md: ${context.taskListContent ? "found" : "missing"}
4577
+ - tasks: total=${summary.total}, pending=${summary.pending}, in_progress=${summary.inProgress}, completed=${summary.completed}, blocked=${summary.blocked}, cancelled=${summary.cancelled}
4578
+ - nextTask: ${nextTask ? formatTaskForDisplay(nextTask) : "N/A"}
4579
+ - runState: ${snapshot.state}
4580
+ - runTask: ${snapshot.taskId ? `${snapshot.taskId} ${snapshot.taskDescription ?? ""}`.trim() : "N/A"}
4581
+ - runApproved: ${snapshot.approved === null ? "N/A" : snapshot.approved ? "yes" : "no"}
4582
+ - runError: ${snapshot.error ?? "N/A"}`
4583
+ );
4584
+ } catch (error) {
4585
+ await this.channel.sendNotice(message.conversationId, `[CodeHarbor] AutoDev \u72B6\u6001\u8BFB\u53D6\u5931\u8D25: ${formatError2(error)}`);
4586
+ }
4587
+ }
4588
+ async handleAutoDevRunCommand(taskId, sessionKey, message, requestId, workdir) {
4589
+ const requestedTaskId = taskId?.trim() || null;
4590
+ const context = await loadAutoDevContext(workdir);
4591
+ if (!context.requirementsContent) {
4592
+ await this.channel.sendNotice(
4593
+ message.conversationId,
4594
+ `[CodeHarbor] AutoDev \u9700\u8981 ${context.requirementsPath}\uFF0C\u8BF7\u5148\u51C6\u5907\u9700\u6C42\u6587\u6863\u3002`
4595
+ );
4596
+ return;
4597
+ }
4598
+ if (!context.taskListContent) {
4599
+ await this.channel.sendNotice(
4600
+ message.conversationId,
4601
+ `[CodeHarbor] AutoDev \u9700\u8981 ${context.taskListPath}\uFF0C\u8BF7\u5148\u51C6\u5907\u4EFB\u52A1\u6E05\u5355\u3002`
4602
+ );
4603
+ return;
4604
+ }
4605
+ if (context.tasks.length === 0) {
4606
+ await this.channel.sendNotice(
4607
+ message.conversationId,
4608
+ "[CodeHarbor] \u672A\u5728 TASK_LIST.md \u8BC6\u522B\u5230\u4EFB\u52A1\uFF08\u9700\u5305\u542B\u4EFB\u52A1 ID \u4E0E\u72B6\u6001\u5217\uFF09\u3002"
4609
+ );
4610
+ return;
4611
+ }
4612
+ const selectedTask = selectAutoDevTask(context.tasks, requestedTaskId);
4613
+ if (!selectedTask) {
4614
+ if (requestedTaskId) {
4615
+ await this.channel.sendNotice(message.conversationId, `[CodeHarbor] \u672A\u627E\u5230\u4EFB\u52A1 ${requestedTaskId}\u3002`);
4616
+ return;
4617
+ }
4618
+ await this.channel.sendNotice(message.conversationId, "[CodeHarbor] \u5F53\u524D\u6CA1\u6709\u53EF\u6267\u884C\u4EFB\u52A1\uFF08pending/in_progress\uFF09\u3002");
4619
+ return;
4620
+ }
4621
+ if (selectedTask.status === "completed") {
4622
+ await this.channel.sendNotice(message.conversationId, `[CodeHarbor] \u4EFB\u52A1 ${selectedTask.id} \u5DF2\u5B8C\u6210\uFF08\u2705\uFF09\u3002`);
4623
+ return;
4624
+ }
4625
+ if (selectedTask.status === "cancelled") {
4626
+ await this.channel.sendNotice(message.conversationId, `[CodeHarbor] \u4EFB\u52A1 ${selectedTask.id} \u5DF2\u53D6\u6D88\uFF08\u274C\uFF09\u3002`);
4627
+ return;
4628
+ }
4629
+ let activeTask = selectedTask;
4630
+ let promotedToInProgress = false;
4631
+ if (selectedTask.status === "pending") {
4632
+ activeTask = await updateAutoDevTaskStatus(context.taskListPath, selectedTask, "in_progress");
4633
+ promotedToInProgress = true;
4634
+ }
4635
+ const startedAtIso = (/* @__PURE__ */ new Date()).toISOString();
4636
+ this.autoDevSnapshots.set(sessionKey, {
4637
+ state: "running",
4638
+ startedAt: startedAtIso,
4639
+ endedAt: null,
4640
+ taskId: activeTask.id,
4641
+ taskDescription: activeTask.description,
4642
+ approved: null,
4643
+ repairRounds: 0,
4644
+ error: null
4645
+ });
4646
+ await this.channel.sendNotice(
4647
+ message.conversationId,
4648
+ `[CodeHarbor] AutoDev \u542F\u52A8\u4EFB\u52A1 ${activeTask.id}: ${activeTask.description}`
4649
+ );
4650
+ try {
4651
+ const result = await this.handleWorkflowRunCommand(
4652
+ buildAutoDevObjective(activeTask),
4653
+ sessionKey,
4654
+ message,
4655
+ requestId,
4656
+ workdir
4657
+ );
4658
+ if (!result) {
4659
+ return;
4660
+ }
4661
+ let finalTask = activeTask;
4662
+ if (result.approved) {
4663
+ finalTask = await updateAutoDevTaskStatus(context.taskListPath, activeTask, "completed");
4664
+ }
4665
+ const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
4666
+ this.autoDevSnapshots.set(sessionKey, {
4667
+ state: "succeeded",
4668
+ startedAt: startedAtIso,
4669
+ endedAt: endedAtIso,
4670
+ taskId: finalTask.id,
4671
+ taskDescription: finalTask.description,
4672
+ approved: result.approved,
4673
+ repairRounds: result.repairRounds,
4674
+ error: null
4675
+ });
4676
+ const refreshed = await loadAutoDevContext(workdir);
4677
+ const nextTask = selectAutoDevTask(refreshed.tasks);
4678
+ await this.channel.sendNotice(
4679
+ message.conversationId,
4680
+ `[CodeHarbor] AutoDev \u4EFB\u52A1\u7ED3\u679C
4681
+ - task: ${finalTask.id}
4682
+ - reviewer approved: ${result.approved ? "yes" : "no"}
4683
+ - task status: ${statusToSymbol(finalTask.status)}
4684
+ - nextTask: ${nextTask ? formatTaskForDisplay(nextTask) : "N/A"}`
4685
+ );
4686
+ } catch (error) {
4687
+ if (promotedToInProgress) {
4688
+ try {
4689
+ await updateAutoDevTaskStatus(context.taskListPath, activeTask, "pending");
4690
+ } catch (restoreError) {
4691
+ this.logger.warn("Failed to restore AutoDev task status after failure", {
4692
+ taskId: activeTask.id,
4693
+ error: formatError2(restoreError)
4694
+ });
4695
+ }
4696
+ }
4697
+ const status = classifyExecutionOutcome(error);
4698
+ const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
4699
+ this.autoDevSnapshots.set(sessionKey, {
4700
+ state: status === "cancelled" ? "idle" : "failed",
4701
+ startedAt: startedAtIso,
4702
+ endedAt: endedAtIso,
4703
+ taskId: activeTask.id,
4704
+ taskDescription: activeTask.description,
4705
+ approved: null,
4706
+ repairRounds: 0,
4707
+ error: formatError2(error)
4708
+ });
4709
+ throw error;
4710
+ }
4711
+ }
4111
4712
  async handleWorkflowRunCommand(objective, sessionKey, message, requestId, workdir) {
4112
4713
  const normalizedObjective = objective.trim();
4113
4714
  if (!normalizedObjective) {
4114
4715
  await this.channel.sendNotice(message.conversationId, "[CodeHarbor] /agents run \u9700\u8981\u63D0\u4F9B\u4EFB\u52A1\u76EE\u6807\u3002");
4115
- return;
4716
+ return null;
4116
4717
  }
4117
4718
  const requestStartedAt = Date.now();
4118
4719
  let progressNoticeEventId = null;
@@ -4174,6 +4775,7 @@ var Orchestrator = class {
4174
4775
  });
4175
4776
  await this.channel.sendMessage(message.conversationId, buildWorkflowResultReply(result));
4176
4777
  await this.finishProgress(progressCtx, `\u591A\u667A\u80FD\u4F53\u6D41\u7A0B\u5B8C\u6210\uFF08\u8017\u65F6 ${formatDurationMs(Date.now() - requestStartedAt)}\uFF09`);
4778
+ return result;
4177
4779
  } catch (error) {
4178
4780
  const status = classifyExecutionOutcome(error);
4179
4781
  const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
@@ -4206,6 +4808,16 @@ var Orchestrator = class {
4206
4808
  await this.channel.sendMessage(conversationId, `[CodeHarbor] Multi-Agent workflow \u5931\u8D25: ${formatError2(error)}`);
4207
4809
  return Date.now() - startedAt;
4208
4810
  }
4811
+ async sendAutoDevFailure(conversationId, error) {
4812
+ const startedAt = Date.now();
4813
+ const status = classifyExecutionOutcome(error);
4814
+ if (status === "cancelled") {
4815
+ await this.channel.sendNotice(conversationId, "[CodeHarbor] AutoDev \u5DF2\u53D6\u6D88\u3002");
4816
+ return Date.now() - startedAt;
4817
+ }
4818
+ await this.channel.sendMessage(conversationId, `[CodeHarbor] AutoDev \u5931\u8D25: ${formatError2(error)}`);
4819
+ return Date.now() - startedAt;
4820
+ }
4209
4821
  async handleStopCommand(sessionKey, message, requestId) {
4210
4822
  this.stateStore.deactivateSession(sessionKey);
4211
4823
  this.stateStore.clearCodexSessionId(sessionKey);
@@ -4401,6 +5013,18 @@ ${attachmentSummary}
4401
5013
  }
4402
5014
  }
4403
5015
  };
5016
+ function createIdleAutoDevSnapshot() {
5017
+ return {
5018
+ state: "idle",
5019
+ startedAt: null,
5020
+ endedAt: null,
5021
+ taskId: null,
5022
+ taskDescription: null,
5023
+ approved: null,
5024
+ repairRounds: 0,
5025
+ error: null
5026
+ };
5027
+ }
4404
5028
  function buildSessionKey(message) {
4405
5029
  return `${message.channel}:${message.conversationId}:${message.senderId}`;
4406
5030
  }
@@ -4424,7 +5048,7 @@ async function cleanupAttachmentFiles(imagePaths) {
4424
5048
  await Promise.all(
4425
5049
  imagePaths.map(async (imagePath) => {
4426
5050
  try {
4427
- await import_promises3.default.unlink(imagePath);
5051
+ await import_promises4.default.unlink(imagePath);
4428
5052
  } catch {
4429
5053
  }
4430
5054
  })
@@ -4474,11 +5098,11 @@ function stripLeadingBotMention(text, matrixUserId) {
4474
5098
  if (!matrixUserId) {
4475
5099
  return text;
4476
5100
  }
4477
- const escapedUserId = escapeRegex(matrixUserId);
5101
+ const escapedUserId = escapeRegex2(matrixUserId);
4478
5102
  const mentionPattern = new RegExp(`^\\s*(?:<)?${escapedUserId}(?:>)?[\\s,:\uFF0C\uFF1A-]*`, "i");
4479
5103
  return text.replace(mentionPattern, "").trim();
4480
5104
  }
4481
- function escapeRegex(value) {
5105
+ function escapeRegex2(value) {
4482
5106
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4483
5107
  }
4484
5108
  function formatDurationMs(durationMs) {
@@ -4541,7 +5165,7 @@ ${result.review}
4541
5165
 
4542
5166
  // src/store/state-store.ts
4543
5167
  var import_node_fs7 = __toESM(require("fs"));
4544
- var import_node_path8 = __toESM(require("path"));
5168
+ var import_node_path10 = __toESM(require("path"));
4545
5169
  var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
4546
5170
  var PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
4547
5171
  var SQLITE_MODULE_ID = `node:${"sqlite"}`;
@@ -4567,7 +5191,7 @@ var StateStore = class {
4567
5191
  this.maxProcessedEventsPerSession = maxProcessedEventsPerSession;
4568
5192
  this.maxSessionAgeMs = maxSessionAgeDays * ONE_DAY_MS;
4569
5193
  this.maxSessions = maxSessions;
4570
- import_node_fs7.default.mkdirSync(import_node_path8.default.dirname(this.dbPath), { recursive: true });
5194
+ import_node_fs7.default.mkdirSync(import_node_path10.default.dirname(this.dbPath), { recursive: true });
4571
5195
  this.db = new DatabaseSync(this.dbPath);
4572
5196
  this.initializeSchema();
4573
5197
  this.importLegacyStateIfNeeded();
@@ -4938,7 +5562,7 @@ function boolToInt(value) {
4938
5562
  }
4939
5563
 
4940
5564
  // src/app.ts
4941
- var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process4.execFile);
5565
+ var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process5.execFile);
4942
5566
  var CodeHarborApp = class {
4943
5567
  config;
4944
5568
  logger;
@@ -5024,6 +5648,7 @@ var CodeHarborAdminApp = class {
5024
5648
  host: options?.host ?? config.adminBindHost,
5025
5649
  port: options?.port ?? config.adminPort,
5026
5650
  adminToken: config.adminToken,
5651
+ adminTokens: config.adminTokens,
5027
5652
  adminIpAllowlist: config.adminIpAllowlist,
5028
5653
  adminAllowedOrigins: config.adminAllowedOrigins
5029
5654
  });
@@ -5034,7 +5659,7 @@ var CodeHarborAdminApp = class {
5034
5659
  this.logger.info("CodeHarbor admin server started", {
5035
5660
  host: address?.host ?? this.config.adminBindHost,
5036
5661
  port: address?.port ?? this.config.adminPort,
5037
- tokenProtected: Boolean(this.config.adminToken)
5662
+ tokenProtected: Boolean(this.config.adminToken) || this.config.adminTokens.length > 0
5038
5663
  });
5039
5664
  }
5040
5665
  async stop() {
@@ -5050,7 +5675,7 @@ async function runDoctor(config) {
5050
5675
  const logger = new Logger(config.logLevel);
5051
5676
  logger.info("Doctor check started");
5052
5677
  try {
5053
- const { stdout } = await execFileAsync2(config.codexBin, ["--version"]);
5678
+ const { stdout } = await execFileAsync3(config.codexBin, ["--version"]);
5054
5679
  logger.info("codex available", { version: stdout.trim() });
5055
5680
  } catch (error) {
5056
5681
  logger.error("codex unavailable", error);
@@ -5088,7 +5713,7 @@ function isNonLoopbackHost(host) {
5088
5713
 
5089
5714
  // src/config.ts
5090
5715
  var import_node_fs8 = __toESM(require("fs"));
5091
- var import_node_path9 = __toESM(require("path"));
5716
+ var import_node_path11 = __toESM(require("path"));
5092
5717
  var import_dotenv2 = __toESM(require("dotenv"));
5093
5718
  var import_zod = require("zod");
5094
5719
  var configSchema = import_zod.z.object({
@@ -5139,6 +5764,7 @@ var configSchema = import_zod.z.object({
5139
5764
  ADMIN_BIND_HOST: import_zod.z.string().default("127.0.0.1"),
5140
5765
  ADMIN_PORT: import_zod.z.string().default("8787").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().min(1).max(65535)),
5141
5766
  ADMIN_TOKEN: import_zod.z.string().default(""),
5767
+ ADMIN_TOKENS_JSON: import_zod.z.string().default(""),
5142
5768
  ADMIN_IP_ALLOWLIST: import_zod.z.string().default(""),
5143
5769
  ADMIN_ALLOWED_ORIGINS: import_zod.z.string().default(""),
5144
5770
  LOG_LEVEL: import_zod.z.enum(["debug", "info", "warn", "error"]).default("info")
@@ -5149,7 +5775,7 @@ var configSchema = import_zod.z.object({
5149
5775
  matrixCommandPrefix: v.MATRIX_COMMAND_PREFIX,
5150
5776
  codexBin: v.CODEX_BIN,
5151
5777
  codexModel: v.CODEX_MODEL?.trim() || null,
5152
- codexWorkdir: import_node_path9.default.resolve(v.CODEX_WORKDIR),
5778
+ codexWorkdir: import_node_path11.default.resolve(v.CODEX_WORKDIR),
5153
5779
  codexDangerousBypass: v.CODEX_DANGEROUS_BYPASS,
5154
5780
  codexExecTimeoutMs: v.CODEX_EXEC_TIMEOUT_MS,
5155
5781
  codexSandboxMode: v.CODEX_SANDBOX_MODE?.trim() || null,
@@ -5160,8 +5786,8 @@ var configSchema = import_zod.z.object({
5160
5786
  enabled: v.AGENT_WORKFLOW_ENABLED,
5161
5787
  autoRepairMaxRounds: v.AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS
5162
5788
  },
5163
- stateDbPath: import_node_path9.default.resolve(v.STATE_DB_PATH),
5164
- legacyStateJsonPath: v.STATE_PATH.trim() ? import_node_path9.default.resolve(v.STATE_PATH) : null,
5789
+ stateDbPath: import_node_path11.default.resolve(v.STATE_DB_PATH),
5790
+ legacyStateJsonPath: v.STATE_PATH.trim() ? import_node_path11.default.resolve(v.STATE_PATH) : null,
5165
5791
  maxProcessedEventsPerSession: v.MAX_PROCESSED_EVENTS_PER_SESSION,
5166
5792
  maxSessionAgeDays: v.MAX_SESSION_AGE_DAYS,
5167
5793
  maxSessions: v.MAX_SESSIONS,
@@ -5192,17 +5818,18 @@ var configSchema = import_zod.z.object({
5192
5818
  disableReplyChunkSplit: v.CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT,
5193
5819
  progressThrottleMs: v.CLI_COMPAT_PROGRESS_THROTTLE_MS,
5194
5820
  fetchMedia: v.CLI_COMPAT_FETCH_MEDIA,
5195
- recordPath: v.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path9.default.resolve(v.CLI_COMPAT_RECORD_PATH) : null
5821
+ recordPath: v.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path11.default.resolve(v.CLI_COMPAT_RECORD_PATH) : null
5196
5822
  },
5197
5823
  doctorHttpTimeoutMs: v.DOCTOR_HTTP_TIMEOUT_MS,
5198
5824
  adminBindHost: v.ADMIN_BIND_HOST.trim() || "127.0.0.1",
5199
5825
  adminPort: v.ADMIN_PORT,
5200
5826
  adminToken: v.ADMIN_TOKEN.trim() || null,
5827
+ adminTokens: parseAdminTokens(v.ADMIN_TOKENS_JSON),
5201
5828
  adminIpAllowlist: parseCsvList(v.ADMIN_IP_ALLOWLIST),
5202
5829
  adminAllowedOrigins: parseCsvList(v.ADMIN_ALLOWED_ORIGINS),
5203
5830
  logLevel: v.LOG_LEVEL
5204
5831
  }));
5205
- function loadEnvFromFile(filePath = import_node_path9.default.resolve(process.cwd(), ".env"), env = process.env) {
5832
+ function loadEnvFromFile(filePath = import_node_path11.default.resolve(process.cwd(), ".env"), env = process.env) {
5206
5833
  import_dotenv2.default.config({
5207
5834
  path: filePath,
5208
5835
  processEnv: env,
@@ -5215,9 +5842,9 @@ function loadConfig(env = process.env) {
5215
5842
  const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`).join("; ");
5216
5843
  throw new Error(`Invalid configuration: ${message}`);
5217
5844
  }
5218
- import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(parsed.data.stateDbPath), { recursive: true });
5845
+ import_node_fs8.default.mkdirSync(import_node_path11.default.dirname(parsed.data.stateDbPath), { recursive: true });
5219
5846
  if (parsed.data.legacyStateJsonPath) {
5220
- import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(parsed.data.legacyStateJsonPath), { recursive: true });
5847
+ import_node_fs8.default.mkdirSync(import_node_path11.default.dirname(parsed.data.legacyStateJsonPath), { recursive: true });
5221
5848
  }
5222
5849
  return parsed.data;
5223
5850
  }
@@ -5288,10 +5915,57 @@ function parseExtraEnv(raw) {
5288
5915
  function parseCsvList(raw) {
5289
5916
  return raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
5290
5917
  }
5918
+ function parseAdminTokens(raw) {
5919
+ const trimmed = raw.trim();
5920
+ if (!trimmed) {
5921
+ return [];
5922
+ }
5923
+ let parsed;
5924
+ try {
5925
+ parsed = JSON.parse(trimmed);
5926
+ } catch {
5927
+ throw new Error("ADMIN_TOKENS_JSON must be valid JSON.");
5928
+ }
5929
+ if (!Array.isArray(parsed)) {
5930
+ throw new Error("ADMIN_TOKENS_JSON must be a JSON array.");
5931
+ }
5932
+ const seenTokens = /* @__PURE__ */ new Set();
5933
+ return parsed.map((entry, index) => {
5934
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
5935
+ throw new Error(`ADMIN_TOKENS_JSON[${index}] must be an object.`);
5936
+ }
5937
+ const payload = entry;
5938
+ const tokenValue = payload.token;
5939
+ if (typeof tokenValue !== "string" || !tokenValue.trim()) {
5940
+ throw new Error(`ADMIN_TOKENS_JSON[${index}].token must be a non-empty string.`);
5941
+ }
5942
+ const token = tokenValue.trim();
5943
+ if (seenTokens.has(token)) {
5944
+ throw new Error(`ADMIN_TOKENS_JSON contains duplicated token at index ${index}.`);
5945
+ }
5946
+ seenTokens.add(token);
5947
+ let role = "admin";
5948
+ if (payload.role !== void 0) {
5949
+ if (payload.role !== "admin" && payload.role !== "viewer") {
5950
+ throw new Error(`ADMIN_TOKENS_JSON[${index}].role must be "admin" or "viewer".`);
5951
+ }
5952
+ role = payload.role;
5953
+ }
5954
+ if (payload.actor !== void 0 && payload.actor !== null && typeof payload.actor !== "string") {
5955
+ throw new Error(`ADMIN_TOKENS_JSON[${index}].actor must be a string when provided.`);
5956
+ }
5957
+ const actor = typeof payload.actor === "string" ? payload.actor.trim() || null : null;
5958
+ return {
5959
+ token,
5960
+ role,
5961
+ actor
5962
+ };
5963
+ });
5964
+ }
5291
5965
 
5292
5966
  // src/config-snapshot.ts
5293
5967
  var import_node_fs9 = __toESM(require("fs"));
5294
- var import_node_path10 = __toESM(require("path"));
5968
+ var import_node_path12 = __toESM(require("path"));
5295
5969
  var import_zod2 = require("zod");
5296
5970
  var CONFIG_SNAPSHOT_SCHEMA_VERSION = 1;
5297
5971
  var CONFIG_SNAPSHOT_ENV_KEYS = [
@@ -5342,6 +6016,7 @@ var CONFIG_SNAPSHOT_ENV_KEYS = [
5342
6016
  "ADMIN_BIND_HOST",
5343
6017
  "ADMIN_PORT",
5344
6018
  "ADMIN_TOKEN",
6019
+ "ADMIN_TOKENS_JSON",
5345
6020
  "ADMIN_IP_ALLOWLIST",
5346
6021
  "ADMIN_ALLOWED_ORIGINS",
5347
6022
  "LOG_LEVEL"
@@ -5406,6 +6081,7 @@ var envSnapshotSchema = import_zod2.z.object({
5406
6081
  ADMIN_BIND_HOST: import_zod2.z.string(),
5407
6082
  ADMIN_PORT: integerStringSchema("ADMIN_PORT", 1, 65535),
5408
6083
  ADMIN_TOKEN: import_zod2.z.string(),
6084
+ ADMIN_TOKENS_JSON: jsonArrayStringSchema("ADMIN_TOKENS_JSON", true).default(""),
5409
6085
  ADMIN_IP_ALLOWLIST: import_zod2.z.string(),
5410
6086
  ADMIN_ALLOWED_ORIGINS: import_zod2.z.string().default(""),
5411
6087
  LOG_LEVEL: import_zod2.z.enum(LOG_LEVELS)
@@ -5478,8 +6154,8 @@ async function runConfigExportCommand(options = {}) {
5478
6154
  const snapshot = buildConfigSnapshot(config, stateStore.listRoomSettings(), options.now ?? /* @__PURE__ */ new Date());
5479
6155
  const serialized = serializeConfigSnapshot(snapshot);
5480
6156
  if (options.outputPath) {
5481
- const targetPath = import_node_path10.default.resolve(cwd, options.outputPath);
5482
- import_node_fs9.default.mkdirSync(import_node_path10.default.dirname(targetPath), { recursive: true });
6157
+ const targetPath = import_node_path12.default.resolve(cwd, options.outputPath);
6158
+ import_node_fs9.default.mkdirSync(import_node_path12.default.dirname(targetPath), { recursive: true });
5483
6159
  import_node_fs9.default.writeFileSync(targetPath, serialized, "utf8");
5484
6160
  output.write(`Exported config snapshot to ${targetPath}
5485
6161
  `);
@@ -5494,7 +6170,7 @@ async function runConfigImportCommand(options) {
5494
6170
  const cwd = options.cwd ?? process.cwd();
5495
6171
  const output = options.output ?? process.stdout;
5496
6172
  const actor = options.actor?.trim() || "cli:config-import";
5497
- const sourcePath = import_node_path10.default.resolve(cwd, options.filePath);
6173
+ const sourcePath = import_node_path12.default.resolve(cwd, options.filePath);
5498
6174
  if (!import_node_fs9.default.existsSync(sourcePath)) {
5499
6175
  throw new Error(`Config snapshot file not found: ${sourcePath}`);
5500
6176
  }
@@ -5525,7 +6201,7 @@ async function runConfigImportCommand(options) {
5525
6201
  synchronizeRoomSettings(stateStore, normalizedRooms);
5526
6202
  stateStore.appendConfigRevision(
5527
6203
  actor,
5528
- `import config snapshot from ${import_node_path10.default.basename(sourcePath)}`,
6204
+ `import config snapshot from ${import_node_path12.default.basename(sourcePath)}`,
5529
6205
  JSON.stringify({
5530
6206
  type: "config_snapshot_import",
5531
6207
  sourcePath,
@@ -5539,7 +6215,7 @@ async function runConfigImportCommand(options) {
5539
6215
  output.write(
5540
6216
  [
5541
6217
  `Imported config snapshot from ${sourcePath}`,
5542
- `- updated .env in ${import_node_path10.default.resolve(cwd, ".env")}`,
6218
+ `- updated .env in ${import_node_path12.default.resolve(cwd, ".env")}`,
5543
6219
  `- synchronized room settings: ${normalizedRooms.length}`,
5544
6220
  "- restart required: yes (global env settings are restart-scoped)"
5545
6221
  ].join("\n") + "\n"
@@ -5594,6 +6270,7 @@ function buildSnapshotEnv(config) {
5594
6270
  ADMIN_BIND_HOST: config.adminBindHost,
5595
6271
  ADMIN_PORT: String(config.adminPort),
5596
6272
  ADMIN_TOKEN: config.adminToken ?? "",
6273
+ ADMIN_TOKENS_JSON: serializeAdminTokens(config.adminTokens),
5597
6274
  ADMIN_IP_ALLOWLIST: config.adminIpAllowlist.join(","),
5598
6275
  ADMIN_ALLOWED_ORIGINS: config.adminAllowedOrigins.join(","),
5599
6276
  LOG_LEVEL: config.logLevel
@@ -5613,10 +6290,10 @@ function parseJsonFile(filePath) {
5613
6290
  function normalizeSnapshotEnv(env, cwd) {
5614
6291
  return {
5615
6292
  ...env,
5616
- CODEX_WORKDIR: import_node_path10.default.resolve(cwd, env.CODEX_WORKDIR),
5617
- STATE_DB_PATH: import_node_path10.default.resolve(cwd, env.STATE_DB_PATH),
5618
- STATE_PATH: env.STATE_PATH.trim() ? import_node_path10.default.resolve(cwd, env.STATE_PATH) : "",
5619
- CLI_COMPAT_RECORD_PATH: env.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path10.default.resolve(cwd, env.CLI_COMPAT_RECORD_PATH) : ""
6293
+ CODEX_WORKDIR: import_node_path12.default.resolve(cwd, env.CODEX_WORKDIR),
6294
+ STATE_DB_PATH: import_node_path12.default.resolve(cwd, env.STATE_DB_PATH),
6295
+ STATE_PATH: env.STATE_PATH.trim() ? import_node_path12.default.resolve(cwd, env.STATE_PATH) : "",
6296
+ CLI_COMPAT_RECORD_PATH: env.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path12.default.resolve(cwd, env.CLI_COMPAT_RECORD_PATH) : ""
5620
6297
  };
5621
6298
  }
5622
6299
  function normalizeSnapshotRooms(rooms, cwd) {
@@ -5631,7 +6308,7 @@ function normalizeSnapshotRooms(rooms, cwd) {
5631
6308
  throw new Error(`Duplicate roomId in snapshot: ${roomId}`);
5632
6309
  }
5633
6310
  seen.add(roomId);
5634
- const workdir = import_node_path10.default.resolve(cwd, room.workdir);
6311
+ const workdir = import_node_path12.default.resolve(cwd, room.workdir);
5635
6312
  ensureDirectory2(workdir, `room workdir (${roomId})`);
5636
6313
  normalized.push({
5637
6314
  roomId,
@@ -5658,8 +6335,8 @@ function synchronizeRoomSettings(stateStore, rooms) {
5658
6335
  }
5659
6336
  }
5660
6337
  function persistEnvSnapshot(cwd, env) {
5661
- const envPath = import_node_path10.default.resolve(cwd, ".env");
5662
- const examplePath = import_node_path10.default.resolve(cwd, ".env.example");
6338
+ const envPath = import_node_path12.default.resolve(cwd, ".env");
6339
+ const examplePath = import_node_path12.default.resolve(cwd, ".env.example");
5663
6340
  const template = import_node_fs9.default.existsSync(envPath) ? import_node_fs9.default.readFileSync(envPath, "utf8") : import_node_fs9.default.existsSync(examplePath) ? import_node_fs9.default.readFileSync(examplePath, "utf8") : "";
5664
6341
  const overrides = {};
5665
6342
  for (const key of CONFIG_SNAPSHOT_ENV_KEYS) {
@@ -5683,6 +6360,9 @@ function parseIntStrict(raw) {
5683
6360
  function serializeJsonObject(value) {
5684
6361
  return Object.keys(value).length > 0 ? JSON.stringify(value) : "";
5685
6362
  }
6363
+ function serializeAdminTokens(tokens) {
6364
+ return tokens.length > 0 ? JSON.stringify(tokens) : "";
6365
+ }
5686
6366
  function booleanStringSchema(key) {
5687
6367
  return import_zod2.z.string().refine((value) => BOOLEAN_STRING.test(value), {
5688
6368
  message: `${key} must be a boolean string (true/false).`
@@ -5720,13 +6400,30 @@ function jsonObjectStringSchema(key, allowEmpty) {
5720
6400
  message: `${key} must be an empty string or a JSON object string.`
5721
6401
  });
5722
6402
  }
6403
+ function jsonArrayStringSchema(key, allowEmpty) {
6404
+ return import_zod2.z.string().refine((value) => {
6405
+ const trimmed = value.trim();
6406
+ if (!trimmed) {
6407
+ return allowEmpty;
6408
+ }
6409
+ let parsed;
6410
+ try {
6411
+ parsed = JSON.parse(trimmed);
6412
+ } catch {
6413
+ return false;
6414
+ }
6415
+ return Array.isArray(parsed);
6416
+ }, {
6417
+ message: `${key} must be an empty string or a JSON array string.`
6418
+ });
6419
+ }
5723
6420
 
5724
6421
  // src/preflight.ts
5725
- var import_node_child_process5 = require("child_process");
6422
+ var import_node_child_process6 = require("child_process");
5726
6423
  var import_node_fs10 = __toESM(require("fs"));
5727
- var import_node_path11 = __toESM(require("path"));
5728
- var import_node_util3 = require("util");
5729
- var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process5.execFile);
6424
+ var import_node_path13 = __toESM(require("path"));
6425
+ var import_node_util4 = require("util");
6426
+ var execFileAsync4 = (0, import_node_util4.promisify)(import_node_child_process6.execFile);
5730
6427
  var REQUIRED_ENV_KEYS = ["MATRIX_HOMESERVER", "MATRIX_USER_ID", "MATRIX_ACCESS_TOKEN"];
5731
6428
  async function runStartupPreflight(options = {}) {
5732
6429
  const env = options.env ?? process.env;
@@ -5735,7 +6432,9 @@ async function runStartupPreflight(options = {}) {
5735
6432
  const fileExists = options.fileExists ?? import_node_fs10.default.existsSync;
5736
6433
  const isDirectory = options.isDirectory ?? defaultIsDirectory;
5737
6434
  const issues = [];
5738
- const envPath = import_node_path11.default.resolve(cwd, ".env");
6435
+ const envPath = import_node_path13.default.resolve(cwd, ".env");
6436
+ let resolvedCodexBin = null;
6437
+ let usedCodexFallback = false;
5739
6438
  if (!fileExists(envPath)) {
5740
6439
  issues.push({
5741
6440
  level: "warn",
@@ -5783,18 +6482,34 @@ async function runStartupPreflight(options = {}) {
5783
6482
  const codexBin = readEnv(env, "CODEX_BIN") || "codex";
5784
6483
  try {
5785
6484
  await checkCodexBinary(codexBin);
6485
+ resolvedCodexBin = codexBin;
5786
6486
  } catch (error) {
5787
- const reason = error instanceof Error && error.message ? ` (${error.message})` : "";
5788
- issues.push({
5789
- level: "error",
5790
- code: "missing_codex_bin",
5791
- check: "CODEX_BIN",
5792
- message: `Unable to execute "${codexBin}"${reason}.`,
5793
- fix: `Install Codex CLI and ensure "${codexBin}" is in PATH, or set CODEX_BIN=/absolute/path/to/codex.`
5794
- });
6487
+ const fallbackBin = await findWorkingCodexBin(codexBin, { env, checkBinary: checkCodexBinary });
6488
+ if (fallbackBin && fallbackBin !== codexBin) {
6489
+ resolvedCodexBin = fallbackBin;
6490
+ usedCodexFallback = true;
6491
+ issues.push({
6492
+ level: "warn",
6493
+ code: "codex_bin_fallback",
6494
+ check: "CODEX_BIN",
6495
+ message: `Configured CODEX_BIN "${codexBin}" is unavailable; fallback to "${fallbackBin}".`,
6496
+ fix: `Update CODEX_BIN=${fallbackBin} in .env to avoid fallback probing on startup.`
6497
+ });
6498
+ } else if (fallbackBin) {
6499
+ resolvedCodexBin = fallbackBin;
6500
+ } else {
6501
+ const reason = error instanceof Error && error.message ? ` (${error.message})` : "";
6502
+ issues.push({
6503
+ level: "error",
6504
+ code: "missing_codex_bin",
6505
+ check: "CODEX_BIN",
6506
+ message: `Unable to execute "${codexBin}"${reason}.`,
6507
+ fix: `Install Codex CLI and ensure "${codexBin}" is in PATH, or set CODEX_BIN=/absolute/path/to/codex.`
6508
+ });
6509
+ }
5795
6510
  }
5796
6511
  const configuredWorkdir = readEnv(env, "CODEX_WORKDIR");
5797
- const workdir = import_node_path11.default.resolve(cwd, configuredWorkdir || cwd);
6512
+ const workdir = import_node_path13.default.resolve(cwd, configuredWorkdir || cwd);
5798
6513
  if (!fileExists(workdir) || !isDirectory(workdir)) {
5799
6514
  issues.push({
5800
6515
  level: "error",
@@ -5806,7 +6521,9 @@ async function runStartupPreflight(options = {}) {
5806
6521
  }
5807
6522
  return {
5808
6523
  ok: issues.every((issue) => issue.level !== "error"),
5809
- issues
6524
+ issues,
6525
+ resolvedCodexBin,
6526
+ usedCodexFallback
5810
6527
  };
5811
6528
  }
5812
6529
  function formatPreflightReport(result, commandName) {
@@ -5829,7 +6546,7 @@ function formatPreflightReport(result, commandName) {
5829
6546
  `;
5830
6547
  }
5831
6548
  async function defaultCheckCodexBinary(bin) {
5832
- await execFileAsync3(bin, ["--version"]);
6549
+ await execFileAsync4(bin, ["--version"]);
5833
6550
  }
5834
6551
  function defaultIsDirectory(targetPath) {
5835
6552
  try {
@@ -5892,11 +6609,12 @@ admin.command("serve").description("Start admin config API server").option("--ho
5892
6609
  const host = options.host?.trim() || config.adminBindHost;
5893
6610
  const port = options.port ? parsePortOption(options.port, config.adminPort) : config.adminPort;
5894
6611
  const allowInsecureNoToken = options.allowInsecureNoToken ?? false;
5895
- if (!config.adminToken && !allowInsecureNoToken && isNonLoopbackHost(host)) {
6612
+ const hasAdminAuth = Boolean(config.adminToken) || config.adminTokens.length > 0;
6613
+ if (!hasAdminAuth && !allowInsecureNoToken && isNonLoopbackHost(host)) {
5896
6614
  process.stderr.write(
5897
6615
  [
5898
- "Refusing to start admin server on non-loopback host without ADMIN_TOKEN.",
5899
- "Fix: set ADMIN_TOKEN in .env, or explicitly pass --allow-insecure-no-token.",
6616
+ "Refusing to start admin server on non-loopback host without admin auth token.",
6617
+ "Fix: set ADMIN_TOKEN or ADMIN_TOKENS_JSON in .env, or explicitly pass --allow-insecure-no-token.",
5900
6618
  ""
5901
6619
  ].join("\n")
5902
6620
  );
@@ -6015,7 +6733,11 @@ if (process.argv.length <= 2) {
6015
6733
  }
6016
6734
  void program.parseAsync(process.argv);
6017
6735
  async function loadConfigWithPreflight(commandName, runtimeHomePath) {
6018
- const preflight = await runStartupPreflight({ cwd: runtimeHomePath });
6736
+ const env = { ...process.env };
6737
+ const preflight = await runStartupPreflight({ cwd: runtimeHomePath, env });
6738
+ if (preflight.resolvedCodexBin) {
6739
+ env.CODEX_BIN = preflight.resolvedCodexBin;
6740
+ }
6019
6741
  if (preflight.issues.length > 0) {
6020
6742
  const report = formatPreflightReport(preflight, commandName);
6021
6743
  if (preflight.ok) {
@@ -6026,7 +6748,7 @@ async function loadConfigWithPreflight(commandName, runtimeHomePath) {
6026
6748
  }
6027
6749
  }
6028
6750
  try {
6029
- return loadConfig();
6751
+ return loadConfig(env);
6030
6752
  } catch (error) {
6031
6753
  const message = error instanceof Error ? error.message : String(error);
6032
6754
  process.stderr.write(`Configuration error: ${message}
@@ -6056,7 +6778,7 @@ function ensureRuntimeHomeOrExit() {
6056
6778
  `);
6057
6779
  process.exit(1);
6058
6780
  }
6059
- loadEnvFromFile(import_node_path12.default.resolve(home, ".env"));
6781
+ loadEnvFromFile(import_node_path14.default.resolve(home, ".env"));
6060
6782
  runtimeHome = home;
6061
6783
  return runtimeHome;
6062
6784
  }
@@ -6071,7 +6793,7 @@ function parsePortOption(raw, fallback) {
6071
6793
  }
6072
6794
  function resolveCliVersion() {
6073
6795
  try {
6074
- const packagePath = import_node_path12.default.resolve(__dirname, "..", "package.json");
6796
+ const packagePath = import_node_path14.default.resolve(__dirname, "..", "package.json");
6075
6797
  const content = import_node_fs11.default.readFileSync(packagePath, "utf8");
6076
6798
  const parsed = JSON.parse(content);
6077
6799
  return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version : "0.0.0";
@@ -6082,9 +6804,9 @@ function resolveCliVersion() {
6082
6804
  function resolveCliScriptPath() {
6083
6805
  const argvPath = process.argv[1];
6084
6806
  if (argvPath && argvPath.trim()) {
6085
- return import_node_path12.default.resolve(argvPath);
6807
+ return import_node_path14.default.resolve(argvPath);
6086
6808
  }
6087
- return import_node_path12.default.resolve(__dirname, "cli.js");
6809
+ return import_node_path14.default.resolve(__dirname, "cli.js");
6088
6810
  }
6089
6811
  function maybeReexecServiceCommandWithSudo() {
6090
6812
  if (typeof process.getuid !== "function" || process.getuid() === 0) {
@@ -6095,7 +6817,7 @@ function maybeReexecServiceCommandWithSudo() {
6095
6817
  return;
6096
6818
  }
6097
6819
  const cliScriptPath = resolveCliScriptPath();
6098
- const child = (0, import_node_child_process6.spawnSync)("sudo", [process.execPath, cliScriptPath, ...serviceArgs], {
6820
+ const child = (0, import_node_child_process7.spawnSync)("sudo", [process.execPath, cliScriptPath, ...serviceArgs], {
6099
6821
  stdio: "inherit"
6100
6822
  });
6101
6823
  if (child.error) {