c8ctl-plugin-nano 1.26.0 → 1.26.2
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 +11 -3
- package/c8ctl-plugin.js +289 -20
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -346,13 +346,21 @@ the harness:
|
|
|
346
346
|
as the checkout target) into a throwaway workspace under
|
|
347
347
|
`<state>/agent-runs/run-*`;
|
|
348
348
|
3. create `branch.create` (if set) off that target;
|
|
349
|
-
4.
|
|
349
|
+
4. set a **committer identity** on the workspace, preferring the operator's own
|
|
350
|
+
(`GIT_AUTHOR_*` env → global `git config user.name/email` → the
|
|
351
|
+
`gh`-authenticated GitHub user), and only falling back to `nano-agent` when
|
|
352
|
+
none resolve — so autonomous commits are authored by the human running the
|
|
353
|
+
fleet (who has signed any CLA/DCO), not an anonymous bot;
|
|
354
|
+
5. run the harness **in the workspace** (`cwd`), with `AGENT_WORKSPACE`,
|
|
350
355
|
`AGENT_REPO_URL`, `AGENT_REPO_BRANCH`, `AGENT_REPO_REF` exported and the job
|
|
351
356
|
envelope on stdin;
|
|
352
|
-
|
|
357
|
+
6. on success, enumerate new commits, `git push` the branch when `branch.push`
|
|
353
358
|
(default true), and — when `task.allowPr` — **reconcile the PR the agent
|
|
354
359
|
opened** for the branch (`gh pr list --head <branch>`; `openedBy` reports the
|
|
355
|
-
PR's actual author login, or `null` when none is found)
|
|
360
|
+
PR's actual author login, or `null` when none is found), then post a one-time
|
|
361
|
+
attribution comment recording that the change was agent-generated (marker-
|
|
362
|
+
guarded so convergence rounds don't repeat it; disable with
|
|
363
|
+
`NANO_AGENT_ATTRIBUTION=0`, rename the agent with `NANO_AGENT_NAME`).
|
|
356
364
|
|
|
357
365
|
The token is delivered to git via `GIT_ASKPASS` (env), never on argv or in the
|
|
358
366
|
remote URL, and is redacted from all logs/results. Credential helpers are
|
package/c8ctl-plugin.js
CHANGED
|
@@ -2295,14 +2295,9 @@ function makeSecretResolver(kind) {
|
|
|
2295
2295
|
// Resolve the names a job needs (setup.secretRefs, plus the repo/PR credential
|
|
2296
2296
|
// when allowPr). Returns resolved values + a list of names that were missing so
|
|
2297
2297
|
// the caller can fail the job with a clear provisioning error.
|
|
2298
|
-
function resolveJobSecrets(resolver, envelope) {
|
|
2298
|
+
function resolveJobSecrets(resolver, envelope, { ghAuthToken = ghAuthTokenFromCli } = {}) {
|
|
2299
2299
|
const names = new Set();
|
|
2300
2300
|
for (const n of envelope.setup?.secretRefs || []) if (n) names.add(n);
|
|
2301
|
-
if (envelope.task?.allowPr) {
|
|
2302
|
-
const provider = envelope.repository?.provider || 'github';
|
|
2303
|
-
const authRef = envelope.repository?.authRef || (provider === 'github' ? 'GITHUB_TOKEN' : undefined);
|
|
2304
|
-
if (authRef) names.add(authRef);
|
|
2305
|
-
}
|
|
2306
2301
|
const resolved = {};
|
|
2307
2302
|
const missing = [];
|
|
2308
2303
|
for (const name of names) {
|
|
@@ -2310,6 +2305,34 @@ function resolveJobSecrets(resolver, envelope) {
|
|
|
2310
2305
|
if (v === undefined) missing.push(name);
|
|
2311
2306
|
else resolved[name] = v;
|
|
2312
2307
|
}
|
|
2308
|
+
// The github clone/push credential is resolved with a gh-CLI fallback so a
|
|
2309
|
+
// default GITHUB_TOKEN isn't reported "missing" merely because it isn't in the
|
|
2310
|
+
// env when `gh auth login` provides it. A custom authRef stays strict.
|
|
2311
|
+
if (envelope.task?.allowPr) {
|
|
2312
|
+
const provider = envelope.repository?.provider || 'github';
|
|
2313
|
+
const authRef = envelope.repository?.authRef;
|
|
2314
|
+
const ref = normalizeAuthRef(authRef);
|
|
2315
|
+
if (ref.kind === 'invalid') {
|
|
2316
|
+
// A present-but-blank authRef is a misconfiguration: surface it as missing
|
|
2317
|
+
// so provisioning sheds rather than silently borrowing the default/gh token.
|
|
2318
|
+
if (!missing.includes('repository.authRef')) missing.push('repository.authRef');
|
|
2319
|
+
} else {
|
|
2320
|
+
const ghAuthRef = ref.kind === 'custom'
|
|
2321
|
+
? ref.name
|
|
2322
|
+
: (provider === 'github' ? 'GITHUB_TOKEN' : undefined);
|
|
2323
|
+
if (ghAuthRef) {
|
|
2324
|
+
names.add(ghAuthRef);
|
|
2325
|
+
const token = githubCloneToken({ provider, authRef, secretResolver: resolver, ghAuthToken });
|
|
2326
|
+
const missingIdx = missing.indexOf(ghAuthRef);
|
|
2327
|
+
if (token) {
|
|
2328
|
+
resolved[ghAuthRef] = token;
|
|
2329
|
+
if (missingIdx !== -1) missing.splice(missingIdx, 1);
|
|
2330
|
+
} else if (missingIdx === -1) {
|
|
2331
|
+
missing.push(ghAuthRef);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2313
2336
|
return { resolved, missing, names: [...names] };
|
|
2314
2337
|
}
|
|
2315
2338
|
|
|
@@ -2460,6 +2483,147 @@ function credArgs() {
|
|
|
2460
2483
|
return ['-c', 'credential.helper='];
|
|
2461
2484
|
}
|
|
2462
2485
|
|
|
2486
|
+
// Fall back to the `gh` CLI's stored credential when GITHUB_TOKEN is not exported
|
|
2487
|
+
// to the env. Most interactive setups authenticate with `gh auth login` (keychain)
|
|
2488
|
+
// rather than an env var, so an env-only secret resolver yields no token and a
|
|
2489
|
+
// private/internal clone fails with "could not read Username". Best effort: returns
|
|
2490
|
+
// a trimmed token, or null when gh is missing / not logged in. The token is fed to
|
|
2491
|
+
// git via GIT_ASKPASS only (never argv/URL/helper), preserving the ephemeral-token
|
|
2492
|
+
// guarantee.
|
|
2493
|
+
// Memoized for the process lifetime: this is a synchronous spawnSync (up to a
|
|
2494
|
+
// 10s timeout) that can be reached per job, and jobs may run concurrently
|
|
2495
|
+
// (maxParallelJobs > 1), so consult the CLI at most once per worker run rather
|
|
2496
|
+
// than blocking every handler. A sentinel distinguishes "not yet computed" from
|
|
2497
|
+
// a cached null (gh missing / not logged in).
|
|
2498
|
+
//
|
|
2499
|
+
// Memoization alone still lets the *first* job pay the synchronous spawn on the
|
|
2500
|
+
// event loop, stalling any sibling handlers (and lock-extension heartbeats) for
|
|
2501
|
+
// up to the timeout. So `nano work` primes this cache once at startup via
|
|
2502
|
+
// `primeGhAuthToken()` — before the poll loop — moving the one unavoidable
|
|
2503
|
+
// blocking spawn off the job-handling path entirely. Any later call is a warm
|
|
2504
|
+
// cache hit; the memoization here is the safety net for paths that never primed.
|
|
2505
|
+
const GH_AUTH_TOKEN_UNSET = Symbol('gh-auth-token-unset');
|
|
2506
|
+
let ghAuthTokenCache = GH_AUTH_TOKEN_UNSET;
|
|
2507
|
+
function ghAuthTokenFromCli() {
|
|
2508
|
+
if (ghAuthTokenCache !== GH_AUTH_TOKEN_UNSET) return ghAuthTokenCache;
|
|
2509
|
+
let token = null;
|
|
2510
|
+
try {
|
|
2511
|
+
const r = spawnSync('gh', ['auth', 'token'], { encoding: 'utf8', timeout: 10_000 });
|
|
2512
|
+
const tok = r.status === 0 ? (r.stdout || '').trim() : '';
|
|
2513
|
+
token = tok || null;
|
|
2514
|
+
} catch {
|
|
2515
|
+
token = null;
|
|
2516
|
+
}
|
|
2517
|
+
ghAuthTokenCache = token;
|
|
2518
|
+
return token;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
// Warm the gh-token cache once, off the job-handling path. Safe to call any
|
|
2522
|
+
// number of times: the first call performs the (possibly blocking) lookup, the
|
|
2523
|
+
// rest are cache hits. Returns true once the cache is populated.
|
|
2524
|
+
function primeGhAuthToken() {
|
|
2525
|
+
ghAuthTokenFromCli();
|
|
2526
|
+
return ghAuthTokenCache !== GH_AUTH_TOKEN_UNSET;
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// Read the operator's own git identity from their GLOBAL config, using the real
|
|
2530
|
+
// host environment (process.env) rather than a job's sanitized gitEnv — so it
|
|
2531
|
+
// resolves even on the anonymous clone path, where gitEnv points
|
|
2532
|
+
// GIT_CONFIG_GLOBAL at /dev/null. Returns { name, email }, each possibly ''.
|
|
2533
|
+
function hostGitIdentity() {
|
|
2534
|
+
const read = (key) => {
|
|
2535
|
+
try {
|
|
2536
|
+
const r = spawnSync('git', ['config', '--global', '--get', key], { encoding: 'utf8', timeout: 5_000 });
|
|
2537
|
+
return r.status === 0 ? (r.stdout || '').trim() : '';
|
|
2538
|
+
} catch {
|
|
2539
|
+
return '';
|
|
2540
|
+
}
|
|
2541
|
+
};
|
|
2542
|
+
return { name: read('user.name'), email: read('user.email') };
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// Fall back to the gh-authenticated GitHub user for a committer identity. Uses
|
|
2546
|
+
// the account's public email, or the id+login noreply address when the email is
|
|
2547
|
+
// private/unset. Returns { name, email }, each possibly ''.
|
|
2548
|
+
function ghUserIdentity() {
|
|
2549
|
+
try {
|
|
2550
|
+
const r = spawnSync('gh', ['api', 'user', '--jq', '[.name // "", .login // "", .email // "", (.id // "" | tostring)] | @tsv'],
|
|
2551
|
+
{ encoding: 'utf8', timeout: 10_000,
|
|
2552
|
+
env: { ...process.env, GH_PROMPT_DISABLED: '1', GH_NO_UPDATE_NOTIFIER: '1' } });
|
|
2553
|
+
if (r.status !== 0) return { name: '', email: '' };
|
|
2554
|
+
const [name = '', login = '', email = '', id = ''] = (r.stdout || '').trim().split('\t');
|
|
2555
|
+
const resolvedName = name || login || '';
|
|
2556
|
+
const resolvedEmail = email || (id && login ? `${id}+${login}@users.noreply.github.com` : '');
|
|
2557
|
+
return { name: resolvedName, email: resolvedEmail };
|
|
2558
|
+
} catch {
|
|
2559
|
+
return { name: '', email: '' };
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
// Resolve the committer identity the harness stamps onto the cloned workspace.
|
|
2564
|
+
// Per-field precedence: explicit GIT_AUTHOR_* env → the operator's global git
|
|
2565
|
+
// config → the gh-authenticated GitHub user → the `nano-agent` fallback.
|
|
2566
|
+
// Preferring the operator's real identity means autonomous commits are authored
|
|
2567
|
+
// by the human running the fleet (who has signed any CLA) rather than an
|
|
2568
|
+
// anonymous bot that hasn't; the agent's own authorship is recorded as a PR
|
|
2569
|
+
// comment (see postAgentAttribution) instead of forged onto the commit. Both the
|
|
2570
|
+
// git-config and gh lookups are lazy and performed at most once each, and only
|
|
2571
|
+
// when a higher-precedence source didn't already supply the field — so explicit
|
|
2572
|
+
// GIT_AUTHOR_* env fully short-circuits them (no `git config`/`gh` spawns, hence
|
|
2573
|
+
// no added latency or failure modes when the override is present).
|
|
2574
|
+
// `gitIdentity`/`ghIdentity` are injectable for testing.
|
|
2575
|
+
function resolveCommitterIdentity({ gitIdentity = hostGitIdentity, ghIdentity = ghUserIdentity } = {}) {
|
|
2576
|
+
const envName = process.env.GIT_AUTHOR_NAME || '';
|
|
2577
|
+
const envEmail = process.env.GIT_AUTHOR_EMAIL || '';
|
|
2578
|
+
let g = null;
|
|
2579
|
+
const gitOnce = () => (g ??= (gitIdentity() || { name: '', email: '' }));
|
|
2580
|
+
let gh = null;
|
|
2581
|
+
const ghOnce = () => (gh ??= (ghIdentity() || { name: '', email: '' }));
|
|
2582
|
+
|
|
2583
|
+
const name = envName || gitOnce().name || ghOnce().name || 'nano-agent';
|
|
2584
|
+
const email = envEmail || gitOnce().email || ghOnce().email || 'nano-agent@users.noreply.github.com';
|
|
2585
|
+
|
|
2586
|
+
const source =
|
|
2587
|
+
(envName || envEmail) ? 'env'
|
|
2588
|
+
: (g && (g.name || g.email)) ? 'git-global'
|
|
2589
|
+
: (gh && (gh.name || gh.email)) ? 'gh'
|
|
2590
|
+
: 'fallback';
|
|
2591
|
+
return { name, email, source };
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// Normalize a repository authRef into one of three intents. Trimming matters so
|
|
2595
|
+
// a present-but-blank authRef ('' or whitespace) is treated as a misconfiguration
|
|
2596
|
+
// rather than "absent": absence enables the default/gh fallback, but a blank
|
|
2597
|
+
// custom ref must NOT silently borrow the operator's gh login.
|
|
2598
|
+
// { kind: 'default' } no custom authRef configured (undefined/null)
|
|
2599
|
+
// { kind: 'custom', name } a non-empty custom authRef (strict)
|
|
2600
|
+
// { kind: 'invalid' } authRef present but blank (config error)
|
|
2601
|
+
function normalizeAuthRef(authRef) {
|
|
2602
|
+
if (authRef === undefined || authRef === null) return { kind: 'default' };
|
|
2603
|
+
const trimmed = String(authRef).trim();
|
|
2604
|
+
if (trimmed === '') return { kind: 'invalid' };
|
|
2605
|
+
return { kind: 'custom', name: trimmed };
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
// Resolve the github clone/push credential. The default credential (env
|
|
2609
|
+
// GITHUB_TOKEN) falls back to the gh CLI's stored token so `gh auth login`
|
|
2610
|
+
// setups work without exporting GITHUB_TOKEN. A custom `authRef` is honored
|
|
2611
|
+
// strictly (env/secret resolver only, no gh fallback) so a misconfigured named
|
|
2612
|
+
// secret surfaces as missing rather than silently borrowing the operator's gh
|
|
2613
|
+
// login. An authRef that is present but blank is a misconfiguration and yields no
|
|
2614
|
+
// token (never the gh fallback). `ghAuthToken` is injectable for testing.
|
|
2615
|
+
// Returns a token or null.
|
|
2616
|
+
function githubCloneToken({ provider, authRef, secretResolver, ghAuthToken = ghAuthTokenFromCli }) {
|
|
2617
|
+
const prov = provider || 'github';
|
|
2618
|
+
const ref = normalizeAuthRef(authRef);
|
|
2619
|
+
if (ref.kind === 'invalid') return null;
|
|
2620
|
+
const usesDefault = prov === 'github' && ref.kind === 'default';
|
|
2621
|
+
const name = ref.kind === 'custom' ? ref.name : (prov === 'github' ? 'GITHUB_TOKEN' : null);
|
|
2622
|
+
let token = name ? (secretResolver.resolve(name) || null) : null;
|
|
2623
|
+
if (!token && usesDefault) token = ghAuthToken() || null;
|
|
2624
|
+
return token;
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2463
2627
|
// Clone repo into <runDir>/workspace and check out / create the working branch.
|
|
2464
2628
|
// Returns { workspaceDir, gitEnv, startSha, workingBranch, remote }. Throws a
|
|
2465
2629
|
// ProvisionError (token-redacted) on any git failure so the caller can shed.
|
|
@@ -2517,9 +2681,15 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
|
|
|
2517
2681
|
}
|
|
2518
2682
|
}
|
|
2519
2683
|
|
|
2520
|
-
// Give the harness a committer identity in case it commits (many do).
|
|
2521
|
-
|
|
2522
|
-
|
|
2684
|
+
// Give the harness a committer identity in case it commits (many do). Prefer
|
|
2685
|
+
// the operator's real identity (git global / gh user) over the `nano-agent`
|
|
2686
|
+
// fallback so autonomous commits are authored by the human running the fleet —
|
|
2687
|
+
// who has signed any CLA — instead of an anonymous bot. The agent's authorship
|
|
2688
|
+
// is instead recorded as a PR comment (postAgentAttribution). Set via repo-
|
|
2689
|
+
// level config, which overrides global, so the identity is deterministic.
|
|
2690
|
+
const committer = resolveCommitterIdentity();
|
|
2691
|
+
runGit(['config', 'user.name', committer.name], { cwd: workspaceDir, env: gitEnv });
|
|
2692
|
+
runGit(['config', 'user.email', committer.email], { cwd: workspaceDir, env: gitEnv });
|
|
2523
2693
|
|
|
2524
2694
|
// Determine the working branch. With branch.create we make a real branch.
|
|
2525
2695
|
// Otherwise we're on whatever the clone checked out: a branch only if
|
|
@@ -2549,15 +2719,7 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
|
|
|
2549
2719
|
// gh returns whatever PR is open for the head branch, which may not be ours.
|
|
2550
2720
|
function reconcileAgentPr({ workspaceDir, token, branch, provider }) {
|
|
2551
2721
|
if (provider && provider !== 'github') return { openedBy: null, found: false, error: `PR reconcile unsupported for provider "${provider}"` };
|
|
2552
|
-
const env =
|
|
2553
|
-
if (token) {
|
|
2554
|
-
env.GH_TOKEN = token;
|
|
2555
|
-
} else {
|
|
2556
|
-
// No job token ⇒ honor the anonymous guarantee: never let gh fall back to an
|
|
2557
|
-
// operator-provided token in the ambient env. Scrub every gh auth source so
|
|
2558
|
-
// PR reconcile can only use credentials we were explicitly handed.
|
|
2559
|
-
for (const k of ['GH_TOKEN', 'GITHUB_TOKEN', 'GH_ENTERPRISE_TOKEN', 'GITHUB_ENTERPRISE_TOKEN']) delete env[k];
|
|
2560
|
-
}
|
|
2722
|
+
const env = ghAuthEnv(token, workspaceDir);
|
|
2561
2723
|
try {
|
|
2562
2724
|
const r = spawnSync('gh', ['pr', 'list', '--head', branch, '--state', 'all', '--json', 'number,url,state,isDraft,title,author', '--limit', '1'],
|
|
2563
2725
|
{ cwd: workspaceDir, env, encoding: 'utf8', timeout: 30_000 });
|
|
@@ -2572,6 +2734,96 @@ function reconcileAgentPr({ workspaceDir, token, branch, provider }) {
|
|
|
2572
2734
|
}
|
|
2573
2735
|
}
|
|
2574
2736
|
|
|
2737
|
+
// Build the gh environment for PR-side calls: inject the resolved job token, or
|
|
2738
|
+
// (anonymous path) scrub every ambient gh credential so we can only use what we
|
|
2739
|
+
// were explicitly handed. Scrubbing the token env vars alone is not enough — gh
|
|
2740
|
+
// will still authenticate from its on-disk config (hosts.yml / OS keychain), so
|
|
2741
|
+
// in the anonymous path we also point gh at a private, empty GH_CONFIG_DIR
|
|
2742
|
+
// (created inside the harness-reaped workspace) and disable interactive prompts,
|
|
2743
|
+
// guaranteeing a token-less job cannot act as the operator via stored creds.
|
|
2744
|
+
function ghAuthEnv(token, workspaceDir) {
|
|
2745
|
+
const env = { ...process.env };
|
|
2746
|
+
// These apply on every path: workers are non-interactive, so gh must fail
|
|
2747
|
+
// fast rather than block on an auth/update prompt — even a provided token can
|
|
2748
|
+
// be invalid/expired, in which case gh would otherwise try to prompt.
|
|
2749
|
+
env.GH_PROMPT_DISABLED = '1';
|
|
2750
|
+
env.GH_NO_UPDATE_NOTIFIER = '1';
|
|
2751
|
+
if (token) {
|
|
2752
|
+
env.GH_TOKEN = token;
|
|
2753
|
+
return env;
|
|
2754
|
+
}
|
|
2755
|
+
for (const k of ['GH_TOKEN', 'GITHUB_TOKEN', 'GH_ENTERPRISE_TOKEN', 'GITHUB_ENTERPRISE_TOKEN']) delete env[k];
|
|
2756
|
+
// Fail closed: always point gh at an isolated (empty) config dir so it can
|
|
2757
|
+
// never fall back to the operator's on-disk config/keychain. Set GH_CONFIG_DIR
|
|
2758
|
+
// unconditionally — even if mkdirSync fails, gh reading a missing/empty dir
|
|
2759
|
+
// errors out rather than silently authenticating as the operator, preserving
|
|
2760
|
+
// the "token-less job cannot act as the operator" guarantee.
|
|
2761
|
+
const dir = join(workspaceDir || tmpdir(), '.nano-gh-anon');
|
|
2762
|
+
env.GH_CONFIG_DIR = dir;
|
|
2763
|
+
try {
|
|
2764
|
+
mkdirSync(dir, { recursive: true });
|
|
2765
|
+
} catch {
|
|
2766
|
+
// Directory couldn't be created; GH_CONFIG_DIR still points at it so gh
|
|
2767
|
+
// fails closed rather than using ambient operator credentials.
|
|
2768
|
+
}
|
|
2769
|
+
return env;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// The agent whose authorship we record on the PR. Because commits are now
|
|
2773
|
+
// authored under the operator's own identity (so they satisfy CLA/DCO), this
|
|
2774
|
+
// comment preserves the provenance that the change was machine-generated.
|
|
2775
|
+
const AGENT_ATTRIBUTION_NAME = process.env.NANO_AGENT_NAME || 'nano-agent';
|
|
2776
|
+
const ATTRIBUTION_MARKER = '<!-- nano-agent-attribution -->';
|
|
2777
|
+
|
|
2778
|
+
// Post a one-time attribution comment on the agent-opened PR, recording that the
|
|
2779
|
+
// change was produced by the autonomous agent even though the commits carry the
|
|
2780
|
+
// operator's identity. Idempotent via a hidden marker so convergence's repeated
|
|
2781
|
+
// rounds don't spam the thread. Gated off with NANO_AGENT_ATTRIBUTION=0. Best
|
|
2782
|
+
// effort: never throws; returns a small status object.
|
|
2783
|
+
function postAgentAttribution({ workspaceDir, token, number, agentName = AGENT_ATTRIBUTION_NAME }) {
|
|
2784
|
+
if (!coerceBool(process.env.NANO_AGENT_ATTRIBUTION, true)) return { posted: false, reason: 'disabled' };
|
|
2785
|
+
if (!number) return { posted: false, reason: 'no-pr' };
|
|
2786
|
+
const env = ghAuthEnv(token, workspaceDir);
|
|
2787
|
+
try {
|
|
2788
|
+
// Ask jq for a single boolean ("marker present?") rather than streaming every
|
|
2789
|
+
// comment body back through stdout. On PRs with many/large comments the full
|
|
2790
|
+
// dump can be slow and can exceed spawnSync's output buffer (maxBuffer),
|
|
2791
|
+
// surfacing as existing.error and wedging attribution forever; a lone
|
|
2792
|
+
// true/false keeps output tiny while preserving idempotency.
|
|
2793
|
+
const markerFilter = `any((.comments // [])[].body; contains(${JSON.stringify(ATTRIBUTION_MARKER)}))`;
|
|
2794
|
+
const existing = spawnSync('gh', ['pr', 'view', String(number), '--json', 'comments', '--jq', markerFilter],
|
|
2795
|
+
{ cwd: workspaceDir, env, encoding: 'utf8', timeout: 30_000 });
|
|
2796
|
+
// Idempotency hinges on reliably reading the existing comments: if we cannot
|
|
2797
|
+
// verify whether the marker is already present (transient gh failure — rate
|
|
2798
|
+
// limit, auth glitch, timeout), do NOT post. Posting blind would let repeated
|
|
2799
|
+
// convergence rounds spam duplicate attribution comments. Bail out instead.
|
|
2800
|
+
if (existing.error) {
|
|
2801
|
+
return { posted: false, error: `gh not runnable: ${redactToken(existing.error.message, token).trim().slice(0, 200)}` };
|
|
2802
|
+
}
|
|
2803
|
+
if (existing.status !== 0) {
|
|
2804
|
+
return { posted: false, error: redactToken(existing.stderr || existing.stdout, token).trim().slice(0, 200) || `gh pr view failed (exit ${existing.status ?? 'null'}${existing.signal ? `, signal ${existing.signal}` : ''})` };
|
|
2805
|
+
}
|
|
2806
|
+
if ((existing.stdout || '').trim() === 'true') {
|
|
2807
|
+
return { posted: false, reason: 'exists' };
|
|
2808
|
+
}
|
|
2809
|
+
const body = `${ATTRIBUTION_MARKER}\n`
|
|
2810
|
+
+ `🤖 The changes in this PR were produced by **${agentName}**, an autonomous agent. `
|
|
2811
|
+
+ `Commits are authored under the operator's own git identity when one is resolvable (the human running the fleet), so they can satisfy CLA/DCO requirements; `
|
|
2812
|
+
+ `this note records that the work was generated by the agent.`;
|
|
2813
|
+
const r = spawnSync('gh', ['pr', 'comment', String(number), '--body', body],
|
|
2814
|
+
{ cwd: workspaceDir, env, encoding: 'utf8', timeout: 30_000 });
|
|
2815
|
+
if (r.error) {
|
|
2816
|
+
return { posted: false, error: `gh not runnable: ${redactToken(r.error.message, token).trim().slice(0, 200)}` };
|
|
2817
|
+
}
|
|
2818
|
+
if (r.status !== 0) {
|
|
2819
|
+
return { posted: false, error: redactToken(r.stderr || r.stdout, token).trim().slice(0, 200) || `gh pr comment failed (exit ${r.status ?? 'null'}${r.signal ? `, signal ${r.signal}` : ''})` };
|
|
2820
|
+
}
|
|
2821
|
+
return { posted: true };
|
|
2822
|
+
} catch (err) {
|
|
2823
|
+
return { posted: false, error: err.message };
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2575
2827
|
// After the harness runs: enumerate new commits, push the branch (when
|
|
2576
2828
|
// branch.push), and reconcile the agent-opened PR (when task.allowPr). A push
|
|
2577
2829
|
// failure is reported (pushError) rather than thrown — the process model decides
|
|
@@ -2604,6 +2856,11 @@ function finalizeGit({ workspaceDir, gitEnv, startSha, workingBranch, envelope,
|
|
|
2604
2856
|
|
|
2605
2857
|
if (workingBranch && envelope.task?.allowPr) {
|
|
2606
2858
|
out.pr = reconcileAgentPr({ workspaceDir, token, branch: workingBranch, provider: envelope.repository?.provider || 'github' });
|
|
2859
|
+
// Record the agent's authorship on the PR (commits carry the operator's
|
|
2860
|
+
// identity now, so this preserves the machine-generated provenance).
|
|
2861
|
+
if (out.pr?.found && out.pr.number && (envelope.repository?.provider || 'github') === 'github') {
|
|
2862
|
+
out.attribution = postAgentAttribution({ workspaceDir, token, number: out.pr.number });
|
|
2863
|
+
}
|
|
2607
2864
|
}
|
|
2608
2865
|
return out;
|
|
2609
2866
|
}
|
|
@@ -3194,6 +3451,12 @@ async function workAgent(req, flags) {
|
|
|
3194
3451
|
const extraNote = extraJobTypes.length > 0 ? ` (${extraJobTypes.length} via --job-type)` : '';
|
|
3195
3452
|
logger.info(` listening on ${jobTypes.length} job type(s)${extraNote}: ${jobTypes.join(' ')}`);
|
|
3196
3453
|
logger.info(` max parallel: ${maxParallelJobs}; recovery window: ${recoveryWindowMs}ms; idle timeout: ${idleTimeoutMs}ms; hard cap: ${hardCapMs > 0 ? `${hardCapMs}ms` : 'off'}; poll timeout: ${pollTimeoutMs}ms`);
|
|
3454
|
+
// Warm the gh-token cache now, off the job-handling path: githubCloneToken()
|
|
3455
|
+
// may consult `gh auth token` (a synchronous spawn, up to 10s) as its default
|
|
3456
|
+
// credential fallback, and doing that inside a job handler would stall sibling
|
|
3457
|
+
// handlers + lock heartbeats when maxParallelJobs > 1. Priming here pays that
|
|
3458
|
+
// cost once at startup so every later lookup is a warm cache hit.
|
|
3459
|
+
primeGhAuthToken();
|
|
3197
3460
|
logger.info('Polling for work — press Ctrl-C to stop.');
|
|
3198
3461
|
|
|
3199
3462
|
// When launched under the supervisor, report per-job activity — which job(s)
|
|
@@ -3295,8 +3558,8 @@ async function workAgent(req, flags) {
|
|
|
3295
3558
|
let repoToken = null;
|
|
3296
3559
|
if (hasRepo) {
|
|
3297
3560
|
const provider = envelope.repository.provider || 'github';
|
|
3298
|
-
const authRef = envelope.repository.authRef
|
|
3299
|
-
|
|
3561
|
+
const authRef = envelope.repository.authRef;
|
|
3562
|
+
repoToken = githubCloneToken({ provider, authRef, secretResolver }); // absent → anonymous clone
|
|
3300
3563
|
try {
|
|
3301
3564
|
mkdirSync(agentRunsRoot(), { recursive: true });
|
|
3302
3565
|
runDir = mkdtempSync(join(agentRunsRoot(), 'run-'));
|
|
@@ -6215,8 +6478,14 @@ export {
|
|
|
6215
6478
|
provisionRepo,
|
|
6216
6479
|
finalizeGit,
|
|
6217
6480
|
reconcileAgentPr,
|
|
6481
|
+
resolveCommitterIdentity,
|
|
6482
|
+
postAgentAttribution,
|
|
6218
6483
|
reapAgentRunDirs,
|
|
6219
6484
|
authUrl,
|
|
6485
|
+
githubCloneToken,
|
|
6486
|
+
ghAuthTokenFromCli,
|
|
6487
|
+
primeGhAuthToken,
|
|
6488
|
+
ghAuthEnv,
|
|
6220
6489
|
redactToken,
|
|
6221
6490
|
agentRunsRoot,
|
|
6222
6491
|
ProvisionError,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
|
|
6
6
|
"main": "c8ctl-plugin.js",
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"semantic-release": "^25.0.3"
|
|
48
48
|
},
|
|
49
49
|
"optionalDependencies": {
|
|
50
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.26.
|
|
51
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.26.
|
|
52
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.26.
|
|
53
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.26.
|
|
54
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.26.
|
|
55
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.26.
|
|
56
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.26.
|
|
50
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.26.2",
|
|
51
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.26.2",
|
|
52
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.26.2",
|
|
53
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.26.2",
|
|
54
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.26.2",
|
|
55
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.26.2",
|
|
56
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.26.2"
|
|
57
57
|
}
|
|
58
58
|
}
|