maesttro 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/maesttro.js CHANGED
@@ -351,15 +351,342 @@ async function status() {
351
351
 
352
352
  // src/configure.ts
353
353
  import {
354
- cpSync,
355
- existsSync,
356
- mkdirSync as mkdirSync2,
354
+ cpSync as cpSync2,
355
+ existsSync as existsSync2,
356
+ mkdirSync as mkdirSync4,
357
357
  readdirSync,
358
- readFileSync as readFileSync2,
359
- writeFileSync as writeFileSync2
358
+ readFileSync as readFileSync4,
359
+ writeFileSync as writeFileSync4
360
360
  } from "node:fs";
361
361
  import { homedir as homedir2 } from "node:os";
362
- import { dirname as dirname3, join as join3 } from "node:path";
362
+ import { dirname as dirname5, join as join4 } from "node:path";
363
+
364
+ // src/client-configs.ts
365
+ import {
366
+ chmodSync as chmodSync2,
367
+ cpSync,
368
+ existsSync,
369
+ mkdirSync as mkdirSync3,
370
+ mkdtempSync,
371
+ readFileSync as readFileSync3,
372
+ rmSync as rmSync2,
373
+ writeFileSync as writeFileSync3
374
+ } from "node:fs";
375
+ import { tmpdir } from "node:os";
376
+ import { dirname as dirname3, join as join2 } from "node:path";
377
+
378
+ // src/json-file.ts
379
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
380
+ import { dirname as dirname2 } from "node:path";
381
+ function isObject(value) {
382
+ return typeof value === "object" && value !== null && !Array.isArray(value);
383
+ }
384
+ function mergeJson(existing, curated) {
385
+ const merged = { ...existing };
386
+ for (const [key, value] of Object.entries(curated)) {
387
+ const current = merged[key];
388
+ merged[key] = isObject(current) && isObject(value) ? mergeJson(current, value) : value;
389
+ }
390
+ return merged;
391
+ }
392
+ function readJson(path) {
393
+ try {
394
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
395
+ return isObject(parsed) ? parsed : {};
396
+ } catch {
397
+ return {};
398
+ }
399
+ }
400
+ function writeJson(path, value) {
401
+ mkdirSync2(dirname2(path), { recursive: true });
402
+ writeFileSync2(path, `${JSON.stringify(value, null, 2)}
403
+ `);
404
+ }
405
+ function skipInsignificant(text, from) {
406
+ let index = from;
407
+ while (index < text.length) {
408
+ if (/\s/.test(text[index] ?? "")) {
409
+ index++;
410
+ } else if (text.startsWith("//", index)) {
411
+ while (index < text.length && text[index] !== `
412
+ `)
413
+ index++;
414
+ } else if (text.startsWith("/*", index)) {
415
+ const end = text.indexOf("*/", index + 2);
416
+ index = end === -1 ? text.length : end + 2;
417
+ } else {
418
+ return index;
419
+ }
420
+ }
421
+ return index;
422
+ }
423
+ function stripJsonc(text) {
424
+ let output = "";
425
+ let index = 0;
426
+ while (index < text.length) {
427
+ const char = text[index];
428
+ if (char === '"') {
429
+ const start = index;
430
+ index++;
431
+ while (index < text.length && text[index] !== '"') {
432
+ index += text[index] === "\\" ? 2 : 1;
433
+ }
434
+ index++;
435
+ output += text.slice(start, index);
436
+ continue;
437
+ }
438
+ const significant = skipInsignificant(text, index);
439
+ if (significant > index) {
440
+ output += " ";
441
+ index = significant;
442
+ continue;
443
+ }
444
+ const trailingComma = char === "," && /[}\]]/.test(text[skipInsignificant(text, index + 1)] ?? "");
445
+ if (!trailingComma)
446
+ output += char;
447
+ index++;
448
+ }
449
+ return output;
450
+ }
451
+ function parseJsonc(text) {
452
+ const parsed = JSON.parse(stripJsonc(text));
453
+ if (!isObject(parsed))
454
+ throw new Error("expected a JSON object");
455
+ return parsed;
456
+ }
457
+
458
+ // src/maesttro-endpoint.ts
459
+ var DEFAULT_MAESTTRO_URL = "http://100.64.0.2";
460
+ var ORCHESTRATOR_MODEL = "maesttro";
461
+ function maesttroUrl(env = process.env) {
462
+ return (env.MAESTTRO_URL?.trim() || DEFAULT_MAESTTRO_URL).replace(/\/+$/, "");
463
+ }
464
+
465
+ // src/provider-configs.ts
466
+ var PROVIDER_ID = "maesttro";
467
+ var MAESTTRO_API_KEY = "maesttro";
468
+ var DGX_SPARK_PROVIDER_ID = "dgx-spark";
469
+ var DGX_SPARK_URL = "http://100.64.0.1:8001";
470
+ var INJECTED_OPENCODE_PROVIDERS = [PROVIDER_ID, DGX_SPARK_PROVIDER_ID];
471
+ function without(object, keys) {
472
+ if (!isObject(object))
473
+ return {};
474
+ return Object.fromEntries(Object.entries(object).filter(([key]) => !keys.includes(key)));
475
+ }
476
+ function stringList(value) {
477
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : undefined;
478
+ }
479
+ function opencodeHostBase(host) {
480
+ return isObject(host.provider) ? { ...host, provider: without(host.provider, INJECTED_OPENCODE_PROVIDERS) } : host;
481
+ }
482
+ var IMAGE_INPUT = {
483
+ attachment: true,
484
+ modalities: { input: ["text", "image"] }
485
+ };
486
+ function opencodeProviders(url) {
487
+ return {
488
+ [PROVIDER_ID]: {
489
+ npm: "@ai-sdk/openai-compatible",
490
+ name: "Maesttro",
491
+ options: { baseURL: `${url}/v1`, apiKey: MAESTTRO_API_KEY },
492
+ models: {
493
+ [ORCHESTRATOR_MODEL]: { name: "Orchestrate", ...IMAGE_INPUT }
494
+ }
495
+ },
496
+ [DGX_SPARK_PROVIDER_ID]: {
497
+ npm: "@ai-sdk/openai-compatible",
498
+ name: "DGX-Spark",
499
+ options: { baseURL: `${DGX_SPARK_URL}/v1`, apiKey: "" },
500
+ models: {
501
+ "maesttro/lorem": {
502
+ name: "Qwen3.8 27B",
503
+ ...IMAGE_INPUT,
504
+ limit: { context: 131072, output: 32768 }
505
+ }
506
+ }
507
+ }
508
+ };
509
+ }
510
+ function opencodeConfig(base, url) {
511
+ const merged = mergeJson({ $schema: "https://opencode.ai/config.json", ...base }, {
512
+ model: `${PROVIDER_ID}/${ORCHESTRATOR_MODEL}`,
513
+ compaction: { auto: true, keep: { tokens: 24000 }, buffer: 32000 }
514
+ });
515
+ const enabled = stringList(base.enabled_providers);
516
+ const disabled = stringList(base.disabled_providers);
517
+ return {
518
+ ...merged,
519
+ ...enabled ? {
520
+ enabled_providers: [
521
+ ...enabled.filter((id) => !INJECTED_OPENCODE_PROVIDERS.includes(id)),
522
+ ...INJECTED_OPENCODE_PROVIDERS
523
+ ]
524
+ } : {},
525
+ ...disabled ? {
526
+ disabled_providers: disabled.filter((id) => !INJECTED_OPENCODE_PROVIDERS.includes(id))
527
+ } : {},
528
+ provider: {
529
+ ...isObject(base.provider) ? base.provider : {},
530
+ ...opencodeProviders(url)
531
+ }
532
+ };
533
+ }
534
+
535
+ // src/secrets.ts
536
+ var SECRET_KEY = /api[_-]?key|token|secret|passw(or)?d|credential|authorization|bearer|private[_-]?key|cookie/i;
537
+ var ENV_NAME_KEY = /(^|_)env(_var|_key|_vars)?$/i;
538
+ var SECRET_MAPS = new Set(["env", "environment", "headers", "http_headers"]);
539
+ var CREDENTIAL_SHAPES = [
540
+ /(^|[^A-Za-z0-9])sk[-_][A-Za-z0-9_-]{16,}/,
541
+ /(^|[^A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{20,}/,
542
+ /github_pat_[A-Za-z0-9_]{20,}/,
543
+ /glpat-[A-Za-z0-9_-]{16,}/,
544
+ /xox[abposr]-[A-Za-z0-9-]{10,}/,
545
+ /(^|[^A-Za-z0-9])(AKIA|ASIA)[0-9A-Z]{16}/,
546
+ /AIza[0-9A-Za-z_-]{30,}/,
547
+ /\bbearer\s+[A-Za-z0-9._~+/=-]{8,}/i,
548
+ /:\/\/[^/\s:@]+:[^/\s@]+@/,
549
+ /[?&](api[_-]?key|key|token|access_token|secret)=/i,
550
+ /hooks\.slack\.com\/services\//
551
+ ];
552
+ var SECRET_FLAG = /^--?[\w-]*(token|api[_-]?key|secret|passw(or)?d)[\w-]*$/i;
553
+ var SECRET_FLAG_WITH_VALUE = /^--?[\w-]*(token|api[_-]?key|secret|passw(or)?d)[\w-]*=/i;
554
+ var REFERENCE = /^\{(env|file):[^}]+\}$|^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/;
555
+ var HARMLESS_MAP_VALUE = /^(\d+(\.\d+)?|true|false|yes|no|on|off)$/i;
556
+ function isSecretString({
557
+ key,
558
+ value,
559
+ inSecretMap
560
+ }) {
561
+ if (value.trim() === "" || REFERENCE.test(value.trim()))
562
+ return false;
563
+ if (CREDENTIAL_SHAPES.some((shape) => shape.test(value)))
564
+ return true;
565
+ if (inSecretMap)
566
+ return !HARMLESS_MAP_VALUE.test(value.trim());
567
+ return SECRET_KEY.test(key) && !ENV_NAME_KEY.test(key);
568
+ }
569
+ function redactValue(value, key, path, inSecretMap, removed) {
570
+ if (typeof value === "string") {
571
+ if (isSecretString({ key, value, inSecretMap })) {
572
+ removed.push(path);
573
+ return { keep: false, value };
574
+ }
575
+ return { keep: true, value };
576
+ }
577
+ if (Array.isArray(value)) {
578
+ const items = value.flatMap((item, index) => {
579
+ const previous = value[index - 1];
580
+ const flagged = typeof item === "string" && (typeof previous === "string" && SECRET_FLAG.test(previous) || SECRET_FLAG_WITH_VALUE.test(item));
581
+ if (flagged) {
582
+ removed.push(`${path}[${index}]`);
583
+ return [];
584
+ }
585
+ const result = redactValue(item, key, `${path}[${index}]`, inSecretMap, removed);
586
+ return result.keep ? [result.value] : [];
587
+ });
588
+ return { keep: true, value: items };
589
+ }
590
+ if (isObject(value)) {
591
+ return {
592
+ keep: true,
593
+ value: redactObject(value, path, inSecretMap || SECRET_MAPS.has(key), removed)
594
+ };
595
+ }
596
+ return { keep: true, value };
597
+ }
598
+ function redactObject(object, path, inSecretMap, removed) {
599
+ const kept = {};
600
+ for (const [key, value] of Object.entries(object)) {
601
+ const result = redactValue(value, key, path ? `${path}.${key}` : key, inSecretMap, removed);
602
+ const emptied = isObject(value) && Object.keys(value).length > 0 && isObject(result.value) && Object.keys(result.value).length === 0;
603
+ if (result.keep && !emptied)
604
+ kept[key] = result.value;
605
+ }
606
+ return kept;
607
+ }
608
+ function redactSecrets(config) {
609
+ const removed = [];
610
+ return { value: redactObject(config, "", false, removed), removed };
611
+ }
612
+
613
+ // src/client-configs.ts
614
+ var CONTAINER_CLIENT_CONFIGS = "/opt/maesttro-clients";
615
+ var OPENCODE_HOST_CONFIGS = [
616
+ ".config/opencode/opencode.json",
617
+ ".config/opencode/opencode.jsonc"
618
+ ];
619
+ var OPENCODE_CONFIG = ".config/opencode/opencode.json";
620
+ var OPENCODE_TUI_STATE = ".local/state/opencode/kv.json";
621
+ var CURATED_OPENCODE_TUI_STATE = {
622
+ thinking_mode: "show",
623
+ thinking_visibility: true
624
+ };
625
+ function readHostFile({
626
+ home,
627
+ file,
628
+ parse,
629
+ warn
630
+ }) {
631
+ const path = join2(home, file);
632
+ if (!existsSync(path))
633
+ return {};
634
+ try {
635
+ return parse(readFileSync3(path, "utf8"));
636
+ } catch {
637
+ warn(`maesttro: ignoring your ~/${file}, it doesn't parse`);
638
+ return {};
639
+ }
640
+ }
641
+ function readHostClientConfigs({
642
+ home,
643
+ warn = (message) => console.error(message)
644
+ }) {
645
+ return {
646
+ opencode: OPENCODE_HOST_CONFIGS.reduce((config, file) => mergeJson(config, readHostFile({ home, file, parse: parseJsonc, warn })), {})
647
+ };
648
+ }
649
+ function json(value) {
650
+ return `${JSON.stringify(value, null, 2)}
651
+ `;
652
+ }
653
+ function buildClientConfigs({
654
+ host,
655
+ url
656
+ }) {
657
+ const opencode = redactSecrets(opencodeHostBase(host.opencode));
658
+ return {
659
+ files: { [OPENCODE_CONFIG]: json(opencodeConfig(opencode.value, url)) },
660
+ removed: opencode.removed.map((path) => `opencode ${path}`)
661
+ };
662
+ }
663
+ function writeClientConfigDir(files, parent = tmpdir()) {
664
+ const root = mkdtempSync(join2(parent, "maesttro-clients-"));
665
+ chmodSync2(root, 448);
666
+ const dir = join2(root, "home");
667
+ for (const [file, content] of Object.entries(files)) {
668
+ const path = join2(dir, file);
669
+ mkdirSync3(dirname3(path), { recursive: true, mode: 493 });
670
+ writeFileSync3(path, content);
671
+ chmodSync2(path, 420);
672
+ }
673
+ chmodSync2(dir, 493);
674
+ return { dir, remove: () => rmSync2(root, { recursive: true, force: true }) };
675
+ }
676
+ function installClientConfigs({
677
+ home,
678
+ source = CONTAINER_CLIENT_CONFIGS
679
+ }) {
680
+ if (!existsSync(source))
681
+ return false;
682
+ cpSync(source, home, { recursive: true, force: true });
683
+ return true;
684
+ }
685
+ function injectOpencodeTuiState({ home }) {
686
+ const path = join2(home, OPENCODE_TUI_STATE);
687
+ writeJson(path, mergeJson(readJson(path), CURATED_OPENCODE_TUI_STATE));
688
+ return path;
689
+ }
363
690
 
364
691
  // src/clients.ts
365
692
  var CLIENTS = {
@@ -374,15 +701,9 @@ var CLIENTS = {
374
701
  npmPackage: "opencode-ai",
375
702
  binary: "opencode",
376
703
  configDir: "/home/agent/.config/opencode"
377
- },
378
- codex: {
379
- label: "OpenAI Codex CLI",
380
- npmPackage: "@openai/codex",
381
- binary: "codex",
382
- configDir: "/home/agent/.codex"
383
704
  }
384
705
  };
385
- var DEFAULT_CLIENT = "claude";
706
+ var DEFAULT_CLIENT = "opencode";
386
707
  function isClientKey(value) {
387
708
  return Object.hasOwn(CLIENTS, value);
388
709
  }
@@ -396,109 +717,108 @@ var STAGED_DIRECTORIES = [
396
717
  ];
397
718
  var INSTRUCTION_FILES = [
398
719
  ".claude/CLAUDE.md",
399
- ".config/opencode/AGENTS.md",
400
- ".codex/AGENTS.md"
720
+ ".config/opencode/AGENTS.md"
401
721
  ];
402
722
  var STAGED_PATHS = [...INSTRUCTION_FILES, ...STAGED_DIRECTORIES];
403
723
 
404
724
  // src/paths.ts
405
- import { dirname as dirname2, join as join2, resolve } from "node:path";
725
+ import { dirname as dirname4, join as join3, resolve } from "node:path";
406
726
  import { fileURLToPath } from "node:url";
407
- var PACKAGE_ROOT = resolve(dirname2(fileURLToPath(import.meta.url)), "..");
408
- var DOCKERFILE_DIR = join2(PACKAGE_ROOT, "docker");
727
+ var PACKAGE_ROOT = resolve(dirname4(fileURLToPath(import.meta.url)), "..");
728
+ var DOCKERFILE_DIR = join3(PACKAGE_ROOT, "docker");
409
729
  var IMAGE = "maesttro-runner:latest";
410
730
  var VOLUME = process.env.MAESTTRO_VOLUME?.trim() || "maesttro-home";
411
731
 
412
- // src/sandbox-context.ts
413
- function sandboxContextFromEnv(env) {
732
+ // src/runner-context.ts
733
+ function githubMode(value) {
734
+ return value === "on" || value === "strict" ? value : "off";
735
+ }
736
+ function githubRules(context) {
737
+ switch (context.github) {
738
+ case "on":
739
+ return [
740
+ `- \`gh\` works for ${context.repo ?? "this repo"} only: issues, pull requests, comments. Its token is short-lived and refreshed for you.`,
741
+ "- Merging pull requests and deleting remote branches are not possible from here. A person merges."
742
+ ];
743
+ case "strict":
744
+ return [
745
+ "- This session runs in strict mode: GitHub is disabled on purpose and the runner holds no GitHub credentials. `gh` does not work, and `git fetch` fails for private repositories.",
746
+ "- Do not look for credentials or ask the user for a token. Local git work still works.",
747
+ "- When a task needs GitHub (an issue, a pull request, a comment, a review, a label), write out what should be posted and hand it to the user."
748
+ ];
749
+ case "off":
750
+ return [
751
+ "- GitHub access is unavailable in this session, so `gh` has no token. Local git work still works."
752
+ ];
753
+ }
754
+ }
755
+ function afterPushRule(github) {
756
+ return github === "on" ? "- After the user confirms it is pushed, `git fetch` and continue. Open the pull request with `gh pr create --head <branch>` if asked to." : "- After the user confirms it is pushed, continue on the same branch. The user opens the pull request.";
757
+ }
758
+ function runnerContextFromEnv(env) {
414
759
  return {
415
760
  sessionId: env.MAESTTRO_SESSION_ID,
416
761
  base: env.MAESTTRO_BASE,
417
762
  repo: env.MAESTTRO_REPO,
418
- githubAccess: env.MAESTTRO_GITHUB === "on"
763
+ github: githubMode(env.MAESTTRO_GITHUB)
419
764
  };
420
765
  }
421
- function sandboxInstructions(context) {
766
+ function runnerInstructions(context) {
422
767
  const base = context.base ?? "the default branch";
423
- const github = context.githubAccess ? [
424
- `- \`gh\` works for ${context.repo ?? "this repo"} only: issues, pull requests, comments. Its token is short-lived and refreshed for you.`,
425
- "- Merging pull requests and deleting remote branches are not possible from here. A person merges."
426
- ] : [
427
- "- GitHub access is unavailable in this session, so `gh` has no token. Local git work still works."
428
- ];
429
768
  return [
430
- "# Maesttro sandbox",
769
+ "# Maesttro runner",
431
770
  "",
432
- "You are running inside Maesttro's isolated sandbox container. These rules describe what the environment allows, so plan around them.",
771
+ "You are running inside Maesttro's isolated runner container. These rules describe what the environment allows, so plan around them.",
433
772
  "",
434
773
  "## Your workspace",
435
774
  "",
436
775
  `- The current directory is an isolated clone of the repository${context.sessionId ? ` for this session (\`${context.sessionId}\`)` : ""}, detached at \`${base}\`. It is the only checkout you can see or change; your main checkout is not here.`,
437
776
  "- Before your first commit, create a branch for the task: `git switch -c <short-descriptive-name>`. Keep one branch per task.",
438
777
  "- Work in this directory. Anything outside it is not persisted.",
439
- "- Commit on a branch you create here. The user pushes it from their machine with `maesttro push`; the sandbox cannot push, merge, or delete remote branches.",
778
+ "- Commit on a branch you create here. The user pushes it from their machine with `maesttro push`; the runner cannot push, merge, or delete remote branches.",
440
779
  "",
441
780
  "## Pushing",
442
781
  "",
443
- "- `git push` does not work here: the sandbox has read-only access to the remote. This is deliberate.",
782
+ "- `git push` does not work here: the runner has read-only access to the remote. This is deliberate.",
444
783
  '- When a branch is ready, commit everything, then tell the user: "Ready to push: <branch>. Run `maesttro push <branch>` from the repository root."',
445
- "- After the user confirms it is pushed, `git fetch` and continue. Open the pull request with `gh pr create --head <branch>` if asked to.",
784
+ afterPushRule(context.github),
446
785
  "",
447
786
  "## GitHub",
448
787
  "",
449
- ...github,
788
+ ...githubRules(context),
450
789
  ""
451
790
  ].join(`
452
791
  `);
453
792
  }
454
793
 
455
794
  // src/configure.ts
456
- var SANDBOX_MARKER = "/etc/maesttro/sandbox";
457
- function isObject(value) {
458
- return typeof value === "object" && value !== null && !Array.isArray(value);
459
- }
795
+ var RUNNER_MARKER = "/etc/maesttro/runner";
460
796
  function withoutComments(value) {
461
797
  return Object.fromEntries(Object.entries(value).filter(([key]) => !key.startsWith("_comment")).map(([key, inner]) => [
462
798
  key,
463
799
  isObject(inner) ? withoutComments(inner) : inner
464
800
  ]));
465
801
  }
466
- function mergeJson(existing, curated) {
467
- const merged = { ...existing };
468
- for (const [key, value] of Object.entries(curated)) {
469
- const current = merged[key];
470
- merged[key] = isObject(current) && isObject(value) ? mergeJson(current, value) : value;
471
- }
472
- return merged;
473
- }
474
- function readJson(path) {
475
- try {
476
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
477
- return isObject(parsed) ? parsed : {};
478
- } catch {
479
- return {};
480
- }
481
- }
482
802
  function applyProviderConfigs({
483
803
  home,
484
- configsDir = join3(DOCKERFILE_DIR, "configs")
804
+ configsDir = join4(DOCKERFILE_DIR, "configs")
485
805
  }) {
486
806
  const written = [];
487
807
  for (const [key, client] of Object.entries(CLIENTS)) {
488
- const source = join3(configsDir, key);
489
- if (!existsSync(source))
808
+ const source = join4(configsDir, key);
809
+ if (!existsSync2(source))
490
810
  continue;
491
- const targetDir = join3(home, client.configDir.replace(/^\/home\/agent\/?/, ""));
811
+ const targetDir = join4(home, client.configDir.replace(/^\/home\/agent\/?/, ""));
492
812
  for (const name of readdirSync(source)) {
493
813
  if (!name.endsWith(".json"))
494
814
  continue;
495
- const curated = withoutComments(readJson(join3(source, name)));
815
+ const curated = withoutComments(readJson(join4(source, name)));
496
816
  if (Object.keys(curated).length === 0)
497
817
  continue;
498
- const target = join3(targetDir, name);
499
- mkdirSync2(targetDir, { recursive: true });
818
+ const target = join4(targetDir, name);
819
+ mkdirSync4(targetDir, { recursive: true });
500
820
  const merged = mergeJson(readJson(target), curated);
501
- writeFileSync2(target, `${JSON.stringify(merged, null, 2)}
821
+ writeFileSync4(target, `${JSON.stringify(merged, null, 2)}
502
822
  `);
503
823
  written.push(target);
504
824
  }
@@ -511,33 +831,41 @@ function stageHostInstructions({
511
831
  context
512
832
  }) {
513
833
  for (const directory of STAGED_DIRECTORIES) {
514
- const source = join3(staging, directory);
515
- if (!existsSync(source))
834
+ const source = join4(staging, directory);
835
+ if (!existsSync2(source))
516
836
  continue;
517
837
  try {
518
- cpSync(source, join3(home, directory), { recursive: true, force: true });
838
+ cpSync2(source, join4(home, directory), { recursive: true, force: true });
519
839
  } catch {}
520
840
  }
521
- const instructions = sandboxInstructions(context);
841
+ const instructions = runnerInstructions(context);
522
842
  for (const file of INSTRUCTION_FILES) {
523
- const source = join3(staging, file);
524
- const hostCopy = existsSync(source) ? readFileSync2(source, "utf8") : "";
525
- const target = join3(home, file);
526
- mkdirSync2(dirname3(target), { recursive: true });
527
- writeFileSync2(target, hostCopy ? `${hostCopy.trimEnd()}
843
+ const source = join4(staging, file);
844
+ const hostCopy = existsSync2(source) ? readFileSync4(source, "utf8") : "";
845
+ const target = join4(home, file);
846
+ mkdirSync4(dirname5(target), { recursive: true });
847
+ writeFileSync4(target, hostCopy ? `${hostCopy.trimEnd()}
528
848
 
529
849
  ${instructions}` : instructions);
530
850
  }
531
851
  }
532
852
  function configure({
533
853
  home = homedir2(),
534
- inSandbox = existsSync(SANDBOX_MARKER),
535
- env = process.env
854
+ inRunner = existsSync2(RUNNER_MARKER),
855
+ env = process.env,
856
+ staging = HOST_STAGING_DIR,
857
+ clientConfigs
536
858
  } = {}) {
537
859
  applyProviderConfigs({ home });
538
- if (inSandbox) {
539
- stageHostInstructions({ home, context: sandboxContextFromEnv(env) });
540
- }
860
+ if (!inRunner)
861
+ return;
862
+ installClientConfigs({ home, source: clientConfigs });
863
+ injectOpencodeTuiState({ home });
864
+ stageHostInstructions({
865
+ home,
866
+ staging,
867
+ context: runnerContextFromEnv(env)
868
+ });
541
869
  }
542
870
 
543
871
  // src/doctor.ts
@@ -646,7 +974,7 @@ function purgeLegacyToken() {
646
974
  function hasLegacyToken() {
647
975
  return inVolume(DETECT_LEGACY_TOKEN).includes("FOUND");
648
976
  }
649
- var REVOKE_NOTICE = "Removed a long-lived GitHub token that an older maesttro stored in the sandbox volume. Revoke it on GitHub too: https://github.com/settings/tokens?type=beta";
977
+ var REVOKE_NOTICE = "Removed a long-lived GitHub token that an older maesttro stored in the runner volume. Revoke it on GitHub too: https://github.com/settings/tokens?type=beta";
650
978
  function purgeLegacyTokenOnce({
651
979
  config,
652
980
  saveConfig,
@@ -661,8 +989,8 @@ function purgeLegacyTokenOnce({
661
989
  }
662
990
 
663
991
  // src/repo.ts
664
- import { appendFileSync, mkdirSync as mkdirSync3, realpathSync } from "node:fs";
665
- import { join as join4 } from "node:path";
992
+ import { appendFileSync, mkdirSync as mkdirSync5, realpathSync } from "node:fs";
993
+ import { join as join5 } from "node:path";
666
994
  var MINIMUM_GIT = [2, 48];
667
995
  var WORKTREES_DIR = ".worktrees";
668
996
 
@@ -705,7 +1033,7 @@ function repoRoot(run, cwd) {
705
1033
  "--git-common-dir"
706
1034
  ]).out;
707
1035
  const root = realpathSync(top.out);
708
- if (realpathSync(commonDir) !== join4(root, ".git")) {
1036
+ if (realpathSync(commonDir) !== join5(root, ".git")) {
709
1037
  throw new LaunchError("This is a git worktree. Run maesttro from the main checkout; each session gets its own isolated clone.");
710
1038
  }
711
1039
  if (realpathSync(cwd) !== root) {
@@ -732,12 +1060,12 @@ function defaultBranch(run, root) {
732
1060
  throw new LaunchError("Can't find the default branch of origin. Run `git remote set-head origin --auto`.");
733
1061
  }
734
1062
  function ensureWorktreesDir(run, root) {
735
- mkdirSync3(join4(root, WORKTREES_DIR), { recursive: true });
1063
+ mkdirSync5(join5(root, WORKTREES_DIR), { recursive: true });
736
1064
  if (git(run, root, ["check-ignore", "-q", `${WORKTREES_DIR}/`]).ok)
737
1065
  return;
738
- const info = join4(root, ".git", "info");
739
- mkdirSync3(info, { recursive: true });
740
- appendFileSync(join4(info, "exclude"), `
1066
+ const info = join5(root, ".git", "info");
1067
+ mkdirSync5(info, { recursive: true });
1068
+ appendFileSync(join5(info, "exclude"), `
741
1069
  ${WORKTREES_DIR}/
742
1070
  `);
743
1071
  }
@@ -779,7 +1107,7 @@ async function check(label, probe) {
779
1107
  return false;
780
1108
  }
781
1109
  }
782
- function inSandbox(command, { withVolume = true } = {}) {
1110
+ function inRunner(command, { withVolume = true } = {}) {
783
1111
  const volume = withVolume ? `-v ${VOLUME}:/home/agent ` : "";
784
1112
  return execSync2(`docker run --rm ${hostIdentityFlags()} ${volume}${IMAGE} "${command}"`).toString();
785
1113
  }
@@ -802,7 +1130,7 @@ async function doctor() {
802
1130
  }
803
1131
  ],
804
1132
  [
805
- "Sandbox image built",
1133
+ "Runner image built",
806
1134
  () => `built ${execSync2(`docker image inspect ${IMAGE} --format "{{.Created}}"`).toString().trim()}`
807
1135
  ],
808
1136
  [
@@ -845,9 +1173,9 @@ async function doctor() {
845
1173
  }
846
1174
  ],
847
1175
  [
848
- "Personal SSH key NOT visible inside sandbox (isolation intact)",
1176
+ "Personal SSH key NOT visible inside runner (isolation intact)",
849
1177
  () => {
850
- const out = inSandbox("cat ~/.ssh/id_rsa 2>&1 || echo MISSING");
1178
+ const out = inRunner("cat ~/.ssh/id_rsa 2>&1 || echo MISSING");
851
1179
  if (!out.includes("MISSING") && !out.includes("No such file")) {
852
1180
  throw new Error("isolation broken");
853
1181
  }
@@ -856,7 +1184,7 @@ async function doctor() {
856
1184
  ...Object.values(CLIENTS).map((client) => [
857
1185
  `Client available: ${client.label}`,
858
1186
  () => {
859
- inSandbox(`which ${client.binary}`, { withVolume: false });
1187
+ inRunner(`which ${client.binary}`, { withVolume: false });
860
1188
  }
861
1189
  ])
862
1190
  ];
@@ -873,12 +1201,15 @@ async function doctor() {
873
1201
  }
874
1202
 
875
1203
  // src/help.ts
876
- var HELP = `maesttro — isolated sandbox for local coding agents
1204
+ var HELP = `maesttro — isolated runner for local coding agents
877
1205
 
878
1206
  Usage:
879
- maesttro Start a session from the repo root: new clone, default client (claude)
1207
+ maesttro Start a session from the repo root: new clone, default client (opencode)
880
1208
  maesttro <client> Same, with a specific client — see \`maesttro list\`
881
- maesttro resume <id> Continue a kept session in its clone
1209
+ maesttro --strict [client]
1210
+ Same, in strict mode: no GitHub token reaches the runner, so \`gh\` is off
1211
+ maesttro resume <id> Continue a kept session in its clone, in the mode it started with
1212
+ (\`--strict\` or \`--no-strict\` after the id changes it)
882
1213
  maesttro sessions List this repo's sessions (\`sessions remove <id>\` to drop one)
883
1214
  maesttro push [branch] Push a branch a session created, after showing what goes out
884
1215
  maesttro list Show available clients
@@ -886,57 +1217,57 @@ Usage:
886
1217
  maesttro login Log in to GitHub (device flow) so sessions get repo access
887
1218
  maesttro logout Remove the stored GitHub credentials
888
1219
  maesttro status Show GitHub login status
889
- maesttro configure Apply maesttro's client configs (also runs inside the sandbox)
890
- maesttro update Update the package and rebuild the image with refreshed client configs
1220
+ maesttro configure Apply maesttro's client configs (also runs inside the runner)
1221
+ maesttro update Update the package and rebuild the runner image with refreshed client configs
891
1222
  maesttro doctor Health check — Docker, image, volume, GitHub login, broker, isolation, clients
892
1223
  maesttro help Show this message
893
1224
  `;
894
1225
 
895
1226
  // src/launch.ts
896
1227
  import { execSync as execSync3, spawn } from "node:child_process";
897
- import { existsSync as existsSync3, rmSync as rmSync5 } from "node:fs";
1228
+ import { existsSync as existsSync4, rmSync as rmSync6 } from "node:fs";
898
1229
  import { constants, homedir as homedir4 } from "node:os";
899
- import { join as join10 } from "node:path";
1230
+ import { join as join11 } from "node:path";
900
1231
 
901
1232
  // src/backup.ts
902
1233
  import { createHash } from "node:crypto";
903
- import { mkdirSync as mkdirSync5, readdirSync as readdirSync2, rmSync as rmSync2 } from "node:fs";
904
- import { basename, join as join6 } from "node:path";
1234
+ import { mkdirSync as mkdirSync7, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
1235
+ import { basename, join as join7 } from "node:path";
905
1236
 
906
1237
  // src/maesttro-home.ts
907
1238
  import {
908
1239
  appendFileSync as appendFileSync2,
909
- mkdirSync as mkdirSync4,
910
- readFileSync as readFileSync3,
1240
+ mkdirSync as mkdirSync6,
1241
+ readFileSync as readFileSync5,
911
1242
  renameSync as renameSync2,
912
- writeFileSync as writeFileSync3
1243
+ writeFileSync as writeFileSync5
913
1244
  } from "node:fs";
914
1245
  import { homedir as homedir3 } from "node:os";
915
- import { join as join5 } from "node:path";
1246
+ import { join as join6 } from "node:path";
916
1247
  function maesttroHome() {
917
- return join5(homedir3(), ".config", "maesttro");
1248
+ return join6(homedir3(), ".config", "maesttro");
918
1249
  }
919
1250
  function configPath() {
920
- return join5(maesttroHome(), "config.json");
1251
+ return join6(maesttroHome(), "config.json");
921
1252
  }
922
1253
  function readMaesttroConfig() {
923
1254
  try {
924
- return JSON.parse(readFileSync3(configPath(), "utf8"));
1255
+ return JSON.parse(readFileSync5(configPath(), "utf8"));
925
1256
  } catch {
926
1257
  return {};
927
1258
  }
928
1259
  }
929
1260
  function writeMaesttroConfig(config) {
930
- mkdirSync4(maesttroHome(), { recursive: true, mode: 448 });
1261
+ mkdirSync6(maesttroHome(), { recursive: true, mode: 448 });
931
1262
  const temporary = `${configPath()}.tmp`;
932
- writeFileSync3(temporary, `${JSON.stringify(config, null, 2)}
1263
+ writeFileSync5(temporary, `${JSON.stringify(config, null, 2)}
933
1264
  `);
934
1265
  renameSync2(temporary, configPath());
935
1266
  }
936
- function fileSessionLog(path = join5(maesttroHome(), "maesttro.log")) {
1267
+ function fileSessionLog(path = join6(maesttroHome(), "maesttro.log")) {
937
1268
  return (event, detail) => {
938
1269
  try {
939
- mkdirSync4(maesttroHome(), { recursive: true, mode: 448 });
1270
+ mkdirSync6(maesttroHome(), { recursive: true, mode: 448 });
940
1271
  appendFileSync2(path, `${new Date().toISOString()} ${event}${detail ? ` ${detail}` : ""}
941
1272
  `);
942
1273
  } catch {}
@@ -949,7 +1280,7 @@ function shortHash(value) {
949
1280
  return createHash("sha256").update(value).digest("hex").slice(0, 8);
950
1281
  }
951
1282
  function backupDir(repoRoot, home = maesttroHome()) {
952
- return join6(home, "backups", `${basename(repoRoot)}-${shortHash(repoRoot)}`);
1283
+ return join7(home, "backups", `${basename(repoRoot)}-${shortHash(repoRoot)}`);
953
1284
  }
954
1285
  function refsFingerprint(refs) {
955
1286
  const sorted = Object.keys(refs).sort().map((ref) => `${ref} ${refs[ref]}`);
@@ -967,20 +1298,20 @@ function backupRepo({
967
1298
  }) {
968
1299
  if (Object.keys(refs).length === 0)
969
1300
  return;
970
- mkdirSync5(dir, { recursive: true, mode: 448 });
1301
+ mkdirSync7(dir, { recursive: true, mode: 448 });
971
1302
  const fingerprint = refsFingerprint(refs);
972
1303
  const existing = readdirSync2(dir).filter((name) => name.endsWith(".bundle")).sort();
973
1304
  const latest = existing.at(-1);
974
1305
  if (latest?.endsWith(`-${fingerprint}.bundle`)) {
975
- return { path: join6(dir, latest), reused: true };
1306
+ return { path: join7(dir, latest), reused: true };
976
1307
  }
977
1308
  const stamp = now.toISOString().replace(/[:.]/g, "-");
978
- const path = join6(dir, `${stamp}-${sessionId}-${fingerprint}.bundle`);
1309
+ const path = join7(dir, `${stamp}-${sessionId}-${fingerprint}.bundle`);
979
1310
  if (!git(run, repoRoot, ["bundle", "create", "--quiet", path, "--all"]).ok) {
980
1311
  return;
981
1312
  }
982
1313
  for (const stale of [...existing, basename(path)].sort().slice(0, -keep)) {
983
- rmSync2(join6(dir, stale), { force: true });
1314
+ rmSync3(join7(dir, stale), { force: true });
984
1315
  }
985
1316
  return { path, reused: false };
986
1317
  }
@@ -1065,20 +1396,20 @@ function startRefresher({
1065
1396
 
1066
1397
  // src/session-dir.ts
1067
1398
  import {
1068
- chmodSync as chmodSync2,
1069
- mkdirSync as mkdirSync6,
1070
- mkdtempSync,
1399
+ chmodSync as chmodSync3,
1400
+ mkdirSync as mkdirSync8,
1401
+ mkdtempSync as mkdtempSync2,
1071
1402
  renameSync as renameSync3,
1072
- rmSync as rmSync3,
1073
- writeFileSync as writeFileSync4
1403
+ rmSync as rmSync4,
1404
+ writeFileSync as writeFileSync6
1074
1405
  } from "node:fs";
1075
- import { tmpdir } from "node:os";
1076
- import { join as join7 } from "node:path";
1406
+ import { tmpdir as tmpdir2 } from "node:os";
1407
+ import { join as join8 } from "node:path";
1077
1408
  var CONTAINER_GH_CONFIG_DIR = "/home/agent/.config/gh-maesttro";
1078
1409
  function writeAtomically(path, content) {
1079
1410
  const temporary = `${path}.tmp`;
1080
- writeFileSync4(temporary, content, { mode: 420 });
1081
- chmodSync2(temporary, 420);
1411
+ writeFileSync6(temporary, content, { mode: 420 });
1412
+ chmodSync3(temporary, 420);
1082
1413
  renameSync3(temporary, path);
1083
1414
  }
1084
1415
  function hostsYaml(token, user) {
@@ -1091,26 +1422,27 @@ function hostsYaml(token, user) {
1091
1422
  ].join(`
1092
1423
  `);
1093
1424
  }
1094
- function createSessionDir(parent = tmpdir()) {
1095
- const root = mkdtempSync(join7(parent, "maesttro-"));
1096
- chmodSync2(root, 448);
1097
- const ghDir = join7(root, "gh");
1098
- mkdirSync6(ghDir);
1099
- chmodSync2(ghDir, 493);
1100
- writeAtomically(join7(ghDir, "config.yml"), `version: "1"
1425
+ function createSessionDir(parent = tmpdir2()) {
1426
+ const root = mkdtempSync2(join8(parent, "maesttro-"));
1427
+ chmodSync3(root, 448);
1428
+ const ghDir = join8(root, "gh");
1429
+ mkdirSync8(ghDir);
1430
+ chmodSync3(ghDir, 493);
1431
+ writeAtomically(join8(ghDir, "config.yml"), `version: "1"
1101
1432
  `);
1102
1433
  return {
1103
1434
  ghDir,
1104
- writeToken: (token, user) => writeAtomically(join7(ghDir, "hosts.yml"), hostsYaml(token, user)),
1105
- writeStatus: (status) => writeAtomically(join7(ghDir, "status"), `${status}
1435
+ writeToken: (token, user) => writeAtomically(join8(ghDir, "hosts.yml"), hostsYaml(token, user)),
1436
+ writeStatus: (status) => writeAtomically(join8(ghDir, "status"), `${status}
1106
1437
  `),
1107
- remove: () => rmSync3(root, { recursive: true, force: true })
1438
+ remove: () => rmSync4(root, { recursive: true, force: true })
1108
1439
  };
1109
1440
  }
1110
1441
 
1111
1442
  // src/github-session.ts
1112
1443
  var UNAVAILABLE = "GitHub access unavailable";
1113
1444
  var WITHOUT = "running without gh";
1445
+ var STRICT_NOTICE = "Strict mode: no GitHub token in the runner, so `gh` is off. Local git works; push with `maesttro push`.";
1114
1446
  function refusalNotice(code, repo) {
1115
1447
  switch (code) {
1116
1448
  case "broker_unreachable":
@@ -1153,6 +1485,7 @@ function errorCode(error) {
1153
1485
  function withoutGitHub(author, notice) {
1154
1486
  const env = {
1155
1487
  GH_CONFIG_DIR: CONTAINER_GH_CONFIG_DIR,
1488
+ GIT_TERMINAL_PROMPT: "0",
1156
1489
  ...identityEnv(author, undefined)
1157
1490
  };
1158
1491
  return {
@@ -1165,12 +1498,15 @@ function withoutGitHub(author, notice) {
1165
1498
  async function openGitHubSession({
1166
1499
  repo,
1167
1500
  author,
1501
+ strict = false,
1168
1502
  broker,
1169
1503
  login,
1170
1504
  log,
1171
1505
  fetch: fetchImpl = fetch,
1172
1506
  createDir = createSessionDir
1173
1507
  }) {
1508
+ if (strict)
1509
+ return withoutGitHub(author, STRICT_NOTICE);
1174
1510
  if (!repo)
1175
1511
  return withoutGitHub(author);
1176
1512
  const mint = async ({ interactive }) => {
@@ -1247,13 +1583,13 @@ function ask(question) {
1247
1583
  // src/session-clone.ts
1248
1584
  import {
1249
1585
  copyFileSync,
1250
- existsSync as existsSync2,
1586
+ existsSync as existsSync3,
1251
1587
  lstatSync,
1252
- mkdirSync as mkdirSync7,
1588
+ mkdirSync as mkdirSync9,
1253
1589
  readdirSync as readdirSync3,
1254
- readFileSync as readFileSync4
1590
+ readFileSync as readFileSync6
1255
1591
  } from "node:fs";
1256
- import { join as join8 } from "node:path";
1592
+ import { join as join9 } from "node:path";
1257
1593
 
1258
1594
  // src/sanitize.ts
1259
1595
  function stripControl(text) {
@@ -1345,14 +1681,14 @@ function dirents(dir) {
1345
1681
  }
1346
1682
  }
1347
1683
  function cloneGitDir(clonePath) {
1348
- const gitDir = join8(clonePath, ".git");
1684
+ const gitDir = join9(clonePath, ".git");
1349
1685
  return realDir(gitDir) ? gitDir : undefined;
1350
1686
  }
1351
1687
  function readSha(path) {
1352
1688
  try {
1353
1689
  if (!lstatSync(path).isFile())
1354
1690
  return;
1355
- const text = readFileSync4(path, "utf8").trim();
1691
+ const text = readFileSync6(path, "utf8").trim();
1356
1692
  return SHA.test(text) ? text : undefined;
1357
1693
  } catch {
1358
1694
  return;
@@ -1362,7 +1698,7 @@ function packedRefs(gitDir) {
1362
1698
  const refs = new Map;
1363
1699
  let text;
1364
1700
  try {
1365
- text = readFileSync4(join8(gitDir, "packed-refs"), "utf8");
1701
+ text = readFileSync6(join9(gitDir, "packed-refs"), "utf8");
1366
1702
  } catch {
1367
1703
  return refs;
1368
1704
  }
@@ -1387,12 +1723,12 @@ function cloneBranches(clonePath) {
1387
1723
  continue;
1388
1724
  const name = prefix ? `${prefix}/${entry.name}` : entry.name;
1389
1725
  if (entry.isDirectory())
1390
- walk(join8(dir, entry.name), name);
1726
+ walk(join9(dir, entry.name), name);
1391
1727
  else if (entry.isFile())
1392
1728
  names.add(name);
1393
1729
  }
1394
1730
  };
1395
- walk(join8(gitDir, "refs", "heads"), "");
1731
+ walk(join9(gitDir, "refs", "heads"), "");
1396
1732
  for (const ref of packedRefs(gitDir).keys()) {
1397
1733
  if (ref.startsWith(HEADS_PREFIX))
1398
1734
  names.add(ref.slice(HEADS_PREFIX.length));
@@ -1405,7 +1741,7 @@ function branchTip(clonePath, branch) {
1405
1741
  const gitDir = cloneGitDir(clonePath);
1406
1742
  if (!gitDir)
1407
1743
  return;
1408
- return readSha(join8(gitDir, "refs", "heads", branch)) ?? packedRefs(gitDir).get(`${HEADS_PREFIX}${branch}`);
1744
+ return readSha(join9(gitDir, "refs", "heads", branch)) ?? packedRefs(gitDir).get(`${HEADS_PREFIX}${branch}`);
1409
1745
  }
1410
1746
  function cloneHead(clonePath) {
1411
1747
  const gitDir = cloneGitDir(clonePath);
@@ -1413,9 +1749,9 @@ function cloneHead(clonePath) {
1413
1749
  return;
1414
1750
  let text;
1415
1751
  try {
1416
- if (!lstatSync(join8(gitDir, "HEAD")).isFile())
1752
+ if (!lstatSync(join9(gitDir, "HEAD")).isFile())
1417
1753
  return;
1418
- text = readFileSync4(join8(gitDir, "HEAD"), "utf8").trim();
1754
+ text = readFileSync6(join9(gitDir, "HEAD"), "utf8").trim();
1419
1755
  } catch {
1420
1756
  return;
1421
1757
  }
@@ -1436,31 +1772,31 @@ function copyObjectFiles(fromDir, intoDir) {
1436
1772
  for (const entry of dirents(fromDir)) {
1437
1773
  if (!entry.isFile())
1438
1774
  continue;
1439
- const dest = join8(intoDir, entry.name);
1440
- if (existsSync2(dest))
1775
+ const dest = join9(intoDir, entry.name);
1776
+ if (existsSync3(dest))
1441
1777
  continue;
1442
1778
  if (!created) {
1443
- mkdirSync7(intoDir, { recursive: true });
1779
+ mkdirSync9(intoDir, { recursive: true });
1444
1780
  created = true;
1445
1781
  }
1446
- copyFileSync(join8(fromDir, entry.name), dest);
1782
+ copyFileSync(join9(fromDir, entry.name), dest);
1447
1783
  }
1448
1784
  }
1449
1785
  function importObjects(run, root, clonePath, tip) {
1450
1786
  const gitDir = cloneGitDir(clonePath);
1451
1787
  if (!gitDir)
1452
1788
  return false;
1453
- const objects = join8(gitDir, "objects");
1789
+ const objects = join9(gitDir, "objects");
1454
1790
  if (!realDir(objects))
1455
1791
  return false;
1456
- const into = join8(root, ".git", "objects");
1792
+ const into = join9(root, ".git", "objects");
1457
1793
  for (const entry of dirents(objects)) {
1458
1794
  if (entry.name === "info" || entry.isSymbolicLink() || !entry.isDirectory()) {
1459
1795
  continue;
1460
1796
  }
1461
1797
  if (!OBJECT_SUBDIR.test(entry.name) && entry.name !== "pack")
1462
1798
  continue;
1463
- copyObjectFiles(join8(objects, entry.name), join8(into, entry.name));
1799
+ copyObjectFiles(join9(objects, entry.name), join9(into, entry.name));
1464
1800
  }
1465
1801
  return git(run, root, ["cat-file", "-e", `${tip}^{commit}`]).ok;
1466
1802
  }
@@ -1468,36 +1804,36 @@ function importObjects(run, root, clonePath, tip) {
1468
1804
  // src/sessions.ts
1469
1805
  import { randomBytes } from "node:crypto";
1470
1806
  import {
1471
- mkdirSync as mkdirSync8,
1807
+ mkdirSync as mkdirSync10,
1472
1808
  readdirSync as readdirSync4,
1473
- readFileSync as readFileSync5,
1809
+ readFileSync as readFileSync7,
1474
1810
  renameSync as renameSync4,
1475
- rmSync as rmSync4,
1476
- writeFileSync as writeFileSync5
1811
+ rmSync as rmSync5,
1812
+ writeFileSync as writeFileSync7
1477
1813
  } from "node:fs";
1478
- import { join as join9 } from "node:path";
1814
+ import { join as join10 } from "node:path";
1479
1815
  function newSessionId() {
1480
1816
  return `maesttro-${randomBytes(4).toString("hex")}`;
1481
1817
  }
1482
1818
  function sessionsDir(home = maesttroHome()) {
1483
- return join9(home, "sessions");
1819
+ return join10(home, "sessions");
1484
1820
  }
1485
1821
  function saveSession(record, dir = sessionsDir()) {
1486
- mkdirSync8(dir, { recursive: true, mode: 448 });
1487
- const path = join9(dir, `${record.id}.json`);
1488
- writeFileSync5(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
1822
+ mkdirSync10(dir, { recursive: true, mode: 448 });
1823
+ const path = join10(dir, `${record.id}.json`);
1824
+ writeFileSync7(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
1489
1825
  `);
1490
1826
  renameSync4(`${path}.tmp`, path);
1491
1827
  }
1492
1828
  function readSession(id, dir = sessionsDir()) {
1493
1829
  try {
1494
- return JSON.parse(readFileSync5(join9(dir, `${id}.json`), "utf8"));
1830
+ return JSON.parse(readFileSync7(join10(dir, `${id}.json`), "utf8"));
1495
1831
  } catch {
1496
1832
  return;
1497
1833
  }
1498
1834
  }
1499
1835
  function deleteSession(id, dir = sessionsDir()) {
1500
- rmSync4(join9(dir, `${id}.json`), { force: true });
1836
+ rmSync5(join10(dir, `${id}.json`), { force: true });
1501
1837
  }
1502
1838
  function sessionsForRepo(repoRoot, dir = sessionsDir()) {
1503
1839
  let names;
@@ -1555,19 +1891,25 @@ function sessionMounts({
1555
1891
  packageRoot,
1556
1892
  hostHome,
1557
1893
  stagedPaths,
1894
+ clientConfigs,
1558
1895
  sharedSkills
1559
1896
  }) {
1560
- const objects = join10(repoRoot, ".git", "objects");
1897
+ const objects = join11(repoRoot, ".git", "objects");
1561
1898
  const mounts = [
1562
1899
  { host: VOLUME, container: "/home/agent" },
1563
1900
  { host: objects, container: objects, readOnly: true },
1564
1901
  { host: sessionDir, container: CONTAINER_REPO },
1565
1902
  { host: packageRoot, container: CONTAINER_PACKAGE, readOnly: true },
1566
1903
  ...stagedPaths.map((path) => ({
1567
- host: join10(hostHome, path),
1904
+ host: join11(hostHome, path),
1568
1905
  container: `${HOST_STAGING_DIR}/${path}`,
1569
1906
  readOnly: true
1570
- }))
1907
+ })),
1908
+ {
1909
+ host: clientConfigs,
1910
+ container: CONTAINER_CLIENT_CONFIGS,
1911
+ readOnly: true
1912
+ }
1571
1913
  ];
1572
1914
  if (sharedSkills) {
1573
1915
  mounts.push({
@@ -1616,12 +1958,12 @@ function ensureReady() {
1616
1958
  execSync3(`docker image inspect ${IMAGE}`, { stdio: "ignore" });
1617
1959
  execSync3(`docker volume inspect ${VOLUME}`, { stdio: "ignore" });
1618
1960
  } catch {
1619
- throw new LaunchError("Sandbox isn't set up yet. Run `maesttro setup` first.");
1961
+ throw new LaunchError("Runner isn't set up yet. Run `maesttro setup` first.");
1620
1962
  }
1621
1963
  }
1622
1964
  function sharedSkillsDir() {
1623
- const dir = process.env.AGENT_SHARED_SKILLS ?? join10(homedir4(), "agent-shared", "skills");
1624
- return existsSync3(dir) ? dir : undefined;
1965
+ const dir = process.env.AGENT_SHARED_SKILLS ?? join11(homedir4(), "agent-shared", "skills");
1966
+ return existsSync4(dir) ? dir : undefined;
1625
1967
  }
1626
1968
  async function resolveAuthor(workspace) {
1627
1969
  const fromRepo = workspaceAuthor(runCommand, workspace);
@@ -1632,7 +1974,7 @@ async function resolveAuthor(workspace) {
1632
1974
  return config.author;
1633
1975
  if (!process.stdin.isTTY)
1634
1976
  return;
1635
- console.log("No git identity found for this repo. Commits in the sandbox need an author (stored in ~/.config/maesttro/config.json).");
1977
+ console.log("No git identity found for this repo. Commits in the runner need an author (stored in ~/.config/maesttro/config.json).");
1636
1978
  const name = await ask("Author name: ");
1637
1979
  const email = await ask("Author email: ");
1638
1980
  if (!name || !email)
@@ -1650,7 +1992,7 @@ function startSession(run, root) {
1650
1992
  if (!baseSha)
1651
1993
  throw new LaunchError(`Can't resolve ${base}.`);
1652
1994
  const id = newSessionId();
1653
- const sessionDir = join10(root, WORKTREES_DIR, id);
1995
+ const sessionDir = join11(root, WORKTREES_DIR, id);
1654
1996
  createSessionClone(run, root, sessionDir, baseSha);
1655
1997
  return {
1656
1998
  id,
@@ -1667,7 +2009,7 @@ function resumeSession(root, id) {
1667
2009
  if (!session || session.repoRoot !== root) {
1668
2010
  throw new LaunchError(`No session ${id} in this repo. See \`maesttro sessions\`.`);
1669
2011
  }
1670
- if (!existsSync3(session.worktree)) {
2012
+ if (!existsSync4(session.worktree)) {
1671
2013
  throw new LaunchError(`The clone for ${id} is gone: ${session.worktree}`);
1672
2014
  }
1673
2015
  return session;
@@ -1690,13 +2032,13 @@ function preserveBranches(run, session) {
1690
2032
  return preserved;
1691
2033
  }
1692
2034
  function finishSession(run, session) {
1693
- if (!existsSync3(session.worktree)) {
2035
+ if (!existsSync4(session.worktree)) {
1694
2036
  deleteSession(session.id);
1695
2037
  return;
1696
2038
  }
1697
2039
  const preserved = preserveBranches(run, session);
1698
2040
  if (!cloneHasWork(session.worktree, session.baseSha)) {
1699
- rmSync5(session.worktree, { recursive: true, force: true });
2041
+ rmSync6(session.worktree, { recursive: true, force: true });
1700
2042
  deleteSession(session.id);
1701
2043
  return;
1702
2044
  }
@@ -1710,6 +2052,18 @@ Session ${session.id} kept. Continue with \`maesttro resume ${session.id}\`.`);
1710
2052
  console.log("You have commits on a detached HEAD. Resume and `git switch -c <name>` to keep them.");
1711
2053
  }
1712
2054
  }
2055
+ function isStrict(requested, session) {
2056
+ return requested ?? session.strict ?? false;
2057
+ }
2058
+ function runnerGitHubMode({
2059
+ strict,
2060
+ repo,
2061
+ notice
2062
+ }) {
2063
+ if (strict)
2064
+ return "strict";
2065
+ return notice || !repo ? "off" : "on";
2066
+ }
1713
2067
  function exitCodeOf(code, signal) {
1714
2068
  if (code !== null)
1715
2069
  return code;
@@ -1718,7 +2072,8 @@ function exitCodeOf(code, signal) {
1718
2072
  var FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
1719
2073
  async function launch({
1720
2074
  args,
1721
- resume
2075
+ resume,
2076
+ strict: requestedStrict
1722
2077
  }) {
1723
2078
  const run = runCommand;
1724
2079
  ensureReady();
@@ -1736,9 +2091,14 @@ async function launch({
1736
2091
  refs: snapshotRefs(run, root),
1737
2092
  sessionId: started.id
1738
2093
  });
1739
- const saved = { ...started, backup: started.backup ?? backup?.path };
2094
+ const strict = isStrict(requestedStrict, started);
2095
+ const saved = {
2096
+ ...started,
2097
+ backup: started.backup ?? backup?.path,
2098
+ strict
2099
+ };
1740
2100
  saveSession(saved);
1741
- console.log(`Session ${saved.id}: clone .worktrees/${saved.id} at ${saved.base}${backup ? `, backup ${backup.reused ? "reused" : "written"}` : ""}.`);
2101
+ console.log(`Session ${saved.id}${strict ? " (strict)" : ""}: clone .worktrees/${saved.id} at ${saved.base}${backup ? `, backup ${backup.reused ? "reused" : "written"}` : ""}.`);
1742
2102
  const repo = workspaceRepo(run, root);
1743
2103
  if (repo && !resume) {
1744
2104
  git(run, saved.worktree, [
@@ -1751,6 +2111,7 @@ async function launch({
1751
2111
  const github = await openGitHubSession({
1752
2112
  repo,
1753
2113
  author: await resolveAuthor(root),
2114
+ strict,
1754
2115
  broker: brokerUrl(),
1755
2116
  login: {
1756
2117
  clientId: githubAppClientId(),
@@ -1762,19 +2123,28 @@ async function launch({
1762
2123
  if (github.notice)
1763
2124
  console.log(github.notice);
1764
2125
  const hostHome = homedir4();
2126
+ const clientConfigs = buildClientConfigs({
2127
+ host: readHostClientConfigs({ home: hostHome }),
2128
+ url: maesttroUrl()
2129
+ });
2130
+ if (clientConfigs.removed.length > 0) {
2131
+ console.log(`Kept out of the runner as secrets: ${clientConfigs.removed.join(", ")}`);
2132
+ }
2133
+ const clientConfigDir = writeClientConfigDir(clientConfigs.files);
1765
2134
  const { mounts, workdir } = sessionMounts({
1766
2135
  repoRoot: root,
1767
2136
  sessionDir: saved.worktree,
1768
2137
  packageRoot: PACKAGE_ROOT,
1769
2138
  hostHome,
1770
- stagedPaths: STAGED_PATHS.filter((path) => existsSync3(join10(hostHome, path))),
2139
+ stagedPaths: STAGED_PATHS.filter((path) => existsSync4(join11(hostHome, path))),
2140
+ clientConfigs: clientConfigDir.dir,
1771
2141
  sharedSkills: sharedSkillsDir()
1772
2142
  });
1773
2143
  const sessionEnv = {
1774
2144
  ...github.env,
1775
2145
  MAESTTRO_SESSION_ID: saved.id,
1776
2146
  MAESTTRO_BASE: saved.base,
1777
- MAESTTRO_GITHUB: github.notice || !repo ? "off" : "on",
2147
+ MAESTTRO_GITHUB: runnerGitHubMode({ strict, repo, notice: github.notice }),
1778
2148
  ...hostIdentityEnv(),
1779
2149
  ...repo ? { MAESTTRO_REPO: repo } : {}
1780
2150
  };
@@ -1801,6 +2171,7 @@ async function launch({
1801
2171
  for (const signal of FORWARDED_SIGNALS)
1802
2172
  process.off(signal, forward);
1803
2173
  await github.close();
2174
+ clientConfigDir.remove();
1804
2175
  finishSession(run, saved);
1805
2176
  process.exit(exitCode);
1806
2177
  }
@@ -1925,7 +2296,7 @@ Push? [y/N] `);
1925
2296
  }
1926
2297
 
1927
2298
  // src/sessions-command.ts
1928
- import { existsSync as existsSync4, rmSync as rmSync6 } from "node:fs";
2299
+ import { existsSync as existsSync5, rmSync as rmSync7 } from "node:fs";
1929
2300
  function sessionsCommand({ args }) {
1930
2301
  const run = runCommand;
1931
2302
  const root = repoRoot(run, process.cwd());
@@ -1937,7 +2308,7 @@ function sessionsCommand({ args }) {
1937
2308
  process.exitCode = 1;
1938
2309
  return;
1939
2310
  }
1940
- if (existsSync4(session.worktree)) {
2311
+ if (existsSync5(session.worktree)) {
1941
2312
  const preserved = preserveBranches(run, session);
1942
2313
  const head = cloneHead(session.worktree);
1943
2314
  if (preserved.length === 0 && head?.detached !== undefined && head.detached !== session.baseSha) {
@@ -1945,7 +2316,7 @@ function sessionsCommand({ args }) {
1945
2316
  process.exitCode = 1;
1946
2317
  return;
1947
2318
  }
1948
- rmSync6(session.worktree, { recursive: true, force: true });
2319
+ rmSync7(session.worktree, { recursive: true, force: true });
1949
2320
  const kept = preserved.length > 0 ? ` Its branches are kept as refs/maesttro/${session.id}/*.` : "";
1950
2321
  console.log(`Removed session ${session.id}.${kept}`);
1951
2322
  } else {
@@ -1960,9 +2331,10 @@ function sessionsCommand({ args }) {
1960
2331
  return;
1961
2332
  }
1962
2333
  for (const session of sessions) {
1963
- const branches = existsSync4(session.worktree) ? cloneBranches(session.worktree) : undefined;
2334
+ const branches = existsSync5(session.worktree) ? cloneBranches(session.worktree) : undefined;
1964
2335
  const where = branches === undefined ? "clone missing" : branches.length > 0 ? `branches: ${branches.join(", ")}` : "no branches yet";
1965
- console.log(` ${session.id} ${session.createdAt.slice(0, 16)} ${where}`);
2336
+ const mode = session.strict ? " strict" : "";
2337
+ console.log(` ${session.id} ${session.createdAt.slice(0, 16)} ${where}${mode}`);
1966
2338
  }
1967
2339
  console.log("\n`maesttro resume <id>` continues one; `maesttro sessions remove <id>` removes its clone.");
1968
2340
  }
@@ -1984,7 +2356,7 @@ async function setup({ args }) {
1984
2356
  console.log(`== maesttro setup ==
1985
2357
  `);
1986
2358
  ensureDocker();
1987
- console.log("-> Building sandbox image (this pulls node:22-slim + installs CLIs, first run only)...");
2359
+ console.log("-> Building runner image (this pulls node:22-slim + installs CLIs, first run only)...");
1988
2360
  run(`docker build -t ${IMAGE} "${DOCKERFILE_DIR}"`);
1989
2361
  console.log(`
1990
2362
  -> Ensuring persistent volume exists...`);
@@ -2010,10 +2382,10 @@ async function setup({ args }) {
2010
2382
 
2011
2383
  // src/update.ts
2012
2384
  import { execSync as execSync5 } from "node:child_process";
2013
- import { readFileSync as readFileSync6 } from "node:fs";
2014
- import { join as join11 } from "node:path";
2385
+ import { readFileSync as readFileSync8 } from "node:fs";
2386
+ import { join as join12 } from "node:path";
2015
2387
  function installedPackageName() {
2016
- const manifest = JSON.parse(readFileSync6(join11(PACKAGE_ROOT, "package.json"), "utf8"));
2388
+ const manifest = JSON.parse(readFileSync8(join12(PACKAGE_ROOT, "package.json"), "utf8"));
2017
2389
  return manifest.name;
2018
2390
  }
2019
2391
  function update() {
@@ -2023,11 +2395,11 @@ function update() {
2023
2395
  console.log(`-> Updating ${packageName} package...`);
2024
2396
  execSync5(`npm i -g ${packageName}@latest`, { stdio: "inherit" });
2025
2397
  console.log(`
2026
- -> Rebuilding sandbox image with refreshed client configs...`);
2398
+ -> Rebuilding runner image with refreshed client configs...`);
2027
2399
  execSync5(`docker build -t ${IMAGE} "${DOCKERFILE_DIR}"`, { stdio: "inherit" });
2028
2400
  console.log(`
2029
2401
  Update complete. Bot identity and your project files are untouched —`);
2030
- console.log("only the image (client versions + curated configs) was refreshed.");
2402
+ console.log("only the runner image (client versions + curated configs) was refreshed.");
2031
2403
  }
2032
2404
 
2033
2405
  // src/main.ts
@@ -2044,11 +2416,48 @@ var defaultCommands = {
2044
2416
  status,
2045
2417
  print: (text) => console.log(text)
2046
2418
  };
2419
+ var MODE_FLAGS = {
2420
+ "--strict": true,
2421
+ "--no-strict": false
2422
+ };
2423
+ function takeModeFlags(args) {
2424
+ let strict;
2425
+ let index = 0;
2426
+ while (index < args.length && Object.hasOwn(MODE_FLAGS, args[index] ?? "")) {
2427
+ strict = MODE_FLAGS[args[index] ?? ""];
2428
+ index++;
2429
+ }
2430
+ return {
2431
+ ...strict === undefined ? {} : { strict },
2432
+ rest: args.slice(index)
2433
+ };
2434
+ }
2435
+ var NOT_LAUNCHING = new Set([
2436
+ "setup",
2437
+ "update",
2438
+ "doctor",
2439
+ "login",
2440
+ "logout",
2441
+ "status",
2442
+ "push",
2443
+ "sessions",
2444
+ "configure",
2445
+ "list",
2446
+ "help",
2447
+ "--help",
2448
+ "-h"
2449
+ ]);
2047
2450
  async function main(argv, commands = defaultCommands) {
2048
- const [command, ...rest] = argv;
2451
+ const leading = takeModeFlags(argv);
2452
+ const [command, ...rest] = leading.rest;
2453
+ const mode = leading.strict === undefined ? {} : { strict: leading.strict };
2454
+ if (leading.strict !== undefined && command !== undefined && NOT_LAUNCHING.has(command)) {
2455
+ commands.print(`--strict and --no-strict apply to launching a session, not to \`${command}\`.`);
2456
+ return;
2457
+ }
2049
2458
  switch (command) {
2050
2459
  case undefined:
2051
- await commands.launch({ args: [] });
2460
+ await commands.launch({ args: [], ...mode });
2052
2461
  return;
2053
2462
  case "setup":
2054
2463
  await commands.setup({ args: rest });
@@ -2075,12 +2484,19 @@ async function main(argv, commands = defaultCommands) {
2075
2484
  await commands.sessions({ args: rest });
2076
2485
  return;
2077
2486
  case "resume": {
2078
- const [id, ...clientArgs] = rest;
2487
+ const beforeId = takeModeFlags(rest);
2488
+ const [id, ...afterId] = beforeId.rest;
2489
+ const afterIdFlags = takeModeFlags(afterId);
2490
+ const strict = afterIdFlags.strict ?? beforeId.strict ?? leading.strict;
2079
2491
  if (!id) {
2080
- commands.print("Usage: maesttro resume <session-id> [client] [args...]");
2492
+ commands.print("Usage: maesttro resume <session-id> [--strict|--no-strict] [client] [args...]");
2081
2493
  return;
2082
2494
  }
2083
- await commands.launch({ args: clientArgs, resume: id });
2495
+ await commands.launch({
2496
+ args: afterIdFlags.rest,
2497
+ resume: id,
2498
+ ...strict === undefined ? {} : { strict }
2499
+ });
2084
2500
  return;
2085
2501
  }
2086
2502
  case "configure":
@@ -2095,7 +2511,7 @@ async function main(argv, commands = defaultCommands) {
2095
2511
  commands.print(HELP);
2096
2512
  return;
2097
2513
  default:
2098
- await commands.launch({ args: argv });
2514
+ await commands.launch({ args: leading.rest, ...mode });
2099
2515
  }
2100
2516
  }
2101
2517