@sagentlab/navarch-runtime 0.1.129 → 0.1.130

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 CHANGED
@@ -476,7 +476,7 @@ unchanged across the deployment.
476
476
  | `NAVARCH_OPENCODE_STARTUP_TIMEOUT_MS` | `120000` (2 min) | Stop OpenCode when it produces no stdout/stderr during startup. This catches provider auth/quota failures that leave its CLI process alive. `0` disables the startup deadline. |
477
477
  | `NAVARCH_WORKTREE_CLEANUP_INTERVAL_MS` | `3600000` (1 hour) | How often the runtime scans for session worktrees abandoned by a hard exit or host restart. |
478
478
  | `NAVARCH_WORKTREE_STALE_AFTER_MS` | `86400000` (24 hours) | Minimum inactivity age before an abandoned session worktree is removed. Active sessions are always protected. |
479
- | `NAVARCH_GIT_AUTHOR_NAME` / `NAVARCH_GIT_AUTHOR_EMAIL` | `sagentlab` / `z@sagentlab.com` | Git identity forced into session commits so host-level personal config is not inherited; override both for a project-authorized bot. |
479
+ | `NAVARCH_GIT_AUTHOR_NAME` / `NAVARCH_GIT_AUTHOR_EMAIL` | `sagentlab` / `z@sagentlab.com` | Git identity forced into session commits so host-level personal config is not inherited; override both for a project-authorized bot. For a project whose git proxy mode is not `off`, the control plane's commit identity (the GitHub App bot, or the consenting requester per the project's `git_attribution_mode`) replaces it, and a shared `prepare-commit-msg` hook adds `Navarch-Task` and any `Co-authored-by` trailer once. |
480
480
  | `NAVARCH_SANDBOX_MODE` | `host` | `host` uses the resources already available to the agent process. Set `docker` explicitly for container isolation. |
481
481
  | `NAVARCH_SANDBOX_PROFILE` | `trusted-development` | Named security profile for Docker sessions (`src/sandbox-profile.cts`): `trusted-development` (image-default user, uncapped, open egress), `untrusted-code` (non-root, 2 CPU / 4g / 512 PIDs, deny-by-default egress, read-only shared git), `elevated-verification` (non-root, 4 CPU / 8g / 2048 PIDs, egress limited to GitHub plus package registries). The default is the exact pre-profile flag set. |
