c8ctl-plugin-nano 1.33.0 → 1.33.1

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.
Files changed (2) hide show
  1. package/c8ctl-plugin.js +82 -10
  2. package/package.json +8 -8
package/c8ctl-plugin.js CHANGED
@@ -2853,9 +2853,55 @@ function ghUserIdentity() {
2853
2853
  }
2854
2854
  }
2855
2855
 
2856
+ // Reject a commit-author email that can't be attributed to a real account and
2857
+ // can't receive mail — a `*@nano.local` (or other non-routable) placeholder
2858
+ // injected by the launch environment. Such an address produces UNVERIFIED
2859
+ // commits that look like a person but map to no GitHub user, so the harness must
2860
+ // never stamp it onto a commit; it falls through to the next identity source
2861
+ // instead. An EMPTY email is NOT a placeholder — it is an absent field handled
2862
+ // by ordinary per-field fallthrough, so it does not invalidate its source.
2863
+ // Matching is trim + case-insensitive.
2864
+ function isPlaceholderEmail(email) {
2865
+ const e = String(email || '').trim().toLowerCase();
2866
+ if (!e) return false; // absent — handled by per-field fallthrough, not a placeholder
2867
+ const at = e.lastIndexOf('@');
2868
+ if (at < 0) return true; // no domain at all — not a routable address
2869
+ const local = e.slice(0, at);
2870
+ const domain = e.slice(at + 1);
2871
+ // Malformed addresses missing a local part (`@example.com`) or a domain
2872
+ // (`user@`) can't be routed or attributed either — reject them too.
2873
+ if (!local || !domain) return true;
2874
+ // Non-routable mDNS/host-local TLDs and the loopback host: unattributable and
2875
+ // undeliverable, so never a legitimate commit author.
2876
+ return domain === 'localhost'
2877
+ || domain.endsWith('.local')
2878
+ || domain.endsWith('.internal');
2879
+ }
2880
+
2881
+ // Coerce one identity source into a usable { name, email }. When the source's
2882
+ // email is a non-routable placeholder we discard the WHOLE candidate (both
2883
+ // fields) rather than just the email — otherwise a placeholder-derived name
2884
+ // (e.g. `trial-merge`) would be stitched onto a borrowed email from a lower
2885
+ // source, forging a Frankenstein author. An empty email is preserved as-is so
2886
+ // ordinary per-field fill still works (e.g. git supplies a name, gh the email).
2887
+ // Fields are trimmed so a whitespace-only/space-padded name or email behaves
2888
+ // like "absent" (empty) rather than a truthy value that would block per-field
2889
+ // fallthrough and get stamped as an invalid commit identity — this matches
2890
+ // isPlaceholderEmail, which already normalizes via trim().
2891
+ function sanitizeIdentity(id) {
2892
+ const name = String((id && id.name) || '').trim();
2893
+ const email = String((id && id.email) || '').trim();
2894
+ if (isPlaceholderEmail(email)) return { name: '', email: '' };
2895
+ return { name, email };
2896
+ }
2897
+
2856
2898
  // Resolve the committer identity the harness stamps onto the cloned workspace.
2857
- // Per-field precedence: explicit GIT_AUTHOR_* env → the operator's global git
2899
+ // Source precedence: explicit GIT_AUTHOR_* env → the operator's global git
2858
2900
  // config → the gh-authenticated GitHub user → the `nano-agent` fallback.
2901
+ // Precedence is per-field only for ABSENT fields (an empty name/email falls
2902
+ // through to the next source); a source whose email is a non-routable
2903
+ // placeholder is discarded WHOLE by sanitizeIdentity (name included), so in that
2904
+ // case its name does not participate in per-field fill (see sanitizeIdentity).
2859
2905
  // Preferring the operator's real identity means autonomous commits are authored
2860
2906
  // by the human running the fleet (who has signed any CLA) rather than an
2861
2907
  // anonymous bot that hasn't; the agent's own authorship is recorded as a PR
