flowviant 0.103.0 → 0.104.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/bin/cli.mjs +87 -474
- package/bin/lib/admission.mjs +19 -3
- package/bin/lib/agentPlan.mjs +2 -2
- package/bin/lib/agentTaskKinds.mjs +93 -0
- package/bin/lib/agySessions.mjs +158 -0
- package/bin/lib/artifactPolicy.mjs +44 -0
- package/bin/lib/artifactPreview.mjs +52 -0
- package/bin/lib/artifactReporter.mjs +163 -0
- package/bin/lib/artifacts.mjs +47 -227
- package/bin/lib/authReject.mjs +16 -4
- package/bin/lib/authproxy.mjs +47 -304
- package/bin/lib/boxIdentity.mjs +245 -0
- package/bin/lib/childEnv.mjs +11 -9
- package/bin/lib/claude.mjs +14 -851
- package/bin/lib/claudeAuth.mjs +3 -3
- package/bin/lib/claudePosture.mjs +367 -0
- package/bin/lib/claudeSessions.mjs +411 -0
- package/bin/lib/claudeStream.mjs +207 -0
- package/bin/lib/cliLimit.mjs +34 -0
- package/bin/lib/commandSpecs.mjs +173 -0
- package/bin/lib/config.mjs +11 -42
- package/bin/lib/credentialDate.mjs +22 -0
- package/bin/lib/credentials.mjs +21 -26
- package/bin/lib/daemonLogging.mjs +55 -0
- package/bin/lib/daemonState.mjs +64 -0
- package/bin/lib/deploy.mjs +148 -647
- package/bin/lib/deployCheckout.mjs +265 -0
- package/bin/lib/deployConfig.mjs +142 -0
- package/bin/lib/deployRunner.mjs +311 -0
- package/bin/lib/deployWire.mjs +41 -0
- package/bin/lib/desktopContract.mjs +56 -105
- package/bin/lib/env.mjs +49 -484
- package/bin/lib/fleet.mjs +183 -2383
- package/bin/lib/fleetJobs.mjs +120 -0
- package/bin/lib/fleetPost.mjs +40 -0
- package/bin/lib/fleetReports.mjs +392 -0
- package/bin/lib/fleetRoster.mjs +470 -0
- package/bin/lib/fleetStartup.mjs +196 -0
- package/bin/lib/fleetWake.mjs +45 -0
- package/bin/lib/fleetWire.mjs +46 -0
- package/bin/lib/git.mjs +2 -2
- package/bin/lib/gitMergeIdentity.mjs +51 -0
- package/bin/lib/grant.mjs +5 -3
- package/bin/lib/help.mjs +3 -125
- package/bin/lib/holder.mjs +482 -0
- package/bin/lib/instance.mjs +124 -46
- package/bin/lib/knowledge.mjs +16 -799
- package/bin/lib/knowledgeFetch.mjs +38 -0
- package/bin/lib/knowledgeFileSync.mjs +112 -0
- package/bin/lib/knowledgeFiles.mjs +487 -0
- package/bin/lib/knowledgeLibrary.mjs +249 -0
- package/bin/lib/landed.mjs +2 -1
- package/bin/lib/listeners.mjs +99 -141
- package/bin/lib/localSessions.mjs +30 -555
- package/bin/lib/login.mjs +3 -2
- package/bin/lib/loginPath.mjs +3 -5
- package/bin/lib/machineDisconnect.mjs +203 -0
- package/bin/lib/machineEnv.mjs +22 -0
- package/bin/lib/machineLimits.mjs +61 -0
- package/bin/lib/machineListing.mjs +338 -0
- package/bin/lib/machines.mjs +78 -455
- package/bin/lib/machinesCommand.mjs +214 -0
- package/bin/lib/mcp-cli.mjs +2 -1
- package/bin/lib/patchRevert.mjs +60 -0
- package/bin/lib/prWorkflow.mjs +157 -0
- package/bin/lib/preflight.mjs +2 -1
- package/bin/lib/pressurePolicy.mjs +104 -0
- package/bin/lib/preview.mjs +2 -272
- package/bin/lib/previewFramePolicy.mjs +112 -0
- package/bin/lib/previewGatePolicy.mjs +236 -0
- package/bin/lib/previewRegistry.mjs +235 -0
- package/bin/lib/printable.mjs +2 -2
- package/bin/lib/procRegistry.mjs +28 -18
- package/bin/lib/procScan.mjs +146 -0
- package/bin/lib/processes.mjs +29 -37
- package/bin/lib/projectToolRuntimes.mjs +138 -0
- package/bin/lib/projectTools.mjs +13 -88
- package/bin/lib/prompts.mjs +28 -42
- package/bin/lib/repoPath.mjs +26 -0
- package/bin/lib/resources.mjs +7 -69
- package/bin/lib/resumeLost.mjs +107 -0
- package/bin/lib/runTurn.mjs +324 -0
- package/bin/lib/runtimeAntigravity.mjs +230 -0
- package/bin/lib/runtimeCapabilities.mjs +334 -0
- package/bin/lib/runtimeClaude.mjs +126 -0
- package/bin/lib/runtimeCodex.mjs +217 -0
- package/bin/lib/runtimeDetection.mjs +121 -0
- package/bin/lib/runtimeEvents.mjs +482 -0
- package/bin/lib/runtimeOfferings.mjs +4 -1
- package/bin/lib/runtimes.mjs +42 -1402
- package/bin/lib/safeFileName.mjs +74 -0
- package/bin/lib/standDownExit.mjs +181 -0
- package/bin/lib/standDownGate.mjs +48 -0
- package/bin/lib/startProject.mjs +279 -0
- package/bin/lib/trace.mjs +3 -3
- package/bin/lib/turnLock.mjs +68 -0
- package/bin/lib/turnProfile.mjs +77 -0
- package/bin/lib/uninstall.mjs +55 -45
- package/bin/lib/uninstallCopies.mjs +112 -0
- package/bin/lib/update.mjs +30 -17
- package/bin/lib/uplinkScrub.mjs +265 -0
- package/bin/lib/vault.mjs +22 -151
- package/bin/lib/vaultDiff.mjs +183 -0
- package/bin/lib/versionOrder.mjs +42 -0
- package/bin/lib/views.mjs +17 -17
- package/bin/lib/wikiRunner.mjs +630 -0
- package/bin/lib/work.mjs +188 -3034
- package/bin/lib/workAdoptCarry.mjs +94 -0
- package/bin/lib/workAgentCheck.mjs +278 -0
- package/bin/lib/workAgentMerges.mjs +54 -142
- package/bin/lib/workAgentPlans.mjs +14 -12
- package/bin/lib/workAgentPrecheck.mjs +393 -0
- package/bin/lib/workAgentPublish.mjs +177 -0
- package/bin/lib/workAgentReview.mjs +8 -666
- package/bin/lib/workAgentTurnBegun.mjs +109 -0
- package/bin/lib/workAgentTurnExecution.mjs +604 -0
- package/bin/lib/workAgentTurnOutcome.mjs +207 -0
- package/bin/lib/workAgentTurnReports.mjs +189 -0
- package/bin/lib/workAgentTurns.mjs +91 -898
- package/bin/lib/workAttachments.mjs +93 -0
- package/bin/lib/workBrain.mjs +53 -0
- package/bin/lib/workChildren.mjs +110 -0
- package/bin/lib/workCommandAudit.mjs +62 -0
- package/bin/lib/workDiffs.mjs +2 -1
- package/bin/lib/workNarration.mjs +152 -0
- package/bin/lib/workPlaceReports.mjs +426 -0
- package/bin/lib/workPlaces.mjs +228 -0
- package/bin/lib/workPreviews.mjs +31 -9
- package/bin/lib/workProcesses.mjs +4 -5
- package/bin/lib/workPullRequests.mjs +52 -126
- package/bin/lib/workReportQueue.mjs +254 -0
- package/bin/lib/workRetire.mjs +185 -0
- package/bin/lib/workSessionGroups.mjs +166 -0
- package/bin/lib/workSessionRuntime.mjs +164 -0
- package/bin/lib/workSessionTokens.mjs +68 -0
- package/bin/lib/workSessionTurns.mjs +892 -0
- package/bin/lib/workShip.mjs +9 -29
- package/bin/lib/workToolLog.mjs +76 -0
- package/package.json +3 -2
- package/bin/lib/patch.mjs +0 -363
package/bin/lib/artifacts.mjs
CHANGED
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
* any of them. What they cannot be is REWRITTEN: `envScrub` swapping bytes
|
|
52
52
|
* inside one corrupts it, and a corrupted binary served as the thing the agent
|
|
53
53
|
* made is worse than none. So each binary is checked against the same scrub
|
|
54
|
-
* list as exact byte substrings (`secretIn`,
|
|
54
|
+
* list as exact byte substrings (`secretIn`, uplinkScrub.mjs) before it is uploaded,
|
|
55
55
|
* and a hit SKIPS the file with a warn line naming it and the variable — never
|
|
56
56
|
* a redacted copy, never a by-name row the page would draw as a file it merely
|
|
57
57
|
* could not show. STATED: a value inside a deflated stream is not a substring
|
|
@@ -60,73 +60,49 @@
|
|
|
60
60
|
* The SERVER computes the sha256 of what it stored; the one sent here is for
|
|
61
61
|
* the log.
|
|
62
62
|
*
|
|
63
|
-
* ── DELIVERY ──
|
|
63
|
+
* ── DELIVERY LIVES NEXT DOOR (2026-09-26) ──
|
|
64
64
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* scrubbed — and retries it on the next report beat, the settle's shape, at
|
|
71
|
-
* most `MAX_TRIES` times; a newer copy of the same file replaces a held one.
|
|
65
|
+
* This module is the DISK half: scan, diff against the snapshot, and prepare
|
|
66
|
+
* one upload body (scrubbed text, withheld binaries). The screenshot of an
|
|
67
|
+
* HTML page is `artifactPreview.mjs` (a browser); the queued POST with its
|
|
68
|
+
* bounded retries is `artifactReporter.mjs` (the network). Three reasons to
|
|
69
|
+
* change, three files — split from one 439-line module (SOLID F051).
|
|
72
70
|
*
|
|
73
71
|
* NO VERSION FLOOR: a daemon→server report on a new endpoint. The prompt
|
|
74
72
|
* paragraph is gated instead, on the roster's `artifactsAccepted` — a server
|
|
75
73
|
* that cannot show an artifact must not have the CLI told it will.
|
|
76
74
|
*/
|
|
77
75
|
|
|
78
|
-
import { constants, closeSync, fstatSync, lstatSync, openSync, readdirSync, readSync
|
|
76
|
+
import { constants, closeSync, fstatSync, lstatSync, openSync, readdirSync, readSync } from 'node:fs';
|
|
79
77
|
import { createHash } from 'node:crypto';
|
|
80
78
|
import { join } from 'node:path';
|
|
81
|
-
import {
|
|
82
|
-
import {
|
|
83
|
-
|
|
79
|
+
import { cutKeepingExtension } from './safeFileName.mjs';
|
|
80
|
+
import {
|
|
81
|
+
ARTIFACT_BINARY_MODEL_EXTS,
|
|
82
|
+
ARTIFACT_BINARY_MODEL_MAX_BYTES,
|
|
83
|
+
ARTIFACT_MAX_BYTES,
|
|
84
|
+
ARTIFACT_MAX_PER_OWNER,
|
|
85
|
+
ARTIFACT_TYPES,
|
|
86
|
+
} from './artifactPolicy.mjs';
|
|
84
87
|
|
|
85
88
|
/** Relative to the directory the turn stands in. Under `.flowviant/`, which
|
|
86
89
|
* the exclude file already hides from git (`excludeInWorktree`). */
|
|
87
90
|
export const ARTIFACT_DIR = '.flowviant/artifacts';
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const MAX_PENDING = 40;
|
|
101
|
-
|
|
102
|
-
/** The allowlist, by extension. The server's `ARTIFACT_TYPES`, repeated. */
|
|
103
|
-
export const ARTIFACT_TYPES = {
|
|
104
|
-
html: { mime: 'text/html', text: true },
|
|
105
|
-
htm: { mime: 'text/html', text: true },
|
|
106
|
-
md: { mime: 'text/markdown', text: true },
|
|
107
|
-
svg: { mime: 'image/svg+xml', text: true },
|
|
108
|
-
png: { mime: 'image/png', text: false },
|
|
109
|
-
jpg: { mime: 'image/jpeg', text: false },
|
|
110
|
-
jpeg: { mime: 'image/jpeg', text: false },
|
|
111
|
-
gif: { mime: 'image/gif', text: false },
|
|
112
|
-
webp: { mime: 'image/webp', text: false },
|
|
113
|
-
json: { mime: 'application/json', text: true },
|
|
114
|
-
csv: { mime: 'text/csv', text: true },
|
|
115
|
-
txt: { mime: 'text/plain', text: true },
|
|
116
|
-
// DOCUMENTS (0.97.0): a Word file, a deck, a sheet or a PDF — what the
|
|
117
|
-
// machine's docx/pptx/xlsx/pdf skills write. Binary, so never REWRITTEN (a
|
|
118
|
-
// scrub inside a zip or a PDF corrupts it) but checked for a known secret's
|
|
119
|
-
// bytes and withheld on a hit — see the header. Under the same cap. The app
|
|
120
|
-
// offers the first three as a download and frames the PDF.
|
|
121
|
-
docx: { mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', text: false },
|
|
122
|
-
pptx: { mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', text: false },
|
|
123
|
-
xlsx: { mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', text: false },
|
|
124
|
-
pdf: { mime: 'application/pdf', text: false },
|
|
125
|
-
gltf: { mime: 'model/gltf+json', text: true },
|
|
126
|
-
obj: { mime: 'model/obj', text: true },
|
|
127
|
-
glb: { mime: 'model/gltf-binary', text: false },
|
|
128
|
-
bin: { mime: 'application/octet-stream', text: false },
|
|
91
|
+
/**
|
|
92
|
+
* THE ALLOWLIST AND THE CEILINGS are the server's, from its GENERATED snapshot
|
|
93
|
+
* (`artifactPolicy.mjs`, rendered from packages/shared/src/schemas/
|
|
94
|
+
* artifact.schema.ts — 2026-09-26, SOLID F044). They were hand-repeated here,
|
|
95
|
+
* and a type added on one side only was either reported by name while the
|
|
96
|
+
* server could serve it, or sent as a type the server refuses. Re-checked here
|
|
97
|
+
* all the same: this is somebody's disk, and one place checking is one deploy
|
|
98
|
+
* from zero.
|
|
99
|
+
*/
|
|
100
|
+
export const artifactMaxBytesFor = (name) => {
|
|
101
|
+
const ext = String(name).slice(String(name).lastIndexOf('.') + 1).toLowerCase();
|
|
102
|
+
return ARTIFACT_BINARY_MODEL_EXTS.includes(ext) ? ARTIFACT_BINARY_MODEL_MAX_BYTES : ARTIFACT_MAX_BYTES;
|
|
129
103
|
};
|
|
104
|
+
/** Listed per scan, newest first — the server keeps as many per owner. */
|
|
105
|
+
export const ARTIFACT_MAX_FILES = ARTIFACT_MAX_PER_OWNER;
|
|
130
106
|
|
|
131
107
|
export function artifactTypeFor(name) {
|
|
132
108
|
const dot = String(name).lastIndexOf('.');
|
|
@@ -193,6 +169,12 @@ export function changedArtifacts(before, entries) {
|
|
|
193
169
|
return entries.filter((e) => before?.get(e.name) !== sigOf(e));
|
|
194
170
|
}
|
|
195
171
|
|
|
172
|
+
/** What the turn wrote in `placeDir` since `before` — the scanner the reporter
|
|
173
|
+
* is handed (`artifactReporter.mjs`), as one function. */
|
|
174
|
+
export function changedSince(placeDir, before) {
|
|
175
|
+
return changedArtifacts(before, scanArtifacts(placeDir));
|
|
176
|
+
}
|
|
177
|
+
|
|
196
178
|
/** Read a regular file WITHOUT following a symlink planted after the lstat. */
|
|
197
179
|
function readNoFollow(path, limit) {
|
|
198
180
|
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
@@ -212,38 +194,23 @@ function readNoFollow(path, limit) {
|
|
|
212
194
|
}
|
|
213
195
|
}
|
|
214
196
|
|
|
215
|
-
/** FNV-1a, 32-bit, as 8 hex — verbatim against the server's `fnv1a8`
|
|
216
|
-
* (apps/api/src/routes/sessionsAttachments.routes.ts). */
|
|
217
|
-
function artifactFnv1a8(s) {
|
|
218
|
-
let h = 0x811c9dc5;
|
|
219
|
-
for (let i = 0; i < s.length; i++) {
|
|
220
|
-
h ^= s.charCodeAt(i);
|
|
221
|
-
h = Math.imul(h, 0x01000193);
|
|
222
|
-
}
|
|
223
|
-
return (h >>> 0).toString(16).padStart(8, '0');
|
|
224
|
-
}
|
|
225
|
-
const ARTIFACT_SAFE_EXT_RE = /^[A-Za-z0-9]{1,10}$/;
|
|
226
197
|
/**
|
|
227
|
-
* `safeFileName
|
|
228
|
-
*
|
|
198
|
+
* `safeFileName`'s CUT (bin/lib/safeFileName.mjs — `cutKeepingExtension`, the
|
|
199
|
+
* server's rule): keeps the extension through a cut. Applied to each relative
|
|
229
200
|
* path component, so a model under `scene/` keeps that relation to its page.
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
201
|
+
* The context rule around the cut is this lane's own: a component that is
|
|
202
|
+
* already safe — spaces included — passes through untouched, because a bundle
|
|
203
|
+
* path names files the page references by exactly that spelling. The cut only
|
|
204
|
+
* differs from the component on an over-long name — but that is exactly the
|
|
205
|
+
* case the server's own `safeFileName(rawName)` would otherwise re-cut
|
|
206
|
+
* DIFFERENTLY from whatever this reported, so applying the same cut here means
|
|
207
|
+
* the server's re-application is a no-op and the name a design or research
|
|
208
|
+
* delivery is judged by is the one this machine reported.
|
|
236
209
|
*/
|
|
237
210
|
function safeArtifactName(raw) {
|
|
238
211
|
if (typeof raw === 'string' && raw.length <= 80 && /^[A-Za-z0-9][A-Za-z0-9._ -]*$/.test(raw) && !raw.endsWith('.')) return raw;
|
|
239
212
|
const clean = String(raw ?? '').replace(/[^A-Za-z0-9._-]/g, '_');
|
|
240
|
-
|
|
241
|
-
const dot = clean.lastIndexOf('.');
|
|
242
|
-
const ext = dot > 0 && ARTIFACT_SAFE_EXT_RE.test(clean.slice(dot + 1)) ? clean.slice(dot + 1) : '';
|
|
243
|
-
const stem = ext ? clean.slice(0, dot) : clean;
|
|
244
|
-
const tag = `-${artifactFnv1a8(clean)}`;
|
|
245
|
-
const room = 80 - tag.length - (ext ? ext.length + 1 : 0);
|
|
246
|
-
return `${stem.slice(0, room)}${tag}${ext ? `.${ext}` : ''}`;
|
|
213
|
+
return clean ? cutKeepingExtension(clean) : 'artifact';
|
|
247
214
|
}
|
|
248
215
|
|
|
249
216
|
/**
|
|
@@ -290,150 +257,3 @@ export function buildArtifactUpload(entry, { sessionId, agentId, turnId }, scrub
|
|
|
290
257
|
bytes,
|
|
291
258
|
};
|
|
292
259
|
}
|
|
293
|
-
|
|
294
|
-
/** Render the scrubbed HTML, never the checkout's unsanitized source. A
|
|
295
|
-
* failed browser measurement is explicit; older daemon reports are absent. */
|
|
296
|
-
export async function renderDesignPreview(bytes, capture = captureScreenshot) {
|
|
297
|
-
const dir = mkdtempSync(join(tmpdir(), 'flowviant-design-'));
|
|
298
|
-
try {
|
|
299
|
-
const html = join(dir, 'design.html');
|
|
300
|
-
const png = join(dir, 'design.png');
|
|
301
|
-
// A fresh Chrome profile has no Flowviant credentials. Put the artifact
|
|
302
|
-
// box's network fence before any model-written markup executes.
|
|
303
|
-
writeFileSync(html, Buffer.concat([
|
|
304
|
-
Buffer.from(`<meta http-equiv="Content-Security-Policy" content="${PREVIEW_CSP}">`),
|
|
305
|
-
bytes,
|
|
306
|
-
]));
|
|
307
|
-
const result = await capture({ url: pathToFileURL(html).href, out: png, width: 720, height: 450, timeoutMs: 15_000 });
|
|
308
|
-
if (!result.ok) return { renderState: 'unavailable' };
|
|
309
|
-
const image = readFileSync(png);
|
|
310
|
-
if (image.byteLength > DESIGN_PREVIEW_MAX_BYTES || !image.subarray(0, 8).equals(Buffer.from([137,80,78,71,13,10,26,10]))) {
|
|
311
|
-
return { renderState: 'unavailable' };
|
|
312
|
-
}
|
|
313
|
-
return { renderState: 'rendered', preview: image };
|
|
314
|
-
} catch {
|
|
315
|
-
return { renderState: 'unavailable' };
|
|
316
|
-
} finally {
|
|
317
|
-
rmSync(dir, { recursive: true, force: true });
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* The reporter a work manager holds for its life. `report` is called once per
|
|
323
|
-
* finished turn and never awaited by the turn (a slow uplink must not hold the
|
|
324
|
-
* next turn behind a readout); `retryPending` rides the settle retry beat.
|
|
325
|
-
*/
|
|
326
|
-
export function createArtifactReporter({ fleetUrl, token, userAgent, scrub, secretIn, fetchImpl, log = () => {} }) {
|
|
327
|
-
const url = String(fleetUrl).replace(/\/agents\/?$/, '/artifact');
|
|
328
|
-
const doFetch = fetchImpl ?? ((...a) => fetch(...a));
|
|
329
|
-
const pending = new Map(); // `${owner}:${name}` -> { body, tries }
|
|
330
|
-
|
|
331
|
-
/** true = settled (delivered or permanently refused); false = hold it. */
|
|
332
|
-
const post = async (body) => {
|
|
333
|
-
const form = new FormData();
|
|
334
|
-
for (const [k, v] of Object.entries(body.fields)) form.set(k, v);
|
|
335
|
-
if (body.bytes) form.set('file', new Blob([body.bytes]), body.fields.name);
|
|
336
|
-
if (body.preview) form.set('preview', new Blob([body.preview], { type: 'image/png' }), 'preview.png');
|
|
337
|
-
try {
|
|
338
|
-
const res = await doFetch(url, {
|
|
339
|
-
method: 'POST',
|
|
340
|
-
headers: { Authorization: `Bearer ${token}`, 'User-Agent': userAgent },
|
|
341
|
-
signal: AbortSignal.timeout(30_000),
|
|
342
|
-
body: form,
|
|
343
|
-
});
|
|
344
|
-
return (
|
|
345
|
-
res.ok || (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429)
|
|
346
|
-
);
|
|
347
|
-
} catch {
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
const hold = (key, body, tries) => {
|
|
353
|
-
if (tries >= MAX_TRIES) {
|
|
354
|
-
pending.delete(key);
|
|
355
|
-
log(`artifact ${body.fields.name}: not delivered after ${tries} tries — dropped`);
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
pending.delete(key); // re-insert at the tail, so the cap sheds the oldest
|
|
359
|
-
pending.set(key, { body, tries });
|
|
360
|
-
while (pending.size > MAX_PENDING) {
|
|
361
|
-
const shed = pending.keys().next().value;
|
|
362
|
-
pending.delete(shed);
|
|
363
|
-
if (!chains.has(shed)) genOf.delete(shed);
|
|
364
|
-
}
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
// ONE COPY OF A FILE IS IN FLIGHT AT A TIME, AND ONLY THE NEWEST MAY
|
|
368
|
-
// DECIDE. Each report of a (owner, name) takes a fresh generation, and every
|
|
369
|
-
// post for that key is chained behind the previous one — so a slow retry of
|
|
370
|
-
// an OLD body can neither land on the server after the newer copy (the
|
|
371
|
-
// server upserts by name, so the last write wins) nor re-hold itself over
|
|
372
|
-
// the newer one when it fails. A body superseded before it went out is not
|
|
373
|
-
// sent at all; one superseded while in flight has its answer discarded.
|
|
374
|
-
let generation = 0;
|
|
375
|
-
const genOf = new Map(); // key -> the generation allowed to decide
|
|
376
|
-
const chains = new Map(); // key -> the tail of that key's post chain
|
|
377
|
-
|
|
378
|
-
const send = (key, body, tries, gen) => {
|
|
379
|
-
const run = (chains.get(key) ?? Promise.resolve()).then(async () => {
|
|
380
|
-
if (genOf.get(key) !== gen) return; // superseded before it went out
|
|
381
|
-
const ok = await post(body);
|
|
382
|
-
if (genOf.get(key) !== gen) return; // a newer copy decides for this key
|
|
383
|
-
if (ok) pending.delete(key);
|
|
384
|
-
else hold(key, body, tries + 1);
|
|
385
|
-
});
|
|
386
|
-
const tail = run.catch(() => {});
|
|
387
|
-
chains.set(key, tail);
|
|
388
|
-
tail.then(() => {
|
|
389
|
-
if (chains.get(key) !== tail) return;
|
|
390
|
-
chains.delete(key);
|
|
391
|
-
if (!pending.has(key)) genOf.delete(key); // settled: nothing left to order
|
|
392
|
-
});
|
|
393
|
-
return run;
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
let retrying = false;
|
|
397
|
-
const retryPending = async () => {
|
|
398
|
-
if (retrying || pending.size === 0) return;
|
|
399
|
-
retrying = true;
|
|
400
|
-
try {
|
|
401
|
-
for (const [key, held] of [...pending]) {
|
|
402
|
-
if (pending.get(key) !== held) continue; // superseded meanwhile
|
|
403
|
-
await send(key, held.body, held.tries, genOf.get(key));
|
|
404
|
-
}
|
|
405
|
-
} finally {
|
|
406
|
-
retrying = false;
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
/** Upload what this turn changed in `placeDir`. Resolves to the number of
|
|
411
|
-
* bodies SENT this call (delivered or held), for the tests and the log. */
|
|
412
|
-
const report = async ({ placeDir, before, sessionId, agentId, turnId }) => {
|
|
413
|
-
if (!placeDir || (!sessionId && !agentId)) return 0;
|
|
414
|
-
const changed = changedArtifacts(before, scanArtifacts(placeDir));
|
|
415
|
-
let n = 0;
|
|
416
|
-
for (const entry of changed) {
|
|
417
|
-
const body = buildArtifactUpload(entry, { sessionId, agentId, turnId }, scrub, secretIn);
|
|
418
|
-
if (!body) continue;
|
|
419
|
-
if (body.withheld) {
|
|
420
|
-
log(`artifact ${entry.name}: not uploaded — it contains the value of ${body.withheld} from this machine's environment`);
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
if (body.bytes && /\.html?$/i.test(entry.name)) {
|
|
424
|
-
const measured = await renderDesignPreview(body.bytes);
|
|
425
|
-
body.fields.renderState = measured.renderState;
|
|
426
|
-
if (measured.preview) body.preview = measured.preview;
|
|
427
|
-
}
|
|
428
|
-
const key = `${sessionId ?? agentId}:${entry.name}`;
|
|
429
|
-
pending.delete(key); // this copy supersedes a held older one
|
|
430
|
-
const gen = ++generation;
|
|
431
|
-
genOf.set(key, gen); // …and one still in flight
|
|
432
|
-
await send(key, body, 0, gen);
|
|
433
|
-
n++;
|
|
434
|
-
}
|
|
435
|
-
return n;
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
return { report, retryPending, pendingCount: () => pending.size };
|
|
439
|
-
}
|
package/bin/lib/authReject.mjs
CHANGED
|
@@ -21,12 +21,24 @@
|
|
|
21
21
|
*/
|
|
22
22
|
export async function credentialRejected(res) {
|
|
23
23
|
if (res?.status !== 401 && res?.status !== 403) return false;
|
|
24
|
-
|
|
25
|
-
if (!/\bapplication\/json\b/i.test(type)) return false;
|
|
24
|
+
let body;
|
|
26
25
|
try {
|
|
27
|
-
|
|
28
|
-
return Boolean(body) && body.success === false;
|
|
26
|
+
body = await res.json();
|
|
29
27
|
} catch {
|
|
30
28
|
return false; // not the API's envelope — an edge page mislabelled as JSON
|
|
31
29
|
}
|
|
30
|
+
return isCredentialRejection(res.status, res.headers?.get?.('content-type'), body);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* THE SAME TEST, ON A BODY ALREADY READ — for a caller that parsed the answer
|
|
35
|
+
* before it knew it was a refusal (deployWire.mjs reads every `/fleet/deploy-*`
|
|
36
|
+
* answer once). The rule is this one: a 401/403, labelled JSON, wearing the
|
|
37
|
+
* API's `{ success: false }` envelope. A body that did not parse is passed as
|
|
38
|
+
* anything but that envelope and answers false.
|
|
39
|
+
*/
|
|
40
|
+
export function isCredentialRejection(status, contentType, body) {
|
|
41
|
+
if (status !== 401 && status !== 403) return false;
|
|
42
|
+
if (!/\bapplication\/json\b/i.test(String(contentType ?? ''))) return false;
|
|
43
|
+
return Boolean(body) && typeof body === 'object' && body.success === false;
|
|
32
44
|
}
|