482
482
  | `NAVARCH_SANDBOX_EGRESS_NETWORK` | _(unset)_ | Docker network that enforces a profile's egress allowlist. Docker cannot filter by domain itself, so an allowlist profile without this fails closed to `--network=none` and records the denial. |
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ // Commit attribution for proxied sessions (CX-3.7, issue #1546 "design 4/5").
3
+ //
4
+ // For a project whose git proxy mode is not `off`, the control plane resolves
5
+ // who the session's commits name and sends it as `repository.commit_identity`
6
+ // in the claim bundle (the git grant's `actor`/`attribution` claims carry the
7
+ // same identity). The runtime sets git's author and committer from it, and the
8
+ // shared `prepare-commit-msg` hook appends its trailers once per commit.
9
+ //
10
+ // Mirrors lib/navarch/commit-attribution.ts in the control plane, which this
11
+ // standalone package cannot import. Keep PREPARE_COMMIT_MSG_HOOK in sync; a
12
+ // root test compares the two.
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.PREPARE_COMMIT_MSG_HOOK = exports.COMMIT_TRAILER_CONFIG_KEY = void 0;
18
+ exports.parseCommitIdentity = parseCommitIdentity;
19
+ exports.commitIdentityGitConfig = commitIdentityGitConfig;
20
+ exports.applyCommitIdentityEnv = applyCommitIdentityEnv;
21
+ exports.installCommitTrailerHook = installCommitTrailerHook;
22
+ const promises_1 = __importDefault(require("node:fs/promises"));
23
+ const node_path_1 = __importDefault(require("node:path"));
24
+ /** Git config key the hook reads; multi-valued, one trailer per value. */
25
+ exports.COMMIT_TRAILER_CONFIG_KEY = "navarch.commitTrailer";
26
+ const MODES = ["bot", "human_coauthor", "human_author"];
27
+ const SAFE_VALUE = /^[^\r\n\0]{1,256}$/;
28
+ function parseIdentity(candidate) {
29
+ const c = candidate;
30
+ if (!c || typeof c.name !== "string" || typeof c.email !== "string")
31
+ return null;
32
+ if (!SAFE_VALUE.test(c.name) || !SAFE_VALUE.test(c.email) || /[<>]/.test(c.name) || /[<>\s]/.test(c.email))
33
+ return null;
34
+ return { name: c.name, email: c.email };
35
+ }
36
+ /** Validates the claim bundle's `commit_identity`. Null when absent or malformed (keep the configured identity). */
37
+ function parseCommitIdentity(value) {
38
+ const v = value;
39
+ if (!v || typeof v !== "object" || !MODES.includes(v.mode))
40
+ return null;
41
+ const author = parseIdentity(v.author);
42
+ const committer = parseIdentity(v.committer);
43
+ if (!author || !committer || !Array.isArray(v.trailers) || v.trailers.length > 8)
44
+ return null;
45
+ const trailers = [];
46
+ for (const trailer of v.trailers) {
47
+ if (typeof trailer !== "string" || !SAFE_VALUE.test(trailer) || !/^[A-Za-z0-9-]+: \S/.test(trailer))
48
+ return null;
49
+ trailers.push(trailer);
50
+ }
51
+ return { mode: v.mode, author, committer, trailers };
52
+ }
53
+ /** Git config entries for the identity: user.*, author.*, committer.* and the trailers. */
54
+ function commitIdentityGitConfig(identity) {
55
+ return [
56
+ ["user.name", identity.author.name],
57
+ ["user.email", identity.author.email],
58
+ ["author.name", identity.author.name],
59
+ ["author.email", identity.author.email],
60
+ ["committer.name", identity.committer.name],
61
+ ["committer.email", identity.committer.email],
62
+ ...identity.trailers.map((trailer) => [exports.COMMIT_TRAILER_CONFIG_KEY, trailer]),
63
+ ];
64
+ }
65
+ /**
66
+ * Appends git config entries to an env map that already uses
67
+ * `GIT_CONFIG_COUNT`/`GIT_CONFIG_KEY_n`/`GIT_CONFIG_VALUE_n` (toEnvMap). Later
68
+ * entries win for single-valued keys, so this replaces the default
69
+ * `user.name`/`user.email` (NAVARCH_GIT_AUTHOR_*) for this session only. The
70
+ * entries are session-scoped: nothing is written to the shared repository cache.
71
+ */
72
+ function applyCommitIdentityEnv(env, identity) {
73
+ const parsed = Number.parseInt(env.GIT_CONFIG_COUNT ?? "0", 10);
74
+ let count = Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
75
+ for (const [key, value] of commitIdentityGitConfig(identity)) {
76
+ env[`GIT_CONFIG_KEY_${count}`] = key;
77
+ env[`GIT_CONFIG_VALUE_${count}`] = value;
78
+ count += 1;
79
+ }
80
+ env.GIT_CONFIG_COUNT = String(count);
81
+ }
82
+ /**
83
+ * `prepare-commit-msg` hook. Inert unless the session set
84
+ * `navarch.commitTrailer`. `--if-exists addIfDifferent` makes it idempotent:
85
+ * an amend, reword or rebase that already carries a trailer does not get it
86
+ * twice. An empty message stays empty so git still aborts the commit.
87
+ * Git's own comment lines and the `commit -v` scissors section are set aside
88
+ * before the trailers are added (see the control-plane copy for details).
89
+ */
90
+ exports.PREPARE_COMMIT_MSG_HOOK = `#!/bin/sh
91
+ # Managed by Navarch (commit attribution, CX-3.7).
92
+ msg_file="$1"
93
+ source="$2"
94
+ [ -n "$msg_file" ] && [ -f "$msg_file" ] || exit 0
95
+ trailers="$(git config --get-all ${exports.COMMIT_TRAILER_CONFIG_KEY} 2>/dev/null)" || exit 0
96
+ [ -n "$trailers" ] || exit 0
97
+ cc="$(git config core.commentChar 2>/dev/null)"
98
+ [ -n "$cc" ] && [ "$cc" != auto ] || cc='#'
99
+ body="$msg_file.navarch-body"
100
+ rest="$msg_file.navarch-rest"
101
+ trap 'rm -f "$body" "$rest"' EXIT
102
+ : > "$body"
103
+ : > "$rest"
104
+ awk -v cc="$cc" -v src="$source" -v body="$body" -v rest="$rest" '
105
+ cut { print >> rest; next }
106
+ index($0, cc " ------------------------ >8 ------------------------") == 1 { cut = 1; print >> rest; next }
107
+ src != "message" && index($0, cc) == 1 { print >> rest; next }
108
+ { print >> body }
109
+ ' "$msg_file" || exit 1
110
+ grep -q '[^[:space:]]' "$body" || exit 0
111
+ printf '%s\\n' "$trailers" | while IFS= read -r trailer; do
112
+ [ -n "$trailer" ] || continue
113
+ git interpret-trailers --in-place --if-exists addIfDifferent --trailer "$trailer" "$body" || exit 1
114
+ done || exit 1
115
+ { cat "$body"; if [ -s "$rest" ]; then echo; cat "$rest"; fi; } > "$msg_file"
116
+ `;
117
+ /** Writes the hook into a hooks directory (the shared cache's, like the pre-push guard). */
118
+ async function installCommitTrailerHook(hooksDir) {
119
+ await promises_1.default.mkdir(hooksDir, { recursive: true });
120
+ await promises_1.default.writeFile(node_path_1.default.join(hooksDir, "prepare-commit-msg"), exports.PREPARE_COMMIT_MSG_HOOK, {
121
+ encoding: "utf8",
122
+ mode: 0o755,
123
+ });
124
+ }
@@ -11,6 +11,7 @@ const node_path_1 = __importDefault(require("node:path"));
11
11
  const worktree_janitor_cjs_1 = require("./worktree-janitor.cjs");
