@melite/harness-provisioner 0.1.4-alpha → 0.2.0-alpha
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 +561 -457
- package/README.md +33 -1
- package/bin/infer-provision.mjs +348 -54
- package/cordis.patch.yml +18 -8
- package/package.json +12 -15
package/README.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: The vanilla-DSH provisioner — mounts the Melite estate bundle and renders ready-to-boot profiles into any DSH_HOME.
|
|
3
|
+
kind: package-bundle
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @melite/harness-provisioner
|
|
2
7
|
|
|
3
8
|
The provisioner mounts the Melite estate into a vanilla DSH installation and
|
|
@@ -19,12 +24,13 @@ The workspace-linked bundle mounts the estate host row and its DSH surfaces:
|
|
|
19
24
|
|
|
20
25
|
The package's own `cordis.patch.yml` and the packages' self-registration rows
|
|
21
26
|
own the Cordis composition. This README does not duplicate those rows.
|
|
27
|
+
Declared exports: `.` (the workspace-linked bundle row) and `./package.json`.
|
|
22
28
|
|
|
23
29
|
## Render profiles
|
|
24
30
|
|
|
25
31
|
```sh
|
|
26
32
|
node packages/harness/provisioner/bin/infer-provision.mjs all
|
|
27
|
-
node packages/harness/provisioner/bin/infer-provision.mjs profile
|
|
33
|
+
node packages/harness/provisioner/bin/infer-provision.mjs profile melite-test
|
|
28
34
|
node packages/harness/provisioner/bin/infer-provision.mjs profile headless --dsh-home /tmp/melite-dsh
|
|
29
35
|
```
|
|
30
36
|
|
|
@@ -63,3 +69,29 @@ pnpm run lint
|
|
|
63
69
|
The profile renderer and its package links are the source of truth for profile
|
|
64
70
|
membership; the current documentation index points here for the operator
|
|
65
71
|
workflow.
|
|
72
|
+
|
|
73
|
+
## Model Experience
|
|
74
|
+
|
|
75
|
+
### Request context and condition
|
|
76
|
+
|
|
77
|
+
#### What the model sees
|
|
78
|
+
|
|
79
|
+
None — the package contributes no model-visible input; it renders profiles
|
|
80
|
+
and mounts rows, and whatever reaches a model request arrives through the
|
|
81
|
+
mounted tool plane, not this package.
|
|
82
|
+
|
|
83
|
+
#### Token effect
|
|
84
|
+
|
|
85
|
+
Zero-direct token effect.
|
|
86
|
+
|
|
87
|
+
#### KV Cache effect
|
|
88
|
+
|
|
89
|
+
Independent — no conversation state.
|
|
90
|
+
|
|
91
|
+
## Known Limitations and Deferred Work
|
|
92
|
+
|
|
93
|
+
- **Rendered profiles are machine-local state** — they are not a source of
|
|
94
|
+
truth; re-render after manifest or workspace changes.
|
|
95
|
+
- **Headless selection is render-time** — `MELITE_HEADLESS` picks the
|
|
96
|
+
delegated run interface per profile; there is no per-request interface
|
|
97
|
+
switch.
|
package/bin/infer-provision.mjs
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* infer-provision — profile provisioner for vanilla DSH installs.
|
|
4
4
|
*
|
|
5
5
|
* The bundle patch (cordis.patch.yml) covers the ROWS; this bin covers
|
|
6
|
-
* the PROFILES: renders ready-to-boot profile dirs (
|
|
7
|
-
* headless / estate
|
|
6
|
+
* the PROFILES: renders ready-to-boot profile dirs (melite-test /
|
|
7
|
+
* headless / estate) and installs their link deps. Every render
|
|
8
|
+
* stamps render-provenance.json (F5: who/what/when/flags) and holds
|
|
9
|
+
* the profile's render lock across render+install (F6).
|
|
8
10
|
* Self-locating — every path derives from this file's own location
|
|
9
11
|
* (the provisioner package and its siblings), so a family re-split
|
|
10
12
|
* or repo move changes nothing.
|
|
@@ -14,17 +16,29 @@
|
|
|
14
16
|
* infer-provision profile <name> # render one profile dir
|
|
15
17
|
* infer-provision preset # install the agent preset
|
|
16
18
|
* Flags:
|
|
17
|
-
* --dsh-home <dir>
|
|
19
|
+
* --dsh-home <dir> default $DSH_HOME || ~/.dsh
|
|
20
|
+
* --estate-agent-route <p/m> estate agent brain, "provider/model"
|
|
21
|
+
* --estate-state-root <dir> override the estate row's stateRoot
|
|
22
|
+
* (walls render hermetic roots through
|
|
23
|
+
* config — never an env redirect)
|
|
24
|
+
* --profile-rows <file> operator rows (llm providers…), verbatim
|
|
18
25
|
*
|
|
19
|
-
* Rendered profiles mount the SAME rows the bundle patch mounts
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
26
|
+
* Rendered profiles mount the SAME melite rows the bundle patch mounts
|
|
27
|
+
* (the bundle owns the rows, so `dsh plugin add` and rendered profiles
|
|
28
|
+
* can never drift apart). The PROFILE layer (this dir's cordis.patch.yml)
|
|
29
|
+
* owns what no bundle can know: the estate row's seat config — repoRoot
|
|
30
|
+
* always (mechanical: this render's own repo root; the queue seats refuse
|
|
31
|
+
* an unpinned boot loud by design) and agentRoute when the flag names a
|
|
32
|
+
* brain — plus any operator rows spliced verbatim by --profile-rows
|
|
33
|
+
* (llm provider rows ride the profile layer the same way the web
|
|
34
|
+
* profile's own patch carries them; the file is never re-serialized).
|
|
35
|
+
* The template pins: our bundle + the dsh web base for 'melite-test',
|
|
36
|
+
* the plain headless base for 'headless'.
|
|
24
37
|
*/
|
|
25
38
|
|
|
26
39
|
import { execSync } from 'node:child_process'
|
|
27
|
-
import {
|
|
40
|
+
import { createHash } from 'node:crypto'
|
|
41
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
28
42
|
import { dirname, join, resolve } from 'node:path'
|
|
29
43
|
import { fileURLToPath } from 'node:url'
|
|
30
44
|
import process from 'node:process'
|
|
@@ -40,14 +54,117 @@ const flag = (name, dflt) => {
|
|
|
40
54
|
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt
|
|
41
55
|
}
|
|
42
56
|
const DSH_HOME = flag('--dsh-home', process.env.DSH_HOME || join(process.env.HOME || '~', '.dsh'))
|
|
43
|
-
const PROFILE = argv[1] && !argv[1].startsWith('--') ? argv[1] : '
|
|
57
|
+
const PROFILE = argv[1] && !argv[1].startsWith('--') ? argv[1] : 'melite-test'
|
|
58
|
+
const ESTATE_AGENT_ROUTE = flag('--estate-agent-route', '')
|
|
59
|
+
const ESTATE_STATE_ROOT = flag('--estate-state-root', '')
|
|
60
|
+
const PROFILE_ROWS = flag('--profile-rows', '')
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
// files IT rendered. A package.json with no provenance stamp naming
|
|
64
|
+
// this renderer (a hand-composed profile, another tool's output)
|
|
65
|
+
|
|
66
|
+
// original gone. See the guard at the package.json write.
|
|
67
|
+
const FORCE_RENDER = argv.includes('--force-render')
|
|
44
68
|
|
|
45
69
|
const log = (...a) => console.log(...a)
|
|
46
70
|
const die = (msg) => { console.error('infer-provision: ' + msg); process.exit(1) }
|
|
47
71
|
|
|
72
|
+
// ── F6 guards: atomic writes, the render lock, the install ceiling ──
|
|
73
|
+
// ATOMIC WRITES: the renderer owns its files wholesale but wrote
|
|
74
|
+
// them direct — a crash mid-render left a half-written profile that
|
|
75
|
+
// boots strange. Temp + rename per file (J6's writeJsonAtomic idiom,
|
|
76
|
+
// local: this bin is self-contained, no workspace imports). The tmp
|
|
77
|
+
// name is FIXED per file (never pid-suffixed): a killed run's
|
|
78
|
+
// leftover is overwritten by the next render, never accumulated —
|
|
79
|
+
// and the render lock below serializes renders, so no two writers
|
|
80
|
+
// share a tmp.
|
|
81
|
+
function writeFileAtomic(path, content) {
|
|
82
|
+
writeFileSync(path + '.tmp', content)
|
|
83
|
+
renameSync(path + '.tmp', path)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// THE RENDER LOCK: two renders (two `all` runs, a render racing a
|
|
87
|
+
// re-render) interleaved package.json and the patch row by row —
|
|
88
|
+
// both callers rendered-then-installed with no mutual exclusion.
|
|
89
|
+
// One lock file per profile dir, exclusive-create: a live holder
|
|
90
|
+
// refuses loud, a stale one (dead pid) is taken with a warning.
|
|
91
|
+
const LOCK_NAME = '.render.lock'
|
|
92
|
+
function acquireLock(dst) {
|
|
93
|
+
const lock = join(dst, LOCK_NAME)
|
|
94
|
+
const take = () => writeFileSync(lock, String(process.pid) + '\n', { flag: 'wx' })
|
|
95
|
+
try {
|
|
96
|
+
take()
|
|
97
|
+
return
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (err === null || typeof err !== 'object' || err.code !== 'EEXIST') throw err
|
|
100
|
+
}
|
|
101
|
+
const prev = readFileSync(lock, 'utf8').split('\n')[0]
|
|
102
|
+
const pid = Number(prev)
|
|
103
|
+
let alive = false
|
|
104
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
105
|
+
try { process.kill(pid, 0); alive = true } catch { /* dead */ }
|
|
106
|
+
}
|
|
107
|
+
if (alive) die(`profile dir ${dst} is already being rendered (lock ${lock}, holder pid ${pid}) — refusing a concurrent render`)
|
|
108
|
+
log(` stale render lock (holder pid ${prev.trim() || '<unparsed>'} dead) — taking it`)
|
|
109
|
+
try { unlinkSync(lock) } catch { /* raced away */ }
|
|
110
|
+
try {
|
|
111
|
+
take()
|
|
112
|
+
} catch {
|
|
113
|
+
die(`profile dir ${dst} lost a lock race (${lock}) — re-run the render`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function releaseLock(dst) {
|
|
117
|
+
try { unlinkSync(join(dst, LOCK_NAME)) } catch { /* never held / already gone */ }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// PROVENANCE (provisioner audit F5): a rendered profile dir is an
|
|
121
|
+
// artifact with no memory — when a boot misbehaves nothing says
|
|
122
|
+
// which provisioner, which tree, which flags laid it. Every render
|
|
123
|
+
// writes render-provenance.json beside the profile files:
|
|
124
|
+
// machine-readable, DSH-loader-inert (a data file, never a config
|
|
125
|
+
// plane — the loader reads package.json/cordis.* only), the
|
|
126
|
+
// fossil-vs-fresh question answered without archaeology.
|
|
127
|
+
function provenanceDoc(name, provPkg, bundles, profileRowsSha) {
|
|
128
|
+
const root = resolve(PROVISIONER, '../../..')
|
|
129
|
+
let commit = null
|
|
130
|
+
let dirty = null
|
|
131
|
+
try {
|
|
132
|
+
const sha = execSync('git rev-parse HEAD', { cwd: root, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
|
|
133
|
+
commit = /^[0-9a-f]{40}$/.test(sha) ? sha : null
|
|
134
|
+
if (commit !== null) {
|
|
135
|
+
dirty = execSync('git status --porcelain', { cwd: root, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim() !== ''
|
|
136
|
+
}
|
|
137
|
+
} catch { /* not a git checkout — the stamp says so honestly (nulls) */ }
|
|
138
|
+
return {
|
|
139
|
+
renderedAt: new Date().toISOString(),
|
|
140
|
+
renderer: 'infer-provision',
|
|
141
|
+
provisioner: { name: provPkg.name, version: provPkg.version },
|
|
142
|
+
source: { root, commit, dirty },
|
|
143
|
+
profile: name,
|
|
144
|
+
bundles: [...bundles],
|
|
145
|
+
flags: {
|
|
146
|
+
estateAgentRoute: ESTATE_AGENT_ROUTE || null,
|
|
147
|
+
estateStateRoot: ESTATE_STATE_ROOT || null,
|
|
148
|
+
profileRows: PROFILE_ROWS ? { path: PROFILE_ROWS, sha256: profileRowsSha } : null,
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// agentRoute wears the estate row's own shape (packages/web/melite-estate
|
|
154
|
+
// src/index.ts): "provider/model" — the same regex the seat applies. A
|
|
155
|
+
// malformed route dies HERE, at the earliest resolvable point, never as a
|
|
156
|
+
// silently-unseated engine three boots later.
|
|
157
|
+
if (ESTATE_AGENT_ROUTE && !/^([^/\s]+)\/(.+)$/.test(ESTATE_AGENT_ROUTE)) {
|
|
158
|
+
die('--estate-agent-route wants provider/model (e.g. opencode-go/muse-spark-1.3-contributor), got "' + ESTATE_AGENT_ROUTE + '"')
|
|
159
|
+
}
|
|
160
|
+
|
|
48
161
|
// ── profile rendering ───────────────────────────────────────────────────
|
|
49
162
|
const PROFILE_BASES = {
|
|
50
|
-
//
|
|
163
|
+
// melite-test = THE studio/test lane, ONE name (single-setup law
|
|
164
|
+
|
|
165
|
+
// two names for one mount, and the README + runbook pointed at
|
|
166
|
+
// different ones. `inference` is gone; this base is the only web
|
|
167
|
+
// mount): web app base + the SELF-REGISTERING
|
|
51
168
|
|
|
52
169
|
// its existence row in ITS OWN package — the provisioner stopped
|
|
53
170
|
|
|
@@ -59,9 +176,38 @@ const PROFILE_BASES = {
|
|
|
59
176
|
// @melite/card-base/client for the browser graph — every card
|
|
60
177
|
// client externalizes it, and without the row the loader's
|
|
61
178
|
// require misses the module table at web boot.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
179
|
+
// three-client rides the WEB base for the SAME law (the :3088 boot
|
|
180
|
+
|
|
181
|
+
// externalize @melite/three-client/client, and three-client's own
|
|
182
|
+
// patch is the ONLY row that materializes it ("bundling
|
|
183
|
+
// @melite/three-client in a profile applies this patch" — the
|
|
184
|
+
// package's own contract). Without the bundle, four plugins die
|
|
185
|
+
// at boot with `missed the module table` and the deck pends on
|
|
186
|
+
// remote.manage behind them.
|
|
187
|
+
// dsh-estate rides the web base too (the :3088 browser census,
|
|
188
|
+
|
|
189
|
+
// as child plugins — drafts (bench/subscribe), models (the
|
|
190
|
+
// /v1/model-catalog discovery door), media, queue, runs, manage.
|
|
191
|
+
// Without it the studio lane BOOTS CLEAN but serves a husk: the
|
|
192
|
+
// bench spotlight stream and the catalog toast 500 in the browser
|
|
193
|
+
// console while the boot log stays silent — the same
|
|
194
|
+
// seed-has-it-base-does-not drift as three-client.
|
|
195
|
+
// the two card STUDIOS ride the web base (the H3 bar autopsy,
|
|
196
|
+
|
|
197
|
+
// own their rows in their OWN bundle patches, and this bundle's
|
|
198
|
+
// patch carries no twin ("NO row here — the profile bundles it",
|
|
199
|
+
// cordis.patch.yml) — so while no base mounted them they were
|
|
200
|
+
// deps with no door: installed by the manifest mirror above, rows
|
|
201
|
+
// mounted by nothing, client entries in no browser graph. The H3
|
|
202
|
+
// card then rendered the generic form — NO timeline bar — with
|
|
203
|
+
// every wall green (the e2e seed mounts both; the live base did
|
|
204
|
+
// not). @melite/3d-img-to-mesh needs no bundle row: this bundle's
|
|
205
|
+
// own card-studio-img-to-mesh row mounts it, and a bundle would be
|
|
206
|
+
// the twin-row crash law.
|
|
207
|
+
'melite-test': ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@melite/harness-provisioner',
|
|
208
|
+
'@melite/harness-tools', '@melite-plugin/dsh-estate', '@melite/card-base', '@melite/three-client',
|
|
209
|
+
'@melite-plugin/dsh-card-view', '@melite-plugin/dsh-card-deck', '@melite-plugin/dsh-gallery',
|
|
210
|
+
'@melite/video-h3-timeline', '@melite/motion-motion', '@melite/image-input-prep'],
|
|
65
211
|
// headless = the agent server: one-shot task runner + the tools
|
|
66
212
|
// plane (harness-tools self-registers its melite-tools row; the
|
|
67
213
|
// renderer disables the web-plane doors row in this mount).
|
|
@@ -81,16 +227,6 @@ const PROFILE_BASES = {
|
|
|
81
227
|
// tools row's HEADLESS interface by its self-registration id.
|
|
82
228
|
estate: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-sdk-app', '@melite/harness-provisioner',
|
|
83
229
|
'@melite/harness-tools', '@melite-plugin/dsh-estate'],
|
|
84
|
-
// melite-test = THE TEST LANE, RE-COMPOSED (REBUILD-20, gateway
|
|
85
|
-
|
|
86
|
-
// melite-test profile mounting the estate API as a SECOND
|
|
87
|
-
// process on :33081) died with the gateway. Its replacement is
|
|
88
|
-
// the SAME row set as the studio — web app base + the estate
|
|
89
|
-
// rows in ONE process. Custom bakes serve from this name; the
|
|
90
|
-
// :33081 wire never existed here.
|
|
91
|
-
'melite-test': ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@melite/harness-provisioner',
|
|
92
|
-
'@melite/harness-tools', '@melite/card-base', '@melite-plugin/dsh-card-view', '@melite-plugin/dsh-card-deck',
|
|
93
|
-
'@melite-plugin/dsh-gallery'],
|
|
94
230
|
}
|
|
95
231
|
|
|
96
232
|
/** Maps every workspace package's name to its directory by reading
|
|
@@ -138,7 +274,14 @@ function workspacePackages() {
|
|
|
138
274
|
function renderProfile(name) { // In-box bundles (@deepseek-ai/*) resolve from the DSH INSTALL, never
|
|
139
275
|
// from a registry - only OUR link goes in dependencies. Mirrors what
|
|
140
276
|
// `dsh plugin` itself initializes (PROFILE_TEMPLATES + reconcile).
|
|
141
|
-
|
|
277
|
+
|
|
278
|
+
// old `|| PROFILE_BASES.headless` fallback turned a typo into a
|
|
279
|
+
// bootable-looking directory with the wrong rows and exit 0.
|
|
280
|
+
if (!(name in PROFILE_BASES)) {
|
|
281
|
+
console.error(`infer-provision: unknown profile '${name}' — known: ${Object.keys(PROFILE_BASES).join(', ')}`)
|
|
282
|
+
process.exit(1)
|
|
283
|
+
}
|
|
284
|
+
const bundles = PROFILE_BASES[name]
|
|
142
285
|
const dst = join(DSH_HOME, 'profiles', name)
|
|
143
286
|
mkdirSync(dst, { recursive: true })
|
|
144
287
|
const link = 'link:' + PROVISIONER
|
|
@@ -170,10 +313,45 @@ function renderProfile(name) { // In-box bundles (@deepseek-ai/*) resolve from
|
|
|
170
313
|
// the provisioner is linked, its install resolves it in the repo
|
|
171
314
|
// workspace.
|
|
172
315
|
}
|
|
316
|
+
|
|
317
|
+
// non-in-box bundle the profile base mounts ALSO becomes a direct
|
|
318
|
+
// link: dep — the F5 comment's own intent above ("every plugin
|
|
319
|
+
// package the bundle rows name must become a DIRECT link: dep"),
|
|
320
|
+
// which the mirror-the-manifest loop never implemented for bundles
|
|
321
|
+
// outside the provisioner's own manifest (@melite/card-base rode
|
|
322
|
+
// the inference/melite-test bases with no installed package, and
|
|
323
|
+
|
|
324
|
+
// @deepseek-ai/* bundles resolve from the DSH install, never here;
|
|
325
|
+
// any other unresolvable bundle fails the RENDER loud, never a
|
|
326
|
+
// boot three steps later.
|
|
327
|
+
const bundleDeps = {}
|
|
328
|
+
for (const b of bundles) {
|
|
329
|
+
if (b.startsWith('@deepseek-ai/')) continue
|
|
330
|
+
if (!byName.has(b)) die(`profile base mounts ${b} but no workspace package carries that name — refusing an unresolvable bundle`)
|
|
331
|
+
bundleDeps[b] = 'link:' + byName.get(b)
|
|
332
|
+
}
|
|
173
333
|
|
|
174
334
|
const pkgPath = join(dst, 'package.json')
|
|
175
335
|
let pkg = { name: 'dsh-profile-' + name, private: true, dependencies: {} }
|
|
176
336
|
if (existsSync(pkgPath)) {
|
|
337
|
+
// F1 refuse-or-backup: without a stamp naming THIS renderer the
|
|
338
|
+
// render dies loud; --force-render backs the file up and proceeds.
|
|
339
|
+
// A file with our own stamp re-renders freely — the no-drift law
|
|
340
|
+
// (the renderer owns its output wholesale) is unchanged.
|
|
341
|
+
if (!FORCE_RENDER) {
|
|
342
|
+
let mine = false
|
|
343
|
+
try {
|
|
344
|
+
const prov = JSON.parse(readFileSync(join(dst, 'render-provenance.json'), 'utf8'))
|
|
345
|
+
mine = prov !== null && typeof prov === 'object' && prov.renderer === 'infer-provision'
|
|
346
|
+
} catch { /* no stamp, or unparseable: not provably ours */ }
|
|
347
|
+
if (!mine) {
|
|
348
|
+
die('refusing to overwrite ' + pkgPath + ' — no render-provenance.json names this renderer '
|
|
349
|
+
+ '(a hand-composed or foreign file). Move it aside, or pass --force-render to back it up '
|
|
350
|
+
+ 'as package.json.prerender.bak and re-render')
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
copyFileSync(pkgPath, pkgPath + '.prerender.bak')
|
|
354
|
+
}
|
|
177
355
|
try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) } catch { /* re-render */ }
|
|
178
356
|
}
|
|
179
357
|
pkg.name = pkg.name || 'dsh-profile-' + name
|
|
@@ -185,9 +363,10 @@ function renderProfile(name) { // In-box bundles (@deepseek-ai/*) resolve from
|
|
|
185
363
|
// and pnpm warned "installing a dependency from a non-existent
|
|
186
364
|
// directory" on every install while the profile silently carried
|
|
187
365
|
|
|
188
|
-
// on every render;
|
|
189
|
-
//
|
|
190
|
-
|
|
366
|
+
// on every render; bundleDeps cover every mounted non-in-box bundle
|
|
367
|
+
// (the coverage cure above); user-added registry deps belong in the
|
|
368
|
+
// bundle, not here.
|
|
369
|
+
pkg.dependencies = { ...bundleDeps, ...rowDeps, '@melite/harness-provisioner': link }
|
|
191
370
|
|
|
192
371
|
// twin rows for the self-registering packages (channel law
|
|
193
372
|
|
|
@@ -203,57 +382,173 @@ function renderProfile(name) { // In-box bundles (@deepseek-ai/*) resolve from
|
|
|
203
382
|
// the bases). The bases OWN this list wholesale; a retired
|
|
204
383
|
// package leaves the profile the day it leaves PROFILE_BASES.
|
|
205
384
|
pkg.dsh = { profile: { bundles: [...new Set(bundles)] } }
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
// row (no drift). NON-web mounts (headless/SDK) get the
|
|
209
|
-
// rows mechanically DISABLED (by the
|
|
385
|
+
writeFileAtomic(join(dst, 'package.json'), JSON.stringify(pkg, null, 2) + String.fromCharCode(10))
|
|
386
|
+
// The cordis.yml USER layer stays EMPTY — the bundle patch owns every
|
|
387
|
+
// melite row (no drift). NON-web mounts (headless/SDK) get the
|
|
388
|
+
// web-surface rows mechanically DISABLED in the profile patch (by the
|
|
210
389
|
|
|
211
|
-
// The bundle owns the rows; the renderer
|
|
212
|
-
// shape.
|
|
390
|
+
// exists to activate them. The bundle owns the rows; the renderer
|
|
391
|
+
// owns the per-app mount shape and the seat pins below.
|
|
213
392
|
const webMount = bundles.includes('@deepseek-ai/dsh-web-app')
|
|
214
393
|
// melite-tools-doors is web-profiles-only by its own contract (the
|
|
215
394
|
// ✦ doors need webServer+llm — absent in sdk/headless mounts); the
|
|
216
395
|
// renderer disables it with the other web rows in non-web mounts.
|
|
217
396
|
const webRows = ['melite-tools-doors',
|
|
218
397
|
'infer-card-view', 'ray-card-deck', 'infer-gallery']
|
|
219
|
-
|
|
398
|
+
|
|
399
|
+
// decided seating is retired — the estate row's CONFIG owns it. Every
|
|
400
|
+
// profile mounting dsh-estate carries repoRoot here: mechanical (this
|
|
401
|
+
// render's own repo root), so the runs/drafts seats never boot
|
|
402
|
+
// unpinned (they refuse loud by design). agentRoute names the agent
|
|
403
|
+
// brain (provider/model); absent, the estate logs the engine unseated
|
|
404
|
+
// — the honest state, never a silent default. acquireStallTimeoutMs
|
|
405
|
+
|
|
406
|
+
// the lane rides a pinned 10s, never the library default by accident.
|
|
407
|
+
|
|
408
|
+
// downloads-stream audit): the lane is the long-lived process that
|
|
409
|
+
// owns fetch lifetimes — its durable claim ticker claims requested
|
|
410
|
+
// rows and stamps the claimer heartbeat, so a headless press's
|
|
411
|
+
// wanted download survives its own session dying. A non-web mount
|
|
412
|
+
// (headless task, estate job server) is short-lived: a claimer in
|
|
413
|
+
// it would orphan every fetch it died mid-flight, so its presses
|
|
414
|
+
// take their own wire, exactly as before.
|
|
415
|
+
const estateRow = bundles.includes('@melite-plugin/dsh-estate')
|
|
416
|
+
? '# ESTATE SEAT PIN — the estate row config owns every seat:\n'
|
|
417
|
+
+ '# repoRoot pins the runs/drafts seats (unpinned boots refuse loud);\n'
|
|
418
|
+
+ '# stateRoot overrides it (J12: walls render hermetic roots by\n'
|
|
419
|
+
+ '# config — stateRoot wins over repoRoot in the queue seat).\n'
|
|
420
|
+
+ '# agentRoute names the agent brain (absent = unseated, logged loud).\n'
|
|
421
|
+
+ '# acquireStallTimeoutMs: the deliberate stall budget (audit F7).\n'
|
|
422
|
+
+ (webMount
|
|
423
|
+
? '# durableDownloads: the lane claims requested rows (the lifetime\n'
|
|
424
|
+
+ '# finding) — long-lived web mounts only, never a task process.\n'
|
|
425
|
+
: '')
|
|
426
|
+
+ '- id: melite-estate\n'
|
|
427
|
+
+ ' config:\n'
|
|
428
|
+
+ ' repoRoot: ' + JSON.stringify(resolve(PROVISIONER, '../../..')) + '\n'
|
|
429
|
+
+ ' acquireStallTimeoutMs: 10000\n'
|
|
430
|
+
+ (webMount ? ' durableDownloads: true\n' : '')
|
|
431
|
+
+ (ESTATE_STATE_ROOT
|
|
432
|
+
? ' stateRoot: ' + JSON.stringify(ESTATE_STATE_ROOT) + '\n'
|
|
433
|
+
: '')
|
|
434
|
+
+ (ESTATE_AGENT_ROUTE
|
|
435
|
+
? ' agentRoute: ' + JSON.stringify(ESTATE_AGENT_ROUTE) + '\n'
|
|
436
|
+
: '')
|
|
437
|
+
: ''
|
|
438
|
+
// OPERATOR ROWS (--profile-rows <file>): verbatim text splice, never
|
|
439
|
+
// re-serialized (the web profile's preset-row law). Llm provider rows
|
|
440
|
+
// ride the profile layer because the bundle patch owns MELITE rows
|
|
441
|
+
// only — the same home the web profile's own patch gives its
|
|
442
|
+
// providers. Rows apply by id; a row here never inserts a melite
|
|
443
|
+
// bundle row (the no-drift law holds — the ids belong to the
|
|
444
|
+
// operator's adapter mounts).
|
|
445
|
+
let operatorRows = ''
|
|
446
|
+
let profileRowsSha = null
|
|
447
|
+
if (PROFILE_ROWS) {
|
|
448
|
+
if (!existsSync(PROFILE_ROWS)) die('--profile-rows: no such file: ' + PROFILE_ROWS)
|
|
449
|
+
const text = readFileSync(PROFILE_ROWS, 'utf8')
|
|
450
|
+
profileRowsSha = createHash('sha256').update(text, 'utf8').digest('hex')
|
|
451
|
+
const firstRow = text.split('\n').find((l) => l.trim() !== '' && !l.trim().startsWith('#'))
|
|
452
|
+
if (firstRow === undefined || !firstRow.startsWith('- ')) {
|
|
453
|
+
die('--profile-rows: ' + PROFILE_ROWS + ' is not a YAML row list (the first non-comment line must start with "- ")')
|
|
454
|
+
}
|
|
455
|
+
operatorRows = '# operator rows (verbatim splice: ' + PROFILE_ROWS + ')\n'
|
|
456
|
+
+ text + (text.endsWith('\n') ? '' : '\n')
|
|
457
|
+
}
|
|
458
|
+
writeFileAtomic(join(dst, 'cordis.patch.yml'),
|
|
220
459
|
webMount
|
|
221
|
-
? '# rendered by infer-provision - rows live in the bundle
|
|
222
|
-
|
|
460
|
+
? '# rendered by infer-provision - melite rows live in the bundle;\n'
|
|
461
|
+
+ '# the rows below are the profile layer (seat pin + operator rows).\n'
|
|
462
|
+
+ (estateRow + operatorRows || '[]\n')
|
|
463
|
+
: '# rendered by infer-provision - melite rows live in the bundle.\n'
|
|
223
464
|
+ '# No webServer service in this mount: the four web-surface rows\n'
|
|
224
465
|
+ '# are disabled so activation cannot pend forever.\n'
|
|
225
466
|
+ webRows.map(id => '- id: ' + id + '\n disabled: true\n').join('')
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
467
|
+
+ estateRow + operatorRows
|
|
468
|
+
// DELEGATED-OPERATOR MOUNTS (estate | headless): this mount
|
|
469
|
+
// RUNS jobs — the tools row selects the HEADLESS interface
|
|
470
|
+
// (the melite_flow + run_status run doors). Pinned in the
|
|
471
|
+
// render (not ambient env) because the interface is profile
|
|
472
|
+
// state: browser mounts compose, the job server executes.
|
|
473
|
+
// The 'headless' agent server rides the same pin — its base
|
|
474
|
+
// list promises melite_flow ("a headless task that composes
|
|
475
|
+
// media reaches the same runs/queue/drafts seats"), and the
|
|
476
|
+
|
|
477
|
+
// compose-only mount: benches composed and validated, every
|
|
478
|
+
// execution refused at the absent verb. Every other mount
|
|
479
|
+
// keeps the browser default.
|
|
480
|
+
+ (name === 'estate' || name === 'headless'
|
|
233
481
|
? '# HEADLESS interface: the delegated operator runs jobs here.\n'
|
|
234
482
|
+ '- id: melite-tools\n config:\n headless: true\n'
|
|
235
483
|
: ''))
|
|
236
|
-
|
|
484
|
+
writeFileAtomic(join(dst, 'cordis.yml'), '# rendered by infer-provision' + '\n' + '[]' + '\n')
|
|
237
485
|
const ws = join(dst, 'pnpm-workspace.yaml')
|
|
238
|
-
if (!existsSync(ws))
|
|
486
|
+
if (!existsSync(ws)) writeFileAtomic(ws, 'packages:' + '\n' + ' - node_modules/*' + '\n')
|
|
487
|
+
// F5 — the provenance stamp (see provenanceDoc): fossil-vs-fresh,
|
|
488
|
+
// answered in one read.
|
|
489
|
+
writeFileAtomic(join(dst, 'render-provenance.json'),
|
|
490
|
+
JSON.stringify(provenanceDoc(name, provPkg, bundles, profileRowsSha), null, 2) + '\n')
|
|
239
491
|
|
|
240
492
|
log('rendered profile :', dst)
|
|
241
493
|
log(' bundles :', bundles.join(', '))
|
|
242
494
|
return dst
|
|
243
495
|
}
|
|
244
496
|
|
|
497
|
+
// INSTALL GUARDS (F6): the install ran with no timeout (a hung
|
|
498
|
+
// registry stalled forever), no catch (a failed install threw a raw
|
|
499
|
+
// stack with no manual-command hint), and both callers ignored the
|
|
500
|
+
// `false` return (missing-pnpm printed a warning, then the boot
|
|
501
|
+
// lines printed as if everything were installed). Now: a 10-minute
|
|
502
|
+
// ceiling (link-only installs take seconds; a cold-registry fetch
|
|
503
|
+
// takes minutes; a hang dies loud, never stalls CI forever) and
|
|
504
|
+
// every failure logged loud with the manual command. The boolean is
|
|
505
|
+
// HONORED by both callers through renderInstall below.
|
|
506
|
+
const INSTALL_TIMEOUT_MS = 600_000
|
|
245
507
|
function installDeps(profileDir) {
|
|
246
508
|
const pm = execSync('command -v pnpm || true').toString().trim()
|
|
247
509
|
|| join(process.env.HOME || '', '.local/share/pnpm/pnpm')
|
|
248
510
|
if (!existsSync(pm)) {
|
|
249
|
-
|
|
511
|
+
console.error('infer-provision: pnpm not found — deps NOT installed for ' + profileDir
|
|
512
|
+
+ '; run manually: (cd ' + profileDir + ' && pnpm install)')
|
|
250
513
|
return false
|
|
251
514
|
}
|
|
252
515
|
log(' installing deps with', pm, '…')
|
|
253
|
-
|
|
516
|
+
try {
|
|
517
|
+
execSync('"' + pm + '" install', { cwd: profileDir, stdio: 'inherit', timeout: INSTALL_TIMEOUT_MS })
|
|
518
|
+
} catch (err) {
|
|
519
|
+
const e = (err !== null && typeof err === 'object') ? err : {}
|
|
520
|
+
const why = e.code === 'ETIMEDOUT'
|
|
521
|
+
? 'timed out after ' + String(INSTALL_TIMEOUT_MS / 1000) + 's'
|
|
522
|
+
: 'failed (status ' + String(e.status ?? e.signal ?? e.code ?? 'unknown') + ') — output above'
|
|
523
|
+
console.error('infer-provision: pnpm install ' + why + ' in ' + profileDir
|
|
524
|
+
+ '; fix the failure and re-run, or install manually: (cd ' + profileDir + ' && pnpm install)')
|
|
525
|
+
return false
|
|
526
|
+
}
|
|
254
527
|
return true
|
|
255
528
|
}
|
|
256
529
|
|
|
530
|
+
// RENDER+INSTALL, LOCKED (F6): the one door both commands use — the
|
|
531
|
+
// lock covers the render AND the install (an install reading
|
|
532
|
+
// package.json mid-render is the same race). The install's boolean
|
|
533
|
+
// is honored HERE, and the die sits AFTER the finally on purpose:
|
|
534
|
+
// process.exit skips finally blocks, so a die inside the try would
|
|
535
|
+
// leak the lock forever.
|
|
536
|
+
function renderInstall(name) {
|
|
537
|
+
const dst = join(DSH_HOME, 'profiles', name)
|
|
538
|
+
mkdirSync(dst, { recursive: true })
|
|
539
|
+
acquireLock(dst)
|
|
540
|
+
let dir = dst
|
|
541
|
+
let ok = false
|
|
542
|
+
try {
|
|
543
|
+
dir = renderProfile(name)
|
|
544
|
+
ok = installDeps(dir)
|
|
545
|
+
} finally {
|
|
546
|
+
releaseLock(dst)
|
|
547
|
+
}
|
|
548
|
+
if (!ok) die(`profile '${name}' rendered but deps NOT installed — boot would fail at bundle resolution; run manually: (cd ${dir} && pnpm install)`)
|
|
549
|
+
return dir
|
|
550
|
+
}
|
|
551
|
+
|
|
257
552
|
// ── main ────────────────────────────────────────────────────────────────
|
|
258
553
|
switch (cmd) {
|
|
259
554
|
case 'all': {
|
|
@@ -264,8 +559,8 @@ switch (cmd) {
|
|
|
264
559
|
// command renders + installs every profile (the gateway health
|
|
265
560
|
// check died with the gateway — there is no second process to
|
|
266
561
|
// probe anymore).
|
|
267
|
-
for (const name of ['
|
|
268
|
-
|
|
562
|
+
for (const name of ['melite-test', 'headless', 'estate']) {
|
|
563
|
+
renderInstall(name)
|
|
269
564
|
}
|
|
270
565
|
log('boot with:')
|
|
271
566
|
log(' LANES BOOT ONLY THROUGH THE PROVISION (the lane law, li-bu):')
|
|
@@ -273,15 +568,14 @@ switch (cmd) {
|
|
|
273
568
|
log(' (this command installs the profiles; the sibling provisioner owns the')
|
|
274
569
|
log(' launch — binary pin, version floor, home, and unit. The shapes below')
|
|
275
570
|
log(' are what the door runs UNDER THE HOOD, never a hand-run shortcut:)')
|
|
276
|
-
log(' dsh --profile
|
|
571
|
+
log(' dsh --profile melite-test # the studio GUI + TEST lane (one process, no :33081)')
|
|
277
572
|
log(' dsh --profile headless "<task>" # one-shot agent with card tools')
|
|
278
573
|
log(' dsh --profile estate # the SDK job server (estate row)')
|
|
279
|
-
log(' dsh --profile melite-test # the TEST lane (one process, no :33081)')
|
|
280
574
|
break
|
|
281
575
|
}
|
|
282
576
|
case 'profile': {
|
|
283
|
-
|
|
284
|
-
log('boot with: dsh --profile ' + PROFILE + (PROFILE === '
|
|
577
|
+
renderInstall(PROFILE)
|
|
578
|
+
log('boot with: dsh --profile ' + PROFILE + (PROFILE === 'melite-test' ? '' : ' "<task>"'))
|
|
285
579
|
break
|
|
286
580
|
}
|
|
287
581
|
// the `preset` arm died with installPreset (undefined since the
|
package/cordis.patch.yml
CHANGED
|
@@ -23,8 +23,13 @@
|
|
|
23
23
|
# owning DSH cross-host).
|
|
24
24
|
#
|
|
25
25
|
# All rows resolve BY PACKAGE NAME through this bundle's own link:
|
|
26
|
-
# dependencies
|
|
27
|
-
#
|
|
26
|
+
# dependencies. THE RENDER PATH BAKES ABSOLUTE PATHS (the bin's
|
|
27
|
+
# renderProfile resolves every link: against the checkout at render
|
|
28
|
+
|
|
29
|
+
# renamed checkout needs a re-provision, and a stale profile
|
|
30
|
+
# carries dead links pnpm warns about. Rows THIS patch serves to an
|
|
31
|
+
# installed profile (via the bundle's own dependency table) stay
|
|
32
|
+
# name-resolved.
|
|
28
33
|
#
|
|
29
34
|
|
|
30
35
|
# is the ONLY app — there is no second process, no :33081, no
|
|
@@ -85,16 +90,21 @@
|
|
|
85
90
|
# ride the packages' dsh.client declarations (src/client ->
|
|
86
91
|
# lib/client.js).
|
|
87
92
|
#
|
|
88
|
-
# card-studio-anny: NO row here
|
|
89
|
-
#
|
|
90
|
-
#
|
|
93
|
+
# card-studio-anny: NO row here, and nothing to mount either —
|
|
94
|
+
# @melite/3d-anny's studio is RETIRED (stroke 89): the package
|
|
95
|
+
# ships no client face and no cordis.patch.yml, and its bundle
|
|
96
|
+
# left the web bases (bin/infer-provision.mjs, the stroke-89
|
|
97
|
+
# note). A row here would mount a package that composes
|
|
98
|
+
# nothing. Should a self-registering anny studio ever return, it
|
|
99
|
+
# comes back as a mounted bundle in the bases, never as a twin
|
|
91
100
|
|
|
92
|
-
# profile (their self-patch never
|
|
101
|
+
# for studios not bundled by the profile (their self-patch never
|
|
102
|
+
# applies there).
|
|
93
103
|
|
|
94
104
|
# card-studio-h3-timeline: NO row here — @melite/video-h3-timeline
|
|
95
105
|
# self-registers (its own cordis.patch.yml); a twin row here
|
|
96
106
|
# double-mounted the package in every profile bundling both
|
|
97
|
-
|
|
107
|
+
|
|
98
108
|
|
|
99
109
|
# card-studio-ideogram4 RETIRED with its card (the form-seams
|
|
100
110
|
|
|
@@ -111,7 +121,7 @@
|
|
|
111
121
|
# card-studio-motion: NO row here — @melite/motion-motion
|
|
112
122
|
# self-registers (its own cordis.patch.yml); a twin row here
|
|
113
123
|
# double-mounted the package in every profile bundling both
|
|
114
|
-
|
|
124
|
+
|
|
115
125
|
|
|
116
126
|
|
|
117
127
|
|