maesttro 0.2.1 → 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/README.md +12 -2
- package/dist/maesttro.js +112 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,9 +58,10 @@ maesttro doctor # health check anytime
|
|
|
58
58
|
|
|
59
59
|
maesttro # from a repo root: new session clone + default client (opencode)
|
|
60
60
|
maesttro opencode # same, with opencode
|
|
61
|
+
maesttro --strict [client] # same, with no GitHub token in the runner (no `gh`)
|
|
61
62
|
maesttro bash # plain shell inside the runner, for debugging
|
|
62
63
|
maesttro sessions # list this repo's kept sessions
|
|
63
|
-
maesttro resume <id> # continue one
|
|
64
|
+
maesttro resume <id> # continue one, in the mode it started with
|
|
64
65
|
maesttro push [branch] # push a branch a session created (shows what goes out, asks first)
|
|
65
66
|
|
|
66
67
|
maesttro update # bump the package + rebuild image with refreshed configs
|
|
@@ -106,6 +107,15 @@ comments). It acts as the Maesttro GitHub App's bot:
|
|
|
106
107
|
- Commits are authored by you (your repo's `user.name` / `user.email`) and committed by the bot.
|
|
107
108
|
- The broker is reached over the tailnet. Off the tailnet, the session still starts, just without `gh`.
|
|
108
109
|
|
|
110
|
+
The write side is coarse. GitHub has no finer grant than "issues" and "pull
|
|
111
|
+
requests", so the same token that lets the agent comment and open PRs also lets
|
|
112
|
+
it edit label definitions and milestones, edit or close other people's issues and
|
|
113
|
+
comments, and submit reviews. If that is more than you want to hand an agent,
|
|
114
|
+
start the session with `maesttro --strict`: no token is minted and none reaches
|
|
115
|
+
the runner. Local git and `maesttro push` work as usual; `gh` and `git fetch` on a
|
|
116
|
+
private repo don't, and the agent hands GitHub actions to you instead. Resuming a
|
|
117
|
+
strict session stays strict; `maesttro resume <id> --no-strict` lifts it.
|
|
118
|
+
|
|
109
119
|
## Your instructions come along
|
|
110
120
|
|
|
111
121
|
At every container start, `maesttro configure` copies your host
|
|
@@ -169,7 +179,7 @@ untouched by an update.
|
|
|
169
179
|
| Mount | Source | Purpose |
|
|
170
180
|
|---|---|---|
|
|
171
181
|
| `/home/agent` | `maesttro-home` named volume | Per-client session state. Survives across runs, lives only in Docker. Holds no GitHub credential. |
|
|
172
|
-
| `/home/agent/.config/gh-maesttro` (read-only) | per-session temp dir on host | The session's GitHub token (`hosts.yml`). The CLI refreshes it before its 1 h expiry and revokes it on exit. |
|
|
182
|
+
| `/home/agent/.config/gh-maesttro` (read-only) | per-session temp dir on host | The session's GitHub token (`hosts.yml`). The CLI refreshes it before its 1 h expiry and revokes it on exit. Not mounted in `--strict` sessions, or when GitHub access is unavailable. |
|
|
173
183
|
| `<repo>/.git/objects` (read-only, same absolute path) | your repo's object store | Base history, borrowed by the clone through its `alternates`. The real `.git` is never mounted. |
|
|
174
184
|
| `/workspace` | the session clone (`.worktrees/<id>`) | The working directory: a `git clone --shared` detached at the default branch. |
|
|
175
185
|
| `/opt/maesttro`, `/opt/maesttro-host` (read-only) | this package; your instruction files | Used by `maesttro configure` at container start. |
|
package/dist/maesttro.js
CHANGED
|
@@ -730,22 +730,41 @@ var IMAGE = "maesttro-runner:latest";
|
|
|
730
730
|
var VOLUME = process.env.MAESTTRO_VOLUME?.trim() || "maesttro-home";
|
|
731
731
|
|
|
732
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
|
+
}
|
|
733
758
|
function runnerContextFromEnv(env) {
|
|
734
759
|
return {
|
|
735
760
|
sessionId: env.MAESTTRO_SESSION_ID,
|
|
736
761
|
base: env.MAESTTRO_BASE,
|
|
737
762
|
repo: env.MAESTTRO_REPO,
|
|
738
|
-
|
|
763
|
+
github: githubMode(env.MAESTTRO_GITHUB)
|
|
739
764
|
};
|
|
740
765
|
}
|
|
741
766
|
function runnerInstructions(context) {
|
|
742
767
|
const base = context.base ?? "the default branch";
|
|
743
|
-
const github = context.githubAccess ? [
|
|
744
|
-
`- \`gh\` works for ${context.repo ?? "this repo"} only: issues, pull requests, comments. Its token is short-lived and refreshed for you.`,
|
|
745
|
-
"- Merging pull requests and deleting remote branches are not possible from here. A person merges."
|
|
746
|
-
] : [
|
|
747
|
-
"- GitHub access is unavailable in this session, so `gh` has no token. Local git work still works."
|
|
748
|
-
];
|
|
749
768
|
return [
|
|
750
769
|
"# Maesttro runner",
|
|
751
770
|
"",
|
|
@@ -762,11 +781,11 @@ function runnerInstructions(context) {
|
|
|
762
781
|
"",
|
|
763
782
|
"- `git push` does not work here: the runner has read-only access to the remote. This is deliberate.",
|
|
764
783
|
'- When a branch is ready, commit everything, then tell the user: "Ready to push: <branch>. Run `maesttro push <branch>` from the repository root."',
|
|
765
|
-
|
|
784
|
+
afterPushRule(context.github),
|
|
766
785
|
"",
|
|
767
786
|
"## GitHub",
|
|
768
787
|
"",
|
|
769
|
-
...
|
|
788
|
+
...githubRules(context),
|
|
770
789
|
""
|
|
771
790
|
].join(`
|
|
772
791
|
`);
|
|
@@ -1187,7 +1206,10 @@ var HELP = `maesttro — isolated runner for local coding agents
|
|
|
1187
1206
|
Usage:
|
|
1188
1207
|
maesttro Start a session from the repo root: new clone, default client (opencode)
|
|
1189
1208
|
maesttro <client> Same, with a specific client — see \`maesttro list\`
|
|
1190
|
-
maesttro
|
|
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)
|
|
1191
1213
|
maesttro sessions List this repo's sessions (\`sessions remove <id>\` to drop one)
|
|
1192
1214
|
maesttro push [branch] Push a branch a session created, after showing what goes out
|
|
1193
1215
|
maesttro list Show available clients
|
|
@@ -1420,6 +1442,7 @@ function createSessionDir(parent = tmpdir2()) {
|
|
|
1420
1442
|
// src/github-session.ts
|
|
1421
1443
|
var UNAVAILABLE = "GitHub access unavailable";
|
|
1422
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`.";
|
|
1423
1446
|
function refusalNotice(code, repo) {
|
|
1424
1447
|
switch (code) {
|
|
1425
1448
|
case "broker_unreachable":
|
|
@@ -1462,6 +1485,7 @@ function errorCode(error) {
|
|
|
1462
1485
|
function withoutGitHub(author, notice) {
|
|
1463
1486
|
const env = {
|
|
1464
1487
|
GH_CONFIG_DIR: CONTAINER_GH_CONFIG_DIR,
|
|
1488
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
1465
1489
|
...identityEnv(author, undefined)
|
|
1466
1490
|
};
|
|
1467
1491
|
return {
|
|
@@ -1474,12 +1498,15 @@ function withoutGitHub(author, notice) {
|
|
|
1474
1498
|
async function openGitHubSession({
|
|
1475
1499
|
repo,
|
|
1476
1500
|
author,
|
|
1501
|
+
strict = false,
|
|
1477
1502
|
broker,
|
|
1478
1503
|
login,
|
|
1479
1504
|
log,
|
|
1480
1505
|
fetch: fetchImpl = fetch,
|
|
1481
1506
|
createDir = createSessionDir
|
|
1482
1507
|
}) {
|
|
1508
|
+
if (strict)
|
|
1509
|
+
return withoutGitHub(author, STRICT_NOTICE);
|
|
1483
1510
|
if (!repo)
|
|
1484
1511
|
return withoutGitHub(author);
|
|
1485
1512
|
const mint = async ({ interactive }) => {
|
|
@@ -2025,6 +2052,18 @@ Session ${session.id} kept. Continue with \`maesttro resume ${session.id}\`.`);
|
|
|
2025
2052
|
console.log("You have commits on a detached HEAD. Resume and `git switch -c <name>` to keep them.");
|
|
2026
2053
|
}
|
|
2027
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
|
+
}
|
|
2028
2067
|
function exitCodeOf(code, signal) {
|
|
2029
2068
|
if (code !== null)
|
|
2030
2069
|
return code;
|
|
@@ -2033,7 +2072,8 @@ function exitCodeOf(code, signal) {
|
|
|
2033
2072
|
var FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
2034
2073
|
async function launch({
|
|
2035
2074
|
args,
|
|
2036
|
-
resume
|
|
2075
|
+
resume,
|
|
2076
|
+
strict: requestedStrict
|
|
2037
2077
|
}) {
|
|
2038
2078
|
const run = runCommand;
|
|
2039
2079
|
ensureReady();
|
|
@@ -2051,9 +2091,14 @@ async function launch({
|
|
|
2051
2091
|
refs: snapshotRefs(run, root),
|
|
2052
2092
|
sessionId: started.id
|
|
2053
2093
|
});
|
|
2054
|
-
const
|
|
2094
|
+
const strict = isStrict(requestedStrict, started);
|
|
2095
|
+
const saved = {
|
|
2096
|
+
...started,
|
|
2097
|
+
backup: started.backup ?? backup?.path,
|
|
2098
|
+
strict
|
|
2099
|
+
};
|
|
2055
2100
|
saveSession(saved);
|
|
2056
|
-
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"}` : ""}.`);
|
|
2057
2102
|
const repo = workspaceRepo(run, root);
|
|
2058
2103
|
if (repo && !resume) {
|
|
2059
2104
|
git(run, saved.worktree, [
|
|
@@ -2066,6 +2111,7 @@ async function launch({
|
|
|
2066
2111
|
const github = await openGitHubSession({
|
|
2067
2112
|
repo,
|
|
2068
2113
|
author: await resolveAuthor(root),
|
|
2114
|
+
strict,
|
|
2069
2115
|
broker: brokerUrl(),
|
|
2070
2116
|
login: {
|
|
2071
2117
|
clientId: githubAppClientId(),
|
|
@@ -2098,7 +2144,7 @@ async function launch({
|
|
|
2098
2144
|
...github.env,
|
|
2099
2145
|
MAESTTRO_SESSION_ID: saved.id,
|
|
2100
2146
|
MAESTTRO_BASE: saved.base,
|
|
2101
|
-
MAESTTRO_GITHUB:
|
|
2147
|
+
MAESTTRO_GITHUB: runnerGitHubMode({ strict, repo, notice: github.notice }),
|
|
2102
2148
|
...hostIdentityEnv(),
|
|
2103
2149
|
...repo ? { MAESTTRO_REPO: repo } : {}
|
|
2104
2150
|
};
|
|
@@ -2287,7 +2333,8 @@ function sessionsCommand({ args }) {
|
|
|
2287
2333
|
for (const session of sessions) {
|
|
2288
2334
|
const branches = existsSync5(session.worktree) ? cloneBranches(session.worktree) : undefined;
|
|
2289
2335
|
const where = branches === undefined ? "clone missing" : branches.length > 0 ? `branches: ${branches.join(", ")}` : "no branches yet";
|
|
2290
|
-
|
|
2336
|
+
const mode = session.strict ? " strict" : "";
|
|
2337
|
+
console.log(` ${session.id} ${session.createdAt.slice(0, 16)} ${where}${mode}`);
|
|
2291
2338
|
}
|
|
2292
2339
|
console.log("\n`maesttro resume <id>` continues one; `maesttro sessions remove <id>` removes its clone.");
|
|
2293
2340
|
}
|
|
@@ -2369,11 +2416,48 @@ var defaultCommands = {
|
|
|
2369
2416
|
status,
|
|
2370
2417
|
print: (text) => console.log(text)
|
|
2371
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
|
+
]);
|
|
2372
2450
|
async function main(argv, commands = defaultCommands) {
|
|
2373
|
-
const
|
|
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
|
+
}
|
|
2374
2458
|
switch (command) {
|
|
2375
2459
|
case undefined:
|
|
2376
|
-
await commands.launch({ args: [] });
|
|
2460
|
+
await commands.launch({ args: [], ...mode });
|
|
2377
2461
|
return;
|
|
2378
2462
|
case "setup":
|
|
2379
2463
|
await commands.setup({ args: rest });
|
|
@@ -2400,12 +2484,19 @@ async function main(argv, commands = defaultCommands) {
|
|
|
2400
2484
|
await commands.sessions({ args: rest });
|
|
2401
2485
|
return;
|
|
2402
2486
|
case "resume": {
|
|
2403
|
-
const
|
|
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;
|
|
2404
2491
|
if (!id) {
|
|
2405
|
-
commands.print("Usage: maesttro resume <session-id> [client] [args...]");
|
|
2492
|
+
commands.print("Usage: maesttro resume <session-id> [--strict|--no-strict] [client] [args...]");
|
|
2406
2493
|
return;
|
|
2407
2494
|
}
|
|
2408
|
-
await commands.launch({
|
|
2495
|
+
await commands.launch({
|
|
2496
|
+
args: afterIdFlags.rest,
|
|
2497
|
+
resume: id,
|
|
2498
|
+
...strict === undefined ? {} : { strict }
|
|
2499
|
+
});
|
|
2409
2500
|
return;
|
|
2410
2501
|
}
|
|
2411
2502
|
case "configure":
|
|
@@ -2420,7 +2511,7 @@ async function main(argv, commands = defaultCommands) {
|
|
|
2420
2511
|
commands.print(HELP);
|
|
2421
2512
|
return;
|
|
2422
2513
|
default:
|
|
2423
|
-
await commands.launch({ args:
|
|
2514
|
+
await commands.launch({ args: leading.rest, ...mode });
|
|
2424
2515
|
}
|
|
2425
2516
|
}
|
|
2426
2517
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maesttro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Isolated Docker runner for local coding agents (opencode, claude, ...) under a dedicated bot GitHub identity, with curated configs baked in per client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|