12
12
  const node_fs_1 = require("node:fs");
13
13
  const sandbox_cjs_1 = require("./sandbox.cjs");
14
+ const commit_attribution_cjs_1 = require("./commit-attribution.cjs");
14
15
  const repositoryLocks = new Map();
15
16
  /** Rewrites a github.com HTTPS clone URL onto the proxy; other URLs are returned unchanged. */
16
17
  function proxyCloneUrl(cloneUrl, proxyBaseUrl) {
@@ -51,6 +52,7 @@ class GitWorktree {
51
52
  workingBranch;
52
53
  repoLocalGithubToken;
53
54
  prContinuation;
55
+ ownershipCommitIdentity;
54
56
  taskBranchSuffix;
55
57
  legacyTaskBranchSuffix;
56
58
  taskOwnershipTrailer;
@@ -93,6 +95,7 @@ class GitWorktree {
93
95
  this.workingBranch = options.workingBranch?.trim() || null;
94
96
  this.repoLocalGithubToken = options.repoLocalGithubToken;
95
97
  this.prContinuation = options.prContinuation ?? false;
98
+ this.ownershipCommitIdentity = options.ownershipCommitIdentity ?? { name: "Navarch", email: "runtime@navarch.local" };
96
99
  this.hostedCheckpointRef = options.restoreHostedCheckpoint ? hostedCheckpointRef(options.taskId) : null;
97
100
  this.hostedCheckpointLocalRef = `refs/navarch/restored-checkpoints/${taskKey}`;
98
101
  }
@@ -155,6 +158,9 @@ class GitWorktree {
155
158
  encoding: "utf8",
156
159
  mode: 0o755,
157
160
  });
161
+ // Commit attribution trailers (CX-3.7). Like the guard, the hook is inert
162
+ // unless the session's own git config sets `navarch.commitTrailer`.
163
+ await (0, commit_attribution_cjs_1.installCommitTrailerHook)(hooksDir);
158
164
  }
159
165
  /** Clones the bare cache if missing, repoints origin if needed, and fetches all branches. */
160
166
  async ensureRepositoryCache() {
@@ -500,9 +506,9 @@ class GitWorktree {
500
506
  "-C",
501
507
  this.worktreePath,
502
508
  "-c",
503
- "user.name=Navarch",
509
+ `user.name=${this.ownershipCommitIdentity.name}`,
504
510
  "-c",
505
- "user.email=runtime@navarch.local",
511
+ `user.email=${this.ownershipCommitIdentity.email}`,
506
512
  "commit",
507
513
  "--allow-empty",
508
514
  "-m",
package/dist/session.cjs CHANGED
@@ -38,6 +38,7 @@ const agent_model_cjs_1 = require("./agent-model.cjs");
38
38
  const github_credential_cjs_1 = require("./github-credential.cjs");
39
39
  const browser_evidence_cjs_1 = require("./browser-evidence.cjs");
40
40
  const git_grant_cjs_1 = require("./git-grant.cjs");
41
+ const commit_attribution_cjs_1 = require("./commit-attribution.cjs");
41
42
  /** Filename the generated platform MCP config is written under inside the session metadata directory. */
42
43
  const MCP_CONFIG_FILENAME = "mcp-config.json";
43
44
  const GIT_CREDENTIAL_HELPER_FILENAME = "git-credential-navarch.cjs";
@@ -248,6 +249,11 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
248
249
  };
249
250
  }
250
251
  const sessionEnv = toEnvMap(secrets, gitCredentialRefresh, config.gitAuthorName, config.gitAuthorEmail, gitGrantEnv);
252
+ // Proxied projects (CX-3.7): the control-plane identity replaces the
253
+ // NAVARCH_GIT_AUTHOR_* default for this session; mode `off` sends none.
254
+ const commitIdentity = (0, commit_attribution_cjs_1.parseCommitIdentity)(bundle.repository?.commit_identity);
255
+ if (commitIdentity)
256
+ (0, commit_attribution_cjs_1.applyCommitIdentityEnv)(sessionEnv, commitIdentity);
251
257
  const ghConfigDir = (0, worktree_guard_cjs_1.sessionGhConfigDir)(workDir);
252
258
  await node_fs_1.promises.mkdir(ghConfigDir, { recursive: true, mode: 0o700 });
253
259
  sessionEnv.GH_CONFIG_DIR = ghConfigDir;
@@ -305,6 +311,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
305
311
  : undefined,
306
312
  // Hosted runs skip checkpoints for verify tasks, which deliver a verdict.
307
313
  restoreHostedCheckpoint: !allowBranchRename && task.task_type !== "verify",
314
+ ...(commitIdentity ? { ownershipCommitIdentity: commitIdentity.committer } : {}),
308
315
  });
309
316
  const knownGuidanceIds = new Set((bundle.guidance ?? []).map((entry) => entry.id));
310
317
  const deliveredGuidance = [...(bundle.guidance ?? [])];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.129",
3
+ "version": "0.1.130",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through local coding agents.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",