@wenathlan/saddle 2.0.163 → 2.0.165

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.
@@ -0,0 +1,149 @@
1
+ # os.distributions — the seven system seats the saddle container carries
2
+
3
+ > the owner order (pr15, verbatim): "iniciar distribuição do container
4
+ > saddle com por exemplo, grapheneos, omarchy e outros sistemas dentro do
5
+ > saddle" — the saddle container starts its distribution catalog with
6
+ > grapheneos, omarchy and the other systems inside the saddle. and:
7
+ > "saddle também vira com outras distribuições de sistema operacional
8
+ > além de linux como por exemplo, maru os is firmware not an application
9
+ > / vanilla os is based on ubuntu / v / omarchy is arch linux-based
10
+ > distribution created by dhh / grapheneos is a privacy and security
11
+ > hardened version of android / linux normal / max / lite / balanced /
12
+ > android / arm64 / amd64 / vgpu / vcpu / por exemplo, 50gb por plano
13
+ > via config."
14
+
15
+ ## the registry
16
+
17
+ `saddle/containerarch.ts` — the file that already owned the
18
+ eleven-architecture standard and the build architecture table — now
19
+ carries the os distribution catalog beside them (section 1c of the
20
+ file): one honest row per system seat, pure data and pure functions,
21
+ frozen at module load, zero storage, zero node-only imports (the one
22
+ import it takes — `engineplans.ts` — is itself pure data).
23
+
24
+ | seat | kind | base (the true fact) | package | arch pairing | legs | tiers |
25
+ | --- | --- | --- | --- | --- | --- | --- |
26
+ | grapheneos | mobile | android | apk | linux-arm64, darwin-arm64-v8 | arm64 | max/balanced/lite |
27
+ | omarchy | desktop | arch linux | pacman | linux-x64, linux-x64-musl, linux-arm64 | amd64+arm64 | max/balanced/lite |
28
+ | vanillaos | distribution | ubuntu | apt | linux-x64, linux-x64-musl, linux-arm64 | amd64+arm64 | max/balanced/lite |
29
+ | maruos | firmware | android firmware with a debian desktop | abuild | linux-arm64, darwin-arm64-v8 | arm64 | max/balanced/lite |
30
+ | v | languageos | the v language platform (vlang, the 0.5.2 catalog seat) | vpm | linux-x64, linux-arm64, darwin-arm64, darwin-x64, windows-x64, wasm32 | amd64+arm64 | max/balanced/lite |
31
+ | linux | distribution | the plain linux baseline the engine container rides | apt | linux-x64, linux-x64-musl, linux-arm64, linux-riscv64 | amd64+arm64+riscv64 | max/balanced/lite |
32
+ | android | mobile | the android application platform | apk | linux-arm64, darwin-arm64-v8 | arm64 | max/balanced/lite |
33
+
34
+ the true facts the owner's law pins, verbatim in the rows' notes:
35
+
36
+ - **grapheneos** is a privacy and security hardened version of android
37
+ (the hardened mobile seat, the apk grammar, the arm64 legs).
38
+ - **omarchy** is the arch linux-based distribution created by dhh (the
39
+ desktop seat, the pacman grammar, the amd64 and arm64 legs).
40
+ - **vanilla os** is based on ubuntu (the atomic desktop seat beside the
41
+ plain linux baseline).
42
+ - **maru os** is firmware, not an application (the catalog carries the
43
+ seat honestly, the engine never installs it as a guest application —
44
+ it rides no vgpu path).
45
+ - **v** names the v language platform the dependency catalog pins at
46
+ 0.5.2 (`package.json` carries `"v": "^0.5.2"`), never a guest
47
+ distribution — the languagev compiler seat of the ninety modalities
48
+ answers it (the owner's nickname law, the same law
49
+ `docs/ninetymodalities.md` records).
50
+ - **linux** is the normal baseline: the default distribution the saddle
51
+ container itself rides (the resolver's default), every linux seat the
52
+ standard carries answers it.
53
+ - **android** is the mobile platform the grapheneos hardening builds on.
54
+
55
+ ## the pairings (real joins, never duplicated lists)
56
+
57
+ - the **arch pairing**: every `archs` entry of every row is a real seat
58
+ id of the eleven-architecture standard (`CONTAINERARCHIDS` — the
59
+ tests assert it, never an invented seat id). the legs column is not
60
+ stored data: `osdistributiondockerarchs(id)` derives the amd64/arm64
61
+ docker legs by joining the row's pairings against the carried seats
62
+ of `CONTAINERARCHES` (the target seats the v row pairs contribute no
63
+ leg — the binary and npm lanes own them).
64
+ - the **tier binding**: every seat binds the full max/balanced/lite
65
+ engine plan trio, and the trio itself is derived from
66
+ `ENGINEPLANS.map((row) => row.id)` — the ids stay the engineplans
67
+ table's own, never a duplicated tier list. the tier axis scales the
68
+ virtual hardware plan, never the catalog: the tier and the seat stay
69
+ orthogonal, exactly like the sandbox bases (`SANDBOXBASES`) stay
70
+ orthogonal to the guest os ids.
71
+ - the **virtual device law** (vgpu/vcpu): `vgpu` and `vcpu` are law
72
+ fields, not caps — vgpu marks the seats that present a virtual gpu
73
+ path (false for the firmware seat and the language seat), vcpu pins
74
+ that every seat rides the engine's virtual cpu bank. the counts
75
+ arrive from the engine plan profile, never from the catalog.
76
+
77
+ ## the plan-resource law (config, never a cap)
78
+
79
+ the owner's law: "por exemplo, 50gb por plano via config". the
80
+ `OSDISTRIBUTIONPLANLAW` row carries it as data:
81
+
82
+ - `examplestoragegb: 50` — the example per-plan storage quota, plan
83
+ config data a plan row stamps;
84
+ - `quotaowner: "devthink"` — the devthink system plan logic owns every
85
+ quota;
86
+ - `enginelimits: ENGINELIMITS` — the engineplans limits law rides the
87
+ row verbatim (the reuse law): unlimited workloads, unlimited
88
+ sessions, unlimited modalities, unlimited artifacts.
89
+
90
+ no row of the catalog carries a numeric resource field — the tests scan
91
+ every field of every row and fail on any number. the 50 gb is plan
92
+ config, the engine is unlimited, and the catalog never bakes a ceiling
93
+ (an engine limit would be a product decision wearing an engine costume
94
+ — the same law `docs/planningengine.md` and `engineplans.ts` record).
95
+
96
+ ## the consumable surface (what the container and the modality system read)
97
+
98
+ `containerarch.ts` exports the catalog the same way it exports the
99
+ arch standard:
100
+
101
+ - `OSDISTRIBUTIONIDS`, `OSDISTRIBUTIONS`, `OSDISTRIBUTIONCOUNT` — the
102
+ catalog rows (the publish matrix and the modality seats read the ids);
103
+ - `OSDISTRIBUTIONPROFILEIDS` — the derived engine plan trio (the tier
104
+ binding);
105
+ - `OSDISTRIBUTIONPLANLAW` — the plan-resource law of record;
106
+ - `getosdistribution`, `resolveosdistribution`, `osdistributionerror`
107
+ — the resolver (undefined/null answers the linux row, a known id
108
+ answers its row, anything else throws the coded
109
+ `OS_DISTRIBUTION_UNKNOWN` refusal naming the seven seats);
110
+ - `osdistributionsforarch`, `osdistributionsforprofile` — the joins the
111
+ modality system reads (which seats ride one arch seat; which seats
112
+ ride one tier — an unknown tier throws
113
+ `OS_DISTRIBUTION_PROFILE_UNKNOWN`);
114
+ - `osdistributiondockerarchs` — the derived docker legs (amd64, arm64,
115
+ riscv64) the container matrix reads;
116
+ - `osdistributioncoverage`, `osdistributionlines` — the honest coverage
117
+ report and the rendered table lines the container lanes stamp on
118
+ every profile run.
119
+
120
+ ## the build wiring (why no new build legs)
121
+
122
+ the distribution seats ride the arch axis the pinned base already
123
+ resolves: the mobile and firmware seats ride the arm64 legs, the
124
+ desktop and baseline seats ride the amd64 and arm64 legs — the same
125
+ legs the eleven-architecture standard carries. the dockerfile documents
126
+ the law (the `THE OS DISTRIBUTION SEATS` paragraph) and the publish
127
+ lanes stay untouched: no per-os build matrix exists or is needed, the
128
+ container itself keeps riding the normal linux baseline (the container
129
+ lane law) and the seven seats are the catalog the modality system hosts
130
+ inside the engine, never host distributions the container installs.
131
+
132
+ ## the visitor law (the critical law)
133
+
134
+ every system seat of the catalog is a hosted GUEST the engine drives on
135
+ its own twenty-four hour internet infrastructure — the catalog never
136
+ describes the visitor machine (no visitor gpu, no visitor cpu, no
137
+ visitor browser, no visitor local storage, no localforage). the engine
138
+ owns every cycle it spends through saddle and vault edge processing.
139
+
140
+ ## the pinning battery
141
+
142
+ `saddle/tests/osdistrib.test.ts` (eight tests, green): the seven seats
143
+ in law order, the true facts verbatim, the arch pairing honesty (every
144
+ pairing a real seat of the standard, the derived legs per seat), the
145
+ tier binding reused from engineplans (the deep-equality against
146
+ `ENGINEPLANS.map`), the plan-resource law (50 as plan config, the
147
+ engine unlimited, zero numeric fields in the rows), the resolver (the
148
+ linux default, the coded refusals), the arch and profile joins, and the
149
+ coverage report plus the rendered lines.
package/integration.ts ADDED
@@ -0,0 +1,221 @@
1
+ /**
2
+ * integration.ts — replaceable forge, application, MCP and transport adapters.
3
+ *
4
+ * This file is the v2.0.0 grand-merge consolidation of the former nested
5
+ * folders of the repository (adapters) into the single
6
+ * root-level domain file of the saddle family standard: one optimized
7
+ * TypeScript file per correlated domain, ordinal sections, JSDoc on every
8
+ * block, intra-domain imports dissolved, cross-domain imports rewritten to
9
+ * ./<domain>.js. Every capability of every source file is preserved here;
10
+ * the pure re-export barrels (none in this domain) folded their surface into this file directly.
11
+ */
12
+
13
+ import { jsondecode, jsonencode } from "./communication.ts";
14
+
15
+ /* ════════════════════════════════════════════════════════════════════ */
16
+ /* Section 1: adapters/transport.ts — transport centralizes timeout retry and jitter without choosing a host or vendor. */
17
+ /* ════════════════════════════════════════════════════════════════════ */
18
+ /**
19
+ * transport centralizes timeout retry and jitter without choosing a host or vendor.
20
+ */
21
+ export function transport(options = {}) {
22
+ const fetcher = options.fetcher ?? fetch;
23
+ const attempts = options.attempts ?? 3;
24
+ const timeout = options.timeout ?? 30000;
25
+ const retrycodes = new Set(options.retrycodes ?? [408, 409, 429, 500, 502, 503, 504]);
26
+ async function request(url, init = {}) {
27
+ let last;
28
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
29
+ const controller = new AbortController();
30
+ const timer = setTimeout(() => controller.abort(), timeout);
31
+ try {
32
+ const response = await fetcher(url, { ...init, signal: init.signal ?? controller.signal });
33
+ if (response.ok || !retrycodes.has(response.status) || attempt === attempts - 1) return response;
34
+ last = new Error(`request failed with ${response.status}`);
35
+ } catch (error) {
36
+ last = error;
37
+ if (attempt === attempts - 1) throw error;
38
+ } finally { clearTimeout(timer); }
39
+ await delay(backoff(options, attempt));
40
+ }
41
+ throw last ?? new Error("request failed");
42
+ }
43
+ return { request };
44
+ }
45
+
46
+ function backoff(options, attempt) { const base = options.backoff ?? 250; const jitter = options.jitter ?? 0; return base * 2 ** attempt + Math.floor(Math.random() * jitter); }
47
+ function delay(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)); }
48
+
49
+ /* ════════════════════════════════════════════════════════════════════ */
50
+ /* Section 2: adapters/socket.ts — socket adapter keeps realtime optional and accepts a caller supplied websocket constructor. */
51
+ /* ════════════════════════════════════════════════════════════════════ */
52
+ /**
53
+ * socket adapter keeps realtime optional and accepts a caller supplied websocket constructor.
54
+ */
55
+ export function socketadapter(options = {}) {
56
+ const websocket = options.websocket ?? globalThis.WebSocket;
57
+ if (!websocket) throw new Error("websocket implementation is required");
58
+ return {
59
+ connect(url, protocols) {
60
+ if (!url) throw new TypeError("socket url is required");
61
+ const socket = new websocket(url, protocols);
62
+ return { socket, send(value) { socket.send(typeof value === "string" ? value : JSON.stringify(value)); }, close(code, reason) { socket.close(code, reason); } };
63
+ }
64
+ };
65
+ }
66
+
67
+ /* ════════════════════════════════════════════════════════════════════ */
68
+ /* Section 3: adapters/github.ts — github adapter uses caller supplied credentials and base url configuration. */
69
+ /* ════════════════════════════════════════════════════════════════════ */
70
+ /**
71
+ * github adapter uses caller supplied credentials and base url configuration.
72
+ */
73
+
74
+ export function githubadapter(options = {}) {
75
+ if (!options.baseurl || typeof options.token !== "function") throw new TypeError("github adapter requires baseurl and token function");
76
+ const client = transport({ fetcher: options.fetcher, attempts: options.attempts, timeout: options.timeout });
77
+ async function call(path, init = {}) {
78
+ const token = await options.token();
79
+ const headers = { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "x-github-api-version": options.apiversion ?? "2022-11-28", ...(init.headers ?? {}) };
80
+ return client.request(new URL(path, options.baseurl), { ...init, headers });
81
+ }
82
+ return {
83
+ async health() { const response = await call("/rate_limit"); return { ok: response.ok, status: response.status }; },
84
+ async dispatch(owner, repository, workflow, input = {}) { const response = await call(`/repos/${owner}/${repository}/actions/workflows/${workflow}/dispatches`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ref: input.ref ?? "main", inputs: input.inputs ?? {} }) }); return { accepted: response.status === 204, status: response.status }; },
85
+ async run(owner, repository, runid) { const response = await call(`/repos/${owner}/${repository}/actions/runs/${runid}`); return response.json(); }
86
+ };
87
+ }
88
+
89
+ /* ════════════════════════════════════════════════════════════════════ */
90
+ /* Section 4: adapters/gitlab.ts — gitlab adapter keeps project addressing and token ownership outside the package. */
91
+ /* ════════════════════════════════════════════════════════════════════ */
92
+ /**
93
+ * gitlab adapter keeps project addressing and token ownership outside the package.
94
+ */
95
+
96
+ export function gitlabadapter(options = {}) {
97
+ const project = encodeURIComponent(options.project ?? "");
98
+ const base = forgeadapter({ ...options, kind: "gitlab" });
99
+ return { ...base, async dispatch(spec) { return base.dispatch({ ...spec, path: spec.path ?? `/api/v4/projects/${project}/pipeline` }); } };
100
+ }
101
+
102
+ /* ════════════════════════════════════════════════════════════════════ */
103
+ /* Section 5: adapters/forge.ts — forge adapter defines the common dispatch and artifact surface for compatible forges. */
104
+ /* ════════════════════════════════════════════════════════════════════ */
105
+ /**
106
+ * forge adapter defines the common dispatch and artifact surface for compatible forges.
107
+ */
108
+
109
+ export function forgeadapter(options = {}) {
110
+ if (!options.baseurl || typeof options.token !== "function") throw new TypeError("forge adapter requires baseurl and token function");
111
+ const client = transport({ fetcher: options.fetcher, attempts: options.attempts, timeout: options.timeout });
112
+ async function call(path, init = {}) { const token = await options.token(); return client.request(new URL(path, options.baseurl), { ...init, headers: { authorization: `Bearer ${token}`, accept: "application/json", ...(init.headers ?? {}) } }); }
113
+ return {
114
+ kind: options.kind ?? "forge",
115
+ async health(path = "/") { const response = await call(path); return { ok: response.ok, status: response.status }; },
116
+ async dispatch(spec) { if (!spec?.path || !spec.ref) throw new TypeError("forge dispatch requires path and ref"); const response = await call(spec.path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ref: spec.ref, inputs: spec.inputs ?? {} }) }); return { accepted: response.ok, status: response.status, body: response.json ? await response.json() : undefined }; },
117
+ async upload(spec) { if (!spec?.path || !spec.data) throw new TypeError("forge upload requires path and data"); const response = await call(spec.path, { method: "PUT", headers: { "content-type": spec.contenttype ?? "application/octet-stream" }, body: spec.data }); return { accepted: response.ok, status: response.status }; }
118
+ };
119
+ }
120
+
121
+ /* ════════════════════════════════════════════════════════════════════ */
122
+ /* Section 6: adapters/forgejo.ts — forgejo and gitea can reuse the open forge contract with a caller supplied base url. */
123
+ /* ════════════════════════════════════════════════════════════════════ */
124
+ /**
125
+ * forgejo and gitea can reuse the open forge contract with a caller supplied base url.
126
+ */
127
+
128
+ export function forgejoadapter(options = {}) { return forgeadapter({ ...options, kind: "forgejo" }); }
129
+ export function giteaadapter(options = {}) { return forgeadapter({ ...options, kind: "gitea" }); }
130
+ export function codebergadapter(options = {}) { return forgeadapter({ ...options, kind: "codeberg" }); }
131
+
132
+ /* ════════════════════════════════════════════════════════════════════ */
133
+ /* Section 7: adapters/huggingface.ts — hugging face storage remains an explicit adapter with caller supplied repository path. */
134
+ /* ════════════════════════════════════════════════════════════════════ */
135
+ /**
136
+ * hugging face storage remains an explicit adapter with caller supplied repository path.
137
+ */
138
+
139
+ export function huggingfaceadapter(options = {}) { return forgeadapter({ ...options, kind: "huggingface" }); }
140
+
141
+ /* ════════════════════════════════════════════════════════════════════ */
142
+ /* Section 8: adapters/apps.ts — app registry keeps installation, scope and revocation state outside platform credentials. */
143
+ /* ════════════════════════════════════════════════════════════════════ */
144
+ /**
145
+ * app registry keeps installation, scope and revocation state outside platform credentials.
146
+ */
147
+
148
+ export const appstatuses = Object.freeze(["installed", "suspended", "revoked"]);
149
+
150
+ /** Creates a caller-owned app installation registry with explicit scopes. */
151
+ export function appregistry(options = {}) {
152
+ const values = new Map(options.apps ? options.apps.map((app) => [app.id, normalize(app)]) : []);
153
+ function install(input = {}) { if (!input.id || !input.name) throw new TypeError("app installation requires id and name"); const app = normalize({ ...input, status: "installed", installedat: input.installedat ?? Date.now() }); values.set(app.id, app); return { ...app, scopes: [...app.scopes] }; }
154
+ function suspend(id) { return transition(id, "suspended"); }
155
+ function revoke(id) { return transition(id, "revoked"); }
156
+ function restore(id) { return transition(id, "installed"); }
157
+ function authorize(id, required = []) { const app = requireapp(id); if (app.status !== "installed") return { allowed: false, reason: `app-${app.status}` }; const missing = required.filter((scope) => !app.scopes.includes(scope)); return { allowed: missing.length === 0, missing, appid: app.id }; }
158
+ function get(id) { const app = values.get(String(id)); return app ? { ...app, scopes: [...app.scopes] } : null; }
159
+ function list() { return [...values.values()].map((app) => ({ ...app, scopes: [...app.scopes] })); }
160
+ return { install, suspend, revoke, restore, authorize, get, list };
161
+ function requireapp(id) { const app = values.get(String(id)); if (!app) throw new Error(`app not found: ${id}`); return app; }
162
+ function transition(id, status) { const app = requireapp(id); if (!appstatuses.includes(status)) throw new TypeError(`unsupported app status: ${status}`); app.status = status; app.updatedat = Date.now(); return get(id); }
163
+ }
164
+
165
+ function normalize(input) { return { id: String(input.id), name: String(input.name), status: input.status ?? "installed", scopes: [...new Set((input.scopes ?? []).map(String))], installedat: Number(input.installedat ?? Date.now()), updatedat: Number(input.updatedat ?? Date.now()), metadata: { ...(input.metadata ?? {}) } }; }
166
+
167
+ /* ════════════════════════════════════════════════════════════════════ */
168
+ /* Section 9: adapters/mcptransport.ts — mcp transport keeps JSON RPC framing separate from the tool server. */
169
+ /* ════════════════════════════════════════════════════════════════════ */
170
+ /**
171
+ * mcp transport keeps JSON RPC framing separate from the tool server.
172
+ */
173
+
174
+ /** Creates JSONL and HTTP handlers for the transport agnostic MCP server. */
175
+ export function mcptransport(server) {
176
+ if (typeof server?.handle !== "function") throw new TypeError("mcp transport requires server");
177
+ return {
178
+ async handleline(line) { const request = jsondecode(line); return jsonencode(await server.handle(request)); },
179
+ async *stream(lines) { for await (const line of lines) if (String(line).trim()) yield await this.handleline(line); },
180
+ async handlehttp(request) { const body = await request.text(); const response = await server.handle(jsondecode(body)); return new Response(jsonencode(response), { status: 200, headers: { "content-type": "application/json" } }); }
181
+ };
182
+ }
183
+
184
+ /* ════════════════════════════════════════════════════════════════════ */
185
+ /* Section 10: adapters/mcpserver.ts — mcp server implements a small JSON RPC tool surface and remains transport agnostic. */
186
+ /* ════════════════════════════════════════════════════════════════════ */
187
+ /**
188
+ * mcp server implements a small JSON RPC tool surface and remains transport agnostic.
189
+ */
190
+
191
+ export function mcpserver(options = {}) {
192
+ const tools = {
193
+ serialize: async (input) => jsonencode(input.value),
194
+ ...(options.browser ? browsertools(options.browser) : {}),
195
+ ...(options.tools ?? {})
196
+ };
197
+ return {
198
+ listtools() { return Object.keys(tools).map((name) => ({ name, description: `saddle ${name} tool` })); },
199
+ async handle(request) {
200
+ if (request.method === "tools/list") return { jsonrpc: "2.0", id: request.id, result: { tools: this.listtools() } };
201
+ if (request.method === "tools/call") { const handler = tools[request.params?.name]; if (!handler) return { jsonrpc: "2.0", id: request.id, error: { code: -32601, message: "tool not found" } }; try { return { jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text", text: JSON.stringify(await handler(request.params.arguments ?? {})) }] } }; } catch (error) { return { jsonrpc: "2.0", id: request.id, error: { code: -32000, message: error.message } }; } }
202
+ return { jsonrpc: "2.0", id: request.id, error: { code: -32601, message: "method not found" } };
203
+ }
204
+ };
205
+ }
206
+
207
+ /* ════════════════════════════════════════════════════════════════════ */
208
+ /* Section 11: adapters/mcpbrowser.ts — browser MCP tools expose snapshots and actions only through an injected browser adapter. */
209
+ /* ════════════════════════════════════════════════════════════════════ */
210
+ /**
211
+ * browser MCP tools expose snapshots and actions only through an injected browser adapter.
212
+ */
213
+
214
+ /** Creates optional browser tools for an MCP server without selecting Playwright or another vendor. */
215
+ export function browsertools(browser) {
216
+ if (typeof browser?.snapshot !== "function" || typeof browser?.action !== "function") throw new TypeError("browser MCP tools require snapshot and action");
217
+ return {
218
+ browser_snapshot: async (input = {}) => browser.snapshot(input),
219
+ browser_action: async (input = {}) => browser.action(input)
220
+ };
221
+ }