@wenathlan/saddle 1.8.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.
- package/LICENSE +203 -0
- package/README.md +192 -0
- package/adapters/forge.js +16 -0
- package/adapters/forgejo.js +8 -0
- package/adapters/github.js +19 -0
- package/adapters/gitlab.js +10 -0
- package/adapters/huggingface.js +6 -0
- package/adapters/socket.js +14 -0
- package/adapters/transport.js +30 -0
- package/ai/chunk.js +22 -0
- package/ai/llmstxt.js +12 -0
- package/ai/provenance.js +18 -0
- package/ai/rag.js +14 -0
- package/ai/tokens.js +9 -0
- package/api/auth.js +13 -0
- package/api/contracts.js +17 -0
- package/api/control.js +33 -0
- package/api/http.js +12 -0
- package/api/rate.js +31 -0
- package/api/security.js +42 -0
- package/api/service.js +36 -0
- package/binary/build.js +17 -0
- package/bot/adapter.js +8 -0
- package/bot/bot.js +39 -0
- package/bot/commands.js +18 -0
- package/bot/permissions.js +16 -0
- package/browser/actions.js +33 -0
- package/browser/agent.js +9 -0
- package/browser/context.js +52 -0
- package/browser/fingerprint.js +12 -0
- package/browser/index.js +10 -0
- package/browser/recorder.js +15 -0
- package/browser/session.js +19 -0
- package/browser/snapshot.js +57 -0
- package/captcha/contract.js +15 -0
- package/captcha/evidence.js +9 -0
- package/captcha/guard.js +10 -0
- package/cli/main.js +36 -0
- package/core/errors.js +37 -0
- package/core/events.js +21 -0
- package/core/hash.js +73 -0
- package/core/ids.js +15 -0
- package/crawl/crawler.js +29 -0
- package/crawl/frontier.js +34 -0
- package/crawl/normalize.js +14 -0
- package/crawl/persistent.js +13 -0
- package/dispatch/resumable.js +31 -0
- package/dispatch/workflow.js +33 -0
- package/docs/assets/architecture.svg +45 -0
- package/docs/assets/saddlemark.svg +13 -0
- package/docs/comparativeaudit.md +63 -0
- package/docs/ecosystemplan.md +59 -0
- package/docs/enginearchitecture.md +83 -0
- package/docs/featureaudit.md +63 -0
- package/docs/gapmatrix.md +80 -0
- package/docs/libraryapi.md +63 -0
- package/docs/modes.md +27 -0
- package/docs/productindex.md +28 -0
- package/docs/registryresearch.md +56 -0
- package/docs/release.md +28 -0
- package/docs/release17notes.md +24 -0
- package/docs/release181notes.md +15 -0
- package/docs/release18notes.md +15 -0
- package/docs/roadmapp2p3.md +33 -0
- package/docs/toolchains.md +28 -0
- package/docs/usage.md +107 -0
- package/domain/artifacts.js +13 -0
- package/domain/jobs.js +20 -0
- package/domain/providers.js +8 -0
- package/domain/runtime.js +10 -0
- package/domain/sessions.js +34 -0
- package/errors/taxonomy.js +18 -0
- package/examples/localjob.js +15 -0
- package/examples/publicapi.js +7 -0
- package/extension/README.md +23 -0
- package/extension/content.js +85 -0
- package/extension/index.js +5 -0
- package/extension/manifest.json +10 -0
- package/extension/popup.css +13 -0
- package/extension/popup.html +24 -0
- package/extension/popup.js +25 -0
- package/extension/protocol.js +76 -0
- package/extension/serviceworker.js +43 -0
- package/extension/worker.js +20 -0
- package/format/check.js +21 -0
- package/index.js +120 -0
- package/library/public.js +83 -0
- package/license.md +203 -0
- package/license.txt +203 -0
- package/mcp/browser.js +12 -0
- package/mcp/server.js +28 -0
- package/mcp/transport.js +14 -0
- package/memory/bridge.js +16 -0
- package/memory/engine.js +45 -0
- package/memory/modes.js +55 -0
- package/memory/objects.js +18 -0
- package/memory/targets.js +21 -0
- package/memory/transforms.js +15 -0
- package/modes/matrix.js +20 -0
- package/modes/modes.js +16 -0
- package/modes/resolve.js +39 -0
- package/package.json +47 -0
- package/packager/manifest.js +28 -0
- package/packager/publish.js +15 -0
- package/persistence/adapter.js +8 -0
- package/persistence/drizzle.js +10 -0
- package/persistence/memory.js +26 -0
- package/persistence/migrations.js +14 -0
- package/persistence/prisma.js +23 -0
- package/persistence/schema.js +29 -0
- package/persistence/sql.js +30 -0
- package/protocol/blocks.js +18 -0
- package/protocol/json.js +5 -0
- package/protocol/ndjson.js +17 -0
- package/protocol/sse.js +22 -0
- package/proxy/pool.js +12 -0
- package/queue/idempotency.js +12 -0
- package/queue/persistent.js +44 -0
- package/queue/queue.js +50 -0
- package/queue/saga.js +13 -0
- package/readme.txt +163 -0
- package/retry/circuit.js +15 -0
- package/retry/policy.js +12 -0
- package/runners/health.js +23 -0
- package/runners/heartbeat.js +26 -0
- package/runners/inprocess.js +19 -0
- package/runners/scheduler.js +16 -0
- package/runtime/abort.js +10 -0
- package/runtime/compatibility.js +13 -0
- package/runtime/detect.js +14 -0
- package/runtime/engine.js +56 -0
- package/runtime/worker.js +18 -0
- package/scrape/cache.js +14 -0
- package/scrape/extract.js +14 -0
- package/scrape/robots.js +32 -0
- package/scrape/schema.js +21 -0
- package/scrape/scraper.js +40 -0
- package/scrape/semantic.js +22 -0
- package/server/node.js +34 -0
- package/sessions/file.js +13 -0
- package/sessions/replay.js +21 -0
- package/sessions/store.js +13 -0
- package/storage/adapter.js +8 -0
- package/storage/cache.js +54 -0
- package/storage/checksum.js +17 -0
- package/storage/chunked.js +58 -0
- package/storage/content.js +42 -0
- package/storage/filehosting.js +17 -0
- package/storage/githubcontents.js +18 -0
- package/storage/index.js +10 -0
- package/storage/local.js +35 -0
- package/storage/memory.js +28 -0
- package/storage/s3compatible.js +23 -0
- package/storage/sync.js +55 -0
- package/surfaces/adapters.js +48 -0
- package/surfaces/controls.js +37 -0
- package/surfaces/manifest.js +25 -0
- package/surfaces/n8n.js +24 -0
- package/surfaces/operations.js +43 -0
- package/surfaces/targets.js +16 -0
- package/webhook/delivery.js +26 -0
- package/webhook/receiver.js +20 -0
- package/webhook/signature.js +7 -0
- package/workflow/manifest.js +20 -0
- package/workflow/registry.js +16 -0
- package/workflow/templates.js +18 -0
- package/workflow/triggers.js +31 -0
package/core/hash.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hash helpers use standard JavaScript primitives so transport-neutral modules do not require node:crypto.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const constants = Object.freeze([
|
|
6
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
7
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
8
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
9
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
10
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
11
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
12
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
13
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
/** Returns a SHA-256 hexadecimal digest for bytes or text. */
|
|
17
|
+
export function sha256(value) { return tohex(hashbytes(value)); }
|
|
18
|
+
|
|
19
|
+
/** Returns an HMAC-SHA-256 hexadecimal digest for bytes or text. */
|
|
20
|
+
export function hmacsha256(value, secret) {
|
|
21
|
+
let key = tobytes(secret);
|
|
22
|
+
if (key.length > 64) key = hashbytes(key);
|
|
23
|
+
const padded = new Uint8Array(64);
|
|
24
|
+
padded.set(key);
|
|
25
|
+
const outer = new Uint8Array(64);
|
|
26
|
+
const inner = new Uint8Array(64);
|
|
27
|
+
for (let index = 0; index < 64; index += 1) { outer[index] = padded[index] ^ 0x5c; inner[index] = padded[index] ^ 0x36; }
|
|
28
|
+
return sha256(concat(outer, hashbytes(concat(inner, tobytes(value)))));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Compares hexadecimal values without early exit based on content. */
|
|
32
|
+
export function constanttimeequal(left, right) { const a = String(left ?? ""); const b = String(right ?? ""); let difference = a.length ^ b.length; const length = Math.max(a.length, b.length); for (let index = 0; index < length; index += 1) difference |= (a.charCodeAt(index) || 0) ^ (b.charCodeAt(index) || 0); return difference === 0; }
|
|
33
|
+
|
|
34
|
+
function hashbytes(value) {
|
|
35
|
+
const input = tobytes(value);
|
|
36
|
+
const bitlength = BigInt(input.length) * 8n;
|
|
37
|
+
const length = Math.ceil((input.length + 9) / 64) * 64;
|
|
38
|
+
const padded = new Uint8Array(length);
|
|
39
|
+
padded.set(input);
|
|
40
|
+
padded[input.length] = 0x80;
|
|
41
|
+
const inputview = new DataView(padded.buffer);
|
|
42
|
+
inputview.setUint32(length - 8, Number((bitlength >> 32n) & 0xffffffffn));
|
|
43
|
+
inputview.setUint32(length - 4, Number(bitlength & 0xffffffffn));
|
|
44
|
+
const state = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
|
|
45
|
+
const words = new Uint32Array(64);
|
|
46
|
+
for (let offset = 0; offset < length; offset += 64) {
|
|
47
|
+
for (let index = 0; index < 16; index += 1) words[index] = inputview.getUint32(offset + index * 4);
|
|
48
|
+
for (let index = 16; index < 64; index += 1) {
|
|
49
|
+
const sigma0 = smallright(words[index - 15], 7) ^ smallright(words[index - 15], 18) ^ (words[index - 15] >>> 3);
|
|
50
|
+
const sigma1 = smallright(words[index - 2], 17) ^ smallright(words[index - 2], 19) ^ (words[index - 2] >>> 10);
|
|
51
|
+
words[index] = (sigma1 + words[index - 7] + sigma0 + words[index - 16]) >>> 0;
|
|
52
|
+
}
|
|
53
|
+
let [a, b, c, d, e, f, g, h] = state;
|
|
54
|
+
for (let index = 0; index < 64; index += 1) {
|
|
55
|
+
const choose = (e & f) ^ (~e & g);
|
|
56
|
+
const majority = (a & b) ^ (a & c) ^ (b & c);
|
|
57
|
+
const first = (h + (smallright(e, 6) ^ smallright(e, 11) ^ smallright(e, 25)) + choose + constants[index] + words[index]) >>> 0;
|
|
58
|
+
const second = ((smallright(a, 2) ^ smallright(a, 13) ^ smallright(a, 22)) + majority) >>> 0;
|
|
59
|
+
[h, g, f, e, d, c, b, a] = [g, f, e, (d + first) >>> 0, c, b, a, (first + second) >>> 0];
|
|
60
|
+
}
|
|
61
|
+
state[0] = (state[0] + a) >>> 0; state[1] = (state[1] + b) >>> 0; state[2] = (state[2] + c) >>> 0; state[3] = (state[3] + d) >>> 0;
|
|
62
|
+
state[4] = (state[4] + e) >>> 0; state[5] = (state[5] + f) >>> 0; state[6] = (state[6] + g) >>> 0; state[7] = (state[7] + h) >>> 0;
|
|
63
|
+
}
|
|
64
|
+
const output = new Uint8Array(32);
|
|
65
|
+
const view = new DataView(output.buffer);
|
|
66
|
+
state.forEach((word, index) => view.setUint32(index * 4, word));
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function tobytes(value) { if (value instanceof Uint8Array) return value; if (value instanceof ArrayBuffer) return new Uint8Array(value); return new TextEncoder().encode(String(value ?? "")); }
|
|
71
|
+
function concat(left, right) { const result = new Uint8Array(left.length + right.length); result.set(left); result.set(right, left.length); return result; }
|
|
72
|
+
function smallright(value, amount) { return (value >>> amount) | (value << (32 - amount)); }
|
|
73
|
+
function tohex(value) { return [...value].map((byte) => byte.toString(16).padStart(2, "0")).join(""); }
|
package/core/ids.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* injectable time and id factories make every engine path reproducible.
|
|
3
|
+
*/
|
|
4
|
+
export function systemclock() {
|
|
5
|
+
return { now: () => Date.now() };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function idfactory(randomuuid = globalThis.crypto?.randomUUID?.bind(globalThis.crypto)) {
|
|
9
|
+
return {
|
|
10
|
+
next(prefix) {
|
|
11
|
+
const suffix = randomuuid?.() ?? `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
12
|
+
return `${prefix}${suffix}`;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
package/crawl/crawler.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawler performs bounded breadth first traversal through the scraper contract.
|
|
3
|
+
*/
|
|
4
|
+
import { normalizeurl, sameorigin } from "./normalize.js";
|
|
5
|
+
import { crawlfrontier } from "./frontier.js";
|
|
6
|
+
|
|
7
|
+
export async function crawl(start, options = {}) {
|
|
8
|
+
const maxdepth = options.maxdepth ?? 1;
|
|
9
|
+
const maxpages = options.maxpages ?? 20;
|
|
10
|
+
const sameDomain = options.samedomain ?? true;
|
|
11
|
+
const frontier = crawlfrontier({ maxpages, maxperdomain: options.maxperdomain ?? maxpages });
|
|
12
|
+
frontier.add({ url: normalizeurl(start), depth: 0, priority: options.startpriority ?? 0 });
|
|
13
|
+
const results = [];
|
|
14
|
+
while (frontier.state().queued && results.length < maxpages) {
|
|
15
|
+
const current = frontier.next();
|
|
16
|
+
if (!current || current.depth > maxdepth) continue;
|
|
17
|
+
const result = await options.scrape(current.url);
|
|
18
|
+
results.push({ ...result, depth: current.depth });
|
|
19
|
+
frontier.complete(current.url);
|
|
20
|
+
if (current.depth >= maxdepth) continue;
|
|
21
|
+
for (const link of result.links ?? []) {
|
|
22
|
+
let url;
|
|
23
|
+
try { url = normalizeurl(link); } catch { continue; }
|
|
24
|
+
if (sameDomain && !sameorigin(start, url)) continue;
|
|
25
|
+
frontier.add({ url, depth: current.depth + 1, priority: Number(options.priority?.(url, result) ?? 0) });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { results, stats: { ...frontier.state(), completed: results.length, maxdepth, maxpages } };
|
|
29
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl frontier provides priorities, per-domain budgets and persistent-friendly queue state.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Creates a bounded priority frontier for crawler and queue adapters. */
|
|
6
|
+
export function crawlfrontier(options = {}) {
|
|
7
|
+
const maxpages = Number(options.maxpages ?? 20);
|
|
8
|
+
const maxperdomain = Number(options.maxperdomain ?? maxpages);
|
|
9
|
+
const queue = [];
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
const completed = new Set();
|
|
12
|
+
const domains = new Map();
|
|
13
|
+
function add(input = {}) {
|
|
14
|
+
const url = String(input.url ?? "");
|
|
15
|
+
if (!url || seen.has(url) || seen.size >= maxpages) return false;
|
|
16
|
+
seen.add(url);
|
|
17
|
+
queue.push({ url, depth: Number(input.depth ?? 0), priority: Number(input.priority ?? 0), discoveredat: Number(input.discoveredat ?? Date.now()) });
|
|
18
|
+
queue.sort((left, right) => right.priority - left.priority || left.discoveredat - right.discoveredat);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
function next() {
|
|
22
|
+
while (queue.length) {
|
|
23
|
+
const item = queue.shift();
|
|
24
|
+
const domain = new URL(item.url).hostname;
|
|
25
|
+
if ((domains.get(domain) ?? 0) >= maxperdomain) continue;
|
|
26
|
+
domains.set(domain, (domains.get(domain) ?? 0) + 1);
|
|
27
|
+
return { ...item };
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function complete(url) { completed.add(String(url)); }
|
|
32
|
+
function state() { return { maxpages, maxperdomain, queued: queue.length, discovered: seen.size, completed: completed.size, domains: Object.fromEntries(domains) }; }
|
|
33
|
+
return { add, next, complete, state, list() { return queue.map((item) => ({ ...item })); } };
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* url normalization removes tracking noise before frontier deduplication.
|
|
3
|
+
*/
|
|
4
|
+
const tracking = /^(utm_|fbclid$|msclkid$|gclid$|gclsrc$|dclid$|gbraid$|wbraid$|twclid$|campaign$|content$|term$|source$|medium$|ref$|share_id$)/i;
|
|
5
|
+
|
|
6
|
+
export function normalizeurl(value) {
|
|
7
|
+
const url = new URL(value);
|
|
8
|
+
url.hash = "";
|
|
9
|
+
for (const key of [...url.searchParams.keys()]) if (tracking.test(key)) url.searchParams.delete(key);
|
|
10
|
+
if (url.pathname.length > 1) url.pathname = url.pathname.replace(/\/+$/, "");
|
|
11
|
+
return url.href;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function sameorigin(left, right) { return new URL(left).origin === new URL(right).origin; }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* persistent crawl queue uses an injected store and falls back to memory when no store exists.
|
|
3
|
+
*/
|
|
4
|
+
export function persistentqueue(options = {}) {
|
|
5
|
+
const store = options.store;
|
|
6
|
+
const values = [];
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
async function restore() { if (typeof store?.list !== "function") return; for (const item of await store.list()) if (!seen.has(item.url) && item.status !== "done") { seen.add(item.url); values.push(item); } }
|
|
9
|
+
async function add(item) { if (!item?.url || seen.has(item.url)) return false; const value = { url: item.url, depth: item.depth ?? 0, status: "queued", createdat: Date.now(), metadata: item.metadata ?? {} }; seen.add(value.url); values.push(value); if (typeof store?.save === "function") await store.save(value); return true; }
|
|
10
|
+
async function next() { const item = values.find((value) => value.status === "queued"); if (!item) return null; item.status = "running"; if (typeof store?.update === "function") await store.update(item.url, item); return item; }
|
|
11
|
+
async function complete(url, patch = {}) { const item = values.find((value) => value.url === url); if (!item) return null; Object.assign(item, patch, { status: patch.status ?? "done", processedat: Date.now() }); if (typeof store?.update === "function") await store.update(url, item); return item; }
|
|
12
|
+
return { restore, add, next, complete, list() { return values.map((value) => ({ ...value })); } };
|
|
13
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resumable runs keep remote execution state explicit and recoverable across process restarts.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const runstatuses = Object.freeze(["created", "submitted", "running", "succeeded", "failed", "cancelled"]);
|
|
6
|
+
const transitions = { created: ["submitted", "cancelled"], submitted: ["running", "failed", "cancelled"], running: ["succeeded", "failed", "cancelled"], succeeded: [], failed: ["submitted", "cancelled"], cancelled: [] };
|
|
7
|
+
|
|
8
|
+
/** Creates a validated run record with explicit transition history. */
|
|
9
|
+
export function runrecord(input = {}) {
|
|
10
|
+
if (!input.requestid || !input.name) throw new TypeError("run record requires requestid and name");
|
|
11
|
+
return { version: 1, requestid: String(input.requestid), name: String(input.name), status: input.status ?? "created", runid: input.runid, attempt: Number(input.attempt ?? 0), createdat: Number(input.createdat ?? Date.now()), updatedat: Number(input.updatedat ?? Date.now()), history: Array.isArray(input.history) ? input.history.map((event) => ({ ...event })) : [], metadata: { ...(input.metadata ?? {}) } };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Moves a run through a legal state transition and appends an auditable event. */
|
|
15
|
+
export function transitionrun(record, status, options = {}) {
|
|
16
|
+
const current = runrecord(record);
|
|
17
|
+
if (!runstatuses.includes(status) || !transitions[current.status]?.includes(status)) { const error = new Error(`invalid run transition: ${current.status} to ${status}`); error.code = "INVALID_RUN_TRANSITION"; throw error; }
|
|
18
|
+
const event = { from: current.status, to: status, at: Number(options.at ?? Date.now()), reason: options.reason };
|
|
19
|
+
return { ...current, status, runid: options.runid ?? current.runid, attempt: status === "submitted" ? current.attempt + 1 : current.attempt, updatedat: event.at, history: [...current.history, event] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Coordinates submit, status, resume and cancel operations through an injected remote adapter. */
|
|
23
|
+
export function resumablerun(adapter, input = {}) {
|
|
24
|
+
if (typeof adapter?.submit !== "function" || typeof adapter?.status !== "function") throw new TypeError("resumable run adapter requires submit and status");
|
|
25
|
+
let record = runrecord(input);
|
|
26
|
+
async function submit() { const response = await adapter.submit(input); record = transitionrun(record, "submitted", { runid: response?.runid, reason: "submitted" }); return { ...record, response }; }
|
|
27
|
+
async function resume() { if (record.status === "created") await submit(); if (typeof adapter.resume === "function") await adapter.resume(record); const response = await adapter.status(record.runid); if (response?.status && response.status !== record.status && transitions[record.status]?.includes(response.status)) record = transitionrun(record, response.status, { reason: "remote-status" }); return { ...record, response }; }
|
|
28
|
+
async function cancel() { if (typeof adapter.cancel !== "function") throw new TypeError("resumable run adapter does not support cancel"); const response = await adapter.cancel(record.runid); if (record.status !== "cancelled" && transitions[record.status]?.includes("cancelled")) record = transitionrun(record, "cancelled", { reason: "cancelled" }); return { ...record, response }; }
|
|
29
|
+
function get() { return runrecord(record); }
|
|
30
|
+
return { submit, resume, cancel, get };
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workflow dispatch keeps remote execution explicit and records a stable request identity.
|
|
3
|
+
*/
|
|
4
|
+
import { idempotency } from "../queue/idempotency.js";
|
|
5
|
+
|
|
6
|
+
export function workflowdispatch(adapter, options = {}) {
|
|
7
|
+
if (typeof adapter?.dispatch !== "function") throw new TypeError("workflow adapter requires dispatch");
|
|
8
|
+
const records = options.records ?? idempotency();
|
|
9
|
+
return {
|
|
10
|
+
async submit(spec) {
|
|
11
|
+
if (!spec?.owner || !spec.repository || !spec.workflow || !spec.ref) throw new TypeError("workflow owner repository workflow and ref are required");
|
|
12
|
+
const requestid = spec.requestid ?? `${spec.owner}/${spec.repository}/${spec.workflow}/${spec.ref}/${JSON.stringify(spec.inputs ?? {})}`;
|
|
13
|
+
if (records.has(requestid)) return records.get(requestid);
|
|
14
|
+
const response = await adapter.dispatch(spec.owner, spec.repository, spec.workflow, { ref: spec.ref, inputs: spec.inputs ?? {} });
|
|
15
|
+
const record = { requestid, ...spec, response, submittedat: Date.now() };
|
|
16
|
+
records.set(requestid, record);
|
|
17
|
+
return record;
|
|
18
|
+
},
|
|
19
|
+
get(requestid) { return records.get(requestid); }
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function waitforrun(adapter, owner, repository, runid, options = {}) {
|
|
24
|
+
if (typeof adapter?.run !== "function") throw new TypeError("workflow adapter requires run");
|
|
25
|
+
const attempts = options.attempts ?? 20;
|
|
26
|
+
const interval = options.interval ?? 3000;
|
|
27
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
28
|
+
const run = await adapter.run(owner, repository, runid);
|
|
29
|
+
if (["completed", "failure", "cancelled", "success"].includes(run.status)) return run;
|
|
30
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`workflow run ${runid} did not finish within configured attempts`);
|
|
33
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 360" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">Saddle runtime architecture</title>
|
|
3
|
+
<desc id="desc">Storage enters a bridge, becomes a working set, runs through an injected runner, and returns as an artifact.</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="paper" x1="0" x2="1" y1="0" y2="1">
|
|
6
|
+
<stop offset="0" stop-color="#fbf7f0"/>
|
|
7
|
+
<stop offset="1" stop-color="#efe6d8"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
<marker id="arrow" markerHeight="8" markerWidth="8" orient="auto" refX="7" refY="4">
|
|
10
|
+
<path d="M0 0 8 4 0 8Z" fill="#d35d3d"/>
|
|
11
|
+
</marker>
|
|
12
|
+
</defs>
|
|
13
|
+
<rect width="1200" height="360" rx="30" fill="url(#paper)"/>
|
|
14
|
+
<path d="M60 72H1140M60 286H1140" stroke="#d9cdbc" stroke-dasharray="5 9"/>
|
|
15
|
+
<g font-family="Arial, Helvetica, sans-serif">
|
|
16
|
+
<g transform="translate(70 108)">
|
|
17
|
+
<rect width="205" height="130" rx="18" fill="#202a2f"/>
|
|
18
|
+
<text x="24" y="38" fill="#f7f1e8" font-size="14" letter-spacing="2">01 · STORAGE</text>
|
|
19
|
+
<text x="24" y="76" fill="#fff" font-size="23" font-weight="700">Objects</text>
|
|
20
|
+
<text x="24" y="104" fill="#b8c4c1" font-size="14">local · S3 · forge</text>
|
|
21
|
+
</g>
|
|
22
|
+
<path d="M290 173h80" fill="none" stroke="#d35d3d" stroke-width="4" marker-end="url(#arrow)"/>
|
|
23
|
+
<g transform="translate(390 108)">
|
|
24
|
+
<rect width="205" height="130" rx="18" fill="#d35d3d"/>
|
|
25
|
+
<text x="24" y="38" fill="#fff3e7" font-size="14" letter-spacing="2">02 · BRIDGE</text>
|
|
26
|
+
<text x="24" y="76" fill="#fff" font-size="23" font-weight="700">Working set</text>
|
|
27
|
+
<text x="24" y="104" fill="#ffe5d5" font-size="14">load · transform · sync</text>
|
|
28
|
+
</g>
|
|
29
|
+
<path d="M610 173h80" fill="none" stroke="#d35d3d" stroke-width="4" marker-end="url(#arrow)"/>
|
|
30
|
+
<g transform="translate(710 108)">
|
|
31
|
+
<rect width="205" height="130" rx="18" fill="#e5c26f"/>
|
|
32
|
+
<text x="24" y="38" fill="#4b3d22" font-size="14" letter-spacing="2">03 · RUNNER</text>
|
|
33
|
+
<text x="24" y="76" fill="#202a2f" font-size="23" font-weight="700">Job cycle</text>
|
|
34
|
+
<text x="24" y="104" fill="#4b3d22" font-size="14">prepare · process · cleanup</text>
|
|
35
|
+
</g>
|
|
36
|
+
<path d="M930 173h80" fill="none" stroke="#d35d3d" stroke-width="4" marker-end="url(#arrow)"/>
|
|
37
|
+
<g transform="translate(1030 108)">
|
|
38
|
+
<rect width="100" height="130" rx="18" fill="#fff" stroke="#202a2f" stroke-width="2"/>
|
|
39
|
+
<text x="18" y="38" fill="#202a2f" font-size="14" letter-spacing="2">04</text>
|
|
40
|
+
<text x="18" y="74" fill="#202a2f" font-size="20" font-weight="700">Artifact</text>
|
|
41
|
+
<text x="18" y="102" fill="#6d7777" font-size="13">persist</text>
|
|
42
|
+
</g>
|
|
43
|
+
<text x="70" y="319" fill="#6d7777" font-size="14">Saddle owns the contracts. The caller chooses credentials, transports, storage, browser and runner.</text>
|
|
44
|
+
</g>
|
|
45
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 180" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">Saddle</title>
|
|
3
|
+
<desc id="desc">Saddle wordmark with a warm ember mark and a storage-to-compute arc.</desc>
|
|
4
|
+
<rect width="720" height="180" rx="28" fill="#f7f1e8"/>
|
|
5
|
+
<g transform="translate(42 34)">
|
|
6
|
+
<path d="M52 10c-27 0-48 20-48 45 0 32 24 57 57 57 19 0 35-8 46-22-13 5-25 7-36 7-23 0-39-17-39-39 0-19 13-35 31-42 12-5 25-6 39-4C90 18 73 10 52 10Z" fill="#d35d3d"/>
|
|
7
|
+
<path d="M31 76c15 11 33 17 54 17 25 0 44-9 59-28" fill="none" stroke="#202a2f" stroke-linecap="round" stroke-width="8"/>
|
|
8
|
+
<circle cx="35" cy="45" r="7" fill="#f7f1e8"/>
|
|
9
|
+
<circle cx="137" cy="65" r="7" fill="#d35d3d"/>
|
|
10
|
+
</g>
|
|
11
|
+
<text x="220" y="91" fill="#202a2f" font-family="Arial, Helvetica, sans-serif" font-size="58" font-weight="700" letter-spacing="10">SADDLE</text>
|
|
12
|
+
<text x="224" y="126" fill="#6d7777" font-family="Arial, Helvetica, sans-serif" font-size="14" letter-spacing="3">STORAGE · WORKING SET · RUNNER</text>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# comparative audit
|
|
2
|
+
|
|
3
|
+
## sources reviewed
|
|
4
|
+
|
|
5
|
+
### Chrome extension messaging
|
|
6
|
+
|
|
7
|
+
Source: Chrome for Developers, [Message passing](https://developer.chrome.com/docs/extensions/develop/concepts/messaging).
|
|
8
|
+
|
|
9
|
+
Manifest V3 extensions need explicit message contracts between service workers, extension pages and content scripts. Chrome exposes one-time request/response through `runtime.sendMessage()` and `tabs.sendMessage()`, plus long-lived connections through `runtime.connect()` and ports. Asynchronous responses must keep the channel open with an explicit `true` return in broadly compatible implementations; response values must be serializable, and sender errors need a defined handling path.
|
|
10
|
+
|
|
11
|
+
Implication for Saddle: the future `extension/` surface needs a versioned message envelope, sender and tab metadata, request correlation, timeout handling, serializable payload validation and a distinction between one-shot commands and long-lived streams. The browser adapter contract alone is not enough for an extension runtime.
|
|
12
|
+
|
|
13
|
+
### Vercel agent-browser
|
|
14
|
+
|
|
15
|
+
Source: [vercel-labs/agent-browser](https://github.com/vercel-labs/agent-browser).
|
|
16
|
+
|
|
17
|
+
The project separates a browser daemon from the CLI and exposes a compact interaction loop: open, snapshot an accessibility tree with stable refs, act on refs, read text, screenshot and close. It also supports traditional selectors, role-based finders, uploads, keyboard events, DOM reads, CDP connection and a remote browser mode. Its README emphasizes early click failure when an overlay covers a target and requires a fresh snapshot before retrying.
|
|
18
|
+
|
|
19
|
+
Implication for Saddle: the browser contract should add snapshot identity and stale-reference errors, explicit action results, overlay or obstruction diagnostics, upload and keyboard primitives, session lifecycle, and a transport that can point to a local or remote browser without making the CLI the core.
|
|
20
|
+
|
|
21
|
+
## initial gap hypotheses
|
|
22
|
+
|
|
23
|
+
The current Saddle code has browser action names, sessions, replay, fingerprint and MCP contracts, but the audit still needs to verify whether it has snapshot references, stale-state detection, tab/frame ownership, extension messaging, permission minimization, content-script isolation and a real buildable Manifest V3 surface. These items remain hypotheses until the repository and more primary sources are reviewed.
|
|
24
|
+
|
|
25
|
+
### Microsoft Playwright MCP
|
|
26
|
+
|
|
27
|
+
Source: [microsoft/playwright-mcp](https://github.com/microsoft/playwright-mcp).
|
|
28
|
+
|
|
29
|
+
Playwright MCP uses structured accessibility snapshots rather than pixel-only input and supports navigation, interaction, inspection and persistent browser context through an MCP server. Its own documentation distinguishes a CLI path for token-efficient coding-agent workflows from MCP for persistent state, rich introspection and long-running autonomous loops.
|
|
30
|
+
|
|
31
|
+
Implication for Saddle: the extension and MCP layers should share a structured page snapshot model, while keeping a lower-level browser action contract underneath. The model should support stable element references, snapshot refresh, action diagnostics and a choice between compact command output and richer introspection.
|
|
32
|
+
|
|
33
|
+
### Chrome content scripts
|
|
34
|
+
|
|
35
|
+
Source: Chrome for Developers, [Content scripts](https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts).
|
|
36
|
+
|
|
37
|
+
Content scripts run in an isolated world, can use only a limited set of extension APIs directly, and must message the service worker for other capabilities. Static, dynamic and programmatic injection have different permission and lifecycle implications; host match patterns and web-accessible resources expand the security surface.
|
|
38
|
+
|
|
39
|
+
Implication for Saddle: the first extension implementation should use a narrow static content script, an isolated DOM bridge, a service worker as the privileged coordinator and minimal host permissions. It should not expose the core package or arbitrary remote code as a web-accessible resource.
|
|
40
|
+
|
|
41
|
+
### Manifest permissions and lifecycle
|
|
42
|
+
|
|
43
|
+
Sources: Chrome for Developers, [Declare permissions](https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions) and [The extension service worker lifecycle](https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle).
|
|
44
|
+
|
|
45
|
+
Permissions, host permissions, optional permissions and optional host permissions are separate manifest controls. Optional permissions should be preferred when a feature allows it because permission changes and broad match patterns can create install or runtime warnings. MV3 service workers are event-driven and can be terminated after inactivity or long-running limits; global variables do not survive shutdown, so durable state belongs in extension storage or IndexedDB and handlers must be reentrant.
|
|
46
|
+
|
|
47
|
+
Implication for Saddle: the extension manifest should begin with `storage` and a deliberately small content match policy, avoid `tabs`, `cookies`, `webRequest`, debugger and broad host permissions until a tested feature requires them, and expose an explicit permission escalation flow. The service worker should rehydrate state per event, persist session metadata, and treat ports, messages and in-flight jobs as recoverable rather than process-local.
|
|
48
|
+
|
|
49
|
+
### Browser Use
|
|
50
|
+
|
|
51
|
+
Source: [browser-use/browser-use](https://github.com/browser-use/browser-use).
|
|
52
|
+
|
|
53
|
+
Browser Use exposes a high-level task agent, a lower-level programmable library, custom tools, authentication profiles, persistent filesystem and memory, and a hosted execution path with scaling, proxy rotation and browser fingerprinting. It explicitly separates one-off CLI usage from repeatable automation embedded in code.
|
|
54
|
+
|
|
55
|
+
Implication for Saddle: the core should distinguish task orchestration from browser primitives, provide tool registration and structured task history, and treat authentication profiles, persistence and remote execution as adapters. Saddle already has bot, workflow, session and storage contracts, but the extension surface needs a clear task-to-command bridge rather than exposing raw browser actions only.
|
|
56
|
+
|
|
57
|
+
### WXT
|
|
58
|
+
|
|
59
|
+
Source: [WXT](https://wxt.dev/).
|
|
60
|
+
|
|
61
|
+
WXT demonstrates the value of an opinionated extension build surface: fast development reloads, multiple entrypoints, multi-browser output, automated packaging and publishing, and bundle analysis. The framework uses TypeScript by default, but its architectural lessons apply independently of language choice.
|
|
62
|
+
|
|
63
|
+
Implication for Saddle: `extension/` should be a buildable, vendor-light surface with explicit manifest, background, content and control entrypoints, a packageable artifact, browser capability detection and a future adapter boundary for Chrome, Firefox and other WebExtension-compatible runtimes. The first implementation can stay pure JavaScript and avoid adding a framework dependency.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# ecosystem implementation plan
|
|
2
|
+
|
|
3
|
+
Saddle is treated as the contract layer for a larger ecosystem. The implementation is progressive: each block adds a cohesive capability, tests it without real credentials, updates the public documentation and records the release impact before the next block begins.
|
|
4
|
+
|
|
5
|
+
## north star
|
|
6
|
+
|
|
7
|
+
The library remains usable as a root-based JavaScript ESM package in library, CLI, binary, browser, extension, desktop, mobile, container and runner contexts. A caller can choose storage, browser, provider, runner, database, proxy, bot and credential adapters without changing the core contracts.
|
|
8
|
+
|
|
9
|
+
The engine owns **contracts, validation, orchestration, recovery and auditability**. The caller owns **accounts, credentials, remote infrastructure, browser profiles, service terms and deployment choices**.
|
|
10
|
+
|
|
11
|
+
## non-negotiable invariants
|
|
12
|
+
|
|
13
|
+
| Invariant | Rule |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| Runtime | JavaScript ESM, root-based, no `src/`, lowercase filenames and English JSDoc |
|
|
16
|
+
| Infrastructure | no hardcoded host, port, credential, cloud account or vendor-specific function |
|
|
17
|
+
| Library | works without an extension, browser, remote runner, external memory or database |
|
|
18
|
+
| Security | validate URLs, isolate browser contexts, minimize permissions, verify webhooks and audit actions |
|
|
19
|
+
| Recovery | jobs, commands, artifacts and sync operations are idempotent, retryable or explicitly terminal |
|
|
20
|
+
| Evidence | a feature is complete only with an executable contract, deterministic tests and documentation |
|
|
21
|
+
| Distribution | package contents, license, provenance, checksums and registry namespace are verified before release |
|
|
22
|
+
|
|
23
|
+
## block order
|
|
24
|
+
|
|
25
|
+
| Block | Scope | Current state | Exit evidence |
|
|
26
|
+
| --- | --- | --- | --- |
|
|
27
|
+
| 1 | audit and governance | active | gap matrix, sources, claims reconciled |
|
|
28
|
+
| 2 | browser agent foundation | complete | snapshots, refs, stale errors, tabs, frames, action results and replay provenance tests |
|
|
29
|
+
| 3 | extension runtime | first slice complete | MV3 unpacked surface, protocol, worker, content bridge and tests |
|
|
30
|
+
| 4 | working set and storage | complete | range chunks, content dedupe, tiered cache, conflict sync and memory capabilities |
|
|
31
|
+
| 5 | runners and execution | complete | provider health, triggers, cancellation, heartbeat and resumable runs |
|
|
32
|
+
| 6 | scraping and context | complete | semantic extraction, crawl budgets, RAG lineage and low-cardinality metrics |
|
|
33
|
+
| 7 | API, MCP and security | complete | request identity, optional auth, secure headers, browser MCP tools and redirect/DNS checks |
|
|
34
|
+
| 8 | bots and integrations | complete | app lifecycle, command scopes, idempotency, delivery retries and dead letters |
|
|
35
|
+
| 9 | packaging and distribution | partial; registry slice verified for 1.7.0 | reproducible extension, desktop, mobile, n8n and binary artifacts |
|
|
36
|
+
| 10 | product surfaces and operations | first slice complete | desktop, mobile, n8n and operator control contracts; observability, retention and threat model remain |
|
|
37
|
+
| 11 | cross-runtime compatibility | first slice complete | Node, Bun and Deno root probe; browser worker, extension and package tree-shaking checks remain |
|
|
38
|
+
| 12 | release gates | active | deterministic checks, docs, clean diffs and claim/code parity |
|
|
39
|
+
|
|
40
|
+
## execution method
|
|
41
|
+
|
|
42
|
+
Each block follows the same loop:
|
|
43
|
+
|
|
44
|
+
1. Compare the requested behavior with primary repositories or platform documentation.
|
|
45
|
+
2. Record the gap and the intended boundary in `docs/gapmatrix.md`.
|
|
46
|
+
3. Define the smallest transport-neutral contract.
|
|
47
|
+
4. Implement grouped logic in the domain folder without adding a vendor dependency unless required.
|
|
48
|
+
5. Add deterministic tests with fake transports and no real credentials.
|
|
49
|
+
6. Update API docs, README claims, changelog and `todo.md`.
|
|
50
|
+
7. Run format, syntax, tests, package and security checks.
|
|
51
|
+
8. Commit and push the block before starting the next one.
|
|
52
|
+
|
|
53
|
+
## current implementation
|
|
54
|
+
|
|
55
|
+
Version 1.7 contains the first bot and integration lifecycle slice after browser, storage, runners, scraping and API hardening. It adds app installation and scope state, command authorization, idempotent bot execution, retryable webhook delivery and dead-letter records. The registry publication slice for 1.7.0 is verified for GitHub npm, GHCR, Maven, NuGet and RubyGems; public npmjs remains blocked because the owner account or scope still rejects the configured publication with HTTP 404. The first product surface slice adds desktop, mobile, n8n, operator control, operational policy and framework-neutral HTTP contracts. The first cross-runtime slice now validates the root on Node, Bun and Deno, while browser worker and extension import checks remain.
|
|
56
|
+
|
|
57
|
+
## evidence sources
|
|
58
|
+
|
|
59
|
+
The current plan is grounded in the repository's [`comparativeaudit.md`](comparativeaudit.md), [`gapmatrix.md`](gapmatrix.md), the supplied conclusions documents, and primary platform or project sources listed there. External claims remain evidence-backed notes; they do not become product promises until the corresponding block has executable tests.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# saddle engine architecture
|
|
2
|
+
|
|
3
|
+
## purpose
|
|
4
|
+
|
|
5
|
+
Saddle is a library first. The engine turns a serializable job into a temporary working set, runs the job through a selected provider, commits the result to a storage adapter, and leaves an event trail for observation.
|
|
6
|
+
|
|
7
|
+
The engine does not claim that remote storage is physical vram. It models the same bytes under two usage flags: `keep` for persistent state and `process` for a temporary working set. Capacity, latency, durability, and availability remain properties of the selected backend.
|
|
8
|
+
|
|
9
|
+
## root layout
|
|
10
|
+
|
|
11
|
+
The project has no `src` folder. The root is the map of the engine.
|
|
12
|
+
|
|
13
|
+
| folder | responsibility |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `core` | errors, ids, clock, events, and tracing primitives |
|
|
16
|
+
| `domain` | jobs, sessions, artifacts, providers, and runtime records |
|
|
17
|
+
| `storage` | storage adapter contract, local backend, and checksums |
|
|
18
|
+
| `memory` | working set preparation, sync, and cleanup |
|
|
19
|
+
| `runners` | provider factories and deterministic scheduling |
|
|
20
|
+
| `runtime` | engine orchestration and output encoding |
|
|
21
|
+
| `cli` | explicit command surface with local error handling |
|
|
22
|
+
| `tests` | local deterministic tests without credentials |
|
|
23
|
+
| `examples` | small runnable integration examples |
|
|
24
|
+
|
|
25
|
+
All internal file names are lowercase and contain no underscore or hyphen. Related logic stays grouped and each module remains small enough to reason about in isolation.
|
|
26
|
+
|
|
27
|
+
## public contracts
|
|
28
|
+
|
|
29
|
+
The public contract uses plain objects and factory functions. The engine never requires a class instance from a vendor adapter.
|
|
30
|
+
|
|
31
|
+
| contract | responsibility | open choice |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `storageadapter` | put, get, head, delete, and list artifacts | local, s3 compatible, webdav, hf, kaggle, or another backend |
|
|
34
|
+
| `localmemory` | prepare, sync, and cleanup a working set | tmpfs, mmap, sqlite, r2, or a remote bridge |
|
|
35
|
+
| `scheduler` | select the first available provider by stable priority | github, forgejo, gitea, gitlab, hf, kaggle, or custom |
|
|
36
|
+
| `engine` | coordinate the lifecycle and emit events | library, cli, browser, desktop, mobile, or service |
|
|
37
|
+
| `validatesession` | accept a versioned session record | browser capture, replay, or external event source |
|
|
38
|
+
|
|
39
|
+
## execution lifecycle
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
jobqueued
|
|
43
|
+
-> jobpreparing
|
|
44
|
+
-> runnerselected
|
|
45
|
+
-> jobrunning
|
|
46
|
+
-> jobsyncing
|
|
47
|
+
-> storagecommitted
|
|
48
|
+
-> jobcompleted
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Failures emit `jobfailed` with an error code, retryability, and a short message. Cleanup runs in a `finally` path after a working set is created. No provider owns retry policy; retry stays at the scheduler and orchestration boundary.
|
|
52
|
+
|
|
53
|
+
## operation modes
|
|
54
|
+
|
|
55
|
+
The same contracts support paired modes. A mode can exist without its pair, and an application can add the pair without changing the engine core.
|
|
56
|
+
|
|
57
|
+
| family | without | with |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| library | import the public factories | embed the engine inside an application |
|
|
60
|
+
| cli | use the local command surface | connect a custom dispatch adapter |
|
|
61
|
+
| binary | run the node entry point | package through a compiler or container |
|
|
62
|
+
| memory | internal working set | external, physical, vectorized, or library memory |
|
|
63
|
+
| file | internal artifact | external storage or content delivery |
|
|
64
|
+
| browser | headless job definition | capture and replay adapter |
|
|
65
|
+
| network | local deterministic job | remote provider and storage adapter |
|
|
66
|
+
|
|
67
|
+
The first cut implements library, cli, binary entry point, internal memory, and internal file. The other modes are extension points, not hardcoded promises.
|
|
68
|
+
|
|
69
|
+
## infrastructure rules
|
|
70
|
+
|
|
71
|
+
The engine uses no Netlify Functions and no Vercel Functions. A deployment can use an open Node server, a container, a worker, a forge workflow, or another host that can run the package.
|
|
72
|
+
|
|
73
|
+
The core does not hardcode a host or port. A future server adapter must receive both as configuration. A temporary session port must be drawn and locked by the caller before it is passed to the adapter. Credentials are never embedded in code and every external service is replaceable through a contract.
|
|
74
|
+
|
|
75
|
+
Database work remains an adapter concern. Prisma, Drizzle, MySQL2, and other compatible implementations may persist manifests, sessions, chunks, and jobs. Realtime remains an adapter concern and may use Socket or another open transport.
|
|
76
|
+
|
|
77
|
+
## security boundary
|
|
78
|
+
|
|
79
|
+
The core does not execute untrusted network code and does not expose credentials to job handlers. A remote runner adapter must declare its permissions, network policy, and cleanup behavior. Captcha solving, anti detection, and browser control are separate opt in surfaces and are not silently enabled by the engine.
|
|
80
|
+
|
|
81
|
+
## release rule
|
|
82
|
+
|
|
83
|
+
Every feature is added as a small root module, tested without credentials, and committed after the package check succeeds. External backends arrive one at a time. The public factory signatures stay stable while adapters evolve.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# saddle feature audit
|
|
2
|
+
|
|
3
|
+
This audit compares the current repository with the project README and the additional conclusions file. The repository is a library engine. It does not claim that a contract stub is the same as a production integration.
|
|
4
|
+
|
|
5
|
+
## foundation and engine
|
|
6
|
+
|
|
7
|
+
| feature | status | evidence or gap |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| root based JavaScript library | complete | no `src`; ESM package; grouped domains; lowercase paths |
|
|
10
|
+
| memory bridge | complete | load, persist, release, safe load, targets, transforms |
|
|
11
|
+
| memory modes | complete | internal, external, physical, vectorized, library contracts |
|
|
12
|
+
| job engine | complete | prepare, process, sync, cleanup, runner selection, events |
|
|
13
|
+
| chunked artifacts | complete | local and chunked adapters; checksums; manifest contracts |
|
|
14
|
+
| sessions | partial | JSONL save, load, validation, replay; no real browser capture runtime |
|
|
15
|
+
| crawler | partial | bounded BFS, same domain, robots, normalization, cache; no sitemap or adaptive renderer |
|
|
16
|
+
| universal API | partial | web request handler and public library helpers; no production task service with durable polling API |
|
|
17
|
+
| MCP | partial | optional tools contract; no external MCP SDK or packaged remote server mode |
|
|
18
|
+
| queue | partial | in memory and file persistence; no SQLite queue with crash recovery semantics |
|
|
19
|
+
| errors and retry | complete | taxonomy, recovery hints, retry policy, circuit breaker |
|
|
20
|
+
|
|
21
|
+
## browser, captcha, and proxy
|
|
22
|
+
|
|
23
|
+
| feature | status | decision |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| browser agent | partial | injected adapter only; Playwright, Brave, CDP, screenshots, and real input are not bundled |
|
|
26
|
+
| session fingerprint | partial | coherent profile and proxy binding exist; no TLS or HTTP2 patch layer |
|
|
27
|
+
| stealth patches | deferred | browser and network evasion is not silently embedded in a general library |
|
|
28
|
+
| proxy pool | complete | least used selection, health failures, graveyard, revive |
|
|
29
|
+
| captcha detection | complete | explicit contract, review pause, solver injection, evidence hash |
|
|
30
|
+
| captcha bypass | not implemented by design | no automatic challenge bypass or token abuse is shipped |
|
|
31
|
+
|
|
32
|
+
## storage, database, and compute
|
|
33
|
+
|
|
34
|
+
| feature | status | gap |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| GitHub Contents | partial | adapter is injectable; no live GitHub App authentication or release asset flow |
|
|
37
|
+
| S3 compatible and WebDAV | partial | generic file hosting adapter exists; no provider specific signing or multipart upload |
|
|
38
|
+
| GitLab, Forgejo, Gitea, HF, Kaggle, ModelScope, Terabox, Telegram, Discord | partial | some forge workflow templates exist; provider storage and bot adapters are not complete |
|
|
39
|
+
| Prisma, Drizzle, MySQL2 | partial | neutral contracts and query adapters exist; no deployed schema, migrations, or database runtime |
|
|
40
|
+
| file as compute | partial | manifests and local working sets exist; no durable chunk table, job table, or real rebuild worker |
|
|
41
|
+
| runner farm | partial | workflow manifests exist; no live first free runner dispatcher across accounts |
|
|
42
|
+
| Docker and compose | partial | safe templates exist; no built and published image or runtime acceptance test |
|
|
43
|
+
|
|
44
|
+
## productization
|
|
45
|
+
|
|
46
|
+
| feature | status | gap |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| npm package metadata | partial | package is publishable but still version `0.2.0` |
|
|
49
|
+
| GitHub release | missing | no release workflow, tag, changelog, or release assets for `1.0.0` |
|
|
50
|
+
| npm publishing | missing | no publish workflow; `NPM_TOKEN` must be configured as a GitHub secret |
|
|
51
|
+
| GitHub Packages and GHCR | missing | no package or image publishing jobs |
|
|
52
|
+
| Maven, NuGet, RubyGems, PyPI | missing | no language specific wrappers or trusted publishing jobs |
|
|
53
|
+
| jsDelivr, UNPKG, esm.sh | partial | URLs are documented; no release verification or SRI manifest |
|
|
54
|
+
| browser extension | partial | surface manifest only; no CRX source or permissions manifest |
|
|
55
|
+
| mobile and desktop apps | partial | target manifests only; no Tauri, Capacitor, Android, or iOS project |
|
|
56
|
+
| site and per site database | missing | no `web` application, Hono server, Drizzle schema, Prisma schema, or site deployment adapter |
|
|
57
|
+
| multi platform app identity | partial | generic bot and forge contracts; no OAuth or GitHub App installation flow |
|
|
58
|
+
|
|
59
|
+
## safety and release decision
|
|
60
|
+
|
|
61
|
+
The next production priority is not to add more vendor claims. It is to close the publish path: version metadata, changelog, release workflow, npm provenance, package smoke test, and a provider neutral durable job schema. Browser stealth, captcha bypass, account farming, and storage abuse are not release blockers because they are not safe defaults for this library.
|
|
62
|
+
|
|
63
|
+
The exposed npm token is treated as compromised. It must be revoked before any publication. A new token must be stored as the GitHub repository secret `NPM_TOKEN` or replaced by npm trusted publishing through GitHub Actions OIDC. No token belongs in source, commit history, command output, or chat.
|