@@ -2864,20 +2910,22 @@ function ghUserIdentity() {
2864
2910
  // when a higher-precedence source didn't already supply the field — so explicit
2865
2911
  // GIT_AUTHOR_* env fully short-circuits them (no `git config`/`gh` spawns, hence
2866
2912
  // no added latency or failure modes when the override is present).
2867
- // `gitIdentity`/`ghIdentity` are injectable for testing.
2913
+ // Every candidate source is passed through sanitizeIdentity, so a non-routable
2914
+ // `*@nano.local` placeholder from ANY source (env, git-global) is discarded and
2915
+ // falls through to the gh identity / marked bot fallback — never stamped onto a
2916
+ // commit. `gitIdentity`/`ghIdentity` are injectable for testing.
2868
2917
  function resolveCommitterIdentity({ gitIdentity = hostGitIdentity, ghIdentity = ghUserIdentity } = {}) {
2869
- const envName = process.env.GIT_AUTHOR_NAME || '';
2870
- const envEmail = process.env.GIT_AUTHOR_EMAIL || '';
2918
+ const env = sanitizeIdentity({ name: process.env.GIT_AUTHOR_NAME || '', email: process.env.GIT_AUTHOR_EMAIL || '' });
2871
2919
  let g = null;
2872
- const gitOnce = () => (g ??= (gitIdentity() || { name: '', email: '' }));
2920
+ const gitOnce = () => (g ??= sanitizeIdentity(gitIdentity() || {}));
2873
2921
  let gh = null;
2874
- const ghOnce = () => (gh ??= (ghIdentity() || { name: '', email: '' }));
2922
+ const ghOnce = () => (gh ??= sanitizeIdentity(ghIdentity() || {}));
2875
2923
 
2876
- const name = envName || gitOnce().name || ghOnce().name || 'nano-agent';
2877
- const email = envEmail || gitOnce().email || ghOnce().email || 'nano-agent@users.noreply.github.com';
2924
+ const name = env.name || gitOnce().name || ghOnce().name || 'nano-agent';
2925
+ const email = env.email || gitOnce().email || ghOnce().email || 'nano-agent@users.noreply.github.com';
2878
2926
 
2879
2927
  const source =
2880
- (envName || envEmail) ? 'env'
2928
+ (env.name || env.email) ? 'env'
2881
2929
  : (g && (g.name || g.email)) ? 'git-global'
2882
2930
  : (gh && (gh.name || gh.email)) ? 'gh'
2883
2931
  : 'fallback';
@@ -2983,6 +3031,20 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
2983
3031
  const committer = resolveCommitterIdentity();
2984
3032
  runGit(['config', 'user.name', committer.name], { cwd: workspaceDir, env: gitEnv });
2985
3033
  runGit(['config', 'user.email', committer.email], { cwd: workspaceDir, env: gitEnv });
3034
+ // Config alone is not enough: git honours GIT_AUTHOR_*/GIT_COMMITTER_* OVER
3035
+ // user.name/user.email config, so a placeholder GIT_AUTHOR_EMAIL inherited from
3036
+ // the launch environment (e.g. `trial-merge@nano.local`) would still be stamped
3037
+ // onto commits even though we just wrote a clean identity into config. Pin all
3038
+ // four env vars to the resolved (already placeholder-sanitized) identity so
3039
+ // EVERY commit — finalizeGit's own rebase commits (which run with gitEnv) and
3040
+ // the harness's commits (extraEnv below carries these into harnessEnv) — uses
3041
+ // it deterministically, and a non-routable `*@nano.local` author can never be
3042
+ // written. When GIT_AUTHOR_* already held a real identity, resolveCommitterIdentity
3043
+ // returned it verbatim, so this is a no-op in that case.
3044
+ gitEnv.GIT_AUTHOR_NAME = committer.name;
3045
+ gitEnv.GIT_AUTHOR_EMAIL = committer.email;
3046
+ gitEnv.GIT_COMMITTER_NAME = committer.name;
3047
+ gitEnv.GIT_COMMITTER_EMAIL = committer.email;
2986
3048
 
2987
3049
  // Determine the working branch. With branch.create we make a real branch.
2988
3050
  // Otherwise we're on whatever the clone checked out: a branch only if
@@ -3003,7 +3065,7 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
3003
3065
  // `git rev-parse HEAD` on an unborn branch (freshly cloned empty repo) exits
3004
3066
  // non-zero and echoes the literal "HEAD" on stdout — treat that as "no base
3005
3067
  // commit" (empty startSha) rather than a bogus revision.
3006
- return { workspaceDir, gitEnv, startSha: sha.status === 0 ? (sha.stdout || '').trim() : '', workingBranch, detached: !workingBranch, ref: target || '', remote: redactToken(repo.url, token) };
3068
+ return { workspaceDir, gitEnv, committer, startSha: sha.status === 0 ? (sha.stdout || '').trim() : '', workingBranch, detached: !workingBranch, ref: target || '', remote: redactToken(repo.url, token) };
3007
3069
  }
3008
3070
 
3009
3071
  // Look up a PR for this branch (2a does NOT open it — the harness does, driven
@@ -4239,6 +4301,15 @@ async function workAgent(req, flags) {
4239
4301
  AGENT_REPO_URL: provisioned.remote,
4240
4302
  AGENT_REPO_BRANCH: provisioned.workingBranch || '',
4241
4303
  AGENT_REPO_REF: provisioned.ref || '',
4304
+ // Pin the harness's commit identity to the resolved (placeholder-
4305
+ // sanitized) committer so the agent's own `git commit` can't be
4306
+ // hijacked by a placeholder GIT_AUTHOR_* inherited from process.env
4307
+ // (git honours these over user.name/user.email config). Layered via
4308
+ // extraEnv so they override any inherited placeholder in harnessEnv.
4309
+ GIT_AUTHOR_NAME: provisioned.committer.name,
4310
+ GIT_AUTHOR_EMAIL: provisioned.committer.email,
4311
+ GIT_COMMITTER_NAME: provisioned.committer.name,
4312
+ GIT_COMMITTER_EMAIL: provisioned.committer.email,
4242
4313
  };
4243
4314
  } catch (err) {
4244
4315
  if (runDir) { try { rmSync(runDir, { recursive: true, force: true }); } catch { /* best effort */ } liveRunDirs.delete(runDir); }
@@ -7506,6 +7577,7 @@ export {
7506
7577
  finalizeGit,
7507
7578
  reconcileAgentPr,
7508
7579
  resolveCommitterIdentity,
7580
+ isPlaceholderEmail,
7509
7581
  postAgentAttribution,
7510
7582
  reapAgentRunDirs,
7511
7583
  authUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.33.0",
3
+ "version": "1.33.1",
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",
@@ -57,12 +57,12 @@
57
57
  },
58
58
  "optionalDependencies": {
59
59
  "node-pty": "^1.0.0",
60
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.33.0",
61
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.33.0",
62
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.33.0",
63
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.33.0",
64
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.33.0",
65
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.33.0",
66
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.33.0"
60
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.33.1",
61
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.33.1",
62
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.33.1",
63
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.33.1",
64
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.33.1",
65
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.33.1",
66
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.33.1"
67
67
  }
68
68
  }