@wenathlan/saddle 2.0.163 → 2.0.164
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/Dockerfile +26 -3
- package/README.md +1 -1
- package/cli.ts +99 -2
- package/communication.ts +455 -0
- package/docs/distributionlanes.md +19 -0
- package/docs/doc.go +25 -0
- package/docs/ninetymodalities.md +8 -0
- package/docs/osdistributions.md +149 -0
- package/integration.ts +221 -0
- package/modalities.ts +4665 -0
- package/package.json +11 -5
- package/scriptlanes.ts +774 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Distribution lanes
|
|
2
|
+
|
|
3
|
+
The eleven distribution lanes of the PR14 law and the honest status each lane answers for saddle at the 2.0.163 family rung. A lane never ships metadata its real surface cannot back: a lane without a real carrier stays a documented stub until the surface lands, and no version ever bumps to fake a lane.
|
|
4
|
+
|
|
5
|
+
| Lane | Carrier | Status | Note |
|
|
6
|
+
| --- | --- | --- | --- |
|
|
7
|
+
| npmjs | package.json — @wenathlan/saddle | live | the family-npmjs lane publishes the engine envelope (the heaviest pass, always last) |
|
|
8
|
+
| GitHub Packages npm | package.json — @wenathlan/saddle | live | the family-githubnpm lane publishes the same envelope idempotently |
|
|
9
|
+
| Maven | pom.xml — io.github.wenathlan:saddle | live | the family-maven lane deploys the one real jar of the family (the Saddle.java adapter) |
|
|
10
|
+
| NuGet | saddle.csproj | live | the family-nuget lane packs the engine envelope |
|
|
11
|
+
| Gradle | build.gradle.kts — com.wenathlan:saddle | live | the family-gradle lane publishes the module beside the maven coordinates |
|
|
12
|
+
| Ruby Gems | saddle.gemspec | live | the family-rubygems lane pushes the gem carrying the SaddleCli.rb process adapter |
|
|
13
|
+
| Docker Container | Dockerfile | live | the saddle-container lane attaches the engine archive and the vhe lanes publish the max / balanced / lite profile indexes under the eleven-architecture standard of containerarch.ts |
|
|
14
|
+
| Go Get | go.mod | stub | no Go source surface ships yet — the lane lands when a real Go adapter exists, never as an empty module |
|
|
15
|
+
| Cargo Crates | Cargo.toml | stub | no Rust source surface ships yet — the lane lands with a real crate, never as an empty manifest (the tauri rust envelope stays runner-side toolchain output) |
|
|
16
|
+
| PyPi | pyproject.toml | stub | no Python package surface ships yet — the qemubridge.py helper is a bridge script, not a wheel surface; the lane lands with a real package, never as an empty project table |
|
|
17
|
+
| Homebrew | Formula | stub | no tap repository ships yet — the formula lands pointing at the real release binaries with real checksums, never at a placeholder |
|
|
18
|
+
|
|
19
|
+
The engines block of package.json keeps Go, Rust and Python as declared toolchain floors — a catalog row is not a distribution lane. The dual npm law answers through the npmjs and GitHub Packages rows; the release binaries (the desktop bundles, the mobile artifacts, the vhe container archives) ride the family-desktop, family-mobile and saddle-container lanes every release rung.
|
package/docs/doc.go
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Package docs carries the go module identity of the saddle engine
|
|
2
|
+
// envelope — the binary computing engine that turns distributed storage
|
|
3
|
+
// into a publishable working set, carrying the merged virtual-hardware
|
|
4
|
+
// engine.
|
|
5
|
+
//
|
|
6
|
+
// The goget wave seats the go module rung beside the maven, nuget,
|
|
7
|
+
// rubygems and gradle rungs: the module resolves at
|
|
8
|
+
// github.com/wenathlan/devthink/saddle/v2 through the
|
|
9
|
+
// proxy.golang.org chain, and the version answers the per-module tag
|
|
10
|
+
// (saddle/vX.Y.Z on the release commit) the publish lane syncs — the
|
|
11
|
+
// package carries no version constant the metadata lockstep would have
|
|
12
|
+
// to stamp, the same way the gradle marker reads the envelope
|
|
13
|
+
// package.json at build time. The identity carrier rides the
|
|
14
|
+
// documentation support seat because the app root carries the C engine
|
|
15
|
+
// sources the go package law refuses beside a package.
|
|
16
|
+
package docs
|
|
17
|
+
|
|
18
|
+
// ModulePath is the go module path the go get consumer resolves.
|
|
19
|
+
const ModulePath = "github.com/wenathlan/devthink/saddle/v2"
|
|
20
|
+
|
|
21
|
+
// Family is the distribution family every envelope of the monorepo answers.
|
|
22
|
+
const Family = "wenathlan"
|
|
23
|
+
|
|
24
|
+
// Description is the envelope summary the registry metadata carries.
|
|
25
|
+
const Description = "The saddle engine envelope (the binary computing engine carrying the merged virtual-hardware engine)."
|
package/docs/ninetymodalities.md
CHANGED
|
@@ -189,3 +189,11 @@ os → grapheneos, omashi → omarchy, linux normal → linux. the owner's "v"
|
|
|
189
189
|
names no guest os: it answers the language-v lane of the modality
|
|
190
190
|
registry (the v compiler seat). nothing to add — the vocabulary already
|
|
191
191
|
covers every name the owner used.
|
|
192
|
+
|
|
193
|
+
the container side of the same law rides its own registry: the
|
|
194
|
+
OSDISTRIBUTIONS catalog of saddle/containerarch.ts (section 1c) carries
|
|
195
|
+
the seven system seats the saddle container starts its distribution with
|
|
196
|
+
— grapheneos, omarchy, vanilla os, maru os, the v seat, the normal linux
|
|
197
|
+
baseline and android — with the arch pairings, the engine plan trio
|
|
198
|
+
binding and the plan-resource law. see
|
|
199
|
+
[osdistributions.md](osdistributions.md) for the record.
|
|
@@ -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
|
+
}
|