@sagentlab/navarch-runtime 0.1.71 → 0.1.73
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/browser-evidence.cjs +42 -27
- package/dist/git-worktree.cjs +39 -3
- package/package.json +1 -1
|
@@ -96,40 +96,55 @@ async function startBrowserEvidence(options) {
|
|
|
96
96
|
throw new Error("Screenshot exceeds 25 MiB.");
|
|
97
97
|
assertActive();
|
|
98
98
|
const fileName = `${args.evidence_role}-${viewport}-${(0, node_crypto_1.randomUUID)()}.png`;
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
} })
|
|
99
|
+
const callTool = async (name, toolArgs) => {
|
|
100
|
+
const response = await fetchImpl(`${options.apiBase.replace(/\/+$/, "")}/api/mcp`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
signal: AbortSignal.timeout(30_000),
|
|
103
|
+
headers: {
|
|
104
|
+
authorization: `Bearer ${options.machineToken}`,
|
|
105
|
+
"x-navarch-lease-id": options.leaseId,
|
|
106
|
+
"content-type": "application/json",
|
|
107
|
+
accept: "application/json, text/event-stream",
|
|
108
|
+
"mcp-protocol-version": "2025-06-18",
|
|
109
|
+
},
|
|
110
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: (0, node_crypto_1.randomUUID)(), method: "tools/call", params: {
|
|
111
|
+
name, arguments: toolArgs,
|
|
112
|
+
} }),
|
|
113
|
+
});
|
|
114
|
+
if (!response.ok)
|
|
115
|
+
throw new Error(`Artifact ${name} returned HTTP ${response.status}.`);
|
|
116
|
+
const result = await response.json();
|
|
117
|
+
if (result.error || result.result?.isError || !result.result?.structuredContent) {
|
|
118
|
+
throw new Error(`Artifact ${name} was rejected.`);
|
|
119
|
+
}
|
|
120
|
+
return result.result.structuredContent;
|
|
121
|
+
};
|
|
122
|
+
const registered = await callTool("register_artifact", {
|
|
123
|
+
file_name: fileName, mime_type: "image/png", size_bytes: bytes.length,
|
|
124
|
+
checksum_sha256: (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex"),
|
|
125
|
+
source_commit_sha: args.source_commit_sha, evidence_role: args.evidence_role,
|
|
116
126
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const destination = result.result?.structuredContent;
|
|
121
|
-
if (result.error || result.result?.isError || !destination?.upload_url || !destination.public_url
|
|
122
|
-
|| new URL(destination.upload_url).protocol !== "https:" || new URL(destination.public_url).protocol !== "https:") {
|
|
123
|
-
throw new Error("Artifact registration did not provide secure upload and public URLs.");
|
|
127
|
+
const { upload_url: uploadUrl, artifact_version_id: versionId } = registered;
|
|
128
|
+
if (typeof uploadUrl !== "string" || typeof versionId !== "string" || new URL(uploadUrl).protocol !== "https:") {
|
|
129
|
+
throw new Error("Artifact registration did not provide a secure upload URL.");
|
|
124
130
|
}
|
|
125
131
|
assertActive();
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
// The signed URL is creation-only: Content-Type and If-None-Match are
|
|
133
|
+
// signed headers, so the PUT must send both or storage returns 403.
|
|
134
|
+
const upload = await fetchImpl(uploadUrl, {
|
|
135
|
+
method: "PUT", headers: { "content-type": "image/png", "if-none-match": "*" },
|
|
128
136
|
body: new Uint8Array(bytes), signal: AbortSignal.timeout(30_000),
|
|
129
137
|
});
|
|
130
138
|
if (!upload.ok)
|
|
131
139
|
throw new Error(`Screenshot upload returned HTTP ${upload.status}.`);
|
|
132
|
-
|
|
140
|
+
assertActive();
|
|
141
|
+
// The artifact is not published (or completion-eligible) until finalized.
|
|
142
|
+
const finalized = await callTool("finalize_artifact", { artifact_version_id: versionId });
|
|
143
|
+
const publicUrl = finalized.public_url;
|
|
144
|
+
if (finalized.artifact_version_id !== versionId || typeof publicUrl !== "string" || new URL(publicUrl).protocol !== "https:") {
|
|
145
|
+
throw new Error("Artifact finalization did not provide a secure public URL.");
|
|
146
|
+
}
|
|
147
|
+
const item = { fileName, publicUrl,
|
|
133
148
|
sourceCommitSha: args.source_commit_sha, role: args.evidence_role, viewport };
|
|
134
149
|
evidence.push(item);
|
|
135
150
|
captured.push(item);
|
package/dist/git-worktree.cjs
CHANGED
|
@@ -11,6 +11,14 @@ const worktree_janitor_cjs_1 = require("./worktree-janitor.cjs");
|
|
|
11
11
|
const node_fs_1 = require("node:fs");
|
|
12
12
|
const sandbox_cjs_1 = require("./sandbox.cjs");
|
|
13
13
|
const repositoryLocks = new Map();
|
|
14
|
+
/**
|
|
15
|
+
* A freshly minted installation token can be rejected for a second or two
|
|
16
|
+
* before GitHub propagates it, and git reports that 404 on a private repo as
|
|
17
|
+
* "Repository not found". Every lease gets a new token, so the first clone
|
|
18
|
+
* right after a claim can land in that window.
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_AUTH_RETRY_DELAYS_MS = [1_000, 2_000, 4_000];
|
|
21
|
+
const TRANSIENT_GIT_AUTH_FAILURE = /Repository not found|Authentication failed|could not read Username|The requested URL returned error: (?:401|403|404)|\bHTTP (?:401|403|404)\b/i;
|
|
14
22
|
/**
|
|
15
23
|
* Maintains one bare repository cache per project and checks out each session
|
|
16
24
|
* into its own uniquely named worktree. Sessions that need a repo-local secret
|
|
@@ -27,6 +35,8 @@ class GitWorktree {
|
|
|
27
35
|
/** Why an existing hosted checkpoint could not be restored, for the session log. */
|
|
28
36
|
hostedCheckpointRestoreFailure = null;
|
|
29
37
|
runner;
|
|
38
|
+
authRetryDelaysMs;
|
|
39
|
+
sleep;
|
|
30
40
|
checkpointRoot;
|
|
31
41
|
cloneUrl;
|
|
32
42
|
githubToken;
|
|
@@ -64,6 +74,8 @@ class GitWorktree {
|
|
|
64
74
|
this.branch = `navarch/${branchSlug(options)}${this.taskBranchSuffix}` +
|
|
65
75
|
(options.prContinuation ? `-${sessionKey}` : "");
|
|
66
76
|
this.runner = options.runner ?? sandbox_cjs_1.nodeCommandRunner;
|
|
77
|
+
this.authRetryDelaysMs = options.authRetryDelaysMs ?? DEFAULT_AUTH_RETRY_DELAYS_MS;
|
|
78
|
+
this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
67
79
|
this.cloneUrl = options.cloneUrl;
|
|
68
80
|
this.githubToken = options.githubToken;
|
|
69
81
|
this.repoLocalGithubToken = options.repoLocalGithubToken;
|
|
@@ -134,7 +146,11 @@ class GitWorktree {
|
|
|
134
146
|
/** Clones the bare cache if missing, repoints origin if needed, and fetches all branches. */
|
|
135
147
|
async ensureRepositoryCache() {
|
|
136
148
|
if (!(await pathExists(node_path_1.default.join(this.repositoryPath, "HEAD")))) {
|
|
137
|
-
await this.
|
|
149
|
+
await this.runAuthenticatedGitWithRetry(["clone", "--bare", this.cloneUrl, this.repositoryPath],
|
|
150
|
+
// A failed clone can leave a partial bare repository behind, and git
|
|
151
|
+
// refuses to clone into a non-empty directory. No HEAD means nothing
|
|
152
|
+
// usable (or shared) lives there yet.
|
|
153
|
+
() => node_fs_1.promises.rm(this.repositoryPath, { recursive: true, force: true }));
|
|
138
154
|
}
|
|
139
155
|
else {
|
|
140
156
|
const origin = await this.runGit(["--git-dir", this.repositoryPath, "remote", "get-url", "origin"], false);
|
|
@@ -142,14 +158,34 @@ class GitWorktree {
|
|
|
142
158
|
await this.runGit(["--git-dir", this.repositoryPath, "remote", "set-url", "origin", this.cloneUrl], false);
|
|
143
159
|
}
|
|
144
160
|
}
|
|
145
|
-
await this.
|
|
161
|
+
await this.runAuthenticatedGitWithRetry([
|
|
146
162
|
"--git-dir",
|
|
147
163
|
this.repositoryPath,
|
|
148
164
|
"fetch",
|
|
149
165
|
"--prune",
|
|
150
166
|
"origin",
|
|
151
167
|
"+refs/heads/*:refs/remotes/origin/*",
|
|
152
|
-
]
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Runs a token-authenticated git command, retrying with backoff while GitHub
|
|
172
|
+
* rejects the credential in a way a not-yet-propagated token produces.
|
|
173
|
+
* Without a token such a failure is a real access problem and fails at once.
|
|
174
|
+
*/
|
|
175
|
+
async runAuthenticatedGitWithRetry(args, beforeRetry) {
|
|
176
|
+
for (let attempt = 0;; attempt += 1) {
|
|
177
|
+
try {
|
|
178
|
+
return await this.runGit(args, true);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
const delay = this.authRetryDelaysMs[attempt];
|
|
182
|
+
if (!this.githubToken || delay === undefined || !TRANSIENT_GIT_AUTH_FAILURE.test(errorMessage(error))) {
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
await this.sleep(delay);
|
|
186
|
+
await beforeRetry?.();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
153
189
|
}
|
|
154
190
|
async addSessionWorktree() {
|
|
155
191
|
// A failed session may have pushed before it could report completion. The
|
package/package.json
CHANGED